From 3c3b39bbe1069b0a80736d6d5371f674e0f529df Mon Sep 17 00:00:00 2001 From: Reiase Date: Fri, 2 Oct 2026 07:52:06 +0800 Subject: [PATCH 1/4] drop pvisor --- .cargo/config.toml | 3 - .github/actions/setup-build-env/action.yml | 2 +- .github/workflows/ci.yml | 178 +- .github/workflows/nightly.yml | 2 +- .github/workflows/pvisor-benchmark.yml | 101 - .gitignore | 6 - AGENTS.md | 6 +- Cargo.lock | 1820 +-------- Cargo.toml | 29 +- NOTICE | 22 - README.md | 44 +- benchmark/README.md | 20 +- benchmark/gateway/README.md | 2 +- benchmark/pvisor/README.md | 41 - benchmark/pvisor/bench.py | 545 --- benchmark/pvisor/run.sh | 7 - benchmark/pvisor/test_bench.py | 59 - crates/persisting-agentctl/Cargo.toml | 4 - crates/persisting-agentctl/README.md | 11 +- crates/persisting-agentctl/src/lib.rs | 6 +- crates/persisting-agentctl/src/process.rs | 192 - crates/persisting-gateway/README.md | 11 +- crates/persisting-overlay-core/Cargo.toml | 17 - crates/persisting-overlay-core/src/core.rs | 1410 ------- crates/persisting-overlay-core/src/lib.rs | 9 - crates/persisting-overlay-core/src/sys.rs | 281 -- crates/persisting-overlayfs/Cargo.toml | 38 - crates/persisting-overlayfs/README.md | 54 - crates/persisting-overlayfs/src/fs.rs | 1192 ------ crates/persisting-overlayfs/src/jj_backend.rs | 300 -- .../persisting-overlayfs/src/jj_disabled.rs | 64 - crates/persisting-overlayfs/src/lib.rs | 401 -- crates/persisting-overlayfs/src/main.rs | 240 -- crates/persisting-overlaynet/Cargo.toml | 2 +- crates/persisting-overlaynet/README.md | 9 +- crates/persisting-pchronicle-cli/README.md | 11 +- crates/persisting-pchronicle/Cargo.toml | 4 +- crates/persisting-pchronicle/README.md | 11 +- crates/persisting-ppilot/Cargo.toml | 40 - crates/persisting-ppilot/README.md | 71 - crates/persisting-ppilot/src/agentctl.rs | 3 - crates/persisting-ppilot/src/batch.rs | 480 --- crates/persisting-ppilot/src/bin/ppilot.rs | 176 - crates/persisting-ppilot/src/blocks.rs | 123 - crates/persisting-ppilot/src/check.rs | 308 -- crates/persisting-ppilot/src/checkpoint.rs | 350 -- crates/persisting-ppilot/src/cli.rs | 565 --- crates/persisting-ppilot/src/coordination.rs | 1203 ------ crates/persisting-ppilot/src/digest.rs | 24 - crates/persisting-ppilot/src/dist.rs | 310 -- crates/persisting-ppilot/src/driver.rs | 446 --- crates/persisting-ppilot/src/executor.rs | 871 ----- crates/persisting-ppilot/src/future.rs | 106 - crates/persisting-ppilot/src/job_control.rs | 374 -- crates/persisting-ppilot/src/lib.rs | 80 - crates/persisting-ppilot/src/observe.rs | 513 --- crates/persisting-ppilot/src/plan.rs | 236 -- crates/persisting-ppilot/src/pulsing_ext.rs | 55 - crates/persisting-ppilot/src/python_env.rs | 95 - crates/persisting-ppilot/src/result_cache.rs | 99 - crates/persisting-ppilot/src/runtime.rs | 561 --- .../persisting-ppilot/src/runtime_bridge.rs | 372 -- crates/persisting-ppilot/src/scheduler.rs | 458 --- crates/persisting-ppilot/src/sink.rs | 291 -- crates/persisting-ppilot/src/sink_traj.rs | 147 - crates/persisting-ppilot/src/sink_writer.rs | 226 -- crates/persisting-ppilot/src/skip.rs | 82 - crates/persisting-ppilot/src/supervisor.rs | 410 -- crates/persisting-ppilot/src/task.rs | 493 --- crates/persisting-ppilot/src/worker.rs | 416 --- crates/persisting-ppilot/tests/common/mod.rs | 33 - .../tests/integration_batch_scenarios.rs | 123 - .../tests/integration_local.rs | 416 --- crates/persisting-pvisor/Cargo.toml | 53 - crates/persisting-pvisor/README.md | 71 - .../macos-hypervisor.entitlements | 8 - crates/persisting-pvisor/src/agentctl.rs | 955 ----- crates/persisting-pvisor/src/artifact.rs | 76 - crates/persisting-pvisor/src/bin/pvisor.rs | 14 - crates/persisting-pvisor/src/bundle.rs | 719 ---- crates/persisting-pvisor/src/checkpoint.rs | 318 -- crates/persisting-pvisor/src/cli/env.rs | 689 ---- crates/persisting-pvisor/src/cli/mod.rs | 291 -- crates/persisting-pvisor/src/cli/product.rs | 422 --- crates/persisting-pvisor/src/cli/replay.rs | 743 ---- crates/persisting-pvisor/src/cli/run.rs | 3187 ---------------- crates/persisting-pvisor/src/cli/runtime.rs | 435 --- .../persisting-pvisor/src/cli/trajectory.rs | 326 -- crates/persisting-pvisor/src/config.rs | 627 ---- crates/persisting-pvisor/src/container.rs | 799 ---- crates/persisting-pvisor/src/control.rs | 7 - crates/persisting-pvisor/src/delegated.rs | 123 - crates/persisting-pvisor/src/event.rs | 212 -- crates/persisting-pvisor/src/executor.rs | 134 - crates/persisting-pvisor/src/firmware.rs | 254 -- crates/persisting-pvisor/src/lib.rs | 78 - crates/persisting-pvisor/src/oci.rs | 964 ----- crates/persisting-pvisor/src/process.rs | 1370 ------- crates/persisting-pvisor/src/pvisor.rs | 1506 -------- .../persisting-pvisor/src/runtime/attempt.rs | 1348 ------- .../persisting-pvisor/src/runtime/implant.rs | 60 - crates/persisting-pvisor/src/runtime/mod.rs | 32 - .../persisting-pvisor/src/runtime/overlay.rs | 3299 ----------------- .../persisting-pvisor/src/runtime/registry.rs | 723 ---- .../src/runtime/supervisor.rs | 590 --- crates/persisting-pvisor/src/sandbox.rs | 1277 ------- crates/persisting-pvisor/src/supervisor.rs | 297 -- crates/persisting-pvisor/src/util.rs | 108 - crates/persisting-pvisor/src/vm.rs | 1118 ------ .../tests/agentctl_contract.rs | 181 - .../tests/fixtures/bundles/v1-minimal.json | 37 - crates/persisting-pvisor/tests/macos_safe.rs | 258 -- .../tests/overlayfs_performance.rs | 116 - .../tests/ppilot_supervisor.rs | 61 - .../persisting-pvisor/tests/rootless_local.rs | 838 ----- .../persisting-pvisor/tests/run_config_cli.rs | 537 --- crates/persisting-replay/Cargo.toml | 24 - .../assets/mini_swe_agent_runner.py | 249 -- .../assets/pi_agent_runner.mjs | 305 -- .../assets/swe_agent_runner.py | 174 - .../src/adapter/claude_code.rs | 2592 ------------- .../persisting-replay/src/adapter/generic.rs | 2480 ------------- .../src/adapter/mini_swe_agent.rs | 326 -- crates/persisting-replay/src/adapter/mod.rs | 637 ---- .../src/adapter/openhands.rs | 856 ----- .../persisting-replay/src/adapter/pi_agent.rs | 350 -- .../persisting-replay/src/adapter/runtime.rs | 595 --- .../src/adapter/swe_agent.rs | 215 -- crates/persisting-replay/src/claude_bridge.rs | 1927 ---------- crates/persisting-replay/src/claude_resume.rs | 949 ----- crates/persisting-replay/src/codex_bridge.rs | 761 ---- crates/persisting-replay/src/comparison.rs | 305 -- crates/persisting-replay/src/config.rs | 472 --- crates/persisting-replay/src/engine.rs | 870 ----- crates/persisting-replay/src/error.rs | 203 - crates/persisting-replay/src/io.rs | 107 - crates/persisting-replay/src/journal.rs | 225 -- crates/persisting-replay/src/lib.rs | 30 - crates/persisting-replay/src/model.rs | 411 -- .../persisting-replay/src/opencode_bridge.rs | 804 ---- crates/persisting-replay/src/process.rs | 680 ---- .../tests/fixtures/claude_bash_one_step.jsonl | 4 - .../tests/fixtures/fake_agent_runtime.py | 241 -- .../tests/fixtures/replay-managed-smoke.toml | 15 - .../tests/fixtures/replay-smoke.toml | 9 - .../tests/replay_contract.rs | 597 --- .../legacy/en/ppilot/design/orchestration.md | 65 - docs/archive/legacy/en/ppilot/get-started.md | 54 - .../legacy/en/ppilot/guides/orchestrate.md | 59 - docs/archive/legacy/en/ppilot/index.md | 24 - .../archive/legacy/en/ppilot/reference/cli.md | 80 - .../legacy/zh/ppilot/design/orchestration.md | 55 - docs/archive/legacy/zh/ppilot/get-started.md | 53 - .../legacy/zh/ppilot/guides/orchestrate.md | 53 - docs/archive/legacy/zh/ppilot/index.md | 22 - .../archive/legacy/zh/ppilot/reference/cli.md | 73 - docs/overrides/home.html | 8 +- docs/src/en/guides/using-persisting.md | 15 +- docs/src/en/index.md | 2 +- docs/src/en/installation.md | 106 +- docs/src/en/overview.md | 36 +- .../pchronicle/design/trajectory-storage.md | 4 +- docs/src/en/pchronicle/get-started.md | 1 - docs/src/en/pchronicle/guides/index.md | 1 - .../src/en/pchronicle/guides/serve-gateway.md | 4 +- docs/src/en/pchronicle/index.md | 2 +- docs/src/en/pchronicle/reference/agenticmd.md | 1 - docs/src/en/ppilot/design/orchestration.md | 65 - docs/src/en/ppilot/get-started.md | 54 - docs/src/en/ppilot/guides/orchestrate.md | 59 - docs/src/en/ppilot/index.md | 24 - docs/src/en/ppilot/reference/cli.md | 80 - docs/src/en/project/engineering.md | 14 +- docs/src/en/project/examples.md | 57 +- docs/src/en/project/index.md | 2 +- docs/src/en/project/releasing.md | 14 +- docs/src/en/pvisor/concepts/agentvisor.md | 348 -- .../concepts/capabilities-and-evidence.md | 34 - docs/src/en/pvisor/concepts/index.md | 28 - docs/src/en/pvisor/concepts/run-model.md | 54 - docs/src/en/pvisor/design/cli.md | 101 - docs/src/en/pvisor/design/gateway.md | 661 ---- docs/src/en/pvisor/design/index.md | 15 - docs/src/en/pvisor/design/isolation.md | 833 ----- docs/src/en/pvisor/design/overlaynet.md | 262 -- docs/src/en/pvisor/get-started.md | 96 - docs/src/en/pvisor/guides/capture.md | 53 - docs/src/en/pvisor/guides/execution.md | 200 - docs/src/en/pvisor/guides/index.md | 20 - docs/src/en/pvisor/guides/network.md | 218 -- docs/src/en/pvisor/guides/review-apply.md | 102 - docs/src/en/pvisor/guides/sandbox-replay.md | 204 - docs/src/en/pvisor/guides/troubleshooting.md | 81 - docs/src/en/pvisor/index.md | 78 - docs/src/en/pvisor/reference/cases.md | 53 - docs/src/en/pvisor/reference/cli.md | 453 --- docs/src/en/pvisor/reference/index.md | 8 - docs/src/en/rfcs/0002-events-format.md | 4 +- docs/src/en/roadmap.md | 7 +- docs/src/en/system-design/architecture.md | 15 +- .../src/en/system-design/design-principles.md | 3 + docs/src/en/system-design/index.md | 15 +- docs/src/en/system-design/local-to-fleet.md | 7 +- .../src/en/system-design/security-evidence.md | 9 +- docs/src/en/why-persisting.md | 12 +- docs/src/zh/guides/using-persisting.md | 11 +- docs/src/zh/index.md | 2 +- docs/src/zh/installation.md | 99 +- docs/src/zh/overview.md | 30 +- .../pchronicle/design/trajectory-storage.md | 4 +- docs/src/zh/pchronicle/get-started.md | 1 - docs/src/zh/pchronicle/guides/index.md | 1 - .../src/zh/pchronicle/guides/serve-gateway.md | 4 +- docs/src/zh/pchronicle/index.md | 2 +- docs/src/zh/pchronicle/reference/agenticmd.md | 1 - docs/src/zh/ppilot/design/orchestration.md | 55 - docs/src/zh/ppilot/get-started.md | 53 - docs/src/zh/ppilot/guides/orchestrate.md | 53 - docs/src/zh/ppilot/index.md | 22 - docs/src/zh/ppilot/reference/cli.md | 73 - docs/src/zh/project/engineering.md | 12 +- docs/src/zh/project/examples.md | 58 +- docs/src/zh/project/index.md | 2 +- docs/src/zh/project/releasing.md | 15 +- docs/src/zh/pvisor/concepts/agentvisor.md | 294 -- .../concepts/capabilities-and-evidence.md | 28 - docs/src/zh/pvisor/concepts/index.md | 22 - docs/src/zh/pvisor/concepts/run-model.md | 45 - docs/src/zh/pvisor/design/cli.md | 87 - docs/src/zh/pvisor/design/gateway.md | 549 --- docs/src/zh/pvisor/design/index.md | 13 - docs/src/zh/pvisor/design/isolation.md | 735 ---- docs/src/zh/pvisor/design/overlaynet.md | 226 -- docs/src/zh/pvisor/get-started.md | 88 - docs/src/zh/pvisor/guides/capture.md | 42 - docs/src/zh/pvisor/guides/execution.md | 200 - docs/src/zh/pvisor/guides/index.md | 17 - docs/src/zh/pvisor/guides/network.md | 200 - docs/src/zh/pvisor/guides/review-apply.md | 93 - docs/src/zh/pvisor/guides/sandbox-replay.md | 842 ----- docs/src/zh/pvisor/guides/troubleshooting.md | 69 - docs/src/zh/pvisor/index.md | 71 - docs/src/zh/pvisor/reference/cases.md | 1394 ------- docs/src/zh/pvisor/reference/cli.md | 399 -- docs/src/zh/pvisor/reference/index.md | 7 - docs/src/zh/rfcs/0002-events-format.md | 4 +- docs/src/zh/roadmap.md | 6 +- docs/src/zh/system-design/architecture.md | 14 +- .../src/zh/system-design/design-principles.md | 2 + docs/src/zh/system-design/index.md | 12 +- docs/src/zh/system-design/local-to-fleet.md | 6 +- .../src/zh/system-design/security-evidence.md | 6 +- docs/src/zh/why-persisting.md | 7 +- docs/zensical.toml | 2 - examples/README.md | 29 +- examples/ppilot/01-run/README.md | 19 - examples/ppilot/01-run/plan.py | 8 - examples/ppilot/01-run/run.sh | 17 - examples/ppilot/02-produce/README.md | 19 - examples/ppilot/02-produce/production.py | 7 - examples/ppilot/02-produce/run.sh | 17 - examples/ppilot/README.md | 26 - .../pvisor/01-filesystem-isolation/README.md | 22 - .../pvisor/01-filesystem-isolation/run.sh | 33 - .../pvisor/01-filesystem-isolation/test.sh | 26 - .../pvisor/02-changeset-management/README.md | 20 - .../pvisor/02-changeset-management/run.sh | 41 - .../pvisor/02-changeset-management/test.sh | 20 - .../pvisor/03-network-isolation/README.md | 55 - examples/pvisor/03-network-isolation/run.sh | 69 - examples/pvisor/03-network-isolation/test.sh | 34 - .../pvisor/04-gateway-llm-control/README.md | 22 - .../pvisor/04-gateway-llm-control/agent.py | 19 - .../configs/allowlist.toml | 19 - .../configs/deepseek.toml | 15 - .../configs/multi-provider.toml | 28 - .../dialogue_fixture.py | 11 - .../pvisor/04-gateway-llm-control/mock_llm.py | 37 - examples/pvisor/04-gateway-llm-control/run.sh | 40 - .../pvisor/04-gateway-llm-control/run.toml | 17 - .../pvisor/04-gateway-llm-control/test.sh | 25 - examples/pvisor/README.md | 38 - examples/pvisor/common.sh | 72 - examples/pvisor/run.sh | 42 - examples/pvisor/test.sh | 42 - justfile | 231 +- pyproject.toml | 4 +- scripts/check-docs.py | 4 +- scripts/install-nightly.sh | 2 +- scripts/libkrun-linux-cc | 34 - scripts/packaging/stage_wheel_binaries.py | 156 +- scripts/packaging/verify_wheel.py | 50 +- scripts/run-pvisor-cases.py | 660 ---- tests/regression/README.md | 2 +- tests/regression/gateway-echo/README.md | 2 +- tests/regression/gateway-fuzz/README.md | 2 +- tests/test_release_packaging.py | 100 +- vendor/fuser/.cargo-ok | 1 - vendor/fuser/.cargo_vcs_info.json | 6 - vendor/fuser/.cirrus.yml | 14 - vendor/fuser/.dockerignore | 10 - vendor/fuser/.github/workflows/ci.yml | 97 - vendor/fuser/.gitignore | 4 - vendor/fuser/CHANGELOG.md | 181 - vendor/fuser/Cargo.toml | 141 - vendor/fuser/Cargo.toml.orig | 71 - vendor/fuser/LICENSE.md | 23 - vendor/fuser/Makefile | 51 - vendor/fuser/README.md | 137 - vendor/fuser/build.rs | 58 - vendor/fuser/deny.toml | 142 - vendor/fuser/examples/hello.rs | 149 - vendor/fuser/examples/ioctl.rs | 209 -- vendor/fuser/examples/null.rs | 12 - vendor/fuser/examples/simple.rs | 2062 ----------- vendor/fuser/mount_tests.Dockerfile | 13 - vendor/fuser/mount_tests.sh | 160 - vendor/fuser/osx_mount_tests.sh | 45 - vendor/fuser/pjdfs.Dockerfile | 24 - vendor/fuser/pjdfs.sh | 34 - vendor/fuser/rust-toolchain | 1 - vendor/fuser/rustfmt.toml | 1 - vendor/fuser/simplefs_tests.sh | 46 - vendor/fuser/src/channel.rs | 78 - vendor/fuser/src/lib.rs | 1056 ------ vendor/fuser/src/ll/argument.rs | 163 - vendor/fuser/src/ll/fuse_abi.rs | 1145 ------ vendor/fuser/src/ll/mod.rs | 297 -- vendor/fuser/src/ll/notify.rs | 215 -- vendor/fuser/src/ll/reply.rs | 882 ----- vendor/fuser/src/ll/request.rs | 2337 ------------ vendor/fuser/src/mnt/fuse2.rs | 107 - vendor/fuser/src/mnt/fuse2_sys.rs | 121 - vendor/fuser/src/mnt/fuse3.rs | 62 - vendor/fuser/src/mnt/fuse3_sys.rs | 31 - vendor/fuser/src/mnt/fuse_pure.rs | 520 --- vendor/fuser/src/mnt/mod.rs | 187 - vendor/fuser/src/mnt/mount_options.rs | 241 -- vendor/fuser/src/notify.rs | 121 - vendor/fuser/src/reply.rs | 1094 ------ vendor/fuser/src/request.rs | 673 ---- vendor/fuser/src/session.rs | 304 -- vendor/fuser/tests/integration_tests.rs | 28 - vendor/fuser/xfstests.Dockerfile | 23 - vendor/fuser/xfstests.sh | 142 - vendor/krun-devices/.cargo-ok | 1 - vendor/krun-devices/.cargo_vcs_info.json | 6 - vendor/krun-devices/Cargo.lock | 1168 ------ vendor/krun-devices/Cargo.toml | 182 - vendor/krun-devices/Cargo.toml.orig | 58 - vendor/krun-devices/src/bus.rs | 287 -- vendor/krun-devices/src/fdt/aarch64.rs | 444 --- vendor/krun-devices/src/fdt/mod.rs | 12 - vendor/krun-devices/src/fdt/riscv64.rs | 286 -- .../krun-devices/src/legacy/aarch64/gpio.rs | 258 -- vendor/krun-devices/src/legacy/aarch64/mod.rs | 2 - .../krun-devices/src/legacy/aarch64/serial.rs | 429 --- vendor/krun-devices/src/legacy/aia.rs | 22 - vendor/krun-devices/src/legacy/gic.rs | 16 - vendor/krun-devices/src/legacy/gicv3.rs | 524 --- vendor/krun-devices/src/legacy/hvfgicv3.rs | 183 - vendor/krun-devices/src/legacy/i8042.rs | 488 --- vendor/krun-devices/src/legacy/ioapic.rs | 472 --- vendor/krun-devices/src/legacy/irqchip.rs | 227 -- vendor/krun-devices/src/legacy/kvmaia.rs | 171 - vendor/krun-devices/src/legacy/kvmgicv2.rs | 146 - vendor/krun-devices/src/legacy/kvmgicv3.rs | 146 - vendor/krun-devices/src/legacy/kvmioapic.rs | 69 - vendor/krun-devices/src/legacy/mod.rs | 92 - vendor/krun-devices/src/legacy/riscv64/mod.rs | 4 - .../krun-devices/src/legacy/riscv64/serial.rs | 320 -- vendor/krun-devices/src/legacy/rtc_pl031.rs | 241 -- vendor/krun-devices/src/legacy/vcpu.rs | 264 -- vendor/krun-devices/src/legacy/x86_64/cmos.rs | 79 - vendor/krun-devices/src/legacy/x86_64/mod.rs | 2 - .../krun-devices/src/legacy/x86_64/serial.rs | 563 --- vendor/krun-devices/src/lib.rs | 58 - .../krun-devices/src/virtio/balloon/device.rs | 195 - .../src/virtio/balloon/event_handler.rs | 189 - vendor/krun-devices/src/virtio/balloon/mod.rs | 30 - vendor/krun-devices/src/virtio/bindings.rs | 219 -- .../krun-devices/src/virtio/block/device.rs | 444 --- .../src/virtio/block/event_handler.rs | 0 vendor/krun-devices/src/virtio/block/mod.rs | 84 - .../src/virtio/block/test_utils.rs | 41 - .../krun-devices/src/virtio/block/worker.rs | 308 -- .../src/virtio/console/console_control.rs | 152 - .../krun-devices/src/virtio/console/device.rs | 369 -- .../src/virtio/console/event_handler.rs | 169 - vendor/krun-devices/src/virtio/console/mod.rs | 49 - .../krun-devices/src/virtio/console/port.rs | 202 - .../src/virtio/console/port_io.rs | 334 -- .../src/virtio/console/port_queue_mapping.rs | 74 - .../src/virtio/console/process_rx.rs | 116 - .../src/virtio/console/process_tx.rs | 107 - .../src/virtio/descriptor_utils.rs | 984 ----- vendor/krun-devices/src/virtio/device.rs | 184 - vendor/krun-devices/src/virtio/file_traits.rs | 482 --- .../krun-devices/src/virtio/fs/augment_fs.rs | 745 ---- vendor/krun-devices/src/virtio/fs/device.rs | 257 -- .../krun-devices/src/virtio/fs/filesystem.rs | 1206 ------ vendor/krun-devices/src/virtio/fs/fuse.rs | 1379 ------- .../krun-devices/src/virtio/fs/inode_alloc.rs | 28 - .../src/virtio/fs/linux/fs_utils.rs | 9 - .../krun-devices/src/virtio/fs/linux/mod.rs | 2 - .../src/virtio/fs/linux/passthrough.rs | 2304 ------------ .../src/virtio/fs/macos/fs_utils.rs | 11 - .../krun-devices/src/virtio/fs/macos/mod.rs | 2 - .../src/virtio/fs/macos/passthrough.rs | 2763 -------------- vendor/krun-devices/src/virtio/fs/mod.rs | 83 - vendor/krun-devices/src/virtio/fs/multikey.rs | 274 -- vendor/krun-devices/src/virtio/fs/null_fs.rs | 50 - vendor/krun-devices/src/virtio/fs/overlay.rs | 896 ----- .../krun-devices/src/virtio/fs/read_only.rs | 501 --- vendor/krun-devices/src/virtio/fs/server.rs | 1667 --------- .../src/virtio/fs/virtual_entry.rs | 56 - vendor/krun-devices/src/virtio/fs/worker.rs | 286 -- vendor/krun-devices/src/virtio/gpu/device.rs | 237 -- vendor/krun-devices/src/virtio/gpu/display.rs | 102 - vendor/krun-devices/src/virtio/gpu/edid.rs | 316 -- vendor/krun-devices/src/virtio/gpu/mod.rs | 61 - .../krun-devices/src/virtio/gpu/protocol.rs | 944 ----- .../krun-devices/src/virtio/gpu/virtio_gpu.rs | 1055 ------ vendor/krun-devices/src/virtio/gpu/worker.rs | 456 --- .../krun-devices/src/virtio/input/device.rs | 263 -- vendor/krun-devices/src/virtio/input/mod.rs | 64 - .../src/virtio/input/passthrough.rs | 211 -- .../krun-devices/src/virtio/input/worker.rs | 280 -- vendor/krun-devices/src/virtio/linux_errno.rs | 219 -- vendor/krun-devices/src/virtio/mmio.rs | 1057 ------ vendor/krun-devices/src/virtio/mod.rs | 114 - vendor/krun-devices/src/virtio/net/backend.rs | 54 - vendor/krun-devices/src/virtio/net/device.rs | 210 -- vendor/krun-devices/src/virtio/net/mod.rs | 38 - vendor/krun-devices/src/virtio/net/tap.rs | 128 - .../krun-devices/src/virtio/net/unixgram.rs | 189 - .../krun-devices/src/virtio/net/unixstream.rs | 222 -- vendor/krun-devices/src/virtio/net/worker.rs | 475 --- vendor/krun-devices/src/virtio/queue.rs | 1143 ------ vendor/krun-devices/src/virtio/rng/device.rs | 158 - .../src/virtio/rng/event_handler.rs | 80 - vendor/krun-devices/src/virtio/rng/mod.rs | 27 - .../src/virtio/snd/audio_backends.rs | 81 - .../src/virtio/snd/audio_backends/pipewire.rs | 650 ---- .../snd/audio_backends/pipewire/test_utils.rs | 134 - vendor/krun-devices/src/virtio/snd/device.rs | 149 - vendor/krun-devices/src/virtio/snd/mod.rs | 312 -- vendor/krun-devices/src/virtio/snd/stream.rs | 624 ---- .../src/virtio/snd/virtio_sound.rs | 512 --- vendor/krun-devices/src/virtio/snd/worker.rs | 648 ---- .../krun-devices/src/virtio/vsock/device.rs | 279 -- .../src/virtio/vsock/event_handler.rs | 160 - vendor/krun-devices/src/virtio/vsock/mod.rs | 179 - vendor/krun-devices/src/virtio/vsock/muxer.rs | 713 ---- .../src/virtio/vsock/muxer_rxq.rs | 227 -- .../src/virtio/vsock/muxer_thread.rs | 206 - .../krun-devices/src/virtio/vsock/packet.rs | 787 ---- vendor/krun-devices/src/virtio/vsock/proxy.rs | 98 - .../krun-devices/src/virtio/vsock/reaper.rs | 74 - .../krun-devices/src/virtio/vsock/timesync.rs | 88 - .../src/virtio/vsock/tsi_dgram.rs | 493 --- .../src/virtio/vsock/tsi_stream.rs | 901 ----- vendor/krun-devices/src/virtio/vsock/unix.rs | 721 ---- vendor/krun-init-blob/.cargo-ok | 1 - vendor/krun-init-blob/.cargo_vcs_info.json | 6 - vendor/krun-init-blob/Cargo.lock | 7 - vendor/krun-init-blob/Cargo.toml | 30 - vendor/krun-init-blob/Cargo.toml.orig | 11 - vendor/krun-init-blob/build.rs | 68 - vendor/krun-init-blob/init/dhcp.c | 634 ---- vendor/krun-init-blob/init/dhcp.h | 61 - vendor/krun-init-blob/init/init.c | 1578 -------- vendor/krun-init-blob/init/jsmn.h | 494 --- vendor/krun-init-blob/src/lib.rs | 1 - vendor/krun-vmm/.cargo-ok | 1 - vendor/krun-vmm/.cargo_vcs_info.json | 6 - vendor/krun-vmm/Cargo.lock | 1558 -------- vendor/krun-vmm/Cargo.toml | 201 - vendor/krun-vmm/Cargo.toml.orig | 64 - vendor/krun-vmm/build.rs | 18 - vendor/krun-vmm/edk2/KRUN_EFI.silent.fd | Bin 2097152 -> 0 bytes vendor/krun-vmm/edk2/License.txt | 51 - vendor/krun-vmm/edk2/Sources.txt | 1 - vendor/krun-vmm/src/builder.rs | 2494 ------------- .../krun-vmm/src/device_manager/hvf/mmio.rs | 564 --- vendor/krun-vmm/src/device_manager/hvf/mod.rs | 1 - .../krun-vmm/src/device_manager/kvm/mmio.rs | 578 --- vendor/krun-vmm/src/device_manager/kvm/mod.rs | 1 - vendor/krun-vmm/src/device_manager/legacy.rs | 184 - vendor/krun-vmm/src/device_manager/mod.rs | 22 - vendor/krun-vmm/src/device_manager/shm.rs | 91 - vendor/krun-vmm/src/lib.rs | 442 --- vendor/krun-vmm/src/linux/mod.rs | 4 - .../src/linux/tee/amdsnp/launch/error.rs | 481 --- .../src/linux/tee/amdsnp/launch/firmware.rs | 29 - .../linux/tee/amdsnp/launch/linux/ioctl.rs | 150 - .../src/linux/tee/amdsnp/launch/linux/mod.rs | 6 - .../src/linux/tee/amdsnp/launch/linux/snp.rs | 162 - .../src/linux/tee/amdsnp/launch/mod.rs | 381 -- .../tee/amdsnp/launch/util/impl_const_id.rs | 48 - .../src/linux/tee/amdsnp/launch/util/mod.rs | 3 - vendor/krun-vmm/src/linux/tee/amdsnp/mod.rs | 399 -- vendor/krun-vmm/src/linux/tee/inteltdx.rs | 55 - vendor/krun-vmm/src/linux/tee/mod.rs | 5 - vendor/krun-vmm/src/linux/vstate.rs | 2043 ---------- vendor/krun-vmm/src/macos/mod.rs | 1 - vendor/krun-vmm/src/macos/vstate.rs | 731 ---- vendor/krun-vmm/src/resources.rs | 517 --- vendor/krun-vmm/src/signal_handler.rs | 136 - vendor/krun-vmm/src/terminal.rs | 27 - vendor/krun-vmm/src/vmm_config/block.rs | 76 - .../src/vmm_config/external_kernel.rs | 31 - vendor/krun-vmm/src/vmm_config/firmware.rs | 9 - vendor/krun-vmm/src/vmm_config/fs.rs | 21 - .../krun-vmm/src/vmm_config/instance_info.rs | 15 - .../krun-vmm/src/vmm_config/kernel_bundle.rs | 65 - .../krun-vmm/src/vmm_config/kernel_cmdline.rs | 38 - .../krun-vmm/src/vmm_config/machine_config.rs | 110 - vendor/krun-vmm/src/vmm_config/mod.rs | 35 - vendor/krun-vmm/src/vmm_config/net.rs | 71 - vendor/krun-vmm/src/vmm_config/vsock.rs | 161 - vendor/krun-vmm/src/worker.rs | 157 - vendor/libkrun/.cargo-ok | 1 - vendor/libkrun/.cargo_vcs_info.json | 6 - vendor/libkrun/Cargo.lock | 1921 ---------- vendor/libkrun/Cargo.toml | 164 - vendor/libkrun/Cargo.toml.orig | 54 - vendor/libkrun/build.rs | 13 - vendor/libkrun/src/lib.rs | 3274 ---------------- 529 files changed, 408 insertions(+), 147848 deletions(-) delete mode 100644 .github/workflows/pvisor-benchmark.yml delete mode 100644 benchmark/pvisor/README.md delete mode 100755 benchmark/pvisor/bench.py delete mode 100755 benchmark/pvisor/run.sh delete mode 100755 benchmark/pvisor/test_bench.py delete mode 100644 crates/persisting-agentctl/src/process.rs delete mode 100644 crates/persisting-overlay-core/Cargo.toml delete mode 100644 crates/persisting-overlay-core/src/core.rs delete mode 100644 crates/persisting-overlay-core/src/lib.rs delete mode 100644 crates/persisting-overlay-core/src/sys.rs delete mode 100644 crates/persisting-overlayfs/Cargo.toml delete mode 100644 crates/persisting-overlayfs/README.md delete mode 100644 crates/persisting-overlayfs/src/fs.rs delete mode 100644 crates/persisting-overlayfs/src/jj_backend.rs delete mode 100644 crates/persisting-overlayfs/src/jj_disabled.rs delete mode 100644 crates/persisting-overlayfs/src/lib.rs delete mode 100644 crates/persisting-overlayfs/src/main.rs delete mode 100644 crates/persisting-ppilot/Cargo.toml delete mode 100644 crates/persisting-ppilot/README.md delete mode 100644 crates/persisting-ppilot/src/agentctl.rs delete mode 100644 crates/persisting-ppilot/src/batch.rs delete mode 100644 crates/persisting-ppilot/src/bin/ppilot.rs delete mode 100644 crates/persisting-ppilot/src/blocks.rs delete mode 100644 crates/persisting-ppilot/src/check.rs delete mode 100644 crates/persisting-ppilot/src/checkpoint.rs delete mode 100644 crates/persisting-ppilot/src/cli.rs delete mode 100644 crates/persisting-ppilot/src/coordination.rs delete mode 100644 crates/persisting-ppilot/src/digest.rs delete mode 100644 crates/persisting-ppilot/src/dist.rs delete mode 100644 crates/persisting-ppilot/src/driver.rs delete mode 100644 crates/persisting-ppilot/src/executor.rs delete mode 100644 crates/persisting-ppilot/src/future.rs delete mode 100644 crates/persisting-ppilot/src/job_control.rs delete mode 100644 crates/persisting-ppilot/src/lib.rs delete mode 100644 crates/persisting-ppilot/src/observe.rs delete mode 100644 crates/persisting-ppilot/src/plan.rs delete mode 100644 crates/persisting-ppilot/src/pulsing_ext.rs delete mode 100644 crates/persisting-ppilot/src/python_env.rs delete mode 100644 crates/persisting-ppilot/src/result_cache.rs delete mode 100644 crates/persisting-ppilot/src/runtime.rs delete mode 100644 crates/persisting-ppilot/src/runtime_bridge.rs delete mode 100644 crates/persisting-ppilot/src/scheduler.rs delete mode 100644 crates/persisting-ppilot/src/sink.rs delete mode 100644 crates/persisting-ppilot/src/sink_traj.rs delete mode 100644 crates/persisting-ppilot/src/sink_writer.rs delete mode 100644 crates/persisting-ppilot/src/skip.rs delete mode 100644 crates/persisting-ppilot/src/supervisor.rs delete mode 100644 crates/persisting-ppilot/src/task.rs delete mode 100644 crates/persisting-ppilot/src/worker.rs delete mode 100644 crates/persisting-ppilot/tests/common/mod.rs delete mode 100644 crates/persisting-ppilot/tests/integration_batch_scenarios.rs delete mode 100644 crates/persisting-ppilot/tests/integration_local.rs delete mode 100644 crates/persisting-pvisor/Cargo.toml delete mode 100644 crates/persisting-pvisor/README.md delete mode 100644 crates/persisting-pvisor/macos-hypervisor.entitlements delete mode 100644 crates/persisting-pvisor/src/agentctl.rs delete mode 100644 crates/persisting-pvisor/src/artifact.rs delete mode 100644 crates/persisting-pvisor/src/bin/pvisor.rs delete mode 100644 crates/persisting-pvisor/src/bundle.rs delete mode 100644 crates/persisting-pvisor/src/checkpoint.rs delete mode 100644 crates/persisting-pvisor/src/cli/env.rs delete mode 100644 crates/persisting-pvisor/src/cli/mod.rs delete mode 100644 crates/persisting-pvisor/src/cli/product.rs delete mode 100644 crates/persisting-pvisor/src/cli/replay.rs delete mode 100644 crates/persisting-pvisor/src/cli/run.rs delete mode 100644 crates/persisting-pvisor/src/cli/runtime.rs delete mode 100644 crates/persisting-pvisor/src/cli/trajectory.rs delete mode 100644 crates/persisting-pvisor/src/config.rs delete mode 100644 crates/persisting-pvisor/src/container.rs delete mode 100644 crates/persisting-pvisor/src/control.rs delete mode 100644 crates/persisting-pvisor/src/delegated.rs delete mode 100644 crates/persisting-pvisor/src/event.rs delete mode 100644 crates/persisting-pvisor/src/executor.rs delete mode 100644 crates/persisting-pvisor/src/firmware.rs delete mode 100644 crates/persisting-pvisor/src/lib.rs delete mode 100644 crates/persisting-pvisor/src/oci.rs delete mode 100644 crates/persisting-pvisor/src/process.rs delete mode 100644 crates/persisting-pvisor/src/pvisor.rs delete mode 100644 crates/persisting-pvisor/src/runtime/attempt.rs delete mode 100644 crates/persisting-pvisor/src/runtime/implant.rs delete mode 100644 crates/persisting-pvisor/src/runtime/mod.rs delete mode 100644 crates/persisting-pvisor/src/runtime/overlay.rs delete mode 100644 crates/persisting-pvisor/src/runtime/registry.rs delete mode 100644 crates/persisting-pvisor/src/runtime/supervisor.rs delete mode 100644 crates/persisting-pvisor/src/sandbox.rs delete mode 100644 crates/persisting-pvisor/src/supervisor.rs delete mode 100644 crates/persisting-pvisor/src/util.rs delete mode 100644 crates/persisting-pvisor/src/vm.rs delete mode 100644 crates/persisting-pvisor/tests/agentctl_contract.rs delete mode 100644 crates/persisting-pvisor/tests/fixtures/bundles/v1-minimal.json delete mode 100644 crates/persisting-pvisor/tests/macos_safe.rs delete mode 100644 crates/persisting-pvisor/tests/overlayfs_performance.rs delete mode 100644 crates/persisting-pvisor/tests/ppilot_supervisor.rs delete mode 100644 crates/persisting-pvisor/tests/rootless_local.rs delete mode 100644 crates/persisting-pvisor/tests/run_config_cli.rs delete mode 100644 crates/persisting-replay/Cargo.toml delete mode 100644 crates/persisting-replay/assets/mini_swe_agent_runner.py delete mode 100644 crates/persisting-replay/assets/pi_agent_runner.mjs delete mode 100644 crates/persisting-replay/assets/swe_agent_runner.py delete mode 100644 crates/persisting-replay/src/adapter/claude_code.rs delete mode 100644 crates/persisting-replay/src/adapter/generic.rs delete mode 100644 crates/persisting-replay/src/adapter/mini_swe_agent.rs delete mode 100644 crates/persisting-replay/src/adapter/mod.rs delete mode 100644 crates/persisting-replay/src/adapter/openhands.rs delete mode 100644 crates/persisting-replay/src/adapter/pi_agent.rs delete mode 100644 crates/persisting-replay/src/adapter/runtime.rs delete mode 100644 crates/persisting-replay/src/adapter/swe_agent.rs delete mode 100644 crates/persisting-replay/src/claude_bridge.rs delete mode 100644 crates/persisting-replay/src/claude_resume.rs delete mode 100644 crates/persisting-replay/src/codex_bridge.rs delete mode 100644 crates/persisting-replay/src/comparison.rs delete mode 100644 crates/persisting-replay/src/config.rs delete mode 100644 crates/persisting-replay/src/engine.rs delete mode 100644 crates/persisting-replay/src/error.rs delete mode 100644 crates/persisting-replay/src/io.rs delete mode 100644 crates/persisting-replay/src/journal.rs delete mode 100644 crates/persisting-replay/src/lib.rs delete mode 100644 crates/persisting-replay/src/model.rs delete mode 100644 crates/persisting-replay/src/opencode_bridge.rs delete mode 100644 crates/persisting-replay/src/process.rs delete mode 100644 crates/persisting-replay/tests/fixtures/claude_bash_one_step.jsonl delete mode 100644 crates/persisting-replay/tests/fixtures/fake_agent_runtime.py delete mode 100644 crates/persisting-replay/tests/fixtures/replay-managed-smoke.toml delete mode 100644 crates/persisting-replay/tests/fixtures/replay-smoke.toml delete mode 100644 crates/persisting-replay/tests/replay_contract.rs delete mode 100644 docs/archive/legacy/en/ppilot/design/orchestration.md delete mode 100644 docs/archive/legacy/en/ppilot/get-started.md delete mode 100644 docs/archive/legacy/en/ppilot/guides/orchestrate.md delete mode 100644 docs/archive/legacy/en/ppilot/index.md delete mode 100644 docs/archive/legacy/en/ppilot/reference/cli.md delete mode 100644 docs/archive/legacy/zh/ppilot/design/orchestration.md delete mode 100644 docs/archive/legacy/zh/ppilot/get-started.md delete mode 100644 docs/archive/legacy/zh/ppilot/guides/orchestrate.md delete mode 100644 docs/archive/legacy/zh/ppilot/index.md delete mode 100644 docs/archive/legacy/zh/ppilot/reference/cli.md delete mode 100644 docs/src/en/ppilot/design/orchestration.md delete mode 100644 docs/src/en/ppilot/get-started.md delete mode 100644 docs/src/en/ppilot/guides/orchestrate.md delete mode 100644 docs/src/en/ppilot/index.md delete mode 100644 docs/src/en/ppilot/reference/cli.md delete mode 100644 docs/src/en/pvisor/concepts/agentvisor.md delete mode 100644 docs/src/en/pvisor/concepts/capabilities-and-evidence.md delete mode 100644 docs/src/en/pvisor/concepts/index.md delete mode 100644 docs/src/en/pvisor/concepts/run-model.md delete mode 100644 docs/src/en/pvisor/design/cli.md delete mode 100644 docs/src/en/pvisor/design/gateway.md delete mode 100644 docs/src/en/pvisor/design/index.md delete mode 100644 docs/src/en/pvisor/design/isolation.md delete mode 100644 docs/src/en/pvisor/design/overlaynet.md delete mode 100644 docs/src/en/pvisor/get-started.md delete mode 100644 docs/src/en/pvisor/guides/capture.md delete mode 100644 docs/src/en/pvisor/guides/execution.md delete mode 100644 docs/src/en/pvisor/guides/index.md delete mode 100644 docs/src/en/pvisor/guides/network.md delete mode 100644 docs/src/en/pvisor/guides/review-apply.md delete mode 100644 docs/src/en/pvisor/guides/sandbox-replay.md delete mode 100644 docs/src/en/pvisor/guides/troubleshooting.md delete mode 100644 docs/src/en/pvisor/index.md delete mode 100644 docs/src/en/pvisor/reference/cases.md delete mode 100644 docs/src/en/pvisor/reference/cli.md delete mode 100644 docs/src/en/pvisor/reference/index.md delete mode 100644 docs/src/zh/ppilot/design/orchestration.md delete mode 100644 docs/src/zh/ppilot/get-started.md delete mode 100644 docs/src/zh/ppilot/guides/orchestrate.md delete mode 100644 docs/src/zh/ppilot/index.md delete mode 100644 docs/src/zh/ppilot/reference/cli.md delete mode 100644 docs/src/zh/pvisor/concepts/agentvisor.md delete mode 100644 docs/src/zh/pvisor/concepts/capabilities-and-evidence.md delete mode 100644 docs/src/zh/pvisor/concepts/index.md delete mode 100644 docs/src/zh/pvisor/concepts/run-model.md delete mode 100644 docs/src/zh/pvisor/design/cli.md delete mode 100644 docs/src/zh/pvisor/design/gateway.md delete mode 100644 docs/src/zh/pvisor/design/index.md delete mode 100644 docs/src/zh/pvisor/design/isolation.md delete mode 100644 docs/src/zh/pvisor/design/overlaynet.md delete mode 100644 docs/src/zh/pvisor/get-started.md delete mode 100644 docs/src/zh/pvisor/guides/capture.md delete mode 100644 docs/src/zh/pvisor/guides/execution.md delete mode 100644 docs/src/zh/pvisor/guides/index.md delete mode 100644 docs/src/zh/pvisor/guides/network.md delete mode 100644 docs/src/zh/pvisor/guides/review-apply.md delete mode 100644 docs/src/zh/pvisor/guides/sandbox-replay.md delete mode 100644 docs/src/zh/pvisor/guides/troubleshooting.md delete mode 100644 docs/src/zh/pvisor/index.md delete mode 100644 docs/src/zh/pvisor/reference/cases.md delete mode 100644 docs/src/zh/pvisor/reference/cli.md delete mode 100644 docs/src/zh/pvisor/reference/index.md delete mode 100644 examples/ppilot/01-run/README.md delete mode 100644 examples/ppilot/01-run/plan.py delete mode 100755 examples/ppilot/01-run/run.sh delete mode 100644 examples/ppilot/02-produce/README.md delete mode 100644 examples/ppilot/02-produce/production.py delete mode 100755 examples/ppilot/02-produce/run.sh delete mode 100644 examples/ppilot/README.md delete mode 100644 examples/pvisor/01-filesystem-isolation/README.md delete mode 100755 examples/pvisor/01-filesystem-isolation/run.sh delete mode 100755 examples/pvisor/01-filesystem-isolation/test.sh delete mode 100644 examples/pvisor/02-changeset-management/README.md delete mode 100755 examples/pvisor/02-changeset-management/run.sh delete mode 100755 examples/pvisor/02-changeset-management/test.sh delete mode 100644 examples/pvisor/03-network-isolation/README.md delete mode 100755 examples/pvisor/03-network-isolation/run.sh delete mode 100755 examples/pvisor/03-network-isolation/test.sh delete mode 100644 examples/pvisor/04-gateway-llm-control/README.md delete mode 100755 examples/pvisor/04-gateway-llm-control/agent.py delete mode 100644 examples/pvisor/04-gateway-llm-control/configs/allowlist.toml delete mode 100644 examples/pvisor/04-gateway-llm-control/configs/deepseek.toml delete mode 100644 examples/pvisor/04-gateway-llm-control/configs/multi-provider.toml delete mode 100644 examples/pvisor/04-gateway-llm-control/dialogue_fixture.py delete mode 100755 examples/pvisor/04-gateway-llm-control/mock_llm.py delete mode 100755 examples/pvisor/04-gateway-llm-control/run.sh delete mode 100644 examples/pvisor/04-gateway-llm-control/run.toml delete mode 100755 examples/pvisor/04-gateway-llm-control/test.sh delete mode 100644 examples/pvisor/README.md delete mode 100644 examples/pvisor/common.sh delete mode 100755 examples/pvisor/run.sh delete mode 100755 examples/pvisor/test.sh delete mode 100755 scripts/libkrun-linux-cc delete mode 100755 scripts/run-pvisor-cases.py delete mode 100644 vendor/fuser/.cargo-ok delete mode 100644 vendor/fuser/.cargo_vcs_info.json delete mode 100644 vendor/fuser/.cirrus.yml delete mode 100644 vendor/fuser/.dockerignore delete mode 100644 vendor/fuser/.github/workflows/ci.yml delete mode 100644 vendor/fuser/.gitignore delete mode 100644 vendor/fuser/CHANGELOG.md delete mode 100644 vendor/fuser/Cargo.toml delete mode 100644 vendor/fuser/Cargo.toml.orig delete mode 100644 vendor/fuser/LICENSE.md delete mode 100644 vendor/fuser/Makefile delete mode 100644 vendor/fuser/README.md delete mode 100644 vendor/fuser/build.rs delete mode 100644 vendor/fuser/deny.toml delete mode 100644 vendor/fuser/examples/hello.rs delete mode 100644 vendor/fuser/examples/ioctl.rs delete mode 100644 vendor/fuser/examples/null.rs delete mode 100644 vendor/fuser/examples/simple.rs delete mode 100644 vendor/fuser/mount_tests.Dockerfile delete mode 100755 vendor/fuser/mount_tests.sh delete mode 100755 vendor/fuser/osx_mount_tests.sh delete mode 100644 vendor/fuser/pjdfs.Dockerfile delete mode 100755 vendor/fuser/pjdfs.sh delete mode 100644 vendor/fuser/rust-toolchain delete mode 100644 vendor/fuser/rustfmt.toml delete mode 100755 vendor/fuser/simplefs_tests.sh delete mode 100644 vendor/fuser/src/channel.rs delete mode 100644 vendor/fuser/src/lib.rs delete mode 100644 vendor/fuser/src/ll/argument.rs delete mode 100644 vendor/fuser/src/ll/fuse_abi.rs delete mode 100644 vendor/fuser/src/ll/mod.rs delete mode 100644 vendor/fuser/src/ll/notify.rs delete mode 100644 vendor/fuser/src/ll/reply.rs delete mode 100644 vendor/fuser/src/ll/request.rs delete mode 100644 vendor/fuser/src/mnt/fuse2.rs delete mode 100644 vendor/fuser/src/mnt/fuse2_sys.rs delete mode 100644 vendor/fuser/src/mnt/fuse3.rs delete mode 100644 vendor/fuser/src/mnt/fuse3_sys.rs delete mode 100644 vendor/fuser/src/mnt/fuse_pure.rs delete mode 100644 vendor/fuser/src/mnt/mod.rs delete mode 100644 vendor/fuser/src/mnt/mount_options.rs delete mode 100644 vendor/fuser/src/notify.rs delete mode 100644 vendor/fuser/src/reply.rs delete mode 100644 vendor/fuser/src/request.rs delete mode 100644 vendor/fuser/src/session.rs delete mode 100644 vendor/fuser/tests/integration_tests.rs delete mode 100644 vendor/fuser/xfstests.Dockerfile delete mode 100755 vendor/fuser/xfstests.sh delete mode 100644 vendor/krun-devices/.cargo-ok delete mode 100644 vendor/krun-devices/.cargo_vcs_info.json delete mode 100644 vendor/krun-devices/Cargo.lock delete mode 100644 vendor/krun-devices/Cargo.toml delete mode 100644 vendor/krun-devices/Cargo.toml.orig delete mode 100644 vendor/krun-devices/src/bus.rs delete mode 100644 vendor/krun-devices/src/fdt/aarch64.rs delete mode 100644 vendor/krun-devices/src/fdt/mod.rs delete mode 100644 vendor/krun-devices/src/fdt/riscv64.rs delete mode 100644 vendor/krun-devices/src/legacy/aarch64/gpio.rs delete mode 100644 vendor/krun-devices/src/legacy/aarch64/mod.rs delete mode 100644 vendor/krun-devices/src/legacy/aarch64/serial.rs delete mode 100644 vendor/krun-devices/src/legacy/aia.rs delete mode 100644 vendor/krun-devices/src/legacy/gic.rs delete mode 100644 vendor/krun-devices/src/legacy/gicv3.rs delete mode 100644 vendor/krun-devices/src/legacy/hvfgicv3.rs delete mode 100644 vendor/krun-devices/src/legacy/i8042.rs delete mode 100644 vendor/krun-devices/src/legacy/ioapic.rs delete mode 100644 vendor/krun-devices/src/legacy/irqchip.rs delete mode 100644 vendor/krun-devices/src/legacy/kvmaia.rs delete mode 100644 vendor/krun-devices/src/legacy/kvmgicv2.rs delete mode 100644 vendor/krun-devices/src/legacy/kvmgicv3.rs delete mode 100644 vendor/krun-devices/src/legacy/kvmioapic.rs delete mode 100644 vendor/krun-devices/src/legacy/mod.rs delete mode 100644 vendor/krun-devices/src/legacy/riscv64/mod.rs delete mode 100644 vendor/krun-devices/src/legacy/riscv64/serial.rs delete mode 100644 vendor/krun-devices/src/legacy/rtc_pl031.rs delete mode 100644 vendor/krun-devices/src/legacy/vcpu.rs delete mode 100644 vendor/krun-devices/src/legacy/x86_64/cmos.rs delete mode 100644 vendor/krun-devices/src/legacy/x86_64/mod.rs delete mode 100644 vendor/krun-devices/src/legacy/x86_64/serial.rs delete mode 100644 vendor/krun-devices/src/lib.rs delete mode 100644 vendor/krun-devices/src/virtio/balloon/device.rs delete mode 100644 vendor/krun-devices/src/virtio/balloon/event_handler.rs delete mode 100644 vendor/krun-devices/src/virtio/balloon/mod.rs delete mode 100644 vendor/krun-devices/src/virtio/bindings.rs delete mode 100644 vendor/krun-devices/src/virtio/block/device.rs delete mode 100644 vendor/krun-devices/src/virtio/block/event_handler.rs delete mode 100644 vendor/krun-devices/src/virtio/block/mod.rs delete mode 100644 vendor/krun-devices/src/virtio/block/test_utils.rs delete mode 100644 vendor/krun-devices/src/virtio/block/worker.rs delete mode 100644 vendor/krun-devices/src/virtio/console/console_control.rs delete mode 100644 vendor/krun-devices/src/virtio/console/device.rs delete mode 100644 vendor/krun-devices/src/virtio/console/event_handler.rs delete mode 100644 vendor/krun-devices/src/virtio/console/mod.rs delete mode 100644 vendor/krun-devices/src/virtio/console/port.rs delete mode 100644 vendor/krun-devices/src/virtio/console/port_io.rs delete mode 100644 vendor/krun-devices/src/virtio/console/port_queue_mapping.rs delete mode 100644 vendor/krun-devices/src/virtio/console/process_rx.rs delete mode 100644 vendor/krun-devices/src/virtio/console/process_tx.rs delete mode 100644 vendor/krun-devices/src/virtio/descriptor_utils.rs delete mode 100644 vendor/krun-devices/src/virtio/device.rs delete mode 100644 vendor/krun-devices/src/virtio/file_traits.rs delete mode 100644 vendor/krun-devices/src/virtio/fs/augment_fs.rs delete mode 100644 vendor/krun-devices/src/virtio/fs/device.rs delete mode 100644 vendor/krun-devices/src/virtio/fs/filesystem.rs delete mode 100644 vendor/krun-devices/src/virtio/fs/fuse.rs delete mode 100644 vendor/krun-devices/src/virtio/fs/inode_alloc.rs delete mode 100644 vendor/krun-devices/src/virtio/fs/linux/fs_utils.rs delete mode 100644 vendor/krun-devices/src/virtio/fs/linux/mod.rs delete mode 100644 vendor/krun-devices/src/virtio/fs/linux/passthrough.rs delete mode 100644 vendor/krun-devices/src/virtio/fs/macos/fs_utils.rs delete mode 100644 vendor/krun-devices/src/virtio/fs/macos/mod.rs delete mode 100644 vendor/krun-devices/src/virtio/fs/macos/passthrough.rs delete mode 100644 vendor/krun-devices/src/virtio/fs/mod.rs delete mode 100644 vendor/krun-devices/src/virtio/fs/multikey.rs delete mode 100644 vendor/krun-devices/src/virtio/fs/null_fs.rs delete mode 100644 vendor/krun-devices/src/virtio/fs/overlay.rs delete mode 100644 vendor/krun-devices/src/virtio/fs/read_only.rs delete mode 100644 vendor/krun-devices/src/virtio/fs/server.rs delete mode 100644 vendor/krun-devices/src/virtio/fs/virtual_entry.rs delete mode 100644 vendor/krun-devices/src/virtio/fs/worker.rs delete mode 100644 vendor/krun-devices/src/virtio/gpu/device.rs delete mode 100644 vendor/krun-devices/src/virtio/gpu/display.rs delete mode 100644 vendor/krun-devices/src/virtio/gpu/edid.rs delete mode 100644 vendor/krun-devices/src/virtio/gpu/mod.rs delete mode 100644 vendor/krun-devices/src/virtio/gpu/protocol.rs delete mode 100644 vendor/krun-devices/src/virtio/gpu/virtio_gpu.rs delete mode 100644 vendor/krun-devices/src/virtio/gpu/worker.rs delete mode 100644 vendor/krun-devices/src/virtio/input/device.rs delete mode 100644 vendor/krun-devices/src/virtio/input/mod.rs delete mode 100644 vendor/krun-devices/src/virtio/input/passthrough.rs delete mode 100644 vendor/krun-devices/src/virtio/input/worker.rs delete mode 100644 vendor/krun-devices/src/virtio/linux_errno.rs delete mode 100644 vendor/krun-devices/src/virtio/mmio.rs delete mode 100644 vendor/krun-devices/src/virtio/mod.rs delete mode 100644 vendor/krun-devices/src/virtio/net/backend.rs delete mode 100644 vendor/krun-devices/src/virtio/net/device.rs delete mode 100644 vendor/krun-devices/src/virtio/net/mod.rs delete mode 100644 vendor/krun-devices/src/virtio/net/tap.rs delete mode 100644 vendor/krun-devices/src/virtio/net/unixgram.rs delete mode 100644 vendor/krun-devices/src/virtio/net/unixstream.rs delete mode 100644 vendor/krun-devices/src/virtio/net/worker.rs delete mode 100644 vendor/krun-devices/src/virtio/queue.rs delete mode 100644 vendor/krun-devices/src/virtio/rng/device.rs delete mode 100644 vendor/krun-devices/src/virtio/rng/event_handler.rs delete mode 100644 vendor/krun-devices/src/virtio/rng/mod.rs delete mode 100644 vendor/krun-devices/src/virtio/snd/audio_backends.rs delete mode 100644 vendor/krun-devices/src/virtio/snd/audio_backends/pipewire.rs delete mode 100644 vendor/krun-devices/src/virtio/snd/audio_backends/pipewire/test_utils.rs delete mode 100644 vendor/krun-devices/src/virtio/snd/device.rs delete mode 100644 vendor/krun-devices/src/virtio/snd/mod.rs delete mode 100644 vendor/krun-devices/src/virtio/snd/stream.rs delete mode 100644 vendor/krun-devices/src/virtio/snd/virtio_sound.rs delete mode 100644 vendor/krun-devices/src/virtio/snd/worker.rs delete mode 100644 vendor/krun-devices/src/virtio/vsock/device.rs delete mode 100644 vendor/krun-devices/src/virtio/vsock/event_handler.rs delete mode 100644 vendor/krun-devices/src/virtio/vsock/mod.rs delete mode 100644 vendor/krun-devices/src/virtio/vsock/muxer.rs delete mode 100644 vendor/krun-devices/src/virtio/vsock/muxer_rxq.rs delete mode 100644 vendor/krun-devices/src/virtio/vsock/muxer_thread.rs delete mode 100644 vendor/krun-devices/src/virtio/vsock/packet.rs delete mode 100644 vendor/krun-devices/src/virtio/vsock/proxy.rs delete mode 100644 vendor/krun-devices/src/virtio/vsock/reaper.rs delete mode 100644 vendor/krun-devices/src/virtio/vsock/timesync.rs delete mode 100644 vendor/krun-devices/src/virtio/vsock/tsi_dgram.rs delete mode 100644 vendor/krun-devices/src/virtio/vsock/tsi_stream.rs delete mode 100644 vendor/krun-devices/src/virtio/vsock/unix.rs delete mode 100644 vendor/krun-init-blob/.cargo-ok delete mode 100644 vendor/krun-init-blob/.cargo_vcs_info.json delete mode 100644 vendor/krun-init-blob/Cargo.lock delete mode 100644 vendor/krun-init-blob/Cargo.toml delete mode 100644 vendor/krun-init-blob/Cargo.toml.orig delete mode 100644 vendor/krun-init-blob/build.rs delete mode 100644 vendor/krun-init-blob/init/dhcp.c delete mode 100644 vendor/krun-init-blob/init/dhcp.h delete mode 100644 vendor/krun-init-blob/init/init.c delete mode 100644 vendor/krun-init-blob/init/jsmn.h delete mode 100644 vendor/krun-init-blob/src/lib.rs delete mode 100644 vendor/krun-vmm/.cargo-ok delete mode 100644 vendor/krun-vmm/.cargo_vcs_info.json delete mode 100644 vendor/krun-vmm/Cargo.lock delete mode 100644 vendor/krun-vmm/Cargo.toml delete mode 100644 vendor/krun-vmm/Cargo.toml.orig delete mode 100644 vendor/krun-vmm/build.rs delete mode 100644 vendor/krun-vmm/edk2/KRUN_EFI.silent.fd delete mode 100644 vendor/krun-vmm/edk2/License.txt delete mode 100644 vendor/krun-vmm/edk2/Sources.txt delete mode 100644 vendor/krun-vmm/src/builder.rs delete mode 100644 vendor/krun-vmm/src/device_manager/hvf/mmio.rs delete mode 100644 vendor/krun-vmm/src/device_manager/hvf/mod.rs delete mode 100644 vendor/krun-vmm/src/device_manager/kvm/mmio.rs delete mode 100644 vendor/krun-vmm/src/device_manager/kvm/mod.rs delete mode 100644 vendor/krun-vmm/src/device_manager/legacy.rs delete mode 100644 vendor/krun-vmm/src/device_manager/mod.rs delete mode 100644 vendor/krun-vmm/src/device_manager/shm.rs delete mode 100644 vendor/krun-vmm/src/lib.rs delete mode 100644 vendor/krun-vmm/src/linux/mod.rs delete mode 100644 vendor/krun-vmm/src/linux/tee/amdsnp/launch/error.rs delete mode 100644 vendor/krun-vmm/src/linux/tee/amdsnp/launch/firmware.rs delete mode 100644 vendor/krun-vmm/src/linux/tee/amdsnp/launch/linux/ioctl.rs delete mode 100644 vendor/krun-vmm/src/linux/tee/amdsnp/launch/linux/mod.rs delete mode 100644 vendor/krun-vmm/src/linux/tee/amdsnp/launch/linux/snp.rs delete mode 100644 vendor/krun-vmm/src/linux/tee/amdsnp/launch/mod.rs delete mode 100644 vendor/krun-vmm/src/linux/tee/amdsnp/launch/util/impl_const_id.rs delete mode 100644 vendor/krun-vmm/src/linux/tee/amdsnp/launch/util/mod.rs delete mode 100644 vendor/krun-vmm/src/linux/tee/amdsnp/mod.rs delete mode 100644 vendor/krun-vmm/src/linux/tee/inteltdx.rs delete mode 100644 vendor/krun-vmm/src/linux/tee/mod.rs delete mode 100644 vendor/krun-vmm/src/linux/vstate.rs delete mode 100644 vendor/krun-vmm/src/macos/mod.rs delete mode 100644 vendor/krun-vmm/src/macos/vstate.rs delete mode 100644 vendor/krun-vmm/src/resources.rs delete mode 100644 vendor/krun-vmm/src/signal_handler.rs delete mode 100644 vendor/krun-vmm/src/terminal.rs delete mode 100644 vendor/krun-vmm/src/vmm_config/block.rs delete mode 100644 vendor/krun-vmm/src/vmm_config/external_kernel.rs delete mode 100644 vendor/krun-vmm/src/vmm_config/firmware.rs delete mode 100644 vendor/krun-vmm/src/vmm_config/fs.rs delete mode 100644 vendor/krun-vmm/src/vmm_config/instance_info.rs delete mode 100644 vendor/krun-vmm/src/vmm_config/kernel_bundle.rs delete mode 100644 vendor/krun-vmm/src/vmm_config/kernel_cmdline.rs delete mode 100644 vendor/krun-vmm/src/vmm_config/machine_config.rs delete mode 100644 vendor/krun-vmm/src/vmm_config/mod.rs delete mode 100644 vendor/krun-vmm/src/vmm_config/net.rs delete mode 100644 vendor/krun-vmm/src/vmm_config/vsock.rs delete mode 100644 vendor/krun-vmm/src/worker.rs delete mode 100644 vendor/libkrun/.cargo-ok delete mode 100644 vendor/libkrun/.cargo_vcs_info.json delete mode 100644 vendor/libkrun/Cargo.lock delete mode 100644 vendor/libkrun/Cargo.toml delete mode 100644 vendor/libkrun/Cargo.toml.orig delete mode 100644 vendor/libkrun/build.rs delete mode 100644 vendor/libkrun/src/lib.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index 6a9915927..847ca2408 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,7 +1,4 @@ [env] -# libkrun embeds a Linux guest init. Native Linux builds use cc; macOS builds -# cross-compile that init with Zig instead of accidentally using Apple clang. -CC_LINUX = { value = "scripts/libkrun-linux-cc", relative = true } # Debug DataFusion/Lance projection uses more than the default 2MiB test-thread # stack, which overflowed canonical event import on Linux CI. RUST_MIN_STACK = "8388608" diff --git a/.github/actions/setup-build-env/action.yml b/.github/actions/setup-build-env/action.yml index e5f7210dc..c34b987e2 100644 --- a/.github/actions/setup-build-env/action.yml +++ b/.github/actions/setup-build-env/action.yml @@ -24,7 +24,7 @@ inputs: install-zig: description: "Install Zig for the macOS-to-Linux guest init cross-build" required: false - default: "true" + default: "false" runs: using: "composite" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e937b4f5..5845d3c88 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -127,15 +127,8 @@ jobs: packages: >- persisting-agentctl persisting-events - persisting-overlay-core - persisting-overlayfs persisting-overlaynet - persisting-replay event_control: true - prepare_pvisor: false - prepare_runtime: false - smoke_pvisor: false - smoke_ppilot: false smoke_pchronicle: false install_zig: false - platform: Linux @@ -143,32 +136,6 @@ jobs: shard: gateway packages: persisting-gateway event_control: false - prepare_pvisor: false - prepare_runtime: false - smoke_pvisor: false - smoke_ppilot: false - smoke_pchronicle: false - install_zig: false - - platform: Linux - os: ubuntu-latest - shard: pvisor - packages: persisting-pvisor - event_control: false - prepare_pvisor: true - prepare_runtime: false - smoke_pvisor: true - smoke_ppilot: false - smoke_pchronicle: false - install_zig: false - - platform: Linux - os: ubuntu-latest - shard: ppilot - packages: persisting-ppilot - event_control: false - prepare_pvisor: false - prepare_runtime: true - smoke_pvisor: false - smoke_ppilot: true smoke_pchronicle: false install_zig: false - platform: Linux @@ -178,43 +145,19 @@ jobs: persisting-pchronicle persisting-pchronicle-cli event_control: false - prepare_pvisor: false - prepare_runtime: false - smoke_pvisor: false - smoke_ppilot: false smoke_pchronicle: true install_zig: false - platform: macOS os: macos-latest - shard: agent-runtime + shard: capture packages: >- persisting-agentctl persisting-events persisting-gateway - persisting-overlay-core - persisting-overlayfs persisting-overlaynet - persisting-ppilot - persisting-pvisor - persisting-replay event_control: false - prepare_pvisor: false - prepare_runtime: true - smoke_pvisor: true - smoke_ppilot: true smoke_pchronicle: false - install_zig: true - - platform: macOS - os: macos-latest - shard: pvisor - packages: persisting-pvisor - event_control: false - prepare_pvisor: true - prepare_runtime: false - smoke_pvisor: true - smoke_ppilot: false - smoke_pchronicle: false - install_zig: true + install_zig: false - platform: macOS os: macos-latest shard: pchronicle @@ -222,10 +165,6 @@ jobs: persisting-pchronicle persisting-pchronicle-cli event_control: false - prepare_pvisor: false - prepare_runtime: false - smoke_pvisor: false - smoke_ppilot: false smoke_pchronicle: true install_zig: false steps: @@ -241,39 +180,13 @@ jobs: with: shared-key: ci-rust-${{ matrix.shard }} - # macOS needs the Hypervisor entitlement on the product binary. Running - # the same recipe on Linux also keeps the smoke-test setup identical. - - name: Build pVisor product binary - if: matrix.prepare_pvisor - run: just pvisor debug - - # pPilot's integration tests resolve the real pVisor and pChronicle - # executables from PATH. Build them once in this shard before nextest. - - name: Build Agent runtime component set - if: matrix.prepare_runtime - run: | - just build-agent-runtime debug - echo "${GITHUB_WORKSPACE}/target/debug" >> "${GITHUB_PATH}" - - name: Run ${{ matrix.shard }} tests - env: - # GitHub-hosted Linux runners may disable unprivileged user - # namespaces. The dedicated isolation job remains strict. - PERSISTING_TEST_ALLOW_NO_USERNS: "1" run: just ci-nextest ${{ matrix.packages }} - name: Test shared event control contract if: matrix.event_control run: just test-events-control - - name: Smoke pVisor CLI - if: matrix.smoke_pvisor - run: just smoke-pvisor-cli - - - name: Smoke pPilot CLI - if: matrix.smoke_ppilot - run: just smoke-ppilot-cli - - name: Smoke pChronicle CLI if: matrix.smoke_pchronicle run: just smoke-pchronicle-cli @@ -321,47 +234,6 @@ jobs: - name: Build and test Web crate run: just test-pchronicle-web - pvisor-isolation-regression: - name: pVisor Isolation Regression / Linux - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - - - name: Setup Build Environment - uses: ./.github/actions/setup-build-env - with: - install-zig: "false" - components: "" - - - uses: Swatinem/rust-cache@v2 - with: - shared-key: ci-pvisor-isolation - - - name: Enable rootless isolation primitives - shell: bash - run: | - sudo apt-get update -qq - sudo apt-get install -y -qq --no-install-recommends fuse3 jq util-linux - if sysctl kernel.unprivileged_userns_clone >/dev/null 2>&1; then - sudo sysctl -w kernel.unprivileged_userns_clone=1 - fi - if sysctl kernel.apparmor_restrict_unprivileged_userns >/dev/null 2>&1; then - sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 - fi - test -c /dev/fuse - unshare --user --map-root-user --mount --net /bin/true - - - name: Run strict pVisor isolation regressions - env: - PERSISTING_RUN_HOME: ${{ runner.temp }}/pvisor-isolation-runs - run: just test-pvisor-isolation - - - name: Run pVisor filesystem examples - env: - WORK_ROOT: ${{ runner.temp }}/pvisor-filesystem-examples - run: just examples-pvisor-filesystem - s3-contract: name: pChronicle S3 Contract / MinIO runs-on: ubuntu-latest @@ -441,44 +313,6 @@ jobs: - name: Pytest run: just test-py - pvisor-cases: - name: pVisor case checklist - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup Build Environment - uses: ./.github/actions/setup-build-env - with: - install-nextest: "false" - install-zig: "false" - components: "" - - - uses: Swatinem/rust-cache@v2 - with: - shared-key: ci-pvisor-cases - - - name: Run documented cases - run: | - just cases pvisor \ - --case A01,A02,A03,A04,A05,A06 \ - --case B01,B02,B03,B04 \ - --case C02,C03,C04 \ - --case D01,D03 \ - --case F01,F02,F03 \ - --case G01,G02,G03,G06 \ - --case H01,H02 \ - --case I01,I02,I03 \ - --case J03 - - - name: Upload case report - if: always() - uses: actions/upload-artifact@v4 - with: - name: pvisor-case-report - path: target/pvisor-case-report.md - if-no-files-found: ignore - examples: name: Examples / ${{ matrix.shard }} runs-on: ubuntu-latest @@ -486,12 +320,6 @@ jobs: fail-fast: false matrix: include: - - shard: pvisor - command: | - just test-pvisor-benchmark - just examples-pvisor-portable - - shard: ppilot - command: just examples-ppilot - shard: pchronicle command: just examples-pchronicle steps: @@ -535,10 +363,8 @@ jobs: - rust-test - rust-proptest - pchronicle-web-test - - pvisor-isolation-regression - s3-contract - python-test - - pvisor-cases - examples runs-on: ubuntu-latest steps: diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 86d9903fe..5c64e3c75 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -129,7 +129,7 @@ jobs: curl -fsSL https://raw.githubusercontent.com/${{ github.repository }}/main/scripts/install-nightly.sh | bash ``` - The wheel includes the matched `pchronicle`, `pvisor`, and `ppilot` CLI component set. Built with local version `+${{ needs.meta.outputs.local_version }}` (base version from `pyproject.toml`). + The wheel includes the `pchronicle` CLI. Built with local version `+${{ needs.meta.outputs.local_version }}` (base version from `pyproject.toml`). publish-benchmark-readme: name: Publish benchmark summary to README diff --git a/.github/workflows/pvisor-benchmark.yml b/.github/workflows/pvisor-benchmark.yml deleted file mode 100644 index 462352333..000000000 --- a/.github/workflows/pvisor-benchmark.yml +++ /dev/null @@ -1,101 +0,0 @@ -name: pVisor Benchmark - -on: - push: - branches: [main, master] - pull_request: - branches: [main, master] - workflow_dispatch: - -concurrency: - group: pvisor-benchmark-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - compare: - name: Process and Run Bundle report - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - name: Checkout candidate - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Setup Build Environment - uses: ./.github/actions/setup-build-env - with: - components: "" - - - uses: Swatinem/rust-cache@v2 - with: - shared-key: pvisor-benchmark - - - name: Resolve main baseline - id: baseline - shell: bash - run: | - set -euo pipefail - if [[ "${{ github.event_name }}" == "pull_request" ]]; then - baseline_ref="origin/${{ github.base_ref }}" - elif git rev-parse --verify HEAD^ >/dev/null 2>&1; then - baseline_ref="HEAD^" - else - baseline_ref="HEAD" - fi - baseline_sha="$(git rev-parse "$baseline_ref")" - baseline_repo="${RUNNER_TEMP}/persisting-main" - git worktree add --detach "$baseline_repo" "$baseline_sha" - echo "repo=$baseline_repo" >> "$GITHUB_OUTPUT" - echo "sha=$baseline_sha" >> "$GITHUB_OUTPUT" - if [[ -x "$baseline_repo/benchmark/pvisor/run.sh" ]] && \ - grep -q '^benchmark-pvisor ' "$baseline_repo/justfile"; then - echo "supported=true" >> "$GITHUB_OUTPUT" - else - echo "supported=false" >> "$GITHUB_OUTPUT" - fi - - - name: Benchmark main - if: steps.baseline.outputs.supported == 'true' - shell: bash - run: | - set -euo pipefail - cd "${{ steps.baseline.outputs.repo }}" - just benchmark-pvisor \ - smoke \ - "${RUNNER_TEMP}/pvisor-benchmark/main" \ - "${GITHUB_WORKSPACE}/target/pvisor-benchmark-main" - - - name: Benchmark candidate - shell: bash - run: | - just benchmark-pvisor \ - smoke \ - "${RUNNER_TEMP}/pvisor-benchmark/candidate" \ - "${GITHUB_WORKSPACE}/target/pvisor-benchmark-candidate" - - - name: Compare reports - shell: bash - run: | - set -euo pipefail - candidate="${RUNNER_TEMP}/pvisor-benchmark/candidate/raw-report.json" - baseline="${RUNNER_TEMP}/pvisor-benchmark/main/raw-report.json" - output="${RUNNER_TEMP}/pvisor-benchmark/comparison" - if [[ -f "$baseline" ]]; then - just benchmark-pvisor-compare "$candidate" "$baseline" "$output" - else - just benchmark-pvisor-compare "$candidate" "" "$output" - fi - cat "$output/report.md" >> "$GITHUB_STEP_SUMMARY" - - - name: Upload benchmark data and report - if: always() - uses: actions/upload-artifact@v4 - with: - name: pvisor-benchmark-${{ github.run_id }} - path: ${{ runner.temp }}/pvisor-benchmark - retention-days: 30 - if-no-files-found: error diff --git a/.gitignore b/.gitignore index dc9f42270..5ee29b208 100644 --- a/.gitignore +++ b/.gitignore @@ -14,12 +14,6 @@ crates/persisting-engine/ds/ # Local review, benchmark, and runtime artifacts /data/ /tmp/ -/examples/pvisor/lease.lock -/examples/pvisor/run.json -/examples/pvisor/run-bundle.json -/examples/pvisor/03-network-isolation/lease.lock -/examples/pvisor/03-network-isolation/run.json -/examples/pvisor/03-network-isolation/run-bundle.json # persisting-dlcapt runtime / release artifacts crates/persisting-dlcapt/var/ diff --git a/AGENTS.md b/AGENTS.md index ba17f3822..206c07b77 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,9 +25,9 @@ This exclusion covers, in particular: - `crates/persisting-dlcapt/` and dlcapt-specific scripts, tests, features, and workflows -The default active scope is the Agent infrastructure centered on pVisor, -pPilot, pChronicle, Gateway, Control, OverlayFS, OverlayNet, and trajectory CLI -surfaces. `persisting-dlcapt` is a separate standalone component; excluding it +The default active scope is pChronicle, its trajectory CLI and Web UI, and +the Gateway, Control, events, and OverlayNet libraries it depends on. pVisor +and pPilot are maintained in external repositories. `persisting-dlcapt` is a separate standalone component; excluding it does not exclude Gateway trajectory capture or pChronicle capture storage. Enter an excluded subsystem only when: diff --git a/Cargo.lock b/Cargo.lock index 2a5f13ba3..1a15eb697 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,22 +79,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", - "anstyle-parse 0.2.7", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstream" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" -dependencies = [ - "anstyle", - "anstyle-parse 1.0.0", + "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", @@ -117,15 +102,6 @@ dependencies = [ "utf8parse", ] -[[package]] -name = "anstyle-parse" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" -dependencies = [ - "utf8parse", -] - [[package]] name = "anstyle-query" version = "1.1.5" @@ -991,25 +967,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bincode" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" -dependencies = [ - "bincode_derive", - "unty", -] - -[[package]] -name = "bincode_derive" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" -dependencies = [ - "virtue", -] - [[package]] name = "bit-set" version = "0.8.0" @@ -1173,34 +1130,6 @@ dependencies = [ "either", ] -[[package]] -name = "bzip2" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" -dependencies = [ - "bzip2-sys", -] - -[[package]] -name = "bzip2-sys" -version = "0.1.13+1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" -dependencies = [ - "cc", - "pkg-config", -] - -[[package]] -name = "caps" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd1ddba47aba30b6a889298ad0109c3b8dcb0e8fc993b459daa7067d46f865e0" -dependencies = [ - "libc", -] - [[package]] name = "cast" version = "0.3.0" @@ -1343,7 +1272,7 @@ version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ - "anstream 0.6.21", + "anstream", "anstyle", "clap_lex", "strsim", @@ -1367,15 +1296,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" -[[package]] -name = "clru" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "197fd99cb113a8d5d9b6376f3aa817f32c1078f2343b714fff7d2ca44fdf67d5" -dependencies = [ - "hashbrown 0.16.1", -] - [[package]] name = "cmake" version = "0.1.58" @@ -2587,29 +2507,6 @@ dependencies = [ "cfg-if 1.0.4", ] -[[package]] -name = "env_filter" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" -dependencies = [ - "anstream 1.0.0", - "anstyle", - "env_filter", - "jiff", - "log", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -2626,16 +2523,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "etcetera" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" -dependencies = [ - "cfg-if 1.0.4", - "windows-sys 0.61.2", -] - [[package]] name = "ethnum" version = "1.5.3" @@ -2669,32 +2556,12 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8eb564c5c7423d25c886fb561d1e4ee69f72354d16918afa32c08811f6b6a55" -[[package]] -name = "faster-hex" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" -dependencies = [ - "heapless 0.8.0", - "serde", -] - [[package]] name = "fastrand" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" -[[package]] -name = "filetime" -version = "0.2.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" -dependencies = [ - "cfg-if 1.0.4", - "libc", -] - [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -2807,21 +2674,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" -[[package]] -name = "fuser" -version = "0.15.1" -dependencies = [ - "libc", - "libloading", - "log", - "memchr", - "nix 0.29.0", - "page_size", - "pkg-config", - "smallvec", - "zerocopy", -] - [[package]] name = "futures" version = "0.3.32" @@ -3131,906 +2983,172 @@ dependencies = [ ] [[package]] -name = "gix" -version = "0.85.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa8b2e38ebfc4484dfef8580ddcaf8abb7285e6f3eb6413ff6775d104ae96ca6" -dependencies = [ - "gix-actor", - "gix-attributes", - "gix-command", - "gix-commitgraph", - "gix-config", - "gix-date", - "gix-diff", - "gix-discover", - "gix-error", - "gix-features", - "gix-filter", - "gix-fs", - "gix-glob", - "gix-hash", - "gix-hashtable", - "gix-ignore", - "gix-index", - "gix-lock", - "gix-object", - "gix-odb", - "gix-pack", - "gix-path", - "gix-pathspec", - "gix-protocol", - "gix-ref", - "gix-refspec", - "gix-revision", - "gix-revwalk", - "gix-sec", - "gix-shallow", - "gix-submodule", - "gix-tempfile", - "gix-trace", - "gix-traverse", - "gix-url", - "gix-utils", - "gix-validate", - "gix-worktree", - "gix-worktree-stream", - "nonempty", - "smallvec", - "thiserror 2.0.18", -] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] -name = "gix-actor" -version = "0.41.2" +name = "gloo-timers" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33f9308ad6fd35b2a865cbe4117ac61b2be59e4a9ef1621c7a9794f7c8e52c5b" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" dependencies = [ - "bstr", - "gix-date", - "gix-error", + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "gix-attributes" -version = "0.33.2" +name = "goosefs-sdk" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39b40888d0ed415c0744a6cdc61eebf0304c9d26ab726725b718443c322e5ba4" +checksum = "e1ea4eee6dcbc31b25ab4fd577adc55b677d2bed3aa3016c44c58fbe1b2298a5" dependencies = [ - "bstr", - "gix-glob", - "gix-path", - "gix-quote", - "gix-trace", - "kstring", - "smallvec", + "arc-swap", + "async-trait", + "bytes", + "dashmap", + "fastrand", + "futures", + "hostname", + "io-uring", + "itoa", + "libc", + "lru 0.18.4", + "memmap2", + "moka", + "prost", + "prost-types", + "rand 0.9.4", + "reqwest 0.12.28", + "serde", "thiserror 2.0.18", - "unicode-bom", + "tokio", + "tokio-stream", + "tonic", + "tonic-prost", + "tracing", + "uuid", + "xxhash-rust", ] [[package]] -name = "gix-bitmap" -version = "0.3.3" +name = "h2" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd1d118d0f5d88b96e6f6e13b566475fef4797ead4a02c26fed36c1375066f7" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ - "gix-error", + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.4.0", + "indexmap 2.14.0", + "slab", + "tokio", + "tokio-util", + "tracing", ] [[package]] -name = "gix-chunk" -version = "0.7.3" +name = "half" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a871e5cab12ba568845714473505deefffb3c04eb47f4708ce344cd459c1cc" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ - "gix-error", + "bytemuck", + "cfg-if 1.0.4", + "crunchy", + "num-traits", + "zerocopy", ] [[package]] -name = "gix-command" -version = "0.9.1" +name = "hash32" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00706d4fef135ef4b01680d5218c6ee40cda8baf697b864296cbc887d19118f6" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" dependencies = [ - "bstr", - "gix-path", - "gix-quote", - "gix-trace", - "shell-words", + "byteorder", ] [[package]] -name = "gix-commitgraph" -version = "0.37.1" +name = "hashbrown" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f675d0df484a7f6a47e64bd6f311af489d947c0323b0564f36d14f3d7762abb" -dependencies = [ - "bstr", - "gix-chunk", - "gix-error", - "gix-hash", - "memmap2", - "nonempty", -] +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] -name = "gix-config" -version = "0.58.0" +name = "hashbrown" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a29bf266c4cdaf759e535c24ad4ce655b987aeb6911075643403cc7cc5ade583" -dependencies = [ - "bstr", - "gix-config-value", - "gix-features", - "gix-glob", - "gix-path", - "gix-ref", - "gix-sec", - "smallvec", - "thiserror 2.0.18", - "unicode-bom", -] +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] -name = "gix-config-value" -version = "0.18.1" +name = "hashbrown" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed42168329552f6c2e5df09665c104199d45d84bedb53683738a49b57fe1baab" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "bitflags 2.11.1", - "bstr", - "gix-path", - "libc", - "thiserror 2.0.18", + "allocator-api2", + "equivalent", + "foldhash 0.1.5", ] [[package]] -name = "gix-date" -version = "0.15.6" +name = "hashbrown" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e47b9e8cdc688296609b706428de570f88b1e0eed7156dde7b4a89d26fa4567" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ - "bstr", - "gix-error", - "itoa", - "jiff", + "allocator-api2", + "equivalent", + "foldhash 0.2.0", ] [[package]] -name = "gix-diff" -version = "0.65.0" +name = "hashbrown" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92c6d56c94edf92d78203a1cd416f770e35e10b6955ede6b9d7d0c22ff88a5f3" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ - "bstr", - "gix-command", - "gix-filter", - "gix-fs", - "gix-hash", - "gix-imara-diff", - "gix-object", - "gix-path", - "gix-tempfile", - "gix-trace", - "gix-traverse", - "gix-worktree", - "thiserror 2.0.18", + "allocator-api2", + "equivalent", + "foldhash 0.2.0", ] [[package]] -name = "gix-discover" -version = "0.53.0" +name = "heapify" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0049b265b7f201ca9ab25475b22b47fe444060126a51abe00f77d986fc5cc52e" + +[[package]] +name = "heapless" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d624d5b23b10c1d85337645227abe353ac95ab8ff66a7bdd5ce689b2db33a722" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" dependencies = [ - "bstr", - "dunce", - "gix-fs", - "gix-path", - "gix-ref", - "gix-sec", - "thiserror 2.0.18", + "hash32", + "stable_deref_trait", ] [[package]] -name = "gix-error" -version = "0.2.5" +name = "heapless" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a9292309fd944e71b2a3c96d3c03a6feb8852db646febdde7cbb9f79cb5f329" +checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" dependencies = [ - "bstr", + "hash32", + "stable_deref_trait", ] [[package]] -name = "gix-features" -version = "0.48.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1849ae154d38bc403185be14fa871e38e3c93ee606875d94e207fdb9fba52dbc" -dependencies = [ - "bytes", - "crc32fast", - "crossbeam-channel", - "gix-path", - "gix-trace", - "gix-utils", - "libc", - "once_cell", - "parking_lot", - "prodash", - "thiserror 2.0.18", - "walkdir", - "zlib-rs", -] - -[[package]] -name = "gix-filter" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6644fb2ef97928c278675b239f366b457103d7e436f811d27331a8daf212759c" -dependencies = [ - "bstr", - "encoding_rs", - "gix-attributes", - "gix-command", - "gix-hash", - "gix-object", - "gix-packetline", - "gix-path", - "gix-quote", - "gix-trace", - "gix-utils", - "smallvec", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-fs" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cdff46db8798e47e2f727d84b9379aac5add3dd3d9d0b07bb4d7d5d640771fe" -dependencies = [ - "bstr", - "fastrand", - "gix-features", - "gix-path", - "gix-utils", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-glob" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1fcb8ef5b16bcf874abe9b68d8abb3c0493c876d367ab824151f30a0f3f3756" -dependencies = [ - "bitflags 2.11.1", - "bstr", - "gix-features", - "gix-path", -] - -[[package]] -name = "gix-hash" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0926d3819c837750b4e03c7754901e73f68b8c9b690753a6372a1bed4eedce" -dependencies = [ - "faster-hex", - "gix-features", - "sha1-checked", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-hashtable" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e261d54091f0d1c729bc83f54548c071bdec60a697de1e58e88bdfd7a99d24e" -dependencies = [ - "gix-hash", - "hashbrown 0.17.1", - "parking_lot", -] - -[[package]] -name = "gix-ignore" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d491bab9bf2c9f341dc754f425c31d5d3f63aca615312167b82e1deeaca97d8d" -dependencies = [ - "bstr", - "gix-glob", - "gix-path", - "gix-trace", - "unicode-bom", -] - -[[package]] -name = "gix-imara-diff" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a791e6620676a875f362f3156ed213e73ca099a09bf992c18812abe65cc37b1" -dependencies = [ - "bstr", - "hashbrown 0.17.1", -] - -[[package]] -name = "gix-index" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36d45f82ec5a4d7542ea595e9ad16e03e26c8cb4f221e5bc9fcdcf469f63a681" -dependencies = [ - "bitflags 2.11.1", - "bstr", - "filetime", - "fnv", - "gix-bitmap", - "gix-features", - "gix-fs", - "gix-hash", - "gix-lock", - "gix-object", - "gix-traverse", - "gix-utils", - "gix-validate", - "hashbrown 0.17.1", - "itoa", - "libc", - "memmap2", - "rustix", - "smallvec", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-lock" -version = "23.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65c9dedd9e90b0d47624d2ed241d394e09294118364e87b9b7e5f1fe755f3c2c" -dependencies = [ - "gix-tempfile", - "gix-utils", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-object" -version = "0.62.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "019b38afc3eac1e41f9fe09a327664b313ba4a120fa5f40e3678795d0e42783e" -dependencies = [ - "bstr", - "gix-actor", - "gix-date", - "gix-features", - "gix-hash", - "gix-hashtable", - "gix-utils", - "gix-validate", - "itoa", - "smallvec", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-odb" -version = "0.82.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fadc59f6fa0f9dd445eceee61060a2b59ca557f48da9fc677f567db535b782a" -dependencies = [ - "arc-swap", - "gix-features", - "gix-fs", - "gix-hash", - "gix-hashtable", - "gix-object", - "gix-pack", - "gix-path", - "gix-quote", - "memmap2", - "parking_lot", - "tempfile", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-pack" -version = "0.72.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3e7f1726cd2c0cd1cf1fc20be8a8e623f0b163f1f8d6fc836cfb9bc8cd758b" -dependencies = [ - "clru", - "gix-chunk", - "gix-error", - "gix-features", - "gix-hash", - "gix-hashtable", - "gix-object", - "gix-path", - "memmap2", - "smallvec", - "thiserror 2.0.18", - "uluru", -] - -[[package]] -name = "gix-packetline" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b217dd0ee0c4021ecf169a4a519b1b4f80d15e3f3765f3dc466223dc0ac891d7" -dependencies = [ - "bstr", - "faster-hex", - "gix-trace", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-path" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ed3e8d7a82e886e17a72e03d4ba0c13db6f2219b6cd4e2900b4cae426ec20c9" -dependencies = [ - "bstr", - "gix-trace", - "gix-validate", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-pathspec" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3050783b41ee11511e1e8fb35623df81806194f4030395f14f48ea37c2798c9f" -dependencies = [ - "bitflags 2.11.1", - "bstr", - "gix-attributes", - "gix-config-value", - "gix-glob", - "gix-path", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-protocol" -version = "0.63.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978468bae4ea2df20c72db3b20d0bdb548a0c1090b85a83643b553e6e0e041f2" -dependencies = [ - "bstr", - "gix-date", - "gix-features", - "gix-hash", - "gix-ref", - "gix-shallow", - "gix-transport", - "gix-utils", - "maybe-async", - "nonempty", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-quote" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6e541fc33cc2b783b7979040d445a0c86a2eca747c8faea4ca84230d06ae6ef" -dependencies = [ - "bstr", - "gix-error", - "gix-utils", -] - -[[package]] -name = "gix-ref" -version = "0.65.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bbfbce1dfd7d7f8469ddef6d3518376aff664348f153cbe0fc3e58ef993d24e" -dependencies = [ - "gix-actor", - "gix-features", - "gix-fs", - "gix-hash", - "gix-lock", - "gix-object", - "gix-path", - "gix-tempfile", - "gix-utils", - "gix-validate", - "memmap2", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-refspec" -version = "0.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bc36a4fb1a1540b59cf2da498783080743fa274b02a3f19ca444fc4015a9d4f" -dependencies = [ - "bstr", - "gix-error", - "gix-glob", - "gix-hash", - "gix-revision", - "gix-validate", - "smallvec", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-revision" -version = "0.47.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "885075c3c21eb9c06e0be3b3728ba5932c04e1c1011dcee7c81801980e3e986f" -dependencies = [ - "bstr", - "gix-commitgraph", - "gix-date", - "gix-error", - "gix-hash", - "gix-object", - "gix-revwalk", - "nonempty", -] - -[[package]] -name = "gix-revwalk" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f11fe7ca2585193d3d70bbe0be175a2008d883a704cc7a55e454e113e689455" -dependencies = [ - "gix-commitgraph", - "gix-date", - "gix-error", - "gix-hash", - "gix-hashtable", - "gix-object", - "smallvec", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-sec" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af4fe6c152c1d50aea36f299825702cd37e303307832fec1d0fdd5844e47ce2f" -dependencies = [ - "bitflags 2.11.1", - "gix-path", - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "gix-shallow" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a292fc2fe548c5dfa575479d16b445b0ddf1dd2f56f1fec6aed386f82553cd97" -dependencies = [ - "bstr", - "gix-hash", - "gix-lock", - "nonempty", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-submodule" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7f9f594f7cbda0b38ba6b633b3e9a7b7901acdc5d27bc186a16633800cd1ac8" -dependencies = [ - "bstr", - "gix-config", - "gix-path", - "gix-pathspec", - "gix-refspec", - "gix-url", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-tempfile" -version = "23.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27850097e1ff9515f46a0dad0f5f9c9d020e972727772dabab9450690c4adb22" -dependencies = [ - "dashmap", - "gix-fs", - "libc", - "parking_lot", - "tempfile", -] - -[[package]] -name = "gix-trace" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be3eb81d9dc914335923e50d52829c551feefd6a72d176c4130c546b67a60814" - -[[package]] -name = "gix-transport" -version = "0.57.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186874f7ad1fb2f9a2f2aa9c2dabc7f9dd087bef74c1a0eee2b4a9cf0248fcb3" -dependencies = [ - "bstr", - "gix-command", - "gix-features", - "gix-packetline", - "gix-quote", - "gix-sec", - "gix-url", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-traverse" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5062cca8f2977565bbaf666ec31dbdb9bc9d9293beb65f9bec52e6c1121b62a1" -dependencies = [ - "bitflags 2.11.1", - "gix-commitgraph", - "gix-date", - "gix-hash", - "gix-hashtable", - "gix-object", - "gix-revwalk", - "smallvec", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-url" -version = "0.36.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d68e70e96da0e5f9c871f1566349e0fd0e1a20bb483c7f54af1dd0b85b4b29" -dependencies = [ - "bstr", - "gix-path", - "percent-encoding", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-utils" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1795bd2a970ca8b2185318c2abb97d955c71992f1cf28de73ad3b593a9f3ce8" -dependencies = [ - "fastrand", - "getrandom 0.4.2", - "unicode-normalization", -] - -[[package]] -name = "gix-validate" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a034e84d1e04e1b1f20f51f12491da230b6ac8b925d0c8e1b89bcd87a7c5ccc" -dependencies = [ - "bstr", -] - -[[package]] -name = "gix-worktree" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92399ed66f259592050c6ed9dc80105e095a2f8e87e6b83d98aa2e21d8e27036" -dependencies = [ - "bstr", - "gix-attributes", - "gix-fs", - "gix-glob", - "gix-hash", - "gix-ignore", - "gix-index", - "gix-object", - "gix-path", - "gix-validate", -] - -[[package]] -name = "gix-worktree-stream" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55f3a878c89a05470ad98c644b0015777c530da24854dd29e41fe4f41176840f" -dependencies = [ - "gix-attributes", - "gix-error", - "gix-features", - "gix-filter", - "gix-fs", - "gix-hash", - "gix-object", - "gix-path", - "gix-traverse", - "parking_lot", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "globset" -version = "0.4.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e47d37d2ae4464254884b60ab7071be2b876a9c35b696bd018ddcc76847309cd" -dependencies = [ - "aho-corasick", - "bstr", - "log", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "gloo-timers" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "goosefs-sdk" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1ea4eee6dcbc31b25ab4fd577adc55b677d2bed3aa3016c44c58fbe1b2298a5" -dependencies = [ - "arc-swap", - "async-trait", - "bytes", - "dashmap", - "fastrand", - "futures", - "hostname", - "io-uring", - "itoa", - "libc", - "lru 0.18.4", - "memmap2", - "moka", - "prost", - "prost-types", - "rand 0.9.4", - "reqwest 0.12.28", - "serde", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "tonic", - "tonic-prost", - "tracing", - "uuid", - "xxhash-rust", -] - -[[package]] -name = "h2" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http 1.4.0", - "indexmap 2.14.0", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "bytemuck", - "cfg-if 1.0.4", - "crunchy", - "num-traits", - "zerocopy", -] - -[[package]] -name = "hash32" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" -dependencies = [ - "byteorder", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.1.5", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] - -[[package]] -name = "heapify" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0049b265b7f201ca9ab25475b22b47fe444060126a51abe00f77d986fc5cc52e" - -[[package]] -name = "heapless" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" -dependencies = [ - "hash32", - "stable_deref_trait", -] - -[[package]] -name = "heapless" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" -dependencies = [ - "hash32", - "stable_deref_trait", -] - -[[package]] -name = "heck" -version = "0.5.0" +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" @@ -4492,27 +3610,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "imago" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "404f567d70cd6d288e43f95ea8f2d6e8fe6156dd07df7da955cb9b147c4ab2a5" -dependencies = [ - "async-trait", - "bincode 2.0.1", - "cfg-if 1.0.4", - "futures", - "libc", - "maybe-async", - "miniz_oxide", - "nix 0.30.1", - "page_size", - "rustc_version", - "tokio", - "tracing", - "windows-sys 0.61.2", -] - [[package]] name = "include_dir" version = "0.7.4" @@ -4565,16 +3662,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "interim" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9ce9099a85f468663d3225bf87e85d0548968441e1db12248b996b24f0f5b5a" -dependencies = [ - "chrono", - "logos 0.15.1", -] - [[package]] name = "io-uring" version = "0.7.12" @@ -4635,15 +3722,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.18" @@ -4728,64 +3806,6 @@ dependencies = [ "jiff-tzdb", ] -[[package]] -name = "jj-lib" -version = "0.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22e2ced34a1a51f0f449bfff2cbacda1c338b2611268b5684a80251e4c8bf71" -dependencies = [ - "async-trait", - "blake2", - "bstr", - "chrono", - "clru", - "digest 0.10.7", - "dunce", - "either", - "etcetera", - "futures", - "gix", - "gix-ignore", - "globset", - "hashbrown 0.17.1", - "indexmap 2.14.0", - "interim", - "itertools 0.15.0", - "jj-lib-proc-macros", - "maplit", - "once_cell", - "pest", - "pest_derive", - "pollster", - "prost", - "rand 0.10.1", - "rand_chacha 0.10.0", - "rayon", - "ref-cast", - "regex", - "rustix", - "same-file", - "serde", - "smallvec", - "strsim", - "tempfile", - "thiserror 2.0.18", - "toml_edit 0.25.13+spec-1.1.0", - "tracing", - "winreg", -] - -[[package]] -name = "jj-lib-proc-macros" -version = "0.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c21554e3755e5ecfec934d84ac64d0295d9c2f2b96f6a740a1ddb66addf4142e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "jni" version = "0.22.4" @@ -4894,181 +3914,6 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e037a2e1d8d5fdbd49b16a4ea09d5d6401c1f29eca5ff29d03d3824dba16256a" -[[package]] -name = "krun-arch" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e408be4923e881a3fb6536322d569a845e55fb0f5af4a9af3202ed97becbb192" -dependencies = [ - "krun-arch-gen", - "krun-smbios", - "krun-utils", - "kvm-bindings", - "kvm-ioctls", - "libc", - "vm-memory", - "vmm-sys-util 0.14.0", -] - -[[package]] -name = "krun-arch-gen" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e903977e89dbb2f77008307ce9953281f681a099e647b79e9220169278ce1970" - -[[package]] -name = "krun-cpuid" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e0bda6161895cc919dff67cf5d51d03085a93e2f2022aa52da406d758a566a" -dependencies = [ - "kvm-bindings", - "kvm-ioctls", - "vmm-sys-util 0.14.0", -] - -[[package]] -name = "krun-devices" -version = "0.1.0-1.19.3" -dependencies = [ - "bitflags 1.3.2", - "caps", - "crossbeam-channel", - "imago", - "krun-arch", - "krun-hvf", - "krun-polly", - "krun-utils", - "kvm-bindings", - "kvm-ioctls", - "libc", - "libloading", - "log", - "lru 0.12.5", - "nix 0.30.1", - "persisting-overlay-core", - "rand 0.9.4", - "virtio-bindings", - "vm-fdt", - "vm-memory", -] - -[[package]] -name = "krun-hvf" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f7e78f0c5431195ca36aded1886024872699a6a56f0a600f619aeb7ef2161bc" -dependencies = [ - "crossbeam-channel", - "krun-arch", - "libloading", - "log", -] - -[[package]] -name = "krun-init-blob" -version = "0.1.0-1.19.3" - -[[package]] -name = "krun-kernel" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48953b0f707aafee40684fdf45fcb0ea068745aaa9500f672c1a39da113e97a" -dependencies = [ - "krun-utils", - "vm-memory", -] - -[[package]] -name = "krun-polly" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d2c61890357072d5751ef813aea744b93a67bfc3b820f36061f9706f72af84d" -dependencies = [ - "krun-utils", - "libc", -] - -[[package]] -name = "krun-smbios" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01462ad735097a9a9650564e7f7ba082db720a41696f94ec9fb56ecaf072c744" -dependencies = [ - "vm-memory", -] - -[[package]] -name = "krun-utils" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8315551f3fd0c86191ff14318ac595a1e62d9c1e0690d30355d3a4e0ac741c87" -dependencies = [ - "bitflags 1.3.2", - "crossbeam-channel", - "kvm-bindings", - "libc", - "log", - "nix 0.30.1", - "vmm-sys-util 0.14.0", -] - -[[package]] -name = "krun-vmm" -version = "0.1.0-1.19.3" -dependencies = [ - "bzip2", - "crossbeam-channel", - "flate2", - "krun-arch", - "krun-arch-gen", - "krun-cpuid", - "krun-devices", - "krun-hvf", - "krun-kernel", - "krun-polly", - "krun-utils", - "kvm-bindings", - "kvm-ioctls", - "libc", - "linux-loader", - "log", - "nix 0.30.1", - "vm-memory", - "vmm-sys-util 0.15.0", - "zstd", -] - -[[package]] -name = "kstring" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b609e7ca5ea38f093c20a4a102335b247221c9643b7a6bc3510f196f99499a9e" -dependencies = [ - "static_assertions", -] - -[[package]] -name = "kvm-bindings" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a537873e15e8daabb416667e606d9b0abc2a8fb9a45bd5853b888ae0ead82f9" -dependencies = [ - "vmm-sys-util 0.14.0", -] - -[[package]] -name = "kvm-ioctls" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8f7370330b4f57981e300fa39b02088f2f2a5c2d0f1f994e8090589619c56d" -dependencies = [ - "bitflags 2.11.1", - "kvm-bindings", - "libc", - "vmm-sys-util 0.14.0", -] - [[package]] name = "lance" version = "11.0.0" @@ -5702,37 +4547,6 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" -[[package]] -name = "libkrun" -version = "1.19.3" -dependencies = [ - "crossbeam-channel", - "env_logger", - "krun-devices", - "krun-hvf", - "krun-init-blob", - "krun-polly", - "krun-utils", - "krun-vmm", - "kvm-bindings", - "kvm-ioctls", - "libc", - "libloading", - "log", - "once_cell", - "vm-memory", -] - -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if 1.0.4", - "windows-link", -] - [[package]] name = "libm" version = "0.2.16" @@ -5769,15 +4583,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "348d0075b1fc163b26d72a7f75fc5141daf2fd1bdf128d873cbaf6785d495bdf" -[[package]] -name = "linux-loader" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de72cb02c55ecffcf75fe78295926f872eb6eb0a58d629c58a8c324dc26380f6" -dependencies = [ - "vm-memory", -] - [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -5942,12 +4747,6 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" -[[package]] -name = "maplit" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" - [[package]] name = "matchers" version = "0.2.0" @@ -5976,17 +4775,6 @@ dependencies = [ "thread-tree", ] -[[package]] -name = "maybe-async" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "746873a384ad60adc5db74471dfaba74bd278afbdcfd81db93fafcdfc8b5ca0c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "md-5" version = "0.10.6" @@ -6032,15 +4820,6 @@ dependencies = [ "libc", ] -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - [[package]] name = "miette" version = "7.6.0" @@ -6130,46 +4909,21 @@ checksum = "1fafa6961cabd9c63bcd77a45d7e3b7f3b552b70417831fb0f56db717e72407e" name = "multimap" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" - -[[package]] -name = "ndarray" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" -dependencies = [ - "matrixmultiply", - "num-complex", - "num-integer", - "num-traits", - "portable-atomic", - "portable-atomic-util", - "rawpointer", -] - -[[package]] -name = "nix" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" -dependencies = [ - "bitflags 2.11.1", - "cfg-if 1.0.4", - "cfg_aliases", - "libc", -] +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" [[package]] -name = "nix" -version = "0.30.1" +name = "ndarray" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" dependencies = [ - "bitflags 2.11.1", - "cfg-if 1.0.4", - "cfg_aliases", - "libc", - "memoffset", + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", ] [[package]] @@ -6181,12 +4935,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "nonempty" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9737e026353e5cd0736f98eddae28665118eb6f6600902a7f50db585621fecb6" - [[package]] name = "ntapi" version = "0.4.3" @@ -7114,13 +5862,9 @@ version = "0.3.0" dependencies = [ "anyhow", "ipnet", - "libc", "proptest", "serde", "serde_json", - "tempfile", - "tokio", - "tokio-util", ] [[package]] @@ -7197,34 +5941,6 @@ dependencies = [ "url", ] -[[package]] -name = "persisting-overlay-core" -version = "0.3.0" -dependencies = [ - "libc", - "log", - "serde", - "serde_json", - "sha2 0.11.0", - "tempfile", -] - -[[package]] -name = "persisting-overlayfs" -version = "0.3.0" -dependencies = [ - "anyhow", - "clap", - "env_logger", - "fuser", - "jj-lib", - "libc", - "log", - "persisting-overlay-core", - "pollster", - "tempfile", -] - [[package]] name = "persisting-overlaynet" version = "0.3.0" @@ -7324,131 +6040,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "persisting-ppilot" -version = "0.3.0" -dependencies = [ - "anyhow", - "async-trait", - "chrono", - "clap", - "futures", - "persisting-agentctl", - "persisting-events", - "proptest", - "pulsing-actor", - "serde", - "serde_json", - "sha2 0.11.0", - "tempfile", - "tokio", - "tokio-stream", - "tokio-util", - "tracing", - "tracing-subscriber", - "uuid", -] - -[[package]] -name = "persisting-pvisor" -version = "0.3.0" -dependencies = [ - "anyhow", - "async-trait", - "chrono", - "clap", - "dirs", - "flate2", - "fs2", - "globset", - "libc", - "libkrun", - "persisting-agentctl", - "persisting-events", - "persisting-gateway", - "persisting-overlay-core", - "persisting-overlayfs", - "persisting-overlaynet", - "persisting-ppilot", - "persisting-replay", - "proptest", - "reqwest 0.12.28", - "serde", - "serde_json", - "sha2 0.11.0", - "tar", - "tempfile", - "thiserror 2.0.18", - "tokio", - "tokio-util", - "toml", - "tracing", - "tracing-subscriber", - "uuid", - "zstd", -] - -[[package]] -name = "persisting-replay" -version = "0.3.0" -dependencies = [ - "anyhow", - "axum", - "chrono", - "fs2", - "libc", - "reqwest 0.12.28", - "serde", - "serde_json", - "sha2 0.11.0", - "tempfile", - "tokio", - "toml", - "uuid", -] - -[[package]] -name = "pest" -version = "2.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df728be843c7070fab6ab7c328c4e9e9d78e23bf749c0669c86ee7ebfa050a2" -dependencies = [ - "memchr", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e2dd6fc3b26b3462ee188aac870f5a41d398f1cd5e2408d16531bd71c9591fd" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a7a9205cfb6f596a9e8b689c0a15f9ceb7a1aafae7aaf788150ac65b29975b6" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pest_meta" -version = "2.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85abd351c0de1e8384fc791a0737111a350394937e92b956b743dac12429f57c" -dependencies = [ - "pest", -] - [[package]] name = "petgraph" version = "0.8.3" @@ -7622,12 +6213,6 @@ dependencies = [ "plotters-backend", ] -[[package]] -name = "pollster" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" - [[package]] name = "portable-atomic" version = "1.13.1" @@ -7708,15 +6293,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "prodash" -version = "31.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "962200e2d7d551451297d9fdce85138374019ada198e30ea9ede38034e27604c" -dependencies = [ - "parking_lot", -] - [[package]] name = "proptest" version = "1.11.0" @@ -7842,7 +6418,7 @@ checksum = "8a751c49db41d362084cb011c77391c1b0e857d11f4e6a839f6452c722a090a6" dependencies = [ "anyhow", "async-trait", - "bincode 1.3.3", + "bincode", "bytes", "dashmap", "futures", @@ -8039,16 +6615,6 @@ dependencies = [ "rand_core 0.9.5", ] -[[package]] -name = "rand_chacha" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e6af7f3e25ded52c41df4e0b1af2d047e45896c2f3281792ed68a1c243daedb" -dependencies = [ - "ppv-lite86", - "rand_core 0.10.1", -] - [[package]] name = "rand_core" version = "0.6.4" @@ -8855,15 +7421,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_spanned" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" -dependencies = [ - "serde_core", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -8943,16 +7500,6 @@ dependencies = [ "digest 0.11.3", ] -[[package]] -name = "sha1-checked" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" -dependencies = [ - "digest 0.10.7", - "sha1 0.10.6", -] - [[package]] name = "sha1_smol" version = "1.0.1" @@ -9000,12 +7547,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shell-words" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" - [[package]] name = "shellexpand" version = "3.1.2" @@ -9082,9 +7623,6 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -dependencies = [ - "serde", -] [[package]] name = "smoltcp" @@ -9326,17 +7864,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" -[[package]] -name = "tar" -version = "0.4.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" -dependencies = [ - "filetime", - "libc", - "xattr", -] - [[package]] name = "tempfile" version = "3.27.0" @@ -9565,7 +8092,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned 0.6.9", + "serde_spanned", "toml_datetime 0.6.11", "toml_edit 0.22.27", ] @@ -9596,7 +8123,7 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap 2.14.0", "serde", - "serde_spanned 0.6.9", + "serde_spanned", "toml_datetime 0.6.11", "toml_write", "winnow 0.7.15", @@ -9609,11 +8136,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ "indexmap 2.14.0", - "serde_core", - "serde_spanned 1.1.1", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "toml_writer", "winnow 1.0.2", ] @@ -9632,12 +8156,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" -[[package]] -name = "toml_writer" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" - [[package]] name = "tonic" version = "0.14.6" @@ -9864,21 +8382,6 @@ version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6" -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - -[[package]] -name = "uluru" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c8a2469e56e6e5095c82ccd3afb98dad95f7af7929aab6d8ba8d6e0f73657da" -dependencies = [ - "arrayvec", -] - [[package]] name = "unarray" version = "0.1.4" @@ -9891,12 +8394,6 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" -[[package]] -name = "unicode-bom" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" - [[package]] name = "unicode-ident" version = "1.0.24" @@ -9948,12 +8445,6 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" -[[package]] -name = "unty" -version = "0.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" - [[package]] name = "url" version = "2.5.8" @@ -10016,55 +8507,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "virtio-bindings" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091f1f09cfbf2a78563b562e7a949465cce1aef63b6065645188d995162f8868" - -[[package]] -name = "virtue" -version = "0.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" - -[[package]] -name = "vm-fdt" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e21282841a059bb62627ce8441c491f09603622cd5a21c43bfedc85a2952f23" - -[[package]] -name = "vm-memory" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f39348a049689cabd3377cdd9182bf526ec76a6f823b79903896452e9d7a7380" -dependencies = [ - "libc", - "thiserror 2.0.18", - "winapi", -] - -[[package]] -name = "vmm-sys-util" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d21f366bf22bfba3e868349978766a965cbe628c323d58e026be80b8357ab789" -dependencies = [ - "bitflags 1.3.2", - "libc", -] - -[[package]] -name = "vmm-sys-util" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "506c62fdf617a5176827c2f9afbcf1be155b03a9b4bf9617a60dbc07e3a1642f" -dependencies = [ - "bitflags 1.3.2", - "libc", -] - [[package]] name = "vsimd" version = "0.8.0" @@ -10559,16 +9001,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" -dependencies = [ - "cfg-if 1.0.4", - "windows-sys 0.61.2", -] - [[package]] name = "wit-bindgen" version = "0.51.0" @@ -10977,12 +9409,6 @@ dependencies = [ "syn", ] -[[package]] -name = "zlib-rs" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b142a20ec14a91d5bc708c1dc21b080c550113d8aa77afa29635673a65dd02c5" - [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index 480791773..1078f8258 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ version = "0.3.0" edition = "2024" authors = ["reiase"] license = "Apache-2.0" -description = "Agent execution, orchestration, and durable history infrastructure" +description = "Durable Agent trajectory capture, storage, and query infrastructure" [workspace] resolver = "3" @@ -12,21 +12,13 @@ members = [ "crates/persisting-events", "crates/persisting-gateway", "crates/persisting-dlcapt", - "crates/persisting-ppilot", - "crates/persisting-pvisor", - "crates/persisting-replay", "crates/persisting-pchronicle", "crates/persisting-pchronicle-cli", - "crates/persisting-overlayfs", - "crates/persisting-overlay-core", "crates/persisting-overlaynet", ] -exclude = ["vendor/libkrun", "vendor/krun-vmm", "vendor/krun-devices"] default-members = [ "crates/persisting-pchronicle-cli", - "crates/persisting-ppilot", - "crates/persisting-pvisor", ] [workspace.dependencies] @@ -41,19 +33,14 @@ clap = "4" dashmap = "6" datafusion = { version = "54.1.0", default-features = false } dirs = "6" -env_logger = "0.11" fs2 = "0.4" -flate2 = "1" -fuser = { version = "0.15", default-features = false } futures = "0.3" futures-util = { version = "0.3", default-features = false } -globset = "0.4" hostname = "0.4" http-body-util = "0.1" hyper = "1" hyper-util = { version = "0.1", default-features = false } ipnet = "2" -jj-lib = "0.43.0" lance = { version = "11.0.0", default-features = false } lance-arrow = "11.0.0" lance-core = "11.0.0" @@ -64,8 +51,6 @@ lance-io = "11.0.0" lance-linalg = "11.0.0" lance-table = "11.0.0" libc = "0.2" -libkrun = "=1.19.3" -log = "0.4" opendal = { version = "0.57.0", default-features = true, features = [ "services-azblob", "services-cos", @@ -80,13 +65,8 @@ object_store = "0.13.2" persisting-agentctl = { path = "crates/persisting-agentctl" } persisting-events = { path = "crates/persisting-events" } persisting-gateway = { path = "crates/persisting-gateway" } -persisting-overlayfs = { path = "crates/persisting-overlayfs", default-features = false } -persisting-overlay-core = { path = "crates/persisting-overlay-core" } persisting-overlaynet = { path = "crates/persisting-overlaynet" } persisting-pchronicle = { path = "crates/persisting-pchronicle", default-features = false } -persisting-pvisor = { path = "crates/persisting-pvisor" } -persisting-replay = { path = "crates/persisting-replay" } -pollster = "0.4" pulsing-actor = { version = "0.1.2", default-features = false } proptest = "1" regex = "1" @@ -94,9 +74,7 @@ reqwest = { version = "0.12", default-features = false } serde = "1" serde_json = "1" serde_yaml = "0.9" -sha2 = "0.11" smoltcp = { version = "0.13.1", default-features = false } -tar = "0.4" tempfile = "3" thiserror = "2" tokio = { version = "1", default-features = false } @@ -155,8 +133,3 @@ panic = "abort" # adapter preserves Prost's code generation API but parses .proto files with # pure-Rust protox, avoiding both a system protoc and vendored protobuf-src. prost-build = { path = "vendor/prost-build-protox" } -fuser = { path = "vendor/fuser" } -libkrun = { path = "vendor/libkrun" } -krun-init-blob = { path = "vendor/krun-init-blob" } -krun-vmm = { path = "vendor/krun-vmm" } -krun-devices = { path = "vendor/krun-devices" } diff --git a/NOTICE b/NOTICE index e822d6d05..92fc7ae05 100644 --- a/NOTICE +++ b/NOTICE @@ -9,28 +9,6 @@ Version 2.0. See the repository root `LICENSE` file. Third-party notices =================== -fuser ------ - -`vendor/fuser` contains a modified copy of the fuser project: - -https://github.com/cberner/fuser - -Copyright its authors and contributors. It is distributed under the MIT -License; see `vendor/fuser/LICENSE.md`. - -libkrun, krun-vmm, and krun-devices ------------------------------------ - -`vendor/libkrun`, `vendor/krun-vmm`, and `vendor/krun-devices` contain pinned, -modified copies of libkrun 1.19.3 and its VMM/device crates: - -https://github.com/containers/libkrun - -Copyright Red Hat, Inc. and the upstream authors and contributors. They are -distributed under the Apache License, Version 2.0. Persisting's modifications -add the mountless copy-on-write virtio-fs backend used by pVisor. - agentgateway test fixtures -------------------------- diff --git a/README.md b/README.md index 6e13fe48a..e2827e2f9 100644 --- a/README.md +++ b/README.md @@ -8,29 +8,16 @@ Persisting logo -Persisting connects durable model state—parameters and KV caches—with durable -Agent history—trajectories and execution records. The current product is the -path from execution to queryable history: +Persisting contains **pChronicle**, which captures, browses, queries, exchanges, +and serves durable Agent trajectory Datasets. pVisor and pPilot have moved to +external repositories and are no longer built or distributed here. -| Governed execution | Durable history | -| --- | --- | -| pVisor | pChronicle | - - -- **`pvisor`** is an executor that produces persistable, reviewable facts: - staged Effects and execution records from one Agent Run; -- **`pchronicle`** browses, queries, exchanges, and serves trajectory Datasets. - -Each command works on its own. Connected, they cover `pvisor run --stage …` → -review/apply → configured capture → a queryable Dataset. - -![Current Persisting workflows and the execution-to-history throughline](docs/src/assets/diagrams/persisting/system-products.svg) +pChronicle ## Install ```bash pip install persisting[lance] -pvisor --version pchronicle --version ``` @@ -41,21 +28,7 @@ curl -fsSL https://raw.githubusercontent.com/DeepLink-org/Persisting/main/script ``` See the [installation guide](https://deeplink-org.github.io/Persisting/installation/) -for platform requirements and executor setup. - -## Run one Agent and review its changes - -```bash -pvisor run --stage ./runs/task-001 -- codex -pvisor review last -pvisor apply last --all # or: pvisor drop last -``` - -`--stage` creates a copy-on-write workspace view for review; without it the -Agent may write the real project tree. The exact boundary is -platform-dependent and recorded with the Run—consult the -[execution guide](https://deeplink-org.github.io/Persisting/pvisor/guides/execution/) -before treating it as a security boundary. +for supported platforms and setup. ## Query Agent trajectory history @@ -69,25 +42,22 @@ The onboarding flow creates a temporary example Dataset—no source checkout required. `pchronicle import` accepts ATIF, ACTF, and OpenAI Messages; `pchronicle serve` starts a loopback-only, read-only Dataset UI and API. -After capture is configured, selected pVisor Run events can enter a pChronicle -Dataset. See the [capture guide](https://deeplink-org.github.io/Persisting/pvisor/guides/capture/). +Gateway capture can write trajectory events into pChronicle Datasets. See the +[Gateway guide](https://deeplink-org.github.io/Persisting/pchronicle/guides/serve-gateway/). ## Current maturity | Capability | Status | |---|---| -| pVisor host execution, review, checkpoints, and transactional workspace | Implemented | | pChronicle local/S3 catalog, bounded SQL, analysis, find, import/export | Implemented | | pChronicle loopback-only read API and embedded Web UI | Implemented | | Gateway capture and cooperative proxy policy | Implemented | -| Container/libkrun executors and transparent network boundaries | Platform-dependent; see the pVisor and OverlayNet docs | | Queue and document Search | Separate stable capabilities | | Tensor Memory / TTAS | Experimental | ## Documentation - [Choose a workflow](https://deeplink-org.github.io/Persisting/overview/) — pick the entry point that matches your task -- [Run your first Agent](https://deeplink-org.github.io/Persisting/pvisor/get-started/) — the run-review-apply loop - [Explore durable history](https://deeplink-org.github.io/Persisting/pchronicle/get-started/) — browse and query a trajectory Dataset - [Project architecture](https://deeplink-org.github.io/Persisting/system-design/) — ownership and delivery boundaries diff --git a/benchmark/README.md b/benchmark/README.md index b340074e7..aea55dedf 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -1,6 +1,6 @@ # Persisting benchmarks -**仓库级黑盒与微基准入口:Gateway、pChronicle、pVisor,以及独立的 Queue 吞吐压测。** +**仓库级黑盒与微基准入口:Gateway、pChronicle,以及独立的 Queue 吞吐压测。** 拥有可复现的压测脚本与报告契约。不拥有被测组件的产品行为;Queue 子系统的语义以 Queue 文档为准,这里只保留既有压测入口。 @@ -34,20 +34,6 @@ just benchmark-pchronicle-compare \ 详见 [`pchronicle/`](pchronicle/README.md)。 -## pVisor - -进程启动与 durable Run Bundle 访问基准: - -```bash -just benchmark-pvisor -just benchmark-pvisor nightly target/pvisor-benchmark/nightly -just benchmark-pvisor-compare \ - target/pvisor-benchmark/candidate/raw-report.json \ - target/pvisor-benchmark/main/raw-report.json -``` - -详见 [`pvisor/`](pvisor/README.md)。 - ## Queue 本目录用于对 Persisting Queue 做吞吐压测,可配置生产者/消费者数量,测试极限吞吐。 @@ -102,6 +88,4 @@ python benchmark/throughput_stress.py -p 8 -c 8 -d 60 -b 100 --warmup 2 ## Links -- [Gateway architecture](../docs/src/pvisor/design/gateway.md) -- [pChronicle design](../docs/src/pchronicle/design/index.md) -- [pVisor design](../docs/src/pvisor/design/index.md) +- [pChronicle design](../docs/src/en/pchronicle/design/index.md) diff --git a/benchmark/gateway/README.md b/benchmark/gateway/README.md index adeab65a1..3d85af107 100644 --- a/benchmark/gateway/README.md +++ b/benchmark/gateway/README.md @@ -115,6 +115,6 @@ just benchmark-gateway-replay examples/data /tmp/gateway-replay-review ## Links -- [Gateway architecture](../../docs/src/pvisor/design/gateway.md) +- [Gateway architecture](../../docs/src/en/pchronicle/guides/serve-gateway.md) - [`persisting-gateway`](../../crates/persisting-gateway/README.md) - [Regression tests](../../tests/regression/README.md) diff --git a/benchmark/pvisor/README.md b/benchmark/pvisor/README.md deleted file mode 100644 index 1a74a1261..000000000 --- a/benchmark/pvisor/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# pVisor benchmark - -**Measures the process-level cost of a minimal host Run and of reading its -durable Run Bundle through `status --json` and `review --json`.** - -Owns the smoke / nightly suites and the `pvisor-benchmark/v1` report schema. -Does not own Run lifecycle or isolation backends. Every sampled Run must finish -successfully and produce a completed, zero-exit-code bundle; a fast but -incomplete Run is rejected. - -## Run - -```bash -just benchmark-pvisor -just benchmark-pvisor nightly target/pvisor-benchmark/nightly - -just benchmark-pvisor-compare \ - target/pvisor-benchmark/candidate/raw-report.json \ - target/pvisor-benchmark/main/raw-report.json -``` - -`just benchmark-pvisor-compare` takes candidate then optional baseline. The -`smoke` suite uses 2 warmups and 10 samples for pull-request feedback. The -`nightly` suite uses 10 warmups and 50 samples for a more stable distribution. -Both write the same `pvisor-benchmark/v1` raw schema and Markdown report. - -Baseline and candidate measurements are meaningful only when collected on the -same host with the same suite. Comparison marks a metric as a regression when -it crosses the configurable threshold (15% by default); the report is -informational unless the runner is explicitly given `--fail-on-regression`. - -Unit-test the report contract without running the suite: - -```bash -just test-pvisor-benchmark -``` - -## Links - -- [pVisor design](../../docs/src/pvisor/design/index.md) -- [`persisting-pvisor`](../../crates/persisting-pvisor/README.md) diff --git a/benchmark/pvisor/bench.py b/benchmark/pvisor/bench.py deleted file mode 100755 index adecdb242..000000000 --- a/benchmark/pvisor/bench.py +++ /dev/null @@ -1,545 +0,0 @@ -#!/usr/bin/env python3 -"""Run and compare the pVisor process-level benchmark suite.""" - -from __future__ import annotations - -import argparse -import datetime as dt -import json -import math -import os -import pathlib -import platform -import shlex -import statistics -import subprocess -import sys -import tempfile -import time -from typing import Any - -SCHEMA = "pvisor-benchmark/v1" -SUITES = { - "smoke": {"warmups": 2, "samples": 10}, - "nightly": {"warmups": 10, "samples": 50}, -} - - -def run_command( - command: list[str], - *, - cwd: pathlib.Path, - env: dict[str, str] | None = None, - log: pathlib.Path | None = None, -) -> subprocess.CompletedProcess[str]: - print(f"==> {shlex.join(command)}", flush=True) - completed = subprocess.run( - command, - cwd=cwd, - env=env, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - check=False, - ) - if log is not None: - log.parent.mkdir(parents=True, exist_ok=True) - log.write_text(completed.stdout, encoding="utf-8") - if completed.returncode != 0: - sys.stderr.write(completed.stdout) - raise RuntimeError( - f"command failed with exit code {completed.returncode}: {shlex.join(command)}" - ) - return completed - - -def command_output(command: list[str], cwd: pathlib.Path) -> str: - return run_command(command, cwd=cwd).stdout.strip() - - -def resolve_path(value: str, repo: pathlib.Path) -> pathlib.Path: - path = pathlib.Path(value) - return path.resolve() if path.is_absolute() else (repo / path).resolve() - - -def cpu_name() -> str: - cpuinfo = pathlib.Path("/proc/cpuinfo") - if cpuinfo.is_file(): - for line in cpuinfo.read_text(encoding="utf-8").splitlines(): - if line.lower().startswith("model name"): - return line.split(":", 1)[1].strip() - return platform.processor() or "unknown" - - -def environment(repo: pathlib.Path, suite: str) -> dict[str, Any]: - dirty = bool(command_output(["git", "status", "--porcelain"], repo)) - return { - "suite": suite, - "git_commit": command_output(["git", "rev-parse", "HEAD"], repo), - "git_dirty": dirty, - "recorded_at": dt.datetime.now(dt.timezone.utc).isoformat(), - "os": platform.system().lower(), - "os_release": platform.release(), - "arch": platform.machine(), - "cpu": cpu_name(), - "logical_cpus": os.cpu_count(), - "rustc": command_output(["rustc", "--version"], repo), - "cargo": command_output(["cargo", "--version"], repo), - "python": platform.python_version(), - } - - -def percentile(values: list[float], percent: float) -> float: - if not values: - raise ValueError("cannot calculate a percentile without samples") - ordered = sorted(values) - position = (len(ordered) - 1) * percent / 100 - lower = math.floor(position) - upper = math.ceil(position) - if lower == upper: - return ordered[lower] - weight = position - lower - return ordered[lower] * (1 - weight) + ordered[upper] * weight - - -def distribution(values: list[float]) -> dict[str, Any]: - return { - "samples": len(values), - "raw_ms": [round(value, 6) for value in values], - "min_ms": round(min(values), 6), - "mean_ms": round(statistics.fmean(values), 6), - "p50_ms": round(percentile(values, 50), 6), - "p95_ms": round(percentile(values, 95), 6), - "p99_ms": round(percentile(values, 99), 6), - "max_ms": round(max(values), 6), - "stdev_ms": round(statistics.pstdev(values), 6), - } - - -def measurement(value: float, unit: str, direction: str, source: str) -> dict[str, Any]: - return { - "value": round(value, 6), - "unit": unit, - "direction": direction, - "source": source, - } - - -def timed_command( - command: list[str], - *, - cwd: pathlib.Path, - env: dict[str, str], - capture_json: bool = False, -) -> tuple[float, dict[str, Any] | None]: - stdout: int | None = subprocess.PIPE if capture_json else subprocess.DEVNULL - started = time.perf_counter_ns() - completed = subprocess.run( - command, - cwd=cwd, - env=env, - stdin=subprocess.DEVNULL, - stdout=stdout, - stderr=subprocess.PIPE, - check=False, - ) - elapsed_ms = (time.perf_counter_ns() - started) / 1_000_000 - if completed.returncode != 0: - stderr = completed.stderr.decode(errors="replace") if completed.stderr else "" - raise RuntimeError( - f"benchmark command failed ({completed.returncode}): {shlex.join(command)}\n{stderr}" - ) - document = None - if capture_json: - try: - document = json.loads(completed.stdout) - except json.JSONDecodeError as error: - raise RuntimeError( - f"benchmark command did not emit JSON: {shlex.join(command)}" - ) from error - return elapsed_ms, document - - -def run_direct(binary: pathlib.Path, cwd: pathlib.Path, env: dict[str, str]) -> float: - elapsed, _ = timed_command([str(binary)], cwd=cwd, env=env) - return elapsed - - -def run_pvisor( - pvisor: pathlib.Path, - cwd: pathlib.Path, - run_home: pathlib.Path, - env: dict[str, str], -) -> tuple[float, pathlib.Path]: - before = set(run_home.glob("run-*")) if run_home.exists() else set() - elapsed, _ = timed_command( - [str(pvisor), "run", "--stdio", "capture", "--", "/usr/bin/true"], - cwd=cwd, - env=env, - ) - created = sorted(set(run_home.glob("run-*")) - before) - if len(created) != 1: - raise RuntimeError(f"expected one pVisor Run, found {len(created)}") - validate_bundle(created[0] / "run-bundle.json") - return elapsed, created[0] - - -def validate_bundle(path: pathlib.Path) -> dict[str, Any]: - document = json.loads(path.read_text(encoding="utf-8")) - run = document.get("run", {}) - if run.get("state") != "completed" or run.get("exit_code") != 0: - raise RuntimeError(f"pVisor benchmark produced an invalid Run Bundle: {path}") - return document - - -def sample_json_command( - command: list[str], - *, - cwd: pathlib.Path, - env: dict[str, str], - expected_state: str, -) -> float: - elapsed, document = timed_command(command, cwd=cwd, env=env, capture_json=True) - assert document is not None - if document.get("run", {}).get("state") != expected_state: - raise RuntimeError(f"unexpected Run state from {shlex.join(command)}") - return elapsed - - -def benchmark( - repo: pathlib.Path, - output: pathlib.Path, - target_dir: pathlib.Path, - suite: str, -) -> dict[str, Any]: - config = SUITES[suite] - output.mkdir(parents=True, exist_ok=True) - target_dir.mkdir(parents=True, exist_ok=True) - build_env = os.environ.copy() - build_env["CARGO_TARGET_DIR"] = str(target_dir) - run_command( - [ - "cargo", - "build", - "--release", - "--locked", - "-p", - "persisting-pvisor", - "--bin", - "pvisor", - ], - cwd=repo, - env=build_env, - log=output / "logs" / "build.log", - ) - pvisor = target_dir / "release" / "pvisor" - direct_binary = pathlib.Path("/usr/bin/true") - if not pvisor.is_file() or not direct_binary.is_file(): - raise RuntimeError("benchmark binaries are unavailable") - - with tempfile.TemporaryDirectory(prefix="pvisor-benchmark-") as temporary: - work = pathlib.Path(temporary) - workspace = work / "workspace" - run_home = work / "runs" - workspace.mkdir() - run_env = os.environ.copy() - run_env["PERSISTING_RUN_HOME"] = str(run_home) - - last_run: pathlib.Path | None = None - for _ in range(config["warmups"]): - run_direct(direct_binary, workspace, run_env) - _, last_run = run_pvisor(pvisor, workspace, run_home, run_env) - - direct_samples: list[float] = [] - pvisor_samples: list[float] = [] - for _ in range(config["samples"]): - direct_samples.append(run_direct(direct_binary, workspace, run_env)) - elapsed, last_run = run_pvisor(pvisor, workspace, run_home, run_env) - pvisor_samples.append(elapsed) - - assert last_run is not None - status_command = [str(pvisor), "status", "--json", str(last_run)] - review_command = [str(pvisor), "review", "--json", str(last_run)] - sample_json_command( - status_command, - cwd=workspace, - env=run_env, - expected_state="completed", - ) - sample_json_command( - review_command, - cwd=workspace, - env=run_env, - expected_state="completed", - ) - status_samples = [ - sample_json_command( - status_command, - cwd=workspace, - env=run_env, - expected_state="completed", - ) - for _ in range(config["samples"]) - ] - review_samples = [ - sample_json_command( - review_command, - cwd=workspace, - env=run_env, - expected_state="completed", - ) - for _ in range(config["samples"]) - ] - bundle_bytes = (last_run / "run-bundle.json").stat().st_size - - distributions = { - "direct": distribution(direct_samples), - "host_run": distribution(pvisor_samples), - "status": distribution(status_samples), - "review": distribution(review_samples), - } - direct_p50 = distributions["direct"]["p50_ms"] - pvisor_p50 = distributions["host_run"]["p50_ms"] - overhead = max(0.0, pvisor_p50 - direct_p50) - ratio = pvisor_p50 / direct_p50 if direct_p50 else 0.0 - document = { - "schema": SCHEMA, - "environment": environment(repo, suite), - "suite_config": config, - "measurements": { - "process": { - "direct": { - "latency_ms_p50": measurement(direct_p50, "ms", "lower", "wall-clock"), - "latency_ms_p95": measurement( - distributions["direct"]["p95_ms"], - "ms", - "lower", - "wall-clock", - ), - }, - "host_run": { - "latency_ms_p50": measurement(pvisor_p50, "ms", "lower", "wall-clock"), - "latency_ms_p95": measurement( - distributions["host_run"]["p95_ms"], - "ms", - "lower", - "wall-clock", - ), - "overhead_ms_p50": measurement(overhead, "ms", "lower", "derived"), - "overhead_ratio_p50": measurement(ratio, "x", "lower", "derived"), - }, - }, - "bundle": { - "status": { - "latency_ms_p50": measurement( - distributions["status"]["p50_ms"], - "ms", - "lower", - "wall-clock", - ) - }, - "review": { - "latency_ms_p50": measurement( - distributions["review"]["p50_ms"], - "ms", - "lower", - "wall-clock", - ) - }, - "run_bundle_bytes": measurement( - float(bundle_bytes), "bytes", "lower", "filesystem" - ), - }, - }, - "distributions": distributions, - } - return document - - -def measurement_items(document: dict[str, Any]) -> dict[str, dict[str, Any]]: - found: dict[str, dict[str, Any]] = {} - - def visit(node: Any, segments: list[str]) -> None: - if isinstance(node, dict) and {"value", "unit", "direction", "source"} <= set(node): - found["/".join(segments)] = node - return - if not isinstance(node, dict): - raise ValueError("measurement tree contains a non-measurement leaf") - for key in sorted(node): - visit(node[key], [*segments, key]) - - visit(document["measurements"], []) - return found - - -def render_run_report(document: dict[str, Any]) -> str: - environment_data = document["environment"] - lines = [ - "# pVisor benchmark", - "", - f"- Suite: `{environment_data['suite']}`", - f"- Commit: `{environment_data['git_commit']}`", - f"- Host: `{environment_data['os']} {environment_data['arch']}` / " - f"`{environment_data['cpu']}`", - f"- Samples: `{document['suite_config']['samples']}` after " - f"`{document['suite_config']['warmups']}` warmups", - "", - "| Metric | Value | Unit |", - "|---|---:|---|", - ] - for path, item in measurement_items(document).items(): - lines.append(f"| `{path}` | {item['value']:.6g} | {item['unit']} |") - lines.extend( - [ - "", - "Every measured `pvisor run` must exit successfully and produce a " - "completed, zero-exit-code Run Bundle.", - "", - ] - ) - return "\n".join(lines) - - -def compare_reports( - candidate: dict[str, Any], - baseline: dict[str, Any] | None, - threshold: float, -) -> dict[str, Any]: - if baseline is not None: - baseline_environment = baseline["environment"] - candidate_environment = candidate["environment"] - for key in ("suite", "os", "arch", "cpu"): - if baseline_environment.get(key) != candidate_environment.get(key): - raise ValueError( - f"cannot compare reports with different environment.{key}: " - f"{baseline_environment.get(key)!r} != " - f"{candidate_environment.get(key)!r}" - ) - candidate_items = measurement_items(candidate) - baseline_items = measurement_items(baseline) if baseline is not None else {} - rows = [] - regressions = 0 - for path, current in candidate_items.items(): - previous = baseline_items.get(path) - delta_pct = None - status = "candidate-only" - if previous is not None: - if previous["unit"] != current["unit"]: - raise ValueError(f"unit changed for {path}") - baseline_value = float(previous["value"]) - candidate_value = float(current["value"]) - if baseline_value != 0: - delta_pct = (candidate_value - baseline_value) / baseline_value * 100 - signed = delta_pct if current["direction"] == "lower" else -delta_pct - if signed > threshold: - status = "regression" - regressions += 1 - elif signed < -threshold: - status = "improvement" - else: - status = "stable" - else: - status = "stable" if candidate_value == 0 else "changed" - rows.append( - { - "path": path, - "baseline": previous["value"] if previous is not None else None, - "candidate": current["value"], - "unit": current["unit"], - "delta_pct": round(delta_pct, 3) if delta_pct is not None else None, - "status": status, - } - ) - return { - "schema": "pvisor-benchmark-comparison/v1", - "threshold_pct": threshold, - "regressions": regressions, - "baseline_commit": baseline["environment"]["git_commit"] if baseline is not None else None, - "candidate_commit": candidate["environment"]["git_commit"], - "metrics": rows, - } - - -def render_comparison(document: dict[str, Any]) -> str: - lines = [ - "# pVisor benchmark comparison", - "", - f"Regression threshold: `{document['threshold_pct']:.3g}%`; " - f"regressions: `{document['regressions']}`.", - "", - "| Metric | Baseline | Candidate | Delta | Status |", - "|---|---:|---:|---:|---|", - ] - for row in document["metrics"]: - baseline = "—" if row["baseline"] is None else f"{row['baseline']:.6g}" - delta = "—" if row["delta_pct"] is None else f"{row['delta_pct']:+.2f}%" - lines.append( - f"| `{row['path']}` | {baseline} | {row['candidate']:.6g} " - f"{row['unit']} | {delta} | {row['status']} |" - ) - lines.append("") - return "\n".join(lines) - - -def load_report(path: pathlib.Path) -> dict[str, Any]: - document = json.loads(path.read_text(encoding="utf-8")) - if document.get("schema") != SCHEMA: - raise ValueError(f"unsupported pVisor benchmark schema in {path}") - measurement_items(document) - return document - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - subparsers = parser.add_subparsers(dest="command", required=True) - - run_parser = subparsers.add_parser("run") - run_parser.add_argument("--repo", default=str(pathlib.Path(__file__).parents[2])) - run_parser.add_argument("--suite", choices=sorted(SUITES), default="smoke") - run_parser.add_argument("--output", required=True) - run_parser.add_argument("--target-dir", default="target/pvisor-benchmark-build") - - compare_parser = subparsers.add_parser("compare") - compare_parser.add_argument("--repo", default=str(pathlib.Path(__file__).parents[2])) - compare_parser.add_argument("--candidate", required=True) - compare_parser.add_argument("--baseline", default="") - compare_parser.add_argument("--output", required=True) - compare_parser.add_argument("--regression-threshold", type=float, default=15.0) - compare_parser.add_argument("--fail-on-regression", action="store_true") - return parser.parse_args() - - -def main() -> int: - args = parse_args() - repo = pathlib.Path(args.repo).resolve() - output = resolve_path(args.output, repo) - output.mkdir(parents=True, exist_ok=True) - if args.command == "run": - target_dir = resolve_path(args.target_dir, repo) - document = benchmark(repo, output, target_dir, args.suite) - (output / "raw-report.json").write_text( - json.dumps(document, indent=2, sort_keys=True) + "\n", encoding="utf-8" - ) - (output / "report.md").write_text(render_run_report(document), encoding="utf-8") - print(f"pVisor benchmark report: {output / 'report.md'}") - return 0 - - candidate = load_report(resolve_path(args.candidate, repo)) - baseline = load_report(resolve_path(args.baseline, repo)) if args.baseline else None - comparison = compare_reports(candidate, baseline, args.regression_threshold) - (output / "comparison.json").write_text( - json.dumps(comparison, indent=2, sort_keys=True) + "\n", encoding="utf-8" - ) - (output / "report.md").write_text(render_comparison(comparison), encoding="utf-8") - print(f"pVisor comparison report: {output / 'report.md'}") - if args.fail_on_regression and comparison["regressions"]: - return 1 - return 0 - - -if __name__ == "__main__": - try: - raise SystemExit(main()) - except (OSError, RuntimeError, ValueError) as error: - print(f"error: {error}", file=sys.stderr) - raise SystemExit(1) from error diff --git a/benchmark/pvisor/run.sh b/benchmark/pvisor/run.sh deleted file mode 100755 index 29a6469d1..000000000 --- a/benchmark/pvisor/run.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -repo_root="$(cd -- "$script_dir/../.." && pwd)" - -exec python3 "$script_dir/bench.py" "$@" --repo "$repo_root" diff --git a/benchmark/pvisor/test_bench.py b/benchmark/pvisor/test_bench.py deleted file mode 100755 index d103d39b2..000000000 --- a/benchmark/pvisor/test_bench.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -import importlib.util -import pathlib -import unittest - -MODULE_PATH = pathlib.Path(__file__).with_name("bench.py") -SPEC = importlib.util.spec_from_file_location("pvisor_bench", MODULE_PATH) -assert SPEC is not None and SPEC.loader is not None -BENCH = importlib.util.module_from_spec(SPEC) -SPEC.loader.exec_module(BENCH) - - -def report(value: float) -> dict: - return { - "schema": BENCH.SCHEMA, - "environment": { - "git_commit": "commit", - "suite": "smoke", - "os": "linux", - "arch": "x86_64", - "cpu": "test-cpu", - }, - "measurements": { - "process": { - "host_run": { - "latency_ms_p50": BENCH.measurement(value, "ms", "lower", "wall-clock") - } - } - }, - } - - -class PVisorBenchmarkTests(unittest.TestCase): - def test_percentile_interpolates(self) -> None: - self.assertEqual(BENCH.percentile([1.0, 2.0, 3.0, 4.0], 50), 2.5) - self.assertAlmostEqual(BENCH.percentile([1.0, 2.0], 95), 1.95) - - def test_comparison_classifies_lower_is_better(self) -> None: - comparison = BENCH.compare_reports(report(120), report(100), 15) - self.assertEqual(comparison["regressions"], 1) - self.assertEqual(comparison["metrics"][0]["status"], "regression") - - def test_candidate_only_report_is_supported(self) -> None: - comparison = BENCH.compare_reports(report(100), None, 15) - self.assertEqual(comparison["regressions"], 0) - self.assertEqual(comparison["metrics"][0]["status"], "candidate-only") - - def test_different_suites_cannot_be_compared(self) -> None: - candidate = report(100) - baseline = report(100) - baseline["environment"]["suite"] = "nightly" - with self.assertRaisesRegex(ValueError, "environment.suite"): - BENCH.compare_reports(candidate, baseline, 15) - - -if __name__ == "__main__": - unittest.main() diff --git a/crates/persisting-agentctl/Cargo.toml b/crates/persisting-agentctl/Cargo.toml index b89a62b1f..8d1f8b6f1 100644 --- a/crates/persisting-agentctl/Cargo.toml +++ b/crates/persisting-agentctl/Cargo.toml @@ -8,13 +8,9 @@ description = "Agent control contracts, policies, wire protocol, and client SDK" [dependencies] anyhow.workspace = true -libc.workspace = true ipnet.workspace = true serde = { workspace = true, features = ["derive"] } serde_json.workspace = true -tempfile.workspace = true -tokio = { workspace = true, features = ["fs", "io-util", "macros", "process", "rt", "time"] } -tokio-util = { workspace = true, features = ["rt"] } [dev-dependencies] proptest.workspace = true diff --git a/crates/persisting-agentctl/README.md b/crates/persisting-agentctl/README.md index 5390f02d5..9d5c11367 100644 --- a/crates/persisting-agentctl/README.md +++ b/crates/persisting-agentctl/README.md @@ -1,5 +1,8 @@ # persisting-agentctl +pVisor and pPilot are maintained in external repositories. This repository +ships pChronicle and the internal libraries needed for capture and history. + **Agent control contracts, policies, the versioned AgentCtl v1 protocol, and its synchronous client SDK.** @@ -65,7 +68,7 @@ just test persisting-agentctl ## Links -- [pVisor isolation architecture](../../docs/src/pvisor/design/isolation.md) -- [OverlayNet architecture](../../docs/src/pvisor/design/overlaynet.md) -- [System architecture](../../docs/src/system-design/architecture.md) -- [`persisting-pvisor`](../persisting-pvisor/README.md) +- pVisor isolation architecture (external repository) +- OverlayNet architecture (external repository) +- [System architecture](../../docs/src/en/system-design/architecture.md) +- `persisting-pvisor` (external repository) diff --git a/crates/persisting-agentctl/src/lib.rs b/crates/persisting-agentctl/src/lib.rs index 900b6fedb..038560048 100644 --- a/crates/persisting-agentctl/src/lib.rs +++ b/crates/persisting-agentctl/src/lib.rs @@ -4,20 +4,16 @@ //! [`ControlController`]. Authorization is represented as a state transition; //! the driver then records whether the authorized operation was applied or //! failed. [`AgentCtlClient`] implements pVisor's optional cooperative -//! AgentCtl protocol, while -//! [`PVisorProcessClient`] submits a [`RunSpec`] to a standalone foreground -//! pVisor binary. Supervisor messages are shared wire contracts rather than +//! AgentCtl protocol. Supervisor messages are shared wire contracts rather than //! types owned by either pPilot or pVisor. mod client; -mod process; pub mod protocol; mod runtime; mod supervisor; pub use client::{AgentCtlClient, AgentCtlClientConfig, AgentCtlResponseError}; use ipnet::IpNet; -pub use process::{PVisorProcessClient, PVisorProcessOptions}; pub use protocol::*; pub use runtime::*; pub use runtime::{AccessEffect as ControlEffect, AccessReason as ControlReason}; diff --git a/crates/persisting-agentctl/src/process.rs b/crates/persisting-agentctl/src/process.rs deleted file mode 100644 index e8e0f7b86..000000000 --- a/crates/persisting-agentctl/src/process.rs +++ /dev/null @@ -1,192 +0,0 @@ -//! Foreground pVisor binary client used by control-plane components. - -use crate::{RunResult, RunSpec}; -use anyhow::{Context, Result}; -use serde::Deserialize; -use std::ffi::OsString; -use std::path::{Path, PathBuf}; -use std::process::Stdio; -use std::time::Duration; -use tokio::io::AsyncReadExt; -use tokio::process::{Child, Command}; -use tokio_util::sync::CancellationToken; - -#[derive(Debug, Clone)] -pub struct PVisorProcessClient { - binary: PathBuf, -} - -#[derive(Debug, Clone, Default)] -pub struct PVisorProcessOptions { - /// Root under which pVisor stores `/`, passed as a per-Run `--stage`. - pub run_home: Option, - /// Additional `pvisor run` switches, before `--spec`. - pub run_args: Vec, -} - -#[derive(Deserialize)] -struct DelegatedRunOutput { - result: RunResult, -} - -impl PVisorProcessClient { - pub fn new(binary: impl Into) -> Self { - let binary = binary.into(); - let binary = if binary.components().count() == 1 { - std::env::current_exe() - .ok() - .and_then(|current| { - let parent = current.parent()?; - let sibling = parent.join(&binary); - if sibling.is_file() { - return Some(sibling); - } - parent - .parent() - .map(|target_profile| target_profile.join(&binary)) - .filter(|candidate| candidate.is_file()) - }) - .unwrap_or(binary) - } else { - binary - }; - Self { binary } - } - - pub fn binary(&self) -> &Path { - &self.binary - } - - pub async fn run( - &self, - spec: &RunSpec, - options: &PVisorProcessOptions, - cancellation: CancellationToken, - ) -> Result { - let control = tempfile::Builder::new() - .prefix("persisting-pvisor-client-") - .tempdir() - .context("create pVisor control directory")?; - let spec_path = control.path().join("run-spec.json"); - let result_path = control.path().join("run-result.json"); - write_private_json(&spec_path, spec).await?; - - let mut command = self.command(spec, options, &spec_path, &result_path)?; - let mut child = command - .spawn() - .with_context(|| format!("spawn pVisor binary {}", self.binary.display()))?; - let stdout = child.stdout.take(); - let stderr = child.stderr.take(); - let stdout_task = tokio::spawn(read_pipe(stdout)); - let stderr_task = tokio::spawn(read_pipe(stderr)); - - let status = tokio::select! { - status = child.wait() => status.context("wait for pVisor process")?, - _ = cancellation.cancelled() => terminate(&mut child).await?, - }; - let stdout = stdout_task.await.context("join pVisor stdout reader")??; - let stderr = stderr_task.await.context("join pVisor stderr reader")??; - - let output = match tokio::fs::read(&result_path).await { - Ok(bytes) => serde_json::from_slice::(&bytes) - .context("decode pVisor RunResult")?, - Err(error) => { - anyhow::bail!( - "pVisor exited with {status} without a RunResult ({error}); stdout: {}; stderr: {}", - String::from_utf8_lossy(&stdout).trim(), - String::from_utf8_lossy(&stderr).trim(), - ) - } - }; - Ok(output.result) - } - - fn command( - &self, - spec: &RunSpec, - options: &PVisorProcessOptions, - spec_path: &Path, - result_path: &Path, - ) -> Result { - let mut command = Command::new(&self.binary); - command.arg("run").args(&options.run_args); - command - .arg("--spec") - .arg(spec_path) - .arg("--result-file") - .arg(result_path) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .kill_on_drop(true); - if let Some(run_home) = &options.run_home { - // RunId is an opaque protocol identifier, not a trusted path. - // Preserve the historical / layout without - // allowing an ID to escape the caller's storage root. - let run_id = spec.run_id.as_str(); - anyhow::ensure!( - !run_id.is_empty() - && run_id != "." - && run_id != ".." - && !run_id.contains(['/', '\\', '\0']), - "Run ID must be a single directory name for pVisor stage storage" - ); - // Absolute paths cannot be confused with --stage drop/drop:... - // and remain independent of the Agent's cwd in the JSON spec. - let run_home = if run_home.is_absolute() { - run_home.clone() - } else { - std::env::current_dir() - .context("resolve pVisor Run storage root")? - .join(run_home) - }; - command.arg("--stage").arg(run_home.join(run_id)); - } - Ok(command) - } -} - -async fn read_pipe(pipe: Option) -> std::io::Result> -where - R: tokio::io::AsyncRead + Unpin, -{ - let mut bytes = Vec::new(); - if let Some(mut pipe) = pipe { - pipe.read_to_end(&mut bytes).await?; - } - Ok(bytes) -} - -async fn terminate(child: &mut Child) -> Result { - #[cfg(unix)] - if let Some(pid) = child.id() { - // SAFETY: `pid` belongs to the live child and SIGTERM is the delegated - // pVisor shutdown contract, allowing it to publish a cancelled result. - let sent = unsafe { libc::kill(pid as libc::pid_t, libc::SIGTERM) }; - if sent != 0 { - child.start_kill().context("kill pVisor process")?; - } - } - #[cfg(not(unix))] - child.start_kill().context("kill pVisor process")?; - - match tokio::time::timeout(Duration::from_secs(5), child.wait()).await { - Ok(status) => status.context("wait for terminating pVisor process"), - Err(_) => { - child.start_kill().context("force-kill pVisor process")?; - child.wait().await.context("wait for killed pVisor process") - } - } -} - -async fn write_private_json(path: &Path, value: &impl serde::Serialize) -> Result<()> { - tokio::fs::write(path, serde_json::to_vec_pretty(value)?) - .await - .with_context(|| format!("write {}", path.display()))?; - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - tokio::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)).await?; - } - Ok(()) -} diff --git a/crates/persisting-gateway/README.md b/crates/persisting-gateway/README.md index dae41e7c9..791846837 100644 --- a/crates/persisting-gateway/README.md +++ b/crates/persisting-gateway/README.md @@ -1,6 +1,6 @@ # persisting-gateway -**pVisor's built-in Agent protocol driver: LLM HTTP forwarding plus canonical +**pChronicle Agent protocol gateway: LLM HTTP forwarding and canonical trajectory capture.** Owns the application-level path from Agent/LLM HTTP exchanges to trajectory @@ -14,9 +14,8 @@ transport, access enforcement, and generic sink dispatch. [`persisting-pchronicle`](../persisting-pchronicle/README.md) owns schemas, persistence, reading, replay, conversion, and derived views. -Capture remains the user-facing capability. It runs through `pvisor run` or -`pchronicle serve --gateway-config`. Gateway is an internal pVisor driver and a -reusable crate, not a peer product or standalone service. +Capture runs through `pchronicle serve --gateway-config`. Gateway is an internal +library; external execution components can also integrate it. This crate implements `persisting-overlaynet::OverlaySink`. Protocol rendering and capture share one in-memory `LlmRequestEventPayload` (`llm/v1`). Provider @@ -38,8 +37,6 @@ benchmarks and regressions. It does not start Gateway itself. ## Links -- [Gateway architecture](../../docs/src/pvisor/design/gateway.md) -- [Capture trajectories](../../docs/src/pvisor/guides/capture.md) -- [Gateway forwarding, rewriting, and capture](../../docs/src/pchronicle/guides/serve-gateway.md) +- [Gateway forwarding, rewriting, and capture](../../docs/src/en/pchronicle/guides/serve-gateway.md) - [`persisting-overlaynet`](../persisting-overlaynet/README.md) - [`persisting-pchronicle`](../persisting-pchronicle/README.md) diff --git a/crates/persisting-overlay-core/Cargo.toml b/crates/persisting-overlay-core/Cargo.toml deleted file mode 100644 index 52cca78ed..000000000 --- a/crates/persisting-overlay-core/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "persisting-overlay-core" -version.workspace = true -edition.workspace = true -authors.workspace = true -license.workspace = true -description = "FUSE-neutral portable overlay filesystem core for pVisor" - -[dependencies] -libc.workspace = true -log.workspace = true -serde = { workspace = true, features = ["derive"] } -serde_json.workspace = true -sha2.workspace = true - -[dev-dependencies] -tempfile.workspace = true diff --git a/crates/persisting-overlay-core/src/core.rs b/crates/persisting-overlay-core/src/core.rs deleted file mode 100644 index 988cd3798..000000000 --- a/crates/persisting-overlay-core/src/core.rs +++ /dev/null @@ -1,1410 +0,0 @@ -use crate::sys; -use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; -use std::collections::{BTreeSet, HashMap}; -use std::ffi::{OsStr, OsString}; -use std::fs::{self, File, Metadata, OpenOptions}; -use std::io::{self, Write}; -use std::os::unix::ffi::{OsStrExt, OsStringExt}; -use std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt}; -use std::path::{Component, Path, PathBuf}; -use std::sync::Mutex; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::time::{Duration, UNIX_EPOCH}; - -pub const WHITEOUT_PREFIX: &str = ".wh."; -pub const OPAQUE_NAME: &str = ".wh..wh..opq"; -const TEMP_PREFIX: &str = ".wh..persisting-copyup-"; -const PREIMAGE_COMPLETE_MARKER: &str = "complete-v1"; -const OPAQUE_XATTRS: [&str; 3] = [ - "trusted.overlay.opaque", - "user.overlay.opaque", - "user.fuseoverlayfs.opaque", -]; - -static TEMP_ID: AtomicU64 = AtomicU64::new(1); - -#[derive(Clone, Debug)] -pub struct Resolved { - pub path: PathBuf, - pub is_upper: bool, -} - -#[derive(Debug)] -pub struct OverlayCore { - lowers: Vec, - upper: PathBuf, - work: Option, - excluded: BTreeSet, - copied_hard_links: Mutex>, - preimage_dir: Option, - preimage_lock: Mutex<()>, -} - -/// Durable first-touch state of one apply target path. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct PathPreimage { - /// Raw Unix path bytes relative to the overlay root. - pub path: Vec, - pub state: PathFingerprint, -} - -impl PathPreimage { - pub fn relative_path(&self) -> PathBuf { - PathBuf::from(OsString::from_vec(self.path.clone())) - } -} - -/// Content and metadata relevant to detecting a destructive apply conflict. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum PathFingerprint { - Absent, - File { - sha256: String, - mode: u32, - uid: u32, - gid: u32, - }, - Directory { - mode: u32, - uid: u32, - gid: u32, - mtime_seconds: i64, - mtime_nanoseconds: i64, - }, - Symlink { - target: Vec, - uid: u32, - gid: u32, - }, - Other { - mode: u32, - uid: u32, - gid: u32, - rdev: u64, - }, -} - -fn error(errno: i32) -> io::Error { - io::Error::from_raw_os_error(errno) -} - -fn exists(path: &Path) -> bool { - fs::symlink_metadata(path).is_ok() -} - -fn ignorable_metadata_error(err: &io::Error) -> bool { - matches!( - err.raw_os_error(), - Some(libc::EPERM) | Some(libc::EACCES) | Some(libc::ENOTSUP) - ) -} - -fn ignorable_ownership_error(err: &io::Error) -> bool { - // A uid or gid outside the current user namespace is reported as EINVAL. - // Ownership is best-effort for an unprivileged overlay, just like EPERM. - ignorable_metadata_error(err) || err.raw_os_error() == Some(libc::EINVAL) -} - -fn sha256_hex(bytes: &[u8]) -> String { - use std::fmt::Write as _; - - let digest = Sha256::digest(bytes); - let mut encoded = String::with_capacity(digest.len() * 2); - for byte in digest { - let _ = write!(&mut encoded, "{byte:02x}"); - } - encoded -} - -/// Fingerprint one path without following its final symlink. -pub fn fingerprint_at(root: &Path, rel: &Path) -> io::Result { - OverlayCore::validate_rel(rel)?; - let path = if rel.as_os_str().is_empty() { - root.to_path_buf() - } else { - root.join(rel) - }; - let metadata = match fs::symlink_metadata(&path) { - Ok(metadata) => metadata, - Err(error) if error.kind() == io::ErrorKind::NotFound => { - return Ok(PathFingerprint::Absent); - } - Err(error) => return Err(error), - }; - let kind = metadata.file_type(); - if kind.is_file() { - return Ok(PathFingerprint::File { - sha256: sha256_hex(&fs::read(&path)?), - mode: metadata.mode(), - uid: metadata.uid(), - gid: metadata.gid(), - }); - } - if kind.is_dir() { - return Ok(PathFingerprint::Directory { - mode: metadata.mode(), - uid: metadata.uid(), - gid: metadata.gid(), - mtime_seconds: metadata.mtime(), - mtime_nanoseconds: metadata.mtime_nsec(), - }); - } - if kind.is_symlink() { - return Ok(PathFingerprint::Symlink { - target: fs::read_link(&path)?.into_os_string().into_vec(), - uid: metadata.uid(), - gid: metadata.gid(), - }); - } - Ok(PathFingerprint::Other { - mode: metadata.mode(), - uid: metadata.uid(), - gid: metadata.gid(), - rdev: metadata.rdev(), - }) -} - -/// Load all durable first-touch entries from a preimage journal. -pub fn load_preimages(directory: &Path) -> io::Result> { - let entries = directory.join("entries"); - let iterator = match fs::read_dir(&entries) { - Ok(iterator) => iterator, - Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()), - Err(error) => return Err(error), - }; - let mut preimages = Vec::new(); - for entry in iterator { - let entry = entry?; - if entry.path().extension() != Some(OsStr::new("json")) { - continue; - } - let preimage = serde_json::from_slice::(&fs::read(entry.path())?) - .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?; - OverlayCore::validate_rel(&preimage.relative_path())?; - preimages.push(preimage); - } - preimages.sort_by(|left, right| left.path.cmp(&right.path)); - Ok(preimages) -} - -pub fn preimage_journal_is_complete(directory: &Path) -> bool { - directory.join(PREIMAGE_COMPLETE_MARKER).is_file() -} - -/// Consume journal entries after their corresponding target paths commit. -pub fn remove_preimages(directory: &Path, paths: &[PathBuf]) -> io::Result<()> { - let entries = directory.join("entries"); - for path in paths { - OverlayCore::validate_rel(path)?; - let journal_path = - entries.join(format!("{}.json", sha256_hex(path.as_os_str().as_bytes()))); - match fs::remove_file(journal_path) { - Ok(()) => {} - Err(error) if error.kind() == io::ErrorKind::NotFound => {} - Err(error) => return Err(error), - } - } - match File::open(entries) { - Ok(directory) => directory.sync_all(), - Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()), - Err(error) => Err(error), - } -} - -impl OverlayCore { - pub fn new(lowers: Vec, upper: PathBuf, work: Option) -> io::Result { - Self::new_with_exclusions(lowers, upper, work, Vec::new()) - } - - pub fn new_with_exclusions( - lowers: Vec, - upper: PathBuf, - work: Option, - excluded: Vec, - ) -> io::Result { - Self::new_with_exclusions_and_preimages(lowers, upper, work, excluded, None) - } - - pub fn new_with_exclusions_and_preimages( - lowers: Vec, - upper: PathBuf, - work: Option, - excluded: Vec, - preimage_dir: Option, - ) -> io::Result { - if lowers.is_empty() { - return Err(error(libc::EINVAL)); - } - for lower in &lowers { - if !lower.is_dir() { - return Err(error(libc::ENOTDIR)); - } - } - fs::create_dir_all(&upper)?; - let upper_was_empty = fs::read_dir(&upper)?.next().is_none(); - if let Some(work) = &work { - fs::create_dir_all(work)?; - if work == &upper || work.starts_with(&upper) || upper.starts_with(work) { - return Err(error(libc::EINVAL)); - } - if fs::metadata(work)?.dev() != fs::metadata(&upper)?.dev() { - return Err(error(libc::EXDEV)); - } - for entry in fs::read_dir(work)? { - let entry = entry?; - if entry - .file_name() - .as_bytes() - .starts_with(TEMP_PREFIX.as_bytes()) - { - let path = entry.path(); - if fs::symlink_metadata(&path)?.is_dir() { - fs::remove_dir_all(path)?; - } else { - fs::remove_file(path)?; - } - } - } - } - let excluded = excluded - .into_iter() - .map(|path| { - Self::validate_rel(&path)?; - if path.as_os_str().is_empty() { - return Err(error(libc::EINVAL)); - } - Ok(path) - }) - .collect::>>()?; - if let Some(directory) = &preimage_dir { - fs::create_dir_all(directory.join("entries"))?; - if upper_was_empty && !preimage_journal_is_complete(directory) { - let marker = directory.join(PREIMAGE_COMPLETE_MARKER); - let mut file = OpenOptions::new() - .write(true) - .create_new(true) - .mode(0o600) - .open(marker)?; - file.write_all(b"pvisor-overlay-preimage-journal-v1\n")?; - file.sync_all()?; - File::open(directory)?.sync_all()?; - } - } - let core = Self { - lowers, - upper, - work, - excluded, - copied_hard_links: Mutex::new(HashMap::new()), - preimage_dir, - preimage_lock: Mutex::new(()), - }; - if fs::read_dir(&core.upper)?.next().is_none() - && let Some(root) = core.lowers.first() - { - let metadata = fs::symlink_metadata(root)?; - core.copy_metadata(root, &core.upper, &metadata)?; - } - Ok(core) - } - - fn record_preimage(&self, rel: &Path) -> io::Result<()> { - let Some(directory) = &self.preimage_dir else { - return Ok(()); - }; - Self::validate_rel(rel)?; - let _guard = self - .preimage_lock - .lock() - .map_err(|_| io::Error::other("preimage journal lock poisoned"))?; - let path_bytes = rel.as_os_str().as_bytes(); - let destination = directory - .join("entries") - .join(format!("{}.json", sha256_hex(path_bytes))); - match fs::symlink_metadata(&destination) { - Ok(_) => return Ok(()), - Err(error) if error.kind() == io::ErrorKind::NotFound => {} - Err(error) => return Err(error), - } - let target = self.lowers.last().ok_or_else(|| error(libc::EINVAL))?; - let preimage = PathPreimage { - path: path_bytes.to_vec(), - state: fingerprint_at(target, rel)?, - }; - let body = serde_json::to_vec_pretty(&preimage) - .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?; - let mut file = OpenOptions::new() - .write(true) - .create_new(true) - .mode(0o600) - .open(&destination)?; - file.write_all(&body)?; - file.sync_all()?; - File::open(directory.join("entries"))?.sync_all() - } - - fn record_logical_tree_mapping(&self, source: &Path, destination: &Path) -> io::Result<()> { - self.record_preimage(destination)?; - if !self.metadata(source)?.is_dir() { - return Ok(()); - } - for name in self.list_names(source)? { - let source_child = Self::child(source, &name)?; - let destination_child = Self::child(destination, &name)?; - self.record_logical_tree_mapping(&source_child, &destination_child)?; - } - Ok(()) - } - - pub fn upper(&self) -> &Path { - &self.upper - } - - fn is_excluded(&self, rel: &Path) -> bool { - self.excluded - .iter() - .any(|prefix| rel == prefix || rel.starts_with(prefix)) - } - - fn require_visible(&self, rel: &Path) -> io::Result<()> { - Self::validate_rel(rel)?; - if self.is_excluded(rel) { - return Err(error(libc::ENOENT)); - } - Ok(()) - } - - pub fn validate_rel(rel: &Path) -> io::Result<()> { - if rel.is_absolute() - || rel - .components() - .any(|component| !matches!(component, Component::Normal(_))) - { - return Err(error(libc::EINVAL)); - } - Ok(()) - } - - pub fn validate_name(name: &OsStr) -> io::Result<()> { - let bytes = name.as_bytes(); - if bytes.is_empty() - || bytes == b"." - || bytes == b".." - || bytes.contains(&b'/') - || bytes.starts_with(WHITEOUT_PREFIX.as_bytes()) - { - return Err(error(libc::EINVAL)); - } - Ok(()) - } - - pub fn child(parent: &Path, name: &OsStr) -> io::Result { - Self::validate_rel(parent)?; - Self::validate_name(name)?; - Ok(if parent.as_os_str().is_empty() { - PathBuf::from(name) - } else { - parent.join(name) - }) - } - - pub fn upper_path(&self, rel: &Path) -> PathBuf { - if rel.as_os_str().is_empty() { - self.upper.clone() - } else { - self.upper.join(rel) - } - } - - fn whiteout_path(&self, parent: &Path, name: &OsStr) -> PathBuf { - let mut marker = OsString::from(WHITEOUT_PREFIX); - marker.push(name); - self.upper_path(parent).join(marker) - } - - pub fn is_whiteout_name(name: &OsStr) -> bool { - name.as_bytes().starts_with(WHITEOUT_PREFIX.as_bytes()) - } - - fn is_whiteouted(&self, parent: &Path, name: &OsStr) -> bool { - exists(&self.whiteout_path(parent, name)) - } - - pub fn is_opaque(&self, rel: &Path) -> bool { - let path = self.upper_path(rel); - exists(&path.join(OPAQUE_NAME)) - || OPAQUE_XATTRS.iter().any(|name| { - sys::get_xattr(&path, OsStr::new(name)).is_ok_and(|value| value == b"y") - }) - } - - fn resolve_component(&self, rel: &Path) -> Option { - let upper = self.upper_path(rel); - if exists(&upper) { - return Some(Resolved { - path: upper, - is_upper: true, - }); - } - let name = rel.file_name()?; - let parent = rel.parent().unwrap_or_else(|| Path::new("")); - if self.is_whiteouted(parent, name) || self.is_opaque(parent) { - return None; - } - self.lowers.iter().find_map(|lower| { - let path = lower.join(rel); - exists(&path).then_some(Resolved { - path, - is_upper: false, - }) - }) - } - - pub fn resolve(&self, rel: &Path) -> Option { - if self.require_visible(rel).is_err() { - return None; - } - if rel.as_os_str().is_empty() { - return Some(Resolved { - path: self.upper.clone(), - is_upper: true, - }); - } - let mut current = PathBuf::new(); - let mut resolved = None; - let count = rel.components().count(); - for (index, component) in rel.components().enumerate() { - current.push(component.as_os_str()); - let item = self.resolve_component(¤t)?; - if index + 1 != count { - let metadata = fs::symlink_metadata(&item.path).ok()?; - if !metadata.is_dir() { - return None; - } - } - resolved = Some(item); - } - resolved - } - - pub fn metadata(&self, rel: &Path) -> io::Result { - self.require_visible(rel)?; - let resolved = self.resolve(rel).ok_or_else(|| error(libc::ENOENT))?; - fs::symlink_metadata(resolved.path) - } - - pub fn exists_in_lower(&self, rel: &Path) -> bool { - if self.require_visible(rel).is_err() { - return false; - } - self.lowers.iter().any(|lower| exists(&lower.join(rel))) - } - - fn copy_metadata( - &self, - source: &Path, - destination: &Path, - metadata: &Metadata, - ) -> io::Result<()> { - let nofollow = metadata.file_type().is_symlink(); - if let Err(err) = sys::chown(destination, metadata.uid(), metadata.gid(), nofollow) - && !ignorable_ownership_error(&err) - { - return Err(err); - } - if !nofollow { - fs::set_permissions( - destination, - fs::Permissions::from_mode(metadata.mode() & 0o7777), - )?; - } - if let Err(err) = sys::copy_xattrs(source, destination) - && !ignorable_metadata_error(&err) - { - return Err(err); - } - let atime = UNIX_EPOCH - + Duration::new(metadata.atime().max(0) as u64, metadata.atime_nsec() as u32); - let mtime = UNIX_EPOCH - + Duration::new(metadata.mtime().max(0) as u64, metadata.mtime_nsec() as u32); - if let Err(err) = sys::set_times(destination, Some(atime), Some(mtime), nofollow) - && !ignorable_metadata_error(&err) - { - return Err(err); - } - Ok(()) - } - - pub fn ensure_upper_parents(&self, rel: &Path) -> io::Result<()> { - self.require_visible(rel)?; - Self::validate_rel(rel)?; - let Some(parent) = rel.parent() else { - return Ok(()); - }; - let mut current = PathBuf::new(); - for component in parent.components() { - current.push(component.as_os_str()); - let upper = self.upper_path(¤t); - if exists(&upper) { - if !fs::symlink_metadata(&upper)?.is_dir() { - return Err(error(libc::ENOTDIR)); - } - continue; - } - let resolved = self.resolve(¤t).ok_or_else(|| error(libc::ENOENT))?; - let metadata = fs::symlink_metadata(&resolved.path)?; - if !metadata.is_dir() { - return Err(error(libc::ENOTDIR)); - } - // Creating a child changes this copied-up directory's metadata, - // and apply may promote that metadata even though the Agent did - // not issue an explicit setattr on the parent. - self.record_preimage(¤t)?; - fs::create_dir(&upper)?; - self.copy_metadata(&resolved.path, &upper, &metadata)?; - } - Ok(()) - } - - fn temporary_path(&self, parent: &Path) -> PathBuf { - let id = TEMP_ID.fetch_add(1, Ordering::Relaxed); - self.work - .as_deref() - .unwrap_or(parent) - .join(format!("{TEMP_PREFIX}{}-{id}", std::process::id())) - } - - pub fn copy_up(&self, rel: &Path) -> io::Result { - self.require_visible(rel)?; - Self::validate_rel(rel)?; - self.record_preimage(rel)?; - let upper = self.upper_path(rel); - if exists(&upper) { - return Ok(upper); - } - let resolved = self.resolve(rel).ok_or_else(|| error(libc::ENOENT))?; - if resolved.is_upper { - return Ok(resolved.path); - } - self.ensure_upper_parents(rel)?; - let metadata = fs::symlink_metadata(&resolved.path)?; - let parent = upper.parent().ok_or_else(|| error(libc::EINVAL))?; - let temporary = self.temporary_path(parent); - let result = (|| { - let kind = metadata.file_type(); - if kind.is_dir() { - fs::create_dir(&temporary)?; - } else if kind.is_symlink() { - std::os::unix::fs::symlink(fs::read_link(&resolved.path)?, &temporary)?; - } else if kind.is_file() { - let identity = (metadata.dev(), metadata.ino()); - let existing = if metadata.nlink() > 1 { - self.copied_hard_links - .lock() - .ok() - .and_then(|links| links.get(&identity).cloned()) - .filter(|path| exists(path)) - } else { - None - }; - if let Some(existing) = existing { - fs::hard_link(existing, &temporary)?; - } else { - let mut options = OpenOptions::new(); - options - .write(true) - .create_new(true) - .mode(metadata.mode() & 0o7777); - let mut destination = options.open(&temporary)?; - let mut source = File::open(&resolved.path)?; - io::copy(&mut source, &mut destination)?; - } - } else { - sys::mknod(&temporary, metadata.mode(), metadata.rdev() as u32)?; - } - self.copy_metadata(&resolved.path, &temporary, &metadata)?; - fs::rename(&temporary, &upper)?; - if metadata.is_file() - && metadata.nlink() > 1 - && let Ok(mut links) = self.copied_hard_links.lock() - { - links.insert((metadata.dev(), metadata.ino()), upper.clone()); - } - Ok(()) - })(); - if result.is_err() { - let _ = if temporary.is_dir() { - fs::remove_dir_all(&temporary) - } else { - fs::remove_file(&temporary) - }; - } - result.map(|()| upper) - } - - pub fn list_names(&self, rel: &Path) -> io::Result> { - self.require_visible(rel)?; - let metadata = self.metadata(rel)?; - if !metadata.is_dir() { - return Err(error(libc::ENOTDIR)); - } - let mut names = BTreeSet::new(); - if !self.is_opaque(rel) { - for lower in &self.lowers { - let directory = lower.join(rel); - let Ok(entries) = fs::read_dir(directory) else { - continue; - }; - for entry in entries.flatten() { - let name = entry.file_name(); - if !Self::is_whiteout_name(&name) { - names.insert(name); - } - } - } - } - if let Ok(entries) = fs::read_dir(self.upper_path(rel)) { - for entry in entries.flatten() { - let name = entry.file_name(); - if !Self::is_whiteout_name(&name) { - names.insert(name); - } - } - } - names.retain(|name| { - !self.is_whiteouted(rel, name) - && Self::child(rel, name).is_ok_and(|child| !self.is_excluded(&child)) - }); - Ok(names.into_iter().collect()) - } - - fn mark_opaque(&self, rel: &Path) -> io::Result<()> { - let path = self.upper_path(rel); - let marker = path.join(OPAQUE_NAME); - if !exists(&marker) { - OpenOptions::new() - .write(true) - .create_new(true) - .mode(0o600) - .open(marker)?; - } - for name in OPAQUE_XATTRS { - match sys::set_xattr(&path, OsStr::new(name), b"y", 0) { - Ok(()) => break, - Err(error) if ignorable_metadata_error(&error) => continue, - Err(error) => return Err(error), - } - } - Ok(()) - } - - fn create_whiteout(&self, rel: &Path) -> io::Result<()> { - self.ensure_upper_parents(rel)?; - let name = rel.file_name().ok_or_else(|| error(libc::EINVAL))?; - let parent = rel.parent().unwrap_or_else(|| Path::new("")); - let marker = self.whiteout_path(parent, name); - if !exists(&marker) { - OpenOptions::new() - .write(true) - .create_new(true) - .mode(0o600) - .open(marker)?; - } - Ok(()) - } - - pub fn clear_whiteout(&self, rel: &Path) -> io::Result<()> { - self.require_visible(rel)?; - let name = rel.file_name().ok_or_else(|| error(libc::EINVAL))?; - let parent = rel.parent().unwrap_or_else(|| Path::new("")); - let marker = self.whiteout_path(parent, name); - match fs::remove_file(marker) { - Ok(()) => Ok(()), - Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()), - Err(err) => Err(err), - } - } - - pub fn create_file(&self, rel: &Path, mode: u32, flags: i32) -> io::Result { - self.require_visible(rel)?; - Self::validate_rel(rel)?; - if self.resolve(rel).is_some() { - return Err(error(libc::EEXIST)); - } - self.record_preimage(rel)?; - self.ensure_upper_parents(rel)?; - self.clear_whiteout(rel)?; - let access_mode = flags & libc::O_ACCMODE; - let mut options = OpenOptions::new(); - options - .read(access_mode != libc::O_WRONLY) - .write(access_mode != libc::O_RDONLY) - .append(flags & libc::O_APPEND != 0) - .truncate(flags & libc::O_TRUNC != 0) - .create_new(true) - .mode(mode & 0o7777) - .custom_flags(flags & !(libc::O_ACCMODE | libc::O_CREAT | libc::O_EXCL)); - options.open(self.upper_path(rel)) - } - - pub fn create_dir(&self, rel: &Path, mode: u32) -> io::Result<()> { - self.require_visible(rel)?; - if self.resolve(rel).is_some() { - return Err(error(libc::EEXIST)); - } - self.record_preimage(rel)?; - let shadows_lower = self.exists_in_lower(rel); - self.ensure_upper_parents(rel)?; - self.clear_whiteout(rel)?; - let path = self.upper_path(rel); - fs::create_dir(&path)?; - fs::set_permissions(&path, fs::Permissions::from_mode(mode & 0o7777))?; - if shadows_lower { - self.mark_opaque(rel)?; - } - Ok(()) - } - - pub fn create_symlink(&self, rel: &Path, target: &Path) -> io::Result<()> { - self.require_visible(rel)?; - if self.resolve(rel).is_some() { - return Err(error(libc::EEXIST)); - } - self.record_preimage(rel)?; - self.ensure_upper_parents(rel)?; - self.clear_whiteout(rel)?; - std::os::unix::fs::symlink(target, self.upper_path(rel)) - } - - pub fn create_node(&self, rel: &Path, mode: u32, rdev: u32) -> io::Result<()> { - self.require_visible(rel)?; - if self.resolve(rel).is_some() { - return Err(error(libc::EEXIST)); - } - self.record_preimage(rel)?; - self.ensure_upper_parents(rel)?; - self.clear_whiteout(rel)?; - sys::mknod(&self.upper_path(rel), mode, rdev) - } - - pub fn remove(&self, rel: &Path, directory: bool) -> io::Result<()> { - self.require_visible(rel)?; - let resolved = self.resolve(rel).ok_or_else(|| error(libc::ENOENT))?; - let metadata = fs::symlink_metadata(&resolved.path)?; - if directory { - if !metadata.is_dir() { - return Err(error(libc::ENOTDIR)); - } - if !self.list_names(rel)?.is_empty() { - return Err(error(libc::ENOTEMPTY)); - } - } else if metadata.is_dir() { - return Err(error(libc::EISDIR)); - } - self.record_logical_tree_mapping(rel, rel)?; - - if resolved.is_upper { - if directory { - fs::remove_dir_all(&resolved.path)?; - } else { - fs::remove_file(&resolved.path)?; - } - } - if self.exists_in_lower(rel) { - self.create_whiteout(rel)?; - } - Ok(()) - } - - /// Materialize the complete merged subtree and make its root opaque. - /// - /// This is required before renaming a lower-backed directory: a single-node - /// copy-up would otherwise lose every child when the upper directory moves. - pub fn materialize_tree(&self, rel: &Path) -> io::Result { - self.require_visible(rel)?; - let metadata = self.metadata(rel)?; - if !metadata.is_dir() { - return self.copy_up(rel); - } - let names = self.list_names(rel)?; - self.copy_up(rel)?; - for name in names { - let child = Self::child(rel, &name)?; - if self.metadata(&child)?.is_dir() { - self.materialize_tree(&child)?; - } else { - self.copy_up(&child)?; - } - } - self.mark_opaque(rel)?; - Ok(self.upper_path(rel)) - } - - fn validate_replacement( - &self, - old: &Path, - new: &Path, - no_replace: bool, - ) -> io::Result> { - let Some(destination) = self.resolve(new) else { - return Ok(None); - }; - if no_replace { - return Err(error(libc::EEXIST)); - } - let source_meta = self.metadata(old)?; - let destination_meta = fs::symlink_metadata(&destination.path)?; - match (source_meta.is_dir(), destination_meta.is_dir()) { - (true, false) => return Err(error(libc::ENOTDIR)), - (false, true) => return Err(error(libc::EISDIR)), - (true, true) if !self.list_names(new)?.is_empty() => { - return Err(error(libc::ENOTEMPTY)); - } - _ => {} - } - Ok(destination.is_upper.then_some(destination.path)) - } - - fn remove_physical(path: &Path) -> io::Result<()> { - if fs::symlink_metadata(path)?.is_dir() { - fs::remove_dir_all(path) - } else { - fs::remove_file(path) - } - } - - fn remap_copied_hard_links(&self, old: &Path, new: &Path) { - let Ok(mut links) = self.copied_hard_links.lock() else { - return; - }; - for path in links.values_mut() { - if (path == old || path.starts_with(old)) - && let Ok(suffix) = path.strip_prefix(old) - { - *path = if suffix.as_os_str().is_empty() { - new.to_path_buf() - } else { - new.join(suffix) - }; - } - } - } - - fn exchange_copied_hard_links(&self, first: &Path, second: &Path) { - let Ok(mut links) = self.copied_hard_links.lock() else { - return; - }; - for path in links.values_mut() { - if path == first || path.starts_with(first) { - if let Ok(suffix) = path.strip_prefix(first) { - *path = if suffix.as_os_str().is_empty() { - second.to_path_buf() - } else { - second.join(suffix) - }; - } - } else if (path == second || path.starts_with(second)) - && let Ok(suffix) = path.strip_prefix(second) - { - *path = if suffix.as_os_str().is_empty() { - first.to_path_buf() - } else { - first.join(suffix) - }; - } - } - } - - pub fn rename(&self, old: &Path, new: &Path, no_replace: bool) -> io::Result<()> { - self.require_visible(old)?; - self.require_visible(new)?; - Self::validate_rel(old)?; - Self::validate_rel(new)?; - if old == new { - return Ok(()); - } - let source_meta = self.metadata(old)?; - if source_meta.is_dir() && new.starts_with(old) { - return Err(error(libc::EINVAL)); - } - let replaced_upper = self.validate_replacement(old, new, no_replace)?; - self.record_logical_tree_mapping(old, old)?; - self.record_logical_tree_mapping(old, new)?; - let source = if source_meta.is_dir() { - self.materialize_tree(old)? - } else { - self.copy_up(old)? - }; - self.ensure_upper_parents(new)?; - self.clear_whiteout(new)?; - let source_needs_whiteout = self.exists_in_lower(old); - if source_needs_whiteout { - self.create_whiteout(old)?; - } - let backup = replaced_upper - .as_ref() - .map(|_| self.temporary_path(self.upper())); - if let (Some(destination), Some(backup)) = (&replaced_upper, &backup) - && let Err(error) = fs::rename(destination, backup) - { - if source_needs_whiteout { - let _ = self.clear_whiteout(old); - } - return Err(error); - } - if let Err(error) = fs::rename(&source, self.upper_path(new)) { - if let (Some(destination), Some(backup)) = (&replaced_upper, &backup) { - let _ = fs::rename(backup, destination); - } - if source_needs_whiteout { - let _ = self.clear_whiteout(old); - } - return Err(error); - } - if let Some(backup) = backup - && let Err(error) = Self::remove_physical(&backup) - { - log::warn!( - "rename committed but cleanup of {} failed: {error}", - backup.display() - ); - } - self.remap_copied_hard_links(&self.upper_path(old), &self.upper_path(new)); - Ok(()) - } - - pub fn hard_link(&self, source: &Path, destination: &Path) -> io::Result<()> { - self.require_visible(source)?; - self.require_visible(destination)?; - let metadata = self.metadata(source)?; - if metadata.is_dir() { - return Err(error(libc::EPERM)); - } - if self.resolve(destination).is_some() { - return Err(error(libc::EEXIST)); - } - self.record_preimage(destination)?; - let source = self.copy_up(source)?; - self.ensure_upper_parents(destination)?; - self.clear_whiteout(destination)?; - fs::hard_link(source, self.upper_path(destination)) - } - - pub fn exchange(&self, first: &Path, second: &Path) -> io::Result<()> { - self.require_visible(first)?; - self.require_visible(second)?; - Self::validate_rel(first)?; - Self::validate_rel(second)?; - if first == second { - return Ok(()); - } - if first.starts_with(second) || second.starts_with(first) { - return Err(error(libc::EINVAL)); - } - let first_meta = self.metadata(first)?; - let second_meta = self.metadata(second)?; - self.record_logical_tree_mapping(first, first)?; - self.record_logical_tree_mapping(second, second)?; - self.record_logical_tree_mapping(first, second)?; - self.record_logical_tree_mapping(second, first)?; - let first_upper = if first_meta.is_dir() { - self.materialize_tree(first)? - } else { - self.copy_up(first)? - }; - let second_upper = if second_meta.is_dir() { - self.materialize_tree(second)? - } else { - self.copy_up(second)? - }; - let temporary = self.temporary_path(self.upper()); - fs::rename(&first_upper, &temporary)?; - if let Err(error) = fs::rename(&second_upper, &first_upper) { - let _ = fs::rename(&temporary, &first_upper); - return Err(error); - } - if let Err(error) = fs::rename(&temporary, &second_upper) { - let _ = fs::rename(&first_upper, &second_upper); - let _ = fs::rename(&temporary, &first_upper); - return Err(error); - } - self.exchange_copied_hard_links(&first_upper, &second_upper); - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::io::{Read, Write}; - use tempfile::TempDir; - - #[test] - fn unmapped_ownership_is_ignorable_but_other_invalid_metadata_is_not() { - let invalid = io::Error::from_raw_os_error(libc::EINVAL); - assert!(ignorable_ownership_error(&invalid)); - assert!(!ignorable_metadata_error(&invalid)); - } - - struct Fixture { - _temp: TempDir, - lower1: PathBuf, - lower2: PathBuf, - upper: PathBuf, - core: OverlayCore, - } - - impl Fixture { - fn new() -> Self { - let temp = tempfile::tempdir().expect("tempdir"); - let lower1 = temp.path().join("lower1"); - let lower2 = temp.path().join("lower2"); - let upper = temp.path().join("upper"); - let work = temp.path().join("work"); - for directory in [&lower1, &lower2, &upper, &work] { - fs::create_dir(directory).expect("create layer"); - } - let core = OverlayCore::new( - vec![lower1.clone(), lower2.clone()], - upper.clone(), - Some(work), - ) - .expect("core"); - Self { - _temp: temp, - lower1, - lower2, - upper, - core, - } - } - } - - #[test] - fn top_lower_wins_and_directories_merge() { - let fixture = Fixture::new(); - fs::create_dir(fixture.lower1.join("dir")).expect("dir"); - fs::create_dir(fixture.lower2.join("dir")).expect("dir"); - fs::write(fixture.lower1.join("dir/a"), b"a").expect("a"); - fs::write(fixture.lower1.join("dir/shared"), b"top").expect("top"); - fs::write(fixture.lower2.join("dir/b"), b"b").expect("b"); - fs::write(fixture.lower2.join("dir/shared"), b"bottom").expect("bottom"); - - let names = fixture.core.list_names(Path::new("dir")).expect("names"); - assert_eq!( - names, - vec![ - OsString::from("a"), - OsString::from("b"), - OsString::from("shared") - ] - ); - let resolved = fixture - .core - .resolve(Path::new("dir/shared")) - .expect("resolved"); - assert_eq!(fs::read(resolved.path).expect("read"), b"top"); - } - - #[test] - fn recreating_a_whiteouted_directory_is_opaque() { - let fixture = Fixture::new(); - fs::create_dir(fixture.lower2.join("old")).expect("old"); - fixture - .core - .remove(Path::new("old"), true) - .expect("whiteout"); - fixture - .core - .create_dir(Path::new("old"), 0o755) - .expect("mkdir"); - assert!(fixture.upper.join("old").join(OPAQUE_NAME).is_file()); - assert!( - fixture - .core - .list_names(Path::new("old")) - .expect("names") - .is_empty() - ); - } - - #[test] - fn renaming_lower_directory_keeps_complete_merged_tree() { - let fixture = Fixture::new(); - fs::create_dir_all(fixture.lower1.join("tree/nested")).expect("tree"); - fs::create_dir_all(fixture.lower2.join("tree/nested")).expect("tree"); - fs::write(fixture.lower1.join("tree/a"), b"a").expect("a"); - fs::write(fixture.lower2.join("tree/b"), b"b").expect("b"); - fs::write(fixture.lower1.join("tree/nested/c"), b"c").expect("c"); - - fixture - .core - .rename(Path::new("tree"), Path::new("moved"), false) - .expect("rename"); - - assert!(fixture.core.resolve(Path::new("tree")).is_none()); - for path in ["moved/a", "moved/b", "moved/nested/c"] { - assert!(fixture.core.resolve(Path::new(path)).is_some(), "{path}"); - } - assert!(fixture.upper.join("moved").join(OPAQUE_NAME).is_file()); - } - - #[test] - fn copy_up_preserves_contents_mode_and_xattrs_when_supported() { - let fixture = Fixture::new(); - let source = fixture.lower2.join("file"); - fs::write(&source, b"payload").expect("write"); - fs::set_permissions(&source, fs::Permissions::from_mode(0o751)).expect("chmod"); - let xattr_supported = - sys::set_xattr(&source, OsStr::new("user.persisting.test"), b"value", 0).is_ok(); - - let copied = fixture.core.copy_up(Path::new("file")).expect("copy up"); - let mut contents = Vec::new(); - File::open(&copied) - .expect("open") - .read_to_end(&mut contents) - .expect("read"); - assert_eq!(contents, b"payload"); - assert_eq!( - fs::symlink_metadata(&copied).expect("meta").mode() & 0o777, - 0o751 - ); - if xattr_supported { - assert_eq!( - sys::get_xattr(&copied, OsStr::new("user.persisting.test")).expect("xattr"), - b"value" - ); - } - } - - #[test] - fn hard_link_copies_up_once_and_shares_data() { - let fixture = Fixture::new(); - fs::write(fixture.lower2.join("source"), b"before").expect("source"); - fixture - .core - .hard_link(Path::new("source"), Path::new("linked")) - .expect("link"); - let mut linked = OpenOptions::new() - .write(true) - .truncate(true) - .open(fixture.upper.join("linked")) - .expect("open"); - linked.write_all(b"after").expect("write"); - assert_eq!( - fs::read(fixture.upper.join("source")).expect("read"), - b"after" - ); - } - - #[test] - fn lower_hard_links_remain_linked_after_independent_copy_up() { - let fixture = Fixture::new(); - fs::write(fixture.lower2.join("one"), b"before").expect("one"); - fs::hard_link(fixture.lower2.join("one"), fixture.lower2.join("two")).expect("link"); - fixture.core.copy_up(Path::new("one")).expect("copy one"); - fixture.core.copy_up(Path::new("two")).expect("copy two"); - fs::write(fixture.upper.join("one"), b"after").expect("write"); - assert_eq!(fs::read(fixture.upper.join("two")).expect("read"), b"after"); - assert_eq!( - fs::metadata(fixture.upper.join("one")).expect("one").ino(), - fs::metadata(fixture.upper.join("two")).expect("two").ino() - ); - } - - #[test] - fn lower_hard_link_index_survives_rename() { - let fixture = Fixture::new(); - fs::write(fixture.lower2.join("one"), b"before").expect("one"); - fs::hard_link(fixture.lower2.join("one"), fixture.lower2.join("two")).expect("link"); - fixture.core.copy_up(Path::new("one")).expect("copy one"); - fixture - .core - .rename(Path::new("one"), Path::new("moved"), false) - .expect("rename"); - fixture.core.copy_up(Path::new("two")).expect("copy two"); - fs::write(fixture.upper.join("moved"), b"after").expect("write"); - assert_eq!(fs::read(fixture.upper.join("two")).expect("read"), b"after"); - } - - #[test] - fn exchange_materializes_and_swaps_lower_entries() { - let fixture = Fixture::new(); - fs::write(fixture.lower2.join("a"), b"a").expect("a"); - fs::create_dir(fixture.lower2.join("b")).expect("b"); - fs::write(fixture.lower2.join("b/child"), b"b").expect("child"); - - fixture - .core - .exchange(Path::new("a"), Path::new("b")) - .expect("exchange"); - - assert_eq!( - fs::read(fixture.core.resolve(Path::new("b")).expect("b").path).expect("read"), - b"a" - ); - assert_eq!( - fs::read( - fixture - .core - .resolve(Path::new("a/child")) - .expect("child") - .path - ) - .expect("read"), - b"b" - ); - } - - #[test] - fn excluded_subtree_is_absent_and_cannot_be_recreated() { - let temporary = tempfile::tempdir().unwrap(); - let lower = temporary.path().join("lower"); - let upper = temporary.path().join("upper"); - let work = temporary.path().join("work"); - fs::create_dir_all(lower.join("visible")).unwrap(); - fs::create_dir_all(lower.join("internal/nested")).unwrap(); - fs::write(lower.join("internal/nested/control"), b"secret").unwrap(); - let core = OverlayCore::new_with_exclusions( - vec![lower], - upper, - Some(work), - vec![PathBuf::from("internal")], - ) - .unwrap(); - - assert!(core.resolve(Path::new("internal")).is_none()); - assert!(core.resolve(Path::new("internal/nested/control")).is_none()); - assert!( - !core - .list_names(Path::new("")) - .unwrap() - .contains(&OsString::from("internal")) - ); - assert_eq!( - core.create_dir(Path::new("internal"), 0o755) - .unwrap_err() - .raw_os_error(), - Some(libc::ENOENT) - ); - assert_eq!( - core.rename( - Path::new("visible"), - Path::new("internal/replacement"), - false - ) - .unwrap_err() - .raw_os_error(), - Some(libc::ENOENT) - ); - } - - #[test] - fn rename_replaces_empty_upper_directory_transactionally() { - let fixture = Fixture::new(); - fs::create_dir_all(fixture.lower2.join("source/child")).expect("source"); - fs::write(fixture.lower2.join("source/child/file"), b"data").expect("file"); - fixture - .core - .create_dir(Path::new("destination"), 0o755) - .expect("destination"); - - fixture - .core - .rename(Path::new("source"), Path::new("destination"), false) - .expect("rename"); - - assert!(fixture.core.resolve(Path::new("source")).is_none()); - assert_eq!( - fs::read( - fixture - .core - .resolve(Path::new("destination/child/file")) - .expect("file") - .path - ) - .expect("read"), - b"data" - ); - } - - #[test] - fn first_touch_preimage_is_durable_and_never_rebased() { - let temporary = tempfile::tempdir().unwrap(); - let target = temporary.path().join("target"); - let upper = temporary.path().join("upper"); - let work = temporary.path().join("work"); - let journal = temporary.path().join("preimages"); - fs::create_dir(&target).unwrap(); - fs::write(target.join("value.txt"), b"original").unwrap(); - let original = fingerprint_at(&target, Path::new("value.txt")).unwrap(); - let core = OverlayCore::new_with_exclusions_and_preimages( - vec![target.clone()], - upper.clone(), - Some(work), - Vec::new(), - Some(journal.clone()), - ) - .unwrap(); - - core.copy_up(Path::new("value.txt")).unwrap(); - fs::write(upper.join("value.txt"), b"staged").unwrap(); - fs::write(target.join("value.txt"), b"concurrent").unwrap(); - core.copy_up(Path::new("value.txt")).unwrap(); - - let entries = load_preimages(&journal).unwrap(); - assert_eq!(entries.len(), 1); - assert_eq!(entries[0].relative_path(), Path::new("value.txt")); - assert_eq!(entries[0].state, original); - - remove_preimages(&journal, &[PathBuf::from("value.txt")]).unwrap(); - fs::remove_file(upper.join("value.txt")).unwrap(); - let rebased = fingerprint_at(&target, Path::new("value.txt")).unwrap(); - core.copy_up(Path::new("value.txt")).unwrap(); - let entries = load_preimages(&journal).unwrap(); - assert_eq!(entries.len(), 1); - assert_eq!(entries[0].state, rebased); - } - - #[test] - fn preimage_journal_covers_create_remove_and_rename_destinations() { - let temporary = tempfile::tempdir().unwrap(); - let target = temporary.path().join("target"); - let upper = temporary.path().join("upper"); - let work = temporary.path().join("work"); - let journal = temporary.path().join("preimages"); - fs::create_dir(&target).unwrap(); - fs::write(target.join("source"), b"source").unwrap(); - fs::write(target.join("victim"), b"victim").unwrap(); - let source = fingerprint_at(&target, Path::new("source")).unwrap(); - let victim = fingerprint_at(&target, Path::new("victim")).unwrap(); - let core = OverlayCore::new_with_exclusions_and_preimages( - vec![target], - upper, - Some(work), - Vec::new(), - Some(journal.clone()), - ) - .unwrap(); - - drop( - core.create_file(Path::new("created"), 0o600, libc::O_RDWR) - .unwrap(), - ); - core.remove(Path::new("victim"), false).unwrap(); - core.rename(Path::new("source"), Path::new("moved"), false) - .unwrap(); - - let entries = load_preimages(&journal) - .unwrap() - .into_iter() - .map(|entry| (entry.relative_path(), entry.state)) - .collect::>(); - assert_eq!(entries[Path::new("created")], PathFingerprint::Absent); - assert_eq!(entries[Path::new("moved")], PathFingerprint::Absent); - assert_eq!(entries[Path::new("source")], source); - assert_eq!(entries[Path::new("victim")], victim); - } -} diff --git a/crates/persisting-overlay-core/src/lib.rs b/crates/persisting-overlay-core/src/lib.rs deleted file mode 100644 index cae614478..000000000 --- a/crates/persisting-overlay-core/src/lib.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! Portable overlay filesystem semantics shared by host FUSE and libkrun virtio-fs. - -mod core; -pub mod sys; - -pub use core::{ - OPAQUE_NAME, OverlayCore, PathFingerprint, PathPreimage, Resolved, WHITEOUT_PREFIX, - fingerprint_at, load_preimages, preimage_journal_is_complete, remove_preimages, -}; diff --git a/crates/persisting-overlay-core/src/sys.rs b/crates/persisting-overlay-core/src/sys.rs deleted file mode 100644 index 5828d8e63..000000000 --- a/crates/persisting-overlay-core/src/sys.rs +++ /dev/null @@ -1,281 +0,0 @@ -//! Small, cross-platform syscall wrappers. -//! -//! Keeping the unsafe boundary here makes the overlay logic easier to audit. - -use std::ffi::{CString, OsStr}; -use std::fs::File; -use std::io; -use std::os::fd::AsRawFd; -use std::os::unix::ffi::OsStrExt; -use std::path::Path; -use std::time::{SystemTime, UNIX_EPOCH}; - -fn c_path(path: &Path) -> io::Result { - CString::new(path.as_os_str().as_bytes()) - .map_err(|_| io::Error::from_raw_os_error(libc::EINVAL)) -} - -fn c_name(name: &OsStr) -> io::Result { - CString::new(name.as_bytes()).map_err(|_| io::Error::from_raw_os_error(libc::EINVAL)) -} - -fn cvt(rc: libc::c_int) -> io::Result<()> { - if rc == 0 { - Ok(()) - } else { - Err(io::Error::last_os_error()) - } -} - -fn timespec(time: SystemTime) -> libc::timespec { - match time.duration_since(UNIX_EPOCH) { - Ok(value) => libc::timespec { - tv_sec: value.as_secs() as libc::time_t, - tv_nsec: value.subsec_nanos() as libc::c_long, - }, - Err(value) => { - let value = value.duration(); - libc::timespec { - tv_sec: -(value.as_secs() as libc::time_t) - 1, - tv_nsec: 1_000_000_000 - value.subsec_nanos() as libc::c_long, - } - } - } -} - -pub fn set_times( - path: &Path, - atime: Option, - mtime: Option, - nofollow: bool, -) -> io::Result<()> { - let path = c_path(path)?; - let omit = libc::timespec { - tv_sec: 0, - tv_nsec: libc::UTIME_OMIT, - }; - let times = [ - atime.map(timespec).unwrap_or(omit), - mtime.map(timespec).unwrap_or(omit), - ]; - let flags = if nofollow { - libc::AT_SYMLINK_NOFOLLOW - } else { - 0 - }; - // SAFETY: path and times are valid for the duration of the call. - cvt(unsafe { libc::utimensat(libc::AT_FDCWD, path.as_ptr(), times.as_ptr(), flags) }) -} - -pub fn chown(path: &Path, uid: u32, gid: u32, nofollow: bool) -> io::Result<()> { - let path = c_path(path)?; - let flags = if nofollow { - libc::AT_SYMLINK_NOFOLLOW - } else { - 0 - }; - // SAFETY: path is a valid NUL-terminated string. - cvt(unsafe { - libc::fchownat( - libc::AT_FDCWD, - path.as_ptr(), - uid as libc::uid_t, - gid as libc::gid_t, - flags, - ) - }) -} - -pub fn access(path: &Path, mask: i32) -> io::Result<()> { - let path = c_path(path)?; - // SAFETY: path is a valid NUL-terminated string. - cvt(unsafe { libc::access(path.as_ptr(), mask) }) -} - -pub fn mknod(path: &Path, mode: u32, rdev: u32) -> io::Result<()> { - let path = c_path(path)?; - // SAFETY: path is a valid NUL-terminated string. - cvt(unsafe { libc::mknod(path.as_ptr(), mode as libc::mode_t, rdev as libc::dev_t) }) -} - -pub struct StatFs { - pub blocks: u64, - pub bfree: u64, - pub bavail: u64, - pub files: u64, - pub ffree: u64, - pub bsize: u32, - pub namelen: u32, - pub frsize: u32, -} - -pub fn statfs(path: &Path) -> io::Result { - let path = c_path(path)?; - // SAFETY: zero is a valid initial representation for statvfs. - let mut stat: libc::statvfs = unsafe { std::mem::zeroed() }; - // SAFETY: path and output pointer are valid. - let rc = unsafe { libc::statvfs(path.as_ptr(), &mut stat) }; - if rc != 0 { - return Err(io::Error::last_os_error()); - } - Ok(StatFs { - blocks: stat.f_blocks as u64, - bfree: stat.f_bfree as u64, - bavail: stat.f_bavail as u64, - files: stat.f_files as u64, - ffree: stat.f_ffree as u64, - bsize: stat.f_bsize as u32, - namelen: stat.f_namemax as u32, - frsize: stat.f_frsize as u32, - }) -} - -fn xattr_buffer(mut call: F) -> io::Result> -where - F: FnMut(*mut libc::c_void, usize) -> libc::ssize_t, -{ - let needed = call(std::ptr::null_mut(), 0); - if needed < 0 { - return Err(io::Error::last_os_error()); - } - let mut buffer = vec![0_u8; needed as usize]; - if buffer.is_empty() { - return Ok(buffer); - } - let actual = call(buffer.as_mut_ptr().cast(), buffer.len()); - if actual < 0 { - return Err(io::Error::last_os_error()); - } - buffer.truncate(actual as usize); - Ok(buffer) -} - -pub fn list_xattrs(path: &Path) -> io::Result>> { - let path = c_path(path)?; - #[cfg(target_os = "macos")] - let data = xattr_buffer(|buf, size| { - // SAFETY: buffers are either null/zero or valid writable allocations. - unsafe { libc::listxattr(path.as_ptr(), buf.cast(), size, libc::XATTR_NOFOLLOW) } - })?; - #[cfg(not(target_os = "macos"))] - let data = xattr_buffer(|buf, size| { - // SAFETY: buffers are either null/zero or valid writable allocations. - unsafe { libc::llistxattr(path.as_ptr(), buf.cast(), size) } - })?; - Ok(data - .split(|byte| *byte == 0) - .filter(|name| !name.is_empty()) - .map(<[u8]>::to_vec) - .collect()) -} - -pub fn get_xattr(path: &Path, name: &OsStr) -> io::Result> { - let path = c_path(path)?; - let name = c_name(name)?; - #[cfg(target_os = "macos")] - { - xattr_buffer(|buf, size| { - // SAFETY: arguments remain valid for the duration of the call. - unsafe { - libc::getxattr( - path.as_ptr(), - name.as_ptr(), - buf, - size, - 0, - libc::XATTR_NOFOLLOW, - ) - } - }) - } - #[cfg(not(target_os = "macos"))] - { - xattr_buffer(|buf, size| { - // SAFETY: arguments remain valid for the duration of the call. - unsafe { libc::lgetxattr(path.as_ptr(), name.as_ptr(), buf, size) } - }) - } -} - -pub fn set_xattr(path: &Path, name: &OsStr, value: &[u8], flags: i32) -> io::Result<()> { - let path = c_path(path)?; - let name = c_name(name)?; - #[cfg(target_os = "macos")] - let rc = unsafe { - // SAFETY: arguments remain valid for the duration of the call. - libc::setxattr( - path.as_ptr(), - name.as_ptr(), - value.as_ptr().cast(), - value.len(), - 0, - flags | libc::XATTR_NOFOLLOW, - ) - }; - #[cfg(not(target_os = "macos"))] - let rc = unsafe { - // SAFETY: arguments remain valid for the duration of the call. - libc::lsetxattr( - path.as_ptr(), - name.as_ptr(), - value.as_ptr().cast(), - value.len(), - flags, - ) - }; - if rc == 0 { - Ok(()) - } else { - Err(io::Error::last_os_error()) - } -} - -pub fn remove_xattr(path: &Path, name: &OsStr) -> io::Result<()> { - let path = c_path(path)?; - let name = c_name(name)?; - #[cfg(target_os = "macos")] - let rc = unsafe { - // SAFETY: arguments remain valid for the duration of the call. - libc::removexattr(path.as_ptr(), name.as_ptr(), libc::XATTR_NOFOLLOW) - }; - #[cfg(not(target_os = "macos"))] - let rc = unsafe { - // SAFETY: arguments remain valid for the duration of the call. - libc::lremovexattr(path.as_ptr(), name.as_ptr()) - }; - cvt(rc) -} - -pub fn copy_xattrs(source: &Path, destination: &Path) -> io::Result<()> { - for name in list_xattrs(source)? { - let name = OsStr::from_bytes(&name); - let value = get_xattr(source, name)?; - set_xattr(destination, name, &value, 0)?; - } - Ok(()) -} - -pub fn fsync(file: &File, datasync: bool) -> io::Result<()> { - if datasync { - file.sync_data() - } else { - file.sync_all() - } -} - -pub fn seek(file: &File, offset: i64, whence: i32) -> io::Result { - // SAFETY: lseek only operates on the valid owned descriptor. - let result = unsafe { libc::lseek(file.as_raw_fd(), offset, whence) }; - if result < 0 { - Err(io::Error::last_os_error()) - } else { - Ok(result) - } -} - -#[cfg(target_os = "macos")] -pub fn set_flags(path: &Path, flags: u32) -> io::Result<()> { - let path = c_path(path)?; - // SAFETY: path is a valid NUL-terminated string. - cvt(unsafe { libc::chflags(path.as_ptr(), flags) }) -} diff --git a/crates/persisting-overlayfs/Cargo.toml b/crates/persisting-overlayfs/Cargo.toml deleted file mode 100644 index ebeb762a4..000000000 --- a/crates/persisting-overlayfs/Cargo.toml +++ /dev/null @@ -1,38 +0,0 @@ -[package] -name = "persisting-overlayfs" -version.workspace = true -edition.workspace = true -authors.workspace = true -license.workspace = true -description = "Cross-platform FUSE overlay (macFUSE / libfuse) for pVisor staging" -readme = "README.md" - -[features] -default = ["jujutsu"] -jujutsu = ["dep:jj-lib", "dep:pollster"] - -[[bin]] -name = "persisting-overlayfs" -path = "src/main.rs" - -[dependencies] -anyhow.workspace = true -libc.workspace = true -persisting-overlay-core.workspace = true -log.workspace = true -env_logger.workspace = true -clap = { workspace = true, features = ["derive"] } -jj-lib = { workspace = true, optional = true } -pollster = { workspace = true, optional = true } - -[target.'cfg(target_os = "macos")'.dependencies] -fuser = { workspace = true, features = ["abi-7-19", "libfuse", "macfuse-5"] } - -[target.'cfg(target_os = "linux")'.dependencies] -fuser = { workspace = true, default-features = false, features = ["abi-7-31"] } - -[target.'cfg(not(any(target_os = "macos", target_os = "linux")))'.dependencies] -fuser = { workspace = true, features = ["abi-7-19", "libfuse"] } - -[dev-dependencies] -tempfile.workspace = true diff --git a/crates/persisting-overlayfs/README.md b/crates/persisting-overlayfs/README.md deleted file mode 100644 index e3f896e60..000000000 --- a/crates/persisting-overlayfs/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# persisting-overlayfs - -**Cross-platform FUSE overlay for pVisor staging (macFUSE / libfuse).** - -Owns the unprivileged, in-process FUSE overlay: ordered multi-`lowerdir` merge, -directory or Jujutsu upper, portable `.wh.*` whiteouts, and the optional -standalone `persisting-overlayfs` diagnostic CLI. - -Does not own review, apply, drop, or Run lifecycle. -[`persisting-pvisor`](../persisting-pvisor/README.md) links this crate as a -library, owns the FUSE request thread, and commits whiteouts through -`apply_overlay`. Portable, FUSE-neutral overlay mechanics live in -`persisting-overlay-core` (used by libkrun virtio-fs without a host FUSE -mount). - -Whiteouts match pVisor's `apply_overlay`, so review → apply works the same -across host FUSE and virtio-fs. - -Linux-only container features are out of scope: UID/GID namespace mapping, -`metacopy`, `redirect_dir`, SELinux labeling, and capability semantics are not -emulated. - -## Develop - -### Prerequisites - -macOS: install [macFUSE](https://macfuse.github.io/) (`brew install --cask macfuse`), -enable third-party kernel extensions on Apple Silicon, and ensure `pkg-config` -can find macFUSE (`brew install pkgconf`). macFUSE 5 is supported through the -workspace's patched `fuser` dependency. - -Linux: FUSE3 development packages, for example `libfuse3-dev`. - -### Build and test - -```bash -cargo build -p persisting-pvisor --bin pvisor --release -just test persisting-overlayfs -``` - -pVisor embeds the overlay library; it does not discover or launch an overlay -binary. The standalone CLI is optional and intended for diagnostics or manual -mounts: - -```bash -cargo build -p persisting-overlayfs --release -# → target/release/persisting-overlayfs -``` - -## Links - -- [Isolation architecture](../../docs/src/pvisor/design/isolation.md) -- [Review and apply effects](../../docs/src/pvisor/guides/review-apply.md) -- [`persisting-pvisor`](../persisting-pvisor/README.md) diff --git a/crates/persisting-overlayfs/src/fs.rs b/crates/persisting-overlayfs/src/fs.rs deleted file mode 100644 index c0df9191e..000000000 --- a/crates/persisting-overlayfs/src/fs.rs +++ /dev/null @@ -1,1192 +0,0 @@ -#[cfg(target_os = "macos")] -use fuser::ReplyXTimes; -use fuser::{ - FUSE_ROOT_ID, FileAttr, FileType, Filesystem, ReplyAttr, ReplyCreate, ReplyData, - ReplyDirectory, ReplyDirectoryPlus, ReplyEmpty, ReplyEntry, ReplyLseek, ReplyOpen, ReplyStatfs, - ReplyWrite, ReplyXattr, Request, TimeOrNow, -}; -use persisting_overlay_core::{OverlayCore, sys}; -use std::collections::{BTreeSet, HashMap}; -use std::ffi::{OsStr, OsString}; -use std::fs::{self, File, OpenOptions}; -use std::io; -use std::os::unix::fs::{FileExt, FileTypeExt, MetadataExt, OpenOptionsExt, PermissionsExt}; -use std::path::{Path, PathBuf}; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; - -const TTL: Duration = Duration::from_secs(1); -const RENAME_NOREPLACE: u32 = 1; -const RENAME_EXCHANGE: u32 = 2; - -#[derive(Clone, Debug)] -struct Node { - paths: BTreeSet, -} - -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -struct ObjectKey { - device: u64, - inode: u64, -} - -#[derive(Debug)] -struct DirectoryEntry { - ino: u64, - kind: FileType, - name: OsString, - attr: FileAttr, -} - -pub struct OverlayFs { - core: OverlayCore, - nodes: HashMap, - by_path: HashMap, - by_object: HashMap, - next_ino: u64, - open_files: HashMap, - open_directories: HashMap>, - next_handle: u64, -} - -fn errno(error: &io::Error) -> i32 { - error.raw_os_error().unwrap_or(libc::EIO) -} - -fn setattr_requires_copy_up( - mode: Option, - uid: Option, - gid: Option, - size: Option, - _atime: Option, - mtime: Option, - flags: Option, -) -> bool { - mode.is_some() - || uid.is_some() - || gid.is_some() - || size.is_some() - || mtime.is_some() - || flags.is_some() -} - -fn file_type(metadata: &fs::Metadata) -> FileType { - let kind = metadata.file_type(); - if kind.is_dir() { - FileType::Directory - } else if kind.is_symlink() { - FileType::Symlink - } else if kind.is_block_device() { - FileType::BlockDevice - } else if kind.is_char_device() { - FileType::CharDevice - } else if kind.is_fifo() { - FileType::NamedPipe - } else if kind.is_socket() { - FileType::Socket - } else { - FileType::RegularFile - } -} - -fn time_value(value: TimeOrNow) -> SystemTime { - match value { - TimeOrNow::SpecificTime(time) => time, - TimeOrNow::Now => SystemTime::now(), - } -} - -impl OverlayFs { - pub fn new( - lowers: Vec, - upper: PathBuf, - work: Option, - ) -> anyhow::Result { - Self::from_core(OverlayCore::new(lowers, upper, work)?) - } - - pub fn new_with_exclusions_and_preimages( - lowers: Vec, - upper: PathBuf, - work: Option, - excluded: Vec, - preimage_dir: Option, - ) -> anyhow::Result { - Self::from_core(OverlayCore::new_with_exclusions_and_preimages( - lowers, - upper, - work, - excluded, - preimage_dir, - )?) - } - - fn from_core(core: OverlayCore) -> anyhow::Result { - let mut root_paths = BTreeSet::new(); - root_paths.insert(PathBuf::new()); - let mut nodes = HashMap::new(); - nodes.insert(FUSE_ROOT_ID, Node { paths: root_paths }); - let mut by_path = HashMap::new(); - by_path.insert(PathBuf::new(), FUSE_ROOT_ID); - Ok(Self { - core, - nodes, - by_path, - by_object: HashMap::new(), - next_ino: FUSE_ROOT_ID + 1, - open_files: HashMap::new(), - open_directories: HashMap::new(), - next_handle: 1, - }) - } - - fn node_path(&self, ino: u64) -> io::Result { - self.nodes - .get(&ino) - .and_then(|node| node.paths.iter().next()) - .cloned() - .ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT)) - } - - fn allocate_inode(&mut self, path: PathBuf, metadata: &fs::Metadata) -> u64 { - if let Some(ino) = self.by_path.get(&path) { - return *ino; - } - let object = (!metadata.is_dir() && metadata.nlink() > 1).then_some(ObjectKey { - device: metadata.dev(), - inode: metadata.ino(), - }); - if let Some(ino) = object.and_then(|key| self.by_object.get(&key).copied()) { - self.add_inode_alias(ino, path); - return ino; - } - let ino = self.next_ino; - self.next_ino += 1; - let mut paths = BTreeSet::new(); - paths.insert(path.clone()); - self.nodes.insert(ino, Node { paths }); - self.by_path.insert(path, ino); - if let Some(object) = object { - self.by_object.insert(object, ino); - } - ino - } - - fn add_inode_alias(&mut self, ino: u64, path: PathBuf) { - self.by_path.insert(path.clone(), ino); - if let Some(node) = self.nodes.get_mut(&ino) { - node.paths.insert(path); - } - } - - fn remove_inode_prefix(&mut self, prefix: &Path) { - let paths: Vec<_> = self - .by_path - .keys() - .filter(|path| *path == prefix || path.starts_with(prefix)) - .cloned() - .collect(); - for path in paths { - if let Some(ino) = self.by_path.remove(&path) - && let Some(node) = self.nodes.get_mut(&ino) - { - node.paths.remove(&path); - } - } - } - - fn remap_inode_prefix(&mut self, old: &Path, new: &Path) { - self.remove_inode_prefix(new); - let mappings: Vec<_> = self - .by_path - .iter() - .filter(|(path, _)| *path == old || path.starts_with(old)) - .map(|(path, ino)| (path.clone(), *ino)) - .collect(); - for (old_path, ino) in mappings { - let suffix = old_path.strip_prefix(old).unwrap_or_else(|_| Path::new("")); - let new_path = if suffix.as_os_str().is_empty() { - new.to_path_buf() - } else { - new.join(suffix) - }; - self.by_path.remove(&old_path); - self.by_path.insert(new_path.clone(), ino); - if let Some(node) = self.nodes.get_mut(&ino) { - node.paths.remove(&old_path); - node.paths.insert(new_path); - } - } - } - - fn exchange_inode_prefixes(&mut self, first: &Path, second: &Path) { - let mappings: Vec<_> = self - .by_path - .iter() - .filter_map(|(path, ino)| { - if path == first || path.starts_with(first) { - let suffix = path.strip_prefix(first).ok()?; - Some((path.clone(), *ino, second.join(suffix))) - } else if path == second || path.starts_with(second) { - let suffix = path.strip_prefix(second).ok()?; - Some((path.clone(), *ino, first.join(suffix))) - } else { - None - } - }) - .collect(); - for (old, ino, _) in &mappings { - self.by_path.remove(old); - if let Some(node) = self.nodes.get_mut(ino) { - node.paths.remove(old); - } - } - for (_, ino, new) in mappings { - self.by_path.insert(new.clone(), ino); - if let Some(node) = self.nodes.get_mut(&ino) { - node.paths.insert(new); - } - } - } - - fn allocate_handle(&mut self) -> u64 { - let handle = self.next_handle; - self.next_handle += 1; - handle - } - - fn attr_from_metadata(ino: u64, metadata: &fs::Metadata) -> FileAttr { - let mtime = metadata.modified().unwrap_or(UNIX_EPOCH); - let atime = metadata.accessed().unwrap_or(mtime); - let ctime = UNIX_EPOCH - + Duration::new( - metadata.ctime().max(0) as u64, - metadata.ctime_nsec().max(0) as u32, - ); - #[cfg(target_os = "macos")] - let flags = { - use std::os::macos::fs::MetadataExt as MacMetadataExt; - MacMetadataExt::st_flags(metadata) - }; - #[cfg(not(target_os = "macos"))] - let flags = 0; - FileAttr { - ino, - size: metadata.len(), - blocks: metadata.blocks(), - atime, - mtime, - ctime, - crtime: metadata.created().unwrap_or(ctime), - kind: file_type(metadata), - perm: (metadata.mode() & 0o7777) as u16, - nlink: metadata.nlink().min(u32::MAX as u64) as u32, - uid: metadata.uid(), - gid: metadata.gid(), - rdev: metadata.rdev() as u32, - blksize: metadata.blksize().min(u32::MAX as u64) as u32, - flags, - } - } - - fn attr(&self, ino: u64, path: &Path) -> io::Result { - Ok(Self::attr_from_metadata(ino, &self.core.metadata(path)?)) - } - - fn child_path(&self, parent: u64, name: &OsStr) -> io::Result { - OverlayCore::child(&self.node_path(parent)?, name) - } - - fn copy_up_inode(&self, ino: u64) -> io::Result { - let path = self.node_path(ino)?; - let aliases = self - .nodes - .get(&ino) - .map(|node| node.paths.iter().cloned().collect::>()) - .unwrap_or_else(|| vec![path.clone()]); - for alias in aliases { - self.core.copy_up(&alias)?; - } - Ok(path) - } - - fn directory_snapshot(&mut self, ino: u64) -> io::Result> { - let path = self.node_path(ino)?; - let parent_path = path.parent().unwrap_or_else(|| Path::new("")); - let parent_ino = self - .by_path - .get(parent_path) - .copied() - .unwrap_or(FUSE_ROOT_ID); - let mut entries = vec![ - DirectoryEntry { - ino, - kind: FileType::Directory, - name: OsString::from("."), - attr: self.attr(ino, &path)?, - }, - DirectoryEntry { - ino: parent_ino, - kind: FileType::Directory, - name: OsString::from(".."), - attr: self - .attr(parent_ino, parent_path) - .or_else(|_| self.attr(FUSE_ROOT_ID, Path::new("")))?, - }, - ]; - for name in self.core.list_names(&path)? { - let child = OverlayCore::child(&path, &name)?; - let metadata = self.core.metadata(&child)?; - let child_ino = self.allocate_inode(child, &metadata); - entries.push(DirectoryEntry { - ino: child_ino, - kind: file_type(&metadata), - name, - attr: Self::attr_from_metadata(child_ino, &metadata), - }); - } - Ok(entries) - } - - fn open_path(&self, path: &Path, flags: i32) -> io::Result { - let writing = flags & libc::O_ACCMODE != libc::O_RDONLY - || flags & (libc::O_APPEND | libc::O_TRUNC) != 0; - let real = if writing { - self.core.copy_up(path)? - } else { - self.core - .resolve(path) - .ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))? - .path - }; - let access_mode = flags & libc::O_ACCMODE; - let mut options = OpenOptions::new(); - options - .read(access_mode != libc::O_WRONLY) - .write(access_mode != libc::O_RDONLY) - .append(flags & libc::O_APPEND != 0) - .truncate(flags & libc::O_TRUNC != 0) - .custom_flags( - flags - & !(libc::O_ACCMODE - | libc::O_CREAT - | libc::O_EXCL - | libc::O_TRUNC - | libc::O_APPEND), - ); - options.open(real) - } -} - -impl Filesystem for OverlayFs { - fn lookup(&mut self, _request: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEntry) { - let result = (|| { - let path = self.child_path(parent, name)?; - let metadata = self.core.metadata(&path)?; - let ino = self.allocate_inode(path, &metadata); - Ok((ino, metadata)) - })(); - match result { - Ok((ino, metadata)) => reply.entry(&TTL, &Self::attr_from_metadata(ino, &metadata), 0), - Err(error) => reply.error(errno(&error)), - } - } - - fn getattr(&mut self, _request: &Request<'_>, ino: u64, _fh: Option, reply: ReplyAttr) { - let result = self.node_path(ino).and_then(|path| self.attr(ino, &path)); - match result { - Ok(attr) => reply.attr(&TTL, &attr), - Err(error) => reply.error(errno(&error)), - } - } - - #[allow(clippy::too_many_arguments)] - fn setattr( - &mut self, - _request: &Request<'_>, - ino: u64, - mode: Option, - uid: Option, - gid: Option, - size: Option, - atime: Option, - mtime: Option, - _ctime: Option, - _fh: Option, - _crtime: Option, - _chgtime: Option, - _bkuptime: Option, - flags: Option, - reply: ReplyAttr, - ) { - let result = (|| { - let path = self.node_path(ino)?; - // macFUSE can report a read-induced atime update through SETATTR. - // Overlay views are mounted noatime, and an atime-only request must - // not turn every file read into a full lower-to-upper copy-up. - if !setattr_requires_copy_up(mode, uid, gid, size, atime, mtime, flags) { - return self.attr(ino, &path); - } - self.copy_up_inode(ino)?; - let upper = self.core.copy_up(&path)?; - if let Some(size) = size { - OpenOptions::new().write(true).open(&upper)?.set_len(size)?; - } - if let Some(mode) = mode { - fs::set_permissions(&upper, fs::Permissions::from_mode(mode & 0o7777))?; - } - if uid.is_some() || gid.is_some() { - let metadata = fs::symlink_metadata(&upper)?; - sys::chown( - &upper, - uid.unwrap_or_else(|| metadata.uid()), - gid.unwrap_or_else(|| metadata.gid()), - metadata.file_type().is_symlink(), - )?; - } - if atime.is_some() || mtime.is_some() { - let nofollow = fs::symlink_metadata(&upper)?.file_type().is_symlink(); - sys::set_times( - &upper, - atime.map(time_value), - mtime.map(time_value), - nofollow, - )?; - } - if let Some(flags) = flags { - #[cfg(target_os = "macos")] - sys::set_flags(&upper, flags)?; - #[cfg(not(target_os = "macos"))] - if flags != 0 { - return Err(io::Error::from_raw_os_error(libc::ENOTSUP)); - } - } - self.attr(ino, &path) - })(); - match result { - Ok(attr) => reply.attr(&TTL, &attr), - Err(error) => reply.error(errno(&error)), - } - } - - fn readlink(&mut self, _request: &Request<'_>, ino: u64, reply: ReplyData) { - let result = self.node_path(ino).and_then(|path| { - let resolved = self - .core - .resolve(&path) - .ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))?; - fs::read_link(resolved.path) - }); - match result { - Ok(target) => reply.data(target.as_os_str().as_encoded_bytes()), - Err(error) => reply.error(errno(&error)), - } - } - - fn mknod( - &mut self, - _request: &Request<'_>, - parent: u64, - name: &OsStr, - mode: u32, - umask: u32, - rdev: u32, - reply: ReplyEntry, - ) { - let result = (|| { - let path = self.child_path(parent, name)?; - self.core.create_node(&path, mode & !umask, rdev)?; - let metadata = self.core.metadata(&path)?; - let ino = self.allocate_inode(path.clone(), &metadata); - self.attr(ino, &path) - })(); - match result { - Ok(attr) => reply.entry(&TTL, &attr, 0), - Err(error) => reply.error(errno(&error)), - } - } - - fn mkdir( - &mut self, - _request: &Request<'_>, - parent: u64, - name: &OsStr, - mode: u32, - umask: u32, - reply: ReplyEntry, - ) { - let result = (|| { - let path = self.child_path(parent, name)?; - self.core.create_dir(&path, mode & !umask)?; - let metadata = self.core.metadata(&path)?; - let ino = self.allocate_inode(path.clone(), &metadata); - self.attr(ino, &path) - })(); - match result { - Ok(attr) => reply.entry(&TTL, &attr, 0), - Err(error) => reply.error(errno(&error)), - } - } - - fn unlink(&mut self, _request: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEmpty) { - let result = self - .child_path(parent, name) - .and_then(|path| self.core.remove(&path, false).map(|()| path)); - match result { - Ok(path) => { - self.remove_inode_prefix(&path); - reply.ok(); - } - Err(error) => reply.error(errno(&error)), - } - } - - fn rmdir(&mut self, _request: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEmpty) { - let result = self - .child_path(parent, name) - .and_then(|path| self.core.remove(&path, true).map(|()| path)); - match result { - Ok(path) => { - self.remove_inode_prefix(&path); - reply.ok(); - } - Err(error) => reply.error(errno(&error)), - } - } - - fn symlink( - &mut self, - _request: &Request<'_>, - parent: u64, - name: &OsStr, - target: &Path, - reply: ReplyEntry, - ) { - let result = (|| { - let path = self.child_path(parent, name)?; - self.core.create_symlink(&path, target)?; - let metadata = self.core.metadata(&path)?; - let ino = self.allocate_inode(path.clone(), &metadata); - self.attr(ino, &path) - })(); - match result { - Ok(attr) => reply.entry(&TTL, &attr, 0), - Err(error) => reply.error(errno(&error)), - } - } - - fn rename( - &mut self, - _request: &Request<'_>, - parent: u64, - name: &OsStr, - newparent: u64, - newname: &OsStr, - flags: u32, - reply: ReplyEmpty, - ) { - if flags & !(RENAME_NOREPLACE | RENAME_EXCHANGE) != 0 - || flags == (RENAME_NOREPLACE | RENAME_EXCHANGE) - { - reply.error(libc::ENOTSUP); - return; - } - let result = (|| { - let old = self.child_path(parent, name)?; - let new = self.child_path(newparent, newname)?; - if flags & RENAME_EXCHANGE != 0 { - self.core.exchange(&old, &new)?; - Ok((old, new, true)) - } else { - self.core - .rename(&old, &new, flags & RENAME_NOREPLACE != 0)?; - Ok((old, new, false)) - } - })(); - match result { - Ok((old, new, exchange)) => { - if exchange { - self.exchange_inode_prefixes(&old, &new); - } else { - self.remap_inode_prefix(&old, &new); - } - reply.ok(); - } - Err(error) => reply.error(errno(&error)), - } - } - - fn link( - &mut self, - _request: &Request<'_>, - ino: u64, - newparent: u64, - newname: &OsStr, - reply: ReplyEntry, - ) { - let result = (|| { - let source = self.node_path(ino)?; - let destination = self.child_path(newparent, newname)?; - self.core.hard_link(&source, &destination)?; - self.add_inode_alias(ino, destination.clone()); - self.attr(ino, &destination) - })(); - match result { - Ok(attr) => reply.entry(&TTL, &attr, 0), - Err(error) => reply.error(errno(&error)), - } - } - - fn open(&mut self, _request: &Request<'_>, ino: u64, flags: i32, reply: ReplyOpen) { - let writing = flags & libc::O_ACCMODE != libc::O_RDONLY - || flags & (libc::O_APPEND | libc::O_TRUNC) != 0; - let result = (if writing { - self.copy_up_inode(ino) - } else { - self.node_path(ino) - }) - .and_then(|path| self.open_path(&path, flags)); - match result { - Ok(file) => { - let handle = self.allocate_handle(); - self.open_files.insert(handle, file); - reply.opened(handle, 0); - } - Err(error) => reply.error(errno(&error)), - } - } - - fn read( - &mut self, - _request: &Request<'_>, - _ino: u64, - fh: u64, - offset: i64, - size: u32, - _flags: i32, - _lock_owner: Option, - reply: ReplyData, - ) { - if offset < 0 { - reply.error(libc::EINVAL); - return; - } - let Some(file) = self.open_files.get(&fh) else { - reply.error(libc::EBADF); - return; - }; - let mut data = vec![0; size as usize]; - match file.read_at(&mut data, offset as u64) { - Ok(read) => { - data.truncate(read); - reply.data(&data); - } - Err(error) => reply.error(errno(&error)), - } - } - - fn write( - &mut self, - _request: &Request<'_>, - _ino: u64, - fh: u64, - offset: i64, - data: &[u8], - _write_flags: u32, - _flags: i32, - _lock_owner: Option, - reply: ReplyWrite, - ) { - if offset < 0 { - reply.error(libc::EINVAL); - return; - } - let Some(file) = self.open_files.get(&fh) else { - reply.error(libc::EBADF); - return; - }; - match file.write_at(data, offset as u64) { - Ok(written) => reply.written(written as u32), - Err(error) => reply.error(errno(&error)), - } - } - - fn flush( - &mut self, - _request: &Request<'_>, - _ino: u64, - fh: u64, - _lock_owner: u64, - reply: ReplyEmpty, - ) { - if self.open_files.contains_key(&fh) { - reply.ok(); - } else { - reply.error(libc::EBADF); - } - } - - fn release( - &mut self, - _request: &Request<'_>, - _ino: u64, - fh: u64, - _flags: i32, - _lock_owner: Option, - _flush: bool, - reply: ReplyEmpty, - ) { - if self.open_files.remove(&fh).is_some() { - reply.ok(); - } else { - reply.error(libc::EBADF); - } - } - - fn fsync( - &mut self, - _request: &Request<'_>, - _ino: u64, - fh: u64, - datasync: bool, - reply: ReplyEmpty, - ) { - match self.open_files.get(&fh) { - Some(file) => match sys::fsync(file, datasync) { - Ok(()) => reply.ok(), - Err(error) => reply.error(errno(&error)), - }, - None => reply.error(libc::EBADF), - } - } - - fn opendir(&mut self, _request: &Request<'_>, ino: u64, _flags: i32, reply: ReplyOpen) { - match self.directory_snapshot(ino) { - Ok(entries) => { - let handle = self.allocate_handle(); - self.open_directories.insert(handle, entries); - reply.opened(handle, 0); - } - Err(error) => reply.error(errno(&error)), - } - } - - fn readdir( - &mut self, - _request: &Request<'_>, - _ino: u64, - fh: u64, - offset: i64, - mut reply: ReplyDirectory, - ) { - if offset < 0 { - reply.error(libc::EINVAL); - return; - } - let Some(entries) = self.open_directories.get(&fh) else { - reply.error(libc::EBADF); - return; - }; - for (index, entry) in entries.iter().enumerate().skip(offset as usize) { - if reply.add(entry.ino, (index + 1) as i64, entry.kind, &entry.name) { - break; - } - } - reply.ok(); - } - - fn readdirplus( - &mut self, - _request: &Request<'_>, - _ino: u64, - fh: u64, - offset: i64, - mut reply: ReplyDirectoryPlus, - ) { - if offset < 0 { - reply.error(libc::EINVAL); - return; - } - let Some(entries) = self.open_directories.get(&fh) else { - reply.error(libc::EBADF); - return; - }; - for (index, entry) in entries.iter().enumerate().skip(offset as usize) { - if reply.add( - entry.ino, - (index + 1) as i64, - &entry.name, - &TTL, - &entry.attr, - 0, - ) { - break; - } - } - reply.ok(); - } - - fn releasedir( - &mut self, - _request: &Request<'_>, - _ino: u64, - fh: u64, - _flags: i32, - reply: ReplyEmpty, - ) { - if self.open_directories.remove(&fh).is_some() { - reply.ok(); - } else { - reply.error(libc::EBADF); - } - } - - fn fsyncdir( - &mut self, - _request: &Request<'_>, - ino: u64, - _fh: u64, - datasync: bool, - reply: ReplyEmpty, - ) { - let result = self.node_path(ino).and_then(|path| { - let upper = self.core.copy_up(&path)?; - let directory = File::open(upper)?; - sys::fsync(&directory, datasync) - }); - match result { - Ok(()) => reply.ok(), - Err(error) => reply.error(errno(&error)), - } - } - - fn statfs(&mut self, _request: &Request<'_>, _ino: u64, reply: ReplyStatfs) { - match sys::statfs(self.core.upper()) { - Ok(stat) => reply.statfs( - stat.blocks, - stat.bfree, - stat.bavail, - stat.files, - stat.ffree, - stat.bsize, - stat.namelen, - stat.frsize, - ), - Err(error) => reply.error(errno(&error)), - } - } - - fn setxattr( - &mut self, - _request: &Request<'_>, - ino: u64, - name: &OsStr, - value: &[u8], - flags: i32, - position: u32, - reply: ReplyEmpty, - ) { - if position != 0 { - reply.error(libc::ENOTSUP); - return; - } - let result = self - .copy_up_inode(ino) - .and_then(|path| self.core.copy_up(&path)) - .and_then(|path| sys::set_xattr(&path, name, value, flags)); - match result { - Ok(()) => reply.ok(), - Err(error) => reply.error(errno(&error)), - } - } - - fn getxattr( - &mut self, - _request: &Request<'_>, - ino: u64, - name: &OsStr, - size: u32, - reply: ReplyXattr, - ) { - let result = self.node_path(ino).and_then(|path| { - let real = self - .core - .resolve(&path) - .ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))?; - sys::get_xattr(&real.path, name) - }); - match result { - Ok(value) if size == 0 => reply.size(value.len() as u32), - Ok(value) if value.len() <= size as usize => reply.data(&value), - Ok(_) => reply.error(libc::ERANGE), - Err(error) => reply.error(errno(&error)), - } - } - - fn listxattr(&mut self, _request: &Request<'_>, ino: u64, size: u32, reply: ReplyXattr) { - let result = self.node_path(ino).and_then(|path| { - let real = self - .core - .resolve(&path) - .ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))?; - let names = sys::list_xattrs(&real.path)?; - let mut encoded = Vec::new(); - for name in names { - encoded.extend_from_slice(&name); - encoded.push(0); - } - Ok(encoded) - }); - match result { - Ok(value) if size == 0 => reply.size(value.len() as u32), - Ok(value) if value.len() <= size as usize => reply.data(&value), - Ok(_) => reply.error(libc::ERANGE), - Err(error) => reply.error(errno(&error)), - } - } - - fn removexattr(&mut self, _request: &Request<'_>, ino: u64, name: &OsStr, reply: ReplyEmpty) { - let result = self - .copy_up_inode(ino) - .and_then(|path| self.core.copy_up(&path)) - .and_then(|path| sys::remove_xattr(&path, name)); - match result { - Ok(()) => reply.ok(), - Err(error) => reply.error(errno(&error)), - } - } - - fn access(&mut self, _request: &Request<'_>, ino: u64, mask: i32, reply: ReplyEmpty) { - let result = self.node_path(ino).and_then(|path| { - let real = self - .core - .resolve(&path) - .ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))?; - sys::access(&real.path, mask) - }); - match result { - Ok(()) => reply.ok(), - Err(error) => reply.error(errno(&error)), - } - } - - fn create( - &mut self, - _request: &Request<'_>, - parent: u64, - name: &OsStr, - mode: u32, - umask: u32, - flags: i32, - reply: ReplyCreate, - ) { - let result = (|| { - let path = self.child_path(parent, name)?; - let file = self.core.create_file(&path, mode & !umask, flags)?; - let metadata = self.core.metadata(&path)?; - let ino = self.allocate_inode(path.clone(), &metadata); - let attr = self.attr(ino, &path)?; - Ok((file, attr)) - })(); - match result { - Ok((file, attr)) => { - let handle = self.allocate_handle(); - self.open_files.insert(handle, file); - reply.created(&TTL, &attr, 0, handle, 0); - } - Err(error) => reply.error(errno(&error)), - } - } - - fn fallocate( - &mut self, - _request: &Request<'_>, - _ino: u64, - fh: u64, - offset: i64, - length: i64, - mode: i32, - reply: ReplyEmpty, - ) { - if offset < 0 || length < 0 { - reply.error(libc::EINVAL); - return; - } - if mode != 0 { - reply.error(libc::ENOTSUP); - return; - } - let Some(file) = self.open_files.get(&fh) else { - reply.error(libc::EBADF); - return; - }; - let end = (offset as u64).saturating_add(length as u64); - let result = file.metadata().and_then(|metadata| { - if metadata.len() < end { - file.set_len(end) - } else { - Ok(()) - } - }); - match result { - Ok(()) => reply.ok(), - Err(error) => reply.error(errno(&error)), - } - } - - fn lseek( - &mut self, - _request: &Request<'_>, - _ino: u64, - fh: u64, - offset: i64, - whence: i32, - reply: ReplyLseek, - ) { - match self.open_files.get(&fh) { - Some(file) => match sys::seek(file, offset, whence) { - Ok(offset) => reply.offset(offset), - Err(error) => reply.error(errno(&error)), - }, - None => reply.error(libc::EBADF), - } - } - - #[allow(clippy::too_many_arguments)] - fn copy_file_range( - &mut self, - _request: &Request<'_>, - _ino_in: u64, - fh_in: u64, - offset_in: i64, - _ino_out: u64, - fh_out: u64, - offset_out: i64, - len: u64, - flags: u32, - reply: ReplyWrite, - ) { - if offset_in < 0 || offset_out < 0 || flags != 0 { - reply.error(libc::EINVAL); - return; - } - let Some(input) = self - .open_files - .get(&fh_in) - .and_then(|file| file.try_clone().ok()) - else { - reply.error(libc::EBADF); - return; - }; - let Some(output) = self - .open_files - .get(&fh_out) - .and_then(|file| file.try_clone().ok()) - else { - reply.error(libc::EBADF); - return; - }; - let mut copied = 0_u64; - let mut buffer = vec![0_u8; (len.min(128 * 1024)) as usize]; - let result = (|| { - while copied < len { - let wanted = (len - copied).min(buffer.len() as u64) as usize; - let read = input.read_at( - &mut buffer[..wanted], - (offset_in as u64).saturating_add(copied), - )?; - if read == 0 { - break; - } - let mut written = 0; - while written < read { - let amount = output.write_at( - &buffer[written..read], - (offset_out as u64) - .saturating_add(copied) - .saturating_add(written as u64), - )?; - if amount == 0 { - return Err(io::Error::new( - io::ErrorKind::WriteZero, - "copy_file_range made no progress", - )); - } - written += amount; - } - copied += read as u64; - } - Ok::<(), io::Error>(()) - })(); - match result { - Ok(()) => reply.written(copied.min(u32::MAX as u64) as u32), - Err(error) => reply.error(errno(&error)), - } - } - - #[cfg(target_os = "macos")] - fn exchange( - &mut self, - _request: &Request<'_>, - parent: u64, - name: &OsStr, - newparent: u64, - newname: &OsStr, - _options: u64, - reply: ReplyEmpty, - ) { - let result = (|| { - let first = self.child_path(parent, name)?; - let second = self.child_path(newparent, newname)?; - self.core.exchange(&first, &second)?; - Ok((first, second)) - })(); - match result { - Ok((first, second)) => { - self.exchange_inode_prefixes(&first, &second); - reply.ok(); - } - Err(error) => reply.error(errno(&error)), - } - } - - #[cfg(target_os = "macos")] - fn getxtimes(&mut self, _request: &Request<'_>, ino: u64, reply: ReplyXTimes) { - let result = self - .node_path(ino) - .and_then(|path| self.core.metadata(&path)); - match result { - Ok(metadata) => { - let created = metadata.created().unwrap_or(UNIX_EPOCH); - reply.xtimes(created, created); - } - Err(error) => reply.error(errno(&error)), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn atime_only_setattr_does_not_require_copy_up() { - assert!(!setattr_requires_copy_up( - None, - None, - None, - None, - Some(TimeOrNow::Now), - None, - None - )); - assert!(setattr_requires_copy_up( - None, - None, - None, - None, - None, - Some(TimeOrNow::Now), - None - )); - } -} diff --git a/crates/persisting-overlayfs/src/jj_backend.rs b/crates/persisting-overlayfs/src/jj_backend.rs deleted file mode 100644 index 758eddfb0..000000000 --- a/crates/persisting-overlayfs/src/jj_backend.rs +++ /dev/null @@ -1,300 +0,0 @@ -//! Jujutsu-managed directory uppers. -//! -//! Each overlay fork is a Jujutsu workspace. All workspaces point at the same -//! repository, so their working-copy commits, operation log, and Git objects -//! live in one store while the writable POSIX directories remain independent. - -use jj_lib::config::StackedConfig; -use jj_lib::gitignore::GitIgnoreFile; -use jj_lib::lock::FileLock; -use jj_lib::matchers::EverythingMatcher; -use jj_lib::object_id::ObjectId as _; -use jj_lib::ref_name::{WorkspaceName, WorkspaceNameBuf}; -use jj_lib::repo::{Repo as _, StoreFactories}; -use jj_lib::settings::UserSettings; -use jj_lib::working_copy::SnapshotOptions; -use jj_lib::workspace::{Workspace, default_working_copy_factories, default_working_copy_factory}; -use pollster::FutureExt as _; -use std::fs; -use std::io; -use std::path::{Path, PathBuf}; - -const CONTROL_DIR: &str = "control"; -const WORKSPACES_DIR: &str = "workspaces"; -const UPPER_DIR: &str = "upper"; - -fn io_other(error: impl std::fmt::Display) -> io::Error { - io::Error::other(error.to_string()) -} - -fn settings() -> io::Result { - UserSettings::from_config(StackedConfig::with_defaults()).map_err(io_other) -} - -fn validate_fork(fork: &str) -> io::Result<()> { - if fork.is_empty() - || fork == "." - || fork == ".." - || fork.contains('/') - || fork.contains('\\') - || fork.as_bytes().contains(&0) - { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!("invalid Jujutsu overlay workspace name: {fork:?}"), - )); - } - Ok(()) -} - -fn load_workspace(settings: &UserSettings, root: &Path) -> io::Result { - Workspace::load( - settings, - root, - &StoreFactories::default(), - &default_working_copy_factories(), - ) - .map_err(io_other) -} - -/// One writable OverlayFS fork backed by a workspace in a shared Jujutsu repo. -pub(crate) struct JujutsuWorkspace { - store_path: PathBuf, - workspace_root: PathBuf, - upper_dir: PathBuf, - fork: WorkspaceNameBuf, - // Held for the lifetime of a writable mount. Jujutsu's normal working-copy - // lock only covers an individual snapshot, while FUSE writes happen between - // snapshots and must not have two writers for the same workspace. - _mount_lock: Option, -} - -impl std::fmt::Debug for JujutsuWorkspace { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter - .debug_struct("JujutsuWorkspace") - .field("store_path", &self.store_path) - .field("workspace_root", &self.workspace_root) - .field("upper_dir", &self.upper_dir) - .field("fork", &self.fork.as_str()) - .finish_non_exhaustive() - } -} - -impl JujutsuWorkspace { - pub(crate) fn open(store_path: PathBuf, fork: String, read_only: bool) -> io::Result { - validate_fork(&fork)?; - fs::create_dir_all(&store_path)?; - let store_path = fs::canonicalize(store_path)?; - let control_root = store_path.join(CONTROL_DIR); - let workspace_root = store_path.join(WORKSPACES_DIR).join(&fork); - let upper_dir = workspace_root.join(UPPER_DIR); - let settings = settings()?; - - // Serialize repository/workspace creation across processes. The lock is - // dropped before the mount begins; Jujutsu handles later op-log writes. - let _init_lock = FileLock::lock(store_path.join("init.lock")).map_err(io_other)?; - if !control_root.join(".jj").is_dir() { - fs::create_dir_all(&control_root)?; - Workspace::init_internal_git(&settings, &control_root) - .block_on() - .map_err(io_other)?; - } - if !workspace_root.join(".jj").is_dir() { - fs::create_dir_all(&workspace_root)?; - let control = load_workspace(&settings, &control_root)?; - let repo = control - .repo_loader() - .load_at_head() - .block_on() - .map_err(io_other)?; - Workspace::init_workspace_with_existing_repo( - &workspace_root, - control.repo_path(), - &repo, - &*default_working_copy_factory(), - WorkspaceNameBuf::from(fork.clone()), - ) - .block_on() - .map_err(io_other)?; - } - fs::create_dir_all(&upper_dir)?; - let _mount_lock = if read_only { - None - } else { - Some( - FileLock::try_lock(workspace_root.join(".jj").join("persisting-overlay.lock")) - .map_err(io_other)? - .ok_or_else(|| { - io::Error::new( - io::ErrorKind::WouldBlock, - format!( - "Jujutsu overlay workspace {fork:?} is already mounted writable" - ), - ) - })?, - ) - }; - - Ok(Self { - store_path, - workspace_root, - upper_dir, - fork: WorkspaceNameBuf::from(fork), - _mount_lock, - }) - } - - pub(crate) fn upper_dir(&self) -> &Path { - &self.upper_dir - } - - /// Snapshot the upper directory into this workspace's working-copy commit. - pub(crate) fn snapshot(&self) -> io::Result> { - snapshot_workspace(&self.workspace_root, &self.fork) - } -} - -fn snapshot_workspace( - workspace_root: &Path, - expected_name: &WorkspaceName, -) -> io::Result> { - let settings = settings()?; - let mut workspace = load_workspace(&settings, workspace_root)?; - if workspace.workspace_name() != expected_name { - return Err(io::Error::other(format!( - "Jujutsu workspace name mismatch: expected {:?}, found {:?}", - expected_name.as_str(), - workspace.workspace_name().as_str() - ))); - } - let repo = workspace - .repo_loader() - .load_at_head() - .block_on() - .map_err(io_other)?; - let wc_commit_id = repo - .view() - .get_wc_commit_id(expected_name) - .ok_or_else(|| io::Error::other("Jujutsu workspace has no working-copy commit"))? - .clone(); - let old_commit = repo.store().get_commit(&wc_commit_id).map_err(io_other)?; - - let everything = EverythingMatcher; - let options = SnapshotOptions { - base_ignores: GitIgnoreFile::empty(), - progress: None, - start_tracking_matcher: &everything, - // Overlay snapshots are exact filesystem state, not source-control - // intent. A file hidden by an upper-layer .gitignore must still be - // recoverable from this workspace head. - force_tracking_matcher: &everything, - max_new_file_size: u64::MAX, - }; - let mut locked = workspace - .start_working_copy_mutation() - .block_on() - .map_err(io_other)?; - let (new_tree, _stats) = locked - .locked_wc() - .snapshot(&options) - .block_on() - .map_err(io_other)?; - - if new_tree.tree_ids() == old_commit.tree().tree_ids() { - locked - .finish(repo.operation().id().clone()) - .block_on() - .map_err(io_other)?; - return Ok(None); - } - - let mut transaction = repo.start_transaction(); - transaction.set_workspace_name(expected_name); - transaction.set_is_snapshot(true); - let new_commit = transaction - .repo_mut() - .rewrite_commit(&old_commit) - .set_tree(new_tree) - .write() - .block_on() - .map_err(io_other)?; - transaction - .repo_mut() - .rebase_descendants() - .block_on() - .map_err(io_other)?; - let new_repo = transaction - .commit(format!( - "snapshot persisting OverlayFS workspace {}", - expected_name.as_str() - )) - .block_on() - .map_err(io_other)?; - locked - .finish(new_repo.operation().id().clone()) - .block_on() - .map_err(io_other)?; - Ok(Some(new_commit.id().hex())) -} - -/// Snapshot a named fork after an out-of-band apply or discard operation. -pub fn snapshot_jujutsu_upper(store_path: &Path, fork: &str) -> io::Result> { - let workspace = JujutsuWorkspace::open(store_path.to_owned(), fork.to_owned(), false)?; - workspace.snapshot() -} - -/// Deterministic directory used as the live upper for a named fork. -pub fn jujutsu_upper_dir(store_path: &Path, fork: &str) -> io::Result { - validate_fork(fork)?; - Ok(store_path.join(WORKSPACES_DIR).join(fork).join(UPPER_DIR)) -} - -/// Initialize a Jujutsu-backed upper for a mountless virtio-fs consumer. -pub fn prepare_jujutsu_upper(store_path: &Path, fork: &str) -> io::Result { - let workspace = JujutsuWorkspace::open(store_path.to_owned(), fork.to_owned(), false)?; - Ok(workspace.upper_dir().to_path_buf()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn two_forks_share_one_repository_and_keep_independent_heads() { - let temp = tempfile::tempdir().unwrap(); - let store = temp.path().join("overlay.jj"); - let first = JujutsuWorkspace::open(store.clone(), "first".into(), false).unwrap(); - fs::write(first.upper_dir().join("value"), b"first").unwrap(); - let first_commit = first.snapshot().unwrap().unwrap(); - drop(first); - - let second = JujutsuWorkspace::open(store.clone(), "second".into(), false).unwrap(); - fs::write(second.upper_dir().join("value"), b"second").unwrap(); - let second_commit = second.snapshot().unwrap().unwrap(); - assert_ne!(first_commit, second_commit); - assert_eq!( - fs::read(store.join("workspaces/first/upper/value")).unwrap(), - b"first" - ); - assert_eq!( - fs::read(store.join("workspaces/second/upper/value")).unwrap(), - b"second" - ); - - let settings = settings().unwrap(); - let control = load_workspace(&settings, &store.join(CONTROL_DIR)).unwrap(); - let repo = control.repo_loader().load_at_head().block_on().unwrap(); - let workspaces = repo.view().wc_commit_ids(); - assert!(workspaces.contains_key(&WorkspaceNameBuf::from("first"))); - assert!(workspaces.contains_key(&WorkspaceNameBuf::from("second"))); - assert_eq!(control.repo_path(), store.join("control/.jj/repo")); - } - - #[test] - fn fork_name_cannot_escape_store() { - let temp = tempfile::tempdir().unwrap(); - assert!(JujutsuWorkspace::open(temp.path().into(), "../escape".into(), false).is_err()); - assert!(jujutsu_upper_dir(temp.path(), "nested/name").is_err()); - } -} diff --git a/crates/persisting-overlayfs/src/jj_disabled.rs b/crates/persisting-overlayfs/src/jj_disabled.rs deleted file mode 100644 index c8fec7217..000000000 --- a/crates/persisting-overlayfs/src/jj_disabled.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! Lightweight build stub for the optional Jujutsu upper backend. - -use std::io; -use std::path::{Path, PathBuf}; - -const WORKSPACES_DIR: &str = "workspaces"; -const UPPER_DIR: &str = "upper"; - -fn unsupported() -> io::Error { - io::Error::new( - io::ErrorKind::Unsupported, - "the Jujutsu OverlayFS backend is not compiled in; enable the `jujutsu` feature", - ) -} - -fn validate_fork(fork: &str) -> io::Result<()> { - if fork.is_empty() - || fork == "." - || fork == ".." - || fork.contains('/') - || fork.contains('\\') - || fork.as_bytes().contains(&0) - { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!("invalid Jujutsu overlay workspace name: {fork:?}"), - )); - } - Ok(()) -} - -#[derive(Debug)] -pub(crate) struct JujutsuWorkspace { - upper_dir: PathBuf, -} - -impl JujutsuWorkspace { - pub(crate) fn open(_store_path: PathBuf, _fork: String, _read_only: bool) -> io::Result { - Err(unsupported()) - } - - pub(crate) fn upper_dir(&self) -> &Path { - &self.upper_dir - } - - pub(crate) fn snapshot(&self) -> io::Result> { - Err(unsupported()) - } -} - -pub fn snapshot_jujutsu_upper(_store_path: &Path, _fork: &str) -> io::Result> { - Err(unsupported()) -} - -/// Return the deterministic upper path without initializing a Jujutsu store. -/// Actual mounting still fails with an explicit feature error in this build. -pub fn jujutsu_upper_dir(store_path: &Path, fork: &str) -> io::Result { - validate_fork(fork)?; - Ok(store_path.join(WORKSPACES_DIR).join(fork).join(UPPER_DIR)) -} - -pub fn prepare_jujutsu_upper(_store_path: &Path, _fork: &str) -> io::Result { - Err(unsupported()) -} diff --git a/crates/persisting-overlayfs/src/lib.rs b/crates/persisting-overlayfs/src/lib.rs deleted file mode 100644 index 027778ae8..000000000 --- a/crates/persisting-overlayfs/src/lib.rs +++ /dev/null @@ -1,401 +0,0 @@ -//! Embeddable cross-platform FUSE overlay implementation. -//! -//! pVisor owns [`OverlaySession`] directly, so the pVisor process is also the -//! FUSE userspace server. The `persisting-overlayfs` binary is only a debugging -//! and manual-mount CLI wrapper around this library. - -mod fs; -#[cfg(feature = "jujutsu")] -mod jj_backend; -#[cfg(not(feature = "jujutsu"))] -#[path = "jj_disabled.rs"] -mod jj_backend; - -use anyhow::{Context, Result, bail}; -use fs::OverlayFs; -use fuser::{BackgroundSession, MountOption, Session}; -use jj_backend::JujutsuWorkspace; -pub use jj_backend::{jujutsu_upper_dir, prepare_jujutsu_upper, snapshot_jujutsu_upper}; -use std::os::unix::fs::MetadataExt; -use std::path::{Path, PathBuf}; -use std::time::Duration; - -#[derive(Clone, Debug)] -pub enum UpperBackend { - Directory { - upper_dir: PathBuf, - work_dir: Option, - }, - Jujutsu { - store_path: PathBuf, - workspace: String, - }, -} - -#[derive(Clone, Debug)] -pub struct OverlayMountConfig { - pub lower_dirs: Vec, - pub upper: UpperBackend, - pub mountpoint: PathBuf, - pub allow_other: bool, - pub allow_root: bool, - pub default_permissions: bool, - pub read_only: bool, - pub fsname: String, - /// macFUSE backend (`kernel` or `fskit`). Ignored on non-macOS hosts. - pub backend: Option, - pub debug: bool, - /// Optional durable first-touch journal used to reject apply conflicts. - pub preimage_dir: Option, - /// Paths relative to the overlay root that are absent from the mounted - /// namespace. Exclusions apply to every lower and the writable upper and - /// cannot be recreated from inside the mount. - pub excluded_paths: Vec, -} - -impl OverlayMountConfig { - pub fn new( - lower_dirs: Vec, - upper_dir: PathBuf, - work_dir: Option, - mountpoint: PathBuf, - ) -> Self { - Self { - lower_dirs, - upper: UpperBackend::Directory { - upper_dir, - work_dir, - }, - mountpoint, - allow_other: false, - allow_root: false, - default_permissions: true, - read_only: false, - fsname: "persisting-overlayfs".into(), - backend: None, - debug: false, - preimage_dir: None, - excluded_paths: Vec::new(), - } - } - - pub fn new_jujutsu( - lower_dirs: Vec, - store_path: PathBuf, - workspace: String, - mountpoint: PathBuf, - ) -> Self { - let mut config = Self::new(lower_dirs, PathBuf::new(), None, mountpoint); - config.upper = UpperBackend::Jujutsu { - store_path, - workspace, - }; - config - } -} - -#[derive(Debug)] -pub struct OverlaySession { - background: Option, - jujutsu: Option, - mountpoint: PathBuf, -} - -impl OverlaySession { - pub fn mountpoint(&self) -> &Path { - &self.mountpoint - } - - pub fn has_exited(&self) -> bool { - self.background - .as_ref() - .is_none_or(|session| session.guard.is_finished()) - } - - /// Unmount by dropping the libfuse mount owned by this process. - pub fn unmount(mut self) -> Result<()> { - self.unmount_inner() - } - - fn unmount_inner(&mut self) -> Result<()> { - if let Some(background) = self.background.take() { - background - .unmount() - .context("unmount FUSE session and stop request loop")?; - for _ in 0..250 { - if !is_mountpoint(&self.mountpoint) { - break; - } - std::thread::sleep(Duration::from_millis(20)); - } - if is_mountpoint(&self.mountpoint) { - bail!("FUSE mount did not detach: {}", self.mountpoint.display()); - } - } - if let Some(workspace) = self.jujutsu.take() { - workspace - .snapshot() - .context("snapshot Jujutsu overlay workspace")?; - } - Ok(()) - } -} - -impl Drop for OverlaySession { - fn drop(&mut self) { - let _ = self.unmount_inner(); - } -} - -pub fn mount(config: OverlayMountConfig) -> Result { - let (filesystem, mountpoint, options, jujutsu) = prepare(config)?; - let session = Session::new(filesystem, &mountpoint, &options) - .with_context(|| format!("mount {}", mountpoint.display()))?; - let background = BackgroundSession::new(session).context("start FUSE request loop")?; - log::info!("persisting-overlayfs mounted at {}", mountpoint.display()); - Ok(OverlaySession { - background: Some(background), - jujutsu, - mountpoint, - }) -} - -pub fn run_foreground(config: OverlayMountConfig) -> Result<()> { - let (filesystem, mountpoint, options, jujutsu) = prepare(config)?; - log::info!("persisting-overlayfs mounted at {}", mountpoint.display()); - let mut session = Session::new(filesystem, &mountpoint, &options) - .with_context(|| format!("mount {}", mountpoint.display()))?; - session.run().context("FUSE session")?; - if let Some(workspace) = jujutsu { - workspace - .snapshot() - .context("snapshot Jujutsu overlay workspace")?; - } - Ok(()) -} - -fn prepare( - mut config: OverlayMountConfig, -) -> Result<( - OverlayFs, - PathBuf, - Vec, - Option, -)> { - if config.lower_dirs.is_empty() { - bail!("lowerdir must list at least one path"); - } - match &config.upper { - UpperBackend::Directory { - upper_dir, - work_dir, - } => { - std::fs::create_dir_all(upper_dir) - .with_context(|| format!("create upperdir {}", upper_dir.display()))?; - if let Some(work) = work_dir { - std::fs::create_dir_all(work) - .with_context(|| format!("create workdir {}", work.display()))?; - } - } - UpperBackend::Jujutsu { store_path, .. } => { - std::fs::create_dir_all(store_path) - .with_context(|| format!("create Jujutsu store {}", store_path.display()))?; - } - } - let fskit = config.backend.as_deref() == Some("fskit"); - if let Some(backend) = &config.backend - && !matches!(backend.as_str(), "kernel" | "fskit") - { - bail!("unsupported macFUSE backend: {backend}"); - } - if !fskit { - std::fs::create_dir_all(&config.mountpoint) - .with_context(|| format!("create mountpoint {}", config.mountpoint.display()))?; - } - - config.upper = match config.upper { - UpperBackend::Directory { - upper_dir, - work_dir, - } => UpperBackend::Directory { - upper_dir: std::fs::canonicalize(upper_dir)?, - work_dir: work_dir - .map(std::fs::canonicalize) - .transpose() - .context("canonicalize workdir")?, - }, - UpperBackend::Jujutsu { - store_path, - workspace, - } => UpperBackend::Jujutsu { - store_path: std::fs::canonicalize(store_path)?, - workspace, - }, - }; - config.lower_dirs = config - .lower_dirs - .into_iter() - .map(std::fs::canonicalize) - .collect::>>() - .context("canonicalize lowerdir")?; - let mountpoint = if fskit && !config.mountpoint.exists() { - let parent = config - .mountpoint - .parent() - .context("FSKit mountpoint must have a parent")?; - let name = config - .mountpoint - .file_name() - .context("FSKit mountpoint must have a final component")?; - std::fs::canonicalize(parent)?.join(name) - } else { - std::fs::canonicalize(&config.mountpoint)? - }; - if fskit && !mountpoint.starts_with("/Volumes") { - bail!("macFUSE FSKit mountpoints must be under /Volumes"); - } - - let hidden_from_lower = |lower: &Path, candidate: &Path| { - candidate.strip_prefix(lower).is_ok_and(|relative| { - !relative.as_os_str().is_empty() - && config - .excluded_paths - .iter() - .any(|hidden| relative == hidden || relative.starts_with(hidden)) - }) - }; - for lower in &config.lower_dirs { - if !lower.is_dir() { - bail!("lowerdir is not a directory: {}", lower.display()); - } - let upper_overlaps = match &config.upper { - UpperBackend::Directory { upper_dir, .. } => { - (upper_dir.starts_with(lower) && !hidden_from_lower(lower, upper_dir)) - || lower.starts_with(upper_dir) - } - UpperBackend::Jujutsu { store_path, .. } => { - (store_path.starts_with(lower) && !hidden_from_lower(lower, store_path)) - || lower.starts_with(store_path) - } - }; - let mount_overlaps = (mountpoint.starts_with(lower) - && !hidden_from_lower(lower, &mountpoint)) - || lower.starts_with(&mountpoint); - if upper_overlaps || mount_overlaps { - bail!( - "lowerdir must not overlap upperdir or mountpoint: {}", - lower.display() - ); - } - } - if let UpperBackend::Directory { - upper_dir, - work_dir, - } = &config.upper - { - if mountpoint.starts_with(upper_dir) || upper_dir.starts_with(&mountpoint) { - bail!("upperdir and mountpoint must not overlap"); - } - if let Some(work) = work_dir { - if std::fs::metadata(upper_dir)?.dev() != std::fs::metadata(work)?.dev() { - bail!( - "upperdir and workdir must be on the same filesystem: {} and {}", - upper_dir.display(), - work.display() - ); - } - if upper_dir == work { - bail!("upperdir and workdir must be different directories"); - } - if mountpoint.starts_with(work) - || work.starts_with(&mountpoint) - || config.lower_dirs.iter().any(|lower| { - (work.starts_with(lower) && !hidden_from_lower(lower, work)) - || lower.starts_with(work) - }) - { - bail!("workdir must not overlap lowerdir or mountpoint"); - } - } - } - - let mut jujutsu = None; - let preimage_dir = config.preimage_dir; - let filesystem = match config.upper { - UpperBackend::Directory { - upper_dir, - work_dir, - } => { - if config.excluded_paths.is_empty() && preimage_dir.is_none() { - OverlayFs::new(config.lower_dirs, upper_dir, work_dir)? - } else { - OverlayFs::new_with_exclusions_and_preimages( - config.lower_dirs, - upper_dir, - work_dir, - config.excluded_paths, - preimage_dir, - )? - } - } - UpperBackend::Jujutsu { - store_path, - workspace, - } => { - let workspace = JujutsuWorkspace::open(store_path, workspace, config.read_only)?; - let upper_dir = workspace.upper_dir().to_path_buf(); - let filesystem = if config.excluded_paths.is_empty() && preimage_dir.is_none() { - OverlayFs::new(config.lower_dirs, upper_dir, None)? - } else { - OverlayFs::new_with_exclusions_and_preimages( - config.lower_dirs, - upper_dir, - None, - config.excluded_paths, - preimage_dir, - )? - }; - if !config.read_only { - jujutsu = Some(workspace); - } - filesystem - } - }; - // Access time is not part of a pVisor changeset. Disabling it also avoids - // macFUSE issuing read-induced SETATTR requests that would otherwise force - // lower files into the writable upper. - let mut options = vec![MountOption::FSName(config.fsname), MountOption::NoAtime]; - if config.debug { - options.push(MountOption::CUSTOM("debug".into())); - } - if let Some(backend) = config.backend { - options.push(MountOption::CUSTOM(format!("backend={backend}"))); - } - if config.default_permissions { - options.push(MountOption::DefaultPermissions); - } - if config.allow_other { - options.push(MountOption::AllowOther); - } - if config.allow_root { - options.push(MountOption::AllowRoot); - } - if config.read_only { - options.push(MountOption::RO); - } - Ok((filesystem, mountpoint, options, jujutsu)) -} - -fn is_mountpoint(path: &Path) -> bool { - let Ok(metadata) = std::fs::metadata(path) else { - return false; - }; - let Some(parent) = path.parent() else { - return true; - }; - let Ok(parent_metadata) = std::fs::metadata(parent) else { - return false; - }; - metadata.dev() != parent_metadata.dev() - || (metadata.dev() == parent_metadata.dev() && metadata.ino() == parent_metadata.ino()) -} diff --git a/crates/persisting-overlayfs/src/main.rs b/crates/persisting-overlayfs/src/main.rs deleted file mode 100644 index 2e5acf5da..000000000 --- a/crates/persisting-overlayfs/src/main.rs +++ /dev/null @@ -1,240 +0,0 @@ -//! Cross-platform FUSE overlay for pVisor. -//! -//! Portable overlay filesystem semantics: -//! - Reads resolve upper → lowers (top to bottom) -//! - Writes copy-up into `upper` -//! - Deletes create `.wh.` when the name exists in a lower -//! - Opaque dirs use `.wh..wh..opq` -//! -//! Compatible with pVisor's `apply_overlay` whiteout handling. - -use anyhow::{Context, Result, bail}; -use clap::Parser; -use persisting_overlayfs::{OverlayMountConfig, run_foreground}; -use std::path::PathBuf; - -#[derive(Debug, Parser)] -#[command( - name = "persisting-overlayfs", - about = "Cross-platform FUSE overlay for pVisor (macFUSE / libfuse)" -)] -struct Args { - /// Mount options: lowerdir=a:b plus upperdir=u or jjstore=s,jjworkspace=w. - #[arg(short = 'o', long = "options", value_name = "OPTS")] - #[arg(required = true)] - options: Vec, - /// Mount point (merged view). - #[arg(value_name = "MOUNTPOINT")] - mountpoint: PathBuf, - /// Debug logging. - #[arg(short = 'd', long = "debug", default_value_t = false)] - debug: bool, -} - -#[derive(Debug)] -struct MountOpts { - lowerdir: Vec, - upperdir: Option, - jjstore: Option, - jjworkspace: Option, - workdir: Option, - allow_other: bool, - allow_root: bool, - default_permissions: bool, - read_only: bool, - fsname: String, - backend: Option, -} - -fn split_escaped(raw: &str, separator: char) -> Vec { - let mut values = Vec::new(); - let mut current = String::new(); - let mut characters = raw.chars().peekable(); - while let Some(character) = characters.next() { - if character == '\\' { - match characters.peek().copied() { - Some(next) if next == separator || next == '\\' => { - let _ = characters.next(); - current.push(next); - } - _ => current.push(character), - } - } else if character == separator { - values.push(std::mem::take(&mut current)); - } else { - current.push(character); - } - } - values.push(current); - values -} - -fn parse_options(raw: &str) -> Result { - let mut lowerdir = None; - let mut upperdir = None; - let mut jjstore = None; - let mut jjworkspace = None; - let mut workdir = None; - let mut allow_other = false; - let mut allow_root = false; - let mut default_permissions = true; - let mut read_only = false; - let mut fsname = "persisting-overlayfs".to_string(); - let mut backend = None; - for part in split_escaped(raw, ',') { - let part = part.trim(); - if part.is_empty() { - continue; - } - let Some((k, v)) = part.split_once('=') else { - match part { - "allow_other" => allow_other = true, - "allow_root" => allow_root = true, - "default_permissions" => default_permissions = true, - "nodefault_permissions" => default_permissions = false, - "ro" => read_only = true, - "rw" => read_only = false, - _ => log::debug!("ignoring unsupported mount option: {part}"), - } - continue; - }; - match k { - "lowerdir" => { - lowerdir = Some( - split_escaped(v, ':') - .into_iter() - .filter(|s| !s.is_empty()) - .map(PathBuf::from) - .collect::>(), - ); - } - "upperdir" => upperdir = Some(PathBuf::from(v)), - "jjstore" => jjstore = Some(PathBuf::from(v)), - "jjworkspace" => jjworkspace = Some(v.to_owned()), - "workdir" => workdir = Some(PathBuf::from(v)), - "fsname" => fsname = v.to_string(), - "backend" if matches!(v, "kernel" | "fskit") => backend = Some(v.to_string()), - "backend" => bail!("unsupported macFUSE backend: {v}"), - _ => log::debug!("ignoring unsupported mount option: {part}"), - } - } - let lowerdir = lowerdir.context("missing lowerdir=")?; - if lowerdir.is_empty() { - bail!("lowerdir must list at least one path"); - } - let backend_count = usize::from(upperdir.is_some()) + usize::from(jjstore.is_some()); - if backend_count == 0 { - bail!("missing upper backend: specify upperdir= or jjstore="); - } - if backend_count != 1 { - bail!("upperdir= and jjstore= are mutually exclusive"); - } - if upperdir.is_none() && workdir.is_some() { - bail!("workdir= is only valid with the directory upper backend"); - } - if jjstore.is_some() && jjworkspace.as_deref().is_none_or(str::is_empty) { - bail!("jjworkspace= is required with jjstore="); - } - if jjstore.is_none() && jjworkspace.is_some() { - bail!("jjworkspace= is only valid with jjstore="); - } - Ok(MountOpts { - lowerdir, - upperdir, - jjstore, - jjworkspace, - workdir, - allow_other, - allow_root, - default_permissions, - read_only, - fsname, - backend, - }) -} - -fn main() -> Result<()> { - let args = Args::parse(); - let level = if args.debug { - log::LevelFilter::Debug - } else { - log::LevelFilter::Warn - }; - env_logger::Builder::new() - .filter_level(level) - .parse_default_env() - .format_timestamp(None) - .init(); - - let opts = parse_options(&args.options.join(","))?; - let mut config = match (opts.upperdir, opts.jjstore) { - (Some(upperdir), None) => { - OverlayMountConfig::new(opts.lowerdir, upperdir, opts.workdir, args.mountpoint) - } - (None, Some(store)) => OverlayMountConfig::new_jujutsu( - opts.lowerdir, - store, - opts.jjworkspace - .expect("parse_options requires jjworkspace"), - args.mountpoint, - ), - _ => unreachable!("parse_options validates the upper backend"), - }; - config.allow_other = opts.allow_other; - config.allow_root = opts.allow_root; - config.default_permissions = opts.default_permissions; - config.read_only = opts.read_only; - config.fsname = opts.fsname; - config.backend = opts.backend; - config.debug = args.debug; - run_foreground(config) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn options_accept_escaped_layer_separators() { - let options = - parse_options(r"lowerdir=/base:/path\:with\:colon,upperdir=/u,workdir=/w,allow_other") - .expect("options"); - assert_eq!( - options.lowerdir, - vec![PathBuf::from("/base"), PathBuf::from("/path:with:colon")] - ); - assert!(options.allow_other); - } - - #[test] - fn cli_accepts_repeated_option_arguments() { - let args = Args::try_parse_from([ - "persisting-overlayfs", - "-o", - "lowerdir=/lower,upperdir=/upper", - "-o", - "workdir=/work", - "/merged", - ]) - .expect("cli"); - let options = parse_options(&args.options.join(",")).expect("options"); - assert_eq!(options.workdir, Some(PathBuf::from("/work"))); - assert_eq!(args.mountpoint, PathBuf::from("/merged")); - } - - #[test] - fn jujutsu_upper_requires_store_and_workspace() { - let options = - parse_options("lowerdir=/lower,jjstore=/shared/overlay.jj,jjworkspace=attempt-1") - .expect("Jujutsu options"); - assert_eq!(options.jjstore, Some(PathBuf::from("/shared/overlay.jj"))); - assert_eq!(options.jjworkspace.as_deref(), Some("attempt-1")); - assert!(parse_options("lowerdir=/lower,jjstore=/shared/overlay.jj").is_err()); - assert!( - parse_options( - "lowerdir=/lower,jjstore=/shared/overlay.jj,jjworkspace=x,upperdir=/upper" - ) - .is_err() - ); - } -} diff --git a/crates/persisting-overlaynet/Cargo.toml b/crates/persisting-overlaynet/Cargo.toml index 8dddb957b..e72e2c357 100644 --- a/crates/persisting-overlaynet/Cargo.toml +++ b/crates/persisting-overlaynet/Cargo.toml @@ -4,7 +4,7 @@ version.workspace = true edition.workspace = true authors.workspace = true license.workspace = true -description = "Network interception and egress policy drivers for pVisor" +description = "Network interception and egress policy for trajectory capture" [dependencies] anyhow.workspace = true diff --git a/crates/persisting-overlaynet/README.md b/crates/persisting-overlaynet/README.md index 7a181940f..050f7c953 100644 --- a/crates/persisting-overlaynet/README.md +++ b/crates/persisting-overlaynet/README.md @@ -1,6 +1,9 @@ # persisting-overlaynet -**Network interception and egress-policy data planes for pVisor.** +pVisor and pPilot are maintained in external repositories. This repository +ships pChronicle and the internal libraries needed for capture and history. + +**Network interception and egress-policy data planes for trajectory capture.** Owns the proxy data plane: request classification, HTTP `CONNECT`, absolute-URI forwarding, access enforcement through @@ -36,7 +39,7 @@ just test persisting-overlaynet ## Links -- [OverlayNet architecture](../../docs/src/pvisor/design/overlaynet.md) -- [Network control](../../docs/src/pvisor/guides/network.md) +- OverlayNet architecture (external repository) +- Network control (external repository) - [`persisting-gateway`](../persisting-gateway/README.md) - [`persisting-agentctl`](../persisting-agentctl/README.md) diff --git a/crates/persisting-pchronicle-cli/README.md b/crates/persisting-pchronicle-cli/README.md index 911582152..bf3a2e5b8 100644 --- a/crates/persisting-pchronicle-cli/README.md +++ b/crates/persisting-pchronicle-cli/README.md @@ -1,5 +1,8 @@ # pChronicle CLI +pVisor and pPilot are maintained in external repositories. This repository +ships pChronicle and the internal libraries needed for capture and history. + **Standalone `pchronicle` CLI for onboarding, browsing, querying, importing, exporting, and serving trajectory Datasets.** @@ -57,9 +60,9 @@ just chronicle-binary ## Links -- [pChronicle get started](../../docs/src/pchronicle/get-started.md) -- [pChronicle CLI reference](../../docs/src/pchronicle/reference/cli.md) -- [Local read-only Dataset server](../../docs/src/pchronicle/guides/serve.md) -- [Gateway forwarding, rewriting, and capture](../../docs/src/pchronicle/guides/serve-gateway.md) +- [pChronicle get started](../../docs/src/en/pchronicle/get-started.md) +- [pChronicle CLI reference](../../docs/src/en/pchronicle/reference/cli.md) +- [Local read-only Dataset server](../../docs/src/en/pchronicle/guides/serve.md) +- [Gateway forwarding, rewriting, and capture](../../docs/src/en/pchronicle/guides/serve-gateway.md) - [`persisting-pchronicle`](../persisting-pchronicle/README.md) - [`pchronicle-web`](../../pchronicle-web/README.md) diff --git a/crates/persisting-pchronicle/Cargo.toml b/crates/persisting-pchronicle/Cargo.toml index ba813798d..146bc84d5 100644 --- a/crates/persisting-pchronicle/Cargo.toml +++ b/crates/persisting-pchronicle/Cargo.toml @@ -22,8 +22,8 @@ lance-store = [ "dep:opendal", "dep:object_store", ] -# Remote object stores are separated from the local Lance engine so pPilot and -# other local-only consumers do not compile cloud SDKs. `s3-store` remains a +# Remote object stores are separated from the local Lance engine so local-only +# consumers do not compile cloud SDKs. `s3-store` remains a # default feature for backwards compatibility; workspace dependencies disable # defaults and opt into only the backend they use. s3-store = ["lance-store", "lance/aws"] diff --git a/crates/persisting-pchronicle/README.md b/crates/persisting-pchronicle/README.md index 6ead9e617..a94eeb4cb 100644 --- a/crates/persisting-pchronicle/README.md +++ b/crates/persisting-pchronicle/README.md @@ -1,5 +1,8 @@ # pChronicle +pVisor and pPilot are maintained in external repositories. This repository +ships pChronicle and the internal libraries needed for capture and history. + **Persisting 的结构化轨迹与 Dataset 数据层。** 拥有轨迹领域模型、磁盘格式、Lance 持久化、数据源发现、DataFusion 查询、格式交换 @@ -53,9 +56,9 @@ just proptest pchronicle ## Links -- [pChronicle overview](../../docs/src/pchronicle/index.zh.md) -- [产品架构](../../docs/src/pchronicle/design/architecture.zh.md) -- [记录数据、视图与版本](../../docs/src/pchronicle/concepts/facts-and-projections.zh.md) -- [pChronicle CLI](../../docs/src/pchronicle/reference/cli.zh.md) +- [pChronicle overview](../../docs/src/en/pchronicle/index.zh.md) +- [产品架构](../../docs/src/en/pchronicle/design/architecture.zh.md) +- [记录数据、视图与版本](../../docs/src/en/pchronicle/concepts/facts-and-projections.zh.md) +- [pChronicle CLI](../../docs/src/en/pchronicle/reference/cli.zh.md) - [RFC-0003 ownership](../../docs/src/rfcs/0003-pchronicle-ownership.md) - [`persisting-pchronicle-cli`](../persisting-pchronicle-cli/README.md) diff --git a/crates/persisting-ppilot/Cargo.toml b/crates/persisting-ppilot/Cargo.toml deleted file mode 100644 index 49a532da4..000000000 --- a/crates/persisting-ppilot/Cargo.toml +++ /dev/null @@ -1,40 +0,0 @@ -[package] -name = "persisting-ppilot" -version.workspace = true -edition.workspace = true -authors.workspace = true -license.workspace = true -description = "pPilot durable Run orchestrator: plan, schedule, resume, reconcile, and collect independent Runs" -readme = "README.md" - -[[bin]] -name = "ppilot" -path = "src/bin/ppilot.rs" - -[dependencies] -anyhow.workspace = true -async-trait.workspace = true -chrono = { workspace = true, optional = true } -clap = { workspace = true, features = ["derive", "env"] } -futures.workspace = true -persisting-agentctl.workspace = true -persisting-events = { workspace = true, features = ["control"] } -pulsing-actor = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json.workspace = true -sha2.workspace = true -tempfile.workspace = true -tokio = { workspace = true, features = ["macros", "rt-multi-thread", "process", "io-util", "sync", "time", "signal", "fs", "net"] } -tokio-stream.workspace = true -tokio-util = { workspace = true, features = ["rt"] } -tracing.workspace = true -tracing-subscriber = { workspace = true, features = ["env-filter"] } -uuid = { workspace = true, features = ["v4"] } - -[features] -default = [] -# Append pPilot results through the pChronicle control process (Tee with JsonlFileSink). -traj-sink = ["dep:chrono"] - -[dev-dependencies] -proptest.workspace = true diff --git a/crates/persisting-ppilot/README.md b/crates/persisting-ppilot/README.md deleted file mode 100644 index 629facfe9..000000000 --- a/crates/persisting-ppilot/README.md +++ /dev/null @@ -1,71 +0,0 @@ -# pPilot - -**Durable Run production at scale.** - -pPilot owns planning, bounded execution, leases and fencing decisions, -infrastructure retry and recovery, reconciliation, result collection, and -task-to-Run mapping for many independent Runs. pVisor owns each Run and its -workspace. pChronicle owns durable canonical trajectory storage and Dataset -discovery, query, conversion, analysis, and exchange. - -## Use - -```bash -cargo build -p persisting-pvisor --bin pvisor -cargo build -p persisting-pchronicle-cli --bin pchronicle -cargo build -p persisting-ppilot --bin ppilot - -ppilot run plan.py --workers 8 --sink ./results -ppilot run plan.py --workers 8 --sink ./results \ - --control-uri s3://my-bucket/ppilot-control - -ppilot produce production.py --output ./runs --parallelism 8 \ - --cluster-network-limit 10mbps -``` - -`run` executes a `plan()` / `execute(item)` workload with bounded concurrency, -checkpoint/resume, infrastructure retry, and a durable result journal. - -`produce` consumes a Python planner (or compatibility JSON manifest), creates -one independent pVisor workspace per emitted Run, and writes a durable -production report. Both commands invoke the standalone `pvisor` binary for -each Run and embed a job-scoped Supervisor; there is no separate Supervisor -service to deploy. For `run --sink`, pPilot starts one authenticated, -loopback-only `pchronicle serve --control 127.0.0.1:0 DATASET` child and -uses its versioned client protocol -as a storage/control implementation dependency. The child persists the selected -coordination records. When pPilot is built with `traj-sink` and `--traj` is -enabled, the same child also appends terminal `ppilot.result` / `ppilot.failure` -trajectory events; it does not capture a general Run trajectory. pPilot retains -ownership of lease and fencing decisions, recovery, reconciliation, and -task-to-Run mapping. Delegated pVisor Runs receive no Chronicle overrides, so -`--sink` does not automatically enable their Gateway or lifecycle capture. Any -pVisor capture is a separate integration outside the current delegated -`--run-spec` path. pPilot does not link pChronicle, Lance, Arrow, or DataFusion. -Use -`--pchronicle-binary PATH` or `PERSISTING_PCHRONICLE_BIN` when `pchronicle` is -not installed beside `ppilot` or available on `PATH`. Similarly, use -`--pvisor-binary PATH` or `PERSISTING_PVISOR_BIN` for pVisor. -The default pVisor build does not link Lance/DataFusion. Durable Attempt and -trajectory writes use the same lightweight `pchronicle serve` Control service -protocol; pPilot itself still does not link pVisor. - -The CLI intentionally contains no Dataset catalog, query, conversion, or -analysis commands. Use `pchronicle` for those operations. - -## Develop - -```bash -just test persisting-ppilot -# or: just test-crate ppilot -just examples-ppilot -``` - -## Links - -- [pPilot overview](../../docs/src/ppilot/index.md) -- [Orchestration architecture](../../docs/src/ppilot/design/orchestration.md) -- [pPilot CLI](../../docs/src/ppilot/reference/cli.md) -- [System architecture](../../docs/src/system-design/architecture.md) -- [`persisting-pvisor`](../persisting-pvisor/README.md) -- [`persisting-pchronicle-cli`](../persisting-pchronicle-cli/README.md) diff --git a/crates/persisting-ppilot/src/agentctl.rs b/crates/persisting-ppilot/src/agentctl.rs deleted file mode 100644 index db37e7ffc..000000000 --- a/crates/persisting-ppilot/src/agentctl.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Re-export of the shared AgentCtl client SDK and protocol. - -pub use persisting_agentctl::*; diff --git a/crates/persisting-ppilot/src/batch.rs b/crates/persisting-ppilot/src/batch.rs deleted file mode 100644 index edd00aa9e..000000000 --- a/crates/persisting-ppilot/src/batch.rs +++ /dev/null @@ -1,480 +0,0 @@ -//! Product batch scenarios built from pPilot's pVisor and pChronicle seams. - -use anyhow::{Context, bail}; -use futures::{Stream, StreamExt, stream, stream::FuturesUnordered}; -use persisting_agentctl::{ - PVisorProcessClient, PVisorProcessOptions, RunId, RunInvocation, RunSpec, RunState, StdioMode, -}; -use serde::{Deserialize, Serialize}; -use std::collections::{BTreeMap, BTreeSet}; -use std::path::{Path, PathBuf}; -use std::pin::Pin; - -pub const BATCH_PRODUCTION_SCHEMA_VERSION: u32 = 1; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BatchProductionManifest { - #[serde(default = "production_schema_version")] - pub schema_version: u32, - pub batch_id: String, - pub runs: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TrajectoryProductionRun { - pub id: String, - #[serde(default = "default_agent")] - pub agent: String, - pub command: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub cwd: Option, - #[serde(default)] - pub env: BTreeMap, -} - -#[derive(Debug, Clone)] -pub struct BatchProductionOptions { - pub output_dir: PathBuf, - pub pvisor_binary: PathBuf, - pub parallelism: usize, - pub capture_gateway: bool, - pub supervisor_network_limit_bytes_per_second: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BatchProductionReport { - pub schema_version: u32, - pub batch_id: String, - pub requested_parallelism: usize, - pub total: usize, - pub completed: usize, - pub failed: usize, - pub runs: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ProductionRunOutcome { - pub run_id: String, - pub task_id: String, - pub workspace: PathBuf, - pub state: RunState, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub exit_code: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub failure: Option, -} - -impl BatchProductionManifest { - pub fn from_path(path: &Path) -> anyhow::Result { - let bytes = std::fs::read(path) - .with_context(|| format!("read production manifest {}", path.display()))?; - let manifest: Self = serde_json::from_slice(&bytes) - .with_context(|| format!("parse production manifest {}", path.display()))?; - manifest.validate()?; - Ok(manifest) - } - - pub fn validate(&self) -> anyhow::Result<()> { - if self.schema_version != BATCH_PRODUCTION_SCHEMA_VERSION { - bail!( - "unsupported production manifest schema {}; expected {}", - self.schema_version, - BATCH_PRODUCTION_SCHEMA_VERSION - ); - } - validate_id("batch_id", &self.batch_id)?; - if self.runs.is_empty() { - bail!("production manifest must contain at least one run"); - } - let mut ids = BTreeSet::new(); - for run in &self.runs { - run.validate()?; - if !ids.insert(&run.id) { - bail!("duplicate production run id {:?}", run.id); - } - } - Ok(()) - } -} - -impl TrajectoryProductionRun { - fn validate(&self) -> anyhow::Result<()> { - validate_id("run id", &self.id)?; - if self.agent.trim().is_empty() { - bail!("production run {} has an empty agent", self.id); - } - if self - .command - .first() - .is_none_or(|program| program.trim().is_empty()) - { - bail!("production run {} has an empty command", self.id); - } - Ok(()) - } - - fn from_plan_value(value: serde_json::Value) -> anyhow::Result { - let run: Self = serde_json::from_value(value) - .context("planner item is not a valid trajectory production Run")?; - run.validate()?; - Ok(run) - } -} - -/// Run each manifest entry in an independent pVisor workspace with bounded -/// concurrency. A durable report is written even when individual Runs fail. -pub async fn produce_trajectories( - manifest: BatchProductionManifest, - options: BatchProductionOptions, -) -> anyhow::Result { - manifest.validate()?; - let runs = stream::iter(manifest.runs.into_iter().map(Ok)); - produce_trajectory_stream(manifest.batch_id, Box::pin(runs), options).await -} - -/// Run trajectory descriptions emitted incrementally by a Python planner. -/// The planner is back-pressured by the bounded execution window, so large -/// batches do not need to be materialized in pPilot memory. -pub async fn produce_from_planner( - planner: PathBuf, - python: PathBuf, - planner_args: Vec, - batch_id: String, - options: BatchProductionOptions, -) -> anyhow::Result { - validate_id("batch_id", &batch_id)?; - let runs = crate::plan::stream_plan_values(planner, python, planner_args) - .map(|value| value.and_then(TrajectoryProductionRun::from_plan_value)); - produce_trajectory_stream(batch_id, Box::pin(runs), options).await -} - -async fn produce_trajectory_stream( - batch_id: String, - mut source: Pin> + Send>>, - options: BatchProductionOptions, -) -> anyhow::Result { - if options.supervisor_network_limit_bytes_per_second.is_some() && !options.capture_gateway { - bail!("Supervisor network limit requires the pVisor capture Gateway"); - } - tokio::fs::create_dir_all(&options.output_dir) - .await - .with_context(|| format!("create batch output {}", options.output_dir.display()))?; - let parallelism = options.parallelism.max(1); - let output_dir = options.output_dir.clone(); - let capture_gateway = options.capture_gateway; - let pvisor_binary = options.pvisor_binary.clone(); - let parent_run_id = format!("ppilot-batch-{batch_id}"); - let supervisor = - crate::supervisor::EmbeddedSupervisor::start(crate::supervisor::EmbeddedSupervisorConfig { - network_limit_bytes_per_second: options.supervisor_network_limit_bytes_per_second, - quota_slots: parallelism, - }) - .await - .context("start embedded pPilot Supervisor")?; - let supervisor_bootstrap = supervisor.bootstrap(); - - let execution = async { - let mut seen_ids = BTreeSet::new(); - let mut in_flight = FuturesUnordered::new(); - let mut runs = Vec::new(); - let mut source_finished = false; - - loop { - while !source_finished && in_flight.len() < parallelism { - match source.next().await { - Some(run) => { - let run = run?; - run.validate()?; - if !seen_ids.insert(run.id.clone()) { - bail!("duplicate production run id {:?}", run.id); - } - let output_dir = output_dir.clone(); - let parent_run_id = parent_run_id.clone(); - let batch_id = batch_id.clone(); - let supervisor_bootstrap = supervisor_bootstrap.clone(); - let pvisor_binary = pvisor_binary.clone(); - in_flight.push(async move { - run_production_entry( - run, - &batch_id, - &parent_run_id, - &output_dir, - capture_gateway, - supervisor_bootstrap, - pvisor_binary, - ) - .await - }); - } - None => source_finished = true, - } - } - - match in_flight.next().await { - Some(outcome) => runs.push(outcome?), - None if source_finished => break, - None => continue, - } - } - - if runs.is_empty() { - bail!("production planner must emit at least one Run"); - } - anyhow::Ok(runs) - } - .await; - let shutdown = supervisor.shutdown().await; - let mut runs = execution?; - shutdown.context("shut down embedded pPilot Supervisor")?; - runs.sort_by(|left, right| left.run_id.cmp(&right.run_id)); - let completed = runs - .iter() - .filter(|outcome| outcome.state == RunState::Completed) - .count(); - let report = BatchProductionReport { - schema_version: BATCH_PRODUCTION_SCHEMA_VERSION, - batch_id, - requested_parallelism: parallelism, - total: runs.len(), - completed, - failed: runs.len().saturating_sub(completed), - runs, - }; - write_json_atomic(&output_dir.join("production-report.json"), &report).await?; - Ok(report) -} - -async fn run_production_entry( - run: TrajectoryProductionRun, - batch_id: &str, - parent_run_id: &str, - output_dir: &Path, - capture_gateway: bool, - supervisor: persisting_agentctl::SupervisorBootstrap, - pvisor_binary: PathBuf, -) -> anyhow::Result { - let workspace = output_dir.join(&run.id); - if workspace.exists() { - bail!( - "production workspace already exists for {}: {}", - run.id, - workspace.display() - ); - } - let mut run_args = Vec::new(); - if capture_gateway { - run_args.extend([ - "--gateway-mode".into(), - "capture".into(), - "--gateway-level".into(), - "dialogue".into(), - "--gateway-admin-listen".into(), - free_loopback_address()?.into(), - "--overlaynet-listen".into(), - free_loopback_address()?.into(), - ]); - } - let pvisor = PVisorProcessClient::new(pvisor_binary); - let (program, args) = run.command.split_first().expect("manifest was validated"); - let mut spec = RunSpec::process(run.id.as_str(), run.agent.as_str(), program); - spec.supervisor = Some(supervisor); - spec.parent_run_id = Some(RunId::new(parent_run_id)); - spec.task_id = Some(run.id.clone()); - spec.metadata - .insert("ppilot.batch_id".into(), serde_json::json!(batch_id)); - spec.metadata.insert( - "ppilot.scope".into(), - serde_json::json!("trajectory-production"), - ); - let RunInvocation::Process(process) = &mut spec.invocation; - process.args = args.to_vec(); - process.cwd = run.cwd.map(|path| path.display().to_string()); - process.env = run.env; - process.stdout = StdioMode::Capture; - process.stderr = StdioMode::Capture; - - let result = pvisor - .run( - &spec, - &PVisorProcessOptions { - run_home: Some(output_dir.to_path_buf()), - run_args, - }, - tokio_util::sync::CancellationToken::new(), - ) - .await - .with_context(|| format!("execute production Run {} through pVisor", run.id))?; - Ok(ProductionRunOutcome { - run_id: result.run_id.to_string(), - task_id: run.id, - workspace, - state: result.state, - exit_code: result.exit_code, - failure: result.failure.map(|failure| failure.message), - }) -} - -pub(crate) async fn write_json_atomic(path: &Path, value: &impl Serialize) -> anyhow::Result<()> { - write_bytes_atomic(path, &serde_json::to_vec_pretty(value)?).await -} - -pub(crate) async fn write_bytes_atomic(path: &Path, bytes: &[u8]) -> anyhow::Result<()> { - let name = path - .file_name() - .and_then(|name| name.to_str()) - .context("batch output path has no UTF-8 filename")?; - let temporary = path.with_file_name(format!(".{name}.tmp")); - tokio::fs::write(&temporary, bytes) - .await - .with_context(|| format!("write {}", temporary.display()))?; - tokio::fs::rename(&temporary, path) - .await - .with_context(|| format!("rename {} to {}", temporary.display(), path.display()))?; - Ok(()) -} - -fn validate_id(label: &str, id: &str) -> anyhow::Result<()> { - let id = id.trim(); - if id.is_empty() || id == "." || id == ".." || id.contains('/') || id.contains('\\') { - bail!("{label} must be one non-empty path-safe segment"); - } - Ok(()) -} - -fn free_loopback_address() -> anyhow::Result { - let listener = std::net::TcpListener::bind("127.0.0.1:0")?; - Ok(listener.local_addr()?.to_string()) -} - -const fn production_schema_version() -> u32 { - BATCH_PRODUCTION_SCHEMA_VERSION -} - -fn default_agent() -> String { - "agent".into() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn production_manifest_rejects_duplicate_and_unsafe_ids() { - let mut manifest = BatchProductionManifest { - schema_version: 1, - batch_id: "batch-1".into(), - runs: vec![ - TrajectoryProductionRun { - id: "run-1".into(), - agent: "test".into(), - command: vec!["/bin/true".into()], - cwd: None, - env: Default::default(), - }, - TrajectoryProductionRun { - id: "run-1".into(), - agent: "test".into(), - command: vec!["/bin/true".into()], - cwd: None, - env: Default::default(), - }, - ], - }; - assert!( - manifest - .validate() - .unwrap_err() - .to_string() - .contains("duplicate") - ); - manifest.runs.pop(); - manifest.runs[0].id = "../escape".into(); - assert!(manifest.validate().is_err()); - } - - #[test] - fn production_plan_items_are_typed_and_validated() { - let run = TrajectoryProductionRun::from_plan_value(serde_json::json!({ - "id": "run-1", - "command": ["/bin/true"], - "env": {"MODE": "test"} - })) - .unwrap(); - assert_eq!(run.agent, "agent"); - assert_eq!(run.env["MODE"], "test"); - - let error = TrajectoryProductionRun::from_plan_value(serde_json::json!({ - "id": "../escape", - "command": ["/bin/true"] - })) - .unwrap_err(); - assert!(error.to_string().contains("path-safe")); - } - - #[tokio::test] - async fn production_planner_rejects_duplicate_ids_incrementally() { - let dir = tempfile::tempdir().unwrap(); - let planner = dir.path().join("duplicate.py"); - std::fs::write( - &planner, - r#" -def plan(): - yield {"id": "same", "command": ["/bin/true"]} - yield {"id": "same", "command": ["/bin/true"]} -"#, - ) - .unwrap(); - let error = produce_from_planner( - planner, - PathBuf::from("python3"), - vec![], - "duplicates".into(), - BatchProductionOptions { - output_dir: dir.path().join("runs"), - pvisor_binary: PathBuf::from("pvisor"), - parallelism: 2, - capture_gateway: false, - supervisor_network_limit_bytes_per_second: None, - }, - ) - .await - .unwrap_err(); - assert!( - error.to_string().contains("duplicate production run id"), - "unexpected planner error: {error:#}" - ); - } - - #[tokio::test] - async fn production_limit_requires_the_intercepting_gateway() { - let manifest = BatchProductionManifest { - schema_version: 1, - batch_id: "batch-limit".into(), - runs: vec![TrajectoryProductionRun { - id: "run-1".into(), - agent: "test".into(), - command: vec!["/bin/true".into()], - cwd: None, - env: Default::default(), - }], - }; - let output = tempfile::tempdir().unwrap(); - let error = produce_trajectories( - manifest, - BatchProductionOptions { - output_dir: output.path().join("runs"), - pvisor_binary: PathBuf::from("pvisor"), - parallelism: 1, - capture_gateway: false, - supervisor_network_limit_bytes_per_second: Some(1024), - }, - ) - .await - .unwrap_err(); - assert!( - error - .to_string() - .contains("requires the pVisor capture Gateway") - ); - } -} diff --git a/crates/persisting-ppilot/src/bin/ppilot.rs b/crates/persisting-ppilot/src/bin/ppilot.rs deleted file mode 100644 index d540a7eed..000000000 --- a/crates/persisting-ppilot/src/bin/ppilot.rs +++ /dev/null @@ -1,176 +0,0 @@ -use std::path::PathBuf; -use std::process::ExitCode; - -use anyhow::Result; -use clap::{Args, Parser, Subcommand}; -use persisting_ppilot::{ - BatchProductionManifest, BatchProductionOptions, PPilotArgs, init_tracing_with_verbose, - produce_from_planner, produce_trajectories, run_ppilot, -}; - -#[derive(Debug, Parser)] -#[command( - name = "ppilot", - version, - about = "Produce durable Agent Runs at scale" -)] -struct Cli { - #[command(subcommand)] - command: Command, -} - -#[derive(Debug, Subcommand)] -enum Command { - /// Run a pPilot plan with bounded concurrency and durable resume. - Run(Box), - /// Stream Runs from a Python planner into independent pVisor workspaces. - Produce(ProduceArgs), -} - -#[derive(Debug, Args)] -struct ProduceArgs { - /// Python planner defining plan(); JSON manifests remain accepted for compatibility. - #[arg(value_name = "PLANNER")] - planner: PathBuf, - /// Root containing one durable pVisor workspace per Run. - #[arg(short, long, value_name = "DIR")] - output: PathBuf, - /// Maximum concurrent pVisor Runs. - #[arg(short = 'j', long, default_value_t = 4)] - parallelism: usize, - /// Disable the capture Gateway (mainly for local diagnostics). - #[arg(long)] - no_capture: bool, - /// Job-scoped aggregate rate delivered through the embedded Supervisor. - #[arg(long, value_name = "RATE", value_parser = persisting_ppilot::parse_bandwidth)] - cluster_network_limit: Option, - /// Python interpreter used to evaluate the planner. - #[arg(long, env = "PERSISTING_PYTHON", default_value = "python3")] - python: PathBuf, - /// Standalone pVisor executable used for every produced Run. - #[arg(long, env = "PERSISTING_PVISOR_BIN", default_value = "pvisor")] - pvisor_binary: PathBuf, - /// Stable batch identifier; defaults to the planner filename stem. - #[arg(long, value_name = "ID")] - batch_id: Option, - /// Arguments forwarded to the planner after `--`. - #[arg(last = true, value_name = "ARG")] - planner_args: Vec, -} - -#[tokio::main] -async fn main() -> ExitCode { - let cli = Cli::parse(); - let verbose = matches!(&cli.command, Command::Run(args) if args.verbose); - init_tracing_with_verbose(verbose); - - match dispatch(cli.command).await { - Ok(code) => code, - Err(error) => { - eprintln!("error: {error:#}"); - ExitCode::FAILURE - } - } -} - -async fn dispatch(command: Command) -> Result { - match command { - Command::Run(args) => run_ppilot(*args).await, - Command::Produce(args) => { - let options = BatchProductionOptions { - output_dir: args.output, - pvisor_binary: args.pvisor_binary, - parallelism: args.parallelism, - capture_gateway: !args.no_capture, - supervisor_network_limit_bytes_per_second: args.cluster_network_limit, - }; - let is_legacy_json = args - .planner - .extension() - .and_then(|extension| extension.to_str()) - .is_some_and(|extension| extension.eq_ignore_ascii_case("json")); - let report = if is_legacy_json { - if !args.planner_args.is_empty() { - anyhow::bail!("JSON manifests do not accept planner arguments"); - } - let mut manifest = BatchProductionManifest::from_path(&args.planner)?; - if let Some(batch_id) = args.batch_id { - manifest.batch_id = batch_id; - } - produce_trajectories(manifest, options).await? - } else { - let batch_id = args.batch_id.unwrap_or_else(|| { - args.planner - .file_stem() - .and_then(|stem| stem.to_str()) - .unwrap_or("production") - .to_owned() - }); - produce_from_planner( - args.planner, - args.python, - args.planner_args, - batch_id, - options, - ) - .await? - }; - println!("{}", serde_json::to_string_pretty(&report)?); - Ok(if report.failed == 0 { - ExitCode::SUCCESS - } else { - ExitCode::FAILURE - }) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn exposes_only_production_subcommands() { - assert!(matches!( - Cli::try_parse_from(["ppilot", "run", "plan.py", "--workers", "2"]) - .unwrap() - .command, - Command::Run(_) - )); - - let produce = Cli::try_parse_from([ - "ppilot", - "produce", - "production.py", - "--output", - "runs", - "-j", - "8", - "--cluster-network-limit", - "10mbps", - "--", - "--dataset", - "train", - ]) - .unwrap(); - let Command::Produce(produce) = produce.command else { - panic!("expected produce command") - }; - assert_eq!(produce.cluster_network_limit, Some(1_250_000)); - assert_eq!(produce.planner_args, ["--dataset", "train"]); - - for removed in [ - "query", - "chronicle", - "convert", - "analysis", - "process", - "self-test", - ] { - assert!( - Cli::try_parse_from(["ppilot", removed]).is_err(), - "removed subcommand {removed} must not parse" - ); - } - } -} diff --git a/crates/persisting-ppilot/src/blocks.rs b/crates/persisting-ppilot/src/blocks.rs deleted file mode 100644 index 1ef4d849d..000000000 --- a/crates/persisting-ppilot/src/blocks.rs +++ /dev/null @@ -1,123 +0,0 @@ -//! Index of **semantic primitives** — one contract per owning module. -//! -//! # Testing policy -//! -//! - **Unit / contract tests** live in the **same file** as the primitive -//! (`#[cfg(test)]` at the bottom of `task.rs`, `scheduler.rs`, …). -//! - **`tests/`** holds **integration** only (multi-module paths: fleet, resume -//! through Driver, argv end-to-end, …). -//! -//! | Primitive | Interface (types / traits / fns) | Module | -//! |-----------|----------------------------------|--------| -//! | Task wire | [`TaskExpr`], [`TaskResult`] | [`crate::task`] | -//! | Placement | [`Scheduler`], sticky-only / quarantine | [`crate::scheduler`] | -//! | Slot naming / dist | [`DistEnv`] | [`crate::dist`] | -//! | Run future | [`RunFuture`], [`wait_all`] | [`crate::future`] | -//! | Idempotency cache | [`ResultCache`] | [`crate::result_cache`] | -//! | Live skip / claim | [`SkipSet`] | [`crate::skip`] | -//! | Result sink | [`ResultSink`], [`persist_terminal`] | [`crate::sink`] | -//! | Async sink writer | [`SinkSubmitter`], [`spawn_sink_writer`] | [`crate::sink_writer`] | -//! | Checkpoint | [`CheckpointLedger`], [`CheckpointTracker`] | [`crate::checkpoint`] | -//! | Plan emit | [`stream_plan_tasks`] | [`crate::plan`] | -//! | Execute host | Executor router | `executor` | -//! | Worker seam | [`WorkerActor`], supervised spawn | [`crate::worker`] | -//! | Job cancel | [`JobControlActor`], DeathWatch | [`crate::job_control`] | -//! | Pulsing helpers | resolve / ask_timeout / spawn_supervised | [`crate::pulsing_ext`] | -//! | Driver | [`Driver`], [`RunOptions`] | [`crate::driver`] | -//! | Fleet boot | [`run_local_fleet`], [`run_fleet`] | [`crate::runtime`] | -//! | Observe | [`Observer`] | [`crate::observe`] | -//! | Python env | [`merge_pythonpath_parts`] | [`crate::python_env`] | -//! | AgentCtl | [`AgentCtlClient`], [`AgentCtlClientConfig`] | [`crate::agentctl`] | -//! | Runtime bridge | [`PilotRuntimeBridge`] | [`crate::runtime_bridge`] | -//! | Batch trajectories | production + sharded analysis | [`crate::batch`] | -//! | Federated analysis | Pulsing partial aggregation + coordinator merge | [`crate::federated`] | -//! -//! [`TaskExpr`]: crate::task::TaskExpr -//! [`TaskResult`]: crate::task::TaskResult -//! [`Scheduler`]: crate::scheduler::Scheduler -//! [`DistEnv`]: crate::dist::DistEnv -//! [`RunFuture`]: crate::future::RunFuture -//! [`wait_all`]: crate::future::wait_all -//! [`ResultCache`]: crate::result_cache::ResultCache -//! [`SkipSet`]: crate::skip::SkipSet -//! [`ResultSink`]: crate::sink::ResultSink -//! [`persist_terminal`]: crate::sink::persist_terminal -//! [`SinkSubmitter`]: crate::sink_writer::SinkSubmitter -//! [`spawn_sink_writer`]: crate::sink_writer::spawn_sink_writer -//! [`CheckpointLedger`]: crate::checkpoint::CheckpointLedger -//! [`CheckpointTracker`]: crate::checkpoint::CheckpointTracker -//! [`stream_plan_tasks`]: crate::plan::stream_plan_tasks -//! [`WorkerActor`]: crate::worker::WorkerActor -//! [`WorkerCommand`]: crate::worker::WorkerCommand -//! [`JobControlActor`]: crate::job_control::JobControlActor -//! [`Driver`]: crate::driver::Driver -//! [`RunOptions`]: crate::driver::RunOptions -//! [`run_local_fleet`]: crate::runtime::run_local_fleet -//! [`run_fleet`]: crate::runtime::run_fleet -//! [`Observer`]: crate::observe::Observer -//! [`merge_pythonpath_parts`]: crate::python_env::merge_pythonpath_parts -//! [`AgentCtlClient`]: crate::agentctl::AgentCtlClient -//! [`AgentCtlClientConfig`]: crate::agentctl::AgentCtlClientConfig -//! [`PilotRuntimeBridge`]: crate::runtime_bridge::PilotRuntimeBridge - -/// Stable ids for docs / observability (not a separate test suite). -pub mod ids { - pub const TASK_WIRE: &str = "task_wire"; - pub const PLACEMENT: &str = "placement"; - pub const RUN_FUTURE: &str = "run_future"; - pub const IDEMPOTENCY: &str = "idempotency"; - pub const SKIP: &str = "skip"; - pub const SINK: &str = "sink"; - pub const SINK_WRITER: &str = "sink_writer"; - pub const CHECKPOINT: &str = "checkpoint"; - pub const PLAN: &str = "plan"; - pub const EXECUTE: &str = "execute"; - pub const WORKER: &str = "worker"; - pub const JOB_CONTROL: &str = "job_control"; - pub const PULSING_EXT: &str = "pulsing_ext"; - pub const DRIVER: &str = "driver"; - pub const FLEET: &str = "fleet"; - pub const OBSERVE: &str = "observe"; - pub const PYTHON_ENV: &str = "python_env"; - pub const AGENTCTL: &str = "agentctl"; - pub const RUNTIME_BRIDGE: &str = "runtime_bridge"; - pub const BATCH_TRAJECTORY: &str = "batch_trajectory"; - pub const FEDERATED_ANALYSIS: &str = "federated_analysis"; - - pub const ALL: &[&str] = &[ - TASK_WIRE, - PYTHON_ENV, - AGENTCTL, - RUNTIME_BRIDGE, - BATCH_TRAJECTORY, - FEDERATED_ANALYSIS, - PLACEMENT, - RUN_FUTURE, - IDEMPOTENCY, - SKIP, - SINK, - SINK_WRITER, - CHECKPOINT, - OBSERVE, - PLAN, - EXECUTE, - WORKER, - JOB_CONTROL, - PULSING_EXT, - DRIVER, - FLEET, - ]; -} - -#[cfg(test)] -mod tests { - use super::ids; - - #[test] - fn primitive_ids_unique() { - let mut seen = std::collections::HashSet::new(); - for id in ids::ALL { - assert!(seen.insert(*id), "duplicate id {id}"); - } - } -} diff --git a/crates/persisting-ppilot/src/check.rs b/crates/persisting-ppilot/src/check.rs deleted file mode 100644 index 9864c9da5..000000000 --- a/crates/persisting-ppilot/src/check.rs +++ /dev/null @@ -1,308 +0,0 @@ -//! Local validation: prove env + plan + execute before scale-out. -//! -//! The embedding host exposes this through [`crate::cli::PPilotArgs::check`]. - -use crate::plan::stream_plan_tasks; -use crate::python_env::{self, pythonpath_for_script}; -use crate::runtime::{RunOptions, run_local_fleet}; -use crate::task::TaskExpr; -use anyhow::{Context, Result, bail}; -use futures::StreamExt; -use serde_json::{Value, json}; -use std::collections::BTreeMap; -use std::path::{Path, PathBuf}; -use std::process::Stdio; -use tokio::process::Command; - -#[derive(Debug, Clone)] -pub struct CheckOptions { - pub script: PathBuf, - pub python: PathBuf, - pub pvisor_binary: PathBuf, - /// Max tasks to actually execute (0 = all). - pub limit: usize, - pub workers: usize, - pub verbose: bool, - pub pythonpath_extra: Vec, - /// Forwarded to `task.py` as `sys.argv[1:]` (after `--`). - pub script_args: Vec, -} - -#[derive(Debug, Default)] -pub struct CheckReport { - pub python_ok: bool, - pub python_version: Option, - pub plan_tasks: usize, - pub plan_ops: BTreeMap, - pub execute_ok: bool, - pub run_ok: usize, - pub run_fail: usize, - pub errors: Vec, -} - -impl CheckReport { - pub fn passed(&self) -> bool { - self.python_ok - && self.errors.is_empty() - && self.plan_tasks > 0 - && self.execute_ok - && self.run_fail == 0 - } - - pub fn to_json(&self) -> Value { - json!({ - "ok": self.passed(), - "python": { "ok": self.python_ok, "version": self.python_version }, - "plan": { "tasks": self.plan_tasks, "ops": self.plan_ops }, - "execute": { "ok": self.execute_ok }, - "run": { "ok": self.run_ok, "failed": self.run_fail }, - "errors": self.errors, - }) - } -} - -/// Full local check pipeline. Progress → stderr; JSON summary → stdout. -pub async fn run_check(opts: CheckOptions) -> Result { - let mut report = CheckReport::default(); - let mut extras = pythonpath_for_script(&opts.script); - extras.extend(opts.pythonpath_extra.iter().cloned()); - - eprint_stage(1, 4, "python env"); - match probe_python(&opts.python).await { - Ok(ver) => { - report.python_ok = true; - report.python_version = Some(ver.clone()); - eprintln!(" OK {} ({})", opts.python.display(), ver.trim()); - } - Err(e) => { - report.python_ok = false; - report.errors.push(format!("python: {e:#}")); - eprintln!(" FAIL {e:#}"); - print_summary(&report); - return Ok(report); - } - } - if let Some(pp) = python_env::merge_pythonpath(&extras) { - eprintln!(" PYTHONPATH+= {}", shorten_pp(&pp)); - } - - eprint_stage(2, 4, "plan emit + schema"); - let tasks = - match collect_plan_tasks(&opts.script, &opts.python, &extras, &opts.script_args).await { - Ok(t) => t, - Err(e) => { - report.errors.push(format!("plan: {e:#}")); - eprintln!(" FAIL {e:#}"); - print_summary(&report); - return Ok(report); - } - }; - if tasks.is_empty() { - report.errors.push("plan emitted zero tasks".into()); - eprintln!(" FAIL no tasks"); - print_summary(&report); - return Ok(report); - } - report.plan_tasks = tasks.len(); - for t in &tasks { - *report.plan_ops.entry(t.op.clone()).or_default() += 1; - } - eprintln!( - " OK {} task(s) ops={}", - tasks.len(), - format_ops(&report.plan_ops) - ); - if opts.verbose { - for t in tasks.iter().take(5) { - eprintln!(" {}", t.to_ndjson().unwrap_or_default()); - } - if tasks.len() > 5 { - eprintln!(" … {} more", tasks.len() - 5); - } - } - - eprint_stage(3, 4, "resolve execute"); - match probe_plan_execute(&opts.python, &opts.script, &extras).await { - Ok(()) => { - report.execute_ok = true; - eprintln!(" OK {}::execute(item)", opts.script.display()); - } - Err(e) => { - report.execute_ok = false; - report.errors.push(format!("execute: {e:#}")); - eprintln!(" FAIL execute: {e:#}"); - print_summary(&report); - return Ok(report); - } - } - - eprint_stage(4, 4, "local run"); - let to_run = if opts.limit == 0 { - tasks.len() - } else { - opts.limit.min(tasks.len()) - }; - // Skip the remainder so --limit actually bounds execute without changing plan(). - let skip = if to_run < tasks.len() { - eprintln!( - " note: --limit {to_run} (skipping {} of {} plan tasks)", - tasks.len() - to_run, - tasks.len() - ); - tasks[to_run..].iter().map(|t| t.id.clone()).collect() - } else { - crate::skip::SkipSet::new() - }; - - let run_opts = RunOptions { - script: opts.script.clone(), - python: opts.python.clone(), - pvisor_binary: opts.pvisor_binary.clone(), - workers: opts.workers.max(1), - max_inflight: opts.workers.max(1), - per_worker_inflight: 1, - pythonpath_extra: extras.clone(), - script_args: opts.script_args.clone(), - infra_retries: 2, - job_cancel: tokio_util::sync::CancellationToken::new(), - observer: crate::observe::Observer::disabled(), - skip_task_ids: skip, - checkpoint: None, - sink_submitter: None, - coordinator: None, - }; - - match run_local_fleet(run_opts, |_| {}).await { - Ok(results) => { - for r in &results { - if r.ok { - report.run_ok += 1; - } else { - report.run_fail += 1; - let msg = format!( - "task {}: {}", - r.task_id, - r.error.clone().unwrap_or_else(|| "failed".into()) - ); - report.errors.push(msg.clone()); - if opts.verbose { - if let Some(tb) = &r.traceback { - eprintln!(" FAIL {msg}\n{tb}"); - } else { - eprintln!(" FAIL {msg}"); - } - } - } - } - eprintln!( - " {} {}/{} ok", - if report.run_fail == 0 { "OK" } else { "FAIL" }, - report.run_ok, - results.len() - ); - } - Err(e) => { - report.errors.push(format!("run: {e:#}")); - eprintln!(" FAIL {e:#}"); - } - } - - print_summary(&report); - Ok(report) -} - -fn eprint_stage(n: u32, total: u32, title: &str) { - eprintln!("[{n}/{total}] {title}"); -} - -fn print_summary(report: &CheckReport) { - println!("{}", report.to_json()); -} - -fn format_ops(ops: &BTreeMap) -> String { - ops.iter() - .map(|(k, v)| format!("{k}×{v}")) - .collect::>() - .join(", ") -} - -fn shorten_pp(pp: &str) -> String { - let parts: Vec = std::env::split_paths(pp) - .map(|p| p.to_string_lossy().into_owned()) - .collect(); - if parts.len() <= 3 { - parts.join(":") - } else { - format!("{} … (+{} paths)", parts[..2].join(":"), parts.len() - 2) - } -} - -async fn probe_python(python: &Path) -> Result { - let out = Command::new(python) - .args(["-c", "import sys; print(sys.version.split()[0])"]) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .await - .with_context(|| format!("exec {}", python.display()))?; - if !out.status.success() { - bail!( - "exit {:?}: {}", - out.status.code(), - String::from_utf8_lossy(&out.stderr).trim() - ); - } - Ok(String::from_utf8_lossy(&out.stdout).trim().to_string()) -} - -async fn collect_plan_tasks( - script: &Path, - python: &Path, - extras: &[PathBuf], - script_args: &[String], -) -> Result> { - if let Some(pp) = python_env::merge_pythonpath(extras) { - // This process configures its environment before spawning the plan - // worker; no concurrent environment mutation occurs in this scope. - unsafe { std::env::set_var("PYTHONPATH", pp) }; - } - let mut stream = stream_plan_tasks( - script.to_path_buf(), - python.to_path_buf(), - script_args.to_vec(), - ); - let mut tasks = Vec::new(); - while let Some(item) = stream.next().await { - tasks.push(item?); - } - Ok(tasks) -} - -async fn probe_plan_execute(python: &Path, script: &Path, extras: &[PathBuf]) -> Result<()> { - let script = script - .canonicalize() - .with_context(|| format!("plan script {}", script.display()))?; - let code = r#" -import importlib.util, sys -from pathlib import Path -path = Path(sys.argv[1]) -spec = importlib.util.spec_from_file_location("user_plan_probe", path) -mod = importlib.util.module_from_spec(spec) -spec.loader.exec_module(mod) -if not hasattr(mod, "execute") or not callable(mod.execute): - raise SystemExit("plan must define execute(item)") -print("ok", flush=True) -"#; - let mut cmd = Command::new(python); - cmd.arg("-c") - .arg(code) - .arg(&script) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()); - python_env::apply_pythonpath(&mut cmd, extras); - let out = cmd.output().await.context("probe execute")?; - if !out.status.success() { - bail!("{}", String::from_utf8_lossy(&out.stderr).trim()); - } - Ok(()) -} diff --git a/crates/persisting-ppilot/src/checkpoint.rs b/crates/persisting-ppilot/src/checkpoint.rs deleted file mode 100644 index 7c625757d..000000000 --- a/crates/persisting-ppilot/src/checkpoint.rs +++ /dev/null @@ -1,350 +0,0 @@ -//! Checkpoint ledger: resume unfinished work from `--sink` + progress file. -//! -//! Done set = `task_id`s already in `ready.ndjson` / `failures.ndjson`. -//! Progress snapshot = `checkpoint.json` (throttled writes). - -use crate::task::unix_now; -use anyhow::{Context, Result}; -use serde::{Deserialize, Serialize}; -use std::collections::HashSet; -use std::path::{Path, PathBuf}; -use std::sync::Mutex; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::time::Instant; -use tokio::fs; -use tokio::io::{AsyncBufReadExt, BufReader}; - -/// Terminal task ids already recorded under a sink root. -#[derive(Debug, Clone, Default)] -pub struct CheckpointLedger { - pub ready: HashSet, - pub failed: HashSet, -} - -impl CheckpointLedger { - pub fn skip_ids(&self) -> HashSet { - let mut s = self.ready.clone(); - s.extend(self.failed.iter().cloned()); - s - } - - pub async fn load(root: &Path) -> Result { - let ready = load_task_ids(&root.join("ready.ndjson")).await?; - let failed = load_task_ids(&root.join("failures.ndjson")).await?; - Ok(Self { ready, failed }) - } - - /// Task ids in `failures.ndjson` whose stable `error_kind` matches one of - /// `kinds`. Legacy failure rows without the field are treated as `execute`. - pub async fn failed_ids_matching( - &self, - root: &Path, - kinds: &[String], - ) -> Result> { - let wanted: HashSet<&str> = kinds.iter().map(String::as_str).collect(); - if wanted.is_empty() { - return Ok(HashSet::new()); - } - let path = root.join("failures.ndjson"); - let mut matched = HashSet::new(); - if !path.exists() { - return Ok(matched); - } - let f = fs::File::open(&path) - .await - .with_context(|| format!("open {}", path.display()))?; - let mut lines = BufReader::new(f).lines(); - while let Some(line) = lines.next_line().await? { - let Ok(value) = serde_json::from_str::(line.trim()) else { - continue; - }; - let kind = value - .get("error_kind") - .and_then(|kind| kind.as_str()) - .unwrap_or("execute"); - if wanted.contains(kind) - && let Some(id) = value.get("task_id").and_then(|id| id.as_str()) - { - matched.insert(id.to_string()); - } - } - Ok(matched) - } -} - -async fn load_task_ids(path: &Path) -> Result> { - let mut out = HashSet::new(); - if !path.exists() { - return Ok(out); - } - let f = fs::File::open(path) - .await - .with_context(|| format!("open {}", path.display()))?; - let mut lines = BufReader::new(f).lines(); - let mut line_no = 0u64; - while let Some(line) = lines.next_line().await? { - line_no += 1; - let line = line.trim(); - if line.is_empty() { - continue; - } - let v: serde_json::Value = match serde_json::from_str(line) { - Ok(v) => v, - Err(e) => { - tracing::warn!( - path = %path.display(), - line = line_no, - error = %e, - "skipping corrupt checkpoint JSONL line" - ); - continue; - } - }; - if let Some(id) = v - .get("task_id") - .and_then(|x| x.as_str()) - .map(|s| s.to_string()) - { - out.insert(id); - } else { - tracing::warn!( - path = %path.display(), - line = line_no, - "skipping checkpoint line without task_id" - ); - } - } - Ok(out) -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct CheckpointProgress { - pub ok: u64, - pub fail: u64, - pub cancelled: u64, - pub skipped: u64, - pub dispatched: u64, - pub updated_at: f64, -} - -/// Live progress tracker with throttled `checkpoint.json` writes. -pub struct CheckpointTracker { - root: PathBuf, - state: Mutex, - last_flush: Mutex, - flush_every_ms: u64, - dirty: AtomicU64, -} - -impl CheckpointTracker { - pub fn new(root: impl Into) -> Self { - Self { - root: root.into(), - state: Mutex::new(CheckpointProgress::default()), - last_flush: Mutex::new(Instant::now() - std::time::Duration::from_secs(2)), - flush_every_ms: 1000, - dirty: AtomicU64::new(0), - } - } - - pub fn seed_from_ledger(&self, ledger: &CheckpointLedger) { - let mut g = self.state.lock().unwrap_or_else(|e| e.into_inner()); - g.ok = ledger.ready.len() as u64; - g.fail = ledger.failed.len() as u64; - g.updated_at = unix_now(); - } - - pub fn note_skipped(&self, n: u64) { - let mut g = self.state.lock().unwrap_or_else(|e| e.into_inner()); - g.skipped = g.skipped.saturating_add(n); - g.updated_at = unix_now(); - self.dirty.fetch_add(1, Ordering::Relaxed); - } - - pub fn note_dispatched(&self) { - let mut g = self.state.lock().unwrap_or_else(|e| e.into_inner()); - g.dispatched = g.dispatched.saturating_add(1); - g.updated_at = unix_now(); - self.dirty.fetch_add(1, Ordering::Relaxed); - } - - pub fn note_terminal(&self, ok: bool, cancelled: bool) { - let mut g = self.state.lock().unwrap_or_else(|e| e.into_inner()); - if cancelled { - g.cancelled = g.cancelled.saturating_add(1); - } else if ok { - g.ok = g.ok.saturating_add(1); - } else { - g.fail = g.fail.saturating_add(1); - } - g.updated_at = unix_now(); - self.dirty.fetch_add(1, Ordering::Relaxed); - } - - pub fn snapshot(&self) -> CheckpointProgress { - self.state.lock().unwrap_or_else(|e| e.into_inner()).clone() - } - - pub async fn maybe_flush(&self) -> Result<()> { - if self.dirty.load(Ordering::Relaxed) == 0 { - return Ok(()); - } - let should = { - let last = self.last_flush.lock().unwrap_or_else(|e| e.into_inner()); - last.elapsed().as_millis() as u64 >= self.flush_every_ms - }; - if should { - self.flush().await?; - } - Ok(()) - } - - pub async fn flush(&self) -> Result<()> { - let snap = self.snapshot(); - let path = self.root.join("checkpoint.json"); - let tmp = self.root.join("checkpoint.json.tmp"); - let body = serde_json::to_vec_pretty(&snap)?; - fs::write(&tmp, &body) - .await - .with_context(|| format!("write {}", tmp.display()))?; - fs::rename(&tmp, &path) - .await - .with_context(|| format!("rename {}", path.display()))?; - if let Ok(mut last) = self.last_flush.lock() { - *last = Instant::now(); - } - self.dirty.store(0, Ordering::Relaxed); - Ok(()) - } - - pub fn summary_line(&self) -> String { - let s = self.snapshot(); - format!( - "[ckpt] ok={} fail={} cancelled={} skipped={} dispatched={}", - s.ok, s.fail, s.cancelled, s.skipped, s.dispatched - ) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use tokio::io::AsyncWriteExt; - - #[tokio::test] - async fn load_ready_and_failures() { - let dir = tempfile::tempdir().unwrap(); - let root = dir.path(); - let mut ready = fs::File::create(root.join("ready.ndjson")).await.unwrap(); - ready - .write_all(br#"{"task_id":"t-0","ok":true}"#) - .await - .unwrap(); - ready.write_all(b"\n").await.unwrap(); - let mut fail = fs::File::create(root.join("failures.ndjson")) - .await - .unwrap(); - fail.write_all(br#"{"task_id":"t-1","ok":false}"#) - .await - .unwrap(); - fail.write_all(b"\n").await.unwrap(); - - let ledger = CheckpointLedger::load(root).await.unwrap(); - assert!(ledger.ready.contains("t-0")); - assert!(ledger.failed.contains("t-1")); - assert_eq!(ledger.skip_ids().len(), 2); - } - - #[tokio::test] - async fn tracker_flush_writes_checkpoint_json() { - let dir = tempfile::tempdir().unwrap(); - let tracker = CheckpointTracker::new(dir.path()); - tracker.note_dispatched(); - tracker.note_terminal(true, false); - tracker.note_skipped(2); - tracker.flush().await.unwrap(); - let body = fs::read_to_string(dir.path().join("checkpoint.json")) - .await - .unwrap(); - let snap: CheckpointProgress = serde_json::from_str(&body).unwrap(); - assert_eq!(snap.ok, 1); - assert_eq!(snap.dispatched, 1); - assert_eq!(snap.skipped, 2); - } - - #[tokio::test] - async fn empty_dir_loads_empty_ledger() { - let dir = tempfile::tempdir().unwrap(); - let ledger = CheckpointLedger::load(dir.path()).await.unwrap(); - assert!(ledger.skip_ids().is_empty()); - } - - #[tokio::test] - async fn skip_ids_is_ready_union_failed() { - let dir = tempfile::tempdir().unwrap(); - let mut ready = fs::File::create(dir.path().join("ready.ndjson")) - .await - .unwrap(); - ready.write_all(br#"{"task_id":"ok"}"#).await.unwrap(); - ready.write_all(b"\n").await.unwrap(); - let mut fail = fs::File::create(dir.path().join("failures.ndjson")) - .await - .unwrap(); - fail.write_all(br#"{"task_id":"bad"}"#).await.unwrap(); - fail.write_all(b"\n").await.unwrap(); - let ledger = CheckpointLedger::load(dir.path()).await.unwrap(); - let skip = ledger.skip_ids(); - assert!(skip.contains("ok") && skip.contains("bad")); - let tracker = CheckpointTracker::new(dir.path()); - tracker.seed_from_ledger(&ledger); - assert_eq!(tracker.snapshot().ok, 1); - assert_eq!(tracker.snapshot().fail, 1); - } - - #[tokio::test] - async fn failed_ids_can_be_filtered_by_error_kind() { - let dir = tempfile::tempdir().unwrap(); - fs::write( - dir.path().join("failures.ndjson"), - concat!( - r#"{"task_id":"execute","error_kind":"execute"}"#, - "\n", - r#"{"task_id":"infra","error_kind":"infra"}"#, - "\n", - r#"{"task_id":"legacy"}"#, - "\n" - ), - ) - .await - .unwrap(); - let ledger = CheckpointLedger::load(dir.path()).await.unwrap(); - let ids = ledger - .failed_ids_matching(dir.path(), &["execute".into()]) - .await - .unwrap(); - assert!(ids.contains("execute") && ids.contains("legacy")); - assert!(!ids.contains("infra")); - } - - #[tokio::test] - async fn load_skips_corrupt_lines_and_keeps_valid() { - let dir = tempfile::tempdir().unwrap(); - let mut ready = fs::File::create(dir.path().join("ready.ndjson")) - .await - .unwrap(); - ready - .write_all( - br#"{"task_id":"t-0","ok":true} -not-json -{"task_id":"t-1","ok":true} -{"no_id":true} -"#, - ) - .await - .unwrap(); - let ledger = CheckpointLedger::load(dir.path()).await.unwrap(); - assert!(ledger.ready.contains("t-0")); - assert!(ledger.ready.contains("t-1")); - assert_eq!(ledger.ready.len(), 2); - } -} diff --git a/crates/persisting-ppilot/src/cli.rs b/crates/persisting-ppilot/src/cli.rs deleted file mode 100644 index 48f34e70b..000000000 --- a/crates/persisting-ppilot/src/cli.rs +++ /dev/null @@ -1,565 +0,0 @@ -//! Embeddable argument surface for a pPilot host. -//! -//! With a script, `--check` validates first and the default path executes it. - -use crate::check::{CheckOptions, run_check}; -use crate::checkpoint::{CheckpointLedger, CheckpointTracker}; -use crate::coordination::RunCoordinator; -use crate::observe::{Observer, ObserverOptions}; -use crate::runtime::{RunOptions, run_fleet}; -use crate::sink::{JsonlFileSink, ResultSink, TeeSink}; -use crate::sink_writer::spawn_coordinated_sink_writer; -use crate::skip::SkipSet; -use crate::task::TaskResult; -use anyhow::{Context, Result, bail}; -use clap::{Args, ValueEnum}; -use persisting_events::{ChronicleControl, ChronicleServeProcessClient}; -use std::collections::BTreeMap; -use std::path::PathBuf; -use std::process::ExitCode; -use std::sync::Arc; -use tokio_util::sync::CancellationToken; - -/// Arguments accepted by an embedding pPilot host. -#[derive(Debug, Clone, Args)] -#[command( - about = "Run a pPilot plan (`plan()` + `execute(item)`).", - long_about = "pPilot — Durable Run Orchestrator.\n\nRun a Phase-1 map-style plan with bounded concurrency, checkpoint/resume, infrastructure retry, and a single result sink." -)] -pub struct PPilotArgs { - /// Plan script (`plan()` / `execute()`). - #[arg(value_name = "SCRIPT")] - pub script: PathBuf, - - /// Validate env + plan + execute (+ sample run) instead of a full run. - #[arg(long)] - pub check: bool, - - #[arg(short = 'w', long, default_value_t = 4)] - pub workers: usize, - - /// Concurrent Execute slots per logical worker/rank. Each slot is its own - /// WorkerActor + Python host (real parallelism). Default 1 — best when task - /// times vary widely across workers. - #[arg(long, default_value_t = 1)] - pub per_worker: usize, - - /// Global inflight cap (default: workers × per-worker, or WORLD_SIZE × per-worker under torchrun). - #[arg(long)] - pub max_inflight: Option, - - /// Infrastructure retries when a worker ask fails (not semantic retry). - #[arg(long, default_value_t = 2)] - pub retries: u32, - - /// Durable unique sink directory (`ready.ndjson` + `failures.ndjson` + `checkpoint.json`). - #[arg(long, value_name = "DIR")] - pub sink: Option, - - /// pChronicle Run control root. Supports local paths and object-store URIs. - /// Defaults to `--sink`, keeping `run-control/` beside the result journal. - #[arg(long, env = "PERSISTING_PPILOT_CONTROL_URI", value_name = "URI")] - pub control_uri: Option, - - /// Lifetime advertised by each newly issued Run lease (minimum: 1000ms). - #[arg(long, default_value_t = 30_000)] - pub lease_ttl_ms: u64, - - /// Logical run identifier exposed to workers through `persisting_ppilot.context()`. - /// Defaults to the sink directory name, or the plan filename for ephemeral runs. - #[arg(long)] - pub job_id: Option, - - /// Capability labels exposed as `context()["labels"]` (comma-separated). - /// They are informational in this release; scheduling remains least-loaded. - #[arg(long, value_delimiter = ',')] - pub worker_label: Vec, - - /// Resume from `--sink`: skip task ids already in ready/failures. - #[arg(long)] - pub resume: bool, - - /// With `--resume`, run failures of these kinds again (`execute`, `infra`, - /// or `cancelled`). May be repeated or comma-separated. - #[arg(long, value_delimiter = ',')] - pub rerun_failed: Vec, - - /// Also append terminal results to a Lance trajectory (requires `--sink`). - /// Writes `ppilot.result` / `ppilot.failure` events under traj storage. - #[cfg(feature = "traj-sink")] - #[arg(long)] - pub traj: bool, - - /// Lance storage root (default: `{sink}/traj`). - #[cfg(feature = "traj-sink")] - #[arg(long, value_name = "DIR")] - pub traj_storage: Option, - - /// Trajectory agent_id (default: `ppilot`). - #[cfg(feature = "traj-sink")] - #[arg(long, default_value = "ppilot")] - pub traj_agent: String, - - /// Trajectory session_id (default: sink directory name). - #[cfg(feature = "traj-sink")] - #[arg(long)] - pub traj_session: Option, - - #[arg(long, env = "PERSISTING_PYTHON", default_value = "python3")] - pub python: PathBuf, - - /// Standalone pVisor executable used for each Run. - #[arg(long, env = "PERSISTING_PVISOR_BIN", default_value = "pvisor")] - pub pvisor_binary: PathBuf, - - /// Standalone pChronicle executable used for durable control and trajectory writes. - #[arg(long, env = "PERSISTING_PCHRONICLE_BIN", default_value = "pchronicle")] - pub pchronicle_binary: PathBuf, - - /// Extra PYTHONPATH entries (plan dir is always added). - #[arg(short = 'E', long = "pythonpath")] - pub pythonpath: Vec, - - /// Result stream on stdout. Default: `ndjson`; with `--observe` default becomes `quiet` - /// so progress is not drowned out (pass `--results ndjson` to keep both). - #[arg(long, value_enum)] - pub results: Option, - - /// With `--check`: max tasks to execute (0 = all). Ignored on normal run. - #[arg(long, default_value_t = 0)] - pub limit: usize, - - /// Live queue / placement / duration progress on stderr (`[obs] …`). - /// Must be **before** `--`. Env: `PERSISTING_OBSERVE=1`. - #[arg(long, env = "PERSISTING_OBSERVE")] - pub observe: bool, - - /// Append observe events as NDJSON to FILE (implies observe). - #[arg(long, value_name = "FILE", env = "PERSISTING_OBSERVE_FILE")] - pub observe_file: Option, - - /// Also emit observe NDJSON on stderr (in addition to `[obs]` human lines). - #[arg(long)] - pub observe_json: bool, - - /// More stderr tracing (`persisting_ppilot` + Pulsing at info). - #[arg(short, long)] - pub verbose: bool, - - /// Args forwarded to the plan script (`sys.argv[1:]`). Put after `--`. - /// Example forwarded values: `--model x --n 2`. - #[arg(last = true, value_name = "SCRIPT_ARGS")] - pub script_args: Vec, -} - -#[derive(Clone, Copy, Debug, ValueEnum)] -pub enum ResultsFormat { - Ndjson, - Summary, - Quiet, -} - -/// Run pPilot (async). Caller owns the Tokio runtime. -pub async fn run_ppilot(args: PPilotArgs) -> Result { - let script = args.script; - - if args.check { - let report = run_check(CheckOptions { - script, - python: args.python, - pvisor_binary: args.pvisor_binary, - limit: args.limit, - workers: args.workers.max(1), - verbose: args.verbose, - pythonpath_extra: args.pythonpath, - script_args: args.script_args, - }) - .await?; - return Ok(if report.passed() { - ExitCode::SUCCESS - } else { - ExitCode::FAILURE - }); - } - - let under_torch = std::env::var_os("RANK").is_some(); - let job_id = args.job_id.clone().unwrap_or_else(|| { - args.sink - .as_ref() - .and_then(|path| path.file_name()) - .and_then(|name| name.to_str()) - .filter(|name| !name.is_empty()) - .map(str::to_string) - .or_else(|| { - script - .file_stem() - .and_then(|name| name.to_str()) - .map(str::to_string) - }) - .unwrap_or_else(|| "ppilot".into()) - }); - unsafe { std::env::set_var("PERSISTING_PPILOT_JOB_ID", &job_id) }; - if let Some(dir) = &args.sink { - unsafe { std::env::set_var("PERSISTING_PPILOT_OUTPUT_DIR", dir) }; - } - if !args.worker_label.is_empty() { - unsafe { - std::env::set_var( - "PERSISTING_PPILOT_WORKER_LABELS", - args.worker_label.join(","), - ) - }; - } - let per_worker = args.per_worker.max(1); - let max_inflight = args.max_inflight.unwrap_or_else(|| { - if under_torch { - let ws: usize = std::env::var("WORLD_SIZE") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(4); - ws.saturating_mul(per_worker).max(1) - } else { - args.workers.saturating_mul(per_worker).max(1) - } - }); - - let job_cancel = CancellationToken::new(); - let cancel_bg = job_cancel.clone(); - tokio::spawn(async move { - let _ = tokio::signal::ctrl_c().await; - tracing::warn!("Ctrl-C: cancelling job (RunFutures)"); - cancel_bg.cancel(); - }); - - let chronicle_control: Option> = if let Some(dir) = &args.sink { - let control_root = args - .control_uri - .clone() - .unwrap_or_else(|| dir.display().to_string()); - Some(Arc::new( - ChronicleServeProcessClient::spawn(&args.pchronicle_binary, control_root) - .await - .context("start pChronicle control process")?, - )) - } else { - None - }; - - let file_sink: Option> = if let Some(dir) = &args.sink { - let mut sinks: Vec> = Vec::new(); - sinks.push(Box::new( - JsonlFileSink::open(dir).await.context("open jsonl sink")?, - )); - #[cfg(feature = "traj-sink")] - if args.traj { - let traj_storage = args - .traj_storage - .clone() - .unwrap_or_else(|| dir.join("traj")); - tokio::fs::create_dir_all(&traj_storage) - .await - .with_context(|| format!("mkdir traj {}", traj_storage.display()))?; - let session = args.traj_session.clone().unwrap_or_else(|| { - dir.file_name() - .and_then(|s| s.to_str()) - .unwrap_or("run") - .to_string() - }); - let lance = crate::sink_traj::LanceResultSink::new( - Arc::clone( - chronicle_control - .as_ref() - .context("trajectory sink requires pChronicle control")?, - ), - traj_storage.display().to_string(), - args.traj_agent.clone(), - session.clone(), - ); - if let Ok(ledger) = CheckpointLedger::load(dir).await { - lance.seed_seen(ledger.skip_ids()); - } - eprintln!( - "[traj] lance append → {}/{}/{}", - traj_storage.display(), - args.traj_agent, - session - ); - sinks.push(Box::new(lance)); - } - Some(Arc::new(TeeSink::new(sinks))) - } else { - None - }; - - if args.resume && file_sink.is_none() { - bail!("--resume requires --sink DIR"); - } - if args.control_uri.is_some() && file_sink.is_none() { - bail!("--control-uri requires --sink DIR for the durable result journal"); - } - if !args.rerun_failed.is_empty() && !args.resume { - bail!("--rerun-failed requires --resume --sink DIR"); - } - for kind in &args.rerun_failed { - if !matches!(kind.as_str(), "execute" | "infra" | "cancelled") { - bail!("--rerun-failed expects execute, infra, or cancelled (got {kind:?})"); - } - } - - #[cfg(feature = "traj-sink")] - if args.traj && args.sink.is_none() { - bail!("--traj requires --sink DIR (JSONL ledger for --resume)"); - } - - let (skip_task_ids, checkpoint) = if let Some(dir) = &args.sink { - let tracker = Arc::new(CheckpointTracker::new(dir.clone())); - if args.resume { - let ledger = CheckpointLedger::load(dir) - .await - .context("load checkpoint ledger")?; - let mut skip = ledger.skip_ids(); - let rerun = ledger - .failed_ids_matching(dir, &args.rerun_failed) - .await - .context("filter failed task ids")?; - for id in &rerun { - skip.remove(id); - } - eprintln!( - "[ckpt] resume: ready={} fail={} rerun={} skip_total={}", - ledger.ready.len(), - ledger.failed.len(), - rerun.len(), - skip.len() - ); - tracker.seed_from_ledger(&ledger); - (skip.into_iter().collect(), Some(tracker)) - } else { - (SkipSet::new(), Some(tracker)) - } - } else { - (SkipSet::new(), None) - }; - - let coordinator = if let (Some(dir), Some(sink)) = (&args.sink, &file_sink) { - let coordinator = Arc::new( - RunCoordinator::open_with_control( - Arc::clone( - chronicle_control - .as_ref() - .context("durable coordinator requires pChronicle control")?, - ), - dir, - args.lease_ttl_ms, - Some(crate::executor::job_run_id_prefix(&job_id)), - ) - .await - .context("open pPilot Run coordinator")?, - ); - let observer = coordinator.durable_attempt_observer(); - let report = coordinator - .reconcile(sink.as_ref(), &observer) - .await - .context("reconcile pPilot Runs")?; - for task_id in &report.committed_task_ids { - skip_task_ids.insert(task_id.clone()); - } - for task_id in &report.retry_task_ids { - skip_task_ids.remove(task_id); - } - for task_id in &report.deferred_task_ids { - skip_task_ids.insert(task_id.clone()); - } - if report.recovered_commits > 0 - || report.recovered_sink_appends > 0 - || report.fenced_results > 0 - || !report.retry_task_ids.is_empty() - { - eprintln!( - "[reconcile] commits={} sink={} fenced={} active={} retry={}", - report.recovered_commits, - report.recovered_sink_appends, - report.fenced_results, - report.active_attempts, - report.retry_task_ids.len() - ); - } - Some(coordinator) - } else { - None - }; - - let observe_on = args.observe || args.observe_file.is_some() || args.observe_json; - let observer = if observe_on { - Observer::open(ObserverOptions { - human: true, - json_stderr: args.observe_json, - path: args.observe_file.clone(), - }) - .await - .context("open observe sink")? - } else { - Observer::disabled() - }; - - // With observe, default to quiet results so `[obs]` lines are visible. - let results_fmt = args.results.unwrap_or(if observe_on { - ResultsFormat::Quiet - } else { - ResultsFormat::Ndjson - }); - - let sink_writer = file_sink.as_ref().map(|sink| { - spawn_coordinated_sink_writer( - Arc::clone(sink), - checkpoint.clone(), - Some(skip_task_ids.clone()), - max_inflight.saturating_mul(2).max(16), - Arc::clone(coordinator.as_ref().expect("sink has coordinator")), - ) - }); - let sink_submit = sink_writer.as_ref().map(|w| w.submitter()); - - let opts = RunOptions { - script, - python: args.python, - pvisor_binary: args.pvisor_binary, - workers: args.workers, - max_inflight, - per_worker_inflight: per_worker, - pythonpath_extra: args.pythonpath, - script_args: args.script_args, - infra_retries: args.retries, - job_cancel, - observer, - skip_task_ids, - checkpoint: checkpoint.clone(), - sink_submitter: sink_submit, - coordinator, - }; - - let collected = run_fleet(opts, move |r: TaskResult| { - if matches!(results_fmt, ResultsFormat::Ndjson) - && let Ok(line) = r.to_ndjson() - { - println!("{line}"); - } - }) - .await - .context("run fleet")?; - - if let Some(w) = sink_writer { - w.join().await.context("sink persist")?; - } - - if let Some(ckpt) = &checkpoint { - let _ = ckpt.flush().await; - eprintln!("{}", ckpt.summary_line()); - } - - if collected.is_empty() && under_torch { - let rank: usize = std::env::var("RANK") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(0); - if rank != 0 { - return Ok(ExitCode::SUCCESS); - } - } - - let failed = collected.iter().filter(|r| !r.ok || r.cancelled).count(); - let summary = build_run_summary(&collected, args.sink.as_ref()); - if let Some(dir) = &args.sink { - tokio::fs::write( - dir.join("summary.json"), - serde_json::to_vec_pretty(&summary).context("encode run summary")?, - ) - .await - .context("write summary.json")?; - } - - if matches!(results_fmt, ResultsFormat::Summary) { - println!("{summary}"); - for r in &collected { - if !r.ok - && let Ok(line) = r.to_ndjson() - { - eprintln!("{line}"); - } - } - } - - Ok(if failed == 0 { - ExitCode::SUCCESS - } else { - ExitCode::FAILURE - }) -} - -fn build_run_summary(results: &[TaskResult], sink: Option<&PathBuf>) -> serde_json::Value { - let mut aggregates: BTreeMap = BTreeMap::new(); - let mut error_kinds: BTreeMap = BTreeMap::new(); - let mut artifacts = 0u64; - for result in results { - for (name, value) in &result.metrics { - let entry = aggregates.entry(name.clone()).or_insert((0.0, 0)); - entry.0 += value; - entry.1 += 1; - } - artifacts += result.artifacts.len() as u64; - if let Some(kind) = &result.error_kind { - *error_kinds - .entry(format!("{kind:?}").to_lowercase()) - .or_default() += 1; - } - } - let metrics: BTreeMap<_, _> = aggregates - .into_iter() - .map(|(name, (sum, count))| { - ( - name, - serde_json::json!({"count": count, "sum": sum, "mean": sum / count as f64}), - ) - }) - .collect(); - serde_json::json!({ - "total": results.len(), - "ok": results.iter().filter(|r| r.ok && !r.cancelled).count(), - "failed": results.iter().filter(|r| !r.ok || r.cancelled).count(), - "cancelled": results.iter().filter(|r| r.cancelled).count(), - "error_kinds": error_kinds, - "metrics": metrics, - "artifact_count": artifacts, - "sink": sink.map(|p| p.display().to_string()), - }) -} - -/// Ensure tracing is initialized once (safe to call from nested CLI). -/// -/// Default is quiet: hush Pulsing actor lifecycle noise. Override with `RUST_LOG`, -/// or pass `--verbose` for `persisting_ppilot=info,pulsing_actor=info`. -pub fn init_tracing() { - init_tracing_with_verbose(false); -} - -/// Same as [`init_tracing`], with optional verbose default when `RUST_LOG` is unset. -pub fn init_tracing_with_verbose(verbose: bool) { - let _ = tracing_subscriber::fmt() - .with_env_filter( - tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| { - if verbose { - tracing_subscriber::EnvFilter::new( - "persisting_ppilot=info,pulsing_actor=info,info", - ) - } else { - // Quiet default: only warn+ from Pulsing; pPilot stays at warn. - tracing_subscriber::EnvFilter::new( - "persisting_ppilot=warn,pulsing_actor=warn,warn", - ) - } - }), - ) - .with_writer(std::io::stderr) - .with_target(verbose) - .try_init(); -} diff --git a/crates/persisting-ppilot/src/coordination.rs b/crates/persisting-ppilot/src/coordination.rs deleted file mode 100644 index e9d26aa29..000000000 --- a/crates/persisting-ppilot/src/coordination.rs +++ /dev/null @@ -1,1203 +0,0 @@ -//! Durable pPilot ownership, terminal commit, and restart reconciliation. -//! -//! The local result journal deliberately precedes the pChronicle RunCommit: -//! after a crash, the reconciler can replay either the CAS commit or the sink -//! append without executing the workload again. - -use crate::digest::sha256_hex; -use crate::sink::{ResultSink, persist_terminal}; -use crate::task::TaskResult; -use anyhow::{Context, Result, bail}; -use async_trait::async_trait; -use persisting_agentctl::{ - AttemptId, RunCommitRequest, RunId, RunLeaseRecord, RunResult, RunState, -}; -#[cfg(not(test))] -use persisting_events::ChronicleServeProcessClient; -#[cfg(test)] -use persisting_events::MemoryChronicleControl; -use persisting_events::{ - AttemptRecordState, ChronicleControl, CommitRunOutcome, LeaseAcquireOutcome, unix_now_ms, -}; -use serde::{Deserialize, Serialize}; -use std::collections::{BTreeMap, BTreeSet}; -use std::fs::{File, OpenOptions}; -use std::future::Future; -use std::io::Write; -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use std::sync::Mutex; -use std::sync::atomic::{AtomicU64, Ordering}; -use tokio_util::sync::CancellationToken; - -const RESULT_JOURNAL_SCHEMA_VERSION: u32 = 1; -pub const MIN_LEASE_TTL_MS: u64 = 1_000; -static OWNER_SEQUENCE: AtomicU64 = AtomicU64::new(1); - -enum LeaseRenewalOutcome { - Stopped, - DeadlineExceeded, - Finished(Result), -} - -async fn wait_for_lease_renewal( - stop: &CancellationToken, - deadline: tokio::time::Instant, - renewal: F, -) -> LeaseRenewalOutcome -where - F: Future>, -{ - tokio::select! { - _ = stop.cancelled() => LeaseRenewalOutcome::Stopped, - renewal = tokio::time::timeout_at(deadline, renewal) => match renewal { - Ok(result) => LeaseRenewalOutcome::Finished(result), - Err(_) => LeaseRenewalOutcome::DeadlineExceeded, - }, - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -enum DurableResultStatus { - Staged, - Committed, - Fenced, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct DurableResultRecord { - schema_version: u32, - task_id: String, - run_id: RunId, - attempt_id: AttemptId, - lease_epoch: u64, - result_digest: String, - result: TaskResult, - status: DurableResultStatus, - sink_persisted: bool, -} - -#[derive(Debug, Clone)] -pub enum AttemptObservation { - /// No live attempt is visible; the task should receive a new lease. - Absent, - /// The runtime still owns an attempt for this Run. - Active { - attempt_id: AttemptId, - lease_epoch: u64, - }, - /// The runtime has a terminal result that was not yet committed. - Terminal(Box), - /// The durable lease has not expired, but pVisor has not published an - /// Attempt record yet. Reconciliation must defer rather than re-dispatch. - Pending, -} - -/// Runtime seam used by the reconciler. A future remote pVisor registry can -/// implement this without changing the durable control protocol. -#[async_trait] -pub trait AttemptObserver: Send + Sync { - async fn observe(&self, lease: &RunLeaseRecord) -> Result; - - async fn cancel_stale(&self, _run_id: &RunId, _attempt_id: &AttemptId) -> Result<()> { - Ok(()) - } -} - -/// Startup observer for today's process-local pVisor workers. Their processes -/// cannot survive a pPilot process restart, so every uncommitted lease is orphaned. -pub struct ProcessLocalAttemptObserver; - -#[async_trait] -impl AttemptObserver for ProcessLocalAttemptObserver { - async fn observe(&self, _lease: &RunLeaseRecord) -> Result { - Ok(AttemptObservation::Absent) - } -} - -/// pChronicle-backed observer used by production resume/reconciliation. -pub struct DurableAttemptObserver { - control: Arc, -} - -impl DurableAttemptObserver { - fn new(control: Arc) -> Self { - Self { control } - } -} - -#[async_trait] -impl AttemptObserver for DurableAttemptObserver { - async fn observe(&self, lease: &RunLeaseRecord) -> Result { - let Some(record) = self.control.get_attempt(lease.run_id.as_str()).await? else { - return Ok(if lease.expires_at_unix_ms > unix_now_ms() { - AttemptObservation::Pending - } else { - AttemptObservation::Absent - }); - }; - if record.lease_epoch != lease.epoch { - return Ok(AttemptObservation::Absent); - } - match record.state { - AttemptRecordState::Active if record.is_live_at(unix_now_ms()) => { - Ok(AttemptObservation::Active { - attempt_id: AttemptId::new(record.attempt_id), - lease_epoch: record.lease_epoch, - }) - } - AttemptRecordState::Active => Ok(AttemptObservation::Absent), - AttemptRecordState::Terminal => { - let value = record - .terminal_result - .context("terminal Attempt record has no result")?; - let result: RunResult = serde_json::from_value(value) - .context("decode terminal pVisor RunResult from Attempt registry")?; - let task_id = lease - .task_id - .as_deref() - .context("terminal Run lease has no task_id")?; - Ok(AttemptObservation::Terminal(Box::new( - crate::executor::run_result_to_task_result( - result, - task_id, - "recovered", - crate::task::unix_now(), - ), - ))) - } - } - } -} - -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct ReconcileReport { - pub committed_task_ids: BTreeSet, - pub retry_task_ids: BTreeSet, - pub deferred_task_ids: BTreeSet, - pub recovered_commits: usize, - pub recovered_sink_appends: usize, - pub fenced_results: usize, - pub active_attempts: usize, - pub stale_attempts_cancelled: usize, -} - -/// Shared pPilot coordination state for one job/sink. -#[derive(Clone)] -pub struct RunCoordinator { - control: Arc, - journal_root: PathBuf, - lease_ttl_ms: u64, - owner_id: String, - run_id_prefix: Option, - orphaned_runs: Arc>>, - heartbeats: Arc>>, -} - -impl std::fmt::Debug for RunCoordinator { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter - .debug_struct("RunCoordinator") - .field("control_root", &self.control.root_uri()) - .field("journal_root", &self.journal_root) - .field("lease_ttl_ms", &self.lease_ttl_ms) - .field("owner_id", &self.owner_id) - .field("run_id_prefix", &self.run_id_prefix) - .finish() - } -} - -impl RunCoordinator { - pub async fn open( - control_root: impl AsRef, - sink_root: impl Into, - lease_ttl_ms: u64, - ) -> Result { - Self::open_with_binary("pchronicle", control_root, sink_root, lease_ttl_ms, None).await - } - - pub async fn open_for_job( - control_root: impl AsRef, - sink_root: impl Into, - lease_ttl_ms: u64, - job_id: &str, - ) -> Result { - Self::open_with_binary( - "pchronicle", - control_root, - sink_root, - lease_ttl_ms, - Some(crate::executor::job_run_id_prefix(job_id)), - ) - .await - } - - pub async fn open_for_job_with_binary( - binary: impl AsRef, - control_root: impl AsRef, - sink_root: impl Into, - lease_ttl_ms: u64, - job_id: &str, - ) -> Result { - Self::open_with_binary( - binary, - control_root, - sink_root, - lease_ttl_ms, - Some(crate::executor::job_run_id_prefix(job_id)), - ) - .await - } - - async fn open_with_binary( - binary: impl AsRef, - control_root: impl AsRef, - sink_root: impl Into, - lease_ttl_ms: u64, - run_id_prefix: Option, - ) -> Result { - let control_root = control_root.as_ref().to_owned(); - #[cfg(test)] - let control: Arc = { - let _ = binary; - Arc::new(MemoryChronicleControl::new(control_root)) - }; - #[cfg(not(test))] - let control = Arc::new( - ChronicleServeProcessClient::spawn(binary, control_root) - .await - .context("start pChronicle control client")?, - ); - Self::open_with_control(control, sink_root, lease_ttl_ms, run_id_prefix).await - } - - pub async fn open_with_control( - control: Arc, - sink_root: impl Into, - lease_ttl_ms: u64, - run_id_prefix: Option, - ) -> Result { - anyhow::ensure!( - lease_ttl_ms >= MIN_LEASE_TTL_MS, - "lease TTL must be at least {MIN_LEASE_TTL_MS}ms; got {lease_ttl_ms}ms" - ); - let journal_root = sink_root.into().join(".ppilot-state").join("results"); - tokio::fs::create_dir_all(&journal_root) - .await - .with_context(|| format!("create result journal {}", journal_root.display()))?; - Ok(Self { - control, - journal_root, - lease_ttl_ms, - owner_id: unique_owner_id(), - run_id_prefix, - orphaned_runs: Arc::new(Mutex::new(BTreeSet::new())), - heartbeats: Arc::new(Mutex::new(BTreeMap::new())), - }) - } - - pub fn control(&self) -> &Arc { - &self.control - } - - pub fn owner_id(&self) -> &str { - &self.owner_id - } - - pub fn durable_attempt_observer(&self) -> DurableAttemptObserver { - DurableAttemptObserver::new(Arc::clone(&self.control)) - } - - pub fn lease_ttl_ms(&self) -> u64 { - self.lease_ttl_ms - } - - pub(crate) fn start_lease_heartbeat( - &self, - run_id: RunId, - lease_epoch: u64, - task_cancel: CancellationToken, - ) -> Result { - let stop = CancellationToken::new(); - let old = self - .heartbeats - .lock() - .map_err(|_| anyhow::anyhow!("lease heartbeat lock poisoned"))? - .insert(run_id.clone(), stop.clone()); - if let Some(old) = old { - old.cancel(); - } - let control = Arc::clone(&self.control); - let owner = self.owner_id.clone(); - let ttl_ms = self.lease_ttl_ms; - let heartbeat_stop = stop.clone(); - let heartbeat_run_id = run_id.clone(); - tokio::spawn(async move { - let interval_ms = (ttl_ms / 3).clamp(1, 10_000); - let interval = std::time::Duration::from_millis(interval_ms); - let mut ticker = - tokio::time::interval_at(tokio::time::Instant::now() + interval, interval); - ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - let mut last_success = tokio::time::Instant::now(); - loop { - tokio::select! { - _ = heartbeat_stop.cancelled() => break, - _ = ticker.tick() => {} - } - let deadline = last_success + std::time::Duration::from_millis(ttl_ms); - let renewal = wait_for_lease_renewal( - &heartbeat_stop, - deadline, - control.renew_lease(&heartbeat_run_id, lease_epoch, &owner, ttl_ms), - ) - .await; - match renewal { - LeaseRenewalOutcome::Stopped => break, - LeaseRenewalOutcome::DeadlineExceeded => { - tracing::warn!( - run_id = %heartbeat_run_id, - lease_epoch, - "Run lease renewal exceeded its validity deadline" - ); - task_cancel.cancel(); - break; - } - LeaseRenewalOutcome::Finished(Ok(true)) => { - last_success = tokio::time::Instant::now() - } - LeaseRenewalOutcome::Finished(Ok(false)) => { - tracing::warn!(run_id = %heartbeat_run_id, lease_epoch, "Run lease ownership lost"); - task_cancel.cancel(); - break; - } - LeaseRenewalOutcome::Finished(Err(error)) => { - tracing::warn!(run_id = %heartbeat_run_id, lease_epoch, %error, "Run lease renewal failed"); - if last_success.elapsed() >= std::time::Duration::from_millis(ttl_ms) { - task_cancel.cancel(); - break; - } - } - } - } - }); - Ok(LeaseHeartbeat { - run_id, - stop, - heartbeats: Arc::clone(&self.heartbeats), - detached: false, - }) - } - - /// Acquire a monotonically increasing fencing token. A committed Run is - /// never leased again. - pub async fn acquire_lease(&self, run_id: &RunId, task_id: &str, owner: &str) -> Result { - let force = self - .orphaned_runs - .lock() - .map_err(|_| anyhow::anyhow!("orphaned Run set lock poisoned"))? - .remove(run_id); - let outcome = if force { - self.control - .takeover_lease(run_id, Some(task_id), owner, self.lease_ttl_ms) - .await? - } else { - self.control - .acquire_lease(run_id, Some(task_id), owner, self.lease_ttl_ms) - .await? - }; - match outcome { - LeaseAcquireOutcome::Acquired(lease) => Ok(lease.epoch), - LeaseAcquireOutcome::Held(lease) => bail!( - "Run {run_id} lease epoch {} is held by {} until {}", - lease.epoch, - lease.owner, - lease.expires_at_unix_ms - ), - LeaseAcquireOutcome::AlreadyCommitted(_) => { - bail!("Run {run_id} is already committed") - } - } - } - - /// Stage, fence, commit, then publish a terminal result to the user sink. - pub async fn finalize_result(&self, sink: &dyn ResultSink, result: &TaskResult) -> Result<()> { - let run_id = result.run_id.as_deref().map(RunId::new); - let outcome = async { - let mut record = self.stage_result(result).await?; - self.commit_record(&mut record).await?; - persist_terminal(sink, &record.result).await?; - record.sink_persisted = true; - self.write_record(&record).await - } - .await; - if let Some(run_id) = run_id { - self.stop_lease_heartbeat(&run_id); - } - outcome - } - - /// Compare durable records, pChronicle control state, and observable pVisor - /// attempts. It never guesses success: only a terminal payload can create a commit. - pub async fn reconcile( - &self, - sink: &dyn ResultSink, - observer: &dyn AttemptObserver, - ) -> Result { - let mut report = ReconcileReport::default(); - - for mut record in self.read_records().await? { - if self - .run_id_prefix - .as_ref() - .is_some_and(|prefix| !record.run_id.as_str().starts_with(prefix)) - { - continue; - } - if record.schema_version != RESULT_JOURNAL_SCHEMA_VERSION { - bail!( - "unsupported pPilot result journal schema {} for task {}", - record.schema_version, - record.task_id - ); - } - match record.status { - DurableResultStatus::Staged => match self.commit_record(&mut record).await { - Ok(()) => report.recovered_commits += 1, - Err(error) => { - tracing::warn!(task_id = %record.task_id, %error, "fencing staged result during reconciliation"); - record.status = DurableResultStatus::Fenced; - self.write_record(&record).await?; - report.fenced_results += 1; - report.retry_task_ids.insert(record.task_id.clone()); - self.note_orphaned(&record.run_id)?; - continue; - } - }, - DurableResultStatus::Fenced => { - report.fenced_results += 1; - report.retry_task_ids.insert(record.task_id.clone()); - self.note_orphaned(&record.run_id)?; - continue; - } - DurableResultStatus::Committed => {} - } - report.committed_task_ids.insert(record.task_id.clone()); - if !record.sink_persisted { - persist_terminal(sink, &record.result).await?; - record.sink_persisted = true; - self.write_record(&record).await?; - report.recovered_sink_appends += 1; - } - } - - for control in self.control.list_runs().await? { - if self - .run_id_prefix - .as_ref() - .is_some_and(|prefix| !control.run_id.as_str().starts_with(prefix)) - { - continue; - } - if let Some(commit) = control.commit { - if let Some(task_id) = commit.request.task_id { - report.committed_task_ids.insert(task_id); - } - continue; - } - let Some(lease) = control.lease else { - continue; - }; - let Some(task_id) = lease.task_id.clone() else { - continue; - }; - if report.committed_task_ids.contains(&task_id) { - continue; - } - match observer.observe(&lease).await? { - AttemptObservation::Absent => { - self.note_orphaned(&lease.run_id)?; - report.retry_task_ids.insert(task_id); - } - AttemptObservation::Pending => { - report.active_attempts += 1; - report.deferred_task_ids.insert(task_id); - } - AttemptObservation::Active { - attempt_id, - lease_epoch, - } if lease_epoch == lease.epoch => { - report.active_attempts += 1; - report.deferred_task_ids.insert(task_id.clone()); - // Backfill attempt identity if the submit succeeded before a crash. - let _ = self - .control - .bind_attempt(&lease.run_id, lease.epoch, attempt_id) - .await?; - } - AttemptObservation::Active { attempt_id, .. } => { - observer.cancel_stale(&lease.run_id, &attempt_id).await?; - report.stale_attempts_cancelled += 1; - self.note_orphaned(&lease.run_id)?; - report.retry_task_ids.insert(task_id); - } - AttemptObservation::Terminal(result) => { - self.finalize_result(sink, &result).await?; - report.recovered_commits += 1; - report.recovered_sink_appends += 1; - report.committed_task_ids.insert(task_id); - } - } - } - for task_id in &report.committed_task_ids { - report.retry_task_ids.remove(task_id); - } - Ok(report) - } - - async fn stage_result(&self, result: &TaskResult) -> Result { - let run_id = result - .run_id - .as_deref() - .context("terminal TaskResult has no run_id")?; - let attempt_id = result - .attempt_id - .as_deref() - .context("terminal TaskResult has no attempt_id")?; - if result.lease_epoch == 0 { - bail!("terminal TaskResult has no lease epoch"); - } - let record = DurableResultRecord { - schema_version: RESULT_JOURNAL_SCHEMA_VERSION, - task_id: result.task_id.clone(), - run_id: RunId::new(run_id), - attempt_id: AttemptId::new(attempt_id), - lease_epoch: result.lease_epoch, - result_digest: result_digest(result)?, - result: result.clone(), - status: DurableResultStatus::Staged, - sink_persisted: false, - }; - self.write_record(&record).await?; - Ok(record) - } - - async fn commit_record(&self, record: &mut DurableResultRecord) -> Result<()> { - let bound = self - .control - .bind_attempt( - &record.run_id, - record.lease_epoch, - record.attempt_id.clone(), - ) - .await?; - if !bound { - record.status = DurableResultStatus::Fenced; - self.write_record(record).await?; - bail!( - "Run result attempt {} no longer owns lease epoch {}", - record.attempt_id, - record.lease_epoch - ); - } - let outcome = self - .control - .commit_run(RunCommitRequest { - run_id: record.run_id.clone(), - task_id: Some(record.task_id.clone()), - attempt_id: record.attempt_id.clone(), - lease_epoch: record.lease_epoch, - state: task_result_state(&record.result), - event_high_watermark: None, - result_digest: record.result_digest.clone(), - }) - .await?; - match outcome { - CommitRunOutcome::Committed(_) | CommitRunOutcome::AlreadyCommitted(_) => { - record.status = DurableResultStatus::Committed; - self.write_record(record).await - } - CommitRunOutcome::StaleLease { - supplied_epoch, - current_epoch, - } => { - record.status = DurableResultStatus::Fenced; - self.write_record(record).await?; - bail!( - "stale Run result fenced: supplied epoch {supplied_epoch}, current {current_epoch:?}" - ) - } - CommitRunOutcome::Conflict(existing) => { - record.status = DurableResultStatus::Fenced; - self.write_record(record).await?; - bail!( - "RunCommit conflicts with attempt {} epoch {}", - existing.request.attempt_id, - existing.request.lease_epoch - ) - } - } - } - - async fn read_records(&self) -> Result> { - let root = self.journal_root.clone(); - tokio::task::spawn_blocking(move || { - let mut records = Vec::new(); - for entry in std::fs::read_dir(&root)? { - let path = entry?.path(); - if path.extension().and_then(|value| value.to_str()) != Some("json") { - continue; - } - let bytes = std::fs::read(&path) - .with_context(|| format!("read result journal {}", path.display()))?; - records.push( - serde_json::from_slice(&bytes) - .with_context(|| format!("decode result journal {}", path.display()))?, - ); - } - Ok(records) - }) - .await? - } - - async fn write_record(&self, record: &DurableResultRecord) -> Result<()> { - let path = self.record_path(&record.task_id); - let bytes = serde_json::to_vec_pretty(record)?; - tokio::task::spawn_blocking(move || atomic_write(&path, &bytes)).await? - } - - fn record_path(&self, task_id: &str) -> PathBuf { - let name = format!("{}.json", sha256_hex(task_id)); - self.journal_root.join(name) - } - - fn note_orphaned(&self, run_id: &RunId) -> Result<()> { - self.orphaned_runs - .lock() - .map_err(|_| anyhow::anyhow!("orphaned Run set lock poisoned"))? - .insert(run_id.clone()); - Ok(()) - } - - fn stop_lease_heartbeat(&self, run_id: &RunId) { - if let Ok(mut heartbeats) = self.heartbeats.lock() - && let Some(stop) = heartbeats.remove(run_id) - { - stop.cancel(); - } - } -} - -pub(crate) struct LeaseHeartbeat { - run_id: RunId, - stop: CancellationToken, - heartbeats: Arc>>, - detached: bool, -} - -impl LeaseHeartbeat { - pub(crate) fn detach(mut self) { - self.detached = true; - } -} - -impl Drop for LeaseHeartbeat { - fn drop(&mut self) { - if self.detached { - return; - } - self.stop.cancel(); - if let Ok(mut heartbeats) = self.heartbeats.lock() { - heartbeats.remove(&self.run_id); - } - } -} - -fn task_result_state(result: &TaskResult) -> RunState { - if result.ok && !result.cancelled { - RunState::Completed - } else if result.cancelled { - RunState::Cancelled - } else { - RunState::Failed - } -} - -fn unique_owner_id() -> String { - let nanos = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|duration| duration.as_nanos()) - .unwrap_or(0); - let sequence = OWNER_SEQUENCE.fetch_add(1, Ordering::Relaxed); - format!("ppilot:{}:{nanos}:{sequence}", std::process::id()) -} - -fn result_digest(result: &TaskResult) -> Result { - // serde_json's default map representation is key-sorted; hashing the Value - // makes HashMap insertion order irrelevant across a crash/reload boundary. - let canonical = serde_json::to_value(result)?; - Ok(format!( - "sha256:{}", - sha256_hex(serde_json::to_vec(&canonical)?) - )) -} - -fn atomic_write(path: &Path, bytes: &[u8]) -> Result<()> { - let parent = path.parent().context("journal path has no parent")?; - std::fs::create_dir_all(parent)?; - let temporary = parent.join(format!( - ".{}.{}.tmp", - path.file_name() - .and_then(|name| name.to_str()) - .unwrap_or("result"), - std::process::id() - )); - let mut file = OpenOptions::new() - .create(true) - .truncate(true) - .write(true) - .open(&temporary)?; - file.write_all(bytes)?; - file.sync_all()?; - std::fs::rename(&temporary, path)?; - if let Ok(directory) = File::open(parent) { - let _ = directory.sync_all(); - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::sink::JsonlFileSink; - use serde_json::json; - use std::sync::atomic::{AtomicUsize, Ordering}; - - fn terminal(task: &str, run: &str, attempt: &str, epoch: u64) -> TaskResult { - let mut result = TaskResult::success(task, json!({"answer": 42}), "w0", 1.0); - result.run_id = Some(run.into()); - result.attempt_id = Some(attempt.into()); - result.lease_epoch = epoch; - result - } - - #[tokio::test] - async fn durable_observer_defers_live_attempt_and_recovers_terminal_result() { - let dir = tempfile::tempdir().unwrap(); - let coordinator = RunCoordinator::open( - dir.path().to_string_lossy(), - dir.path().join("sink"), - 30_000, - ) - .await - .unwrap(); - let run = RunId::new("run-durable-observer"); - let epoch = coordinator - .acquire_lease(&run, "task-durable", "driver-a") - .await - .unwrap(); - let lease = coordinator - .control - .get_run(&run) - .await - .unwrap() - .unwrap() - .lease - .unwrap(); - let observer = coordinator.durable_attempt_observer(); - assert!(matches!( - observer.observe(&lease).await.unwrap(), - AttemptObservation::Pending - )); - - coordinator - .control - .publish_attempt_active(run.as_str(), "attempt-durable", epoch, 30_000) - .await - .unwrap(); - assert!(matches!( - observer.observe(&lease).await.unwrap(), - AttemptObservation::Active { lease_epoch, .. } if lease_epoch == epoch - )); - - let mut spec = - persisting_agentctl::RunSpec::process(run.as_str(), "ppilot", "ppilot-plan-host"); - spec.lease_epoch = epoch; - let run_result = crate::executor::task_result_to_run_result( - spec, - AttemptId::new("attempt-durable"), - terminal("task-durable", run.as_str(), "attempt-durable", epoch), - ); - coordinator - .control - .publish_attempt_terminal( - run.as_str(), - "attempt-durable", - epoch, - serde_json::to_value(run_result).unwrap(), - ) - .await - .unwrap(); - let AttemptObservation::Terminal(recovered) = observer.observe(&lease).await.unwrap() - else { - panic!("terminal Attempt should be recoverable"); - }; - assert_eq!(recovered.task_id, "task-durable"); - assert_eq!(recovered.attempt_id.as_deref(), Some("attempt-durable")); - assert_eq!(recovered.lease_epoch, epoch); - } - - #[tokio::test] - async fn reconciliation_closes_stage_before_commit_crash_window() { - let dir = tempfile::tempdir().unwrap(); - let coordinator = RunCoordinator::open( - dir.path().to_string_lossy(), - dir.path().join("sink"), - 30_000, - ) - .await - .unwrap(); - let run = RunId::new("run-1"); - let epoch = coordinator - .acquire_lease(&run, "task-1", "driver-a") - .await - .unwrap(); - coordinator - .stage_result(&terminal("task-1", "run-1", "attempt-1", epoch)) - .await - .unwrap(); - - let sink = JsonlFileSink::open(dir.path().join("sink")).await.unwrap(); - let report = coordinator - .reconcile(&sink, &ProcessLocalAttemptObserver) - .await - .unwrap(); - assert_eq!(report.recovered_commits, 1); - assert_eq!(report.recovered_sink_appends, 1); - assert!(report.committed_task_ids.contains("task-1")); - assert!( - coordinator - .control - .get_run(&run) - .await - .unwrap() - .unwrap() - .commit - .is_some() - ); - } - - #[tokio::test] - async fn reconciliation_closes_commit_before_sink_crash_window() { - let dir = tempfile::tempdir().unwrap(); - let sink_root = dir.path().join("sink"); - let coordinator = RunCoordinator::open(dir.path().to_string_lossy(), &sink_root, 30_000) - .await - .unwrap(); - let run = RunId::new("run-after-commit"); - let epoch = coordinator - .acquire_lease(&run, "task-after-commit", "driver-a") - .await - .unwrap(); - let mut record = coordinator - .stage_result(&terminal( - "task-after-commit", - "run-after-commit", - "attempt-after-commit", - epoch, - )) - .await - .unwrap(); - coordinator.commit_record(&mut record).await.unwrap(); - - let sink = JsonlFileSink::open(&sink_root).await.unwrap(); - let report = coordinator - .reconcile(&sink, &ProcessLocalAttemptObserver) - .await - .unwrap(); - assert_eq!(report.recovered_commits, 0); - assert_eq!(report.recovered_sink_appends, 1); - let ready = tokio::fs::read_to_string(sink_root.join("ready.ndjson")) - .await - .unwrap(); - assert!(ready.contains("task-after-commit")); - } - - struct MixedObserver { - cancelled: AtomicUsize, - } - - #[async_trait] - impl AttemptObserver for MixedObserver { - async fn observe(&self, lease: &RunLeaseRecord) -> Result { - if lease.task_id.as_deref() == Some("active") { - Ok(AttemptObservation::Active { - attempt_id: AttemptId::new("attempt-active"), - lease_epoch: lease.epoch, - }) - } else { - Ok(AttemptObservation::Active { - attempt_id: AttemptId::new("attempt-stale"), - lease_epoch: lease.epoch.saturating_sub(1), - }) - } - } - - async fn cancel_stale(&self, _run_id: &RunId, _attempt_id: &AttemptId) -> Result<()> { - self.cancelled.fetch_add(1, Ordering::AcqRel); - Ok(()) - } - } - - #[tokio::test] - async fn reconciler_keeps_current_attempt_and_cancels_stale_attempt() { - let dir = tempfile::tempdir().unwrap(); - let coordinator = RunCoordinator::open( - dir.path().to_string_lossy(), - dir.path().join("sink"), - 30_000, - ) - .await - .unwrap(); - coordinator - .acquire_lease(&RunId::new("run-active"), "active", "driver-a") - .await - .unwrap(); - coordinator - .acquire_lease(&RunId::new("run-stale"), "stale", "driver-a") - .await - .unwrap(); - let observer = MixedObserver { - cancelled: AtomicUsize::new(0), - }; - let sink = JsonlFileSink::open(dir.path().join("sink")).await.unwrap(); - let report = coordinator.reconcile(&sink, &observer).await.unwrap(); - assert_eq!(report.active_attempts, 1); - assert_eq!(report.stale_attempts_cancelled, 1); - assert_eq!(observer.cancelled.load(Ordering::Acquire), 1); - assert!(!report.retry_task_ids.contains("active")); - assert!(report.retry_task_ids.contains("stale")); - } - - #[tokio::test] - async fn job_scoped_reconciler_ignores_other_jobs_in_shared_control_root() { - let dir = tempfile::tempdir().unwrap(); - let sink_root = dir.path().join("sink"); - let shared: Arc = - Arc::new(MemoryChronicleControl::new(dir.path().to_string_lossy())); - let unscoped = - RunCoordinator::open_with_control(Arc::clone(&shared), &sink_root, 30_000, None) - .await - .unwrap(); - let other_run = RunId::new("ppilot-job-b-task-1"); - let epoch = unscoped - .acquire_lease(&other_run, "same-task-id", "driver-b") - .await - .unwrap(); - unscoped - .stage_result(&terminal( - "same-task-id", - other_run.as_str(), - "attempt-b", - epoch, - )) - .await - .unwrap(); - - let scoped = RunCoordinator::open_with_control( - shared, - &sink_root, - 30_000, - Some(crate::executor::job_run_id_prefix("job-a")), - ) - .await - .unwrap(); - let sink = JsonlFileSink::open(&sink_root).await.unwrap(); - let report = scoped - .reconcile(&sink, &ProcessLocalAttemptObserver) - .await - .unwrap(); - assert!(report.committed_task_ids.is_empty()); - assert!(report.retry_task_ids.is_empty()); - assert!( - scoped - .control - .get_run(&other_run) - .await - .unwrap() - .unwrap() - .commit - .is_none() - ); - } - - #[tokio::test] - async fn renewal_wait_honors_the_lease_deadline() { - let stop = CancellationToken::new(); - let outcome = tokio::time::timeout( - std::time::Duration::from_secs(1), - wait_for_lease_renewal( - &stop, - tokio::time::Instant::now() + std::time::Duration::from_millis(10), - std::future::pending(), - ), - ) - .await - .expect("renewal deadline should not hang"); - - assert!(matches!(outcome, LeaseRenewalOutcome::DeadlineExceeded)); - } - - #[tokio::test] - async fn lease_ttl_below_minimum_is_rejected() { - let dir = tempfile::tempdir().unwrap(); - let error = RunCoordinator::open( - dir.path().to_string_lossy(), - dir.path().join("sink"), - MIN_LEASE_TTL_MS - 1, - ) - .await - .unwrap_err(); - - assert!( - error - .to_string() - .contains("lease TTL must be at least 1000ms") - ); - } - - #[tokio::test] - async fn heartbeat_keeps_long_attempt_lease_unexpired() { - let dir = tempfile::tempdir().unwrap(); - let coordinator = RunCoordinator::open( - dir.path().to_string_lossy(), - dir.path().join("sink"), - MIN_LEASE_TTL_MS, - ) - .await - .unwrap(); - let run = RunId::new("run-heartbeat"); - let epoch = coordinator - .acquire_lease(&run, "task-heartbeat", coordinator.owner_id()) - .await - .unwrap(); - let initial_expiry = coordinator - .control - .get_run(&run) - .await - .unwrap() - .unwrap() - .lease - .unwrap() - .expires_at_unix_ms; - let heartbeat = coordinator - .start_lease_heartbeat(run.clone(), epoch, CancellationToken::new()) - .unwrap(); - tokio::time::timeout(std::time::Duration::from_secs(2), async { - loop { - let expiry = coordinator - .control - .get_run(&run) - .await - .unwrap() - .unwrap() - .lease - .unwrap() - .expires_at_unix_ms; - if expiry > initial_expiry { - break; - } - tokio::time::sleep(std::time::Duration::from_millis(20)).await; - } - }) - .await - .expect("heartbeat should durably renew the lease"); - assert!(matches!( - coordinator - .control - .acquire_lease( - &run, - Some("task-heartbeat"), - "competitor", - MIN_LEASE_TTL_MS, - ) - .await - .unwrap(), - LeaseAcquireOutcome::Held(held) if held.epoch == epoch - )); - drop(heartbeat); - } - - #[tokio::test] - async fn newer_epoch_fences_a_staged_old_result() { - let dir = tempfile::tempdir().unwrap(); - let coordinator = RunCoordinator::open( - dir.path().to_string_lossy(), - dir.path().join("sink"), - 30_000, - ) - .await - .unwrap(); - let run = RunId::new("run-2"); - let old = coordinator - .acquire_lease(&run, "task-2", "driver-a") - .await - .unwrap(); - coordinator - .stage_result(&terminal("task-2", "run-2", "attempt-old", old)) - .await - .unwrap(); - let new = coordinator - .control - .takeover_lease(&run, Some("task-2"), "driver-b", 30_000) - .await - .unwrap(); - let LeaseAcquireOutcome::Acquired(new) = new else { - panic!("takeover should acquire") - }; - assert!(new.epoch > old); - - let sink = JsonlFileSink::open(dir.path().join("sink")).await.unwrap(); - let report = coordinator - .reconcile(&sink, &ProcessLocalAttemptObserver) - .await - .unwrap(); - assert_eq!(report.fenced_results, 1); - assert!(report.retry_task_ids.contains("task-2")); - assert!( - coordinator - .control - .get_run(&run) - .await - .unwrap() - .unwrap() - .commit - .is_none() - ); - } - - #[tokio::test] - async fn committed_replay_is_idempotent_in_sink_and_cas() { - let dir = tempfile::tempdir().unwrap(); - let sink_root = dir.path().join("sink"); - let coordinator = RunCoordinator::open(dir.path().to_string_lossy(), &sink_root, 30_000) - .await - .unwrap(); - let run = RunId::new("run-3"); - let epoch = coordinator - .acquire_lease(&run, "task-3", "driver-a") - .await - .unwrap(); - let result = terminal("task-3", "run-3", "attempt-3", epoch); - let sink = JsonlFileSink::open(&sink_root).await.unwrap(); - coordinator.finalize_result(&sink, &result).await.unwrap(); - coordinator - .reconcile(&sink, &ProcessLocalAttemptObserver) - .await - .unwrap(); - let ready = tokio::fs::read_to_string(sink_root.join("ready.ndjson")) - .await - .unwrap(); - assert_eq!(ready.lines().count(), 1); - } -} diff --git a/crates/persisting-ppilot/src/digest.rs b/crates/persisting-ppilot/src/digest.rs deleted file mode 100644 index aa5e0859f..000000000 --- a/crates/persisting-ppilot/src/digest.rs +++ /dev/null @@ -1,24 +0,0 @@ -use sha2::{Digest, Sha256}; -use std::fmt::Write; - -pub(crate) fn sha256_hex(bytes: impl AsRef<[u8]>) -> String { - let digest = Sha256::digest(bytes.as_ref()); - let mut encoded = String::with_capacity(digest.len() * 2); - for byte in digest { - write!(&mut encoded, "{byte:02x}").expect("writing to a String cannot fail"); - } - encoded -} - -#[cfg(test)] -mod tests { - use super::sha256_hex; - - #[test] - fn encodes_sha256_as_lowercase_hex() { - assert_eq!( - sha256_hex(b"abc"), - "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" - ); - } -} diff --git a/crates/persisting-ppilot/src/dist.rs b/crates/persisting-ppilot/src/dist.rs deleted file mode 100644 index e2498d4f7..000000000 --- a/crates/persisting-ppilot/src/dist.rs +++ /dev/null @@ -1,310 +0,0 @@ -//! Read placement from **torchrun** (or compatible) environment. -//! -//! Semantic block: [`crate::blocks::ids::PLACEMENT`] (distributed naming + seed). -//! -//! We do **not** spawn processes ourselves. An embedding host may launch with: -//! -//! ```bash -//! torchrun --nproc_per_node=4 -- plan.py -//! ``` - -use anyhow::{Context, Result, bail}; -use std::collections::HashMap; -use std::env; -use std::net::SocketAddr; - -#[derive(Debug, Clone)] -pub struct DistEnv { - pub rank: usize, - pub world_size: usize, - pub master_addr: String, - /// Where rank0 binds Pulsing so peers can join (no custom rdzv protocol). - pub pulsing_seed: SocketAddr, -} - -impl DistEnv { - /// `Some` when launched under torchrun (`RANK` + `WORLD_SIZE` present). - pub fn from_env() -> Result> { - let map: HashMap = env::vars().collect(); - Self::from_map(&map) - } - - /// Testable entry: build from an explicit env map (no process-global mutation). - pub fn from_map(env: &HashMap) -> Result> { - let rank = match env.get("RANK") { - Some(v) => v.parse::().context("parse RANK")?, - None => return Ok(None), - }; - let world_size = env - .get("WORLD_SIZE") - .context("WORLD_SIZE required when RANK is set (use torchrun)")? - .parse::() - .context("parse WORLD_SIZE")?; - if world_size == 0 { - bail!("WORLD_SIZE must be >= 1"); - } - if rank >= world_size { - bail!("RANK {rank} >= WORLD_SIZE {world_size}"); - } - let master_addr = env - .get("MASTER_ADDR") - .cloned() - .unwrap_or_else(|| "127.0.0.1".into()); - let master_port: u16 = env - .get("MASTER_PORT") - .map(|s| s.as_str()) - .unwrap_or("29500") - .parse() - .context("parse MASTER_PORT")?; - let pulsing_port: u16 = env - .get("PERSISTING_PULSING_PORT") - .and_then(|v| v.parse().ok()) - .unwrap_or(master_port.saturating_add(17)); - let pulsing_seed = Self::pulsing_seed_addr(&master_addr, pulsing_port)?; - Ok(Some(Self { - rank, - world_size, - master_addr, - pulsing_seed, - })) - } - - pub fn pulsing_seed_addr(master_addr: &str, pulsing_port: u16) -> Result { - format!("{master_addr}:{pulsing_port}") - .parse() - .with_context(|| format!("parse Pulsing seed {master_addr}:{pulsing_port}")) - } - - pub fn is_driver(&self) -> bool { - self.rank == 0 - } - - /// Logical worker actor name when `per_worker == 1`. - pub fn worker_name(rank: usize) -> String { - format!("ppilot/worker/{rank}") - } - - /// One concurrent execute slot under a logical worker/rank. - /// - /// When `per_worker == 1`, uses the legacy name [`Self::worker_name`] (no `/slot/`). - pub fn slot_name(worker: usize, slot: usize, per_worker: usize) -> String { - if per_worker <= 1 { - Self::worker_name(worker) - } else { - format!("ppilot/worker/{worker}/slot/{slot}") - } - } - - /// Flat pool index in **slot-major** order (matches [`Self::slot_names`]). - /// - /// `index = slot * n_workers + worker` — used by Scheduler / DeathWatch. - pub fn slot_flat_index( - worker: usize, - slot: usize, - n_workers: usize, - per_worker: usize, - ) -> usize { - let per_worker = per_worker.max(1); - let n_workers = n_workers.max(1); - debug_assert!( - worker < n_workers, - "worker {worker} >= n_workers {n_workers}" - ); - debug_assert!(slot < per_worker, "slot {slot} >= per_worker {per_worker}"); - slot.saturating_mul(n_workers).saturating_add(worker) - } - - /// Flat pool names in **slot-major** order: all workers' slot0, then slot1, … - pub fn slot_names(n_workers: usize, per_worker: usize) -> Vec { - let per_worker = per_worker.max(1); - let n_workers = n_workers.max(1); - let mut names = Vec::with_capacity(n_workers.saturating_mul(per_worker)); - for slot in 0..per_worker { - for worker in 0..n_workers { - names.push(Self::slot_name(worker, slot, per_worker)); - } - } - names - } - - pub fn worker_names(world_size: usize) -> Vec { - Self::slot_names(world_size, 1) - } - - /// Side-channel cancel actor for rank (separate mailbox from WorkerActor). - pub fn job_control_name(rank: usize) -> String { - format!("ppilot/job_control/{rank}") - } - - /// Node-local pChronicle worker used by federated analysis. - pub fn analysis_worker_name(rank: usize) -> String { - format!("ppilot/analysis/{rank}") - } -} - -#[cfg(test)] -mod tests { - use super::*; - use proptest::prelude::*; - - fn map(pairs: &[(&str, &str)]) -> HashMap { - pairs - .iter() - .map(|(k, v)| ((*k).to_string(), (*v).to_string())) - .collect() - } - - #[test] - fn from_map_none_without_rank() { - assert!(DistEnv::from_map(&HashMap::new()).unwrap().is_none()); - } - - #[test] - fn from_map_parses_torchrun_defaults() { - let env = map(&[ - ("RANK", "1"), - ("WORLD_SIZE", "4"), - ("MASTER_ADDR", "10.0.0.1"), - ]); - let d = DistEnv::from_map(&env).unwrap().unwrap(); - assert_eq!(d.rank, 1); - assert_eq!(d.world_size, 4); - assert!(!d.is_driver()); - assert_eq!(d.pulsing_seed.port(), 29500 + 17); - assert_eq!(d.master_addr, "10.0.0.1"); - } - - #[test] - fn from_map_rejects_bad_rank() { - let env = map(&[("RANK", "4"), ("WORLD_SIZE", "4")]); - assert!(DistEnv::from_map(&env).is_err()); - } - - #[test] - fn pulsing_port_override() { - let env = map(&[ - ("RANK", "0"), - ("WORLD_SIZE", "2"), - ("MASTER_PORT", "30000"), - ("PERSISTING_PULSING_PORT", "31000"), - ]); - let d = DistEnv::from_map(&env).unwrap().unwrap(); - assert!(d.is_driver()); - assert_eq!(d.pulsing_seed.port(), 31000); - } - - #[test] - fn slot_names_slot_major() { - assert_eq!( - DistEnv::slot_names(2, 2), - vec![ - "ppilot/worker/0/slot/0", - "ppilot/worker/1/slot/0", - "ppilot/worker/0/slot/1", - "ppilot/worker/1/slot/1", - ] - ); - assert_eq!( - DistEnv::slot_names(2, 1), - vec!["ppilot/worker/0", "ppilot/worker/1"] - ); - } - - #[test] - fn slot_flat_index_matches_slot_names_order() { - let names = DistEnv::slot_names(3, 2); - for worker in 0..3 { - for slot in 0..2 { - let i = DistEnv::slot_flat_index(worker, slot, 3, 2); - assert_eq!(names[i], DistEnv::slot_name(worker, slot, 2)); - } - } - // Regression: rank0's second slot is NOT flat index 1 when world>1. - assert_eq!(DistEnv::slot_flat_index(0, 1, 2, 2), 2); - assert_eq!(DistEnv::slot_flat_index(1, 0, 2, 2), 1); - } - - #[test] - fn analysis_worker_names_are_rank_stable() { - assert_eq!(DistEnv::analysis_worker_name(3), "ppilot/analysis/3"); - } - - fn placement_strategy() -> impl Strategy { - (1usize..32, 1usize..16).prop_flat_map(|(n_workers, per_worker)| { - ( - Just(n_workers), - Just(per_worker), - 0..n_workers, - 0..per_worker, - ) - }) - } - - proptest! { - #[test] - fn flat_index_selects_the_corresponding_slot_name( - (n_workers, per_worker, worker, slot) in placement_strategy(), - ) { - let names = DistEnv::slot_names(n_workers, per_worker); - let index = DistEnv::slot_flat_index(worker, slot, n_workers, per_worker); - prop_assert_eq!(&names[index], &DistEnv::slot_name(worker, slot, per_worker)); - } - - #[test] - fn slot_names_form_a_unique_complete_slot_major_grid( - n_workers in 1usize..32, - per_worker in 1usize..16, - ) { - let names = DistEnv::slot_names(n_workers, per_worker); - prop_assert_eq!(names.len(), n_workers * per_worker); - let unique = names.iter().collect::>(); - prop_assert_eq!(unique.len(), names.len()); - - for (index, name) in names.iter().enumerate() { - let slot = index / n_workers; - let worker = index % n_workers; - prop_assert_eq!(name, &DistEnv::slot_name(worker, slot, per_worker)); - } - } - - #[test] - fn valid_torchrun_maps_preserve_rank_and_port_defaults( - world_size in 1usize..128, - rank_seed in any::(), - master_port in 1u16..=u16::MAX - 17, - ) { - let rank = rank_seed % world_size; - let env = map(&[ - ("RANK", &rank.to_string()), - ("WORLD_SIZE", &world_size.to_string()), - ("MASTER_PORT", &master_port.to_string()), - ]); - let dist = DistEnv::from_map(&env).unwrap().unwrap(); - prop_assert_eq!(dist.rank, rank); - prop_assert_eq!(dist.world_size, world_size); - prop_assert_eq!(dist.is_driver(), rank == 0); - prop_assert_eq!(dist.pulsing_seed.port(), master_port + 17); - } - - #[test] - fn rank_outside_world_is_rejected( - world_size in 1usize..128, - excess in 0usize..128, - ) { - let rank = world_size + excess; - let env = map(&[ - ("RANK", &rank.to_string()), - ("WORLD_SIZE", &world_size.to_string()), - ]); - prop_assert!(DistEnv::from_map(&env).is_err()); - } - - #[test] - fn worker_names_are_the_single_slot_projection(world_size in 0usize..64) { - prop_assert_eq!( - DistEnv::worker_names(world_size), - DistEnv::slot_names(world_size, 1), - ); - } - } -} diff --git a/crates/persisting-ppilot/src/driver.rs b/crates/persisting-ppilot/src/driver.rs deleted file mode 100644 index ed29b179d..000000000 --- a/crates/persisting-ppilot/src/driver.rs +++ /dev/null @@ -1,446 +0,0 @@ -//! Driver — control plane that drives the worker fleet. -//! -//! Conceptual ownership (one Driver per job on rank0 / local control process): -//! 1. **Plan** — stream tasks from the user `plan()` script -//! 2. **Dispatch** — least-loaded placement; `ask` workers **directly** (parallel) -//! 3. **Drain** — complete → `on_result` immediately; at most `max_inflight` JoinHandles -//! -//! This is a Rust control object, not a Pulsing Actor that `await`s Execute in -//! `receive` (that would serialize the fleet). Workers remain Pulsing actors. - -use crate::checkpoint::CheckpointTracker; -use crate::coordination::RunCoordinator; -use crate::executor::task_run_spec; -use crate::future::RunFuture; -use crate::observe::Observer; -use crate::plan::stream_plan_tasks; -use crate::pulsing_ext::{ASK_TIMEOUT, ask_timeout}; -use crate::scheduler::{AcquireError, Scheduler, StickyLost, WorkerPool}; -use crate::sink_writer::SinkSubmitter; -use crate::skip::SkipSet; -use crate::task::{ErrorKind, TaskExpr, TaskResult, unix_now}; -use crate::worker::{WorkerCommand, WorkerReply}; -use anyhow::Result; -use futures::StreamExt; -use futures::stream::FuturesUnordered; -use std::path::PathBuf; -use std::sync::Arc; -use tokio_util::sync::CancellationToken; - -/// Job knobs owned/consumed by the Driver when running a plan. -#[derive(Clone)] -pub struct RunOptions { - pub script: PathBuf, - pub python: PathBuf, - /// Standalone pVisor executable used for every Run. - pub pvisor_binary: PathBuf, - /// Local-only worker count when not under torchrun. - pub workers: usize, - /// Global cap on concurrent tasks (defaults to `workers * per_worker_inflight`). - /// Also bounds outstanding JoinHandles (complete → drop). - pub max_inflight: usize, - /// Max concurrent Executes per worker (least-loaded scheduling). Default 1. - pub per_worker_inflight: usize, - /// Extra entries prepended onto PYTHONPATH for plan / execute host. - pub pythonpath_extra: Vec, - /// Forwarded to task.py as ``sys.argv[1:]`` (argparse-friendly). - pub script_args: Vec, - /// pPilot infrastructure retries on worker ask failure (default 2). - pub infra_retries: u32, - /// Job-level cancel (Ctrl-C / external). Child tokens per in-flight task. - pub job_cancel: CancellationToken, - /// Optional observability sink (`--observe`). - pub observer: Arc, - /// Task ids to skip / already claimed (`--resume` seed + live completions). - pub skip_task_ids: SkipSet, - /// Optional checkpoint progress (`checkpoint.json` under `--sink`). - pub checkpoint: Option>, - /// Optional async sink enqueue (awaited on completion — back-pressures drain). - pub sink_submitter: Option, - /// Durable lease/commit control. When present every accepted task receives - /// one fencing epoch before it can contact a worker. - pub coordinator: Option>, -} - -/// Drives one pPilot job: emit plan tasks and dispatch them onto the fleet. -pub struct Driver { - pool: WorkerPool, - sched: Arc, -} - -impl Driver { - pub fn new(pool: WorkerPool, sched: Arc) -> Self { - Self { pool, sched } - } - - pub fn scheduler(&self) -> &Arc { - &self.sched - } - - pub fn pool(&self) -> &WorkerPool { - &self.pool - } - - /// Run user `plan()` → place each task on a worker → collect results. - /// - /// Completions call `on_result` in **finish order** (not plan emit order). - /// At most `max_inflight` [`RunFuture`]s are outstanding at once. - pub async fn run_plan( - &self, - opts: &RunOptions, - mut on_result: impl FnMut(TaskResult) + Send, - ) -> Result> { - if opts.coordinator.is_some() && opts.sink_submitter.is_none() { - anyhow::bail!("RunCoordinator requires a coordinated sink writer"); - } - let global_cap = opts.max_inflight.max(1).min(self.sched.capacity().max(1)); - let mut stream = stream_plan_tasks( - opts.script.clone(), - opts.python.clone(), - opts.script_args.clone(), - ); - // Each element is a RunFuture::wait(); len() bounds outstanding JoinHandles. - let mut inflight: FuturesUnordered< - std::pin::Pin> + Send>>, - > = FuturesUnordered::new(); - let mut out = Vec::new(); - let mut plan_done = false; - let retries = opts.infra_retries; - let observer = Arc::clone(&opts.observer); - let coordinator = opts.coordinator.clone(); - - loop { - // Guard before select: empty set + plan_done would disable every arm. - if plan_done && inflight.is_empty() { - break; - } - - tokio::select! { - biased; - - // Prefer draining completions so sink / resume stay current. - Some(joined) = inflight.next(), if !inflight.is_empty() => { - let r: TaskResult = joined?; - on_result(r.clone()); - if let Some(sink) = &opts.sink_submitter { - sink.submit(r.clone()) - .await - .map_err(|e| anyhow::anyhow!("sink enqueue: {e}"))?; - } - out.push(r); - } - - item = stream.next(), if !plan_done - && inflight.len() < global_cap - && !opts.job_cancel.is_cancelled() => - { - match item { - None => plan_done = true, - Some(Err(e)) => return Err(e), - Some(Ok(task)) => { - let task_id = task.id.clone(); - // Claim before dispatch: resume seed, live terminals, and - // duplicate plan() yields share one skip set (cross-worker - // re-dispatch of a finished id is skipped). - if !opts.skip_task_ids.insert(task_id.clone()) { - if let Some(ckpt) = &opts.checkpoint { - ckpt.note_skipped(1); - let _ = ckpt.maybe_flush().await; - } - continue; - } - observer.task_queued(&task_id).await; - if let Some(ckpt) = &opts.checkpoint { - ckpt.note_dispatched(); - } - let pool = Arc::clone(&self.pool); - let sched = Arc::clone(&self.sched); - let observer = Arc::clone(&observer); - let coordinator = coordinator.clone(); - let cancel = opts.job_cancel.child_token(); - let cancel_watch = cancel.clone(); - let join = tokio::spawn(async move { - execute_with_placement( - pool, - sched, - observer, - task, - retries, - cancel_watch, - coordinator, - ) - .await - }); - inflight.push(Box::pin(RunFuture::new(task_id, join, cancel).wait())); - } - } - } - - _ = opts.job_cancel.cancelled(), if !plan_done => { - tracing::debug!("job cancel: stop accepting new plan tasks"); - plan_done = true; - } - } - } - - Ok(out) - } -} - -async fn execute_with_placement( - pool: WorkerPool, - sched: Arc, - observer: Arc, - task: TaskExpr, - infra_retries: u32, - cancel: CancellationToken, - coordinator: Option>, -) -> Result { - let task_id = task.id.clone(); - let started = unix_now(); - let run_id = task_run_spec(&task, "unassigned", 0).run_id; - let lease_epoch = match &coordinator { - Some(coordinator) => { - coordinator - .acquire_lease(&run_id, &task_id, coordinator.owner_id()) - .await? - } - None => 1, - }; - let heartbeat = coordinator - .as_ref() - .map(|coordinator| { - coordinator.start_lease_heartbeat(run_id.clone(), lease_epoch, cancel.clone()) - }) - .transpose()?; - let outcome = async { - let task_json = serde_json::to_vec(&task).map_err(|e| anyhow::anyhow!("encode task: {e}"))?; - let mut last_err = None; - // After first Execute contact: stick forever (result_cache is per-slot). - // Never fall through to another slot — that would be at-least-once re-execute. - let mut sticky: Option = None; - - for attempt in 0..=infra_retries { - if cancel.is_cancelled() { - observer - .task_finished(&task_id, false, true, None, &sched) - .await; - return Ok(stamp_control( - TaskResult::cancelled(task_id), - &run_id, - lease_epoch, - )); - } - let guard = tokio::select! { - biased; - _ = cancel.cancelled() => { - observer - .task_finished(&task_id, false, true, None, &sched) - .await; - return Ok(stamp_control( - TaskResult::cancelled(task_id), - &run_id, - lease_epoch, - )); - } - g = async { - match sticky { - Some(slot) => sched - .acquire_guard_sticky(slot) - .await - .map_err(PlacementErr::Sticky), - None => sched - .acquire_guard_prefer(None) - .await - .map_err(PlacementErr::AllGone), - } - } => g, - }; - let guard = match guard { - Ok(g) => g, - Err(PlacementErr::Sticky(StickyLost::Quarantined(slot))) => { - let err = format!( - "sticky slot {slot} quarantined after contact (refuse cross-slot re-execute)" - ); - tracing::warn!(%task_id, %err); - observer - .task_finished(&task_id, false, false, Some(err.clone()), &sched) - .await; - let mut r = TaskResult::failure_with_kind( - task_id, - format!("infra: {err}"), - None, - "infra", - started, - ErrorKind::Infra, - true, - ); - r.infra_retries = attempt; - return Ok(stamp_control(r, &run_id, lease_epoch)); - } - Err(PlacementErr::AllGone(AcquireError::AllQuarantined)) => { - let err = "all worker slots quarantined".to_string(); - tracing::error!(%task_id, %err); - observer - .task_finished(&task_id, false, false, Some(err.clone()), &sched) - .await; - let mut r = TaskResult::failure_with_kind( - task_id, - format!("infra: {err}"), - None, - "infra", - started, - ErrorKind::Infra, - true, - ); - r.infra_retries = attempt; - return Ok(stamp_control(r, &run_id, lease_epoch)); - } - }; - let idx = guard.index(); - let worker_id = format!("w{idx}"); - observer - .task_assigned(&task_id, idx, &worker_id, attempt, &sched) - .await; - let worker = { - let g = pool - .read() - .map_err(|_| anyhow::anyhow!("worker pool lock"))?; - g.get(idx) - .cloned() - .ok_or_else(|| anyhow::anyhow!("worker index {idx} out of range"))? - }; - - observer.task_running(&task_id).await; - // Contacted this slot: subsequent infra retries must stay here. - sticky = Some(idx); - let ask = ask_timeout::<_, WorkerReply>( - &worker, - WorkerCommand::Execute { - task_json: task_json.clone(), - lease_epoch, - }, - ASK_TIMEOUT, - ); - tokio::pin!(ask); - let reply = tokio::select! { - biased; - _ = cancel.cancelled() => { - // Local workers share job_cancel; remote ranks get Cancel via JobControlActor. - drop(guard); - observer - .task_finished(&task_id, false, true, None, &sched) - .await; - return Ok(stamp_control( - TaskResult::cancelled(task_id), - &run_id, - lease_epoch, - )); - } - r = &mut ask => r, - }; - match reply { - Ok(WorkerReply::Result { result_json }) => { - let mut r: TaskResult = serde_json::from_slice(&result_json) - .map_err(|e| anyhow::anyhow!("decode result: {e}"))?; - if attempt > 0 { - r.infra_retries = attempt; - tracing::debug!(%task_id, attempt, worker = idx, "infra retry succeeded"); - } - if r.worker.is_none() { - r.worker = Some(format!("w{idx}")); - } - if r.run_id.as_deref() != Some(run_id.as_str()) - || r.lease_epoch != lease_epoch - || r.attempt_id.is_none() - { - let detail = format!( - "worker returned invalid Run identity: run={:?}, attempt={:?}, epoch={} (expected run={}, epoch={}); worker error={:?}; traceback={:?}", - r.run_id, - r.attempt_id, - r.lease_epoch, - run_id, - lease_epoch, - r.error, - r.traceback, - ); - r = TaskResult::failure_with_kind( - task_id.clone(), - detail, - None, - &worker_id, - started, - ErrorKind::Infra, - true, - ); - r = stamp_control(r, &run_id, lease_epoch); - } - sched.note_success(idx); - observer - .task_finished(&task_id, r.ok, r.cancelled, r.error.clone(), &sched) - .await; - drop(guard); - return Ok(r); - } - Ok(WorkerReply::Bye) => { - last_err = Some("unexpected Bye on Execute".into()); - sched.note_failure(idx); - drop(guard); - } - Err(e) => { - tracing::warn!( - %task_id, - attempt, - worker = idx, - error = %e, - "infra retry: worker ask failed (sticky-only re-ask)" - ); - last_err = Some(e.to_string()); - sched.note_failure(idx); - drop(guard); - } - } - } - - let err = last_err.unwrap_or_else(|| "unknown".into()); - observer - .task_finished(&task_id, false, false, Some(err.clone()), &sched) - .await; - let mut r = TaskResult::failure_with_kind( - task_id, - format!("infra retries exhausted: {err}"), - None, - "infra", - started, - ErrorKind::Infra, - true, - ); - r.infra_retries = infra_retries; - Ok(stamp_control(r, &run_id, lease_epoch)) - } - .await; - if outcome.is_ok() - && let Some(heartbeat) = heartbeat - { - heartbeat.detach(); - } - outcome -} - -fn stamp_control( - mut result: TaskResult, - run_id: &persisting_agentctl::RunId, - epoch: u64, -) -> TaskResult { - result.run_id = Some(run_id.as_str().to_string()); - result.lease_epoch = epoch; - if result.attempt_id.is_none() { - result.attempt_id = Some(format!("ppilot-control-{}-{epoch}", result.task_id)); - } - result -} - -enum PlacementErr { - Sticky(StickyLost), - AllGone(AcquireError), -} diff --git a/crates/persisting-ppilot/src/executor.rs b/crates/persisting-ppilot/src/executor.rs deleted file mode 100644 index 76a039cb5..000000000 --- a/crates/persisting-ppilot/src/executor.rs +++ /dev/null @@ -1,871 +0,0 @@ -//! External pVisor process provider used by pPilot workers while the Driver schedules TaskExpr. -//! -//! Every TaskExpr is adapted to one stable RunSpec. The long-lived Python host -//! is reached through a small loopback client process, so execution, -//! cancellation and terminal state pass through the standalone pVisor binary -//! without paying one Python module import per task. -//! -//! **Primitive:** [`Executor`] trait · [`ExecutorRouter`] (product: `op=execute` only). -//! -//! ```text -//! Driver --ask--> WorkerActor -- RunSpec --> pVisor --> plan.py::execute(item) -//! ``` - -use crate::python_env; -use crate::task::{ErrorKind, TaskExpr, TaskResult, unix_now}; -use anyhow::{Context, Result, bail}; -#[cfg(test)] -use persisting_agentctl::{ArtifactRef, ProcessOutput}; -use persisting_agentctl::{ - PVisorProcessClient, PVisorProcessOptions, RunFailure, RunFailureKind, RunInvocation, - RunResult, RunSpec, RunState, StdioMode, -}; -use serde::{Deserialize, Serialize}; -use serde_json::{Value, json}; -#[cfg(test)] -use std::collections::BTreeMap; -use std::path::{Path, PathBuf}; -use std::process::Stdio; -use std::sync::Arc; -use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; -use tokio::net::{TcpListener, TcpStream}; -use tokio::process::{Child, ChildStdin, ChildStdout, Command}; -use tokio::sync::Mutex; -use tokio::task::JoinHandle; -use tokio_util::sync::CancellationToken; - -/// Long-lived host: load plan module once, call `execute(item)`. -const PLAN_EXECUTE_HOST: &str = r#" -import importlib.util, json, sys, traceback, types -from pathlib import Path - -plan_mods = {} - -def install_context(raw): - # A tiny injected module keeps execute(item) stateless while making - # placement data available to algorithm code. - mod = types.ModuleType("persisting_ppilot") - frozen = json.loads(json.dumps(raw or {})) - mod.context = lambda: json.loads(json.dumps(frozen)) - sys.modules["persisting_ppilot"] = mod - return frozen - -def load_plan_module(script, argv=None, context=None): - script = str(Path(script).resolve()) - if script in plan_mods: - return plan_mods[script] - path = Path(script) - worker_context = install_context(context) - # Match `python task.py --foo bar` so argparse works at import time. - sys.argv = [script, *(argv or [])] - spec = importlib.util.spec_from_file_location("user_plan_exec", path) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - setup = getattr(mod, "setup_worker", None) - if setup is not None: - if not callable(setup): - raise TypeError("setup_worker must be callable") - setup(worker_context) - plan_mods[script] = mod - return mod - -def handle(msg): - cmd = msg.get("cmd") - if cmd == "shutdown": - for mod in plan_mods.values(): - teardown = getattr(mod, "teardown_worker", None) - if teardown is not None: - if not callable(teardown): - raise TypeError("teardown_worker must be callable") - teardown() - return {"ok": True, "value": "bye"} - if cmd == "run_plan": - script = msg.get("script") - if not script: - raise ValueError("run_plan requires script= path to plan.py") - mod = load_plan_module(script, msg.get("argv") or [], msg.get("context") or {}) - if not hasattr(mod, "execute"): - raise AttributeError( - f"{script} must define execute(item) — same object plan() yields" - ) - fn = getattr(mod, "execute") - if not callable(fn): - raise TypeError("execute must be callable") - # Pass the same shape plan() yields: {id, ...fields}, not wire TaskExpr. - task = msg.get("task") or {} - item = dict(task.get("args") or {}) - if task.get("id") is not None: - item["id"] = task["id"] - return {"ok": True, "value": fn(item)} - raise ValueError(f"unknown cmd: {cmd!r}") - -def main(): - for line in sys.stdin: - line = line.strip() - if not line: - continue - req_id = None - try: - msg = json.loads(line) - req_id = msg.get("id") - out = handle(msg) - out["id"] = req_id - print(json.dumps(out, default=str), flush=True) - if msg.get("cmd") == "shutdown": - break - except Exception as e: - print(json.dumps({ - "id": req_id, - "ok": False, - "error": str(e), - "traceback": traceback.format_exc(), - }), flush=True) - -if __name__ == "__main__": - main() -"#; - -/// Short-lived workload process launched by the standalone pVisor. It only -/// relays one task to the worker-local Python host and emits one TaskResult. -const PLAN_HOST_CLIENT: &str = r#" -import json, socket, sys - -endpoint, token, task_file, worker_id = sys.argv[1:5] -host, port = endpoint.rsplit(":", 1) -with open(task_file, "r", encoding="utf-8") as f: - task = json.load(f) -with socket.create_connection((host, int(port)), timeout=5) as sock: - stream = sock.makefile("rwb", buffering=0) - request = json.dumps({"token": token, "task": task, "worker_id": worker_id}) - stream.write(request.encode("utf-8") + b"\n") - reply = stream.readline() - if not reply: - raise RuntimeError("pPilot plan host closed without a result") -result = json.loads(reply)["result"] -print(json.dumps(result, separators=(",", ":")), flush=True) -if result.get("cancelled"): - raise SystemExit(130) -if not result.get("ok"): - raise SystemExit(1) -"#; - -#[derive(Debug, Deserialize)] -struct PlanHostRequest { - token: String, - task: TaskExpr, - worker_id: String, -} - -#[derive(Debug, Serialize)] -struct PlanHostResponse { - result: TaskResult, -} - -struct PlanHost { - child: Child, - stdin: ChildStdin, - stdout: BufReader, - next_id: u64, -} - -/// Owns one long-lived `--python` process that caches the loaded plan module. -struct PlanHostExecutor { - python: PathBuf, - pythonpath_extra: Vec, - worker_context: Value, - host: Mutex>, -} - -impl PlanHostExecutor { - fn new(python: PathBuf, pythonpath_extra: Vec, worker_context: Value) -> Self { - Self { - python, - pythonpath_extra, - worker_context, - host: Mutex::new(None), - } - } - - async fn ensure_host( - host: &mut Option, - python: &PathBuf, - pythonpath_extra: &[PathBuf], - ) -> Result<()> { - if host.is_some() { - return Ok(()); - } - let mut cmd = Command::new(python); - cmd.arg("-u") - .arg("-c") - .arg(PLAN_EXECUTE_HOST) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::inherit()); - python_env::apply_pythonpath(&mut cmd, pythonpath_extra); - let mut child = cmd - .spawn() - .with_context(|| format!("spawn plan execute host: {}", python.display()))?; - let stdin = child - .stdin - .take() - .ok_or_else(|| anyhow::anyhow!("plan host missing stdin"))?; - let stdout = child - .stdout - .take() - .ok_or_else(|| anyhow::anyhow!("plan host missing stdout"))?; - *host = Some(PlanHost { - child, - stdin, - stdout: BufReader::new(stdout), - next_id: 1, - }); - Ok(()) - } - - async fn request(host: &mut PlanHost, mut msg: Value) -> Result { - let id = host.next_id; - host.next_id += 1; - msg.as_object_mut() - .ok_or_else(|| anyhow::anyhow!("request must be object"))? - .insert("id".into(), json!(id)); - let line = serde_json::to_string(&msg)?; - host.stdin.write_all(line.as_bytes()).await?; - host.stdin.write_all(b"\n").await?; - host.stdin.flush().await?; - - let mut reply = String::new(); - let n = host.stdout.read_line(&mut reply).await?; - if n == 0 { - bail!("plan execute host closed stdout"); - } - let parsed: Value = serde_json::from_str(reply.trim()) - .with_context(|| format!("invalid host reply: {}", reply.trim()))?; - if parsed.get("ok").and_then(|v| v.as_bool()) == Some(true) { - Ok(parsed.get("value").cloned().unwrap_or(Value::Null)) - } else { - let err = parsed - .get("error") - .and_then(|v| v.as_str()) - .unwrap_or("plan execute failed"); - let tb = parsed - .get("traceback") - .and_then(|v| v.as_str()) - .unwrap_or(""); - bail!("{err}\n{tb}") - } - } - - async fn shutdown(&self) { - let mut guard = self.host.lock().await; - if let Some(mut h) = guard.take() { - let _ = Self::request(&mut h, json!({"cmd": "shutdown"})).await; - let _ = h.child.kill().await; - } - } - - async fn run_plan_execute( - &self, - plan_script: &Path, - script_args: &[String], - task: TaskExpr, - worker_id: &str, - cancel: CancellationToken, - ) -> TaskResult { - let started = unix_now(); - if cancel.is_cancelled() { - return TaskResult::cancelled(task.id); - } - let task_json = match serde_json::to_value(&task) { - Ok(v) => v, - Err(e) => { - return TaskResult::failure( - task.id, - format!("encode task: {e}"), - None, - worker_id, - started, - ); - } - }; - let script = match plan_script.canonicalize() { - Ok(p) => p, - Err(_) => plan_script.to_path_buf(), - }; - let mut guard = self.host.lock().await; - if let Err(e) = Self::ensure_host(&mut guard, &self.python, &self.pythonpath_extra).await { - return TaskResult::failure( - task.id, - e.to_string(), - Some(format!("{e:#}")), - worker_id, - started, - ); - } - let msg = json!({ - "cmd": "run_plan", - "script": script, - "argv": script_args, - "task": task_json, - "context": self.worker_context, - }); - // In-flight cancel: kill the Python host so execute does not outlive Ctrl-C. - let result = { - let host = guard.as_mut().expect("host just ensured"); - tokio::select! { - biased; - _ = cancel.cancelled() => { - Err(anyhow::anyhow!("cancelled")) - } - r = Self::request(host, msg) => r, - } - }; - match result { - Ok(value) => TaskResult::success(task.id, value, worker_id, started), - Err(e) if cancel.is_cancelled() || e.to_string().contains("cancelled") => { - if let Some(mut h) = guard.take() { - let _ = h.child.kill().await; - } - TaskResult::cancelled(task.id) - } - Err(e) => { - let tb = format!("{e:#}"); - TaskResult::failure(task.id, e.to_string(), Some(tb), worker_id, started) - } - } - } -} - -struct PlanHostService { - endpoint: String, - token: String, - task_dir: tempfile::TempDir, - stop: CancellationToken, - join: JoinHandle<()>, -} - -impl PlanHostService { - fn start( - host: Arc, - plan_script: PathBuf, - script_args: Vec, - ) -> Result { - let listener = - std::net::TcpListener::bind("127.0.0.1:0").context("bind pPilot plan-host relay")?; - listener.set_nonblocking(true)?; - let endpoint = listener.local_addr()?.to_string(); - let listener = TcpListener::from_std(listener)?; - let token = uuid::Uuid::new_v4().simple().to_string(); - let task_dir = tempfile::Builder::new() - .prefix("persisting-ppilot-worker-") - .tempdir()?; - let stop = CancellationToken::new(); - let task_stop = stop.clone(); - let expected_token = token.clone(); - let join = tokio::spawn(async move { - loop { - let accepted = tokio::select! { - _ = task_stop.cancelled() => break, - accepted = listener.accept() => accepted, - }; - let Ok((stream, _)) = accepted else { continue }; - let connection_host = Arc::clone(&host); - let connection_script = plan_script.clone(); - let connection_args = script_args.clone(); - let connection_token = expected_token.clone(); - tokio::spawn(async move { - if let Err(error) = handle_plan_host_connection( - stream, - connection_host, - connection_script, - connection_args, - connection_token, - ) - .await - { - tracing::debug!(%error, "pPilot plan-host relay ended"); - } - }); - } - }); - Ok(Self { - endpoint, - token, - task_dir, - stop, - join, - }) - } - - async fn write_task(&self, task: &TaskExpr) -> Result { - let path = self - .task_dir - .path() - .join(format!("task-{}.json", uuid::Uuid::new_v4().simple())); - tokio::fs::write(&path, serde_json::to_vec(task)?) - .await - .with_context(|| format!("write pPilot task relay file {}", path.display()))?; - Ok(path) - } -} - -async fn handle_plan_host_connection( - stream: TcpStream, - host: Arc, - plan_script: PathBuf, - script_args: Vec, - expected_token: String, -) -> Result<()> { - let (read, mut write) = stream.into_split(); - let mut lines = BufReader::new(read).lines(); - let line = lines - .next_line() - .await? - .context("plan-host client closed before request")?; - let request: PlanHostRequest = serde_json::from_str(&line)?; - if request.token != expected_token { - bail!("plan-host authentication failed"); - } - let cancellation = CancellationToken::new(); - let execution = host.run_plan_execute( - &plan_script, - &script_args, - request.task, - &request.worker_id, - cancellation.clone(), - ); - tokio::pin!(execution); - let result = tokio::select! { - result = &mut execution => result, - disconnected = lines.next_line() => { - cancellation.cancel(); - let _ = disconnected; - execution.await - } - }; - write - .write_all(&serde_json::to_vec(&PlanHostResponse { result })?) - .await?; - write.write_all(b"\n").await?; - write.shutdown().await?; - Ok(()) -} - -/// Routes `op=execute` through a standalone foreground pVisor process. -pub struct ExecutorRouter { - host: Arc, - service: PlanHostService, - pvisor: PVisorProcessClient, - python: PathBuf, - supervisor: Option, -} - -impl ExecutorRouter { - /// Worker stack for the product surface (plan + execute only). - pub fn local_stack( - pvisor_binary: PathBuf, - python: PathBuf, - pythonpath_extra: Vec, - plan_script: PathBuf, - script_args: Vec, - worker_context: Value, - supervisor: Option, - ) -> Result { - let host = Arc::new(PlanHostExecutor::new( - python.clone(), - pythonpath_extra, - worker_context, - )); - let service = PlanHostService::start(Arc::clone(&host), plan_script, script_args)?; - Ok(Self { - host, - service, - pvisor: PVisorProcessClient::new(pvisor_binary), - python, - supervisor, - }) - } - - pub async fn run_with_cancel( - &self, - task: TaskExpr, - worker_id: &str, - cancel: CancellationToken, - lease_epoch: u64, - ) -> TaskResult { - if task.op != "execute" { - let started = unix_now(); - return TaskResult::failure( - task.id, - format!( - "unknown op {:?}: only op=execute (plan.py::execute) is supported", - task.op - ), - None, - worker_id, - started, - ); - } - let task_id = task.id.clone(); - let started = unix_now(); - let mut spec = task_run_spec(&task, worker_id, lease_epoch); - spec.supervisor = self.supervisor.clone(); - spec.input = match serde_json::to_value(&task) { - Ok(value) => value, - Err(error) => { - return TaskResult::failure( - task_id, - format!("encode task as RunSpec input: {error}"), - None, - worker_id, - started, - ); - } - }; - let task_path = match self.service.write_task(&task).await { - Ok(path) => path, - Err(error) => { - return TaskResult::failure_with_kind( - task_id, - format!("prepare pVisor task input failed: {error}"), - Some(format!("{error:#}")), - worker_id, - started, - ErrorKind::Infra, - true, - ); - } - }; - let RunInvocation::Process(process) = &mut spec.invocation; - process.program = self.python.display().to_string(); - process.args = vec![ - "-u".into(), - "-c".into(), - PLAN_HOST_CLIENT.into(), - self.service.endpoint.clone(), - self.service.token.clone(), - task_path.display().to_string(), - worker_id.into(), - ]; - process.stdout = StdioMode::Capture; - process.stderr = StdioMode::Capture; - let options = PVisorProcessOptions { - run_home: Some(self.service.task_dir.path().join("runs")), - run_args: Vec::new(), - }; - let result = self.pvisor.run(&spec, &options, cancel).await; - let _ = tokio::fs::remove_file(&task_path).await; - match result { - Ok(result) => run_result_to_task_result(result, &task_id, worker_id, started), - Err(error) => TaskResult::failure_with_kind( - task_id, - format!("pVisor Run wait failed: {error}"), - Some(format!("{error:#}")), - worker_id, - started, - ErrorKind::Infra, - true, - ), - } - } - - pub async fn shutdown(&self) { - self.service.stop.cancel(); - self.service.join.abort(); - self.host.shutdown().await; - } -} - -pub(crate) fn task_run_spec(task: &TaskExpr, worker_id: &str, lease_epoch: u64) -> RunSpec { - let job_id = std::env::var("PERSISTING_PPILOT_JOB_ID").unwrap_or_else(|_| "local".into()); - let run_id = format!( - "{}{}", - job_run_id_prefix(&job_id), - encode_run_id_part(&task.id) - ); - let mut spec = RunSpec::process(run_id, "ppilot", "ppilot-plan-host"); - spec.lease_epoch = lease_epoch; - spec.task_id = Some(task.id.clone()); - spec.parent_run_id = Some(persisting_agentctl::RunId::new(format!( - "ppilot-job-{}", - encode_run_id_part(&job_id) - ))); - spec.metadata - .insert("ppilot.worker_id".into(), Value::String(worker_id.into())); - spec.metadata - .insert("ppilot.job_id".into(), Value::String(job_id)); - spec -} - -pub(crate) fn job_run_id_prefix(job_id: &str) -> String { - format!("ppilot-{}-", encode_run_id_part(job_id)) -} - -fn encode_run_id_part(value: &str) -> String { - let mut encoded = String::with_capacity(value.len()); - for byte in value.bytes() { - match byte { - b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-' | b'_' | b'.' => { - encoded.push(char::from(byte)); - } - byte => encoded.push_str(&format!("~{byte:02x}")), - } - } - encoded -} - -#[cfg(test)] -pub(crate) fn task_result_to_run_result( - spec: RunSpec, - attempt_id: persisting_agentctl::AttemptId, - task: TaskResult, -) -> RunResult { - let output = ProcessOutput { - stderr: task.traceback.clone(), - ..ProcessOutput::default() - }; - let state = if task.ok { - RunState::Completed - } else if task.cancelled { - RunState::Cancelled - } else { - RunState::Failed - }; - let failure = (!task.ok && !task.cancelled).then(|| RunFailure { - kind: match task.error_kind { - Some(ErrorKind::Infra) => RunFailureKind::Infrastructure, - _ => RunFailureKind::Workload, - }, - message: task - .error - .clone() - .unwrap_or_else(|| "workload failed".into()), - retryable: task.retryable, - }); - let artifacts = task - .artifacts - .iter() - .map(|(name, value)| ArtifactRef { - name: name.clone(), - uri: value - .as_str() - .map(str::to_string) - .unwrap_or_else(|| value.to_string()), - media_type: None, - digest: None, - }) - .collect(); - RunResult { - run_id: spec.run_id, - attempt_id, - lease_epoch: spec.lease_epoch, - state, - started_at_unix_ms: seconds_to_millis(task.started_at), - finished_at_unix_ms: seconds_to_millis(task.finished_at), - exit_code: None, - failure, - output, - value: task.value, - metrics: task.metrics.into_iter().collect::>(), - artifacts, - event_stream_ref: None, - warnings: Vec::new(), - } -} - -pub(crate) fn run_result_to_task_result( - result: RunResult, - task_id: &str, - worker_id: &str, - fallback_started: f64, -) -> TaskResult { - let run_id = result.run_id.as_str().to_string(); - let attempt_id = result.attempt_id.as_str().to_string(); - let lease_epoch = result.lease_epoch; - if let Some(mut task) = result - .output - .stdout - .as_deref() - .and_then(|stdout| serde_json::from_str::(stdout.trim()).ok()) - { - task.run_id = Some(run_id); - task.attempt_id = Some(attempt_id); - task.lease_epoch = lease_epoch; - return task; - } - let started_at = if result.started_at_unix_ms == 0 { - fallback_started - } else { - result.started_at_unix_ms as f64 / 1000.0 - }; - let mut task = match result.state { - RunState::Completed => { - let mut task = TaskResult::success( - task_id, - result.value.unwrap_or(Value::Null), - worker_id, - started_at, - ); - task.finished_at = Some(result.finished_at_unix_ms as f64 / 1000.0); - task - } - RunState::Cancelled => TaskResult::cancelled(task_id), - _ => { - let failure = result.failure.unwrap_or(RunFailure { - kind: RunFailureKind::Infrastructure, - message: "pVisor Run failed without a failure record".into(), - retryable: true, - }); - TaskResult::failure_with_kind( - task_id, - failure.message, - result.output.stderr, - worker_id, - started_at, - match failure.kind { - RunFailureKind::Infrastructure | RunFailureKind::Spawn => ErrorKind::Infra, - _ => ErrorKind::Execute, - }, - failure.retryable, - ) - } - }; - task.run_id = Some(run_id); - task.attempt_id = Some(attempt_id); - task.lease_epoch = lease_epoch; - task -} - -#[cfg(test)] -fn seconds_to_millis(value: Option) -> u64 { - value.unwrap_or_else(unix_now).max(0.0).mul_add(1000.0, 0.0) as u64 -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::task::TaskExpr; - use serde_json::json; - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn run_with_cancel_kills_slow_execute() { - let dir = tempfile::tempdir().unwrap(); - let script = dir.path().join("slow.py"); - std::fs::write( - &script, - r#" -import time - -def plan(): - yield {"id": "t-0"} - -def execute(item): - time.sleep(5) - return {"done": True} -"#, - ) - .unwrap(); - let host = PlanHostExecutor::new(PathBuf::from("python3"), vec![], json!({})); - let cancel = CancellationToken::new(); - let bg = cancel.clone(); - tokio::spawn(async move { - tokio::time::sleep(std::time::Duration::from_millis(150)).await; - bg.cancel(); - }); - let task = TaskExpr::from_value(json!({"id": "t-0", "x": 1})).unwrap(); - let t0 = std::time::Instant::now(); - let r = host - .run_plan_execute(&script, &[], task, "w0", cancel) - .await; - assert!(r.cancelled, "{r:?}"); - assert!(t0.elapsed().as_secs_f64() < 2.0); - host.shutdown().await; - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn run_execute_returns_value() { - let dir = tempfile::tempdir().unwrap(); - let script = dir.path().join("ok.py"); - std::fs::write( - &script, - r#" -def plan(): - yield {"id": "t-0"} - -def execute(item): - return {"x2": item["x"] * 2} -"#, - ) - .unwrap(); - let host = PlanHostExecutor::new(PathBuf::from("python3"), vec![], json!({})); - let task = TaskExpr::from_value(json!({"id": "t-0", "x": 3})).unwrap(); - let r = host - .run_plan_execute(&script, &[], task, "w0", CancellationToken::new()) - .await; - assert!(r.ok); - assert_eq!(r.value, Some(json!({"x2": 6}))); - host.shutdown().await; - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn workload_failure_roundtrips_through_run_result() { - let dir = tempfile::tempdir().unwrap(); - let script = dir.path().join("fail.py"); - std::fs::write( - &script, - r#" -def execute(item): - raise ValueError("bad item") -"#, - ) - .unwrap(); - let host = PlanHostExecutor::new(PathBuf::from("python3"), vec![], json!({})); - let task = TaskExpr::from_value(json!({"id": "bad/task"})).unwrap(); - let result = host - .run_plan_execute(&script, &[], task, "w0", CancellationToken::new()) - .await; - assert!(!result.ok); - assert_eq!(result.error_kind, Some(ErrorKind::Execute)); - assert!(result.error.as_deref().unwrap().contains("bad item")); - assert!(result.traceback.as_deref().unwrap().contains("ValueError")); - host.shutdown().await; - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn worker_context_and_hooks_do_not_change_execute_signature() { - let dir = tempfile::tempdir().unwrap(); - let script = dir.path().join("context.py"); - std::fs::write( - &script, - r#" -from persisting_ppilot import context - -def setup_worker(ctx): - assert ctx["worker_id"] == "w-context" - -def plan(): - yield {"id": "unused"} - -def execute(item): - ctx = context() - return {"metrics": {"rank": ctx["rank"]}, "artifacts": {"worker": ctx["worker_id"]}} - -def teardown_worker(): - pass -"#, - ) - .unwrap(); - let host = PlanHostExecutor::new( - PathBuf::from("python3"), - vec![], - json!({"worker_id": "w-context", "rank": 3}), - ); - let task = TaskExpr::from_value(json!({"id": "t-0"})).unwrap(); - let result = host - .run_plan_execute(&script, &[], task, "w-context", CancellationToken::new()) - .await; - assert!(result.ok); - assert_eq!(result.metrics.get("rank"), Some(&3.0)); - assert_eq!(result.artifacts.get("worker"), Some(&json!("w-context"))); - host.shutdown().await; - } -} diff --git a/crates/persisting-ppilot/src/future.rs b/crates/persisting-ppilot/src/future.rs deleted file mode 100644 index 165511b30..000000000 --- a/crates/persisting-ppilot/src/future.rs +++ /dev/null @@ -1,106 +0,0 @@ -//! RunFuture — pPilot scheduling atom (TaskSpec → wait / cancel). -//! -//! Cancel is cooperative at dispatch / acquire, and in-flight Python is killed -//! via the shared job [`CancellationToken`] watched by the plan execute host. -//! `wait` always joins and **never rewrites a successful result** as cancelled. - -use crate::task::TaskResult; -use anyhow::{Context, Result}; -use tokio::task::JoinHandle; -use tokio_util::sync::CancellationToken; - -/// One submitted task under pPilot control. -pub struct RunFuture { - task_id: String, - join: JoinHandle>, - cancel: CancellationToken, -} - -impl RunFuture { - pub(crate) fn new( - task_id: String, - join: JoinHandle>, - cancel: CancellationToken, - ) -> Self { - Self { - task_id, - join, - cancel, - } - } - - pub fn task_id(&self) -> &str { - &self.task_id - } - - /// Best-effort cancel: signals placement + shared job token (host kill). - /// Under torchrun, Driver also broadcasts to each rank's job-control actor. - pub fn cancel(&self) { - self.cancel.cancel(); - } - - pub fn is_cancelled(&self) -> bool { - self.cancel.is_cancelled() - } - - /// Wait until the task finishes. Successful results are kept even if cancel raced. - pub async fn wait(self) -> Result { - let task_id = self.task_id.clone(); - let joined = self.join.await.context("run future join")?; - match joined { - Ok(r) => Ok(r), - Err(e) => Err(e).with_context(|| format!("task {task_id} failed")), - } - } -} - -/// Wait for many futures in **submission** order. -/// -/// Prefer [`crate::driver::Driver::run_plan`] for jobs: it drains in completion -/// order and bounds outstanding work. This helper is for callers that need a -/// stable result order matching the input vec. -pub async fn wait_all(futures: Vec) -> Result> { - let mut out = Vec::with_capacity(futures.len()); - for f in futures { - out.push(f.wait().await?); - } - Ok(out) -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - - #[tokio::test] - async fn wait_keeps_success_even_if_cancelled() { - let token = CancellationToken::new(); - token.cancel(); - let join = - tokio::spawn(async { Ok(TaskResult::success("t-1", json!({"x": 1}), "w0", 0.0)) }); - let fut = RunFuture::new("t-1".into(), join, token); - assert!(fut.is_cancelled()); - let r = fut.wait().await.unwrap(); - assert!(r.ok); - assert!(!r.cancelled); - assert_eq!(r.task_id, "t-1"); - } - - #[tokio::test] - async fn wait_all_preserves_submission_order() { - let mk = |id: &str, delay_ms: u64| { - let id = id.to_string(); - let token = CancellationToken::new(); - let id_for_join = id.clone(); - let join = tokio::spawn(async move { - tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await; - Ok(TaskResult::success(id_for_join, json!(1), "w0", 0.0)) - }); - RunFuture::new(id, join, token) - }; - // Slow first, fast second — wait_all still returns submission order. - let out = wait_all(vec![mk("a", 40), mk("b", 1)]).await.unwrap(); - assert_eq!(out[0].task_id, "a"); - assert_eq!(out[1].task_id, "b"); - } -} diff --git a/crates/persisting-ppilot/src/job_control.rs b/crates/persisting-ppilot/src/job_control.rs deleted file mode 100644 index db5dfeef7..000000000 --- a/crates/persisting-ppilot/src/job_control.rs +++ /dev/null @@ -1,374 +0,0 @@ -//! Side-channel job cancel + local DeathWatch for Pulsing fleets. -//! -//! WorkerActor mailboxes are serial: an in-flight `Execute` holds `receive`, so a -//! `Cancel` sitting behind it cannot stop Python. This actor shares the process -//! [`CancellationToken`] on a **separate** mailbox. -//! -//! Pulsing `watch` only supports **local** targets; we register local slot -//! ActorIds here and quarantine them in [`Scheduler`] on termination. - -use crate::dist::DistEnv; -use crate::pulsing_ext::{ASK_TIMEOUT, ask_timeout, resolve_actor}; -use crate::scheduler::Scheduler; -use futures::future::join_all; -use pulsing_actor::actor::{ActorId, StopReason}; -use pulsing_actor::prelude::*; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::sync::{Arc, Mutex}; -use std::time::Duration; -use tokio_util::sync::CancellationToken; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum JobControlCommand { - /// Cancel the shared job token (in-flight execute hosts select! + kill). - Cancel, - /// Register a local slot for DeathWatch → quarantine on stop. - /// `slot` is the **flat pool index** (slot-major), not a per-rank ordinal. - WatchSlot { slot: usize, actor_id: ActorId }, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum JobControlReply { - Ack { already: bool }, - Watched, -} - -pub struct JobControlActor { - job_cancel: CancellationToken, - sched: Option>, - /// actor_id → slot index (local watches only). - watched: Mutex>, -} - -impl JobControlActor { - pub fn new(job_cancel: CancellationToken) -> Self { - Self { - job_cancel, - sched: None, - watched: Mutex::new(HashMap::new()), - } - } - - pub fn with_scheduler(job_cancel: CancellationToken, sched: Arc) -> Self { - Self { - job_cancel, - sched: Some(sched), - watched: Mutex::new(HashMap::new()), - } - } -} - -#[async_trait] -impl Actor for JobControlActor { - fn metadata(&self) -> HashMap { - HashMap::from([ - ("role".into(), "ppilot-job-control".into()), - ( - "cancelled".into(), - self.job_cancel.is_cancelled().to_string(), - ), - ]) - } - - async fn receive( - &mut self, - msg: Message, - ctx: &mut ActorContext, - ) -> pulsing_actor::error::Result { - // DeathWatch notification: (ActorId, StopReason) — parse borrows so we - // can still unpack JobControlCommand on the ask path. - if let Ok((dead_id, reason)) = msg.parse::<(ActorId, StopReason)>() { - let slot = self - .watched - .lock() - .ok() - .and_then(|g| g.get(&dead_id).copied()); - if let Some(slot) = slot { - tracing::warn!( - %dead_id, - slot, - %reason, - "DeathWatch: local worker terminated → quarantine" - ); - if let Some(sched) = &self.sched { - sched.force_quarantine(slot); - } - } - return Message::pack(&JobControlReply::Ack { already: true }); - } - - let cmd: JobControlCommand = msg.unpack()?; - let reply = match cmd { - JobControlCommand::Cancel => { - let already = self.job_cancel.is_cancelled(); - if !already { - tracing::warn!("job control: cancelling shared job token"); - self.job_cancel.cancel(); - } - JobControlReply::Ack { already } - } - JobControlCommand::WatchSlot { slot, actor_id } => { - if let Ok(mut g) = self.watched.lock() { - g.insert(actor_id, slot); - } - if let Err(e) = ctx.watch(&actor_id).await { - tracing::warn!(%actor_id, slot, error = %e, "DeathWatch register failed"); - } else { - tracing::debug!(%actor_id, slot, "DeathWatch registered"); - } - JobControlReply::Watched - } - }; - Message::pack(&reply) - } -} - -/// Ask every rank's job-control actor to cancel (best-effort, parallel). -pub async fn broadcast_job_cancel(system: &Arc, world_size: usize) { - let world_size = world_size.max(1); - let mut futs = Vec::with_capacity(world_size); - for rank in 0..world_size { - let name = DistEnv::job_control_name(rank); - let system = Arc::clone(system); - futs.push(async move { - match resolve_actor(system.as_ref(), &name).await { - Ok(ctrl) => match ask_timeout::<_, JobControlReply>( - &ctrl, - JobControlCommand::Cancel, - ASK_TIMEOUT, - ) - .await - { - Ok(JobControlReply::Ack { already }) => { - tracing::debug!(%name, already, "job cancel broadcast ok"); - } - Ok(_) => tracing::debug!(%name, "job cancel unexpected reply"), - Err(e) => tracing::warn!(%name, error = %e, "job cancel ask failed"), - }, - Err(e) => tracing::warn!(%name, error = %e, "job cancel resolve failed"), - } - }); - } - join_all(futs).await; -} - -/// Register DeathWatch for local slots. -/// -/// Each entry is `(actor_ref, flat_pool_index)`. Callers must pass the same -/// slot-major indices used by [`crate::scheduler::Scheduler`] / [`DistEnv::slot_names`] -/// — **not** `0..local_count` when the fleet spans multiple ranks. -pub async fn register_local_watches( - control: &ActorRef, - slots: &[(ActorRef, usize)], -) -> anyhow::Result<()> { - for (wref, flat_idx) in slots { - let _ = ask_timeout::<_, JobControlReply>( - control, - JobControlCommand::WatchSlot { - slot: *flat_idx, - actor_id: *wref.id(), - }, - Duration::from_secs(5), - ) - .await?; - } - Ok(()) -} - -/// Watch local `job_cancel`; when it fires, fan-out to all ranks' control actors. -pub fn spawn_cancel_broadcast( - system: Arc, - job_cancel: CancellationToken, - world_size: usize, -) -> tokio::task::JoinHandle<()> { - tokio::spawn(async move { - job_cancel.cancelled().await; - tracing::warn!( - world_size, - "local job cancel: broadcasting to job-control actors" - ); - broadcast_job_cancel(&system, world_size).await; - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn cancel_command_trips_shared_token() { - let token = CancellationToken::new(); - let system = Arc::new( - ActorSystem::builder() - .mailbox_capacity(16) - .build() - .await - .unwrap(), - ); - let name = DistEnv::job_control_name(0); - let ctrl = system - .spawn_named(&name, JobControlActor::new(token.clone())) - .await - .unwrap(); - assert!(!token.is_cancelled()); - let ack = ctrl - .ask::<_, JobControlReply>(JobControlCommand::Cancel) - .await - .unwrap(); - assert!(matches!(ack, JobControlReply::Ack { already: false })); - assert!(token.is_cancelled()); - system.shutdown().await.unwrap(); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn broadcast_reaches_peer_rank_control() { - let t0 = CancellationToken::new(); - let t1 = CancellationToken::new(); - let system = Arc::new( - ActorSystem::builder() - .mailbox_capacity(16) - .build() - .await - .unwrap(), - ); - system - .spawn_named( - DistEnv::job_control_name(0), - JobControlActor::new(t0.clone()), - ) - .await - .unwrap(); - system - .spawn_named( - DistEnv::job_control_name(1), - JobControlActor::new(t1.clone()), - ) - .await - .unwrap(); - broadcast_job_cancel(&system, 2).await; - assert!(t0.is_cancelled() && t1.is_cancelled()); - system.shutdown().await.unwrap(); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn death_watch_quarantines_local_slot() { - let token = CancellationToken::new(); - let sched = Scheduler::new(1, 1); - let system = Arc::new( - ActorSystem::builder() - .mailbox_capacity(16) - .build() - .await - .unwrap(), - ); - let ctrl = system - .spawn_named( - DistEnv::job_control_name(0), - JobControlActor::with_scheduler(token.clone(), Arc::clone(&sched)), - ) - .await - .unwrap(); - let worker = system - .spawn_named( - "ppilot/worker/0", - crate::worker::WorkerActor::with_plan( - "w0", - std::path::PathBuf::from("python3"), - vec![], - std::path::PathBuf::from("/dev/null"), - vec![], - token.clone(), - ), - ) - .await - .unwrap(); - register_local_watches(&ctrl, &[(worker.clone(), 0)]) - .await - .unwrap(); - assert!(!sched.is_quarantined(0)); - system.stop("ppilot/worker/0").await.unwrap(); - // Allow DeathWatch delivery. - for _ in 0..50 { - if sched.is_quarantined(0) { - break; - } - tokio::time::sleep(Duration::from_millis(20)).await; - } - assert!(sched.is_quarantined(0), "expected DeathWatch quarantine"); - system.shutdown().await.unwrap(); - } - - /// Regression: torchrun-shaped pool (world=2, per_worker=2) must map - /// rank0 slot1 → flat index 2, not local ordinal 1 (which is peer w1s0). - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn death_watch_uses_slot_major_flat_index() { - let token = CancellationToken::new(); - let world = 2; - let per_worker = 2; - let n_slots = world * per_worker; - let sched = Scheduler::new(n_slots, 1); - let system = Arc::new( - ActorSystem::builder() - .mailbox_capacity(16) - .build() - .await - .unwrap(), - ); - let ctrl = system - .spawn_named( - DistEnv::job_control_name(0), - JobControlActor::with_scheduler(token.clone(), Arc::clone(&sched)), - ) - .await - .unwrap(); - - // Only spawn rank0's two local slots (as driver does before peers join). - let mut watches = Vec::new(); - for slot in 0..per_worker { - let name = DistEnv::slot_name(0, slot, per_worker); - let w = system - .spawn_named( - &name, - crate::worker::WorkerActor::with_plan( - format!("w0s{slot}"), - std::path::PathBuf::from("python3"), - vec![], - std::path::PathBuf::from("/dev/null"), - vec![], - token.clone(), - ), - ) - .await - .unwrap(); - let flat = DistEnv::slot_flat_index(0, slot, world, per_worker); - watches.push((w, flat)); - } - assert_eq!(watches[0].1, 0); - assert_eq!(watches[1].1, 2, "w0s1 must be flat 2, not 1"); - - register_local_watches(&ctrl, &watches).await.unwrap(); - - // Kill rank0 slot1 → must quarantine flat 2, leave 1 (peer) alone. - system - .stop(DistEnv::slot_name(0, 1, per_worker)) - .await - .unwrap(); - for _ in 0..50 { - if sched.is_quarantined(2) { - break; - } - tokio::time::sleep(Duration::from_millis(20)).await; - } - assert!( - sched.is_quarantined(2), - "expected quarantine of flat index 2 (w0s1)" - ); - assert!( - !sched.is_quarantined(1), - "must not quarantine flat 1 (would be peer w1s0)" - ); - assert!(!sched.is_quarantined(0)); - system.shutdown().await.unwrap(); - } -} diff --git a/crates/persisting-ppilot/src/lib.rs b/crates/persisting-ppilot/src/lib.rs deleted file mode 100644 index 380b2d6ca..000000000 --- a/crates/persisting-ppilot/src/lib.rs +++ /dev/null @@ -1,80 +0,0 @@ -//! pPilot — Durable Run Orchestrator. -//! -//! # Semantic primitives -//! -//! Contracts are listed in [`blocks`]. Unit / interface tests live in the same -//! source file as each primitive; `tests/` holds multi-module integration only. -//! -//! # Design sketch -//! -//! The driver owns plan emission and dispatch. A Python workload supplies -//! `plan()` and `execute(item)`; an embedding host chooses local workers or a -//! torchrun-created multi-process environment. The standalone `ppilot` command -//! exposes only scalable Run production. -//! -//! Most modules are `pub(crate)`; only embedding and integration-test surfaces -//! are re-exported or left as `pub mod`. - -pub mod agentctl; -pub mod batch; -pub mod blocks; -pub(crate) mod check; -pub(crate) mod checkpoint; -pub mod cli; -pub mod coordination; -pub(crate) mod digest; -pub mod dist; -pub mod driver; -pub(crate) mod executor; -pub mod future; -pub mod job_control; -pub mod observe; -pub(crate) mod plan; -pub mod pulsing_ext; -pub(crate) mod python_env; -pub(crate) mod result_cache; -pub mod runtime; -pub mod runtime_bridge; -pub(crate) mod scheduler; -pub(crate) mod sink; -#[cfg(feature = "traj-sink")] -pub(crate) mod sink_traj; -pub(crate) mod sink_writer; -pub(crate) mod skip; -pub mod supervisor; -pub mod task; -pub(crate) mod worker; - -// ── Public surface (embedding + integration tests) ────────────────── - -pub use agentctl::{AgentCtlClient, AgentCtlClientConfig}; -pub use batch::{ - BatchProductionManifest, BatchProductionOptions, BatchProductionReport, - TrajectoryProductionRun, produce_from_planner, produce_trajectories, -}; -pub use check::{CheckOptions, CheckReport, run_check}; -pub use checkpoint::CheckpointLedger; -pub use cli::{PPilotArgs, ResultsFormat, init_tracing, init_tracing_with_verbose, run_ppilot}; -pub use coordination::{ - AttemptObservation, AttemptObserver, DurableAttemptObserver, ProcessLocalAttemptObserver, - ReconcileReport, RunCoordinator, -}; -pub use dist::DistEnv; -pub use driver::{Driver, RunOptions}; -pub use observe::{Observer, ObserverOptions}; -pub use runtime::{run_fleet, run_local_fleet}; -pub use runtime_bridge::PilotRuntimeBridge; -pub use skip::SkipSet; -pub use supervisor::{ - EmbeddedSupervisor, EmbeddedSupervisorConfig, SupervisorRegistrationSnapshot, parse_bandwidth, -}; -pub use task::{TaskExpr, TaskResult}; -pub use worker::{WorkerActor, WorkerCommand, WorkerReply}; - -// Sink types used by orchestration hosts. -pub use sink::{JsonlFileSink, ResultSink, TeeSink}; -#[cfg(feature = "traj-sink")] -pub use sink_traj::LanceResultSink; -pub use sink_writer::{ - SinkSubmitter, SinkWriterHandle, spawn_coordinated_sink_writer, spawn_sink_writer, -}; diff --git a/crates/persisting-ppilot/src/observe.rs b/crates/persisting-ppilot/src/observe.rs deleted file mode 100644 index 594ed61c1..000000000 --- a/crates/persisting-ppilot/src/observe.rs +++ /dev/null @@ -1,513 +0,0 @@ -//! Lightweight pPilot observability: queue / placement / per-task timing. -//! -//! Enabled with `--observe`. Progress lines go to **stderr** (prefix `[obs]`). -//! Machine NDJSON goes to `--observe-file` (and optionally stderr with `--observe-json`). - -use crate::scheduler::Scheduler; -use crate::task::unix_now; -use serde::Serialize; -use std::collections::HashMap; -use std::sync::{Arc, Mutex}; -use std::time::Instant; -use tokio::fs::OpenOptions; -use tokio::io::AsyncWriteExt; -use tokio::sync::Mutex as AsyncMutex; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum TaskPhase { - Queued, - Assigned, - Running, - Finished, - Failed, - Cancelled, -} - -#[derive(Debug, Clone, Serialize)] -pub struct ObsEvent { - pub kind: &'static str, - pub ts: f64, - #[serde(skip_serializing_if = "Option::is_none")] - pub task_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub worker: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub worker_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub attempt: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub phase: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub ok: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub duration_ms: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub elapsed_ms: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub loads: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub inflight: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub queued: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub error: Option, -} - -#[derive(Debug, Clone, Serialize)] -pub struct InflightView { - pub task_id: String, - pub worker: usize, - pub worker_id: String, - pub phase: TaskPhase, - pub elapsed_ms: u64, - pub attempt: u32, -} - -struct InflightRec { - worker: usize, - worker_id: String, - phase: TaskPhase, - since: Instant, - attempt: u32, -} - -/// Shared live view of in-flight tasks + event sink. -pub struct Observer { - enabled: bool, - /// Human-readable `[obs] …` lines on stderr. - human: bool, - /// NDJSON on stderr (in addition to optional file). - json_stderr: bool, - file: AsyncMutex>, - inflight: Mutex>, - queued: Mutex, -} - -pub struct ObserverOptions { - pub human: bool, - pub json_stderr: bool, - pub path: Option, -} - -impl Observer { - pub fn disabled() -> Arc { - Arc::new(Self { - enabled: false, - human: false, - json_stderr: false, - file: AsyncMutex::new(None), - inflight: Mutex::new(HashMap::new()), - queued: Mutex::new(0), - }) - } - - pub async fn open(opts: ObserverOptions) -> anyhow::Result> { - let file = if let Some(p) = opts.path.as_deref() - && let Some(parent) = p.parent() - { - if !parent.as_os_str().is_empty() { - tokio::fs::create_dir_all(parent).await?; - } - Some(OpenOptions::new().create(true).append(true).open(p).await?) - } else { - None - }; - let obs = Arc::new(Self { - enabled: true, - human: opts.human, - json_stderr: opts.json_stderr, - file: AsyncMutex::new(file), - inflight: Mutex::new(HashMap::new()), - queued: Mutex::new(0), - }); - if obs.human { - let mut msg = String::from( - "[obs] enabled — progress on stderr (results default to --results quiet)", - ); - if let Some(p) = opts.path.as_deref() { - msg.push_str(&format!("; NDJSON → {}", p.display())); - } - if opts.json_stderr { - msg.push_str("; NDJSON also on stderr (--observe-json)"); - } - eprintln!("{msg}"); - } - Ok(obs) - } - - pub fn enabled(&self) -> bool { - self.enabled - } - - fn human_line(&self, line: &str) { - if self.human { - eprintln!("[obs] {line}"); - } - } - - async fn emit_json(&self, event: &ObsEvent) { - if !self.enabled { - return; - } - let mut guard = self.file.lock().await; - if !self.json_stderr && guard.is_none() { - return; - } - let Ok(line) = serde_json::to_string(event) else { - return; - }; - if self.json_stderr { - eprintln!("{line}"); - } - if let Some(f) = guard.as_mut() { - let _ = f.write_all(line.as_bytes()).await; - let _ = f.write_all(b"\n").await; - let _ = f.flush().await; - } - } - - async fn emit(&self, event: ObsEvent, human: Option) { - if !self.enabled { - return; - } - if let Some(h) = human { - self.human_line(&h); - } - self.emit_json(&event).await; - } - - pub async fn task_queued(&self, task_id: &str) { - if !self.enabled { - return; - } - let q = { - let mut g = self.queued.lock().unwrap_or_else(|e| e.into_inner()); - *g += 1; - *g - }; - self.emit( - ObsEvent { - kind: "task.queued", - ts: unix_now(), - task_id: Some(task_id.into()), - worker: None, - worker_id: None, - attempt: None, - phase: Some(TaskPhase::Queued), - ok: None, - duration_ms: None, - elapsed_ms: None, - loads: None, - inflight: None, - queued: Some(q), - error: None, - }, - Some(format!("queued {task_id} queue={q}")), - ) - .await; - } - - pub async fn task_assigned( - &self, - task_id: &str, - worker: usize, - worker_id: &str, - attempt: u32, - sched: &Scheduler, - ) { - if !self.enabled { - return; - } - { - let mut q = self.queued.lock().unwrap_or_else(|e| e.into_inner()); - *q = q.saturating_sub(1); - } - { - let mut map = self.inflight.lock().unwrap_or_else(|e| e.into_inner()); - map.insert( - task_id.to_string(), - InflightRec { - worker, - worker_id: worker_id.to_string(), - phase: TaskPhase::Assigned, - since: Instant::now(), - attempt, - }, - ); - } - let loads = sched.load_snapshot(); - let q = self.queued_count().unwrap_or(0); - self.emit( - ObsEvent { - kind: "task.assigned", - ts: unix_now(), - task_id: Some(task_id.into()), - worker: Some(worker), - worker_id: Some(worker_id.into()), - attempt: Some(attempt), - phase: Some(TaskPhase::Assigned), - ok: None, - duration_ms: None, - elapsed_ms: None, - loads: Some(loads.clone()), - inflight: None, - queued: Some(q), - error: None, - }, - Some(format!( - "assigned {task_id} → {worker_id} loads={loads:?} queue={q}" - )), - ) - .await; - } - - pub async fn task_running(&self, task_id: &str) { - if !self.enabled { - return; - } - let (worker, worker_id, attempt, elapsed_ms) = { - let mut map = self.inflight.lock().unwrap_or_else(|e| e.into_inner()); - let Some(rec) = map.get_mut(task_id) else { - return; - }; - rec.phase = TaskPhase::Running; - ( - rec.worker, - rec.worker_id.clone(), - rec.attempt, - rec.since.elapsed().as_millis() as u64, - ) - }; - self.emit( - ObsEvent { - kind: "task.running", - ts: unix_now(), - task_id: Some(task_id.into()), - worker: Some(worker), - worker_id: Some(worker_id.clone()), - attempt: Some(attempt), - phase: Some(TaskPhase::Running), - ok: None, - duration_ms: None, - elapsed_ms: Some(elapsed_ms), - loads: None, - inflight: None, - queued: None, - error: None, - }, - Some(format!("running {task_id} on {worker_id}")), - ) - .await; - } - - pub async fn task_finished( - &self, - task_id: &str, - ok: bool, - cancelled: bool, - error: Option, - sched: &Scheduler, - ) { - if !self.enabled { - return; - } - let (worker, worker_id, attempt, duration_ms, phase) = { - let mut map = self.inflight.lock().unwrap_or_else(|e| e.into_inner()); - if let Some(rec) = map.remove(task_id) { - let phase = if cancelled { - TaskPhase::Cancelled - } else if ok { - TaskPhase::Finished - } else { - TaskPhase::Failed - }; - ( - Some(rec.worker), - Some(rec.worker_id), - Some(rec.attempt), - Some(rec.since.elapsed().as_millis() as u64), - Some(phase), - ) - } else { - ( - None, - None, - None, - None, - Some(if cancelled { - TaskPhase::Cancelled - } else if ok { - TaskPhase::Finished - } else { - TaskPhase::Failed - }), - ) - } - }; - let loads = sched.load_snapshot(); - let q = self.queued_count().unwrap_or(0); - let status = if cancelled { - "cancelled" - } else if ok { - "ok" - } else { - "fail" - }; - let wid = worker_id.as_deref().unwrap_or("?").to_string(); - let dur = duration_ms - .map(|d| format!("{d}ms")) - .unwrap_or_else(|| "?".into()); - let err_s = error - .as_ref() - .map(|e| format!(" err={e}")) - .unwrap_or_default(); - let human = format!("finished {task_id} on {wid} {dur} {status}{err_s}"); - self.emit( - ObsEvent { - kind: "task.finished", - ts: unix_now(), - task_id: Some(task_id.into()), - worker, - worker_id, - attempt, - phase, - ok: Some(ok && !cancelled), - duration_ms, - elapsed_ms: duration_ms, - loads: Some(loads), - inflight: None, - queued: Some(q), - error, - }, - Some(human), - ) - .await; - } - - pub async fn fleet_snapshot(&self, sched: &Scheduler) { - if !self.enabled { - return; - } - let inflight = { - let map = self.inflight.lock().unwrap_or_else(|e| e.into_inner()); - map.iter() - .map(|(id, rec)| InflightView { - task_id: id.clone(), - worker: rec.worker, - worker_id: rec.worker_id.clone(), - phase: rec.phase, - elapsed_ms: rec.since.elapsed().as_millis() as u64, - attempt: rec.attempt, - }) - .collect::>() - }; - let loads = sched.load_snapshot(); - let q = self.queued_count().unwrap_or(0); - let n = inflight.len(); - // Keep human fleet lines short: only when something is in flight or queued. - let human = if n > 0 || q > 0 { - let sample: Vec = inflight - .iter() - .take(4) - .map(|v| format!("{}@{}:{}ms", v.task_id, v.worker_id, v.elapsed_ms)) - .collect(); - let more = if n > 4 { - format!(" +{}", n - 4) - } else { - String::new() - }; - Some(format!( - "fleet loads={loads:?} queue={q} inflight={n} [{}{more}]", - sample.join(", ") - )) - } else { - None - }; - self.emit( - ObsEvent { - kind: "fleet.snapshot", - ts: unix_now(), - task_id: None, - worker: None, - worker_id: None, - attempt: None, - phase: None, - ok: None, - duration_ms: None, - elapsed_ms: None, - loads: Some(loads), - inflight: Some(inflight), - queued: Some(q), - error: None, - }, - human, - ) - .await; - } - - fn queued_count(&self) -> Option { - self.queued.lock().ok().map(|g| *g).or(Some(0)) - } -} - -/// Background ticker for [`Observer::fleet_snapshot`]. -pub fn spawn_snapshot_loop( - obs: Arc, - sched: Arc, - cancel: tokio_util::sync::CancellationToken, - every: std::time::Duration, -) -> tokio::task::JoinHandle<()> { - tokio::spawn(async move { - if !obs.enabled() { - return; - } - let mut interval = tokio::time::interval(every); - interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - loop { - tokio::select! { - biased; - _ = cancel.cancelled() => break, - _ = interval.tick() => { - obs.fleet_snapshot(&sched).await; - } - } - } - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::scheduler::Scheduler; - - #[tokio::test] - async fn disabled_observer_is_noop() { - let obs = Observer::disabled(); - assert!(!obs.enabled()); - obs.task_queued("t-0").await; - let sched = Scheduler::new(1, 1); - obs.task_assigned("t-0", 0, "w0", 0, &sched).await; - obs.task_running("t-0").await; - obs.task_finished("t-0", true, false, None, &sched).await; - obs.fleet_snapshot(&sched).await; - } - - #[tokio::test] - async fn enabled_tracks_queued_count() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("obs.ndjson"); - let obs = Observer::open(ObserverOptions { - human: false, - json_stderr: false, - path: Some(path.clone()), - }) - .await - .unwrap(); - obs.task_queued("t-0").await; - obs.task_queued("t-1").await; - let body = tokio::fs::read_to_string(&path).await.unwrap(); - assert!(body.contains("task.queued")); - assert_eq!(body.lines().count(), 2); - } -} diff --git a/crates/persisting-ppilot/src/plan.rs b/crates/persisting-ppilot/src/plan.rs deleted file mode 100644 index a272f1f7c..000000000 --- a/crates/persisting-ppilot/src/plan.rs +++ /dev/null @@ -1,236 +0,0 @@ -//! Run a user plan script and stream typed values (NDJSON on stdout). -//! -//! The control plane never embeds the user's interpreter. It **invokes** -//! `--python` so quirky envs stay isolated; stacks stay in that process. -//! -//! User CLI args after `--` become ``sys.argv`` for the plan module (argparse-friendly). - -use crate::task::TaskExpr; -use anyhow::{Context, Result, bail}; -use futures::{Stream, StreamExt}; -use std::path::PathBuf; -use std::pin::Pin; -use std::process::Stdio; -use tokio::io::{AsyncBufReadExt, BufReader}; -use tokio::process::Command; -use tokio::sync::mpsc; -use tokio_stream::wrappers::ReceiverStream; - -/// Bootstrap: set ``sys.argv = [script, *user_args]`` then import plan(). -const PLAN_BOOTSTRAP: &str = r#" -import asyncio, json, sys -from pathlib import Path -path = Path(sys.argv[1]).resolve() -user_args = sys.argv[2:] -# So argparse in task.py sees the same argv as `python task.py --foo bar` -sys.argv = [str(path), *user_args] -import importlib.util -spec = importlib.util.spec_from_file_location("user_plan", path) -mod = importlib.util.module_from_spec(spec) -spec.loader.exec_module(mod) - -def _dump(item): - if hasattr(item, "to_dict"): - item = item.to_dict() - print(json.dumps(item, ensure_ascii=False), flush=True) - -async def _emit(): - if hasattr(mod, "plan"): - out = mod.plan() - if asyncio.iscoroutine(out): - out = await out - if hasattr(out, "__aiter__"): - async for item in out: - _dump(item) - return - for item in out: - _dump(item) - return - if hasattr(mod, "PLAN"): - for item in mod.PLAN: - _dump(item) - return - raise SystemExit("plan script must define plan() or PLAN") - -asyncio.run(_emit()) -"#; - -/// Stream tasks from a plan script under `python`. -pub fn stream_plan_tasks( - script: PathBuf, - python: PathBuf, - script_args: Vec, -) -> Pin> + Send>> { - Box::pin( - stream_plan_values(script, python, script_args) - .map(|value| value.and_then(TaskExpr::from_value)), - ) -} - -/// Stream raw JSON values from a Python plan. Consumers apply their own -/// boundary type (`TaskExpr`, production Run, ...), while process isolation, -/// async-generator support, and argument forwarding remain shared. -pub(crate) fn stream_plan_values( - script: PathBuf, - python: PathBuf, - script_args: Vec, -) -> Pin> + Send>> { - let (tx, rx) = mpsc::channel::>(64); - tokio::spawn(async move { - if let Err(e) = run_plan_process(script, python, script_args, tx.clone()).await { - let _ = tx.send(Err(e)).await; - } - }); - Box::pin(ReceiverStream::new(rx)) -} - -async fn run_plan_process( - script: PathBuf, - python: PathBuf, - script_args: Vec, - tx: mpsc::Sender>, -) -> Result<()> { - let script = script - .canonicalize() - .with_context(|| format!("plan script not found: {}", script.display()))?; - - let mut cmd = Command::new(&python); - cmd.arg("-c") - .arg(PLAN_BOOTSTRAP) - .arg(&script) - .args(&script_args) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()); - let mut child = cmd - .spawn() - .with_context(|| format!("spawn plan python: {}", python.display()))?; - - let stdout = child - .stdout - .take() - .ok_or_else(|| anyhow::anyhow!("missing plan stdout"))?; - let stderr = child - .stderr - .take() - .ok_or_else(|| anyhow::anyhow!("missing plan stderr"))?; - - let stderr_task = tokio::spawn(async move { - let mut lines = BufReader::new(stderr).lines(); - let mut buf = String::new(); - while let Ok(Some(line)) = lines.next_line().await { - buf.push_str(&line); - buf.push('\n'); - } - buf - }); - - let mut lines = BufReader::new(stdout).lines(); - while let Some(line) = lines.next_line().await? { - let line = line.trim(); - if line.is_empty() || line.starts_with('#') { - continue; - } - let parsed = serde_json::from_str::(line) - .with_context(|| format!("invalid NDJSON from plan: {line}")); - if tx.send(parsed).await.is_err() { - break; - } - } - - let status = child.wait().await.context("wait plan process")?; - let err = stderr_task.await.unwrap_or_default(); - if !status.success() { - bail!( - "plan script exited {}: {}", - status.code().unwrap_or(-1), - err.trim() - ); - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use futures::StreamExt; - - #[tokio::test] - async fn stream_plan_tasks_emits_flat_items() { - let dir = tempfile::tempdir().unwrap(); - let script = dir.path().join("plan.py"); - std::fs::write( - &script, - r#" -def plan(): - for i in range(3): - yield {"id": f"t-{i}", "x": i} - -def execute(item): - return item -"#, - ) - .unwrap(); - let mut stream = stream_plan_tasks(script, PathBuf::from("python3"), vec![]); - let mut ids = Vec::new(); - while let Some(item) = stream.next().await { - ids.push(item.unwrap().id); - } - assert_eq!(ids, vec!["t-0", "t-1", "t-2"]); - } - - #[tokio::test] - async fn stream_plan_values_supports_async_generators_and_forwards_args() { - let dir = tempfile::tempdir().unwrap(); - let script = dir.path().join("production.py"); - std::fs::write( - &script, - r#" -import argparse - -parser = argparse.ArgumentParser() -parser.add_argument("--count", type=int, required=True) -args = parser.parse_args() - -async def plan(): - for i in range(args.count): - yield {"id": f"run-{i}", "command": ["/bin/true"]} -"#, - ) - .unwrap(); - let values = stream_plan_values( - script, - PathBuf::from("python3"), - vec!["--count".into(), "2".into()], - ) - .collect::>() - .await; - assert_eq!(values.len(), 2); - assert_eq!(values[0].as_ref().unwrap()["id"], "run-0"); - assert_eq!(values[1].as_ref().unwrap()["command"][0], "/bin/true"); - } - - #[tokio::test] - async fn stream_plan_values_surfaces_python_stderr() { - let dir = tempfile::tempdir().unwrap(); - let script = dir.path().join("broken.py"); - std::fs::write( - &script, - r#" -def plan(): - raise RuntimeError("planner exploded") -"#, - ) - .unwrap(); - let errors = stream_plan_values(script, PathBuf::from("python3"), vec![]) - .collect::>() - .await; - assert_eq!(errors.len(), 1); - assert!( - errors[0] - .as_ref() - .unwrap_err() - .to_string() - .contains("planner exploded") - ); - } -} diff --git a/crates/persisting-ppilot/src/pulsing_ext.rs b/crates/persisting-ppilot/src/pulsing_ext.rs deleted file mode 100644 index 93f814f36..000000000 --- a/crates/persisting-ppilot/src/pulsing_ext.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! Thin helpers over Pulsing so pPilot uses one resolve/spawn style. - -use anyhow::{Context, Result}; -use pulsing_actor::prelude::*; -use std::sync::Arc; -use std::time::Duration; - -/// Default infra `ask` deadline (select! / timeout wrapper). -pub const ASK_TIMEOUT: Duration = Duration::from_secs(120); - -/// Worker slot supervision: restart on failure, capped. -pub fn worker_supervision() -> SupervisionSpec { - SupervisionSpec::on_failure().with_max_restarts(3) -} - -/// Unified named resolve (prefer this over mixing `resolve` / `resolve_named`). -pub async fn resolve_actor(system: &ActorSystem, name: &str) -> Result { - system - .resolve_named(name, None) - .await - .with_context(|| format!("resolve Pulsing actor {name}")) -} - -/// `ask` with an explicit deadline (Pulsing has no ask_timeout yet). -pub async fn ask_timeout(actor: &ActorRef, msg: M, timeout: Duration) -> Result -where - M: serde::Serialize + 'static, - R: serde::de::DeserializeOwned, -{ - match tokio::time::timeout(timeout, actor.ask::(msg)).await { - Ok(Ok(r)) => Ok(r), - Ok(Err(e)) => Err(anyhow::anyhow!("pulsing ask: {e}")), - Err(_) => Err(anyhow::anyhow!("pulsing ask timed out after {timeout:?}")), - } -} - -/// Spawn a named actor with supervision via factory (enables restart). -pub async fn spawn_supervised( - system: &Arc, - name: &str, - factory: F, -) -> Result -where - F: FnMut() -> pulsing_actor::error::Result + Send + 'static, - A: Actor, -{ - system - .spawning() - .name(name) - .supervision(worker_supervision()) - .mailbox_capacity(256) - .spawn_factory(factory) - .await - .with_context(|| format!("spawn supervised {name}")) -} diff --git a/crates/persisting-ppilot/src/python_env.rs b/crates/persisting-ppilot/src/python_env.rs deleted file mode 100644 index ca4b32634..000000000 --- a/crates/persisting-ppilot/src/python_env.rs +++ /dev/null @@ -1,95 +0,0 @@ -//! Shared helpers for invoking user `--python` with a usable module path. -//! -//! Semantic block: [`crate::blocks::ids::PYTHON_ENV`]. - -use std::env; -use std::ffi::OsStr; -use std::path::{Path, PathBuf}; - -/// Build PYTHONPATH from an explicit existing value + extras (testable without mutating env). -pub fn merge_pythonpath_parts(existing: Option<&OsStr>, extras: &[PathBuf]) -> Option { - let mut parts: Vec = Vec::new(); - if let Some(cur) = existing { - for p in env::split_paths(cur) { - if !p.as_os_str().is_empty() && !parts.iter().any(|x| x == &p) { - parts.push(p); - } - } - } - for e in extras { - let p = e.canonicalize().unwrap_or_else(|_| e.to_path_buf()); - if !parts.iter().any(|x| x == &p) { - parts.push(p); - } - } - if parts.is_empty() { - None - } else { - env::join_paths(&parts) - .ok() - .map(|os| os.to_string_lossy().into_owned()) - } -} - -/// Build PYTHONPATH: existing env + extras (plan dir, `-E` paths, …). -pub fn merge_pythonpath(extras: &[PathBuf]) -> Option { - merge_pythonpath_parts(env::var_os("PYTHONPATH").as_deref(), extras) -} - -/// Default extras for a plan script: its parent directory (so sibling modules import). -pub fn pythonpath_for_script(script: &Path) -> Vec { - let mut out = Vec::new(); - if let Some(parent) = script.parent() { - out.push(parent.to_path_buf()); - } - out -} - -pub fn apply_pythonpath(cmd: &mut tokio::process::Command, extras: &[PathBuf]) { - if let Some(pp) = merge_pythonpath(extras) { - cmd.env("PYTHONPATH", pp); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::ffi::OsString; - - #[test] - fn merge_dedups_and_preserves_order() { - let existing = OsString::from("/a:/b"); - let extras = vec![PathBuf::from("/b"), PathBuf::from("/c")]; - let merged = merge_pythonpath_parts(Some(&existing), &extras).unwrap(); - let parts: Vec<_> = env::split_paths(&merged).collect(); - assert!( - parts - .iter() - .any(|p| p.ends_with("a") || p == Path::new("/a")) - ); - assert_eq!( - parts - .iter() - .filter(|p| p.ends_with("b") || p.as_path() == Path::new("/b")) - .count(), - 1 - ); - assert!( - parts - .iter() - .any(|p| p.ends_with("c") || p == Path::new("/c")) - ); - } - - #[test] - fn merge_empty_returns_none() { - assert!(merge_pythonpath_parts(None, &[]).is_none()); - } - - #[test] - fn pythonpath_for_script_uses_parent() { - let p = Path::new("/tmp/plans/task.py"); - let extras = pythonpath_for_script(p); - assert_eq!(extras, vec![PathBuf::from("/tmp/plans")]); - } -} diff --git a/crates/persisting-ppilot/src/result_cache.rs b/crates/persisting-ppilot/src/result_cache.rs deleted file mode 100644 index 20a0e50d0..000000000 --- a/crates/persisting-ppilot/src/result_cache.rs +++ /dev/null @@ -1,99 +0,0 @@ -//! Infra-retry idempotency cache: `task_id` → last terminal [`TaskResult`]. -//! -//! Semantic block: **pPilot attempt idempotency (same worker)**. -//! Kept separate from [`crate::worker::WorkerActor`] so the policy is unit-testable -//! without Pulsing / Python. - -use crate::task::TaskResult; -use std::collections::{HashMap, VecDeque}; - -/// Default cap for cached results per worker slot. -pub const DEFAULT_RESULT_CACHE_CAP: usize = 4096; - -/// Bounded LRU-ish cache (evict oldest insert order). -#[derive(Debug, Default)] -pub struct ResultCache { - map: HashMap, - order: VecDeque, - cap: usize, -} - -impl ResultCache { - pub fn new(cap: usize) -> Self { - Self { - map: HashMap::new(), - order: VecDeque::new(), - cap: cap.max(1), - } - } - - pub fn get(&self, task_id: &str) -> Option<&TaskResult> { - self.map.get(task_id) - } - - /// Insert or replace. Skips caching cancelled results (caller should not insert them). - pub fn put(&mut self, task_id: impl Into, result: TaskResult) { - let task_id = task_id.into(); - if result.cancelled { - return; - } - if let Some(existing) = self.map.get_mut(&task_id) { - *existing = result; - return; - } - while self.order.len() >= self.cap { - if let Some(old) = self.order.pop_front() { - self.map.remove(&old); - } else { - break; - } - } - self.order.push_back(task_id.clone()); - self.map.insert(task_id, result); - } - - pub fn len(&self) -> usize { - self.map.len() - } - - pub fn is_empty(&self) -> bool { - self.map.is_empty() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - - #[test] - fn put_get_and_skip_cancelled() { - let mut c = ResultCache::new(8); - c.put("t-0", TaskResult::success("t-0", json!(1), "w0", 0.0)); - assert!(c.get("t-0").unwrap().ok); - c.put("t-1", TaskResult::cancelled("t-1")); - assert!(c.get("t-1").is_none()); - assert_eq!(c.len(), 1); - } - - #[test] - fn evicts_oldest_when_over_cap() { - let mut c = ResultCache::new(2); - c.put("a", TaskResult::success("a", json!(1), "w0", 0.0)); - c.put("b", TaskResult::success("b", json!(2), "w0", 0.0)); - c.put("c", TaskResult::success("c", json!(3), "w0", 0.0)); - assert!(c.get("a").is_none()); - assert!(c.get("b").is_some()); - assert!(c.get("c").is_some()); - assert_eq!(c.len(), 2); - } - - #[test] - fn replace_keeps_cap() { - let mut c = ResultCache::new(2); - c.put("a", TaskResult::success("a", json!(1), "w0", 0.0)); - c.put("a", TaskResult::success("a", json!(9), "w0", 0.0)); - assert_eq!(c.len(), 1); - assert_eq!(c.get("a").unwrap().value, Some(json!(9))); - } -} diff --git a/crates/persisting-ppilot/src/runtime.rs b/crates/persisting-ppilot/src/runtime.rs deleted file mode 100644 index a6d151e46..000000000 --- a/crates/persisting-ppilot/src/runtime.rs +++ /dev/null @@ -1,561 +0,0 @@ -//! Boot Pulsing fleet, then hand the job to [`crate::driver::Driver`]. -//! -//! - Local `-w N`: spawn N×`--per-worker` slot actors (each owns a Python host). -//! - torchrun: each rank spawns `--per-worker` slot actors; rank0 is Driver. -//! -//! `--per-worker N` = N concurrent Execute slots per logical worker/rank. Each -//! slot is its own [`WorkerActor`] (serial mailbox) + dedicated plan host, so -//! concurrency is real — not mailbox queuing on one actor. -//! -//! Workers are spawned via [`crate::pulsing_ext::spawn_supervised`] (factory + -//! `SupervisionSpec`) so Pulsing can restart a failed slot. - -use crate::dist::DistEnv; -use crate::driver::Driver; -use crate::job_control::{JobControlActor, register_local_watches, spawn_cancel_broadcast}; -use crate::observe::spawn_snapshot_loop; -use crate::pulsing_ext::{ASK_TIMEOUT, ask_timeout, resolve_actor, spawn_supervised}; -use crate::python_env::pythonpath_for_script; -use crate::scheduler::{Scheduler, WorkerPool}; -use crate::supervisor::{EmbeddedSupervisor, EmbeddedSupervisorConfig}; -use crate::task::TaskResult; -use crate::worker::{ShutdownGate, WorkerCommand, WorkerConfig, WorkerReply}; -use anyhow::{Context, Result, bail}; -use pulsing_actor::prelude::*; -use std::path::PathBuf; -use std::sync::{Arc, RwLock}; -use std::time::Duration; -use tokio::time::sleep; -use tokio_util::sync::CancellationToken; - -pub use crate::driver::RunOptions; - -/// Entry: torchrun env → distributed; else in-process local fleet. -pub async fn run_fleet( - opts: RunOptions, - on_result: impl FnMut(TaskResult) + Send, -) -> Result> { - if let Some(dist) = DistEnv::from_env()? { - tracing::debug!( - rank = dist.rank, - world_size = dist.world_size, - seed = %dist.pulsing_seed, - "torchrun placement detected" - ); - if dist.is_driver() { - run_driver_rank(dist, opts, on_result).await - } else { - let pp = apply_pythonpath(&opts); - run_worker_rank(dist, &opts, pp).await?; - Ok(Vec::new()) - } - } else { - run_local_fleet(opts, on_result).await - } -} - -/// Single-process: spawn N×P slot workers; this process runs the Driver. -pub async fn run_local_fleet( - opts: RunOptions, - on_result: impl FnMut(TaskResult) + Send, -) -> Result> { - let supervisor = EmbeddedSupervisor::start(EmbeddedSupervisorConfig { - network_limit_bytes_per_second: None, - ..EmbeddedSupervisorConfig::default() - }) - .await - .context("start embedded pPilot Supervisor")?; - let mut supervisor_bootstrap = supervisor.bootstrap(); - if let Some(coordinator) = &opts.coordinator { - supervisor_bootstrap.attempt_registry_uri = - Some(coordinator.control().root_uri().to_string()); - supervisor_bootstrap.attempt_ttl_ms = opts - .coordinator - .as_ref() - .map_or(15_000, |coordinator| coordinator.lease_ttl_ms()); - } - let pythonpath = apply_pythonpath(&opts); - let system: Arc = ActorSystem::builder() - .mailbox_capacity(256) - .build() - .await - .context("build ActorSystem")?; - let per_worker = opts.per_worker_inflight.max(1); - let n_workers = opts.workers.max(1); - let names = DistEnv::slot_names(n_workers, per_worker); - let n_slots = names.len(); - // Second arg is always 1: pool is already flattened one-actor-per-slot. - let sched = Scheduler::new(n_slots, 1); - - let control = spawn_job_control( - &system, - 0, - opts.job_cancel.clone(), - Some(Arc::clone(&sched)), - ) - .await?; - let cancel_fanout = spawn_cancel_broadcast(Arc::clone(&system), opts.job_cancel.clone(), 1); - - let watches = spawn_local_fleet_slots( - &system, - n_workers, - per_worker, - &opts, - &pythonpath, - Some(supervisor_bootstrap), - ) - .await?; - let pool: WorkerPool = Arc::new(RwLock::new( - watches.iter().map(|(r, _)| r.clone()).collect(), - )); - register_local_watches(&control, &watches).await?; - - tracing::debug!( - workers = n_workers, - per_worker, - slots = n_slots, - capacity = sched.capacity(), - "driver ready (local fleet)" - ); - - let result = run_driver_loop(pool, sched, &opts, on_result, cancel_fanout, system, None).await; - if let Err(error) = supervisor.shutdown().await { - tracing::warn!(%error, "failed to stop embedded pPilot Supervisor"); - } - result -} - -/// Rank0 under torchrun: bind Pulsing seed, wait for peer slots, run Driver. -async fn run_driver_rank( - dist: DistEnv, - opts: RunOptions, - on_result: impl FnMut(TaskResult) + Send, -) -> Result> { - let supervisor = EmbeddedSupervisor::start(EmbeddedSupervisorConfig { - network_limit_bytes_per_second: None, - ..EmbeddedSupervisorConfig::default() - }) - .await - .context("start rank-local pPilot Supervisor")?; - let mut supervisor_bootstrap = supervisor.bootstrap(); - if let Some(coordinator) = &opts.coordinator { - supervisor_bootstrap.attempt_registry_uri = - Some(coordinator.control().root_uri().to_string()); - supervisor_bootstrap.attempt_ttl_ms = coordinator.lease_ttl_ms(); - } - let bind = format!("0.0.0.0:{}", dist.pulsing_seed.port()); - let system: Arc = ActorSystem::builder() - .mailbox_capacity(256) - .addr(bind.as_str()) - .build() - .await - .context("build driver ActorSystem")?; - tracing::debug!( - advertised = %dist.pulsing_seed, - bound = %system.addr(), - "driver Pulsing listening (peers join via MASTER_ADDR seed)" - ); - - let per_worker = opts.per_worker_inflight.max(1); - let names = DistEnv::slot_names(dist.world_size, per_worker); - let n_slots = names.len(); - // Second arg is always 1: pool is already flattened one-actor-per-slot. - let sched = Scheduler::new(n_slots, 1); - - let control = spawn_job_control( - &system, - 0, - opts.job_cancel.clone(), - Some(Arc::clone(&sched)), - ) - .await?; - - let pythonpath = apply_pythonpath(&opts); - let local_watches = spawn_rank_slots( - &system, - RankPlacement { - rank: 0, - n_workers: dist.world_size, - per_worker, - }, - &opts, - &pythonpath, - None, - Some(supervisor_bootstrap), - ) - .await?; - let pool: WorkerPool = Arc::new(RwLock::new( - local_watches.iter().map(|(r, _)| r.clone()).collect(), - )); - register_local_watches(&control, &local_watches).await?; - - wait_and_fill_workers(&system, &pool, &names, Duration::from_secs(120)).await?; - - let cancel_fanout = spawn_cancel_broadcast( - Arc::clone(&system), - opts.job_cancel.clone(), - dist.world_size, - ); - - tracing::debug!( - world_size = dist.world_size, - per_worker, - slots = n_slots, - capacity = sched.capacity(), - "driver ready (torchrun)" - ); - - let world_size = dist.world_size; - let per_worker_shutdown = per_worker; - let result = run_driver_loop( - pool, - sched, - &opts, - on_result, - cancel_fanout, - system, - Some(DriverPostShutdown { - names, - world_size, - per_worker: per_worker_shutdown, - }), - ) - .await; - if let Err(error) = supervisor.shutdown().await { - tracing::warn!(%error, "failed to stop rank-local pPilot Supervisor"); - } - result -} - -/// Rank > 0: join Pulsing, serve `--per-worker` slot actors until Shutdown. -async fn run_worker_rank(dist: DistEnv, opts: &RunOptions, pythonpath: Vec) -> Result<()> { - let supervisor = EmbeddedSupervisor::start(EmbeddedSupervisorConfig { - network_limit_bytes_per_second: None, - ..EmbeddedSupervisorConfig::default() - }) - .await - .context("start worker-local pPilot Supervisor")?; - let mut supervisor_bootstrap = supervisor.bootstrap(); - if let Some(coordinator) = &opts.coordinator { - supervisor_bootstrap.attempt_registry_uri = - Some(coordinator.control().root_uri().to_string()); - supervisor_bootstrap.attempt_ttl_ms = coordinator.lease_ttl_ms(); - } - let seed = dist.pulsing_seed.to_string(); - let mut last = None; - let system = { - let mut built = None; - for attempt in 1..=60 { - match ActorSystem::builder() - .mailbox_capacity(256) - .addr("0.0.0.0:0") - .seeds([seed.as_str()]) - .build() - .await - { - Ok(s) => { - tracing::debug!(attempt, %seed, "joined Pulsing cluster"); - built = Some(s); - break; - } - Err(e) => { - last = Some(e.to_string()); - tracing::debug!(attempt, error = %e, "waiting for driver Pulsing seed"); - sleep(Duration::from_millis(250)).await; - } - } - } - match built { - Some(s) => s, - None => bail!("failed to join Pulsing at {seed}: {last:?}"), - } - }; - - if let Some(pp) = crate::python_env::merge_pythonpath(&pythonpath) { - unsafe { std::env::set_var("PYTHONPATH", pp) }; - } - - spawn_job_control(&system, dist.rank, opts.job_cancel.clone(), None).await?; - - let per_worker = opts.per_worker_inflight.max(1); - let gate = ShutdownGate::new(per_worker); - let _slots = spawn_rank_slots( - &system, - RankPlacement { - rank: dist.rank, - n_workers: dist.world_size, - per_worker, - }, - opts, - &pythonpath, - Some(Arc::clone(&gate)), - Some(supervisor_bootstrap), - ) - .await?; - - gate.wait().await; - tracing::debug!(rank = dist.rank, "all worker slots shutdown"); - system - .shutdown() - .await - .map_err(|e| anyhow::anyhow!("shutdown: {e}"))?; - supervisor.shutdown().await?; - Ok(()) -} - -// ── shared boot helpers ─────────────────────────────────────────────── - -/// Resolve PYTHONPATH extras and optionally set the process env. -fn apply_pythonpath(opts: &RunOptions) -> Vec { - let mut extras = pythonpath_for_script(&opts.script); - extras.extend(opts.pythonpath_extra.iter().cloned()); - if let Some(pp) = crate::python_env::merge_pythonpath(&extras) { - unsafe { std::env::set_var("PYTHONPATH", pp) }; - } - extras -} - -fn worker_slot_id(worker: usize, slot: usize, per_worker: usize) -> String { - if per_worker <= 1 { - format!("w{worker}") - } else { - format!("w{worker}s{slot}") - } -} - -#[derive(Clone, Copy)] -struct SlotPlacement { - worker: usize, - slot: usize, - n_workers: usize, - per_worker: usize, -} - -async fn spawn_job_control( - system: &Arc, - rank: usize, - job_cancel: CancellationToken, - sched: Option>, -) -> Result { - let name = DistEnv::job_control_name(rank); - let actor = match sched { - Some(s) => JobControlActor::with_scheduler(job_cancel, s), - None => JobControlActor::new(job_cancel), - }; - system - .spawn_named(&name, actor) - .await - .map_err(|e| anyhow::anyhow!("spawn job control {name}: {e}")) -} - -async fn spawn_one_slot( - system: &Arc, - placement: SlotPlacement, - opts: &RunOptions, - pythonpath: &[PathBuf], - gate: Option>, - supervisor: Option, -) -> Result<(ActorRef, usize)> { - let SlotPlacement { - worker, - slot, - n_workers, - per_worker, - } = placement; - let name = DistEnv::slot_name(worker, slot, per_worker); - let cfg = WorkerConfig::with_fresh_cache( - worker_slot_id(worker, slot, per_worker), - opts.python.clone(), - pythonpath.to_vec(), - opts.script.clone(), - opts.script_args.clone(), - opts.job_cancel.clone(), - gate, - ) - .with_pvisor_binary(opts.pvisor_binary.clone()) - .with_supervisor(supervisor); - let wref = spawn_supervised(system, &name, move || { - cfg.build().map_err(|error| { - pulsing_actor::error::PulsingError::from( - pulsing_actor::error::RuntimeError::ActorSpawnFailed { - reason: format!("initialize pPilot worker: {error:#}"), - }, - ) - }) - }) - .await?; - let flat = DistEnv::slot_flat_index(worker, slot, n_workers, per_worker); - tracing::debug!(%name, worker, slot, flat, "worker slot ready"); - Ok((wref, flat)) -} - -/// Spawn all slots for one rank (torchrun driver local / worker rank). -/// Returns `(ActorRef, slot-major flat index)` pairs. -#[derive(Clone, Copy)] -struct RankPlacement { - rank: usize, - n_workers: usize, - per_worker: usize, -} - -async fn spawn_rank_slots( - system: &Arc, - placement: RankPlacement, - opts: &RunOptions, - pythonpath: &[PathBuf], - gate: Option>, - supervisor: Option, -) -> Result> { - let RankPlacement { - rank, - n_workers, - per_worker, - } = placement; - let mut out = Vec::with_capacity(per_worker); - for slot in 0..per_worker { - out.push( - spawn_one_slot( - system, - SlotPlacement { - worker: rank, - slot, - n_workers, - per_worker, - }, - opts, - pythonpath, - gate.clone(), - supervisor.clone(), - ) - .await?, - ); - } - Ok(out) -} - -/// Local fleet: slot-major order over all workers (matches [`DistEnv::slot_names`]). -async fn spawn_local_fleet_slots( - system: &Arc, - n_workers: usize, - per_worker: usize, - opts: &RunOptions, - pythonpath: &[PathBuf], - supervisor: Option, -) -> Result> { - let mut out = Vec::with_capacity(n_workers.saturating_mul(per_worker)); - for slot in 0..per_worker { - for worker in 0..n_workers { - out.push( - spawn_one_slot( - system, - SlotPlacement { - worker, - slot, - n_workers, - per_worker, - }, - opts, - pythonpath, - None, - supervisor.clone(), - ) - .await?, - ); - } - } - Ok(out) -} - -struct DriverPostShutdown { - names: Vec, - world_size: usize, - per_worker: usize, -} - -/// Shared Driver + observe snapshot + system shutdown. -async fn run_driver_loop( - pool: WorkerPool, - sched: Arc, - opts: &RunOptions, - on_result: impl FnMut(TaskResult) + Send, - cancel_fanout: tokio::task::JoinHandle<()>, - system: Arc, - post: Option, -) -> Result> { - let driver = Driver::new(Arc::clone(&pool), Arc::clone(&sched)); - let snap = spawn_snapshot_loop( - Arc::clone(&opts.observer), - Arc::clone(&sched), - opts.job_cancel.clone(), - Duration::from_secs(1), - ); - let out = driver.run_plan(opts, on_result).await?; - snap.abort(); - cancel_fanout.abort(); - if let Some(p) = post { - if opts.job_cancel.is_cancelled() { - crate::job_control::broadcast_job_cancel(&system, p.world_size).await; - } - shutdown_workers_resolved(&system, &p.names, p.per_worker).await?; - } - system - .shutdown() - .await - .map_err(|e| anyhow::anyhow!("shutdown: {e}"))?; - Ok(out) -} - -async fn wait_and_fill_workers( - system: &Arc, - pool: &WorkerPool, - names: &[String], - timeout: Duration, -) -> Result<()> { - let deadline = tokio::time::Instant::now() + timeout; - loop { - let mut refs = Vec::with_capacity(names.len()); - let mut missing = Vec::new(); - for name in names { - match resolve_actor(system.as_ref(), name).await { - Ok(r) => refs.push(r), - Err(_) => missing.push(name.clone()), - } - } - if missing.is_empty() { - let mut g = pool - .write() - .map_err(|_| anyhow::anyhow!("worker pool lock"))?; - *g = refs; - tracing::debug!(n = g.len(), "all worker slots resolved"); - return Ok(()); - } - if tokio::time::Instant::now() >= deadline { - bail!("timed out waiting for workers: missing {missing:?}"); - } - tracing::debug!(?missing, "waiting for worker gossip/resolve"); - sleep(Duration::from_millis(300)).await; - } -} - -async fn shutdown_workers_resolved( - system: &Arc, - names: &[String], - per_worker: usize, -) -> Result<()> { - for name in names { - let is_local = (0..per_worker.max(1)) - .any(|slot| name == &DistEnv::slot_name(0, slot, per_worker.max(1))); - if is_local { - continue; - } - match resolve_actor(system.as_ref(), name).await { - Ok(w) => { - let _ = - ask_timeout::<_, WorkerReply>(&w, WorkerCommand::Shutdown, ASK_TIMEOUT).await; - } - Err(e) => tracing::warn!(%name, error = %e, "shutdown: resolve failed"), - } - } - sleep(Duration::from_millis(200)).await; - Ok(()) -} diff --git a/crates/persisting-ppilot/src/runtime_bridge.rs b/crates/persisting-ppilot/src/runtime_bridge.rs deleted file mode 100644 index 66e823a31..000000000 --- a/crates/persisting-ppilot/src/runtime_bridge.rs +++ /dev/null @@ -1,372 +0,0 @@ -//! Long-lived pPilot adapter for pVisor's AgentCtl v1 Control protocol. - -use crate::agentctl::AgentCtlClient; -use anyhow::{Context, bail}; -use persisting_agentctl::{AgentDirective, AgentState}; -use persisting_events::unix_now_ms; -use std::collections::BTreeMap; -use std::sync::{Arc, Mutex, MutexGuard}; -use std::time::Duration; -use tokio::sync::Notify; -use tokio::task::JoinHandle; -use tokio_util::sync::CancellationToken; - -#[derive(Debug)] -struct BridgeState { - agent_state: AgentState, - accepting_work: bool, - directive: AgentDirective, - quiesce_deadline_unix_ms: Option, - warnings: Vec, -} - -impl BridgeState { - fn new(directive: AgentDirective) -> Self { - let accepting_work = matches!(&directive, AgentDirective::Continue); - let quiesce_deadline_unix_ms = match &directive { - AgentDirective::Quiesce { - deadline_unix_ms, .. - } => *deadline_unix_ms, - AgentDirective::Continue | AgentDirective::Shutdown { .. } => None, - }; - Self { - agent_state: AgentState::Active, - accepting_work, - directive, - quiesce_deadline_unix_ms, - warnings: Vec::new(), - } - } -} - -struct BridgeInner { - sync: Mutex<()>, - client: Mutex, - state: Mutex, - cancellation: CancellationToken, - changed: Notify, -} - -/// One Run-scoped pPilot client that continuously exchanges state and directives. -pub struct PilotRuntimeBridge { - inner: Arc, - stop: CancellationToken, - sync_task: Option>, -} - -impl PilotRuntimeBridge { - /// Connect the client and start periodic state synchronization. - pub fn start( - mut client: AgentCtlClient, - cancellation: CancellationToken, - ) -> anyhow::Result { - let directive = client.connect().context("connect pPilot AgentCtl")?; - if let AgentDirective::Shutdown { reason } = &directive { - bail!( - "pVisor requested shutdown during AgentCtl handshake{}", - reason - .as_deref() - .map(|reason| format!(": {reason}")) - .unwrap_or_default() - ); - } - let interval = Duration::from_millis(client.sync_interval_ms().unwrap_or(1_000).max(20)); - let inner = Arc::new(BridgeInner { - sync: Mutex::new(()), - client: Mutex::new(client), - state: Mutex::new(BridgeState::new(directive)), - cancellation, - changed: Notify::new(), - }); - sync_once(&inner)?; - let stop = CancellationToken::new(); - let loop_stop = stop.clone(); - let loop_inner = Arc::clone(&inner); - let sync_task = tokio::spawn(async move { - let mut ticker = - tokio::time::interval_at(tokio::time::Instant::now() + interval, interval); - ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); - loop { - tokio::select! { - _ = loop_stop.cancelled() => break, - _ = ticker.tick() => { - if let Err(error) = sync_once(&loop_inner) { - push_warning(&loop_inner, format!("AgentCtl sync failed: {error:#}")); - } - } - } - } - }); - - Ok(Self { - inner, - stop, - sync_task: Some(sync_task), - }) - } - - /// Mark the client active if pVisor currently permits work. - pub fn set_active(&self) -> anyhow::Result<()> { - let mut state = lock(&self.inner.state); - if !state.accepting_work { - bail!("pVisor is not accepting Agent work"); - } - state.agent_state = AgentState::Active; - self.inner.changed.notify_waiters(); - Ok(()) - } - - /// Mark the client idle, or quiesced when a checkpoint is already pending. - pub fn set_idle(&self) { - let mut state = lock(&self.inner.state); - state.agent_state = match &state.directive { - AgentDirective::Quiesce { checkpoint_id, .. } => AgentState::Quiesced { - checkpoint_id: checkpoint_id.clone(), - }, - AgentDirective::Continue | AgentDirective::Shutdown { .. } => AgentState::Idle, - }; - self.inner.changed.notify_waiters(); - } - - /// Return the most recently observed pVisor directive. - pub fn directive(&self) -> AgentDirective { - lock(&self.inner.state).directive.clone() - } - - /// Enter an idle safe point and wait for a pending checkpoint to release it. - pub async fn finish(mut self) -> Vec { - self.set_idle(); - if let Err(error) = sync_once(&self.inner) { - push_warning( - &self.inner, - format!("final AgentCtl sync failed: {error:#}"), - ); - } - - loop { - let wait_until = { - let state = lock(&self.inner.state); - if !matches!(state.agent_state, AgentState::Quiesced { .. }) { - break; - } - state.quiesce_deadline_unix_ms - }; - let notified = self.inner.changed.notified(); - if let Some(deadline) = wait_until { - let now = unix_now_ms(); - if now >= deadline.saturating_add(1_000) { - push_warning( - &self.inner, - "checkpoint Continue was not observed before its deadline".into(), - ); - break; - } - let remaining = Duration::from_millis(deadline.saturating_add(1_000) - now); - let _ = tokio::time::timeout(remaining, notified).await; - } else { - let _ = tokio::time::timeout(Duration::from_secs(5), notified).await; - } - if let Err(error) = sync_once(&self.inner) { - push_warning(&self.inner, format!("AgentCtl sync failed: {error:#}")); - } - } - - self.stop.cancel(); - if let Some(sync_task) = self.sync_task.take() { - let _ = sync_task.await; - } - lock(&self.inner.state).warnings.clone() - } - - /// Return the bridge's small diagnostic state. - pub fn snapshot(&self) -> BTreeMap { - let state = lock(&self.inner.state); - BTreeMap::from([ - ("state".into(), serde_json::json!(state.agent_state)), - ("directive".into(), serde_json::json!(state.directive)), - ]) - } -} - -impl Drop for PilotRuntimeBridge { - fn drop(&mut self) { - self.stop.cancel(); - } -} - -fn sync_once(inner: &Arc) -> anyhow::Result<()> { - // The ticker and lifecycle calls may request synchronization concurrently. - // Serialize the complete snapshot/exchange/apply sequence so an older - // response can never overwrite a newer directive. - let _sync = lock(&inner.sync); - let agent_state = lock(&inner.state).agent_state.clone(); - let directive = lock(&inner.client).sync(agent_state)?; - let immediate_sync = { - let mut state = lock(&inner.state); - apply_directive(&mut state, directive) - }; - if matches!( - lock(&inner.state).directive, - AgentDirective::Shutdown { .. } - ) { - inner.cancellation.cancel(); - } - - if immediate_sync { - let agent_state = lock(&inner.state).agent_state.clone(); - let directive = lock(&inner.client).sync(agent_state)?; - let mut state = lock(&inner.state); - apply_directive(&mut state, directive); - if matches!(state.directive, AgentDirective::Shutdown { .. }) { - inner.cancellation.cancel(); - } - } - inner.changed.notify_waiters(); - Ok(()) -} - -/// Apply a directive and return whether the new quiesced state needs immediate Sync. -fn apply_directive(state: &mut BridgeState, directive: AgentDirective) -> bool { - let immediate_sync = match &directive { - AgentDirective::Continue => { - state.accepting_work = true; - state.quiesce_deadline_unix_ms = None; - if matches!(state.agent_state, AgentState::Quiesced { .. }) { - state.agent_state = AgentState::Idle; - } - false - } - AgentDirective::Shutdown { .. } => { - state.accepting_work = false; - false - } - AgentDirective::Quiesce { - checkpoint_id, - deadline_unix_ms, - } => { - state.accepting_work = false; - state.quiesce_deadline_unix_ms = *deadline_unix_ms; - let reached_safe_point = matches!(state.agent_state, AgentState::Idle) - || matches!( - &state.agent_state, - AgentState::Quiesced { - checkpoint_id: current - } if current != checkpoint_id - ); - if reached_safe_point { - state.agent_state = AgentState::Quiesced { - checkpoint_id: checkpoint_id.clone(), - }; - true - } else { - false - } - } - }; - state.directive = directive; - immediate_sync -} - -fn push_warning(inner: &Arc, warning: String) { - lock(&inner.state).warnings.push(warning); -} - -fn lock(mutex: &Mutex) -> MutexGuard<'_, T> { - mutex - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn idle_client_becomes_quiesced_for_the_requested_checkpoint() { - let mut state = BridgeState::new(AgentDirective::Continue); - state.agent_state = AgentState::Idle; - - assert!(apply_directive( - &mut state, - AgentDirective::Quiesce { - checkpoint_id: "cp".into(), - deadline_unix_ms: None, - }, - )); - assert_eq!( - state.agent_state, - AgentState::Quiesced { - checkpoint_id: "cp".into() - } - ); - assert!(!state.accepting_work); - } - - #[test] - fn active_client_drains_before_reporting_quiesced() { - let mut state = BridgeState::new(AgentDirective::Continue); - - assert!(!apply_directive( - &mut state, - AgentDirective::Quiesce { - checkpoint_id: "cp".into(), - deadline_unix_ms: Some(10), - }, - )); - assert_eq!(state.agent_state, AgentState::Active); - assert!(!state.accepting_work); - } - - #[test] - fn continue_releases_a_quiesced_client_to_idle() { - let mut state = BridgeState::new(AgentDirective::Quiesce { - checkpoint_id: "cp".into(), - deadline_unix_ms: None, - }); - state.agent_state = AgentState::Quiesced { - checkpoint_id: "cp".into(), - }; - - assert!(!apply_directive(&mut state, AgentDirective::Continue)); - assert_eq!(state.agent_state, AgentState::Idle); - assert!(state.accepting_work); - } - - #[test] - fn quiesced_client_rebinds_to_a_back_to_back_checkpoint() { - let mut state = BridgeState::new(AgentDirective::Quiesce { - checkpoint_id: "cp-1".into(), - deadline_unix_ms: None, - }); - state.agent_state = AgentState::Quiesced { - checkpoint_id: "cp-1".into(), - }; - - assert!(apply_directive( - &mut state, - AgentDirective::Quiesce { - checkpoint_id: "cp-2".into(), - deadline_unix_ms: None, - }, - )); - assert_eq!( - state.agent_state, - AgentState::Quiesced { - checkpoint_id: "cp-2".into() - } - ); - } - - #[test] - fn shutdown_disables_work_admission() { - let mut state = BridgeState::new(AgentDirective::Continue); - - assert!(!apply_directive( - &mut state, - AgentDirective::Shutdown { - reason: Some("done".into()), - }, - )); - assert!(!state.accepting_work); - } -} diff --git a/crates/persisting-ppilot/src/scheduler.rs b/crates/persisting-ppilot/src/scheduler.rs deleted file mode 100644 index ff57bd20a..000000000 --- a/crates/persisting-ppilot/src/scheduler.rs +++ /dev/null @@ -1,458 +0,0 @@ -//! pPilot task placement: least-loaded slots with optional sticky preference. -//! -//! **Primitive:** [`Scheduler`] · [`SlotGuard`] · [`WorkerPool`]. -//! -//! When the runtime flattens `--per-worker` into one actor+host per slot, -//! construct with `Scheduler::new(n_slots, 1)`. Slot-major pool ordering -//! (see [`crate::dist::DistEnv::slot_names`]) keeps least-loaded spreading -//! across logical workers before filling a second slot on the same worker. -//! -//! Consecutive infra ask failures quarantine a slot for the rest of the job -//! so placement skips known-bad hosts. - -use pulsing_actor::prelude::ActorRef; -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; -use std::sync::{Arc, RwLock}; -use tokio::sync::Notify; - -pub type WorkerPool = Arc>>; - -/// Consecutive infra failures before a slot is quarantined (job-local). -pub const DEFAULT_QUARANTINE_AFTER: usize = 3; - -/// Shared placement state for one fleet run. -pub struct Scheduler { - loads: Vec, - /// Max concurrent Executes per worker (typically 1 for long/uneven tasks). - per_worker: usize, - notify: Notify, - consecutive_failures: Vec, - quarantined: Vec, - quarantine_after: usize, -} - -impl Scheduler { - pub fn new(n_workers: usize, per_worker: usize) -> Arc { - Self::with_quarantine_after(n_workers, per_worker, DEFAULT_QUARANTINE_AFTER) - } - - pub fn with_quarantine_after( - n_workers: usize, - per_worker: usize, - quarantine_after: usize, - ) -> Arc { - let per_worker = per_worker.max(1); - let n_workers = n_workers.max(1); - let quarantine_after = quarantine_after.max(1); - Arc::new(Self { - loads: (0..n_workers).map(|_| AtomicUsize::new(0)).collect(), - per_worker, - notify: Notify::new(), - consecutive_failures: (0..n_workers).map(|_| AtomicUsize::new(0)).collect(), - quarantined: (0..n_workers).map(|_| AtomicBool::new(false)).collect(), - quarantine_after, - }) - } - - pub fn worker_count(&self) -> usize { - self.loads.len() - } - - pub fn per_worker(&self) -> usize { - self.per_worker - } - - pub fn capacity(&self) -> usize { - self.active_slots().saturating_mul(self.per_worker).max(1) - } - - /// Slots not quarantined. - pub fn active_slots(&self) -> usize { - self.quarantined - .iter() - .filter(|q| !q.load(Ordering::Acquire)) - .count() - } - - pub fn is_quarantined(&self, index: usize) -> bool { - self.quarantined - .get(index) - .map(|q| q.load(Ordering::Acquire)) - .unwrap_or(true) - } - - /// Record a successful ask — clears consecutive failure streak. - pub fn note_success(&self, index: usize) { - if index < self.consecutive_failures.len() { - self.consecutive_failures[index].store(0, Ordering::Release); - } - } - - /// Record an infra ask failure; may quarantine the slot. - pub fn note_failure(&self, index: usize) { - if index >= self.consecutive_failures.len() { - return; - } - let n = self.consecutive_failures[index].fetch_add(1, Ordering::AcqRel) + 1; - if n >= self.quarantine_after && !self.quarantined[index].swap(true, Ordering::AcqRel) { - tracing::warn!( - slot = index, - failures = n, - "quarantining slot after consecutive infra failures" - ); - self.notify.notify_waiters(); - } - } - - /// Immediately quarantine a slot (DeathWatch / explicit drop). - pub fn force_quarantine(&self, index: usize) { - if index >= self.quarantined.len() { - return; - } - if !self.quarantined[index].swap(true, Ordering::AcqRel) { - tracing::warn!(slot = index, "quarantining slot (forced)"); - self.notify.notify_waiters(); - } - } - - /// Reserve a worker slot (least in-flight among those under capacity). - pub async fn acquire(&self) -> Result { - loop { - if let Some(i) = self.try_acquire() { - return Ok(i); - } - if self.active_slots() == 0 { - tracing::error!("all slots quarantined; placement fail-fast"); - return Err(AcquireError::AllQuarantined); - } - self.notify.notified().await; - } - } - - fn slot_usable(&self, i: usize, cur: usize) -> bool { - !self.quarantined[i].load(Ordering::Acquire) && cur < self.per_worker - } - - fn try_acquire(&self) -> Option { - loop { - let mut best: Option<(usize, usize)> = None; - for (i, cell) in self.loads.iter().enumerate() { - let cur = cell.load(Ordering::Acquire); - if !self.slot_usable(i, cur) { - continue; - } - match best { - None => best = Some((i, cur)), - Some((_, b)) if cur < b => best = Some((i, cur)), - Some((bi, b)) if cur == b && i < bi => best = Some((i, cur)), - _ => {} - } - } - let (i, expect) = best?; - match self.loads[i].compare_exchange( - expect, - expect + 1, - Ordering::AcqRel, - Ordering::Acquire, - ) { - Ok(_) => return Some(i), - Err(_) => continue, - } - } - } - - /// Prefer `prefer` when that slot still has capacity; otherwise least-loaded. - pub async fn acquire_prefer(&self, prefer: Option) -> Result { - loop { - if let Some(i) = prefer - && self.try_acquire_index(i) - { - return Ok(i); - } - if let Some(i) = self.try_acquire() { - return Ok(i); - } - if self.active_slots() == 0 { - tracing::error!("all slots quarantined; placement fail-fast"); - return Err(AcquireError::AllQuarantined); - } - self.notify.notified().await; - } - } - - /// Acquire **only** `slot` (sticky-after-contact). Err if quarantined. - pub async fn acquire_sticky(&self, slot: usize) -> Result { - loop { - if self.is_quarantined(slot) { - return Err(StickyLost::Quarantined(slot)); - } - if self.try_acquire_index(slot) { - return Ok(slot); - } - self.notify.notified().await; - } - } - - fn try_acquire_index(&self, index: usize) -> bool { - if index >= self.loads.len() || self.quarantined[index].load(Ordering::Acquire) { - return false; - } - loop { - let cur = self.loads[index].load(Ordering::Acquire); - if cur >= self.per_worker { - return false; - } - match self.loads[index].compare_exchange( - cur, - cur + 1, - Ordering::AcqRel, - Ordering::Acquire, - ) { - Ok(_) => return true, - Err(_) => continue, - } - } - } - - pub fn release(&self, index: usize) { - if index >= self.loads.len() { - return; - } - let prev = self.loads[index].fetch_sub(1, Ordering::AcqRel); - debug_assert!(prev > 0, "scheduler release underflow"); - self.notify.notify_waiters(); - } - - pub fn load_snapshot(&self) -> Vec { - self.loads - .iter() - .map(|c| c.load(Ordering::Relaxed)) - .collect() - } - - pub fn quarantine_snapshot(&self) -> Vec { - self.quarantined - .iter() - .map(|q| q.load(Ordering::Relaxed)) - .collect() - } -} - -/// RAII guard that releases a scheduler slot when dropped. -pub struct SlotGuard { - sched: Arc, - index: usize, -} - -impl SlotGuard { - pub fn index(&self) -> usize { - self.index - } -} - -impl Drop for SlotGuard { - fn drop(&mut self) { - self.sched.release(self.index); - } -} - -impl Scheduler { - pub async fn acquire_guard(self: &Arc) -> Result { - let index = self.acquire().await?; - Ok(SlotGuard { - sched: Arc::clone(self), - index, - }) - } - - pub async fn acquire_guard_prefer( - self: &Arc, - prefer: Option, - ) -> Result { - let index = self.acquire_prefer(prefer).await?; - Ok(SlotGuard { - sched: Arc::clone(self), - index, - }) - } - - pub async fn acquire_guard_sticky( - self: &Arc, - slot: usize, - ) -> Result { - let index = self.acquire_sticky(slot).await?; - Ok(SlotGuard { - sched: Arc::clone(self), - index, - }) - } -} - -/// No usable slots remain (all quarantined). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum AcquireError { - AllQuarantined, -} - -impl std::fmt::Display for AcquireError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - AcquireError::AllQuarantined => write!(f, "all worker slots quarantined"), - } - } -} - -/// Sticky-after-contact placement cannot continue on this slot. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum StickyLost { - Quarantined(usize), -} - -impl std::fmt::Display for StickyLost { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - StickyLost::Quarantined(s) => { - write!( - f, - "sticky slot {s} quarantined (refuse cross-slot re-execute)" - ) - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn prefers_idle_worker() { - let s = Scheduler::new(3, 2); - let a = s.acquire().await.unwrap(); - assert_eq!(a, 0); - let b = s.acquire().await.unwrap(); - assert_eq!(b, 1); - let c = s.acquire().await.unwrap(); - assert_eq!(c, 2); - let d = s.acquire().await.unwrap(); - assert_eq!(d, 0); - s.release(b); - let e = s.acquire().await.unwrap(); - assert_eq!(e, 1); - s.release(a); - s.release(c); - s.release(d); - s.release(e); - assert_eq!(s.load_snapshot(), vec![0, 0, 0]); - } - - #[tokio::test] - async fn acquire_prefer_sticky_when_free() { - let s = Scheduler::new(3, 1); - let a = s.acquire_prefer(None).await.unwrap(); - assert_eq!(a, 0); - s.release(a); - let b = s.acquire_prefer(Some(2)).await.unwrap(); - assert_eq!(b, 2); - s.release(b); - } - - #[tokio::test] - async fn blocks_when_full_then_unblocks() { - let s = Scheduler::new(1, 1); - let g = s.acquire_guard().await.unwrap(); - let s2 = Arc::clone(&s); - let handle = tokio::spawn(async move { s2.acquire().await }); - tokio::task::yield_now().await; - assert!(!handle.is_finished()); - drop(g); - let idx = handle.await.unwrap().unwrap(); - assert_eq!(idx, 0); - s.release(idx); - } - - #[tokio::test] - async fn flat_pool_first_wave_spreads_across_slot0() { - let s = Scheduler::new(6, 1); - let mut got = Vec::new(); - for _ in 0..3 { - got.push(s.acquire().await.unwrap()); - } - assert_eq!(got, vec![0, 1, 2]); - for i in got { - s.release(i); - } - } - - #[tokio::test] - async fn concurrent_acquire_fills_distinct_slots() { - let s = Arc::clone(&Scheduler::new(4, 1)); - let mut handles = Vec::new(); - for _ in 0..4 { - let s2 = Arc::clone(&s); - handles.push(tokio::spawn(async move { s2.acquire().await })); - } - let mut got: Vec = futures::future::join_all(handles) - .await - .into_iter() - .map(|r| r.unwrap().unwrap()) - .collect(); - got.sort(); - assert_eq!(got, vec![0, 1, 2, 3]); - for i in got { - s.release(i); - } - } - - #[tokio::test] - async fn quarantine_skips_slot_on_acquire() { - let s = Scheduler::with_quarantine_after(2, 1, 2); - s.note_failure(0); - s.note_failure(0); - assert!(s.is_quarantined(0)); - let g = s.acquire_guard().await.unwrap(); - assert_eq!(g.index(), 1); - drop(g); - let g2 = s.acquire_guard_prefer(Some(0)).await.unwrap(); - assert_eq!(g2.index(), 1); - } - - #[tokio::test] - async fn acquire_sticky_errors_when_quarantined() { - let s = Scheduler::new(2, 1); - s.force_quarantine(0); - assert!(matches!( - s.acquire_sticky(0).await, - Err(StickyLost::Quarantined(0)) - )); - assert_eq!(s.acquire_sticky(1).await.unwrap(), 1); - s.release(1); - } - - #[tokio::test] - async fn all_quarantined_acquire_fail_fast() { - let s = Scheduler::with_quarantine_after(2, 1, 1); - s.note_failure(0); - s.note_failure(1); - assert_eq!(s.active_slots(), 0); - assert!(matches!( - s.acquire().await, - Err(AcquireError::AllQuarantined) - )); - assert!(matches!( - s.acquire_prefer(Some(0)).await, - Err(AcquireError::AllQuarantined) - )); - } - - #[tokio::test] - async fn success_resets_failure_streak() { - let s = Scheduler::with_quarantine_after(1, 1, 3); - s.note_failure(0); - s.note_failure(0); - s.note_success(0); - s.note_failure(0); - s.note_failure(0); - assert!(!s.is_quarantined(0)); - } -} diff --git a/crates/persisting-ppilot/src/sink.rs b/crates/persisting-ppilot/src/sink.rs deleted file mode 100644 index 566bf3743..000000000 --- a/crates/persisting-ppilot/src/sink.rs +++ /dev/null @@ -1,291 +0,0 @@ -//! Unique control-plane sink: ready results append here (not from Executors). -//! -//! **Primitive:** [`ResultSink`] · [`persist_terminal`] · [`JsonlFileSink`] (task_id dedup). -//! -//! - Phase-1 ledger: append-only JSONL under `--sink` (`task_id` for `--resume`). -//! - Optional L1: feature `traj-sink` → [`crate::sink_traj::LanceResultSink`] via Tee. - -use crate::checkpoint::CheckpointLedger; -use crate::task::TaskResult; -use anyhow::{Context, Result}; -use async_trait::async_trait; -use std::collections::HashSet; -use std::path::{Path, PathBuf}; -use std::sync::Mutex; -use tokio::fs::OpenOptions; -use tokio::io::AsyncWriteExt; - -#[async_trait] -pub trait ResultSink: Send + Sync { - /// Ready / terminal result that belongs in the asset ledger. - async fn append_ready(&self, result: &TaskResult) -> Result<()>; - /// Side ledger (infra / execute failure). Must not mix into training pool. - async fn append_failure(&self, result: &TaskResult) -> Result<()>; -} - -/// Append-only JSONL under `{root}/ready.ndjson` + `{root}/failures.ndjson`. -/// -/// Idempotent by `task_id`: seeds from existing files on open, then skips duplicates. -/// `seen` is reserved before write and **rolled back** if the durable append fails, -/// so a failed write never permanently blocks a later retry of the same id. -pub struct JsonlFileSink { - root: PathBuf, - seen_ready: Mutex>, - seen_fail: Mutex>, -} - -impl JsonlFileSink { - pub async fn open(root: impl Into) -> Result { - let root = root.into(); - tokio::fs::create_dir_all(&root) - .await - .with_context(|| format!("mkdir sink {}", root.display()))?; - // Seed from disk so infra-retry / crash-resume duplicates do not re-append. - let ledger = CheckpointLedger::load(&root).await?; - Ok(Self { - root, - seen_ready: Mutex::new(ledger.ready), - seen_fail: Mutex::new(ledger.failed), - }) - } - - pub fn root(&self) -> &Path { - &self.root - } - - async fn append_line(&self, file: &str, result: &TaskResult) -> Result<()> { - let path = self.root.join(file); - let line = result.to_ndjson()?; - let mut f = OpenOptions::new() - .create(true) - .append(true) - .open(&path) - .await - .with_context(|| format!("open {}", path.display()))?; - f.write_all(line.as_bytes()).await?; - f.write_all(b"\n").await?; - f.flush().await?; - Ok(()) - } - - /// Reserve `task_id` in `seen`. Returns `false` if already present (caller skips). - fn reserve(seen: &Mutex>, task_id: &str) -> Result { - let mut g = seen - .lock() - .map_err(|_| anyhow::anyhow!("sink lock poisoned"))?; - Ok(g.insert(task_id.to_string())) - } - - fn unreserve(seen: &Mutex>, task_id: &str) { - if let Ok(mut g) = seen.lock() { - g.remove(task_id); - } - } -} - -#[async_trait] -impl ResultSink for JsonlFileSink { - async fn append_ready(&self, result: &TaskResult) -> Result<()> { - if !Self::reserve(&self.seen_ready, &result.task_id)? { - return Ok(()); - } - match self.append_line("ready.ndjson", result).await { - Ok(()) => Ok(()), - Err(e) => { - Self::unreserve(&self.seen_ready, &result.task_id); - Err(e) - } - } - } - - async fn append_failure(&self, result: &TaskResult) -> Result<()> { - if !Self::reserve(&self.seen_fail, &result.task_id)? { - return Ok(()); - } - match self.append_line("failures.ndjson", result).await { - Ok(()) => Ok(()), - Err(e) => { - Self::unreserve(&self.seen_fail, &result.task_id); - Err(e) - } - } - } -} - -/// Fan-out to several sinks (e.g. stdout view + durable JSONL). -pub struct TeeSink { - sinks: Vec>, -} - -impl TeeSink { - pub fn new(sinks: Vec>) -> Self { - Self { sinks } - } -} - -#[async_trait] -impl ResultSink for TeeSink { - async fn append_ready(&self, result: &TaskResult) -> Result<()> { - let mut first_err: Option = None; - for s in &self.sinks { - if let Err(e) = s.append_ready(result).await { - tracing::error!( - task_id = %result.task_id, - error = %e, - "tee sink append_ready failed (continuing siblings)" - ); - if first_err.is_none() { - first_err = Some(e); - } - } - } - match first_err { - Some(e) => Err(e), - None => Ok(()), - } - } - - async fn append_failure(&self, result: &TaskResult) -> Result<()> { - let mut first_err: Option = None; - for s in &self.sinks { - if let Err(e) = s.append_failure(result).await { - tracing::error!( - task_id = %result.task_id, - error = %e, - "tee sink append_failure failed (continuing siblings)" - ); - if first_err.is_none() { - first_err = Some(e); - } - } - } - match first_err { - Some(e) => Err(e), - None => Ok(()), - } - } -} - -/// Route by result status: cancelled/failed → failure ledger; ok → ready. -pub async fn persist_terminal(sink: &dyn ResultSink, result: &TaskResult) -> Result<()> { - if result.ok && !result.cancelled { - sink.append_ready(result).await - } else { - sink.append_failure(result).await - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - use tokio::io::AsyncWriteExt; - - #[tokio::test] - async fn open_seeds_seen_and_skips_duplicate_append() { - let dir = tempfile::tempdir().unwrap(); - let root = dir.path(); - let mut ready = tokio::fs::File::create(root.join("ready.ndjson")) - .await - .unwrap(); - ready - .write_all(br#"{"task_id":"t-0","ok":true}"#) - .await - .unwrap(); - ready.write_all(b"\n").await.unwrap(); - - let sink = JsonlFileSink::open(root).await.unwrap(); - let dup = TaskResult::success("t-0", json!({"x": 1}), "w0", 0.0); - sink.append_ready(&dup).await.unwrap(); - - let body = tokio::fs::read_to_string(root.join("ready.ndjson")) - .await - .unwrap(); - assert_eq!(body.lines().count(), 1, "duplicate must not append: {body}"); - } - - #[tokio::test] - async fn persist_terminal_routes_ok_and_fail() { - let dir = tempfile::tempdir().unwrap(); - let sink = JsonlFileSink::open(dir.path()).await.unwrap(); - persist_terminal(&sink, &TaskResult::success("ok-1", json!(1), "w0", 0.0)) - .await - .unwrap(); - persist_terminal(&sink, &TaskResult::cancelled("c-1")) - .await - .unwrap(); - persist_terminal(&sink, &TaskResult::failure("f-1", "e", None, "w0", 0.0)) - .await - .unwrap(); - let ready = tokio::fs::read_to_string(dir.path().join("ready.ndjson")) - .await - .unwrap(); - let fail = tokio::fs::read_to_string(dir.path().join("failures.ndjson")) - .await - .unwrap(); - assert!(ready.contains("ok-1")); - assert!(fail.contains("c-1") && fail.contains("f-1")); - assert!(!ready.contains("c-1")); - } - - #[tokio::test] - async fn tee_fanout_to_jsonl() { - let dir = tempfile::tempdir().unwrap(); - let a = JsonlFileSink::open(dir.path().join("a")).await.unwrap(); - let b = JsonlFileSink::open(dir.path().join("b")).await.unwrap(); - let tee = TeeSink::new(vec![Box::new(a), Box::new(b)]); - persist_terminal(&tee, &TaskResult::success("t", json!(1), "w0", 0.0)) - .await - .unwrap(); - for sub in ["a", "b"] { - let body = tokio::fs::read_to_string(dir.path().join(sub).join("ready.ndjson")) - .await - .unwrap(); - assert!(body.contains("\"task_id\":\"t\"")); - } - } - - #[tokio::test] - async fn reopen_skips_duplicate_ready_from_disk() { - let dir = tempfile::tempdir().unwrap(); - { - let sink = JsonlFileSink::open(dir.path()).await.unwrap(); - persist_terminal(&sink, &TaskResult::success("t-0", json!(1), "w0", 0.0)) - .await - .unwrap(); - } - let sink = JsonlFileSink::open(dir.path()).await.unwrap(); - persist_terminal(&sink, &TaskResult::success("t-0", json!(99), "w0", 0.0)) - .await - .unwrap(); - let body = tokio::fs::read_to_string(dir.path().join("ready.ndjson")) - .await - .unwrap(); - assert_eq!(body.lines().count(), 1); - } - - #[tokio::test] - async fn write_failure_unreserves_seen_so_retry_can_persist() { - // P0 regression: seen-before-write must not permanently drop a task_id. - let dir = tempfile::tempdir().unwrap(); - let sink = JsonlFileSink::open(dir.path()).await.unwrap(); - // Make ready.ndjson a directory so OpenOptions::open fails. - tokio::fs::create_dir(dir.path().join("ready.ndjson")) - .await - .unwrap(); - let r = TaskResult::success("t-stuck", json!(1), "w0", 0.0); - assert!( - sink.append_ready(&r).await.is_err(), - "first append must fail" - ); - tokio::fs::remove_dir(dir.path().join("ready.ndjson")) - .await - .unwrap(); - sink.append_ready(&r).await.expect("retry after fix path"); - let body = tokio::fs::read_to_string(dir.path().join("ready.ndjson")) - .await - .unwrap(); - assert_eq!(body.lines().count(), 1); - assert!(body.contains("t-stuck")); - } -} diff --git a/crates/persisting-ppilot/src/sink_traj.rs b/crates/persisting-ppilot/src/sink_traj.rs deleted file mode 100644 index d72b24eec..000000000 --- a/crates/persisting-ppilot/src/sink_traj.rs +++ /dev/null @@ -1,147 +0,0 @@ -//! pChronicle trajectory sink: TaskResult → EventRecord → TrajectoryAppend. -//! -//! Enabled with feature `traj-sink`. Always Tee with [`JsonlFileSink`] so -//! `--resume` keeps using the JSONL task_id ledger. - -use crate::sink::ResultSink; -use crate::task::TaskResult; -use anyhow::{Context, Result}; -use async_trait::async_trait; -use persisting_events::{ChronicleControl, EventIdentity, EventRecord, TrajectoryAppendRequest}; -use std::collections::HashSet; -use std::sync::Arc; -use std::sync::Mutex; - -/// Append terminal pPilot results as `ppilot.result` / `ppilot.failure` events. -pub struct LanceResultSink { - control: Arc, - storage: String, - agent_id: String, - session_id: String, - seen: Mutex>, -} - -impl LanceResultSink { - pub fn new( - control: Arc, - storage: impl Into, - agent_id: impl Into, - session_id: impl Into, - ) -> Self { - Self { - control, - storage: storage.into(), - agent_id: agent_id.into(), - session_id: session_id.into(), - seen: Mutex::new(HashSet::new()), - } - } - - pub fn storage(&self) -> &str { - &self.storage - } - - pub fn agent_id(&self) -> &str { - &self.agent_id - } - - pub fn session_id(&self) -> &str { - &self.session_id - } - - /// Seed dedup set (e.g. from JSONL ledger) so re-appends skip known `task_id`s. - pub fn seed_seen(&self, ids: impl IntoIterator) { - if let Ok(mut seen) = self.seen.lock() { - seen.extend(ids); - } - } - - fn to_record(&self, result: &TaskResult) -> Result { - let kind = if result.ok && !result.cancelled { - "ppilot.result" - } else { - "ppilot.failure" - }; - let payload = serde_json::to_value(result).context("TaskResult to JSON")?; - Ok(EventRecord { - identity: EventIdentity { - event_id: Some(format!("event-{}", uuid::Uuid::new_v4())), - run_id: result.run_id.clone(), - attempt_id: result.attempt_id.clone(), - timestamp_unix_ms: Some((chrono::Utc::now().timestamp_millis()).max(0) as u64), - producer: Some("persisting-ppilot".into()), - ..EventIdentity::default() - }, - seq: 0, - source: "persisting-ppilot".into(), - kind: kind.into(), - timestamp: Some(chrono::Utc::now().to_rfc3339()), - session_id: Some(self.session_id.clone()), - agent_id: Some(self.agent_id.clone()), - parent_uuid: None, - trace_id: None, - call_id: Some(result.task_id.clone()), - subagent_id: None, - parent_agent_id: None, - branch: None, - parent_call_id: None, - payload, - }) - } - - async fn append_record(&self, result: &TaskResult) -> Result<()> { - { - let mut seen = self - .seen - .lock() - .map_err(|_| anyhow::anyhow!("lance sink lock poisoned"))?; - if !seen.insert(result.task_id.clone()) { - return Ok(()); - } - } - let write = async { - let rec = self.to_record(result)?; - let req = TrajectoryAppendRequest { - storage: self.storage.clone(), - agent_id: self.agent_id.clone(), - session_id: self.session_id.clone(), - format: Default::default(), - root_session_id: None, - records: vec![rec], - }; - let resp = self - .control - .append_trajectory(req) - .await - .context("trajectory_append")?; - tracing::debug!( - task_id = %result.task_id, - accepted = resp.accepted_records, - dataset = %resp.dataset, - "pPilot result appended to lance" - ); - Ok(()) - }; - match write.await { - Ok(()) => Ok(()), - Err(e) => { - // Roll back reservation so a later retry is not permanently skipped. - if let Ok(mut seen) = self.seen.lock() { - seen.remove(&result.task_id); - } - Err(e) - } - } - } -} - -#[async_trait] -impl ResultSink for LanceResultSink { - async fn append_ready(&self, result: &TaskResult) -> Result<()> { - self.append_record(result).await - } - - async fn append_failure(&self, result: &TaskResult) -> Result<()> { - self.append_record(result).await - } -} diff --git a/crates/persisting-ppilot/src/sink_writer.rs b/crates/persisting-ppilot/src/sink_writer.rs deleted file mode 100644 index ae3a0d8d8..000000000 --- a/crates/persisting-ppilot/src/sink_writer.rs +++ /dev/null @@ -1,226 +0,0 @@ -//! Bounded async sink writer — keeps Driver `on_result` off the disk path. -//! -//! Completions are enqueued via async `send` (natural backpressure — no -//! `block_in_place`). A single background task runs [`persist_terminal`] + -//! checkpoint notes. Persist failures are counted and surface from -//! [`SinkWriterHandle::join`]; the task id is unclaimed from [`SkipSet`] so a -//! later `--resume` can rediscover work that never hit durable storage. - -use crate::checkpoint::CheckpointTracker; -use crate::coordination::RunCoordinator; -use crate::sink::{ResultSink, persist_terminal}; -use crate::skip::SkipSet; -use crate::task::TaskResult; -use anyhow::{Context, Result, bail}; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex}; -use tokio::sync::mpsc; -use tokio::task::JoinHandle; - -/// Cloneable enqueue handle for Driver (awaited on the completion path). -#[derive(Clone)] -pub struct SinkSubmitter { - tx: mpsc::Sender, -} - -impl SinkSubmitter { - /// Async enqueue — when the bound is full, waits (back-pressures Driver). - pub async fn submit(&self, result: TaskResult) -> Result<()> { - self.tx - .send(result) - .await - .map_err(|_| anyhow::anyhow!("sink writer closed; cannot enqueue")) - } -} - -#[derive(Default)] -struct PersistErrors { - count: AtomicUsize, - first: Mutex>, -} - -impl PersistErrors { - fn note(&self, task_id: &str, err: impl std::fmt::Display) { - let prev = self.count.fetch_add(1, Ordering::AcqRel); - if prev == 0 - && let Ok(mut g) = self.first.lock() - { - *g = Some(format!("{task_id}: {err}")); - } - } - - fn count(&self) -> usize { - self.count.load(Ordering::Acquire) - } - - fn first_msg(&self) -> Option { - self.first.lock().ok().and_then(|g| g.clone()) - } -} - -/// Handle returned by [`spawn_sink_writer`]. Await [`SinkWriterHandle::join`] to drain. -pub struct SinkWriterHandle { - submitter: SinkSubmitter, - join: JoinHandle<()>, - errors: Arc, -} - -impl SinkWriterHandle { - pub fn submitter(&self) -> SinkSubmitter { - self.submitter.clone() - } - - pub async fn submit(&self, result: TaskResult) -> Result<()> { - self.submitter.submit(result).await - } - - /// Drop the queue sender, wait for the writer, then fail if any persist errored. - pub async fn join(self) -> Result<()> { - drop(self.submitter); - self.join.await.context("sink writer task join")?; - let n = self.errors.count(); - if n > 0 { - let first = self.errors.first_msg().unwrap_or_else(|| "unknown".into()); - bail!("sink persist failed for {n} result(s); first: {first}"); - } - Ok(()) - } -} - -/// Spawn a dedicated persist task. `capacity` bounds queued completions. -pub fn spawn_sink_writer( - sink: Arc, - checkpoint: Option>, - skip: Option, - capacity: usize, -) -> SinkWriterHandle { - spawn_sink_writer_inner(sink, checkpoint, skip, capacity, None) -} - -/// Spawn a writer that makes the pChronicle RunCommit authoritative before -/// exposing the result through the user-facing sink. -pub fn spawn_coordinated_sink_writer( - sink: Arc, - checkpoint: Option>, - skip: Option, - capacity: usize, - coordinator: Arc, -) -> SinkWriterHandle { - spawn_sink_writer_inner(sink, checkpoint, skip, capacity, Some(coordinator)) -} - -fn spawn_sink_writer_inner( - sink: Arc, - checkpoint: Option>, - skip: Option, - capacity: usize, - coordinator: Option>, -) -> SinkWriterHandle { - let capacity = capacity.max(1); - let (tx, mut rx) = mpsc::channel::(capacity); - let errors = Arc::new(PersistErrors::default()); - let errors_bg = Arc::clone(&errors); - let join = tokio::spawn(async move { - while let Some(r) = rx.recv().await { - let persisted = match &coordinator { - Some(coordinator) => coordinator.finalize_result(sink.as_ref(), &r).await, - None => persist_terminal(sink.as_ref(), &r).await, - }; - if let Err(e) = persisted { - tracing::error!(task_id = %r.task_id, error = %e, "sink persist failed"); - errors_bg.note(&r.task_id, &e); - // Not durable — allow a future resume / duplicate plan yield to reclaim. - if let Some(skip) = &skip { - skip.remove(&r.task_id); - } - continue; - } - if let Some(ckpt) = &checkpoint { - ckpt.note_terminal(r.ok, r.cancelled); - let _ = ckpt.maybe_flush().await; - } - } - }); - SinkWriterHandle { - submitter: SinkSubmitter { tx }, - join, - errors, - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::sink::JsonlFileSink; - use async_trait::async_trait; - use serde_json::json; - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn writer_persists_and_drains() { - let dir = tempfile::tempdir().unwrap(); - let sink: Arc = Arc::new(JsonlFileSink::open(dir.path()).await.unwrap()); - let w = spawn_sink_writer(Arc::clone(&sink), None, None, 8); - w.submit(TaskResult::success("t-0", json!(1), "w0", 0.0)) - .await - .unwrap(); - w.submit(TaskResult::failure("t-1", "e", None, "w0", 0.0)) - .await - .unwrap(); - w.join().await.unwrap(); - let ready = tokio::fs::read_to_string(dir.path().join("ready.ndjson")) - .await - .unwrap(); - let fail = tokio::fs::read_to_string(dir.path().join("failures.ndjson")) - .await - .unwrap(); - assert!(ready.contains("t-0")); - assert!(fail.contains("t-1")); - } - - struct AlwaysFailSink; - - #[async_trait] - impl ResultSink for AlwaysFailSink { - async fn append_ready(&self, _: &TaskResult) -> Result<()> { - bail!("disk full") - } - async fn append_failure(&self, _: &TaskResult) -> Result<()> { - bail!("disk full") - } - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn persist_failure_surfaces_on_join_and_unclaims_skip() { - let skip: SkipSet = ["t-0".into()].into_iter().collect(); - assert!(skip.contains("t-0")); - let w = spawn_sink_writer(Arc::new(AlwaysFailSink), None, Some(skip.clone()), 4); - w.submit(TaskResult::success("t-0", json!(1), "w0", 0.0)) - .await - .unwrap(); - let err = w.join().await.unwrap_err(); - assert!(err.to_string().contains("sink persist failed")); - assert!( - !skip.contains("t-0"), - "failed persist must unclaim so resume can rediscover" - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn submit_is_async_backpressure_not_block_in_place() { - // Smoke: async send path works under modest load (no block_in_place). - let dir = tempfile::tempdir().unwrap(); - let sink: Arc = Arc::new(JsonlFileSink::open(dir.path()).await.unwrap()); - let w = spawn_sink_writer(Arc::clone(&sink), None, None, 2); - for i in 0..4 { - w.submit(TaskResult::success(format!("t-{i}"), json!(i), "w0", 0.0)) - .await - .unwrap(); - } - // Must not retain submitter clones across join (keeps writer channel open). - w.join().await.unwrap(); - let ready = tokio::fs::read_to_string(dir.path().join("ready.ndjson")) - .await - .unwrap(); - assert_eq!(ready.lines().count(), 4); - } -} diff --git a/crates/persisting-ppilot/src/skip.rs b/crates/persisting-ppilot/src/skip.rs deleted file mode 100644 index 963748ad2..000000000 --- a/crates/persisting-ppilot/src/skip.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! Live skip set: task_ids that must not be dispatched. -//! -//! Seeded from `--resume` (ready ∪ failures) and grown as tasks are claimed / -//! completed in the current job — so mid-run sink persistence and duplicate -//! `plan()` yields do not re-dispatch the same id onto another worker. - -use std::collections::HashSet; -use std::sync::{Arc, Mutex}; - -/// Shared, mutable set of task ids to skip (or already claimed). -#[derive(Clone, Default)] -pub struct SkipSet { - inner: Arc>>, -} - -impl SkipSet { - pub fn new() -> Self { - Self::default() - } - - pub fn contains(&self, id: &str) -> bool { - self.inner.lock().map(|g| g.contains(id)).unwrap_or(false) - } - - /// Insert `id`. Returns `true` if it was newly claimed (caller may dispatch). - pub fn insert(&self, id: impl Into) -> bool { - self.inner - .lock() - .map(|mut g| g.insert(id.into())) - .unwrap_or(false) - } - - /// Drop a claim (e.g. sink persist failed — id was never durable). - pub fn remove(&self, id: &str) -> bool { - self.inner.lock().map(|mut g| g.remove(id)).unwrap_or(false) - } - - pub fn len(&self) -> usize { - self.inner.lock().map(|g| g.len()).unwrap_or(0) - } - - pub fn is_empty(&self) -> bool { - self.len() == 0 - } -} - -impl FromIterator for SkipSet { - fn from_iter>(iter: T) -> Self { - Self { - inner: Arc::new(Mutex::new(iter.into_iter().collect())), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn claim_once() { - let s = SkipSet::new(); - assert!(s.insert("t-0")); - assert!(!s.insert("t-0")); - assert!(s.contains("t-0")); - } - - #[test] - fn from_iter_seeds() { - let s: SkipSet = ["a".into(), "b".into()].into_iter().collect(); - assert!(s.contains("a") && s.contains("b")); - assert!(!s.insert("a")); - } - - #[test] - fn remove_unclaims() { - let s = SkipSet::new(); - assert!(s.insert("t-0")); - assert!(s.remove("t-0")); - assert!(!s.contains("t-0")); - assert!(s.insert("t-0")); - } -} diff --git a/crates/persisting-ppilot/src/supervisor.rs b/crates/persisting-ppilot/src/supervisor.rs deleted file mode 100644 index 7d0a5584f..000000000 --- a/crates/persisting-ppilot/src/supervisor.rs +++ /dev/null @@ -1,410 +0,0 @@ -//! Job-scoped pPilot supervisor embedded into normal orchestration commands. - -use anyhow::{Context, bail}; -use persisting_agentctl::{ - NetworkBandwidthLimit, RunId, SUPERVISOR_PROTOCOL_VERSION, SupervisorBootstrap, - SupervisorClientMessage, SupervisorDirective, SupervisorDirectiveEnvelope, - SupervisorNetworkQuotaGrant, SupervisorServerMessage, -}; -use persisting_events::unix_now_ms; -use std::collections::BTreeMap; -use std::sync::Arc; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::time::Duration; -use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; -use tokio::net::{TcpListener, TcpStream}; -use tokio::sync::{Mutex, mpsc}; -use tokio::task::JoinHandle; -use tokio_util::sync::CancellationToken; - -const MAX_FRAME_BYTES: usize = 64 * 1024; - -#[derive(Debug, Clone)] -pub struct EmbeddedSupervisorConfig { - /// Aggregate job rate divided into conservative fixed shares. - pub network_limit_bytes_per_second: Option, - /// Maximum number of concurrently consuming pVisors. A quota grant receives - /// `network_limit_bytes_per_second / quota_slots`. - pub quota_slots: usize, -} - -impl Default for EmbeddedSupervisorConfig { - fn default() -> Self { - Self { - network_limit_bytes_per_second: None, - quota_slots: 1, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SupervisorRegistrationSnapshot { - pub run_id: RunId, - pub attempt_id: persisting_agentctl::AttemptId, - pub lease_epoch: u64, - pub connected: bool, - pub last_heartbeat_unix_ms: u64, - pub last_applied_directive_seq: u64, -} - -struct LiveRegistration { - snapshot: SupervisorRegistrationSnapshot, - directives: mpsc::Sender, -} - -struct SupervisorState { - token: String, - controller_epoch: u64, - config: EmbeddedSupervisorConfig, - next_directive_seq: AtomicU64, - registrations: Mutex>, -} - -/// Handle owned by one pPilot execution session. -pub struct EmbeddedSupervisor { - bootstrap: SupervisorBootstrap, - state: Arc, - stop: CancellationToken, - join: Option>>, -} - -impl EmbeddedSupervisor { - pub async fn start(config: EmbeddedSupervisorConfig) -> anyhow::Result { - if config.network_limit_bytes_per_second == Some(0) { - bail!("Supervisor network limit must be greater than zero"); - } - if let Some(limit) = config.network_limit_bytes_per_second { - anyhow::ensure!( - config.quota_slots > 0, - "Supervisor quota slots must be non-zero" - ); - anyhow::ensure!( - limit >= config.quota_slots as u64, - "Supervisor network limit is too small for {} quota slots", - config.quota_slots - ); - } - let listener = TcpListener::bind("127.0.0.1:0") - .await - .context("bind embedded pPilot Supervisor")?; - let address = listener.local_addr()?; - let controller_epoch = unix_now_ms().max(1); - let token = uuid::Uuid::new_v4().to_string(); - let state = Arc::new(SupervisorState { - token: token.clone(), - controller_epoch, - config, - next_directive_seq: AtomicU64::new(1), - registrations: Mutex::new(BTreeMap::new()), - }); - let stop = CancellationToken::new(); - let task_state = Arc::clone(&state); - let task_stop = stop.clone(); - let join = tokio::spawn(async move { - loop { - let accepted = tokio::select! { - _ = task_stop.cancelled() => break, - accepted = listener.accept() => accepted, - }; - let (stream, _) = accepted.context("accept pVisor Supervisor connection")?; - let connection_state = Arc::clone(&task_state); - let connection_stop = task_stop.clone(); - tokio::spawn(async move { - if let Err(error) = - handle_connection(stream, connection_state, connection_stop).await - { - tracing::debug!(%error, "pVisor Supervisor session ended"); - } - }); - } - Ok(()) - }); - Ok(Self { - bootstrap: SupervisorBootstrap { - endpoint: format!("tcp://{address}"), - token, - controller_epoch, - connect_timeout_ms: 500, - attempt_registry_uri: None, - attempt_ttl_ms: 15_000, - }, - state, - stop, - join: Some(join), - }) - } - - pub fn bootstrap(&self) -> SupervisorBootstrap { - self.bootstrap.clone() - } - - pub async fn registrations(&self) -> Vec { - self.state - .registrations - .lock() - .await - .values() - .map(|registration| registration.snapshot.clone()) - .collect() - } - - pub async fn cancel(&self, run_id: &RunId) -> anyhow::Result { - let registrations = self.state.registrations.lock().await; - let registration = registrations - .get(run_id) - .ok_or_else(|| anyhow::anyhow!("Run {run_id} is not registered with Supervisor"))?; - let seq = self - .state - .next_directive_seq - .fetch_add(1, Ordering::Relaxed); - registration - .directives - .try_send(SupervisorDirectiveEnvelope { - controller_epoch: self.state.controller_epoch, - lease_epoch: registration.snapshot.lease_epoch, - directive_seq: seq, - directive: SupervisorDirective::Cancel, - }) - .map_err(|_| anyhow::anyhow!("Run {run_id} Supervisor connection is closed"))?; - Ok(seq) - } - - pub async fn shutdown(mut self) -> anyhow::Result<()> { - self.stop.cancel(); - if let Some(join) = self.join.take() { - join.await.context("join embedded pPilot Supervisor")??; - } - Ok(()) - } -} - -impl Drop for EmbeddedSupervisor { - fn drop(&mut self) { - self.stop.cancel(); - } -} - -async fn handle_connection( - stream: TcpStream, - state: Arc, - stop: CancellationToken, -) -> anyhow::Result<()> { - let (read, mut write) = stream.into_split(); - let mut lines = BufReader::new(read).lines(); - let first = tokio::time::timeout(Duration::from_secs(5), lines.next_line()) - .await - .context("pVisor did not register within five seconds")?? - .ok_or_else(|| anyhow::anyhow!("pVisor closed before registration"))?; - if first.len() > MAX_FRAME_BYTES { - bail!("Supervisor registration exceeds {MAX_FRAME_BYTES} bytes"); - } - let message: SupervisorClientMessage = - serde_json::from_str(&first).context("decode pVisor Supervisor registration")?; - let SupervisorClientMessage::Register(registration) = message else { - send_message( - &mut write, - &SupervisorServerMessage::Error { - message: "first Supervisor message must be register".into(), - }, - ) - .await?; - bail!("first Supervisor message was not register"); - }; - if registration.protocol_version != SUPERVISOR_PROTOCOL_VERSION { - bail!( - "unsupported Supervisor protocol {}; expected {}", - registration.protocol_version, - SUPERVISOR_PROTOCOL_VERSION - ); - } - if registration.token != state.token { - send_message( - &mut write, - &SupervisorServerMessage::Error { - message: "Supervisor authentication failed".into(), - }, - ) - .await?; - bail!("Supervisor authentication failed"); - } - - let (directive_tx, mut directive_rx) = mpsc::channel(32); - let mut initial = Vec::new(); - if let Some(bytes_per_second) = state.config.network_limit_bytes_per_second { - let bytes_per_second = bytes_per_second / state.config.quota_slots.max(1) as u64; - let seq = state.next_directive_seq.fetch_add(1, Ordering::Relaxed); - initial.push(SupervisorDirectiveEnvelope { - controller_epoch: state.controller_epoch, - lease_epoch: registration.lease_epoch, - directive_seq: seq, - directive: SupervisorDirective::GrantNetworkQuota(SupervisorNetworkQuotaGrant { - grant_id: format!("{}-{seq}", state.controller_epoch), - quota_epoch: state.controller_epoch, - valid_until_unix_ms: u64::MAX, - limit: NetworkBandwidthLimit { - host: None, - port: None, - bytes_per_second, - }, - }), - }); - } - let run_id = registration.run_id.clone(); - state.registrations.lock().await.insert( - run_id.clone(), - LiveRegistration { - snapshot: SupervisorRegistrationSnapshot { - run_id: registration.run_id.clone(), - attempt_id: registration.attempt_id.clone(), - lease_epoch: registration.lease_epoch, - connected: true, - last_heartbeat_unix_ms: unix_now_ms(), - last_applied_directive_seq: 0, - }, - directives: directive_tx, - }, - ); - send_message( - &mut write, - &SupervisorServerMessage::Registered { - controller_epoch: state.controller_epoch, - directives: initial, - }, - ) - .await?; - - let outcome = loop { - tokio::select! { - _ = stop.cancelled() => break Ok(()), - directive = directive_rx.recv() => { - let Some(directive) = directive else { break Ok(()) }; - send_message(&mut write, &SupervisorServerMessage::Directive(directive)).await?; - } - line = lines.next_line() => { - let Some(line) = line? else { break Ok(()) }; - if line.len() > MAX_FRAME_BYTES { - break Err(anyhow::anyhow!("Supervisor client frame exceeds {MAX_FRAME_BYTES} bytes")); - } - let message: SupervisorClientMessage = serde_json::from_str(&line)?; - let mut registrations = state.registrations.lock().await; - let Some(live) = registrations.get_mut(&run_id) else { continue }; - match message { - SupervisorClientMessage::Heartbeat(heartbeat) => { - live.snapshot.last_heartbeat_unix_ms = unix_now_ms(); - live.snapshot.last_applied_directive_seq = heartbeat.last_applied_directive_seq; - } - SupervisorClientMessage::Ack(ack) => { - if ack.applied { - live.snapshot.last_applied_directive_seq = live - .snapshot - .last_applied_directive_seq - .max(ack.directive_seq); - } - } - SupervisorClientMessage::Register(_) => {} - } - } - } - }; - if let Some(live) = state.registrations.lock().await.get_mut(&run_id) { - live.snapshot.connected = false; - } - outcome -} - -async fn send_message( - write: &mut tokio::net::tcp::OwnedWriteHalf, - message: &SupervisorServerMessage, -) -> anyhow::Result<()> { - let encoded = serde_json::to_vec(message)?; - write.write_all(&encoded).await?; - write.write_all(b"\n").await?; - write.flush().await?; - Ok(()) -} - -/// Parse an explicit network rate. Lowercase `bps` units are bits per second; -/// `B/s` units are bytes per second. -pub fn parse_bandwidth(value: &str) -> Result { - let trimmed = value.trim(); - let lower = trimmed.to_ascii_lowercase(); - let units = [ - ("gbps", 1_000_000_000_u64, true), - ("mbps", 1_000_000, true), - ("kbps", 1_000, true), - ("bps", 1, true), - ("gb/s", 1_000_000_000, false), - ("mb/s", 1_000_000, false), - ("kb/s", 1_000, false), - ("b/s", 1, false), - ]; - for (suffix, multiplier, bits) in units { - if let Some(amount) = lower.strip_suffix(suffix) { - let amount = amount - .trim() - .parse::() - .map_err(|_| format!("invalid bandwidth `{value}`"))?; - let scaled = amount - .checked_mul(multiplier) - .ok_or_else(|| format!("bandwidth `{value}` is too large"))?; - let bytes = if bits { scaled.div_ceil(8) } else { scaled }; - return (bytes > 0) - .then_some(bytes) - .ok_or_else(|| "bandwidth must be greater than zero".into()); - } - } - Err(format!( - "invalid bandwidth `{value}`; use e.g. `10mbps` or `2mb/s`" - )) -} - -#[cfg(test)] -mod tests { - use super::*; - use persisting_agentctl::AttemptId; - use persisting_agentctl::SupervisorRegistration; - - #[test] - fn bandwidth_parser_distinguishes_bits_and_bytes() { - assert_eq!(parse_bandwidth("8bps").unwrap(), 1); - assert_eq!(parse_bandwidth("10mbps").unwrap(), 1_250_000); - assert_eq!(parse_bandwidth("2mb/s").unwrap(), 2_000_000); - assert!(parse_bandwidth("0mbps").is_err()); - } - - #[tokio::test] - async fn embedded_supervisor_authenticates_and_grants_quota() { - let supervisor = EmbeddedSupervisor::start(EmbeddedSupervisorConfig { - network_limit_bytes_per_second: Some(32_768), - quota_slots: 2, - }) - .await - .unwrap(); - let bootstrap = supervisor.bootstrap(); - let address = bootstrap.endpoint.strip_prefix("tcp://").unwrap(); - let stream = TcpStream::connect(address).await.unwrap(); - let (read, mut write) = stream.into_split(); - let registration = SupervisorClientMessage::Register(SupervisorRegistration { - protocol_version: SUPERVISOR_PROTOCOL_VERSION, - token: bootstrap.token, - run_id: RunId::new("run-1"), - attempt_id: AttemptId::new("attempt-1"), - lease_epoch: 7, - }); - write - .write_all(format!("{}\n", serde_json::to_string(®istration).unwrap()).as_bytes()) - .await - .unwrap(); - let mut lines = BufReader::new(read).lines(); - let reply: SupervisorServerMessage = - serde_json::from_str(&lines.next_line().await.unwrap().unwrap()).unwrap(); - let SupervisorServerMessage::Registered { directives, .. } = reply else { - panic!("expected registration response") - }; - let SupervisorDirective::GrantNetworkQuota(grant) = &directives[0].directive else { - panic!("expected quota grant") - }; - assert_eq!(grant.limit.bytes_per_second, 16_384); - supervisor.shutdown().await.unwrap(); - } -} diff --git a/crates/persisting-ppilot/src/task.rs b/crates/persisting-ppilot/src/task.rs deleted file mode 100644 index 60cf19d28..000000000 --- a/crates/persisting-ppilot/src/task.rs +++ /dev/null @@ -1,493 +0,0 @@ -//! Task expression / result wire format (one JSON object per line). -//! -//! **Primitive:** [`TaskExpr`] (plan item) · [`TaskResult`] (terminal outcome). -//! Product contract: `plan()` yield shape `{id, …fields}` ↔ `execute(item)`. - -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use std::collections::HashMap; -use std::time::{SystemTime, UNIX_EPOCH}; - -fn now_secs() -> f64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_secs_f64()) - .unwrap_or(0.0) -} - -/// One unit of work in an execution plan. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TaskExpr { - pub id: String, - #[serde(default = "default_op")] - pub op: String, - #[serde(default)] - pub args: HashMap, - #[serde(default)] - pub meta: HashMap, -} - -fn default_op() -> String { - "execute".into() -} - -impl TaskExpr { - pub fn from_value(mut v: Value) -> anyhow::Result { - let obj = v - .as_object_mut() - .ok_or_else(|| anyhow::anyhow!("task must be a JSON object"))?; - - let id = obj - .remove("id") - .or_else(|| obj.remove("task_id")) - .ok_or_else(|| { - anyhow::anyhow!( - "task must define a stable non-empty 'id' (or 'task_id'); \ - random ids are not generated because they break deduplication and --resume" - ) - }) - .and_then(|x| match x { - Value::String(s) if !s.trim().is_empty() => Ok(s), - Value::Number(n) => Ok(n.to_string()), - Value::String(_) => Err(anyhow::anyhow!("task id must not be empty")), - _ => Err(anyhow::anyhow!("task id must be a string or number")), - })?; - - let op = obj - .remove("op") - .or_else(|| obj.remove("type")) - .and_then(|x| x.as_str().map(|s| s.to_string())) - .unwrap_or_else(|| "execute".into()); - - let meta = match obj.remove("meta") { - Some(Value::Object(m)) => m.into_iter().collect(), - Some(_) => { - return Err(anyhow::anyhow!("task.meta must be an object")); - } - None => HashMap::new(), - }; - - let args = match obj.remove("args") { - Some(Value::Object(m)) => m.into_iter().collect(), - Some(_) => { - return Err(anyhow::anyhow!("task.args must be an object")); - } - None => { - // Flat payload: remaining fields become args. - std::mem::take(obj).into_iter().collect() - } - }; - - Ok(Self { id, op, args, meta }) - } - - pub fn to_ndjson(&self) -> anyhow::Result { - Ok(serde_json::to_string(self)?) - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct TaskResult { - pub task_id: String, - /// Stable pVisor Run identity generated for this task. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub run_id: Option, - /// Concrete pVisor attempt that produced this result. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub attempt_id: Option, - /// Fencing token held by the pPilot owner when the attempt was submitted. - #[serde(default, skip_serializing_if = "is_zero_u64")] - pub lease_epoch: u64, - pub ok: bool, - /// pPilot cancellation (not an execute failure). - #[serde(default)] - pub cancelled: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub value: Option, - /// Numeric measurements returned by `execute()` as `{"metrics": {...}}`. - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub metrics: HashMap, - /// References to large outputs returned by `execute()` as `{"artifacts": {...}}`. - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub artifacts: HashMap, - #[serde(skip_serializing_if = "Option::is_none")] - pub error: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub traceback: Option, - /// Stable terminal failure category for filtering and retry policy. - #[serde(skip_serializing_if = "Option::is_none")] - pub error_kind: Option, - /// Whether retrying the task in a later job may be useful. - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub retryable: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub worker: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub started_at: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub finished_at: Option, - /// How many pPilot infra retries occurred before this terminal result (0 = first try). - #[serde(default, skip_serializing_if = "is_zero")] - pub infra_retries: u32, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum ErrorKind { - Execute, - Infra, - Cancelled, -} - -fn is_zero(v: &u32) -> bool { - *v == 0 -} - -fn is_zero_u64(v: &u64) -> bool { - *v == 0 -} - -impl TaskResult { - pub fn success( - task_id: impl Into, - value: Value, - worker: &str, - started_at: f64, - ) -> Self { - let (metrics, artifacts) = result_metadata(&value); - Self { - task_id: task_id.into(), - run_id: None, - attempt_id: None, - lease_epoch: 0, - ok: true, - cancelled: false, - value: Some(value), - metrics, - artifacts, - error: None, - traceback: None, - error_kind: None, - retryable: false, - worker: Some(worker.to_string()), - started_at: Some(started_at), - finished_at: Some(now_secs()), - infra_retries: 0, - } - } - - pub fn failure( - task_id: impl Into, - error: impl Into, - traceback: Option, - worker: &str, - started_at: f64, - ) -> Self { - Self::failure_with_kind( - task_id, - error, - traceback, - worker, - started_at, - ErrorKind::Execute, - false, - ) - } - - pub fn failure_with_kind( - task_id: impl Into, - error: impl Into, - traceback: Option, - worker: &str, - started_at: f64, - error_kind: ErrorKind, - retryable: bool, - ) -> Self { - Self { - task_id: task_id.into(), - run_id: None, - attempt_id: None, - lease_epoch: 0, - ok: false, - cancelled: false, - value: None, - metrics: HashMap::new(), - artifacts: HashMap::new(), - error: Some(error.into()), - traceback, - error_kind: Some(error_kind), - retryable, - worker: Some(worker.to_string()), - started_at: Some(started_at), - finished_at: Some(now_secs()), - infra_retries: 0, - } - } - - pub fn cancelled(task_id: impl Into) -> Self { - let started = now_secs(); - Self { - task_id: task_id.into(), - run_id: None, - attempt_id: None, - lease_epoch: 0, - ok: false, - cancelled: true, - value: None, - metrics: HashMap::new(), - artifacts: HashMap::new(), - error: Some("cancelled".into()), - traceback: None, - error_kind: Some(ErrorKind::Cancelled), - retryable: false, - worker: None, - started_at: Some(started), - finished_at: Some(started), - infra_retries: 0, - } - } - - pub fn to_ndjson(&self) -> anyhow::Result { - Ok(serde_json::to_string(self)?) - } -} - -fn result_metadata(value: &Value) -> (HashMap, HashMap) { - let Some(obj) = value.as_object() else { - return (HashMap::new(), HashMap::new()); - }; - let metrics = obj - .get("metrics") - .and_then(Value::as_object) - .map(|items| { - items - .iter() - .filter_map(|(name, value)| value.as_f64().map(|number| (name.clone(), number))) - .collect() - }) - .unwrap_or_default(); - let artifacts = obj - .get("artifacts") - .and_then(Value::as_object) - .map(|items| { - items - .iter() - .map(|(name, value)| (name.clone(), value.clone())) - .collect() - }) - .unwrap_or_default(); - (metrics, artifacts) -} - -pub fn unix_now() -> f64 { - now_secs() -} - -#[cfg(test)] -mod tests { - use super::*; - use proptest::prelude::*; - use serde_json::{Map, Value, json}; - - fn scalar_value() -> impl Strategy { - prop_oneof![ - Just(Value::Null), - any::().prop_map(Value::Bool), - any::().prop_map(|value| json!(value)), - proptest::string::string_regex("[a-zA-Z0-9 _-]{0,12}") - .unwrap() - .prop_map(Value::String), - ] - } - - fn non_null_scalar_value() -> impl Strategy { - scalar_value().prop_filter("value must not be JSON null", |value| !value.is_null()) - } - - fn indexed_object(prefix: &'static str, values: Vec) -> Map { - values - .into_iter() - .enumerate() - .map(|(index, value)| (format!("{prefix}_{index}"), value)) - .collect() - } - - fn without_timestamps(mut value: Value) -> Value { - if let Some(object) = value.as_object_mut() { - object.remove("started_at"); - object.remove("finished_at"); - } - value - } - - fn assert_timestamp_roundtrip(actual: Option, expected: Option) { - match (actual, expected) { - (Some(actual), Some(expected)) => { - assert!((actual - expected).abs() < 1e-6, "{actual} != {expected}"); - } - (None, None) => {} - (actual, expected) => panic!("timestamp presence changed: {actual:?} != {expected:?}"), - } - } - - proptest! { - #[test] - fn flat_payload_becomes_args( - fields in prop::collection::vec(scalar_value(), 0..8), - ) { - let mut input = Map::new(); - input.insert("id".into(), json!("t-0")); - let expected = indexed_object("field", fields); - input.extend(expected.clone()); - - let task = TaskExpr::from_value(Value::Object(input)).unwrap(); - prop_assert_eq!(task.id, "t-0"); - prop_assert_eq!(task.op, "execute"); - prop_assert_eq!(task.args, expected.into_iter().collect()); - prop_assert!(task.meta.is_empty()); - } - - #[test] - fn nested_args_and_task_id_alias( - task_id in prop_oneof![ - proptest::string::string_regex("[a-z][a-z0-9-]{0,8}") - .unwrap() - .prop_map(Value::String), - any::().prop_map(|value| json!(value)), - ], - args in prop::collection::vec(scalar_value(), 0..6), - meta in prop::collection::vec(scalar_value(), 0..6), - ) { - let expected_id = match &task_id { - Value::String(value) => value.clone(), - Value::Number(value) => value.to_string(), - _ => unreachable!(), - }; - let expected_args = indexed_object("arg", args); - let expected_meta = indexed_object("meta", meta); - let input = json!({ - "task_id": task_id, - "type": "execute", - "args": expected_args, - "meta": expected_meta, - }); - - let task = TaskExpr::from_value(input).unwrap(); - prop_assert_eq!(task.id, expected_id); - prop_assert_eq!(task.op, "execute"); - prop_assert_eq!(task.args, expected_args.into_iter().collect()); - prop_assert_eq!(task.meta, expected_meta.into_iter().collect()); - } - - #[test] - fn rejects_non_object_meta(meta in scalar_value()) { - let input = json!({"id": "t", "meta": meta}); - prop_assert!(TaskExpr::from_value(input).is_err()); - } - - #[test] - fn result_roundtrip_preserves_terminal_flags( - task_id in proptest::string::string_regex("[a-z][a-z0-9-]{0,8}").unwrap(), - value in non_null_scalar_value(), - worker in proptest::string::string_regex("w[0-9]{1,2}").unwrap(), - started_at in 0u64..1_000_000u64, - ) { - let started_at = started_at as f64; - let ok = TaskResult::success(&task_id, value, &worker, started_at); - let back: TaskResult = serde_json::from_str(&ok.to_ndjson().unwrap()).unwrap(); - let back_wire = serde_json::to_value(&back).unwrap(); - let ok_wire = serde_json::to_value(&ok).unwrap(); - prop_assert_eq!(without_timestamps(back_wire), without_timestamps(ok_wire)); - assert_timestamp_roundtrip(back.started_at, ok.started_at); - assert_timestamp_roundtrip(back.finished_at, ok.finished_at); - prop_assert!(back.ok); - prop_assert!(!back.cancelled); - - let cancelled = TaskResult::cancelled(&task_id); - let cancelled_back: TaskResult = - serde_json::from_str(&cancelled.to_ndjson().unwrap()).unwrap(); - let cancelled_back_wire = serde_json::to_value(&cancelled_back).unwrap(); - let cancelled_wire = serde_json::to_value(&cancelled).unwrap(); - prop_assert_eq!( - without_timestamps(cancelled_back_wire), - without_timestamps(cancelled_wire) - ); - assert_timestamp_roundtrip(cancelled_back.started_at, cancelled.started_at); - assert_timestamp_roundtrip(cancelled_back.finished_at, cancelled.finished_at); - prop_assert!(cancelled_back.cancelled); - prop_assert!(!cancelled_back.ok); - - let failed = TaskResult::failure( - &task_id, - "boom", - Some("traceback".into()), - &worker, - started_at, - ); - let failed_back: TaskResult = - serde_json::from_str(&failed.to_ndjson().unwrap()).unwrap(); - let failed_back_wire = serde_json::to_value(&failed_back).unwrap(); - let failed_wire = serde_json::to_value(&failed).unwrap(); - prop_assert_eq!( - without_timestamps(failed_back_wire), - without_timestamps(failed_wire) - ); - assert_timestamp_roundtrip(failed_back.started_at, failed.started_at); - assert_timestamp_roundtrip(failed_back.finished_at, failed.finished_at); - prop_assert!(!failed_back.ok); - prop_assert_eq!(failed_back.traceback.as_deref(), Some("traceback")); - } - - #[test] - fn success_extracts_metadata_without_changing_value( - metrics in prop::collection::vec(-1_000.0f64..1_000.0, 0..6), - artifacts in prop::collection::vec(scalar_value(), 0..6), - payload in scalar_value(), - ) { - let metric_values = indexed_object( - "metric", - metrics.into_iter().map(|value| json!(value)).collect(), - ); - let artifact_values = indexed_object("artifact", artifacts); - let expected_artifacts: HashMap<_, _> = artifact_values.clone().into_iter().collect(); - let value = json!({ - "metrics": metric_values, - "artifacts": artifact_values, - "payload": payload, - }); - let result = TaskResult::success("t", value.clone(), "w0", 0.0); - - prop_assert_eq!(result.value, Some(value)); - prop_assert_eq!(result.metrics.len(), metric_values.len()); - for (name, metric) in metric_values { - let expected_metric = metric.as_f64(); - prop_assert_eq!(result.metrics.get(&name), expected_metric.as_ref()); - } - prop_assert_eq!(result.artifacts, expected_artifacts); - } - } - - #[test] - fn missing_id_is_rejected_for_stable_resume_identity() { - let err = TaskExpr::from_value(json!({"x": 1})).unwrap_err(); - assert!(err.to_string().contains("stable non-empty 'id'")); - } - - #[test] - fn invalid_ids_are_rejected() { - assert!(TaskExpr::from_value(json!({"id": "", "x": 1})).is_err()); - assert!(TaskExpr::from_value(json!({"id": null, "x": 1})).is_err()); - assert!(TaskExpr::from_value(json!({"id": {"nested": 1}, "x": 1})).is_err()); - } - - #[test] - fn flat_yield_shape_roundtrips_to_result_ndjson() { - // plan() yield {"id", ...fields} → TaskExpr → TaskResult ndjson for sink. - let wire = TaskExpr::from_value(json!({"id": "t-0", "x": 2, "tag": "a"})).unwrap(); - assert_eq!(wire.op, "execute"); - assert_eq!(wire.args.get("x"), Some(&json!(2))); - let ok = TaskResult::success(&wire.id, json!({"x2": 4}), "w0", 0.0); - let line = ok.to_ndjson().unwrap(); - assert!(line.contains("\"task_id\":\"t-0\"") && line.contains("\"ok\":true")); - } -} diff --git a/crates/persisting-ppilot/src/worker.rs b/crates/persisting-ppilot/src/worker.rs deleted file mode 100644 index 2d6768681..000000000 --- a/crates/persisting-ppilot/src/worker.rs +++ /dev/null @@ -1,416 +0,0 @@ -//! Pulsing WorkerActor — dispatches TaskExpr to plan.py `execute(item)`. -//! -//! Semantic block: **fleet worker seam** (see [`crate::blocks`]). -//! - Shares a job [`CancellationToken`]: in-flight execute kills the Python host. -//! - Uses [`crate::result_cache::ResultCache`] for same-worker infra-retry idempotency. -//! - Spawned via factory + [`SupervisionSpec`] so Pulsing can restart a failed slot. - -use crate::executor::ExecutorRouter; -use crate::result_cache::{DEFAULT_RESULT_CACHE_CAP, ResultCache}; -use crate::task::{TaskExpr, TaskResult}; -use persisting_agentctl::SupervisorBootstrap; -use pulsing_actor::prelude::*; -use serde::{Deserialize, Serialize}; -use serde_json::json; -use std::collections::HashMap; -use std::path::PathBuf; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex}; -use tokio::sync::Notify; -use tokio_util::sync::CancellationToken; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum WorkerCommand { - Execute { - task_json: Vec, - /// Driver-issued fencing token. It is part of the cache identity. - lease_epoch: u64, - }, - Shutdown, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum WorkerReply { - Result { result_json: Vec }, - Bye, -} - -/// Shared gate so supervised restarts still signal rank shutdown. -#[derive(Debug)] -pub struct ShutdownGate { - remaining: AtomicUsize, - done: Notify, -} - -impl ShutdownGate { - pub fn new(slots: usize) -> Arc { - Arc::new(Self { - remaining: AtomicUsize::new(slots.max(1)), - done: Notify::new(), - }) - } - - pub fn note_shutdown(&self) { - let prev = self.remaining.fetch_sub(1, Ordering::AcqRel); - if prev <= 1 { - self.remaining.store(0, Ordering::Release); - self.done.notify_waiters(); - } - } - - pub async fn wait(&self) { - loop { - if self.remaining.load(Ordering::Acquire) == 0 { - return; - } - self.done.notified().await; - } - } -} - -/// Cloneable config for supervised `spawn_factory`. -/// -/// [`result_cache`] is shared across supervised restarts so sticky infra -/// re-ask still hits cached TaskResults after the actor is rebuilt. -#[derive(Clone)] -pub struct WorkerConfig { - pub worker_id: String, - pub pvisor_binary: PathBuf, - pub python: PathBuf, - pub pythonpath_extra: Vec, - pub plan_script: PathBuf, - pub script_args: Vec, - pub job_cancel: CancellationToken, - pub shutdown_gate: Option>, - /// Slot-scoped cache; one Arc per logical slot, shared by factory rebuilds. - pub result_cache: Arc>, - pub supervisor: Option, -} - -impl WorkerConfig { - pub fn with_fresh_cache( - worker_id: impl Into, - python: PathBuf, - pythonpath_extra: Vec, - plan_script: PathBuf, - script_args: Vec, - job_cancel: CancellationToken, - shutdown_gate: Option>, - ) -> Self { - Self { - worker_id: worker_id.into(), - pvisor_binary: PathBuf::from("pvisor"), - python, - pythonpath_extra, - plan_script, - script_args, - job_cancel, - shutdown_gate, - result_cache: Arc::new(Mutex::new(ResultCache::new(DEFAULT_RESULT_CACHE_CAP))), - supervisor: None, - } - } - - pub fn with_supervisor(mut self, supervisor: Option) -> Self { - self.supervisor = supervisor; - self - } - - pub fn with_pvisor_binary(mut self, binary: PathBuf) -> Self { - self.pvisor_binary = binary; - self - } - - pub fn build(&self) -> anyhow::Result { - Ok(WorkerActor { - worker_id: self.worker_id.clone(), - executors: Arc::new(ExecutorRouter::local_stack( - self.pvisor_binary.clone(), - self.python.clone(), - self.pythonpath_extra.clone(), - self.plan_script.clone(), - self.script_args.clone(), - worker_context(&self.worker_id), - self.supervisor.clone(), - )?), - done: 0, - shutdown_gate: self.shutdown_gate.clone(), - job_cancel: self.job_cancel.clone(), - result_cache: Arc::clone(&self.result_cache), - }) - } -} - -/// Process-local placement information exposed to Python as -/// `persisting_ppilot.context()`. `execute(item)` remains stateless. -fn worker_context(worker_id: &str) -> serde_json::Value { - let rank = std::env::var("RANK") - .ok() - .and_then(|v| v.parse::().ok()) - .unwrap_or(0); - let local_rank = std::env::var("LOCAL_RANK") - .ok() - .and_then(|v| v.parse::().ok()) - .unwrap_or(rank); - let device = if std::env::var_os("LOCAL_RANK").is_some() { - format!("cuda:{local_rank}") - } else { - "cpu".to_string() - }; - let labels: Vec<_> = std::env::var("PERSISTING_PPILOT_WORKER_LABELS") - .unwrap_or_default() - .split(',') - .map(str::trim) - .filter(|label| !label.is_empty()) - .map(str::to_string) - .collect(); - json!({ - "worker_id": worker_id, - "rank": rank, - "local_rank": local_rank, - "device": device, - "job_id": std::env::var("PERSISTING_PPILOT_JOB_ID").unwrap_or_else(|_| "local".into()), - "output_dir": std::env::var("PERSISTING_PPILOT_OUTPUT_DIR").ok(), - "labels": labels, - }) -} - -pub struct WorkerActor { - pub worker_id: String, - executors: Arc, - done: u64, - shutdown_gate: Option>, - job_cancel: CancellationToken, - result_cache: Arc>, -} - -impl WorkerActor { - pub fn with_plan( - worker_id: impl Into, - python: PathBuf, - pythonpath_extra: Vec, - plan_script: PathBuf, - script_args: Vec, - job_cancel: CancellationToken, - ) -> Self { - WorkerConfig::with_fresh_cache( - worker_id, - python, - pythonpath_extra, - plan_script, - script_args, - job_cancel, - None, - ) - .build() - .expect("initialize pPilot worker") - } - - pub fn from_config(cfg: &WorkerConfig) -> Self { - cfg.build().expect("initialize pPilot worker") - } - - async fn execute(&mut self, task: TaskExpr, lease_epoch: u64) -> TaskResult { - let cache_key = format!("{}@{}", task.id, lease_epoch); - if let Ok(g) = self.result_cache.lock() - && let Some(cached) = g.get(&cache_key) - { - tracing::debug!( - task_id = %task.id, - worker = %self.worker_id, - "infra idempotency: returning cached TaskResult" - ); - return cached.clone(); - } - let r = self - .executors - .run_with_cancel(task, &self.worker_id, self.job_cancel.clone(), lease_epoch) - .await; - if r.ok { - self.done += 1; - } - if let Ok(mut g) = self.result_cache.lock() { - g.put(cache_key, r.clone()); - } - r - } -} - -#[async_trait] -impl Actor for WorkerActor { - fn metadata(&self) -> HashMap { - HashMap::from([ - ("role".into(), "ppilot-worker".into()), - ("worker_id".into(), self.worker_id.clone()), - ("done".into(), self.done.to_string()), - ]) - } - - async fn receive( - &mut self, - msg: Message, - _ctx: &mut ActorContext, - ) -> pulsing_actor::error::Result { - let cmd: WorkerCommand = msg.unpack()?; - let reply = match cmd { - WorkerCommand::Shutdown => { - self.executors.shutdown().await; - if let Some(gate) = &self.shutdown_gate { - gate.note_shutdown(); - } - WorkerReply::Bye - } - WorkerCommand::Execute { - task_json, - lease_epoch, - } => { - let task: TaskExpr = serde_json::from_slice(&task_json).map_err(|e| { - pulsing_actor::error::PulsingError::from( - pulsing_actor::error::RuntimeError::Serialization(e.to_string()), - ) - })?; - let result = self.execute(task, lease_epoch).await; - let result_json = serde_json::to_vec(&result).map_err(|e| { - pulsing_actor::error::PulsingError::from( - pulsing_actor::error::RuntimeError::Serialization(e.to_string()), - ) - })?; - WorkerReply::Result { result_json } - } - }; - Message::pack(&reply) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - - #[tokio::test(flavor = "multi_thread", worker_threads = 4)] - async fn result_cache_avoids_second_execute() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("count.py"); - let counter = dir.path().join("counter.txt"); - let counter_lit = counter.display().to_string(); - std::fs::write( - &path, - format!( - r#" -COUNTER = {counter_lit:?} - -def plan(): - yield {{"id": "unused"}} - -def execute(item): - with open(COUNTER, "a") as f: - f.write("run\n") - return {{"ok": True}} -"# - ), - ) - .unwrap(); - let token = CancellationToken::new(); - let system = Arc::new( - ActorSystem::builder() - .mailbox_capacity(16) - .build() - .await - .unwrap(), - ); - let cfg = WorkerConfig { - worker_id: "w0".into(), - pvisor_binary: PathBuf::from("pvisor"), - python: PathBuf::from("python3"), - pythonpath_extra: vec![], - plan_script: path, - script_args: vec![], - job_cancel: token, - shutdown_gate: None, - result_cache: Arc::new(Mutex::new(ResultCache::new(DEFAULT_RESULT_CACHE_CAP))), - supervisor: None, - }; - let w = crate::pulsing_ext::spawn_supervised(&system, "ppilot/worker/0", move || { - cfg.build().map_err(|error| { - pulsing_actor::error::PulsingError::from( - pulsing_actor::error::RuntimeError::ActorSpawnFailed { - reason: format!("initialize test worker: {error:#}"), - }, - ) - }) - }) - .await - .unwrap(); - let task = TaskExpr::from_value(json!({"id": "t-0", "x": 1})).unwrap(); - let task_json = serde_json::to_vec(&task).unwrap(); - for _ in 0..2 { - let reply = w - .ask::<_, WorkerReply>(WorkerCommand::Execute { - task_json: task_json.clone(), - lease_epoch: 1, - }) - .await - .unwrap(); - assert!(matches!(reply, WorkerReply::Result { .. })); - } - let runs = std::fs::read_to_string(&counter).unwrap(); - assert_eq!(runs.lines().count(), 1, "second ask must hit result cache"); - - // A new lease is a new ownership generation and must never reuse the - // previous generation's cached terminal result. - let reply = w - .ask::<_, WorkerReply>(WorkerCommand::Execute { - task_json, - lease_epoch: 2, - }) - .await - .unwrap(); - let WorkerReply::Result { result_json } = reply else { - panic!("execute returned Bye") - }; - let result: TaskResult = serde_json::from_slice(&result_json).unwrap(); - assert_eq!(result.lease_epoch, 2); - let runs = std::fs::read_to_string(&counter).unwrap(); - assert_eq!(runs.lines().count(), 2, "new epoch must execute again"); - system.shutdown().await.unwrap(); - } - - #[tokio::test] - async fn shutdown_gate_fires_when_all_slots_done() { - let gate = ShutdownGate::new(2); - let g = Arc::clone(&gate); - let h = tokio::spawn(async move { - g.wait().await; - }); - gate.note_shutdown(); - assert!(!h.is_finished()); - gate.note_shutdown(); - h.await.unwrap(); - } - - #[tokio::test] - async fn shared_result_cache_survives_rebuild() { - let cache = Arc::new(Mutex::new(ResultCache::new(8))); - let cfg = WorkerConfig { - worker_id: "w0".into(), - pvisor_binary: PathBuf::from("pvisor"), - python: PathBuf::from("python3"), - pythonpath_extra: vec![], - plan_script: PathBuf::from("/dev/null"), - script_args: vec![], - job_cancel: CancellationToken::new(), - shutdown_gate: None, - result_cache: Arc::clone(&cache), - supervisor: None, - }; - let _a = cfg.build().unwrap(); - cache - .lock() - .unwrap() - .put("t-0", TaskResult::success("t-0", json!(1), "w0", 0.0)); - // Supervised restart = factory rebuild; same Arc must still hold entries. - let _b = cfg.build().unwrap(); - assert!(cache.lock().unwrap().get("t-0").is_some()); - assert!(Arc::ptr_eq(&cfg.result_cache, &cache)); - } -} diff --git a/crates/persisting-ppilot/tests/common/mod.rs b/crates/persisting-ppilot/tests/common/mod.rs deleted file mode 100644 index d7e353599..000000000 --- a/crates/persisting-ppilot/tests/common/mod.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! Helpers for crate integration tests (`tests/integration_*.rs`). - -#![allow(dead_code)] - -use persisting_ppilot::{Observer, RunOptions, SkipSet}; -use std::path::{Path, PathBuf}; -use tokio_util::sync::CancellationToken; - -pub fn write_plan(dir: &Path, name: &str, body: &str) -> PathBuf { - let path = dir.join(name); - std::fs::write(&path, body).expect("write plan"); - path -} - -pub fn run_opts(script: PathBuf) -> RunOptions { - RunOptions { - script, - python: PathBuf::from("python3"), - pvisor_binary: PathBuf::from("pvisor"), - workers: 2, - max_inflight: 4, - per_worker_inflight: 1, - pythonpath_extra: vec![], - script_args: vec![], - infra_retries: 0, - job_cancel: CancellationToken::new(), - observer: Observer::disabled(), - skip_task_ids: SkipSet::new(), - checkpoint: None, - sink_submitter: None, - coordinator: None, - } -} diff --git a/crates/persisting-ppilot/tests/integration_batch_scenarios.rs b/crates/persisting-ppilot/tests/integration_batch_scenarios.rs deleted file mode 100644 index 81ed55815..000000000 --- a/crates/persisting-ppilot/tests/integration_batch_scenarios.rs +++ /dev/null @@ -1,123 +0,0 @@ -use persisting_agentctl::RunState; -use persisting_ppilot::{BatchProductionOptions, produce_from_planner}; -use std::path::PathBuf; -use std::process::Command; - -#[tokio::test(flavor = "multi_thread", worker_threads = 4)] -async fn batch_production_runs_multiple_pvisors_with_reviewable_lineage() { - let temp = tempfile::tempdir().unwrap(); - let output = temp.path().join("production"); - let planner = temp.path().join("production.py"); - std::fs::write( - &planner, - r#" -import argparse - -parser = argparse.ArgumentParser() -parser.add_argument("--count", type=int, required=True) -args = parser.parse_args() - -def plan(): - for index in range(args.count): - yield { - "id": f"trajectory-{index}", - "agent": "fixture-agent", - "command": ["/bin/sh", "-c", f"printf trajectory-{index}"], - } -"#, - ) - .unwrap(); - - let report = produce_from_planner( - planner, - PathBuf::from("python3"), - vec!["--count".into(), "3".into()], - "integration".into(), - BatchProductionOptions { - output_dir: output.clone(), - pvisor_binary: PathBuf::from("pvisor"), - parallelism: 2, - capture_gateway: true, - supervisor_network_limit_bytes_per_second: None, - }, - ) - .await - .unwrap(); - - assert_eq!(report.total, 3); - assert_eq!(report.completed, 3); - assert_eq!(report.failed, 0); - assert!(output.join("production-report.json").is_file()); - for outcome in report.runs { - assert_eq!(outcome.state, RunState::Completed); - let bundle: serde_json::Value = serde_json::from_slice( - &std::fs::read(outcome.workspace.join("run-bundle.json")).unwrap(), - ) - .unwrap(); - assert_eq!( - bundle["orchestration"].get("persisting.ppilot.supervisor.connected"), - Some(&serde_json::json!(true)) - ); - assert_eq!( - bundle["run"]["task_id"].as_str(), - Some(outcome.task_id.as_str()), - ); - assert_eq!( - bundle["run"]["parent_run_id"].as_str(), - Some("ppilot-batch-integration") - ); - assert_eq!(bundle["orchestration"]["ppilot.batch_id"], "integration"); - assert_eq!( - bundle["orchestration"]["ppilot.scope"], - "trajectory-production" - ); - assert!(outcome.workspace.join("run.json").is_file()); - assert!(outcome.workspace.join("run-bundle.json").is_file()); - } -} - -#[test] -fn produce_cli_uses_python_planner_as_primary_input() { - let temp = tempfile::tempdir().unwrap(); - let output = temp.path().join("production-cli"); - let planner = temp.path().join("cli-production.py"); - std::fs::write( - &planner, - r#" -import argparse - -parser = argparse.ArgumentParser() -parser.add_argument("--count", type=int, required=True) -args = parser.parse_args() - -def plan(): - for index in range(args.count): - yield { - "id": f"cli-{index}", - "agent": "fixture-agent", - "command": ["/bin/sh", "-c", f"printf cli-{index}"], - } -"#, - ) - .unwrap(); - - let result = Command::new(env!("CARGO_BIN_EXE_ppilot")) - .arg("produce") - .arg(&planner) - .args(["--output", output.to_str().unwrap()]) - .args(["--parallelism", "2", "--batch-id", "cli"]) - .args(["--", "--count", "2"]) - .output() - .expect("run ppilot produce"); - assert!( - result.status.success(), - "ppilot produce failed: {}", - String::from_utf8_lossy(&result.stderr) - ); - let report: serde_json::Value = serde_json::from_slice(&result.stdout).unwrap(); - assert_eq!(report["batch_id"], "cli"); - assert_eq!(report["completed"], 2); - assert!(output.join("production-report.json").is_file()); - assert!(output.join("cli-0/run-bundle.json").is_file()); - assert!(output.join("cli-1/run-bundle.json").is_file()); -} diff --git a/crates/persisting-ppilot/tests/integration_local.rs b/crates/persisting-ppilot/tests/integration_local.rs deleted file mode 100644 index df76c86dc..000000000 --- a/crates/persisting-ppilot/tests/integration_local.rs +++ /dev/null @@ -1,416 +0,0 @@ -//! Integration: local fleet paths that cross Driver + Runtime + Worker + L3. -//! -//! Contract unit tests live next to their modules (`src/*.rs`). This file only -//! covers multi-module behavior. - -mod common; - -use persisting_events::{ChronicleControl, MemoryChronicleControl}; -use persisting_ppilot::{ - CheckpointLedger, JsonlFileSink, Observer, RunCoordinator, RunOptions, SkipSet, - run_local_fleet, spawn_coordinated_sink_writer, spawn_sink_writer, -}; -use std::collections::HashSet; -use std::path::PathBuf; -use std::sync::{Arc, Mutex}; -use std::time::Instant; -use tokio_util::sync::CancellationToken; - -#[tokio::test(flavor = "multi_thread", worker_threads = 4)] -async fn completion_order_across_fleet() { - let dir = tempfile::tempdir().unwrap(); - let script = common::write_plan( - dir.path(), - "delay.py", - r#" -import time - -def plan(): - for i in range(4): - yield {"id": f"t-{i}", "delay": 0.3 if i == 0 else 0.01} - -def execute(item): - time.sleep(float(item["delay"])) - return {"x": item["id"]} -"#, - ); - let order = Arc::new(Mutex::new(Vec::new())); - let order_cb = Arc::clone(&order); - let mut opts = common::run_opts(script); - opts.workers = 2; - opts.max_inflight = 4; - run_local_fleet(opts, move |r| { - order_cb.lock().unwrap().push(r.task_id.clone()); - }) - .await - .unwrap(); - let seen = order.lock().unwrap().clone(); - assert_ne!( - seen[0], "t-0", - "expected fast task before slow t-0: {seen:?}" - ); -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 4)] -async fn driver_worker_pvisor_result_is_fenced_and_committed() { - let dir = tempfile::tempdir().unwrap(); - let script = common::write_plan( - dir.path(), - "committed.py", - r#" -def plan(): - yield {"id": "committed-task", "value": 7} - -def execute(item): - return {"value": item["value"]} -"#, - ); - let sink_root = dir.path().join("sink"); - let sink: Arc = - Arc::new(JsonlFileSink::open(&sink_root).await.unwrap()); - let control: Arc = - Arc::new(MemoryChronicleControl::new(dir.path().to_string_lossy())); - let coordinator = Arc::new( - RunCoordinator::open_with_control(control, &sink_root, 30_000, None) - .await - .unwrap(), - ); - let writer = spawn_coordinated_sink_writer(sink, None, None, 8, Arc::clone(&coordinator)); - let mut opts = common::run_opts(script); - opts.workers = 1; - opts.max_inflight = 1; - opts.sink_submitter = Some(writer.submitter()); - opts.coordinator = Some(Arc::clone(&coordinator)); - - let results = run_local_fleet(opts, |_| {}).await.unwrap(); - writer.join().await.unwrap(); - assert_eq!(results.len(), 1); - let result = &results[0]; - assert!(result.ok, "unexpected pVisor result: {result:#?}"); - assert!(result.lease_epoch > 0); - assert!(result.attempt_id.is_some()); - let run_id = persisting_agentctl::RunId::new(result.run_id.as_deref().unwrap()); - let control = coordinator - .control() - .get_run(&run_id) - .await - .unwrap() - .unwrap(); - let commit = control.commit.expect("terminal RunCommit"); - assert_eq!(commit.request.lease_epoch, result.lease_epoch); - assert_eq!( - commit.request.attempt_id.as_str(), - result.attempt_id.as_deref().unwrap() - ); -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 4)] -async fn resume_skip_ids_not_dispatched() { - let dir = tempfile::tempdir().unwrap(); - let script = common::write_plan( - dir.path(), - "skip.py", - r#" -def plan(): - for i in range(3): - yield {"id": f"t-{i}", "x": i} - -def execute(item): - return {"x": item["x"]} -"#, - ); - let mut opts = common::run_opts(script); - opts.workers = 1; - opts.skip_task_ids = ["t-0".into(), "t-1".into()].into_iter().collect(); - let results = run_local_fleet(opts, |_| {}).await.unwrap(); - assert_eq!(results.len(), 1); - assert_eq!(results[0].task_id, "t-2"); -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 4)] -async fn ctrl_c_cancels_in_flight_execute() { - let dir = tempfile::tempdir().unwrap(); - let started = dir.path().join("execute-started"); - let started_literal = serde_json::to_string(started.to_str().unwrap()).unwrap(); - let script = common::write_plan( - dir.path(), - "slow.py", - &format!( - r#" -import time -from pathlib import Path - -def plan(): - yield {{"id": "slow"}} - -def execute(item): - Path({started_literal}).write_text("started") - time.sleep(5) - return {{}} -"# - ), - ); - let cancel = CancellationToken::new(); - let c = cancel.clone(); - tokio::spawn(async move { - let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(2); - while !tokio::fs::try_exists(&started).await.unwrap_or(false) { - assert!( - tokio::time::Instant::now() < deadline, - "execute did not start before cancellation deadline" - ); - tokio::time::sleep(std::time::Duration::from_millis(10)).await; - } - c.cancel(); - }); - let opts = RunOptions { - script, - python: PathBuf::from("python3"), - pvisor_binary: PathBuf::from("pvisor"), - workers: 1, - max_inflight: 1, - per_worker_inflight: 1, - pythonpath_extra: vec![], - script_args: vec![], - infra_retries: 0, - job_cancel: cancel, - observer: Observer::disabled(), - skip_task_ids: SkipSet::new(), - checkpoint: None, - sink_submitter: None, - coordinator: None, - }; - let t0 = Instant::now(); - let results = run_local_fleet(opts, |_| {}).await.unwrap(); - assert!(results.iter().any(|r| r.cancelled)); - assert!(t0.elapsed().as_secs_f64() < 2.5); -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 4)] -async fn per_worker_slots_run_in_parallel() { - let dir = tempfile::tempdir().unwrap(); - let barrier = dir.path().join("parallel-barrier"); - std::fs::create_dir(&barrier).unwrap(); - let barrier_literal = serde_json::to_string(barrier.to_str().unwrap()).unwrap(); - let script = common::write_plan( - dir.path(), - "par.py", - &format!( - r#" -import time -from pathlib import Path - -BARRIER = Path({barrier_literal}) - -def plan(): - for i in range(2): - yield {{"id": f"t-{{i}}"}} - -def execute(item): - (BARRIER / f"ready-{{item['id']}}").write_text("ready") - deadline = time.monotonic() + 5 - while len(list(BARRIER.glob("ready-*"))) < 2: - if time.monotonic() >= deadline: - return {{"overlapped": False}} - time.sleep(0.01) - return {{"overlapped": True}} -"#, - ), - ); - let mut opts = common::run_opts(script); - opts.workers = 1; - opts.per_worker_inflight = 2; - opts.max_inflight = 2; - let results = run_local_fleet(opts, |_| {}).await.unwrap(); - assert_eq!(results.len(), 2); - assert!(results.iter().all(|r| r.ok)); - assert!( - results.iter().all(|r| r - .value - .as_ref() - .is_some_and(|value| value["overlapped"] == true)), - "worker slots did not overlap: {results:?}" - ); - let workers: HashSet<_> = results.iter().filter_map(|r| r.worker.clone()).collect(); - assert_eq!(workers.len(), 2, "expected two slot ids, got {workers:?}"); -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 4)] -async fn script_args_reach_plan_and_execute() { - let dir = tempfile::tempdir().unwrap(); - let script = common::write_plan( - dir.path(), - "args.py", - r#" -import argparse - -def _parse(argv=None): - p = argparse.ArgumentParser() - p.add_argument("--n", type=int, default=1) - return p.parse_args(argv) - -def plan(): - args = _parse() - for i in range(args.n): - yield {"id": f"t-{i}", "n": args.n} - -def execute(item): - return {"n": item["n"]} -"#, - ); - let mut opts = common::run_opts(script); - opts.workers = 1; - opts.script_args = vec!["--n".into(), "3".into()]; - let results = run_local_fleet(opts, |_| {}).await.unwrap(); - assert_eq!(results.len(), 3); - assert_eq!(results[0].value.as_ref().unwrap()["n"], 3); -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 4)] -async fn duplicate_plan_ids_dispatch_once() { - let dir = tempfile::tempdir().unwrap(); - let script = common::write_plan( - dir.path(), - "dup.py", - r#" -def plan(): - yield {"id": "same", "x": 1} - yield {"id": "same", "x": 2} - -def execute(item): - return {"x": item["x"]} -"#, - ); - let mut opts = common::run_opts(script); - opts.workers = 2; - let results = run_local_fleet(opts, |_| {}).await.unwrap(); - assert_eq!( - results.len(), - 1, - "second identical id must be claimed/skipped" - ); - assert_eq!(results[0].task_id, "same"); -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 4)] -async fn bounded_inflight_handles_many_tasks() { - let dir = tempfile::tempdir().unwrap(); - let script = common::write_plan( - dir.path(), - "many.py", - r#" -def plan(): - for i in range(40): - yield {"id": f"t-{i}", "x": i} - -def execute(item): - return {"x": item["x"] * 2} -"#, - ); - let mut opts = common::run_opts(script); - opts.workers = 2; - opts.max_inflight = 2; - opts.per_worker_inflight = 1; - let results = run_local_fleet(opts, |_| {}).await.unwrap(); - assert_eq!(results.len(), 40); - assert!(results.iter().all(|r| r.ok)); -} - -/// `--sink` persist then `--resume` from ledger, including corrupt JSONL lines. -#[tokio::test(flavor = "multi_thread", worker_threads = 4)] -async fn sink_then_resume_skips_done_and_tolerates_bad_lines() { - let dir = tempfile::tempdir().unwrap(); - let sink_root = dir.path().join("sink"); - let script = common::write_plan( - dir.path(), - "resume.py", - r#" -def plan(): - for i in range(3): - yield {"id": f"t-{i}", "x": i} - -def execute(item): - return {"x": item["x"]} -"#, - ); - - // Run 1: all three tasks → ready.ndjson - { - let sink: Arc = - Arc::new(JsonlFileSink::open(&sink_root).await.unwrap()); - let writer = spawn_sink_writer(sink, None, None, 32); - let mut opts = common::run_opts(script.clone()); - opts.workers = 1; - opts.sink_submitter = Some(writer.submitter()); - let results = run_local_fleet(opts, |_| {}).await.unwrap(); - writer.join().await.unwrap(); - assert_eq!(results.len(), 3); - assert!(results.iter().all(|r| r.ok)); - } - - // Corrupt a line; valid terminals must still load for resume. - { - let ready_path = sink_root.join("ready.ndjson"); - let mut body = std::fs::read_to_string(&ready_path).unwrap(); - body.push_str("this-is-not-json\n"); - body.push_str("{\"no_id\":true}\n"); - std::fs::write(&ready_path, body).unwrap(); - } - - let ledger = CheckpointLedger::load(&sink_root).await.unwrap(); - assert_eq!( - ledger.ready.len(), - 3, - "corrupt lines must not drop valid ids" - ); - assert!(ledger.skip_ids().contains("t-0")); - assert!(ledger.skip_ids().contains("t-1")); - assert!(ledger.skip_ids().contains("t-2")); - - // Run 2: resume — nothing left to dispatch. - { - let sink: Arc = - Arc::new(JsonlFileSink::open(&sink_root).await.unwrap()); - let writer = spawn_sink_writer(sink, None, None, 32); - let mut opts = common::run_opts(script.clone()); - opts.workers = 1; - opts.skip_task_ids = ledger.skip_ids().into_iter().collect(); - opts.sink_submitter = Some(writer.submitter()); - let results = run_local_fleet(opts, |_| {}).await.unwrap(); - writer.join().await.unwrap(); - assert!( - results.is_empty(), - "resume must skip all ledger terminals, got {results:?}" - ); - } - - // Partial resume: only t-0 done on disk → run t-1/t-2. - let partial = dir.path().join("sink_partial"); - std::fs::create_dir_all(&partial).unwrap(); - std::fs::write( - partial.join("ready.ndjson"), - "{\"task_id\":\"t-0\",\"ok\":true}\nnot-json\n", - ) - .unwrap(); - let partial_ledger = CheckpointLedger::load(&partial).await.unwrap(); - assert_eq!(partial_ledger.ready, HashSet::from(["t-0".into()])); - - let sink: Arc = - Arc::new(JsonlFileSink::open(&partial).await.unwrap()); - let writer = spawn_sink_writer(sink, None, None, 32); - let mut opts = common::run_opts(script); - opts.workers = 1; - opts.skip_task_ids = partial_ledger.skip_ids().into_iter().collect(); - opts.sink_submitter = Some(writer.submitter()); - let results = run_local_fleet(opts, |_| {}).await.unwrap(); - writer.join().await.unwrap(); - let ids: HashSet<_> = results.iter().map(|r| r.task_id.clone()).collect(); - assert_eq!(ids, HashSet::from(["t-1".into(), "t-2".into()])); - assert!(results.iter().all(|r| r.ok)); - - let after = CheckpointLedger::load(&partial).await.unwrap(); - assert_eq!(after.ready.len(), 3); - assert!(after.skip_ids().contains("t-0")); - assert!(after.skip_ids().contains("t-1")); - assert!(after.skip_ids().contains("t-2")); -} diff --git a/crates/persisting-pvisor/Cargo.toml b/crates/persisting-pvisor/Cargo.toml deleted file mode 100644 index b729b3d61..000000000 --- a/crates/persisting-pvisor/Cargo.toml +++ /dev/null @@ -1,53 +0,0 @@ -[package] -name = "persisting-pvisor" -version.workspace = true -edition.workspace = true -authors.workspace = true -license.workspace = true -description = "pVisor foreground Agent Run manager and portable execution runtime" -readme = "README.md" - -[features] -default = [] -jujutsu-overlay = ["persisting-overlayfs/jujutsu"] - -[dependencies] -anyhow.workspace = true -async-trait.workspace = true -chrono.workspace = true -clap = { workspace = true, features = ["derive", "env"] } -dirs.workspace = true -flate2.workspace = true -fs2.workspace = true -globset.workspace = true -libc.workspace = true -libkrun = { workspace = true, features = ["net"] } -persisting-agentctl.workspace = true -persisting-events = { workspace = true, features = ["control"] } -persisting-gateway.workspace = true -persisting-overlaynet.workspace = true -persisting-overlayfs = { workspace = true, default-features = false } -persisting-overlay-core.workspace = true -persisting-replay.workspace = true -serde = { workspace = true, features = ["derive"] } -serde_json.workspace = true -sha2.workspace = true -tar.workspace = true -thiserror.workspace = true -toml.workspace = true -tempfile.workspace = true -tokio = { workspace = true, features = ["io-util", "macros", "process", "rt-multi-thread", "signal", "sync", "time", "net"] } -tokio-util = { workspace = true, features = ["rt"] } -tracing.workspace = true -tracing-subscriber = { workspace = true, features = ["env-filter"] } -uuid = { workspace = true, features = ["v4"] } -zstd.workspace = true -reqwest = { workspace = true, features = ["blocking", "json", "rustls-tls"] } - -[[bin]] -name = "pvisor" -path = "src/bin/pvisor.rs" - -[dev-dependencies] -proptest.workspace = true -persisting-ppilot = { path = "../persisting-ppilot" } diff --git a/crates/persisting-pvisor/README.md b/crates/persisting-pvisor/README.md deleted file mode 100644 index a1b9ad824..000000000 --- a/crates/persisting-pvisor/README.md +++ /dev/null @@ -1,71 +0,0 @@ -# pVisor - -**pVisor is an AgentVisor: the control and containment layer between an -autonomous Agent and the infrastructure that executes it.** - -Owns one Run, its Attempts, capability admission, staged filesystem Effects, -execution placement, and the host CLI (`pvisor`). It can place Runs on host, -container, and libkrun VM executors while preserving one Agent-facing contract. -It is not an Agent framework, an OCI runtime, or an operating system. - -Does not own batch planning or global scheduling -([`persisting-ppilot`](../persisting-ppilot/README.md)), canonical trajectory -storage ([`persisting-pchronicle`](../persisting-pchronicle/README.md)), or the -implementation of every isolation backend. OverlayFS, OverlayNet, Gateway, and -AgentCtl are pVisor runtime drivers, not peer products. - -![pVisor AgentVisor architecture](../../docs/src/assets/diagrams/pvisor/agentvisor-architecture.svg) - -| Product area | Current responsibility | -| --- | --- | -| Agent lifecycle | One logical `Run`, one or more `Attempt`s, cancellation, deadlines, terminal publication, and parent lineage | -| Agent control | Optional authenticated AgentCtl v1 for Sessions, client state, directives, and cooperative quiescence | -| Capabilities | Models, tools, filesystem read/write, network, secrets, subprocess, and resources, with evidence recorded per dimension | -| Filesystem effects | Copy-on-write staging, classified review, logical checkpoint/fork, repeated selective apply, terminal apply/drop, and an apply ledger | -| Network and model access | Gateway capture plus OverlayNet policy; enforcement strength depends on executor and is never inferred from a product label | -| Execution placement | Host process, Docker/Podman transport, or libkrun VM using an OCI image, prepared rootfs, or Linux host rootfs | -| Evidence | Run Bundle, lifecycle events, capability enforcement, filesystem changes, network counters, AgentCtl observations, output, and artifact references | - -The standalone product loop is `RunSpec → admission → Attempt → terminal -RunResult + private Run Bundle + staged Effects → later review/apply/drop`. -pChronicle is not a runtime prerequisite for that loop. Capture is a Gateway -capability, not pVisor's component identity. - -The default build includes the local Lance/DataFusion pChronicle backend for an -optional durable Attempt-state handoff to pPilot. The default build excludes -cloud object-store SDKs, Jujutsu, `prost`, and a protobuf toolchain. Use -`lance-chronicle` for S3 support, `jujutsu-overlay` for the Jujutsu upper -backend, or `--no-default-features` for a storage-light binary. - -## Develop - -```bash -just pvisor # release build + macOS Hypervisor signing -just pvisor debug # debug build + macOS signing -just test persisting-pvisor -just test-pvisor-lance -just examples-pvisor -``` - -```bash -cargo build --locked -p persisting-pvisor --bin pvisor --release -``` - -On macOS, source builds that use HVF must be signed. `just pvisor` does this; -the equivalent entitlements file is `macos-hypervisor.entitlements`. Building -from source on macOS also requires Zig (`brew install zig`) to cross-compile -libkrun's embedded Linux guest init. - -## Links - -- [What is an AgentVisor?](../../docs/src/pvisor/concepts/agentvisor.md) -- [Get started](../../docs/src/pvisor/get-started.md) -- [Isolation architecture](../../docs/src/pvisor/design/isolation.md) -- [Gateway architecture](../../docs/src/pvisor/design/gateway.md) -- [OverlayNet architecture](../../docs/src/pvisor/design/overlaynet.md) -- [pVisor CLI](../../docs/src/pvisor/reference/cli.md) -- [System architecture](../../docs/src/system-design/architecture.md) -- [`persisting-overlayfs`](../persisting-overlayfs/README.md) -- [`persisting-overlaynet`](../persisting-overlaynet/README.md) -- [`persisting-gateway`](../persisting-gateway/README.md) -- [`persisting-agentctl`](../persisting-agentctl/README.md) diff --git a/crates/persisting-pvisor/macos-hypervisor.entitlements b/crates/persisting-pvisor/macos-hypervisor.entitlements deleted file mode 100644 index 154f3308e..000000000 --- a/crates/persisting-pvisor/macos-hypervisor.entitlements +++ /dev/null @@ -1,8 +0,0 @@ - - - - - com.apple.security.hypervisor - - - diff --git a/crates/persisting-pvisor/src/agentctl.rs b/crates/persisting-pvisor/src/agentctl.rs deleted file mode 100644 index cc23b8b59..000000000 --- a/crates/persisting-pvisor/src/agentctl.rs +++ /dev/null @@ -1,955 +0,0 @@ -//! Optional, cooperative Run-scoped AgentCtl channel owned by pVisor. - -pub use persisting_agentctl::{ - AGENTCTL_MAX_FRAME_BYTES, AGENTCTL_VERSION, AgentDirective, AgentErrorCode, AgentRequest, - AgentResponse, AgentState, -}; -use persisting_agentctl::{AttemptId, RunId}; -use serde::{Deserialize, Serialize}; -use std::collections::{BTreeMap, BTreeSet, HashMap}; -use std::fs; -use std::io::{BufRead, Read, Write}; -use std::os::unix::fs::PermissionsExt; -use std::path::{Path, PathBuf}; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; -use std::thread::JoinHandle; -use std::time::Duration; - -/// Maximum live runtime Sessions accepted by one Run. -pub const AGENTCTL_MAX_SESSIONS: usize = 64; - -const SYNC_INTERVAL_MS: u64 = 1_000; - -/// Diagnostic observation of one cooperative runtime client. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct AgentClientSnapshot { - /// Stable identity supplied during `Hello`. - pub client_id: String, - /// Most recently accepted cooperative state. - pub state: AgentState, - /// Time of the most recently accepted `Sync`, if any. - pub last_sync_unix_ms: Option, - /// Whether the Session has missed three synchronization intervals. - pub stale: bool, -} - -/// Serializable AgentCtl observation attached to a Run Bundle. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct AgentCtlSnapshot { - /// Owning Run identity. - pub run_id: String, - /// Owning Attempt identity. - pub attempt_id: String, - /// Current pVisor directive. - pub directive: AgentDirective, - /// Runtime clients sorted by `client_id`. - pub clients: Vec, -} - -#[derive(Debug)] -struct ClientSession { - client_id: String, - state: AgentState, - last_seen_unix_ms: u64, - last_sync_unix_ms: Option, - checkpoint_ack: Option, -} - -#[derive(Debug, Clone, Copy)] -struct CheckpointAcknowledgement { - generation: u64, - acknowledged_at_unix_ms: u64, -} - -#[derive(Debug, Clone)] -struct ActiveCheckpoint { - generation: u64, - checkpoint_id: String, - deadline_unix_ms: Option, - participants: BTreeSet, -} - -#[derive(Debug)] -struct AgentCtlState { - run_id: String, - attempt_id: String, - token: String, - directive: AgentDirective, - sessions: HashMap, - next_checkpoint_generation: u64, - active_checkpoint: Option, -} - -impl AgentCtlState { - fn new(run_id: &RunId, attempt_id: &AttemptId, token: String) -> Self { - Self { - run_id: run_id.as_str().to_string(), - attempt_id: attempt_id.as_str().to_string(), - token, - directive: AgentDirective::Continue, - sessions: HashMap::new(), - next_checkpoint_generation: 0, - active_checkpoint: None, - } - } - - fn snapshot(&self) -> AgentCtlSnapshot { - let now = crate::util::unix_now_ms(); - let mut clients = self - .sessions - .values() - .map(|session| AgentClientSnapshot { - client_id: session.client_id.clone(), - state: session.state.clone(), - last_sync_unix_ms: session.last_sync_unix_ms, - stale: session_is_stale(session, now), - }) - .collect::>(); - clients.sort_by(|left, right| left.client_id.cmp(&right.client_id)); - AgentCtlSnapshot { - run_id: self.run_id.clone(), - attempt_id: self.attempt_id.clone(), - directive: self.directive.clone(), - clients, - } - } -} - -/// Cloneable pVisor-side control surface for one Run's AgentCtl channel. -#[derive(Clone)] -pub struct AgentCtlControl { - endpoint: PathBuf, - state: Arc>, - delegated_snapshot: Arc>>, -} - -/// Cancellation-safe ownership of one live checkpoint transition. -pub(crate) struct AgentCtlCheckpointGuard { - control: AgentCtlControl, - generation: u64, - checkpoint_id: String, -} - -impl AgentCtlCheckpointGuard { - /// Capture while the exact checkpoint generation remains fully quiesced. - /// - /// The closure runs while directive transitions and client Sync requests - /// are blocked. `Ok(None)` means the frozen participant set is still - /// draining. Any completed capture, including a capture error, releases - /// the matching `Quiesce` before returning. - pub(crate) fn try_capture( - &self, - capture: impl FnOnce() -> anyhow::Result, - ) -> anyhow::Result> { - let mut state = lock_state(&self.control.state); - let checkpoint = state - .active_checkpoint - .as_ref() - .filter(|checkpoint| checkpoint.generation == self.generation) - .cloned() - .ok_or_else(|| { - anyhow::anyhow!( - "checkpoint {} no longer owns the active AgentCtl quiesce", - self.checkpoint_id - ) - })?; - anyhow::ensure!( - matches!( - &state.directive, - AgentDirective::Quiesce { checkpoint_id, .. } - if checkpoint_id == &checkpoint.checkpoint_id - ), - "checkpoint {} lost its AgentCtl quiesce directive", - self.checkpoint_id - ); - - let ready = checkpoint.participants.iter().all(|session_id| { - state.sessions.get(session_id).is_some_and(|session| { - matches!( - &session.state, - AgentState::Quiesced { checkpoint_id } - if checkpoint_id == &checkpoint.checkpoint_id - ) && session.checkpoint_ack.is_some_and(|ack| { - ack.generation == checkpoint.generation - && checkpoint - .deadline_unix_ms - .is_none_or(|deadline| ack.acknowledged_at_unix_ms <= deadline) - }) - }) - }); - if !ready { - if checkpoint - .deadline_unix_ms - .is_some_and(|deadline| crate::util::unix_now_ms() >= deadline) - { - anyhow::bail!( - "checkpoint {} timed out waiting for all AgentCtl clients to quiesce", - self.checkpoint_id - ); - } - return Ok(None); - } - - let outcome = capture(); - finish_checkpoint(&mut state, self.generation); - outcome.map(Some) - } -} - -impl Drop for AgentCtlCheckpointGuard { - fn drop(&mut self) { - finish_checkpoint(&mut lock_state(&self.control.state), self.generation); - } -} - -impl AgentCtlControl { - /// Return the Run-local endpoint path. - pub fn endpoint(&self) -> &Path { - &self.endpoint - } - - /// Return the latest cooperative observation. - pub fn snapshot(&self) -> AgentCtlSnapshot { - if let Some(snapshot) = self - .delegated_snapshot - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .clone() - { - return snapshot; - } - lock_state(&self.state).snapshot() - } - - pub(crate) fn import_delegated_snapshot(&self, mut snapshot: AgentCtlSnapshot) { - let state = lock_state(&self.state); - snapshot.run_id = state.run_id.clone(); - snapshot.attempt_id = state.attempt_id.clone(); - drop(state); - *self - .delegated_snapshot - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(snapshot); - } - - /// Freeze all currently live Sessions and request a checkpoint boundary. - pub fn request_quiesce( - &self, - checkpoint_id: impl Into, - deadline_unix_ms: Option, - ) -> anyhow::Result<()> { - self.start_checkpoint(checkpoint_id.into(), deadline_unix_ms) - .map(|_| ()) - } - - pub(crate) fn begin_checkpoint( - &self, - checkpoint_id: String, - deadline_unix_ms: Option, - ) -> anyhow::Result { - let generation = self.start_checkpoint(checkpoint_id.clone(), deadline_unix_ms)?; - Ok(AgentCtlCheckpointGuard { - control: self.clone(), - generation, - checkpoint_id, - }) - } - - fn start_checkpoint( - &self, - checkpoint_id: String, - deadline_unix_ms: Option, - ) -> anyhow::Result { - anyhow::ensure!( - !checkpoint_id.trim().is_empty(), - "AgentCtl checkpoint_id must be non-empty" - ); - let mut state = lock_state(&self.state); - anyhow::ensure!( - matches!(state.directive, AgentDirective::Continue), - "AgentCtl cannot start a checkpoint while {:?} is active", - state.directive - ); - let now = crate::util::unix_now_ms(); - state - .sessions - .retain(|_, session| !session_is_stale(session, now)); - anyhow::ensure!( - !state.sessions.is_empty(), - "live checkpoint requires at least one AgentCtl client" - ); - let generation = state - .next_checkpoint_generation - .checked_add(1) - .ok_or_else(|| anyhow::anyhow!("AgentCtl checkpoint generation exhausted"))?; - state.next_checkpoint_generation = generation; - for session in state.sessions.values_mut() { - session.checkpoint_ack = None; - } - state.active_checkpoint = Some(ActiveCheckpoint { - generation, - checkpoint_id: checkpoint_id.clone(), - deadline_unix_ms, - participants: state.sessions.keys().cloned().collect(), - }); - state.directive = AgentDirective::Quiesce { - checkpoint_id, - deadline_unix_ms, - }; - Ok(generation) - } - - /// Release clients after a checkpoint succeeds or is abandoned. - pub fn continue_execution(&self) { - let mut state = lock_state(&self.state); - if let Some(checkpoint) = &state.active_checkpoint { - let generation = checkpoint.generation; - finish_checkpoint(&mut state, generation); - } - } - - /// Ask all runtime clients to terminate. - pub fn request_shutdown(&self, reason: Option) { - let mut state = lock_state(&self.state); - state.active_checkpoint = None; - for session in state.sessions.values_mut() { - session.checkpoint_ack = None; - } - state.directive = AgentDirective::Shutdown { reason }; - } -} - -fn finish_checkpoint(state: &mut AgentCtlState, generation: u64) { - if state - .active_checkpoint - .as_ref() - .is_some_and(|checkpoint| checkpoint.generation == generation) - { - state.active_checkpoint = None; - for session in state.sessions.values_mut() { - session.checkpoint_ack = None; - } - state.directive = AgentDirective::Continue; - } -} - -/// Owns the Run-scoped Unix listener and removes it on drop. -pub struct AgentCtlServer { - stop: Arc, - join: Option>, - socket_path: PathBuf, - token: String, - control: AgentCtlControl, -} - -impl AgentCtlServer { - /// Create and start one Run-scoped AgentCtl server. - pub fn start(run_id: &RunId, attempt_id: &AttemptId) -> anyhow::Result { - // macOS `sockaddr_un` paths are capped at SUN_LEN (~104 bytes), so bind in - // the fixed, short `/tmp` directory rather than `std::env::temp_dir()`, - // which can point at a deep per-user path (e.g. `/var/folders/.../T/`). - let socket_path = Path::new("/tmp").join(format!( - "pvisor-agent-{}.sock", - uuid::Uuid::new_v4().simple() - )); - let token = uuid::Uuid::new_v4().to_string(); - let state = Arc::new(Mutex::new(AgentCtlState::new( - run_id, - attempt_id, - token.clone(), - ))); - let listener = std::os::unix::net::UnixListener::bind(&socket_path)?; - if let Err(error) = fs::set_permissions(&socket_path, fs::Permissions::from_mode(0o600)) { - let _ = fs::remove_file(&socket_path); - return Err(error.into()); - } - if let Err(error) = listener.set_nonblocking(true) { - let _ = fs::remove_file(&socket_path); - return Err(error.into()); - } - - let stop = Arc::new(AtomicBool::new(false)); - let thread_stop = Arc::clone(&stop); - let thread_state = Arc::clone(&state); - let thread_name = format!("pvisor-agentctl-{}", run_id.as_str()); - let join = std::thread::Builder::new() - .name(thread_name) - .spawn(move || { - while !thread_stop.load(Ordering::Acquire) { - match listener.accept() { - Ok((stream, _)) => serve_connection(stream, &thread_state), - Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { - std::thread::sleep(Duration::from_millis(10)); - } - Err(_) => break, - } - } - }); - let join = match join { - Ok(join) => join, - Err(error) => { - let _ = fs::remove_file(&socket_path); - return Err(error.into()); - } - }; - let control = AgentCtlControl { - endpoint: socket_path.clone(), - state, - delegated_snapshot: Arc::new(Mutex::new(None)), - }; - Ok(Self { - stop, - join: Some(join), - socket_path, - token, - control, - }) - } - - /// Return the cloneable Run control surface. - pub fn control(&self) -> AgentCtlControl { - self.control.clone() - } - - /// Return the environment injected into runtime clients. - pub fn environment(&self) -> BTreeMap { - BTreeMap::from([ - ( - persisting_agentctl::AGENTCTL_ENDPOINT_ENV.into(), - self.socket_path.display().to_string(), - ), - ( - persisting_agentctl::AGENTCTL_TOKEN_ENV.into(), - self.token.clone(), - ), - ( - persisting_agentctl::AGENTCTL_VERSION_ENV.into(), - AGENTCTL_VERSION.to_string(), - ), - ( - persisting_agentctl::AGENTCTL_TRANSPORT_ENV.into(), - "unix".into(), - ), - ]) - } -} - -impl Drop for AgentCtlServer { - fn drop(&mut self) { - self.stop.store(true, Ordering::Release); - let _ = std::os::unix::net::UnixStream::connect(&self.socket_path); - if let Some(join) = self.join.take() { - let _ = join.join(); - } - let _ = fs::remove_file(&self.socket_path); - } -} - -#[derive(Debug)] -struct ProtocolFailure { - code: AgentErrorCode, - message: String, -} - -impl ProtocolFailure { - fn new(code: AgentErrorCode, message: impl Into) -> Self { - Self { - code, - message: message.into(), - } - } -} - -fn serve_connection(mut stream: std::os::unix::net::UnixStream, state: &Arc>) { - let _ = stream.set_nonblocking(false); - let _ = stream.set_read_timeout(Some(Duration::from_secs(2))); - let _ = stream.set_write_timeout(Some(Duration::from_secs(2))); - let response = read_request(&stream) - .map_err(|error| ProtocolFailure::new(AgentErrorCode::InvalidRequest, error.to_string())) - .and_then(|request| dispatch_request(request, state)) - .unwrap_or_else(error_response); - if let Ok(mut body) = serde_json::to_vec(&response) { - body.push(b'\n'); - let _ = stream.write_all(&body); - } -} - -fn read_request(stream: &std::os::unix::net::UnixStream) -> anyhow::Result { - let mut frame = Vec::new(); - let mut reader = std::io::BufReader::new(stream); - reader - .by_ref() - .take((AGENTCTL_MAX_FRAME_BYTES + 1) as u64) - .read_until(b'\n', &mut frame)?; - if frame.len() > AGENTCTL_MAX_FRAME_BYTES { - anyhow::bail!("AgentCtl frame exceeds {AGENTCTL_MAX_FRAME_BYTES} bytes"); - } - if frame.last() == Some(&b'\n') { - frame.pop(); - } - if frame.is_empty() { - anyhow::bail!("empty AgentCtl frame"); - } - Ok(serde_json::from_slice(&frame)?) -} - -fn dispatch_request( - request: AgentRequest, - state: &Arc>, -) -> Result { - let version = match &request { - AgentRequest::Hello { version, .. } | AgentRequest::Sync { version, .. } => *version, - }; - if version != AGENTCTL_VERSION { - return Err(ProtocolFailure::new( - AgentErrorCode::VersionMismatch, - format!("AgentCtl version mismatch: expected {AGENTCTL_VERSION}, got {version}"), - )); - } - let mut state = state.lock().map_err(|_| { - ProtocolFailure::new(AgentErrorCode::Conflict, "AgentCtl state lock poisoned") - })?; - handle_request(request, &mut state) -} - -fn handle_request( - request: AgentRequest, - state: &mut AgentCtlState, -) -> Result { - match request { - AgentRequest::Hello { - token, client_id, .. - } => handle_hello(token, client_id, state), - AgentRequest::Sync { - session_id, - state: reported_state, - .. - } => handle_sync(session_id, reported_state, state), - } -} - -fn handle_hello( - token: String, - client_id: String, - state: &mut AgentCtlState, -) -> Result { - if token != state.token { - return Err(ProtocolFailure::new( - AgentErrorCode::Unauthorized, - "invalid AgentCtl token", - )); - } - if client_id.trim().is_empty() { - return Err(ProtocolFailure::new( - AgentErrorCode::InvalidRequest, - "client_id must be non-empty", - )); - } - if matches!(state.directive, AgentDirective::Quiesce { .. }) { - return Err(ProtocolFailure::new( - AgentErrorCode::Conflict, - "AgentCtl checkpoint is in progress", - )); - } - - let now = crate::util::unix_now_ms(); - state - .sessions - .retain(|_, session| !session_is_stale(session, now)); - if state - .sessions - .values() - .any(|session| session.client_id == client_id) - { - return Err(ProtocolFailure::new( - AgentErrorCode::Conflict, - format!("client {client_id} already has a live Session"), - )); - } - if state.sessions.len() >= AGENTCTL_MAX_SESSIONS { - return Err(ProtocolFailure::new( - AgentErrorCode::Conflict, - format!("AgentCtl Session limit of {AGENTCTL_MAX_SESSIONS} reached"), - )); - } - - let session_id = uuid::Uuid::new_v4().to_string(); - state.sessions.insert( - session_id.clone(), - ClientSession { - client_id, - state: AgentState::Active, - last_seen_unix_ms: now, - last_sync_unix_ms: None, - checkpoint_ack: None, - }, - ); - Ok(AgentResponse::Welcome { - session_id, - sync_interval_ms: SYNC_INTERVAL_MS, - directive: state.directive.clone(), - }) -} - -fn handle_sync( - session_id: String, - reported_state: AgentState, - state: &mut AgentCtlState, -) -> Result { - let session = state.sessions.get(&session_id).ok_or_else(|| { - ProtocolFailure::new(AgentErrorCode::Unauthorized, "unknown AgentCtl Session") - })?; - let active_checkpoint = state.active_checkpoint.as_ref().map(|checkpoint| { - ( - checkpoint.generation, - checkpoint.checkpoint_id.clone(), - checkpoint.participants.contains(&session_id), - ) - }); - if let AgentState::Quiesced { checkpoint_id } = &reported_state { - let repeated = session.state == reported_state; - let matches_active = active_checkpoint - .as_ref() - .is_some_and(|(_, active, participant)| *participant && active == checkpoint_id); - if !repeated && !matches_active { - return Err(ProtocolFailure::new( - AgentErrorCode::Conflict, - "quiesced state does not match the active checkpoint", - )); - } - } - - let now = crate::util::unix_now_ms(); - let session = state - .sessions - .get_mut(&session_id) - .expect("Session validated"); - session.checkpoint_ack = match (&reported_state, active_checkpoint) { - ( - AgentState::Quiesced { checkpoint_id }, - Some((generation, active_checkpoint_id, true)), - ) if checkpoint_id == &active_checkpoint_id => Some(match session.checkpoint_ack { - Some(ack) if ack.generation == generation => ack, - _ => CheckpointAcknowledgement { - generation, - acknowledged_at_unix_ms: now, - }, - }), - _ => None, - }; - session.state = reported_state; - session.last_seen_unix_ms = now; - session.last_sync_unix_ms = Some(now); - Ok(AgentResponse::Synced { - directive: state.directive.clone(), - }) -} - -fn session_is_stale(session: &ClientSession, now_unix_ms: u64) -> bool { - now_unix_ms.saturating_sub(session.last_seen_unix_ms) > SYNC_INTERVAL_MS * 3 -} - -fn lock_state(state: &Arc>) -> std::sync::MutexGuard<'_, AgentCtlState> { - state - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) -} - -fn error_response(error: ProtocolFailure) -> AgentResponse { - AgentResponse::Error { - code: error.code, - message: error.message, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn exchange(path: &Path, request: &AgentRequest) -> AgentResponse { - let mut stream = std::os::unix::net::UnixStream::connect(path).unwrap(); - serde_json::to_writer(&mut stream, request).unwrap(); - stream.write_all(b"\n").unwrap(); - let mut line = String::new(); - std::io::BufReader::new(stream) - .read_line(&mut line) - .unwrap(); - serde_json::from_str(&line).unwrap() - } - - fn connect(server: &AgentCtlServer, client_id: &str) -> String { - match exchange( - &server.socket_path, - &AgentRequest::Hello { - version: AGENTCTL_VERSION, - token: server.token.clone(), - client_id: client_id.into(), - }, - ) { - AgentResponse::Welcome { session_id, .. } => session_id, - response => panic!("unexpected response: {response:?}"), - } - } - - #[test] - fn rejects_invalid_token_and_protocol_version_with_typed_codes() { - let server = - AgentCtlServer::start(&RunId::new("run-1"), &AttemptId::new("attempt-1")).unwrap(); - assert!(matches!( - exchange( - &server.socket_path, - &AgentRequest::Hello { - version: AGENTCTL_VERSION, - token: "wrong".into(), - client_id: "client".into(), - }, - ), - AgentResponse::Error { - code: AgentErrorCode::Unauthorized, - .. - } - )); - assert!(matches!( - exchange( - &server.socket_path, - &AgentRequest::Hello { - version: 99, - token: server.token.clone(), - client_id: "client".into(), - }, - ), - AgentResponse::Error { - code: AgentErrorCode::VersionMismatch, - .. - } - )); - } - - #[test] - fn request_quiesce_purges_sessions_that_were_already_stale() { - let server = - AgentCtlServer::start(&RunId::new("run-1"), &AttemptId::new("attempt-1")).unwrap(); - let stale_id = connect(&server, "stale"); - let live_id = connect(&server, "live"); - { - let mut state = lock_state(&server.control.state); - state.sessions.get_mut(&stale_id).unwrap().last_seen_unix_ms = 0; - state.sessions.get_mut(&live_id).unwrap().last_seen_unix_ms = - crate::util::unix_now_ms(); - } - - server.control.request_quiesce("cp", None).unwrap(); - let snapshot = server.control.snapshot(); - assert_eq!(snapshot.clients.len(), 1); - assert_eq!(snapshot.clients[0].client_id, "live"); - } - - #[test] - fn checkpoint_never_expires_a_frozen_participant() { - let server = - AgentCtlServer::start(&RunId::new("run-1"), &AttemptId::new("attempt-1")).unwrap(); - let session_id = connect(&server, "participant"); - server.control.request_quiesce("cp", None).unwrap(); - lock_state(&server.control.state) - .sessions - .get_mut(&session_id) - .unwrap() - .last_seen_unix_ms = 0; - - let snapshot = server.control.snapshot(); - assert_eq!(snapshot.clients.len(), 1); - assert!(snapshot.clients[0].stale); - } - - #[test] - fn delegated_snapshot_preserves_outer_identity() { - let server = - AgentCtlServer::start(&RunId::new("outer-run"), &AttemptId::new("outer-attempt")) - .unwrap(); - let delegated = AgentCtlSnapshot { - run_id: "inner-run".into(), - attempt_id: "inner-attempt".into(), - directive: AgentDirective::Shutdown { reason: None }, - clients: Vec::new(), - }; - server.control.import_delegated_snapshot(delegated); - - let imported = server.control.snapshot(); - assert_eq!(imported.run_id, "outer-run"); - assert_eq!(imported.attempt_id, "outer-attempt"); - assert!(matches!( - imported.directive, - AgentDirective::Shutdown { .. } - )); - } - - #[test] - fn retrying_a_checkpoint_id_requires_a_fresh_acknowledgement() { - let server = - AgentCtlServer::start(&RunId::new("run-1"), &AttemptId::new("attempt-1")).unwrap(); - let session_id = connect(&server, "participant"); - let deadline = crate::util::unix_now_ms().saturating_add(10_000); - let first = server - .control - .begin_checkpoint("cp".into(), Some(deadline)) - .unwrap(); - let quiesced = AgentState::Quiesced { - checkpoint_id: "cp".into(), - }; - assert!(matches!( - exchange( - &server.socket_path, - &AgentRequest::Sync { - version: AGENTCTL_VERSION, - session_id: session_id.clone(), - state: quiesced.clone(), - }, - ), - AgentResponse::Synced { - directive: AgentDirective::Quiesce { .. } - } - )); - drop(first); - - let retry = server - .control - .begin_checkpoint("cp".into(), Some(deadline)) - .unwrap(); - assert!(retry.try_capture(|| Ok(())).unwrap().is_none()); - - exchange( - &server.socket_path, - &AgentRequest::Sync { - version: AGENTCTL_VERSION, - session_id, - state: quiesced, - }, - ); - assert_eq!(retry.try_capture(|| Ok(())).unwrap(), Some(())); - } - - #[test] - fn checkpoint_guard_drop_releases_only_its_own_quiesce() { - let server = - AgentCtlServer::start(&RunId::new("run-1"), &AttemptId::new("attempt-1")).unwrap(); - connect(&server, "participant"); - let checkpoint = server.control.begin_checkpoint("cp".into(), None).unwrap(); - drop(checkpoint); - assert_eq!( - server.control.snapshot().directive, - AgentDirective::Continue - ); - - let checkpoint = server - .control - .begin_checkpoint("cp-2".into(), None) - .unwrap(); - server.control.request_shutdown(Some("stop".into())); - drop(checkpoint); - assert!(matches!( - server.control.snapshot().directive, - AgentDirective::Shutdown { .. } - )); - server.control.continue_execution(); - assert!(matches!( - server.control.snapshot().directive, - AgentDirective::Shutdown { .. } - )); - } - - #[test] - fn checkpoint_copy_holds_transition_ownership_until_capture_finishes() { - let server = - AgentCtlServer::start(&RunId::new("run-1"), &AttemptId::new("attempt-1")).unwrap(); - let session_id = connect(&server, "participant"); - let checkpoint = server.control.begin_checkpoint("cp".into(), None).unwrap(); - exchange( - &server.socket_path, - &AgentRequest::Sync { - version: AGENTCTL_VERSION, - session_id, - state: AgentState::Quiesced { - checkpoint_id: "cp".into(), - }, - }, - ); - - let (capture_started_tx, capture_started_rx) = std::sync::mpsc::channel(); - let (release_capture_tx, release_capture_rx) = std::sync::mpsc::channel(); - let capture = std::thread::spawn(move || { - checkpoint - .try_capture(|| { - capture_started_tx.send(()).unwrap(); - release_capture_rx.recv().unwrap(); - Ok(()) - }) - .unwrap() - }); - capture_started_rx.recv().unwrap(); - - let control = server.control(); - let (shutdown_done_tx, shutdown_done_rx) = std::sync::mpsc::channel(); - let shutdown = std::thread::spawn(move || { - control.request_shutdown(Some("stop".into())); - shutdown_done_tx.send(()).unwrap(); - }); - assert!( - shutdown_done_rx - .recv_timeout(Duration::from_millis(30)) - .is_err() - ); - - release_capture_tx.send(()).unwrap(); - assert_eq!(capture.join().unwrap(), Some(())); - shutdown_done_rx.recv().unwrap(); - shutdown.join().unwrap(); - assert!(matches!( - server.control.snapshot().directive, - AgentDirective::Shutdown { .. } - )); - } - - #[test] - fn acknowledgement_after_deadline_cannot_satisfy_checkpoint() { - let server = - AgentCtlServer::start(&RunId::new("run-1"), &AttemptId::new("attempt-1")).unwrap(); - let session_id = connect(&server, "participant"); - let deadline = crate::util::unix_now_ms(); - let checkpoint = server - .control - .begin_checkpoint("cp".into(), Some(deadline)) - .unwrap(); - std::thread::sleep(Duration::from_millis(2)); - exchange( - &server.socket_path, - &AgentRequest::Sync { - version: AGENTCTL_VERSION, - session_id, - state: AgentState::Quiesced { - checkpoint_id: "cp".into(), - }, - }, - ); - - let error = checkpoint.try_capture(|| Ok(())).unwrap_err(); - assert!(error.to_string().contains("timed out")); - } - - #[test] - fn hello_purges_all_stale_sessions_before_enforcing_capacity() { - let server = - AgentCtlServer::start(&RunId::new("run-1"), &AttemptId::new("attempt-1")).unwrap(); - for index in 0..AGENTCTL_MAX_SESSIONS { - connect(&server, &format!("stale-{index}")); - } - for session in lock_state(&server.control.state).sessions.values_mut() { - session.last_seen_unix_ms = 0; - } - - connect(&server, "replacement"); - let snapshot = server.control.snapshot(); - assert_eq!(snapshot.clients.len(), 1); - assert_eq!(snapshot.clients[0].client_id, "replacement"); - } -} diff --git a/crates/persisting-pvisor/src/artifact.rs b/crates/persisting-pvisor/src/artifact.rs deleted file mode 100644 index c9435e807..000000000 --- a/crates/persisting-pvisor/src/artifact.rs +++ /dev/null @@ -1,76 +0,0 @@ -//! Resolution and validation of the delegated pVisor runtime injected into a -//! guest. The running executable is the default; an explicit path is only -//! needed when the guest ABI differs from the host, which is why a packaged -//! cross-target pVisor stays configurable. - -use anyhow::Context; -use std::path::{Path, PathBuf}; - -pub(crate) fn resolve_pvisor_binary(explicit: Option<&Path>) -> anyhow::Result { - if let Some(path) = explicit { - return validate_binary(path); - } - let current = std::env::current_exe().context( - "delegated execution needs a pVisor runtime, but the running executable path is \ - unavailable; set the executor pvisor_binary", - )?; - validate_binary(¤t).with_context(|| { - format!( - "the running pVisor at {} cannot be injected into the guest; set the executor \ - pvisor_binary to a guest-compatible build", - current.display() - ) - }) -} - -fn validate_binary(path: &Path) -> anyhow::Result { - anyhow::ensure!( - path.is_file(), - "pVisor runtime is not a file: {}", - path.display() - ); - anyhow::ensure!( - is_executable(path), - "pVisor runtime is not executable: {}", - path.display() - ); - Ok(path.canonicalize()?) -} - -#[cfg(unix)] -fn is_executable(path: &Path) -> bool { - use std::os::unix::fs::PermissionsExt; - path.metadata() - .is_ok_and(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0) -} - -#[cfg(not(unix))] -fn is_executable(path: &Path) -> bool { - path.is_file() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[cfg(unix)] - #[test] - fn explicit_artifact_must_be_executable() { - use std::os::unix::fs::PermissionsExt; - let temporary = tempfile::tempdir().unwrap(); - let binary = temporary.path().join("pvisor"); - std::fs::write(&binary, b"runtime").unwrap(); - assert!(resolve_pvisor_binary(Some(&binary)).is_err()); - std::fs::set_permissions(&binary, std::fs::Permissions::from_mode(0o755)).unwrap(); - assert_eq!( - resolve_pvisor_binary(Some(&binary)).unwrap(), - binary.canonicalize().unwrap() - ); - } - - #[test] - fn omitted_artifact_defaults_to_the_running_executable() { - let expected = std::env::current_exe().unwrap().canonicalize().unwrap(); - assert_eq!(resolve_pvisor_binary(None).unwrap(), expected); - } -} diff --git a/crates/persisting-pvisor/src/bin/pvisor.rs b/crates/persisting-pvisor/src/bin/pvisor.rs deleted file mode 100644 index 37d6c71c6..000000000 --- a/crates/persisting-pvisor/src/bin/pvisor.rs +++ /dev/null @@ -1,14 +0,0 @@ -fn main() -> anyhow::Result<()> { - if persisting_pvisor::run_krun_internal_if_requested()? { - return Ok(()); - } - match persisting_pvisor::sandbox::run_internal_if_requested() { - Ok(true) => return Ok(()), - Ok(false) => {} - Err(error) => { - eprintln!("pVisor local sandbox setup failed: {error:#}"); - std::process::exit(persisting_pvisor::sandbox::SANDBOX_SETUP_EXIT_CODE); - } - } - persisting_pvisor::cli::main() -} diff --git a/crates/persisting-pvisor/src/bundle.rs b/crates/persisting-pvisor/src/bundle.rs deleted file mode 100644 index a8c860f37..000000000 --- a/crates/persisting-pvisor/src/bundle.rs +++ /dev/null @@ -1,719 +0,0 @@ -//! Durable, versioned summary of one pVisor Run. - -use crate::runtime::{ - ChangeEntry, OverlayState, RunLineage, RunRecord, overlay_changes, overlay_status, -}; -use crate::sandbox::SANDBOX_SETUP_FAILED_WARNING; -use crate::util::{atomic_write, sync_directory}; -use crate::{AgentCtlSnapshot, unix_now_ms}; -use persisting_agentctl::{ - ArtifactRef, CapabilityDimension, ExecutorDescriptor, IsolationKind, ProcessOutput, - ResourceLimits, RunFailure, RunResult, RunState, -}; -use persisting_overlaynet::{InterceptionProfile, InterceptionSnapshot}; -use serde::{Deserialize, Serialize}; -use std::fs; -use std::os::unix::fs::MetadataExt; -use std::path::{Path, PathBuf}; - -pub const RUN_BUNDLE_SCHEMA_VERSION: u32 = 2; -pub const RUN_BUNDLE_FILENAME: &str = "run-bundle.json"; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RunBundle { - pub schema_version: u32, - pub generated_at_unix_ms: u64, - pub run: BundleRun, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub lineage: Option, - pub safety: SafetySummary, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub filesystem: Option, - pub network: NetworkSummary, - #[serde(default)] - pub environment: crate::runtime::EnvironmentProjection, - #[serde(default)] - pub resources: ResourceSummary, - pub agentctl: AgentCtlSnapshot, - #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")] - pub orchestration: std::collections::BTreeMap, - #[serde(default)] - pub artifacts: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BundleRun { - pub run_id: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub parent_run_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub task_id: Option, - pub attempt_id: String, - pub session_id: String, - pub agent: String, - pub command: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub executor: Option, - pub state: RunState, - pub started_at_unix_ms: u64, - pub finished_at_unix_ms: u64, - pub duration_ms: u64, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub exit_code: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub failure: Option, - #[serde(default)] - pub warnings: Vec, - #[serde(default)] - pub output: ProcessOutput, - #[serde(default)] - pub metrics: std::collections::BTreeMap, - #[serde(default)] - pub result_artifacts: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub event_stream_ref: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SafetySummary { - pub safe_profile_requested: bool, - pub host_process: bool, - pub filesystem_changes_staged: bool, - /// Both filesystem reads and writes are confined to declared roots. - pub filesystem_non_bypassable: bool, - /// Filesystem reads outside declared roots are blocked by the executor. - #[serde(default)] - pub filesystem_read_non_bypassable: bool, - /// Filesystem writes outside the staged workspace/capabilities are blocked. - #[serde(default)] - pub filesystem_write_non_bypassable: bool, - pub network_non_bypassable: bool, - #[serde(default)] - pub warnings: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FilesystemSummary { - pub state: OverlayState, - pub target: PathBuf, - pub upper: PathBuf, - pub changed_files: usize, - pub whiteouts: usize, - #[serde(default)] - pub root_overlay: bool, - #[serde(default)] - pub excluded_paths: Vec, - /// Identity of the host root used as the immutable lower view. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub host_root_device: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub host_root_inode: Option, - /// Host credentials intentionally mirrored into a full-root guest. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub host_uid: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub host_gid: Option, - #[serde(default)] - pub sample_paths: Vec, - #[serde(default)] - pub changes: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct NetworkSummary { - pub policy: serde_json::Value, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub interception: Option, - /// Present only when a driver exports final counters into the bundle. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub intercepted: Option, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct ResourceSummary { - pub requested: ResourceLimits, - pub effective: ResourceLimits, - #[serde(default)] - pub mechanisms: Vec, - #[serde(default)] - pub limitations: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BundleArtifact { - pub kind: String, - pub path: PathBuf, -} - -impl RunBundle { - pub fn capture( - record: &RunRecord, - result: &RunResult, - agentctl: AgentCtlSnapshot, - safe_profile_requested: bool, - ) -> anyhow::Result { - let filesystem = record - .overlay - .as_ref() - .map(|overlay| { - let status = overlay_status(overlay)?; - let root_overlay = overlay.target == Path::new("/"); - let root_metadata = root_overlay.then(|| fs::metadata("/")).transpose()?; - Ok::<_, anyhow::Error>(FilesystemSummary { - state: overlay.state, - target: overlay.target.clone(), - upper: overlay.upper.path().to_path_buf(), - changed_files: status.changed_files, - whiteouts: status.whiteouts, - root_overlay, - excluded_paths: overlay.excluded_paths.clone(), - host_root_device: root_metadata.as_ref().map(MetadataExt::dev), - host_root_inode: root_metadata.as_ref().map(MetadataExt::ino), - host_uid: root_overlay.then(|| unsafe { libc::geteuid() }), - host_gid: root_overlay.then(|| unsafe { libc::getegid() }), - sample_paths: status.sample_paths, - changes: overlay_changes(overlay, &record.overlay_lowers)?, - }) - }) - .transpose()?; - let filesystem_changes_staged = filesystem - .as_ref() - .is_some_and(|fs| fs.state == OverlayState::Staged); - let host_process = record - .executor - .as_ref() - .is_none_or(|executor| executor.isolation == IsolationKind::HostProcess); - let rootless_process = record - .executor - .as_ref() - .is_some_and(|executor| executor.isolation == IsolationKind::RootlessProcess); - let seatbelt_process = record - .executor - .as_ref() - .is_some_and(|executor| executor.isolation == IsolationKind::SandboxedProcess); - let virtual_machine = record - .executor - .as_ref() - .is_some_and(|executor| executor.isolation == IsolationKind::VirtualMachine); - let sandbox_setup_failed = result - .warnings - .iter() - .any(|warning| warning == SANDBOX_SETUP_FAILED_WARNING); - // Safety claims come from the concrete per-Run enforcement evidence - // persisted in the effective executor descriptor. Isolation labels and - // requested policies are descriptive and must never manufacture a - // non-bypassable claim. - let enforcement = record - .executor - .as_ref() - .map(|executor| &executor.capability_enforcement); - let network_non_bypassable = !sandbox_setup_failed - && enforcement - .is_some_and(|evidence| evidence.is_enforced(CapabilityDimension::Network)); - let filesystem_read_non_bypassable = filesystem.is_some() - && !sandbox_setup_failed - && enforcement - .is_some_and(|evidence| evidence.is_enforced(CapabilityDimension::FilesystemRead)); - let filesystem_write_non_bypassable = filesystem.is_some() - && !sandbox_setup_failed - && enforcement - .is_some_and(|evidence| evidence.is_enforced(CapabilityDimension::FilesystemWrite)); - let filesystem_non_bypassable = - filesystem_read_non_bypassable && filesystem_write_non_bypassable; - let resources = resource_summary(record, result); - let mut safety_warnings = Vec::new(); - if safe_profile_requested { - if host_process { - safety_warnings.push( - "local-process execution can access host paths outside the staged workspace" - .into(), - ); - } - if !network_non_bypassable { - safety_warnings.push( - "network policy covers cooperative proxy traffic; direct sockets may bypass it" - .into(), - ); - } - if rootless_process && !sandbox_setup_failed { - safety_warnings.push( - "filesystem access and process-tree cleanup are kernel-enforced; the host kernel and syscall surface remain shared" - .into(), - ); - } - if seatbelt_process && !sandbox_setup_failed { - safety_warnings.push( - "filesystem writes are Seatbelt-enforced; reads, the host PID namespace, syscall surface, and resource limits remain shared" - .into(), - ); - } - if virtual_machine { - safety_warnings.push( - "the libkrun guest can read the complete configured rootfs and currently runs its workload as guest root" - .into(), - ); - } - if sandbox_setup_failed { - safety_warnings.push( - "the local sandbox boundary failed before the Agent executable was started" - .into(), - ); - } - } - let mut artifacts = vec![BundleArtifact { - kind: "run-record".into(), - path: record.stage_dir().join("run.json"), - }]; - for (kind, relative) in [ - ("capture", ".capture"), - ("chronicle", "chronicle"), - ("live-markdown", "live.md"), - ] { - let path = record.storage.join(relative); - if path.exists() { - artifacts.push(BundleArtifact { - kind: kind.into(), - path, - }); - } - } - - Ok(Self { - schema_version: RUN_BUNDLE_SCHEMA_VERSION, - generated_at_unix_ms: unix_now_ms(), - run: BundleRun { - run_id: record.run_id.clone(), - parent_run_id: record.parent_run_id.clone(), - task_id: record.task_id.clone(), - attempt_id: result.attempt_id.as_str().to_string(), - session_id: record.session_id.clone(), - agent: record.agent.clone(), - command: record.command.clone(), - executor: record.executor.clone(), - state: result.state, - started_at_unix_ms: result.started_at_unix_ms, - finished_at_unix_ms: result.finished_at_unix_ms, - duration_ms: result - .finished_at_unix_ms - .saturating_sub(result.started_at_unix_ms), - exit_code: result.exit_code, - failure: result.failure.clone(), - warnings: result.warnings.clone(), - output: result.output.clone(), - metrics: result.metrics.clone(), - result_artifacts: result.artifacts.clone(), - event_stream_ref: result.event_stream_ref.clone(), - }, - lineage: record.lineage.clone(), - safety: SafetySummary { - safe_profile_requested, - host_process, - filesystem_changes_staged, - filesystem_non_bypassable, - filesystem_read_non_bypassable, - filesystem_write_non_bypassable, - network_non_bypassable, - warnings: safety_warnings, - }, - filesystem, - network: NetworkSummary { - policy: record - .network_policy - .clone() - .unwrap_or_else(|| record.network.clone()), - interception: record.network_interception.clone(), - intercepted: record.network_interception_metrics.clone(), - }, - environment: environment_summary(record), - resources, - agentctl, - orchestration: record.orchestration.clone(), - artifacts, - }) - } - - pub fn path(stage_dir: &Path) -> PathBuf { - stage_dir.join(RUN_BUNDLE_FILENAME) - } - - pub fn write(&self, stage_dir: &Path) -> anyhow::Result { - let path = Self::path(stage_dir); - atomic_write(&path, &serde_json::to_vec_pretty(self)?, 0o600)?; - Ok(path) - } - - pub fn read(stage_dir: &Path) -> anyhow::Result { - let path = Self::path(stage_dir); - let bundle: Self = serde_json::from_slice(&fs::read(&path)?)?; - anyhow::ensure!( - matches!(bundle.schema_version, 1 | RUN_BUNDLE_SCHEMA_VERSION), - "unsupported Run Bundle schema {}; expected 1 or {}", - bundle.schema_version, - RUN_BUNDLE_SCHEMA_VERSION - ); - Ok(bundle) - } - - pub(crate) fn invalidate(stage_dir: &Path) -> anyhow::Result<()> { - let path = Self::path(stage_dir); - match fs::remove_file(&path) { - Ok(()) => sync_directory(stage_dir), - Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), - Err(error) => Err(error.into()), - } - } -} - -fn resource_summary(record: &RunRecord, result: &RunResult) -> ResourceSummary { - let isolation = record.executor.as_ref().map(|executor| executor.isolation); - let mut effective = record.resource_limits.clone(); - let mut mechanisms = Vec::new(); - let mut limitations: Vec = Vec::new(); - if !record.resource_limits.is_empty() { - mechanisms.push("inherited POSIX rlimits".into()); - match isolation { - Some(IsolationKind::Container) => { - mechanisms.push("OCI memory/pids controller flags".into()); - } - Some(IsolationKind::VirtualMachine) => { - mechanisms.push("libkrun VM memory boundary".into()); - if let Some(bytes) = result.metrics.get("resource.vm_memory_bytes") { - effective.memory_bytes = Some(*bytes as u64); - } - limitations.push( - "process/open-file/file-size limits are inherited inside the guest helper" - .into(), - ); - } - _ => limitations.push( - "memory and CPU rlimits are per process/address space, not aggregate cgroup accounting" - .into(), - ), - } - if matches!( - isolation, - None | Some(IsolationKind::HostProcess) - | Some(IsolationKind::RootlessProcess) - | Some(IsolationKind::SandboxedProcess) - ) { - effective = effective_native_limits(&record.resource_limits); - } - if result.metrics.get("resource.cgroup_v2") == Some(&1.0) { - mechanisms.push("Linux cgroup v2 memory/pids controller".into()); - limitations.retain(|limitation| !limitation.contains("not aggregate cgroup")); - if record.resource_limits.cpu_time_ms.is_some() { - limitations.push( - "CPU-time budget uses inherited RLIMIT_CPU; cgroup v2 does not provide a total CPU-time ceiling" - .into(), - ); - } - } - } - ResourceSummary { - requested: record.resource_limits.clone(), - effective, - mechanisms, - limitations, - } -} - -#[cfg(unix)] -fn effective_native_limits(requested: &ResourceLimits) -> ResourceLimits { - #[cfg(target_os = "linux")] - type RlimitResource = libc::__rlimit_resource_t; - #[cfg(not(target_os = "linux"))] - type RlimitResource = libc::c_int; - - fn clamp(resource: RlimitResource, requested: Option) -> Option { - let requested = requested?; - let mut current = libc::rlimit { - rlim_cur: 0, - rlim_max: 0, - }; - if unsafe { libc::getrlimit(resource, &mut current) } != 0 { - return None; - } - Some(requested.min(current.rlim_max)) - } - ResourceLimits { - memory_bytes: clamp(libc::RLIMIT_AS, requested.memory_bytes), - processes: clamp(libc::RLIMIT_NPROC, requested.processes), - cpu_time_ms: clamp( - libc::RLIMIT_CPU, - requested - .cpu_time_ms - .map(|milliseconds| milliseconds.div_ceil(1_000)), - ) - .map(|seconds| seconds.saturating_mul(1_000)), - open_files: clamp(libc::RLIMIT_NOFILE, requested.open_files), - file_size_bytes: clamp(libc::RLIMIT_FSIZE, requested.file_size_bytes), - } -} - -#[cfg(not(unix))] -fn effective_native_limits(_requested: &ResourceLimits) -> ResourceLimits { - ResourceLimits::default() -} - -fn environment_summary(record: &RunRecord) -> crate::runtime::EnvironmentProjection { - let mut summary = record.environment.clone(); - summary.runtime_injected_keys.extend( - [ - "PERSISTING_AGENT", - "PERSISTING_AGENTCTL_ENDPOINT", - "PERSISTING_AGENTCTL_TOKEN", - "PERSISTING_AGENTCTL_TRANSPORT", - "PERSISTING_AGENTCTL_VERSION", - "PERSISTING_PVISOR_ROLE", - "PERSISTING_PVISOR_RUNTIME", - "PERSISTING_PVISOR_STORAGE", - "PERSISTING_RUN_ID", - ] - .into_iter() - .map(str::to_owned), - ); - summary.runtime_injected_keys.sort(); - summary.runtime_injected_keys.dedup(); - summary -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::runtime::{OverlayRecord, OverlayUpper}; - use persisting_agentctl::{AttemptId, CapabilityEnforcementEvidence, NetworkCapability, RunId}; - use std::os::unix::fs::PermissionsExt; - - const V1_MINIMAL_FIXTURE: &[u8] = include_bytes!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/tests/fixtures/bundles/v1-minimal.json" - )); - - #[test] - fn minimal_v1_bundle_fixture_decodes_agentctl() { - let temp = tempfile::tempdir().unwrap(); - fs::write(temp.path().join(RUN_BUNDLE_FILENAME), V1_MINIMAL_FIXTURE).unwrap(); - - let bundle = RunBundle::read(temp.path()).unwrap(); - assert_eq!(bundle.schema_version, 1); - assert_eq!(bundle.run.run_id, "run-v1-fixture"); - assert_eq!(bundle.run.state, RunState::Completed); - assert_eq!(bundle.agentctl.run_id, "run-v1-fixture"); - assert!(!bundle.safety.filesystem_read_non_bypassable); - assert!(!bundle.safety.filesystem_write_non_bypassable); - - let normalized = serde_json::to_value(bundle).unwrap(); - assert!(normalized.get("agentctl").is_some()); - assert!(normalized.get("agent_abi").is_none()); - } - - #[test] - fn unsupported_bundle_schema_is_rejected() { - let temp = tempfile::tempdir().unwrap(); - let mut fixture: serde_json::Value = serde_json::from_slice(V1_MINIMAL_FIXTURE).unwrap(); - fixture["schema_version"] = serde_json::json!(999); - fs::write( - temp.path().join(RUN_BUNDLE_FILENAME), - serde_json::to_vec_pretty(&fixture).unwrap(), - ) - .unwrap(); - - let error = RunBundle::read(temp.path()).unwrap_err(); - assert!( - error - .to_string() - .contains("unsupported Run Bundle schema 999") - ); - } - - #[test] - fn bundle_is_private_and_roundtrips() { - let temp = tempfile::tempdir().unwrap(); - let upper = temp.path().join("upper"); - fs::create_dir(&upper).unwrap(); - fs::write(upper.join("changed.txt"), b"changed").unwrap(); - let mut record = RunRecord { - schema_version: 1, - run_id: "run-1".into(), - parent_run_id: Some("job-1".into()), - task_id: Some("task-1".into()), - session_id: "run-1".into(), - agent: "codex".into(), - pid: 1, - command: vec!["codex".into()], - executor: None, - state: "completed".into(), - started_at_unix_ms: 10, - finished_at_unix_ms: Some(20), - storage: temp.path().to_path_buf(), - workspace: None, - overlaynet_listen: None, - network_interception: Some(InterceptionProfile::explicit_proxy()), - network_interception_metrics: None, - gateway_listen: None, - network: serde_json::json!({"mode": "ambient"}), - network_policy: None, - environment: Default::default(), - resource_limits: Default::default(), - overlay: Some(OverlayRecord { - id: "run-1".into(), - generation: 0, - target: temp.path().join("target"), - upper: OverlayUpper::Directory { - upper_dir: upper, - work_dir: temp.path().join("work"), - }, - merged_dir: temp.path().join("merged"), - stage_dir: temp.path().to_path_buf(), - excluded_paths: Vec::new(), - auto_apply: false, - auto_discard: false, - protect_target: false, - state: OverlayState::Staged, - }), - overlay_lowers: vec![], - lineage: None, - orchestration: std::collections::BTreeMap::from([( - "ppilot.job_id".into(), - serde_json::json!("job-1"), - )]), - }; - let result = RunResult { - run_id: RunId::new("run-1"), - attempt_id: AttemptId::new("attempt-1"), - lease_epoch: 1, - state: RunState::Completed, - started_at_unix_ms: 10, - finished_at_unix_ms: 20, - exit_code: Some(0), - failure: None, - output: Default::default(), - value: None, - metrics: Default::default(), - artifacts: vec![], - event_stream_ref: None, - warnings: vec![], - }; - let agentctl = AgentCtlSnapshot { - run_id: "run-1".into(), - attempt_id: "attempt-1".into(), - directive: crate::AgentDirective::Continue, - clients: vec![], - }; - let bundle = RunBundle::capture(&record, &result, agentctl.clone(), true).unwrap(); - let path = bundle.write(temp.path()).unwrap(); - assert_eq!(RunBundle::read(temp.path()).unwrap().run.run_id, "run-1"); - assert_eq!( - fs::metadata(path).unwrap().permissions().mode() & 0o777, - 0o600 - ); - assert_eq!(bundle.filesystem.unwrap().changed_files, 1); - assert!(!bundle.safety.network_non_bypassable); - assert_eq!(bundle.run.parent_run_id.as_deref(), Some("job-1")); - assert_eq!(bundle.run.task_id.as_deref(), Some("task-1")); - assert_eq!(bundle.orchestration["ppilot.job_id"], "job-1"); - assert!( - bundle - .environment - .runtime_injected_keys - .iter() - .any(|key| key == "PERSISTING_AGENTCTL_VERSION") - ); - - record.executor = Some(ExecutorDescriptor { - name: "libkrun-root-overlay-v1".into(), - kind: persisting_agentctl::ExecutorKind::VirtualMachine, - isolation: IsolationKind::VirtualMachine, - capability_enforcement: Default::default(), - supports_checkpoint: true, - supports_migration: false, - }); - record.network_interception = Some(InterceptionProfile::explicit_proxy()); - let cooperative_vm = RunBundle::capture(&record, &result, agentctl.clone(), true).unwrap(); - assert!(!cooperative_vm.safety.network_non_bypassable); - record.network_interception = Some(InterceptionProfile::vm_smoltcp()); - let label_only_vm = RunBundle::capture(&record, &result, agentctl.clone(), true).unwrap(); - assert!(!label_only_vm.safety.filesystem_non_bypassable); - assert!(!label_only_vm.safety.network_non_bypassable); - record.executor.as_mut().unwrap().capability_enforcement = - CapabilityEnforcementEvidence::default() - .enforced(CapabilityDimension::FilesystemRead, "test-vm-read-boundary") - .enforced( - CapabilityDimension::FilesystemWrite, - "test-vm-write-boundary", - ) - .enforced(CapabilityDimension::Network, "test-vm-network-boundary"); - let intercepted_vm = RunBundle::capture(&record, &result, agentctl.clone(), true).unwrap(); - assert!(intercepted_vm.safety.filesystem_non_bypassable); - assert!(intercepted_vm.safety.network_non_bypassable); - - record.executor = Some(ExecutorDescriptor { - name: "local-rootless-v1".into(), - kind: persisting_agentctl::ExecutorKind::Process, - isolation: IsolationKind::RootlessProcess, - capability_enforcement: Default::default(), - supports_checkpoint: false, - supports_migration: false, - }); - record.network = serde_json::to_value(NetworkCapability::Deny).unwrap(); - let label_only_rootless = - RunBundle::capture(&record, &result, agentctl.clone(), true).unwrap(); - assert!(!label_only_rootless.safety.filesystem_non_bypassable); - assert!(!label_only_rootless.safety.network_non_bypassable); - record.executor.as_mut().unwrap().capability_enforcement = - CapabilityEnforcementEvidence::default() - .enforced( - CapabilityDimension::FilesystemRead, - "test-rootless-read-boundary", - ) - .enforced( - CapabilityDimension::FilesystemWrite, - "test-rootless-write-boundary", - ) - .enforced( - CapabilityDimension::Network, - "test-rootless-network-boundary", - ); - let denied = RunBundle::capture(&record, &result, agentctl.clone(), true).unwrap(); - assert!(denied.safety.filesystem_non_bypassable); - assert!(denied.safety.network_non_bypassable); - assert!( - denied - .safety - .warnings - .iter() - .all(|warning| !warning.contains("direct sockets may bypass")) - ); - - record.executor = Some(ExecutorDescriptor { - name: "local-seatbelt-v1".into(), - kind: persisting_agentctl::ExecutorKind::Process, - isolation: IsolationKind::SandboxedProcess, - capability_enforcement: Default::default(), - supports_checkpoint: false, - supports_migration: false, - }); - record.executor.as_mut().unwrap().capability_enforcement = - CapabilityEnforcementEvidence::default() - .enforced( - CapabilityDimension::FilesystemWrite, - "test-seatbelt-write-boundary", - ) - .enforced( - CapabilityDimension::Network, - "test-seatbelt-network-boundary", - ); - let seatbelt = RunBundle::capture(&record, &result, agentctl, true).unwrap(); - assert!(!seatbelt.safety.filesystem_non_bypassable); - assert!(!seatbelt.safety.filesystem_read_non_bypassable); - assert!(seatbelt.safety.filesystem_write_non_bypassable); - assert!(seatbelt.safety.network_non_bypassable); - assert!( - seatbelt - .safety - .warnings - .iter() - .any(|warning| warning.contains("reads")) - ); - } -} diff --git a/crates/persisting-pvisor/src/checkpoint.rs b/crates/persisting-pvisor/src/checkpoint.rs deleted file mode 100644 index d58094af4..000000000 --- a/crates/persisting-pvisor/src/checkpoint.rs +++ /dev/null @@ -1,318 +0,0 @@ -//! Filesystem-backed logical checkpoints for Agent Runs. - -use crate::runtime::{ - OverlayState, RunRecord, is_live, restore_overlay_upper, snapshot_overlay_upper, -}; -use crate::unix_now_ms; -use crate::util::{atomic_write, create_dir_all_durable}; -use serde::{Deserialize, Serialize}; -use std::fs; -use std::os::unix::fs::PermissionsExt; -use std::path::{Path, PathBuf}; - -pub const CHECKPOINTS_DIR: &str = "checkpoints"; -const CHECKPOINT_FILENAME: &str = "checkpoint.json"; -const CHECKPOINT_SCHEMA_VERSION: u32 = 1; - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum CheckpointConsistency { - /// The process tree was no longer running when the filesystem was copied. - Stopped, - /// Reserved for a live AgentCtl cooperative quiescence barrier. - AgentQuiesced, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct LogicalCheckpoint { - pub schema_version: u32, - pub checkpoint_id: String, - pub run_id: String, - pub created_at_unix_ms: u64, - pub consistency: CheckpointConsistency, - pub source_stage: PathBuf, - pub upper_snapshot: PathBuf, - pub target: PathBuf, - #[serde(default)] - pub lower_dirs: Vec, - #[serde(default)] - pub protect_target: bool, -} - -impl LogicalCheckpoint { - pub fn manifest_path(&self) -> PathBuf { - self.upper_snapshot - .parent() - .unwrap_or(&self.source_stage) - .join(CHECKPOINT_FILENAME) - } - - pub fn read(path: &Path) -> anyhow::Result { - let manifest = if path.is_dir() { - path.join(CHECKPOINT_FILENAME) - } else { - path.to_path_buf() - }; - let checkpoint: Self = serde_json::from_slice(&fs::read(&manifest)?)?; - anyhow::ensure!( - checkpoint.schema_version == CHECKPOINT_SCHEMA_VERSION, - "unsupported logical checkpoint schema {}; expected {}", - checkpoint.schema_version, - CHECKPOINT_SCHEMA_VERSION - ); - Ok(checkpoint) - } -} - -pub fn create_logical_checkpoint( - record: &RunRecord, - requested_id: Option<&str>, -) -> anyhow::Result { - anyhow::ensure!( - !is_live(&record.stage_dir())?, - "Run {} is live; CLI checkpoint requires a stopped Run so it cannot copy a changing upper", - record.run_id - ); - create_checkpoint(record, requested_id, CheckpointConsistency::Stopped) -} - -pub(crate) fn create_agent_quiesced_checkpoint( - record: &RunRecord, - checkpoint_id: &str, -) -> anyhow::Result { - create_checkpoint( - record, - Some(checkpoint_id), - CheckpointConsistency::AgentQuiesced, - ) -} - -fn create_checkpoint( - record: &RunRecord, - requested_id: Option<&str>, - consistency: CheckpointConsistency, -) -> anyhow::Result { - let overlay = record - .overlay - .as_ref() - .ok_or_else(|| anyhow::anyhow!("Run {} has no OverlayFS stage", record.run_id))?; - let expected_state = match consistency { - CheckpointConsistency::Stopped => OverlayState::Staged, - CheckpointConsistency::AgentQuiesced => OverlayState::Active, - }; - anyhow::ensure!( - overlay.state == expected_state, - "Run {} filesystem is {:?}; {:?} checkpoint requires {:?}", - record.run_id, - overlay.state, - consistency, - expected_state - ); - let checkpoint_id = requested_id - .map(str::to_owned) - .unwrap_or_else(|| format!("checkpoint-{}", uuid::Uuid::new_v4().simple())); - validate_checkpoint_id(&checkpoint_id)?; - let root = record - .stage_dir() - .join(CHECKPOINTS_DIR) - .join(&checkpoint_id); - anyhow::ensure!( - !root.exists(), - "logical checkpoint already exists: {}", - root.display() - ); - create_dir_all_durable(&root)?; - fs::set_permissions(&root, fs::Permissions::from_mode(0o700))?; - let upper_snapshot = root.join("upper"); - if let Err(error) = snapshot_overlay_upper(overlay, &upper_snapshot) { - let _ = fs::remove_dir_all(&root); - return Err(error.into()); - } - let checkpoint = LogicalCheckpoint { - schema_version: CHECKPOINT_SCHEMA_VERSION, - checkpoint_id, - run_id: record.run_id.clone(), - created_at_unix_ms: unix_now_ms(), - consistency, - source_stage: record.stage_dir(), - upper_snapshot, - target: overlay.target.clone(), - lower_dirs: if record.overlay_lowers.is_empty() { - vec![overlay.target.clone()] - } else { - record.overlay_lowers.clone() - }, - protect_target: overlay.protect_target, - }; - atomic_write( - &root.join(CHECKPOINT_FILENAME), - &serde_json::to_vec_pretty(&checkpoint)?, - 0o600, - )?; - Ok(checkpoint) -} - -pub fn latest_logical_checkpoint(record: &RunRecord) -> anyhow::Result { - let root = record.stage_dir().join(CHECKPOINTS_DIR); - let mut checkpoints = if root.is_dir() { - fs::read_dir(root)? - .filter_map(Result::ok) - .filter_map(|entry| LogicalCheckpoint::read(&entry.path()).ok()) - .filter(|checkpoint| checkpoint.run_id == record.run_id) - .collect::>() - } else { - Vec::new() - }; - checkpoints.sort_by_key(|checkpoint| std::cmp::Reverse(checkpoint.created_at_unix_ms)); - checkpoints - .into_iter() - .next() - .ok_or_else(|| anyhow::anyhow!("Run {} has no logical checkpoints", record.run_id)) -} - -pub fn restore_logical_checkpoint( - checkpoint: &LogicalCheckpoint, - destination_upper: &Path, -) -> anyhow::Result<()> { - anyhow::ensure!( - checkpoint.upper_snapshot.is_dir(), - "logical checkpoint upper is missing: {}", - checkpoint.upper_snapshot.display() - ); - let source = checkpoint.upper_snapshot.canonicalize()?; - let destination = absolute_candidate(destination_upper)?; - anyhow::ensure!( - !source.starts_with(&destination) && !destination.starts_with(&source), - "checkpoint source and fork upper must not overlap: source={}, destination={}", - source.display(), - destination.display() - ); - restore_overlay_upper(&checkpoint.upper_snapshot, destination_upper)?; - Ok(()) -} - -fn absolute_candidate(path: &Path) -> anyhow::Result { - if path.exists() { - return Ok(path.canonicalize()?); - } - let absolute = if path.is_absolute() { - path.to_path_buf() - } else { - std::env::current_dir()?.join(path) - }; - let parent = absolute - .parent() - .ok_or_else(|| anyhow::anyhow!("destination upper has no parent"))? - .canonicalize()?; - let name = absolute - .file_name() - .ok_or_else(|| anyhow::anyhow!("destination upper has no final component"))?; - Ok(parent.join(name)) -} - -fn validate_checkpoint_id(id: &str) -> anyhow::Result<()> { - let trimmed = id.trim(); - anyhow::ensure!(!trimmed.is_empty(), "checkpoint id cannot be empty"); - anyhow::ensure!( - trimmed != "." && trimmed != ".." && !trimmed.contains('/') && !trimmed.contains('\\'), - "checkpoint id must be one path-safe segment" - ); - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::runtime::{OverlayRecord, OverlayUpper, RunLineage}; - use std::os::unix::fs::MetadataExt; - - fn stopped_record(root: &Path) -> RunRecord { - let target = root.join("target"); - let upper = root.join("upper"); - fs::create_dir_all(&target).unwrap(); - fs::create_dir_all(&upper).unwrap(); - RunRecord { - schema_version: 1, - run_id: "run-source".into(), - parent_run_id: None, - task_id: None, - session_id: "run-source".into(), - agent: "codex".into(), - pid: 0, - command: vec!["codex".into()], - executor: None, - state: "completed".into(), - started_at_unix_ms: 1, - finished_at_unix_ms: Some(2), - storage: root.to_path_buf(), - workspace: None, - overlaynet_listen: None, - network_interception: None, - network_interception_metrics: None, - gateway_listen: None, - network: serde_json::json!({"mode": "ambient"}), - network_policy: None, - environment: Default::default(), - resource_limits: Default::default(), - overlay: Some(OverlayRecord { - id: "run-source".into(), - generation: 0, - target: target.clone(), - upper: OverlayUpper::Directory { - upper_dir: upper, - work_dir: root.join("work"), - }, - merged_dir: root.join("merged"), - stage_dir: root.to_path_buf(), - excluded_paths: Vec::new(), - auto_apply: false, - auto_discard: false, - protect_target: false, - state: OverlayState::Staged, - }), - overlay_lowers: vec![target], - lineage: Some(RunLineage { - parent_run_id: "parent".into(), - checkpoint_id: "parent-cp".into(), - }), - orchestration: Default::default(), - } - } - - #[test] - fn stopped_checkpoint_preserves_links_and_can_seed_a_fork() { - let temp = tempfile::tempdir().unwrap(); - let mut record = stopped_record(temp.path()); - record.overlay.as_mut().unwrap().protect_target = true; - let upper = record.overlay.as_ref().unwrap().upper.path(); - fs::write(upper.join("one"), b"value").unwrap(); - fs::hard_link(upper.join("one"), upper.join("two")).unwrap(); - std::os::unix::fs::symlink("one", upper.join("link")).unwrap(); - - let checkpoint = create_logical_checkpoint(&record, Some("before-refactor")).unwrap(); - assert!(checkpoint.protect_target); - let restored = temp.path().join("restored"); - restore_logical_checkpoint(&checkpoint, &restored).unwrap(); - - assert_eq!(fs::read(restored.join("one")).unwrap(), b"value"); - assert_eq!( - fs::read_link(restored.join("link")).unwrap(), - PathBuf::from("one") - ); - assert_eq!( - fs::metadata(restored.join("one")).unwrap().ino(), - fs::metadata(restored.join("two")).unwrap().ino() - ); - assert_eq!( - latest_logical_checkpoint(&record).unwrap().checkpoint_id, - "before-refactor" - ); - } - - #[test] - fn checkpoint_ids_cannot_escape_the_stage() { - let temp = tempfile::tempdir().unwrap(); - let record = stopped_record(temp.path()); - assert!(create_logical_checkpoint(&record, Some("../escape")).is_err()); - } -} diff --git a/crates/persisting-pvisor/src/cli/env.rs b/crates/persisting-pvisor/src/cli/env.rs deleted file mode 100644 index a0ba42b08..000000000 --- a/crates/persisting-pvisor/src/cli/env.rs +++ /dev/null @@ -1,689 +0,0 @@ -//! Durable reusable execution environments backed by pVisor OverlayFS stages. - -use std::fs; -use std::path::PathBuf; -use std::process::Command; - -use anyhow::{Context, Result, bail}; -use clap::{Args, Subcommand, ValueEnum}; -use persisting_overlayfs::jujutsu_upper_dir; - -use crate::runtime::{ - ApplySelection, OverlayRecord, OverlayState, OverlayUpper, RunLease, RunRecord, all_runs, - apply_overlay_selected, discard_overlay, is_live, load_overlay_record, mount_overlay_record, - resolve_run, write_overlay_record, -}; - -#[derive(Debug, Args)] -pub struct EnvArgs { - #[command(subcommand)] - command: EnvCommand, -} - -#[derive(Debug, Subcommand)] -enum EnvCommand { - /// Create a durable environment in the ready state. - Create(CreateArgs), - /// Allow new exec/shell sessions in a stopped environment. - Start(SelectArgs), - /// Prevent new exec/shell sessions; the environment must be idle. - Stop(SelectArgs), - /// Execute one command with persistent filesystem changes. - Exec(ExecArgs), - /// Open an interactive shell with persistent filesystem changes. - Shell(SelectArgs), - /// List environments. - List(ListArgs), - /// Show environment state and filesystem summary. - Status(StatusArgs), - /// Open a read-only view or run a command in it. - Inspect(InspectArgs), - /// Apply staged filesystem changes to the target. - Apply(ApplyArgs), - /// Discard staged filesystem changes. - Drop(SelectArgs), - /// Permanently remove environment metadata and staged changes. - Delete(DeleteArgs), -} - -#[derive(Debug, Clone, Copy, Default, ValueEnum)] -enum EnvBackend { - #[default] - Directory, - Jujutsu, -} - -#[derive(Debug, Args)] -struct CreateArgs { - /// Stable environment name. - name: String, - /// Read-only filesystem base and default apply destination. - #[arg(long, value_name = "DIR", default_value = ".")] - target: PathBuf, - /// Root containing all pVisor environments. - #[arg(long, value_name = "DIR", env = "PERSISTING_ENV_HOME")] - root: Option, - #[arg(long, value_enum, default_value_t = EnvBackend::Directory)] - backend: EnvBackend, - /// Shared Jujutsu store (defaults to `/.jujutsu`). - #[arg(long, value_name = "DIR")] - jujutsu_store: Option, - #[arg(long, default_value = "agent")] - agent: String, -} - -#[derive(Debug, Clone, Args)] -struct SelectArgs { - /// Environment name or workspace path. - selector: PathBuf, - #[arg(long, value_name = "DIR", env = "PERSISTING_ENV_HOME")] - root: Option, -} - -#[derive(Debug, Args)] -struct ExecArgs { - #[command(flatten)] - select: SelectArgs, - /// Command to execute. - #[arg(last = true, required = true, allow_hyphen_values = true)] - command: Vec, -} - -#[derive(Debug, Args)] -struct ListArgs { - #[arg(long, value_name = "DIR", env = "PERSISTING_ENV_HOME")] - root: Option, - #[arg(long)] - json: bool, -} - -#[derive(Debug, Args)] -struct StatusArgs { - #[command(flatten)] - select: SelectArgs, - #[arg(long)] - json: bool, -} - -#[derive(Debug, Args)] -struct InspectArgs { - #[command(flatten)] - select: SelectArgs, - /// Command for the read-only view; defaults to $SHELL or /bin/bash. - #[arg(last = true, allow_hyphen_values = true)] - command: Vec, -} - -#[derive(Debug, Args)] -struct ApplyArgs { - #[command(flatten)] - select: SelectArgs, - #[arg(long, value_name = "DIR")] - target: Option, - /// Apply this relative path and its descendants. Repeatable. - #[arg(long = "path", value_name = "RELATIVE_PATH")] - paths: Vec, - /// Include staged paths matching this glob. Repeatable. - #[arg(long, value_name = "GLOB")] - include: Vec, - /// Exclude staged paths matching this glob. Repeatable. - #[arg(long, value_name = "GLOB")] - exclude: Vec, - /// Explicitly apply every remaining staged change. - #[arg(long)] - all: bool, -} - -#[derive(Debug, Args)] -struct DeleteArgs { - #[command(flatten)] - select: SelectArgs, - /// Confirm permanent deletion of the staged upper and metadata. - #[arg(long)] - force: bool, -} - -pub fn run(args: EnvArgs) -> Result { - match args.command { - EnvCommand::Create(args) => create(args), - EnvCommand::Start(args) => set_accepting(args, true), - EnvCommand::Stop(args) => set_accepting(args, false), - EnvCommand::Exec(args) => exec(args.select, args.command), - EnvCommand::Shell(args) => { - let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".into()); - exec(args, vec![shell]) - } - EnvCommand::List(args) => list(args), - EnvCommand::Status(args) => status(args), - EnvCommand::Inspect(args) => inspect(args), - EnvCommand::Apply(args) => apply(args), - EnvCommand::Drop(args) => drop_changes(args), - EnvCommand::Delete(args) => delete(args), - } -} - -fn create(args: CreateArgs) -> Result { - validate_name(&args.name)?; - let root = resolve_root(args.root)?; - let target = args - .target - .canonicalize() - .with_context(|| format!("resolve environment target {}", args.target.display()))?; - anyhow::ensure!(target.is_dir(), "environment target must be a directory"); - let stage = root.join(&args.name); - anyhow::ensure!( - !stage.exists(), - "environment '{}' already exists at {}", - args.name, - stage.display() - ); - anyhow::ensure!( - !stage.starts_with(&target) && !target.starts_with(&stage), - "environment root and target must not overlap: root={}, target={}", - root.display(), - target.display() - ); - fs::create_dir_all(&stage)?; - let jujutsu_store = args.jujutsu_store.unwrap_or_else(|| root.join(".jujutsu")); - let overlay = OverlayRecord { - id: args.name.clone(), - generation: 0, - target: target.clone(), - upper: match args.backend { - EnvBackend::Directory => OverlayUpper::Directory { - upper_dir: stage.join("upper"), - work_dir: stage.join("work"), - }, - EnvBackend::Jujutsu => OverlayUpper::Jujutsu { - upper_dir: jujutsu_upper_dir(&jujutsu_store, &args.name)?, - store_path: jujutsu_store, - workspace: args.name.clone(), - }, - }, - merged_dir: stage.join("merged"), - stage_dir: stage.clone(), - excluded_paths: Vec::new(), - auto_apply: false, - auto_discard: false, - protect_target: false, - state: OverlayState::Staged, - }; - let now = crate::unix_now_ms(); - RunRecord { - schema_version: 1, - run_id: args.name.clone(), - parent_run_id: None, - task_id: None, - session_id: args.name.clone(), - agent: args.agent, - pid: 0, - command: Vec::new(), - executor: None, - state: "ready".into(), - started_at_unix_ms: now, - finished_at_unix_ms: Some(now), - storage: root, - workspace: Some(target.clone()), - overlaynet_listen: None, - network_interception: None, - network_interception_metrics: None, - gateway_listen: None, - network: serde_json::json!({"mode": "host"}), - network_policy: None, - environment: Default::default(), - resource_limits: Default::default(), - overlay: Some(overlay), - overlay_lowers: vec![target], - lineage: None, - orchestration: Default::default(), - } - .write()?; - println!("created environment '{}' at {}", args.name, stage.display()); - Ok(0) -} - -fn set_accepting(args: SelectArgs, accepting: bool) -> Result { - let record = selected(&args)?; - ensure_idle(&record)?; - let _lease = RunLease::acquire(&record.stage_dir())?; - let mut record = reload_after_lease(record)?; - let overlay = current_overlay(&record)?; - anyhow::ensure!( - matches!(overlay.state, OverlayState::Staged), - "environment filesystem is {:?}; it cannot be started or stopped", - overlay.state - ); - record.state = if accepting { "ready" } else { "stopped" }.into(); - record.write()?; - println!( - "environment '{}' is {}", - record.run_id, - if accepting { "ready" } else { "stopped" } - ); - Ok(0) -} - -fn exec(args: SelectArgs, command: Vec) -> Result { - let record = selected(&args)?; - ensure_idle(&record)?; - let lease = RunLease::acquire(&record.stage_dir())?; - let mut record = reload_after_lease(record)?; - anyhow::ensure!( - record.state == "ready", - "environment '{}' is {}; run `pvisor env start {}` first", - record.run_id, - record.state, - record.run_id - ); - let overlay = current_overlay(&record)?; - anyhow::ensure!( - overlay.state == OverlayState::Staged, - "environment filesystem is {:?}; exec requires staged changes", - overlay.state - ); - let lowers = if record.overlay_lowers.is_empty() { - vec![overlay.target.clone()] - } else { - record.overlay_lowers.clone() - }; - let mount = mount_overlay_record(&overlay, &lowers)?; - let (program, program_args) = command - .split_first() - .context("missing environment command")?; - let mut child = Command::new(program) - .args(program_args) - .current_dir(mount.mountpoint()) - .env("PERSISTING_ENV", &record.run_id) - .env("PERSISTING_RUN_ID", &record.run_id) - .spawn() - .with_context(|| format!("execute environment command `{program}`"))?; - record.pid = child.id(); - record.command = command; - record.state = "running".into(); - record.started_at_unix_ms = crate::unix_now_ms(); - record.finished_at_unix_ms = None; - record.overlay = Some(mount.record().clone()); - record.write()?; - - let status = child.wait().context("wait for environment command"); - let staged = mount.unmount()?; - record.pid = 0; - record.state = "ready".into(); - record.finished_at_unix_ms = Some(crate::unix_now_ms()); - record.overlay = Some(staged); - record.write()?; - drop(lease); - let status = status?; - Ok(status.code().unwrap_or(1)) -} - -fn list(args: ListArgs) -> Result { - let root = resolve_root(args.root)?; - let records = all_runs(&root)?; - if args.json { - println!("{}", serde_json::to_string_pretty(&records)?); - } else if records.is_empty() { - println!("no environments under {}", root.display()); - } else { - println!("NAME\tSTATE\tTARGET"); - for record in records { - let target = record - .overlay - .as_ref() - .map(|overlay| overlay.target.display().to_string()) - .unwrap_or_else(|| "-".into()); - println!("{}\t{}\t{}", record.run_id, record.state, target); - } - } - Ok(0) -} - -fn status(args: StatusArgs) -> Result { - let root = resolve_root(args.select.root)?; - super::runtime::status(super::runtime::StatusArgs { - selector: Some(args.select.selector), - output_dir: root, - json: args.json, - })?; - Ok(0) -} - -fn inspect(args: InspectArgs) -> Result { - let root = resolve_root(args.select.root)?; - let record = resolve_run(Some(&args.select.selector), &root)?; - ensure_idle(&record)?; - let _lease = RunLease::acquire(&record.stage_dir())?; - super::runtime::inspect(super::runtime::InspectArgs { - selector: Some(args.select.selector), - output_dir: root, - command: args.command, - }) -} - -fn apply(args: ApplyArgs) -> Result { - if args.all && (!args.paths.is_empty() || !args.include.is_empty() || !args.exclude.is_empty()) - { - bail!("--all cannot be combined with --path, --include, or --exclude"); - } - let record = selected(&args.select)?; - ensure_idle(&record)?; - let _lease = RunLease::acquire(&record.stage_dir())?; - let mut record = reload_after_lease(record)?; - let mut overlay = current_overlay(&record)?; - if let Some(target) = args.target { - fs::create_dir_all(&target) - .with_context(|| format!("create apply target {}", target.display()))?; - let target = target - .canonicalize() - .with_context(|| format!("resolve apply target {}", target.display()))?; - let stage = record - .stage_dir() - .canonicalize() - .unwrap_or_else(|_| record.stage_dir()); - anyhow::ensure!( - !target.starts_with(&stage) && !stage.starts_with(&target), - "apply target must not overlap environment stage" - ); - overlay.target = target.clone(); - record.overlay_lowers = vec![target]; - } - let selection = ApplySelection { - paths: args.paths, - includes: args.include, - excludes: args.exclude, - }; - let lowers = if record.overlay_lowers.is_empty() { - vec![overlay.target.clone()] - } else { - record.overlay_lowers.clone() - }; - let outcome = apply_overlay_selected(&mut overlay, &lowers, &selection)?; - if outcome.remaining.is_empty() { - overlay = reset_overlay_generation(overlay)?; - } - record.overlay = Some(overlay); - record.state = "ready".into(); - record.write()?; - println!( - "applied {} changes from environment '{}' (apply_id={}, remaining={})", - outcome.applied.len(), - record.run_id, - outcome.apply_id, - outcome.remaining.len() - ); - Ok(0) -} - -fn drop_changes(args: SelectArgs) -> Result { - let record = selected(&args)?; - ensure_idle(&record)?; - let _lease = RunLease::acquire(&record.stage_dir())?; - let mut record = reload_after_lease(record)?; - let mut overlay = current_overlay(&record)?; - discard_overlay(&mut overlay)?; - overlay = reset_overlay_generation(overlay)?; - record.overlay = Some(overlay); - record.state = "ready".into(); - record.write()?; - println!( - "dropped changes for environment '{}' and reset its stage", - record.run_id - ); - Ok(0) -} - -fn delete(args: DeleteArgs) -> Result { - anyhow::ensure!(args.force, "environment delete requires --force"); - let root = resolve_root(args.select.root)?; - let record = resolve_run(Some(&args.select.selector), &root)?; - ensure_idle(&record)?; - let _lease = RunLease::acquire(&record.stage_dir())?; - let stage = record.stage_dir(); - let root = root.canonicalize().unwrap_or(root); - let stage = stage.canonicalize().unwrap_or(stage); - anyhow::ensure!( - stage.starts_with(&root) && stage != root, - "refusing to delete environment outside root: {}", - stage.display() - ); - fs::remove_dir_all(&stage) - .with_context(|| format!("delete environment stage {}", stage.display()))?; - record.remove_index()?; - println!("deleted environment '{}'", record.run_id); - Ok(0) -} - -fn selected(args: &SelectArgs) -> Result { - let root = resolve_root(args.root.clone())?; - let mut record = resolve_run(Some(&args.selector), &root)?; - if record.state == "running" && !is_live(&record.stage_dir())? { - record.pid = 0; - record.state = "ready".into(); - record.finished_at_unix_ms = Some(crate::unix_now_ms()); - record.overlay = Some(current_overlay(&record)?); - record.write()?; - } - Ok(record) -} - -fn current_overlay(record: &RunRecord) -> Result { - match load_overlay_record(&record.stage_dir()) { - Ok(overlay) => Ok(overlay), - Err(_) => record - .overlay - .clone() - .context("environment has no OverlayFS stage"), - } -} - -fn reload_after_lease(record: RunRecord) -> Result { - let stage = record.stage_dir(); - let mut current = RunRecord::read(&stage) - .with_context(|| format!("reload environment metadata from {}", stage.display()))?; - let overlay = current_overlay(¤t)?; - if matches!( - overlay.state, - OverlayState::Applied | OverlayState::Discarded - ) { - current.overlay = Some(reset_overlay_generation(overlay)?); - current.state = "ready".into(); - current.write()?; - } else { - current.overlay = Some(overlay); - } - Ok(current) -} - -fn reset_overlay_generation(mut overlay: OverlayRecord) -> Result { - anyhow::ensure!( - matches!( - overlay.state, - OverlayState::Applied | OverlayState::Discarded - ), - "overlay generation reset requires a terminal state, found {:?}", - overlay.state - ); - overlay.generation = overlay - .generation - .checked_add(1) - .context("environment overlay generation overflow")?; - overlay.state = OverlayState::Staged; - write_overlay_record(&overlay)?; - Ok(overlay) -} - -fn ensure_idle(record: &RunRecord) -> Result<()> { - anyhow::ensure!( - !is_live(&record.stage_dir())?, - "environment '{}' has a running command", - record.run_id - ); - Ok(()) -} - -fn resolve_root(root: Option) -> Result { - let root = root - .or_else(|| { - std::env::var_os("HOME") - .map(PathBuf::from) - .map(|home| home.join(".persisting/envs")) - }) - .context("cannot determine environment root; pass --root or set PERSISTING_ENV_HOME")?; - fs::create_dir_all(&root)?; - Ok(root.canonicalize().unwrap_or(root)) -} - -fn validate_name(name: &str) -> Result<()> { - if name.is_empty() || name == "." || name == ".." || name.contains('/') || name.contains('\\') { - bail!("environment name must be one non-empty path segment"); - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn create_list_stop_start_and_delete_metadata() -> Result<()> { - let temp = tempfile::tempdir()?; - let root = temp.path().join("envs"); - let target = temp.path().join("target"); - fs::create_dir(&target)?; - create(CreateArgs { - name: "demo".into(), - target, - root: Some(root.clone()), - backend: EnvBackend::Directory, - jujutsu_store: None, - agent: "test".into(), - })?; - let select = SelectArgs { - selector: PathBuf::from("demo"), - root: Some(root.clone()), - }; - assert_eq!(selected(&select)?.state, "ready"); - set_accepting(select.clone(), false)?; - assert_eq!(selected(&select)?.state, "stopped"); - set_accepting(select.clone(), true)?; - assert_eq!(selected(&select)?.state, "ready"); - list(ListArgs { - root: Some(root.clone()), - json: false, - })?; - delete(DeleteArgs { - select, - force: true, - })?; - assert!(!root.join("demo").exists()); - assert!(!root.join(".pvisor/runs/64656d6f.json").exists()); - Ok(()) - } - - #[test] - fn apply_and_drop_reset_environment_for_reuse() -> Result<()> { - let temp = tempfile::tempdir()?; - let root = temp.path().join("envs"); - let target = temp.path().join("target"); - fs::create_dir(&target)?; - create(CreateArgs { - name: "demo".into(), - target: target.clone(), - root: Some(root.clone()), - backend: EnvBackend::Directory, - jujutsu_store: None, - agent: "test".into(), - })?; - let select = SelectArgs { - selector: PathBuf::from("demo"), - root: Some(root), - }; - let record = selected(&select)?; - let overlay = record.overlay.context("overlay")?; - let OverlayUpper::Directory { upper_dir, .. } = &overlay.upper else { - unreachable!("directory fixture") - }; - fs::create_dir_all(upper_dir)?; - fs::write(upper_dir.join("committed.txt"), b"value")?; - fs::write(upper_dir.join("later.txt"), b"later")?; - write_overlay_record(&overlay)?; - - apply(ApplyArgs { - select: select.clone(), - target: None, - paths: vec!["committed.txt".into()], - include: Vec::new(), - exclude: Vec::new(), - all: false, - })?; - assert_eq!(fs::read(target.join("committed.txt"))?, b"value"); - assert!(!target.join("later.txt").exists()); - assert!(upper_dir.join("later.txt").exists()); - let after_partial = selected(&select)?.overlay.context("overlay")?; - assert_eq!(after_partial.state, OverlayState::Staged); - assert_eq!(after_partial.generation, 0); - - apply(ApplyArgs { - select: select.clone(), - target: None, - paths: Vec::new(), - include: Vec::new(), - exclude: Vec::new(), - all: true, - })?; - assert_eq!(fs::read(target.join("later.txt"))?, b"later"); - let after_apply = selected(&select)?.overlay.context("overlay")?; - assert_eq!(after_apply.state, OverlayState::Staged); - assert_eq!(after_apply.generation, 1); - - let OverlayUpper::Directory { upper_dir, .. } = &after_apply.upper else { - unreachable!("directory fixture") - }; - fs::create_dir_all(upper_dir)?; - fs::write(upper_dir.join("discarded.txt"), b"value")?; - drop_changes(select.clone())?; - assert!(!target.join("discarded.txt").exists()); - let after_drop = selected(&select)?.overlay.context("overlay")?; - assert_eq!(after_drop.state, OverlayState::Staged); - assert_eq!(after_drop.generation, 2); - Ok(()) - } - - #[test] - fn names_cannot_escape_root() { - assert!(validate_name("../escape").is_err()); - assert!(validate_name("nested/name").is_err()); - assert!(validate_name("valid-name").is_ok()); - } - - #[test] - fn reload_after_lease_recovers_interrupted_terminal_reset() -> Result<()> { - let temp = tempfile::tempdir()?; - let root = temp.path().join("envs"); - let target = temp.path().join("target"); - fs::create_dir(&target)?; - create(CreateArgs { - name: "recover".into(), - target, - root: Some(root.clone()), - backend: EnvBackend::Directory, - jujutsu_store: None, - agent: "test".into(), - })?; - let select = SelectArgs { - selector: PathBuf::from("recover"), - root: Some(root), - }; - let record = selected(&select)?; - let mut overlay = current_overlay(&record)?; - discard_overlay(&mut overlay)?; - assert_eq!(overlay.state, OverlayState::Discarded); - - let _lease = RunLease::acquire(&record.stage_dir())?; - let recovered = reload_after_lease(record)?; - let overlay = recovered.overlay.context("overlay")?; - assert_eq!(overlay.state, OverlayState::Staged); - assert_eq!(overlay.generation, 1); - Ok(()) - } -} diff --git a/crates/persisting-pvisor/src/cli/mod.rs b/crates/persisting-pvisor/src/cli/mod.rs deleted file mode 100644 index ab52f328c..000000000 --- a/crates/persisting-pvisor/src/cli/mod.rs +++ /dev/null @@ -1,291 +0,0 @@ -//! Standalone `pvisor` command-line frontend. - -mod env; -mod product; -mod replay; -mod run; -pub mod runtime; -mod trajectory; - -use clap::{Parser, Subcommand}; - -#[cfg(target_os = "linux")] -const ROOT_ABOUT: &str = - "Foreground Agent Run manager with rootless Linux sandboxing and reviewable workspaces"; -#[cfg(target_os = "linux")] -const ROOT_LONG_ABOUT: &str = "Foreground Agent Run manager: execute, control, Gateway, and OverlayFS.\n\nOn Linux, host runs use safe-best-effort rootless isolation when supported: user and mount namespaces, a minimal synthetic root with chroot, Landlock, no_new_privs, and dropped capabilities. Add `--overlaynet-deny-all` to isolate direct network sockets in a private network namespace."; - -#[cfg(target_os = "macos")] -const ROOT_ABOUT: &str = - "Foreground Agent Run manager with Seatbelt isolation and reviewable workspaces"; -#[cfg(target_os = "macos")] -const ROOT_LONG_ABOUT: &str = "Foreground Agent Run manager: execute, control, Gateway, and OverlayFS.\n\nOn macOS, host runs use safe-best-effort macFUSE workspace views and Seatbelt confinement when supported. Full-disk reads remain available for local toolchain compatibility. `--overlaynet-deny-all` also blocks non-loopback IP and ambient host Unix sockets while retaining loopback proxy access and Run-local IPC."; - -#[cfg(not(any(target_os = "linux", target_os = "macos")))] -const ROOT_ABOUT: &str = "Foreground Agent Run manager with staged, reviewable workspaces"; -#[cfg(not(any(target_os = "linux", target_os = "macos")))] -const ROOT_LONG_ABOUT: &str = - "Foreground Agent Run manager: execute, control, Gateway, and OverlayFS."; - -#[derive(Debug, Parser)] -#[command( - name = "pvisor", - version, - about = ROOT_ABOUT, - long_about = ROOT_LONG_ABOUT -)] -struct Cli { - #[command(subcommand)] - command: Command, -} - -#[derive(Debug, Subcommand)] -enum Command { - #[command( - about = run::RUN_COMMAND_ABOUT, - long_about = run::RUN_COMMAND_LONG_ABOUT - )] - Run(Box), - /// Replay an agent-native trajectory in this sandbox, then continue the same Agent. - Replay(Box), - /// Manage durable reusable execution environments. - Env(env::EnvArgs), - /// Show the selected Run's process, filesystem, and network status. - Status(runtime::StatusArgs), - /// Open a read-only shell or run a command against a Run filesystem view. - Inspect(runtime::InspectArgs), - /// Review the durable Run Bundle before accepting filesystem changes. - Review(product::ReviewArgs), - /// Create a stopped-consistent logical filesystem checkpoint. - Checkpoint(product::CheckpointArgs), - /// Start a new safe Run from a logical checkpoint. - Fork(run::ForkArgs), - /// Apply a stopped Run's staged filesystem changes to its target. - Apply(runtime::ApplyArgs), - /// Drop a stopped Run's staged filesystem changes. - Drop(runtime::SelectArgs), -} - -pub fn main() -> anyhow::Result<()> { - let args = normalize_default_run(std::env::args_os().collect()); - match Cli::parse_from(args).command { - Command::Run(args) => { - let code = tokio::runtime::Runtime::new()?.block_on(run::run(*args))?; - if code != 0 { - std::process::exit(code); - } - } - Command::Replay(args) => { - let code = replay::run(*args); - if code != 0 { - std::process::exit(code); - } - } - Command::Env(args) => { - let code = env::run(args)?; - if code != 0 { - std::process::exit(code); - } - } - Command::Status(args) => runtime::status(args)?, - Command::Inspect(args) => { - let code = runtime::inspect(args)?; - if code != 0 { - std::process::exit(code); - } - } - Command::Review(args) => product::review(args)?, - Command::Checkpoint(args) => product::checkpoint(args)?, - Command::Fork(args) => { - let code = tokio::runtime::Runtime::new()?.block_on(run::fork(args))?; - if code != 0 { - std::process::exit(code); - } - } - Command::Apply(args) => runtime::apply(args)?, - Command::Drop(args) => runtime::drop_overlay(args)?, - } - Ok(()) -} - -fn normalize_default_run(mut args: Vec) -> Vec { - let first = args.get(1).and_then(|value| value.to_str()); - let reserved = [ - "run", - "replay", - "env", - "status", - "inspect", - "review", - "checkpoint", - "fork", - "apply", - "drop", - "help", - ]; - if first.is_some_and(|value| { - !reserved.contains(&value) && value != "--help" && value != "-h" && value != "--version" - }) { - args.insert(1, "run".into()); - } - args -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn standalone_cli_is_small_and_run_can_be_explicit() { - for args in [ - vec!["pvisor", "status"], - vec!["pvisor", "inspect", "run-1", "--", "rg", "TODO"], - vec!["pvisor", "review", "run-1"], - vec!["pvisor", "checkpoint", "run-1", "--name", "before"], - vec!["pvisor", "fork", "run-1", "--", "codex"], - vec!["pvisor", "apply", "run-1"], - vec!["pvisor", "apply", "run-1", "--target", "/tmp/restored"], - vec!["pvisor", "apply", "run-1", "--path", "src"], - vec![ - "pvisor", - "apply", - "run-1", - "--include", - "src/**", - "--exclude", - "src/generated/**", - ], - vec!["pvisor", "drop", "run-1"], - vec!["pvisor", "env", "create", "demo", "--target", "/tmp"], - vec!["pvisor", "env", "exec", "demo", "--", "/bin/true"], - vec!["pvisor", "env", "shell", "demo"], - vec!["pvisor", "env", "list"], - vec!["pvisor", "env", "status", "demo"], - vec!["pvisor", "env", "delete", "demo", "--force"], - vec!["pvisor", "run", "--", "/usr/bin/true"], - vec!["pvisor", "run", "/usr/bin/true"], - vec![ - "pvisor", - "replay", - "--agent", - "claude-code", - "--trajectory", - "/input/session.jsonl", - "--after-step", - "30", - ], - vec![ - "pvisor", - "replay", - "--agent", - "claude-code", - "--trajectory", - "/input/session.jsonl", - "--after-step", - "30", - "--boundary-user-prompt", - "Review the fresh observation.", - "--agent-entrypoint", - "/usr/bin/claude", - "--overlayfs-path", - "/workspace", - ], - ] { - Cli::try_parse_from(args).expect("valid pvisor command"); - } - } - - #[test] - fn replay_rejects_removed_chronicle_flag() { - let error = Cli::try_parse_from([ - "pvisor", - "replay", - "--agent", - "claude-code", - "--trajectory", - "/input/session.jsonl", - "--after-step", - "30", - "--chronicle-mode", - "off", - ]) - .unwrap_err(); - assert!(error.to_string().contains("--chronicle-mode")); - } - - #[test] - fn replay_modes_are_mutually_exclusive_cli_flags() { - for mode in ["--prepare-only", "--replay-only"] { - Cli::try_parse_from([ - "pvisor", - "replay", - "--agent", - "claude-code", - "--trajectory", - "/input/session.jsonl", - "--after-step", - "1", - mode, - ]) - .expect("individual replay mode flag must be accepted"); - } - - let error = Cli::try_parse_from([ - "pvisor", - "replay", - "--agent", - "claude-code", - "--trajectory", - "/input/session.jsonl", - "--after-step", - "1", - "--prepare-only", - "--replay-only", - ]) - .unwrap_err(); - assert!(error.to_string().contains("cannot be used with")); - } - - #[test] - fn replay_help_describes_phase_modes() { - let help = Cli::try_parse_from(["pvisor", "replay", "--help"]) - .unwrap_err() - .to_string(); - - assert!(help.contains("--prepare-only")); - assert!(help.contains("without executing tools or starting an Agent")); - assert!(help.contains("--replay-only")); - assert!(help.contains("stop before the next model request")); - assert!(help.contains("--allow-stale-observations")); - assert!(help.contains("--boundary-user-prompt")); - assert!(help.contains("after the replayed boundary observation")); - assert!(help.contains("including the replayed prefix and any live continuation")); - } - - #[test] - fn unknown_first_token_becomes_default_run() { - let args = normalize_default_run(vec!["pvisor".into(), "/bin/true".into()]); - assert_eq!(args[1], "run"); - } - - #[test] - fn root_help_names_the_effective_platform_boundary() { - let help = Cli::try_parse_from(["pvisor", "--help"]) - .unwrap_err() - .to_string(); - - #[cfg(target_os = "linux")] - { - assert!(help.contains("safe-best-effort")); - assert!(help.contains("rootless isolation")); - assert!(help.contains("namespace")); - assert!(help.contains("Landlock")); - } - #[cfg(target_os = "macos")] - { - assert!(help.contains("macFUSE")); - assert!(help.contains("Seatbelt")); - assert!(help.contains("Full-disk reads remain available")); - } - } -} diff --git a/crates/persisting-pvisor/src/cli/product.rs b/crates/persisting-pvisor/src/cli/product.rs deleted file mode 100644 index 9502c1181..000000000 --- a/crates/persisting-pvisor/src/cli/product.rs +++ /dev/null @@ -1,422 +0,0 @@ -//! Product-facing review and logical checkpoint commands. - -use crate::runtime::{RunRecord, resolve_run}; -use crate::{ChangeEntryType, ChangeKind, RunBundle, create_logical_checkpoint}; -use anyhow::Context; -use clap::Args; -use std::collections::BTreeMap; -use std::fs; -use std::path::{Path, PathBuf}; -use std::process::Command; - -const DEFAULT_STORAGE: &str = ".persisting/capture"; -const DEFAULT_DIFF_BYTES: usize = 256 * 1024; -const DEFAULT_DIFF_FILE_BYTES: u64 = 1024 * 1024; -const REVIEW_PATH_LIMIT: usize = 200; - -#[derive(Debug, Clone, Args)] -pub struct ReviewArgs { - /// Run id, project workspace, run.json, or a path inside the Run filesystem. - pub selector: Option, - #[arg(long, short = 'o', default_value = DEFAULT_STORAGE)] - pub output_dir: PathBuf, - /// Emit the complete versioned Run Bundle. - #[arg(long)] - pub json: bool, - /// Show bounded unified text diffs after the classified change list. - #[arg(long, conflicts_with = "json")] - pub diff: bool, - /// Maximum total bytes emitted by --diff. - #[arg(long, default_value_t = DEFAULT_DIFF_BYTES, requires = "diff")] - pub max_diff_bytes: usize, - /// Skip content diff for any file larger than this many bytes. - #[arg(long, default_value_t = DEFAULT_DIFF_FILE_BYTES, requires = "diff")] - pub max_diff_file_bytes: u64, -} - -#[derive(Debug, Clone, Args)] -pub struct CheckpointArgs { - /// Stopped Run id, project workspace, or run.json. - pub selector: Option, - #[arg(long, short = 'o', default_value = DEFAULT_STORAGE)] - pub output_dir: PathBuf, - /// Stable checkpoint name; generated when omitted. - #[arg(long, value_name = "NAME")] - pub name: Option, - #[arg(long)] - pub json: bool, -} - -pub fn review(args: ReviewArgs) -> anyhow::Result<()> { - let record = selected(args.selector.as_deref(), &args.output_dir)?; - let bundle = RunBundle::read(&record.stage_dir()).with_context(|| { - format!( - "Run {} has no readable Run Bundle; re-run it with this pVisor version", - record.run_id - ) - })?; - if args.json { - println!("{}", serde_json::to_string_pretty(&bundle)?); - return Ok(()); - } - - println!("pVisor review — {}", bundle.run.run_id); - println!( - "outcome: {:?} (exit {:?})", - bundle.run.state, bundle.run.exit_code - ); - println!("agent: {}", bundle.run.agent); - println!("duration: {} ms", bundle.run.duration_ms); - if bundle.run.parent_run_id.is_some() || bundle.run.task_id.is_some() { - println!( - "orchestration: parent={} task={}", - bundle.run.parent_run_id.as_deref().unwrap_or("-"), - bundle.run.task_id.as_deref().unwrap_or("-") - ); - } - for (key, value) in &bundle.orchestration { - println!(" {key}: {value}"); - } - if let Some(lineage) = &bundle.lineage { - println!( - "lineage: {} @ {}", - lineage.parent_run_id, lineage.checkpoint_id - ); - } - - println!("\nSafety boundary"); - println!( - " filesystem: {}", - if bundle.safety.filesystem_non_bypassable { - "kernel-enforced read/write roots with staged workspace" - } else if bundle.safety.filesystem_write_non_bypassable { - "kernel-enforced staged writes; reads remain ambient" - } else if bundle.safety.filesystem_changes_staged { - "changes staged for review" - } else { - "no staged change set" - } - ); - println!( - " network: {}", - if bundle.safety.network_non_bypassable { - "non-bypassable enforcement" - } else if bundle.network.interception.is_some() { - "cooperative proxy coverage" - } else { - "host network" - } - ); - println!( - " process: {}", - match bundle - .run - .executor - .as_ref() - .map(|executor| executor.isolation) - { - Some(persisting_agentctl::IsolationKind::RootlessProcess) => { - "rootless user namespace + Landlock" - } - Some(persisting_agentctl::IsolationKind::SandboxedProcess) => { - "macOS Seatbelt process sandbox" - } - Some(persisting_agentctl::IsolationKind::Container) => { - "OCI container with injected pVisor" - } - Some(persisting_agentctl::IsolationKind::VirtualMachine) => { - "libkrun/KVM guest over the pVisor root OverlayFS" - } - _ => "host process (not a host-isolation boundary)", - } - ); - for warning in &bundle.safety.warnings { - println!(" warning: {warning}"); - } - if let Some(metrics) = &bundle.network.intercepted { - println!( - " intercepted: {} requests ({} allowed, {} denied, {} failures)", - metrics.requests_seen, metrics.policy_allowed, metrics.policy_denied, metrics.failures - ); - } - - println!("\nChanges"); - if let Some(filesystem) = &bundle.filesystem { - println!( - " {} changed paths, {} deletions/whiteouts", - filesystem.changed_files, filesystem.whiteouts - ); - println!(" target: {}", filesystem.target.display()); - let mut counts = BTreeMap::new(); - for change in &filesystem.changes { - *counts.entry(change.kind).or_insert(0usize) += 1; - } - if !counts.is_empty() { - println!( - " classified: {} added, {} modified, {} deleted, {} type-changed, {} opaque", - counts.get(&ChangeKind::Added).copied().unwrap_or(0), - counts.get(&ChangeKind::Modified).copied().unwrap_or(0), - counts.get(&ChangeKind::Deleted).copied().unwrap_or(0), - counts.get(&ChangeKind::TypeChanged).copied().unwrap_or(0), - counts.get(&ChangeKind::Opaque).copied().unwrap_or(0), - ); - } - for change in filesystem.changes.iter().take(REVIEW_PATH_LIMIT) { - let code = match change.kind { - ChangeKind::Added => "A", - ChangeKind::Modified => "M", - ChangeKind::Deleted => "D", - ChangeKind::TypeChanged => "T", - ChangeKind::Opaque => "O", - }; - let mode = change - .mode - .map(|mode| format!(" mode={mode:04o}")) - .unwrap_or_default(); - println!(" {code} {}{mode}", change.path); - } - if filesystem.changes.len() > REVIEW_PATH_LIMIT { - println!( - " … {} more paths; use --json for the complete manifest", - filesystem.changes.len() - REVIEW_PATH_LIMIT - ); - } else if filesystem.changes.is_empty() { - for path in &filesystem.sample_paths { - println!(" - {path}"); - } - } - } else { - println!(" host filesystem; no transactional change set"); - } - - println!("\nObserved Agent state"); - println!(" AgentCtl clients: {}", bundle.agentctl.clients.len()); - println!("\nEnvironment and resources"); - println!( - " host environment inherited: {}", - bundle.environment.inherits_host - ); - println!( - " projected env keys: {}", - if bundle.environment.projected_keys.is_empty() { - "-".into() - } else { - bundle.environment.projected_keys.join(", ") - } - ); - println!( - " runtime-injected env keys: {}", - if bundle.environment.runtime_injected_keys.is_empty() { - "-".into() - } else { - bundle.environment.runtime_injected_keys.join(", ") - } - ); - println!( - " requested limits: {}", - serde_json::to_string(&bundle.resources.requested)? - ); - println!( - " effective limits: {}", - serde_json::to_string(&bundle.resources.effective)? - ); - if !bundle.resources.mechanisms.is_empty() { - println!(" mechanisms: {}", bundle.resources.mechanisms.join(", ")); - } - for limitation in &bundle.resources.limitations { - println!(" limitation: {limitation}"); - } - if let Some(failure) = &bundle.run.failure { - println!("\nFailure\n {:?}: {}", failure.kind, failure.message); - } - println!( - "\nBundle: {}", - RunBundle::path(&record.stage_dir()).display() - ); - if bundle.filesystem.is_some() { - println!("Next:"); - println!(" pvisor inspect {}", record.stage_dir().display()); - println!(" pvisor checkpoint {}", record.stage_dir().display()); - println!(" pvisor apply {}", record.stage_dir().display()); - println!(" pvisor drop {}", record.stage_dir().display()); - } - if args.diff { - print_diffs( - &record, - &bundle, - args.max_diff_bytes, - args.max_diff_file_bytes, - )?; - } - Ok(()) -} - -fn print_diffs( - record: &RunRecord, - bundle: &RunBundle, - max_total_bytes: usize, - max_file_bytes: u64, -) -> anyhow::Result<()> { - let Some(filesystem) = &bundle.filesystem else { - return Ok(()); - }; - let overlay = record - .overlay - .as_ref() - .context("Run Bundle has a changeset but Run overlay metadata is missing")?; - let lowers = if record.overlay_lowers.is_empty() { - vec![overlay.target.clone()] - } else { - record.overlay_lowers.clone() - }; - let mut remaining = max_total_bytes; - println!("\nDiff"); - for change in &filesystem.changes { - if remaining == 0 { - println!(" … diff output truncated at {max_total_bytes} bytes"); - break; - } - let relative = safe_change_path(&change.path)?; - let old = lowers - .iter() - .map(|lower| lower.join(&relative)) - .find(|path| fs::symlink_metadata(path).is_ok()); - let new = overlay.upper.path().join(&relative); - if change.kind == ChangeKind::Opaque { - println!("opaque directory: {}", change.path); - continue; - } - if change.old_type == Some(ChangeEntryType::Symlink) - || change.new_type == Some(ChangeEntryType::Symlink) - { - println!( - "symlink {}: {} -> {}", - change.path, - old.as_deref() - .and_then(read_link_label) - .unwrap_or_else(|| "-".into()), - read_link_label(&new).unwrap_or_else(|| "-".into()) - ); - continue; - } - let old_file = old.as_deref().filter(|path| path.is_file()); - let new_file = new.is_file().then_some(new.as_path()); - if old_file.is_none() && new_file.is_none() { - continue; - } - if [old_file, new_file] - .into_iter() - .flatten() - .any(|path| fs::metadata(path).is_ok_and(|metadata| metadata.len() > max_file_bytes)) - { - println!("binary/large {} (content diff skipped)", change.path); - continue; - } - if [old_file, new_file] - .into_iter() - .flatten() - .any(is_binary_file) - { - println!("binary {} (content diff skipped)", change.path); - continue; - } - let old_arg = old_file.unwrap_or_else(|| Path::new("/dev/null")); - let new_arg = new_file.unwrap_or_else(|| Path::new("/dev/null")); - let output = Command::new("diff") - .args(["-u", "--label"]) - .arg(format!("a/{}", change.path)) - .arg("--label") - .arg(format!("b/{}", change.path)) - .arg("--") - .arg(old_arg) - .arg(new_arg) - .output() - .with_context(|| format!("render diff for {}", change.path))?; - anyhow::ensure!( - matches!(output.status.code(), Some(0 | 1)), - "diff failed for {}: {}", - change.path, - String::from_utf8_lossy(&output.stderr).trim() - ); - let keep = remaining.min(output.stdout.len()); - print!("{}", String::from_utf8_lossy(&output.stdout[..keep])); - remaining -= keep; - } - Ok(()) -} - -fn safe_change_path(path: &str) -> anyhow::Result { - use std::path::Component; - let path = Path::new(path); - anyhow::ensure!( - path.components() - .all(|component| matches!(component, Component::Normal(_) | Component::CurDir)), - "unsafe change path in Run Bundle: {}", - path.display() - ); - Ok(path.to_path_buf()) -} - -fn read_link_label(path: &Path) -> Option { - fs::read_link(path) - .ok() - .map(|target| target.display().to_string()) -} - -fn is_binary_file(path: &Path) -> bool { - use std::io::Read; - let Ok(mut file) = fs::File::open(path) else { - return true; - }; - let mut prefix = [0_u8; 8192]; - let read = file.read(&mut prefix).unwrap_or(0); - prefix[..read].contains(&0) -} - -pub fn checkpoint(args: CheckpointArgs) -> anyhow::Result<()> { - let record = selected(args.selector.as_deref(), &args.output_dir)?; - let checkpoint = create_logical_checkpoint(&record, args.name.as_deref())?; - if args.json { - println!("{}", serde_json::to_string_pretty(&checkpoint)?); - } else { - println!( - "checkpointed {} @ {} ({:?})", - checkpoint.run_id, checkpoint.checkpoint_id, checkpoint.consistency - ); - println!("manifest: {}", checkpoint.manifest_path().display()); - println!( - "fork: pvisor fork {} --checkpoint {} -- ", - record.stage_dir().display(), - checkpoint.checkpoint_id - ); - } - Ok(()) -} - -fn selected(selector: Option<&Path>, output_dir: &Path) -> anyhow::Result { - let storage = output_dir - .canonicalize() - .unwrap_or_else(|_| output_dir.to_path_buf()); - resolve_run(selector, &storage) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn diff_paths_cannot_escape_the_overlay_roots() { - assert!(safe_change_path("src/lib.rs").is_ok()); - assert!(safe_change_path("../host-secret").is_err()); - assert!(safe_change_path("/etc/passwd").is_err()); - } - - #[test] - fn binary_probe_detects_nul_bytes() { - let temp = tempfile::NamedTempFile::new().unwrap(); - fs::write(temp.path(), b"text\0binary").unwrap(); - assert!(is_binary_file(temp.path())); - fs::write(temp.path(), b"plain text\n").unwrap(); - assert!(!is_binary_file(temp.path())); - } -} diff --git a/crates/persisting-pvisor/src/cli/replay.rs b/crates/persisting-pvisor/src/cli/replay.rs deleted file mode 100644 index d777eceaa..000000000 --- a/crates/persisting-pvisor/src/cli/replay.rs +++ /dev/null @@ -1,743 +0,0 @@ -use std::io::Write; -use std::path::PathBuf; -use std::process::Command; -use std::str::FromStr; - -use clap::Args; -use persisting_replay::{ - AgentKind, OverlayFsConfig as ReplayOverlayFsConfig, - OverlayNetConfig as ReplayOverlayNetConfig, PlaybackRequest, RESULT_SCHEMA_VERSION, - ReplayConfig, ReplayError, ReplayMode, ReplayToml, RunConfig as ReplayRunConfig, execute, - request_from_json, -}; -use serde_json::json; - -use crate::config::{ - OverlayFsBackend as PVisorOverlayFsBackend, OverlayFsCommit as PVisorOverlayFsCommit, - OverlayFsSettings, OverlayNetMode as PVisorOverlayNetMode, - OverlayNetPolicy as PVisorOverlayNetPolicy, RunConfig as PVisorRunConfig, RunExecutorKind, - RunPolicy, -}; - -#[derive(Debug, Clone, Args)] -pub struct ReplayArgs { - /// Complete replay TOML containing [replay] and optional runtime sections. - #[arg(long, value_name = "FILE", conflicts_with = "request")] - config: Option, - - /// Versioned sandbox-playback JSON request. - #[arg(long, value_name = "FILE", conflicts_with = "config")] - request: Option, - - /// Agent-native trajectory format. - #[arg(long)] - agent: Option, - - /// Native trajectory file to replay. - #[arg(long, value_name = "FILE")] - trajectory: Option, - - /// Complete tool batch ordinal after which live execution resumes. - #[arg(long)] - after_step: Option, - - /// Exact version-pinned Agent executable. - #[arg(long, value_name = "PATH")] - agent_entrypoint: Option, - - /// Versioned Agent runtime directory with sandbox-playback-agent.json. - #[arg(long, value_name = "DIR")] - agent_runtime: Option, - - /// Disable a Claude Code tool during the live continuation; repeat as needed. - #[arg(long = "agent-disallowed-tool", value_name = "TOOL")] - disallowed_tools: Vec, - - /// Read-only trajectory assets root, primarily for SWE-agent problem files. - #[arg(long, value_name = "DIR")] - trajectory_assets: Option, - - /// Existing fresh-sandbox workspace; defaults to the current directory. - #[arg(long, value_name = "DIR")] - workspace: Option, - - /// Temporary replay state root; defaults to /tmp/pvisor-sandbox-replay/state. - #[arg(long, value_name = "DIR")] - state_dir: Option, - - /// Temporary replay output root; defaults to /tmp/pvisor-sandbox-replay/output. - #[arg(long, value_name = "DIR")] - output_dir: Option, - - /// Model-router/run session key. Codex native continuation identity is - /// derived from the trajectory and is never taken from this field. - #[arg(long)] - session_id: Option, - - /// Total Agent action budget including the replayed prefix and any live continuation. - #[arg(long)] - max_steps: Option, - - /// Parse and construct the selected prefix without executing tools or starting an Agent. - #[arg(long, conflicts_with = "replay_only")] - prepare_only: bool, - - /// Execute the selected tool prefix and stop before the next model request. - #[arg(long, conflicts_with = "prepare_only")] - replay_only: bool, - - /// Permit replay to reuse source observations that cannot be freshly reproduced. - #[arg(long)] - allow_stale_observations: bool, - - /// Force live continuation model requests to disable thinking. - #[arg(long)] - disable_thinking: bool, - - /// Append one user message after the replayed boundary observation for the first live model request. - #[arg(long, value_name = "TEXT")] - boundary_user_prompt: Option, - - #[arg(long)] - run_id: Option, - - /// Use an outer pVisor Run with the platform safe profile. - #[arg(long)] - safe: bool, - - /// Optional outer execution provider: host, container, or vm. - #[arg(long, value_name = "KIND")] - executor: Option, - - /// Timeout for the outer managed pVisor Run. - #[arg(long, value_name = "MILLISECONDS")] - timeout_ms: Option, - - /// Outer pVisor capability policy: observe or enforce. - #[arg(long, value_name = "MODE")] - policy: Option, - - /// Let the outer managed Run inherit the complete host environment. - #[arg(long)] - inherit_env: bool, - - /// Project one host environment variable into the outer Run; repeat as needed. - #[arg(long, value_name = "NAME")] - pass_env: Vec, - - /// Absolute path visible to the replay Agent after the overlay is mounted. - #[arg(long = "overlayfs-path", value_name = "PATH")] - overlayfs_path: Option, - - /// Host directory layered into the replay view; repeat in order. - #[arg(long, value_name = "DIR")] - overlayfs_compose: Vec, - - /// Outer OverlayFS backend: directory or jujutsu. - #[arg(long, value_name = "BACKEND")] - overlayfs_backend: Option, - - /// Outer OverlayFS commit behavior: manual, apply, or drop. - #[arg(long, value_name = "MODE")] - overlayfs_commit: Option, - - /// Outer OverlayNet mode: auto, off, or proxy. - /// Outer OverlayNet driver: off, auto, or proxy. - #[arg( - long, - value_name = "MODE", - num_args = 0..=1, - default_missing_value = "proxy" - )] - overlaynet: Option, - - /// Outer OverlayNet policy: public, deny, or allowlist. - #[arg(long, value_name = "POLICY")] - overlaynet_policy: Option, -} - -pub fn run(args: ReplayArgs) -> i32 { - if let Some(config_path) = args.config.as_ref() { - match managed_if_requested(&args, config_path) { - Ok(Some(code)) => return code, - Ok(None) => {} - Err(error) => { - print_error(&error); - return error.exit_code(); - } - } - } - if args.request.is_none() && direct_managed_requested(&args) { - match direct_managed_config(&args).and_then(|config| run_managed(&config)) { - Ok(code) => return code, - Err(error) => { - print_error(&error); - return error.exit_code(); - } - } - } - match normalize(args).and_then(execute) { - Ok(report) => { - println!( - "{}", - serde_json::to_string(&report.result).expect("ReplayResult is serializable") - ); - report.exit_code - } - Err(error) => { - print_error(&error); - error.exit_code() - } - } -} - -fn managed_if_requested( - args: &ReplayArgs, - config_path: &std::path::Path, -) -> Result, ReplayError> { - reject_direct(args)?; - let config = ReplayToml::from_file(config_path)?; - if !needs_managed_run(&config) { - return Ok(None); - } - let cwd = std::env::current_dir().map_err(|error| { - ReplayError::configuration(format!("cannot read current directory: {error}")) - })?; - // Validate the inner replay contract before creating an outer Run. - let _ = config.clone().into_request(&cwd)?; - run_managed(&config).map(Some) -} - -fn needs_managed_run(config: &ReplayToml) -> bool { - config.run.safe - || config.run.executor.is_some() - || config.run.timeout_ms.is_some() - || config.run.policy.is_some() - || config.run.inherit_env - || !config.run.pass_env.is_empty() - || config.overlayfs.path.is_some() - || !config.overlayfs.compose.is_empty() - || config.overlayfs.backend.is_some() - || config.overlayfs.commit.is_some() - || config.overlaynet.mode.is_some() - || config.overlaynet.policy.is_some() -} - -fn direct_managed_requested(args: &ReplayArgs) -> bool { - args.safe - || args.executor.is_some() - || args.timeout_ms.is_some() - || args.policy.is_some() - || args.inherit_env - || !args.pass_env.is_empty() - || args.overlayfs_path.is_some() - || !args.overlayfs_compose.is_empty() - || args.overlayfs_backend.is_some() - || args.overlayfs_commit.is_some() - || args.overlaynet.is_some() - || args.overlaynet_policy.is_some() -} - -fn direct_managed_config(args: &ReplayArgs) -> Result { - let agent = args - .agent - .clone() - .ok_or_else(|| ReplayError::configuration("direct CLI mode requires --agent"))?; - let trajectory = args - .trajectory - .clone() - .ok_or_else(|| ReplayError::configuration("direct CLI mode requires --trajectory"))?; - let after_step = args - .after_step - .ok_or_else(|| ReplayError::configuration("direct CLI mode requires --after-step"))?; - Ok(ReplayToml { - replay: ReplayConfig { - agent, - trajectory, - after_step, - agent_entrypoint: args.agent_entrypoint.clone(), - agent_runtime: args.agent_runtime.clone(), - disallowed_tools: args.disallowed_tools.clone(), - trajectory_assets: args.trajectory_assets.clone(), - max_steps: args.max_steps, - session_id: args.session_id.clone(), - replay_only: args.replay_only, - prepare_only: args.prepare_only, - allow_stale_observations: args.allow_stale_observations, - disable_thinking: args.disable_thinking, - boundary_user_prompt: args.boundary_user_prompt.clone(), - run_id: args.run_id.clone(), - workspace: args.workspace.clone(), - state_dir: args.state_dir.clone(), - output_dir: args.output_dir.clone(), - }, - run: ReplayRunConfig { - safe: args.safe, - executor: args.executor.clone(), - timeout_ms: args.timeout_ms, - policy: args.policy.clone(), - inherit_env: args.inherit_env, - pass_env: args.pass_env.clone(), - }, - overlayfs: ReplayOverlayFsConfig { - path: args.overlayfs_path.clone(), - compose: args.overlayfs_compose.clone(), - backend: args.overlayfs_backend.clone(), - commit: args.overlayfs_commit.clone(), - }, - overlaynet: ReplayOverlayNetConfig { - mode: args.overlaynet.clone(), - policy: args.overlaynet_policy.clone(), - }, - }) -} - -fn run_managed(config: &ReplayToml) -> Result { - let executable = std::env::current_exe().map_err(|error| { - ReplayError::configuration(format!("cannot resolve the pVisor executable: {error}")) - })?; - let mut outer = PVisorRunConfig::default(); - outer.run.agent = format!("replay-{}", config.replay.agent); - outer.run.executor = match config.run.executor.as_deref().unwrap_or("host") { - "host" => RunExecutorKind::Host, - "container" => RunExecutorKind::Container, - "vm" | "kvm" => RunExecutorKind::Vm, - other => { - return Err(ReplayError::configuration(format!( - "unsupported run.executor {other:?}" - ))); - } - }; - outer.run.timeout_ms = config.run.timeout_ms; - outer.run.policy = match config.run.policy.as_deref().unwrap_or("observe") { - "observe" => RunPolicy::Observe, - "enforce" => RunPolicy::Enforce, - other => { - return Err(ReplayError::configuration(format!( - "unsupported run.policy {other:?}" - ))); - } - }; - outer.run.inherit_env = config.run.inherit_env; - outer.run.pass_env = config.run.pass_env.clone(); - outer.run.command = inner_replay_command(config, &executable)?; - - if config.overlayfs.path.is_some() - || !config.overlayfs.compose.is_empty() - || config.overlayfs.backend.is_some() - || config.overlayfs.commit.is_some() - { - let mut overlay = OverlayFsSettings { - target: config.overlayfs.path.clone(), - compose: config.overlayfs.compose.clone(), - ..OverlayFsSettings::default() - }; - overlay.backend = match config.overlayfs.backend.as_deref().unwrap_or("directory") { - "directory" => PVisorOverlayFsBackend::Directory, - "jujutsu" => PVisorOverlayFsBackend::Jujutsu, - other => { - return Err(ReplayError::configuration(format!( - "unsupported overlayfs.backend {other:?}" - ))); - } - }; - overlay.commit = match config.overlayfs.commit.as_deref().unwrap_or("manual") { - "manual" => PVisorOverlayFsCommit::Manual, - "apply" => PVisorOverlayFsCommit::Apply, - "drop" => PVisorOverlayFsCommit::Drop, - other => { - return Err(ReplayError::configuration(format!( - "unsupported overlayfs.commit {other:?}" - ))); - } - }; - outer.overlayfs = Some(overlay); - } - if let Some(mode) = config.overlaynet.mode.as_deref() { - outer.overlaynet.mode = match mode { - "auto" => PVisorOverlayNetMode::Auto, - "off" => PVisorOverlayNetMode::Off, - "proxy" => PVisorOverlayNetMode::Proxy, - other => { - return Err(ReplayError::configuration(format!( - "unsupported overlaynet.mode {other:?}" - ))); - } - }; - } - if let Some(policy) = config.overlaynet.policy.as_deref() { - outer.overlaynet.policy = match policy { - "public" => PVisorOverlayNetPolicy::Public, - "deny" => PVisorOverlayNetPolicy::Deny, - "allowlist" => PVisorOverlayNetPolicy::Allowlist, - other => { - return Err(ReplayError::configuration(format!( - "unsupported overlaynet.policy {other:?}" - ))); - } - }; - } - let rendered = toml::to_string_pretty(&outer).map_err(|error| { - ReplayError::configuration(format!("serialize managed pVisor run config: {error}")) - })?; - let mut file = tempfile::Builder::new() - .prefix("pvisor-replay-managed-") - .suffix(".toml") - .tempfile() - .map_err(|error| { - ReplayError::configuration(format!("create managed run config: {error}")) - })?; - file.write_all(rendered.as_bytes()).map_err(|error| { - ReplayError::configuration(format!("write managed run config: {error}")) - })?; - file.flush().map_err(|error| { - ReplayError::configuration(format!("flush managed run config: {error}")) - })?; - - let working_dir = config - .replay - .workspace - .clone() - .map(Ok) - .unwrap_or_else(std::env::current_dir) - .map_err(|error| { - ReplayError::configuration(format!("resolve managed replay workspace: {error}")) - })?; - let mut command = Command::new(&executable); - command.args(["run", "--spec"]).arg(file.path()); - command.current_dir(&working_dir); - let status = command.status().map_err(|error| { - ReplayError::configuration(format!("start managed pVisor replay: {error}")) - })?; - Ok(status.code().unwrap_or(50)) -} - -fn inner_replay_command( - config: &ReplayToml, - executable: &std::path::Path, -) -> Result, ReplayError> { - let replay = &config.replay; - let mut command = vec![ - path_string(executable)?, - "replay".into(), - "--agent".into(), - replay.agent.clone(), - "--trajectory".into(), - path_string(&replay.trajectory)?, - "--after-step".into(), - replay.after_step.to_string(), - "--workspace".into(), - ".".into(), - "--state-dir".into(), - path_string( - replay - .state_dir - .as_deref() - .unwrap_or_else(|| std::path::Path::new("/tmp/pvisor-sandbox-replay/state")), - )?, - "--output-dir".into(), - path_string( - replay - .output_dir - .as_deref() - .unwrap_or_else(|| std::path::Path::new("/tmp/pvisor-sandbox-replay/output")), - )?, - ]; - if let Some(entrypoint) = &replay.agent_entrypoint { - command.extend(["--agent-entrypoint".into(), path_string(entrypoint)?]); - } - if let Some(runtime) = &replay.agent_runtime { - command.extend(["--agent-runtime".into(), path_string(runtime)?]); - } - for tool in &replay.disallowed_tools { - command.extend(["--agent-disallowed-tool".into(), tool.clone()]); - } - if let Some(assets) = &replay.trajectory_assets { - command.extend(["--trajectory-assets".into(), path_string(assets)?]); - } - if let Some(session_id) = &replay.session_id { - command.extend(["--session-id".into(), session_id.clone()]); - } - if let Some(max_steps) = replay.max_steps { - command.extend(["--max-steps".into(), max_steps.to_string()]); - } - if replay.replay_only { - command.push("--replay-only".into()); - } - if replay.prepare_only { - command.push("--prepare-only".into()); - } - if replay.allow_stale_observations { - command.push("--allow-stale-observations".into()); - } - if replay.disable_thinking { - command.push("--disable-thinking".into()); - } - if let Some(prompt) = &replay.boundary_user_prompt { - command.extend(["--boundary-user-prompt".into(), prompt.clone()]); - } - if let Some(run_id) = &replay.run_id { - command.extend(["--run-id".into(), run_id.clone()]); - } - Ok(command) -} - -fn path_string(path: &std::path::Path) -> Result { - path.to_str() - .map(str::to_owned) - .ok_or_else(|| ReplayError::configuration(format!("path is not UTF-8: {}", path.display()))) -} - -fn normalize(args: ReplayArgs) -> Result { - let cwd = std::env::current_dir().map_err(|error| { - ReplayError::configuration(format!("cannot read current directory: {error}")) - })?; - if let Some(config) = &args.config { - reject_direct(&args)?; - return ReplayToml::from_file(config)?.into_request(&cwd); - } - if let Some(request) = &args.request { - reject_direct(&args)?; - return request_from_json(request); - } - let agent = args - .agent - .as_deref() - .ok_or_else(|| ReplayError::configuration("direct CLI mode requires --agent"))?; - let trajectory = args - .trajectory - .ok_or_else(|| ReplayError::configuration("direct CLI mode requires --trajectory"))?; - let after_step = args - .after_step - .ok_or_else(|| ReplayError::configuration("direct CLI mode requires --after-step"))?; - Ok(PlaybackRequest { - agent: AgentKind::from_str(agent).map_err(ReplayError::configuration)?, - trajectory, - after_step, - workspace: args.workspace.unwrap_or(cwd), - state_dir: args - .state_dir - .unwrap_or_else(|| PathBuf::from("/tmp/pvisor-sandbox-replay/state")), - output_dir: args - .output_dir - .unwrap_or_else(|| PathBuf::from("/tmp/pvisor-sandbox-replay/output")), - agent_entrypoint: args.agent_entrypoint, - agent_runtime: args.agent_runtime, - disallowed_tools: args.disallowed_tools, - trajectory_assets: args.trajectory_assets, - session_id: args.session_id, - max_steps: args.max_steps, - mode: if args.prepare_only { - ReplayMode::PrepareOnly - } else if args.replay_only { - ReplayMode::ReplayOnly - } else { - ReplayMode::ReplayAndContinue - }, - allow_stale_observations: args.allow_stale_observations, - run_id: args.run_id, - disable_thinking: args.disable_thinking, - boundary_user_prompt: args.boundary_user_prompt, - }) -} - -fn reject_direct(args: &ReplayArgs) -> Result<(), ReplayError> { - let direct = args.agent.is_some() - || args.trajectory.is_some() - || args.after_step.is_some() - || args.agent_entrypoint.is_some() - || args.agent_runtime.is_some() - || !args.disallowed_tools.is_empty() - || args.trajectory_assets.is_some() - || args.workspace.is_some() - || args.state_dir.is_some() - || args.output_dir.is_some() - || args.session_id.is_some() - || args.max_steps.is_some() - || args.prepare_only - || args.replay_only - || args.allow_stale_observations - || args.disable_thinking - || args.boundary_user_prompt.is_some() - || args.run_id.is_some() - || args.safe - || args.executor.is_some() - || args.timeout_ms.is_some() - || args.policy.is_some() - || args.inherit_env - || !args.pass_env.is_empty() - || args.overlayfs_path.is_some() - || !args.overlayfs_compose.is_empty() - || args.overlayfs_backend.is_some() - || args.overlayfs_commit.is_some() - || args.overlaynet.is_some() - || args.overlaynet_policy.is_some(); - if direct { - return Err(ReplayError::configuration( - "--config/--request cannot be combined with direct replay options", - )); - } - Ok(()) -} - -fn print_error(error: &ReplayError) { - println!("{}", failure_json(error)); -} - -fn failure_json(error: &ReplayError) -> serde_json::Value { - let (run_id, state_dir, output_dir) = error - .locations() - .map(|(run_id, state_dir, output_dir)| (json!(run_id), json!(state_dir), json!(output_dir))) - .unwrap_or(( - serde_json::Value::Null, - serde_json::Value::Null, - serde_json::Value::Null, - )); - json!({ - "schema_version": RESULT_SCHEMA_VERSION, - "phase": null, - "quality": null, - "agent_status": "not_started", - "run_id": run_id, - "state_dir": state_dir, - "output_dir": output_dir, - "artifacts": [], - "failure": { - "category": error.kind.category(), - "message": error.to_string(), - }, - "retryable": error.kind.retryable(), - }) -} -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn full_runtime_toml_selects_managed_outer_run() { - let config: ReplayToml = toml::from_str( - r#" -[replay] -agent = "claude-code" -trajectory = "/input/session.jsonl" -after_step = 30 -agent_entrypoint = "/usr/bin/claude" -max_steps = 200 -session_id = "task-291-attempt-1" -disallowed_tools = [] -disable_thinking = true -boundary_user_prompt = "Review the fresh boundary observation." - -[run] -safe = true -executor = "host" -timeout_ms = 3600000 -policy = "enforce" -inherit_env = false -pass_env = ["OPENAI_BASE_URL", "OPENAI_API_KEY", "MODEL_NAME"] - -[overlayfs] -path = "/workspace" -compose = ["/workspace"] -backend = "directory" -commit = "manual" - -[overlaynet] -mode = "proxy" -policy = "allowlist" -"#, - ) - .unwrap(); - assert!(needs_managed_run(&config)); - let command = - inner_replay_command(&config, std::path::Path::new("/usr/bin/pvisor")).unwrap(); - assert_eq!(command[0], "/usr/bin/pvisor"); - assert!(command.windows(2).any(|pair| pair == ["--workspace", "."])); - assert!( - command - .windows(2) - .any(|pair| { pair == ["--state-dir", "/tmp/pvisor-sandbox-replay/state"] }) - ); - assert!( - command - .windows(2) - .any(|pair| { pair == ["--output-dir", "/tmp/pvisor-sandbox-replay/output"] }) - ); - assert!( - command - .windows(2) - .any(|pair| pair == ["--agent-entrypoint", "/usr/bin/claude"]) - ); - assert!( - command - .iter() - .any(|argument| argument == "--disable-thinking") - ); - assert!(command.windows(2).any(|pair| { - pair == [ - "--boundary-user-prompt", - "Review the fresh boundary observation.", - ] - })); - } - - #[test] - fn minimal_toml_stays_in_current_fresh_sandbox() { - let mut config: ReplayToml = toml::from_str( - r#" -[replay] -agent = "claude-code" -trajectory = "/input/session.jsonl" -after_step = 30 -agent_entrypoint = "/usr/bin/claude" -"#, - ) - .unwrap(); - assert!(!needs_managed_run(&config)); - config.run.inherit_env = true; - assert!(needs_managed_run(&config)); - } - - #[test] - fn managed_command_propagates_prepare_and_stale_observation_flags() { - let config: ReplayToml = toml::from_str( - r#" -[replay] -agent = "claude-code" -trajectory = "/input/session.jsonl" -after_step = 1 -prepare_only = true -allow_stale_observations = true -"#, - ) - .unwrap(); - - let command = - inner_replay_command(&config, std::path::Path::new("/usr/bin/pvisor")).unwrap(); - - assert!(command.iter().any(|argument| argument == "--prepare-only")); - assert!( - command - .iter() - .any(|argument| argument == "--allow-stale-observations") - ); - assert!(!command.iter().any(|argument| argument == "--replay-only")); - } - - #[test] - fn failure_json_keeps_run_locations() { - let error = ReplayError::configuration("invalid request").with_locations( - "replay-1", - PathBuf::from("/state/replay-1"), - PathBuf::from("/output/replay-1"), - ); - - let value = failure_json(&error); - - assert_eq!(value["schema_version"], "sandbox-playback.result/v3"); - assert_eq!(value["run_id"], "replay-1"); - assert_eq!(value["state_dir"], "/state/replay-1"); - assert_eq!(value["output_dir"], "/output/replay-1"); - assert_eq!(value["failure"]["category"], "configuration_error"); - } -} diff --git a/crates/persisting-pvisor/src/cli/run.rs b/crates/persisting-pvisor/src/cli/run.rs deleted file mode 100644 index 30e86b50a..000000000 --- a/crates/persisting-pvisor/src/cli/run.rs +++ /dev/null @@ -1,3187 +0,0 @@ -use std::path::{Path, PathBuf}; - -#[derive(Debug, Clone)] -enum StageSpec { - Persistent(PathBuf), - Temporary, - TemporaryAt(PathBuf), -} - -const STAGE_OWNER_FILE: &str = ".pvisor-stage-owner"; - -fn parse_stage(value: &str) -> anyhow::Result { - if value == "drop" { - return Ok(StageSpec::Temporary); - } - if let Some(path) = value.strip_prefix("drop:") { - anyhow::ensure!(!path.is_empty(), "--stage drop: requires a path"); - return Ok(StageSpec::TemporaryAt(PathBuf::from(path))); - } - anyhow::ensure!(!value.is_empty(), "--stage path must not be empty"); - Ok(StageSpec::Persistent(PathBuf::from(value))) -} - -fn spec_is_json(path: &Path) -> anyhow::Result { - let bytes = - std::fs::read(path).with_context(|| format!("read spec file {}", path.display()))?; - Ok(bytes.iter().find(|byte| !byte.is_ascii_whitespace()) == Some(&b'{')) -} - -#[derive(Debug, Clone, Copy)] -struct ByteSize(u64); - -impl FromStr for ByteSize { - type Err = String; - fn from_str(value: &str) -> Result { - parse_scaled( - value, - &[ - ("kib", 1 << 10), - ("kb", 1_000), - ("mib", 1 << 20), - ("mb", 1_000_000), - ("gib", 1 << 30), - ("gb", 1_000_000_000), - ("b", 1), - ], - ) - .map(ByteSize) - } -} - -fn parse_scaled(value: &str, units: &[(&str, u64)]) -> Result { - let normalized = value.trim().to_ascii_lowercase(); - let (number, multiplier) = units - .iter() - .find_map(|(suffix, multiplier)| { - normalized - .strip_suffix(suffix) - .map(|number| (number, *multiplier)) - }) - .unwrap_or((normalized.as_str(), 1)); - let number = number - .trim() - .parse::() - .map_err(|_| format!("invalid size/duration: {value:?}"))?; - number - .checked_mul(multiplier) - .ok_or_else(|| format!("size/duration overflows u64: {value:?}")) -} - -#[derive(Debug, Clone, Copy)] -struct DurationMs(u64); - -impl FromStr for DurationMs { - type Err = String; - fn from_str(value: &str) -> Result { - parse_scaled( - value, - &[("ms", 1), ("s", 1_000), ("m", 60_000), ("h", 3_600_000)], - ) - .map(DurationMs) - } -} -use std::str::FromStr; -use std::sync::Arc; - -use anyhow::{Context, bail}; -use clap::{Args, ValueEnum}; -use persisting_agentctl::{PolicyMode, RunInvocation, RunSpec, RunState, StdioMode}; -use persisting_events::TrajectoryFormat; -use persisting_gateway::config::{ - CaptureLevel, ModelRoute, NetworkConfig, NetworkMode, OverlayBackend, OverlayConfig, - ProxyConfig, -}; -use persisting_overlaynet::{NetworkAccessRule, NetworkBandwidthLimit}; -use serde::Deserialize; - -use crate::config::{ - ContainerMount, ContainerNetwork, ContainerPlatform, GatewayMode, OverlayFsBackend, - OverlayFsCommit, OverlayFsSettings, OverlayNetMode, OverlayNetPolicy, OverlayNetSettings, - RecordFormat, RunConfig, RunExecutorKind, RunPolicy, RunStdio, -}; -use crate::runtime::{RunLineage, default_run_home, resolve_run}; -use crate::{ - ContainerExecutor, GatewayDriverConfig, LogicalCheckpoint, NetworkDriverConfig, OverlayHint, - PVisor, ProcessExecutor, RunBundle, RunExecutor, TrajectoryEventSink, VmExecutor, - latest_logical_checkpoint, restore_logical_checkpoint, -}; - -use super::trajectory::{ - ChronicleWriter, JsonlEventSink, JsonlWriter, chronicle_sink, jsonl_capture_sink, -}; - -type ChronicleSinks = ( - Arc, - Arc, - Option, - Option>, -); - -#[cfg(target_os = "linux")] -pub(super) const RUN_COMMAND_ABOUT: &str = - "Execute one Agent Run with safe-best-effort host isolation by default"; -#[cfg(target_os = "linux")] -pub(super) const RUN_COMMAND_LONG_ABOUT: &str = "Execute one Agent Run under pVisor management. Host execution uses safe-best-effort isolation when supported by the system."; - -#[cfg(target_os = "macos")] -pub(super) const RUN_COMMAND_ABOUT: &str = - "Execute one Agent Run with safe-best-effort host isolation by default"; -#[cfg(target_os = "macos")] -pub(super) const RUN_COMMAND_LONG_ABOUT: &str = MACOS_RUN_COMMAND_LONG_ABOUT; - -// Compile the macOS description in tests on every platform so Linux CI also -// checks its safety disclosures instead of leaving them to the macOS shard. -#[cfg(any(target_os = "macos", test))] -const MACOS_RUN_COMMAND_LONG_ABOUT: &str = "Execute one Agent Run under pVisor management. Host execution uses safe-best-effort isolation when supported by the system.\n\nOn macOS, staged workspace views use macFUSE and Seatbelt confines writes when available. Full-disk reads remain ambient; selective network policies remain cooperative. With --overlaynet-deny-all, Seatbelt blocks non-loopback IP traffic and ambient host Unix sockets while permitting loopback proxy access and Run-scoped Unix IPC.\n\nUnavailable isolation capabilities are reported as warnings in best-effort mode. With --strict, insufficient isolation guarantees cause the Run to fail before Agent execution."; - -#[cfg(not(any(target_os = "linux", target_os = "macos")))] -pub(super) const RUN_COMMAND_ABOUT: &str = "Execute one Agent Run under pVisor management"; -#[cfg(not(any(target_os = "linux", target_os = "macos")))] -pub(super) const RUN_COMMAND_LONG_ABOUT: &str = RUN_COMMAND_ABOUT; - -#[cfg(target_os = "linux")] -const EXECUTOR_HELP: &str = "Execution provider: host, container, or vm. `vm` uses the statically linked libkrun backend; host uses safe-best-effort isolation"; -#[cfg(target_os = "macos")] -const EXECUTOR_HELP: &str = "Execution provider: host, container, or vm. `vm` uses the statically linked libkrun backend; host uses safe-best-effort isolation"; -#[cfg(not(any(target_os = "linux", target_os = "macos")))] -const EXECUTOR_HELP: &str = "Execution provider for the Agent command"; - -#[cfg(target_os = "linux")] -const DENY_ALL_HELP: &str = "Deny all OverlayNet egress. VM `auto` enforces this on guest TCP; host execution uses a private network namespace when supported"; -#[cfg(target_os = "macos")] -const DENY_ALL_HELP: &str = "Deny all OverlayNet egress. VM `auto` enforces this on guest TCP; host execution uses Seatbelt when supported"; -#[cfg(not(any(target_os = "linux", target_os = "macos")))] -const DENY_ALL_HELP: &str = "Deny all OverlayNet egress; direct sockets remain outside the cooperative host/container proxy rule"; - -#[derive(Debug, Clone, Args)] -pub struct RunArgs { - /// TOML RunConfig or prepared JSON RunSpec; explicit CLI values replace matching fields. - #[arg(long, value_name = "FILE")] - spec: Option, - - /// Atomically write the delegated RunResult as JSON. - #[arg(long, value_name = "FILE")] - result_file: Option, - - /// OverlayFS stage directory; `drop` uses an automatic temporary directory and `drop:` uses a temporary directory at that path. - #[arg(long, value_name = "PATH|drop[:PATH]")] - stage: Option, - - #[command(flatten, next_help_heading = "Run options")] - run: RunOverrides, - #[command(flatten, next_help_heading = "Container executor options")] - container: ContainerOverrides, - #[command(flatten, next_help_heading = "VM executor options")] - vm: VmOverrides, - #[command(flatten, next_help_heading = "OverlayFS options")] - overlayfs: OverlayFsOverrides, - #[command(flatten, next_help_heading = "OverlayNet options")] - overlaynet: OverlayNetOverrides, - #[command(flatten, next_help_heading = "Gateway options")] - gateway: GatewayOverrides, - #[command(flatten, next_help_heading = "Recording options")] - record: RecordOverrides, - - /// Agent command; replaces `run.command` from the TOML spec. - #[arg(trailing_var_arg = true, allow_hyphen_values = true)] - command: Vec, -} - -#[derive(Debug, Clone, Args)] -pub struct ForkArgs { - /// Source Run id, workspace, run.json, or path inside the source Run. - source: PathBuf, - /// Logical checkpoint id; the latest checkpoint is used when omitted. - #[arg(long, value_name = "ID")] - checkpoint: Option, - #[arg(long, short = 'o', default_value = ".persisting/capture")] - output_dir: PathBuf, - /// Agent command; defaults to the source Run command. - #[arg(last = true, allow_hyphen_values = true)] - command: Vec, -} - -#[derive(Debug, Clone, Default, Args)] -struct RunOverrides { - /// Human-readable Run/Agent name. - #[arg(long)] - name: Option, - #[arg(long, value_enum, help = EXECUTOR_HELP)] - executor: Option, - #[arg(long, value_name = "DURATION")] - timeout: Option, - #[arg(long, value_enum)] - stdio: Option, - /// Fail before execution unless every requested capability has a non-bypassable boundary. - #[arg(long)] - strict: bool, - /// Maximum memory for the Agent execution (for example `256MiB` or `4GB`). - #[arg(long, visible_alias = "mem", value_name = "SIZE")] - memory: Option, - /// CPU count allocated to the executor. - #[arg(long, value_name = "COUNT")] - cpu: Option, - /// Project one host environment variable by name; repeat as needed. - #[arg(long, value_name = "NAME")] - pass_env: Vec, - /// Maximum processes/threads admitted for the Run. - #[arg(long, value_name = "COUNT")] - max_processes: Option, - /// CPU-time budget (for example `500ms`, `5s`, or `1m`). - #[arg(long, value_name = "DURATION")] - max_cpu_time: Option, - /// Maximum open file descriptors. - #[arg(long, value_name = "COUNT")] - max_open_files: Option, - /// Maximum size of a file created by the Agent (for example `8MiB`). - #[arg(long = "max-file-size", value_name = "SIZE")] - max_file_size: Option, - /// Maximum total size of the staged filesystem. - #[arg(long, value_name = "SIZE")] - max_stage_size: Option, -} - -#[derive(Debug, Clone, Default, Args)] -struct ContainerOverrides { - /// Native OCI runtime executable (`runc` or `crun`). - #[arg(long, value_name = "PATH")] - container_runtime: Option, - /// OCI image containing the Agent command. Supplying this selects the container executor. - #[arg(long, value_name = "IMAGE")] - container_image: Option, - /// Existing OCI rootfs directory (otherwise container image is prepared). - #[arg(long, value_name = "PATH")] - container_rootfs: Option, - /// pVisor injected into the container; defaults to the running executable. - /// Set it to a statically linked build when the guest ABI differs. - #[arg(long, value_name = "PATH")] - container_pvisor_binary: Option, - /// OCI target platform (`linux/amd64` or `linux/arm64`). - #[arg(long, value_name = "PLATFORM")] - container_platform: Option, - /// Container network mode; host keeps the in-process Gateway reachable. - #[arg(long, value_enum)] - container_network: Option, - /// Container-native workdir used when pVisor does not inject an OverlayFS cwd. - #[arg(long, value_name = "PATH")] - container_workdir: Option, - /// Container user (`uid`, `uid:gid`, or name). - #[arg(long, value_name = "USER")] - container_user: Option, - /// Mount the image root filesystem read-only. - #[arg(long, value_name = "BOOL", num_args = 0..=1, default_missing_value = "true")] - container_read_only_rootfs: Option, - /// TOML inline-table bind mount; repeat to replace configured mounts. - #[arg(long, value_name = "MOUNT")] - container_mount: Vec, -} - -#[derive(Debug, Clone, Default, Args)] -struct VmOverrides { - /// Shorthand for `--executor vm`. - #[arg(long)] - vm: bool, - /// VM rootfs source: host, , or image=. - #[arg(long)] - rootfs: Option, - /// Content-addressed OCI image cache directory. - #[arg(long = "image-store", value_name = "DIR")] - vm_image_store: Option, - /// Directory containing libkrunfw; packaged builds discover it automatically. - #[arg(long = "vm-library-dir", value_name = "PATH")] - vm_library_dir: Option, -} - -#[derive(Debug, Clone)] -struct ContainerMountArg(ContainerMount); - -impl FromStr for ContainerMountArg { - type Err = String; - - fn from_str(value: &str) -> Result { - #[derive(Deserialize)] - struct Wrapper { - mount: ContainerMount, - } - let source = format!("mount = {{ {value} }}"); - toml::from_str::(&source) - .map(|wrapper| Self(wrapper.mount)) - .map_err(|error| format!("invalid container mount: {error}")) - } -} - -#[derive(Debug, Clone, Default, Args)] -struct OverlayFsOverrides { - /// Absolute path visible to the Agent after the overlay view is mounted. - #[arg(long = "overlayfs-path", value_name = "PATH")] - overlayfs_path: Option, - /// Host directory layered into the view; repeat in bottom-to-top order. - /// The current workspace is always added as the implicit bottom layer. - #[arg(long, value_name = "DIR")] - overlayfs_compose: Vec, - #[arg(long, value_enum)] - overlayfs_backend: Option, - #[arg(long, value_enum)] - overlayfs_commit: Option, -} - -#[derive(Debug, Clone, Default, Args)] -struct OverlayNetOverrides { - /// Network driver: auto selects VM smoltcp, proxy is host/container only, off disables it. - #[arg( - long, - value_enum, - value_name = "MODE", - num_args = 0..=1, - default_missing_value = "proxy" - )] - overlaynet: Option, - /// Explicit proxy listen address; supplying it enables OverlayNet. - #[arg(long, value_name = "ADDR")] - overlaynet_listen: Option, - #[arg(long, value_enum, hide = true)] - overlaynet_policy: Option, - /// Allowed HOST[:PORT] or CIDR[:PORT]; enables the executor's OverlayNet driver. - #[arg(long, value_name = "TARGET")] - overlaynet_allow: Vec, - /// Denied HOST[:PORT] or CIDR[:PORT]; enables the executor's OverlayNet driver. - #[arg(long, value_name = "TARGET")] - overlaynet_deny: Vec, - /// Aggregate bandwidth limit; enables the executor's OverlayNet driver. - #[arg(long, value_name = "[TARGET=]RATE")] - overlaynet_limit: Vec, - #[arg( - long, - help = DENY_ALL_HELP, - conflicts_with_all = [ - "overlaynet_allow", - "overlaynet_deny", - "overlaynet_limit", - "overlaynet_rule", - "overlaynet_policy" - ] - )] - overlaynet_deny_all: bool, - /// TOML inline-table fields for one structured rule; repeat to replace configured rules. - #[arg(long, value_name = "RULE", hide = true)] - overlaynet_rule: Vec, -} - -#[derive(Debug, Clone)] -struct OverlayNetTargetArg(NetworkAccessRule); - -impl FromStr for OverlayNetTargetArg { - type Err = String; - - fn from_str(value: &str) -> Result { - parse_overlaynet_target(value).map(Self) - } -} - -#[derive(Debug, Clone)] -struct OverlayNetLimitArg(NetworkBandwidthLimit); - -impl FromStr for OverlayNetLimitArg { - type Err = String; - - fn from_str(value: &str) -> Result { - let (target, rate) = value - .rsplit_once('=') - .map_or((None, value), |(target, rate)| (Some(target), rate)); - let bytes_per_second = parse_bandwidth(rate)?; - let target = target.map(parse_overlaynet_target).transpose()?; - Ok(Self(NetworkBandwidthLimit { - host: target.as_ref().map(|target| target.host.clone()), - port: target.and_then(|target| target.ports.first().copied()), - bytes_per_second, - })) - } -} - -fn parse_overlaynet_target(value: &str) -> Result { - let value = value.trim(); - if value.is_empty() { - return Err("OverlayNet target cannot be empty".into()); - } - let (host, port) = if let Some(rest) = value.strip_prefix('[') { - let end = rest - .find(']') - .ok_or_else(|| format!("invalid bracketed OverlayNet target `{value}`"))?; - let host = &rest[..end]; - let suffix = &rest[end + 1..]; - let port = if suffix.is_empty() { - None - } else { - Some( - suffix - .strip_prefix(':') - .ok_or_else(|| format!("invalid OverlayNet target `{value}`"))? - .parse::() - .map_err(|_| format!("invalid port in OverlayNet target `{value}`"))?, - ) - }; - (host, port) - } else if value.matches(':').count() <= 1 { - match value.rsplit_once(':') { - Some((host, port)) - if !host.is_empty() && port.bytes().all(|byte| byte.is_ascii_digit()) => - { - ( - host, - Some( - port.parse::() - .map_err(|_| format!("invalid port in OverlayNet target `{value}`"))?, - ), - ) - } - _ => (value, None), - } - } else { - (value, None) - }; - if port == Some(0) { - return Err("OverlayNet target port must not be zero".into()); - } - persisting_agentctl::parse_network_rule(host).map_err(|error| error.to_string())?; - Ok(NetworkAccessRule { - host: host.to_string(), - ports: port.into_iter().collect(), - transports: Vec::new(), - allow_private_ips: false, - }) -} - -fn parse_bandwidth(value: &str) -> Result { - let normalized = value.trim().to_ascii_lowercase(); - let units = [ - ("gbps", 1_000_000_000_u64, true), - ("mbps", 1_000_000, true), - ("kbps", 1_000, true), - ("bps", 1, true), - ("gb/s", 1_000_000_000, false), - ("mb/s", 1_000_000, false), - ("kb/s", 1_000, false), - ("b/s", 1, false), - ]; - for (suffix, multiplier, bits) in units { - if let Some(amount) = normalized.strip_suffix(suffix) { - let amount = amount - .trim() - .parse::() - .map_err(|_| format!("invalid OverlayNet bandwidth `{value}`"))?; - let scaled = amount - .checked_mul(multiplier) - .ok_or_else(|| format!("OverlayNet bandwidth `{value}` is too large"))?; - let bytes = if bits { scaled.div_ceil(8) } else { scaled }; - return (bytes > 0) - .then_some(bytes) - .ok_or_else(|| "OverlayNet bandwidth must be greater than zero".into()); - } - } - Err(format!( - "invalid OverlayNet bandwidth `{value}`; use e.g. `10mbps` or `2mb/s`" - )) -} - -#[derive(Debug, Clone)] -struct OverlayNetRuleArg(NetworkAccessRule); - -impl FromStr for OverlayNetRuleArg { - type Err = String; - - fn from_str(value: &str) -> Result { - #[derive(Deserialize)] - struct Wrapper { - rule: NetworkAccessRule, - } - let source = format!("rule = {{ {value} }}"); - toml::from_str::(&source) - .map(|wrapper| Self(wrapper.rule)) - .map_err(|error| format!("invalid OverlayNet rule: {error}")) - } -} - -#[derive(Debug, Clone, Default, Args)] -struct GatewayOverrides { - #[arg(long, value_enum)] - gateway_mode: Option, - #[arg(long, value_name = "ADDR")] - gateway_admin_listen: Option, - #[arg(long, value_enum)] - gateway_level: Option, - #[arg(long, value_name = "HEADER")] - gateway_session_header: Option, - /// Enable or disable Gateway diagnostics. - #[arg(long, value_name = "BOOL", num_args = 0..=1, default_missing_value = "true")] - gateway_debug: Option, - /// Enable or disable the live Markdown projection. - #[arg(long, value_name = "BOOL", num_args = 0..=1, default_missing_value = "true")] - gateway_stream_markdown: Option, - /// TOML inline-table fields for one model route; repeat to replace configured routes. - #[arg(long, value_name = "ROUTE")] - gateway_route: Vec, -} - -#[derive(Debug, Clone, Default, Args)] -struct RecordOverrides { - /// Durable event format. `json` is the lightweight local JSONL path; - /// `lance` starts the full pChronicle warehouse path. - #[arg(long, value_enum, value_name = "FORMAT")] - record_format: Option, - /// Directory or file for JSONL, or warehouse URI/directory for Lance. - #[arg(long, value_name = "PATH|URI")] - record_destination: Option, -} - -#[derive(Debug, Clone, Copy, ValueEnum)] -enum GatewayLevel { - Summary, - Dialogue, - Full, -} - -impl From for CaptureLevel { - fn from(level: GatewayLevel) -> Self { - match level { - GatewayLevel::Summary => Self::Summary, - GatewayLevel::Dialogue => Self::Dialogue, - GatewayLevel::Full => Self::Full, - } - } -} - -#[derive(Debug, Clone)] -struct GatewayRouteArg(ModelRoute); - -impl FromStr for GatewayRouteArg { - type Err = String; - - fn from_str(value: &str) -> Result { - #[derive(Deserialize)] - struct Wrapper { - route: ModelRoute, - } - let source = format!("route = {{ {value} }}"); - toml::from_str::(&source) - .map(|wrapper| Self(wrapper.route)) - .map_err(|error| format!("invalid Gateway route: {error}")) - } -} - -pub async fn run(args: RunArgs) -> anyhow::Result { - if let Some(path) = args.spec.as_deref() - && spec_is_json(path)? - { - return run_prepared_spec(args).await; - } - // Host runs use the safe-best-effort profile by default. - let safe = true; - let run_id = format!("run-{}", uuid::Uuid::new_v4()); - let mut config = args - .spec - .as_deref() - .map(RunConfig::from_file) - .transpose() - .context("load pVisor Run config")? - .unwrap_or_default(); - apply_cli(&mut config, args.clone())?; - let stage_limit = config - .overlayfs - .as_ref() - .and_then(|overlay| overlay.stage_size_bytes); - let mut cleanup_stage = None; - if let Some(stage) = args.stage.as_deref().map(parse_stage).transpose()? { - let (path, cleanup) = match stage { - StageSpec::Persistent(path) => (path, false), - StageSpec::TemporaryAt(path) => (path, true), - StageSpec::Temporary => (tempfile::tempdir()?.keep(), true), - }; - if cleanup { - if path.exists() { - let nonempty = std::fs::read_dir(&path)?.next().is_some(); - anyhow::ensure!( - !nonempty, - "temporary stage must be empty before use: {}", - path.display() - ); - } else { - std::fs::create_dir_all(&path)?; - } - std::fs::write(path.join(STAGE_OWNER_FILE), run_id.as_bytes())?; - cleanup_stage = Some(path.clone()); - } - config - .overlayfs - .get_or_insert_with(OverlayFsSettings::default) - .stage = Some(path); - } - let effective_stage = config - .overlayfs - .as_ref() - .and_then(|overlay| overlay.stage.clone()); - apply_safe_defaults(&mut config)?; - let mut result = execute_config(config, run_id.clone(), safe, None).await; - if result.is_ok() - && let Some(limit) = stage_limit - && let Some(path) = effective_stage - && path.exists() - { - match directory_size_bytes(&path) { - Ok(actual) if actual > limit => { - result = Err(anyhow::anyhow!( - "stage size limit exceeded: {} uses {} bytes (limit {})", - path.display(), - actual, - limit - )); - } - Err(error) => { - result = Err(error).context("measure OverlayFS stage size"); - } - _ => {} - } - } - if let Some(path) = cleanup_stage { - let owner = std::fs::read(path.join(STAGE_OWNER_FILE)).ok(); - let owned = owner.as_deref() == Some(run_id.as_bytes()); - if !owned { - let error = - anyhow::anyhow!("temporary stage ownership marker is missing or does not match"); - if result.is_ok() { - return Err(error) - .with_context(|| format!("validate temporary stage {}", path.display())); - } - eprintln!( - "pVisor warning: refusing to remove unowned temporary stage {}", - path.display() - ); - } else if let Err(error) = std::fs::remove_dir_all(&path) { - if result.is_ok() { - return Err(error) - .with_context(|| format!("remove temporary stage {}", path.display())); - } - eprintln!( - "pVisor warning: failed to remove temporary stage {}: {error}", - path.display() - ); - } - } - result -} - -fn directory_size_bytes(root: &Path) -> anyhow::Result { - fn visit(path: &Path) -> anyhow::Result { - let metadata = std::fs::symlink_metadata(path) - .with_context(|| format!("stat stage entry {}", path.display()))?; - if metadata.file_type().is_symlink() { - return Ok(0); - } - if metadata.is_file() { - return Ok(metadata.len()); - } - if !metadata.is_dir() { - return Ok(0); - } - let mut total = 0u64; - for entry in std::fs::read_dir(path) - .with_context(|| format!("read stage directory {}", path.display()))? - { - total = total - .checked_add(visit(&entry?.path())?) - .ok_or_else(|| anyhow::anyhow!("stage size overflows u64"))?; - } - Ok(total) - } - visit(root) -} - -async fn run_prepared_spec(args: RunArgs) -> anyhow::Result { - anyhow::ensure!( - args.command.is_empty(), - "a command cannot be combined with a JSON --spec" - ); - anyhow::ensure!( - args.run - .executor - .is_none_or(|executor| executor == RunExecutorKind::Host), - "JSON --spec must execute with --executor host" - ); - let spec_path = args.spec.clone().context("missing --spec JSON file")?; - let result_path = args - .result_file - .clone() - .context("JSON --spec requires --result-file")?; - let stage_spec = args.stage.as_deref().map(parse_stage).transpose()?; - let spec: RunSpec = serde_json::from_slice( - &std::fs::read(&spec_path) - .with_context(|| format!("read delegated RunSpec from {}", spec_path.display()))?, - ) - .context("decode delegated RunSpec")?; - let mut stage_guard = None; - let pvisor = if let Some(stage_spec) = stage_spec { - let (stage_path, cleanup) = match stage_spec { - StageSpec::Persistent(path) => (path, false), - StageSpec::TemporaryAt(path) => (path, true), - StageSpec::Temporary => (tempfile::tempdir()?.keep(), true), - }; - if cleanup { - if stage_path.exists() { - let nonempty = std::fs::read_dir(&stage_path)?.next().is_some(); - anyhow::ensure!( - !nonempty, - "temporary stage must be empty before use: {}", - stage_path.display() - ); - } else { - std::fs::create_dir_all(&stage_path)?; - } - std::fs::write( - stage_path.join(STAGE_OWNER_FILE), - spec.run_id.as_str().as_bytes(), - )?; - stage_guard = Some(TemporaryStageGuard::new( - stage_path.clone(), - spec.run_id.to_string(), - )); - } - let mut config = RunConfig::default(); - config.run.agent = spec.agent.name.clone(); - let RunInvocation::Process(process) = &spec.invocation; - config.run.command = std::iter::once(process.program.clone()) - .chain(process.args.iter().cloned()) - .collect(); - config.run.workspace = process.cwd.as_deref().map(PathBuf::from); - apply_cli(&mut config, args)?; - anyhow::ensure!( - config.run.executor == RunExecutorKind::Host, - "JSON --spec currently supports only the host executor" - ); - anyhow::ensure!( - config.overlayfs.as_ref().is_none_or(|overlay| { - overlay.base.is_none() - && overlay.target.is_none() - && overlay.merged_dir.is_none() - && overlay.compose.is_empty() - && overlay.backend == OverlayFsBackend::Directory - && overlay.commit == OverlayFsCommit::Manual - && overlay.stage.as_deref() == Some(stage_path.as_path()) - }), - "JSON --spec accepts only --stage as an OverlayFS override" - ); - anyhow::ensure!( - config.record.destination.is_none() && config.record.format == RecordFormat::Json, - "JSON --spec does not accept recording overrides" - ); - let storage = resolve_run_storage(&stage_path)?; - let proxy = resolve_proxy(&config)?; - let mut builder = PVisor::builder() - .storage(&storage) - .executors(vec![Arc::new(ProcessExecutor::default())]) - .network(NetworkDriverConfig::new( - config.overlaynet.mode, - NetworkConfig { - mode: match config.overlaynet.policy { - OverlayNetPolicy::Public => NetworkMode::Public, - OverlayNetPolicy::Deny => NetworkMode::NoNetwork, - OverlayNetPolicy::Allowlist => NetworkMode::Allowlist, - }, - allowed_hosts: config.overlaynet.allow.clone(), - rules: config.overlaynet.rules.clone(), - deny_rules: config.overlaynet.deny.clone(), - limits: config.overlaynet.limits.clone(), - }, - )); - if let Some(proxy) = proxy { - builder = builder.gateway( - GatewayDriverConfig::new(proxy) - .output_dir(&storage) - .stream_markdown(config.gateway.stream_markdown) - .gateway_enabled(config.gateway.mode == GatewayMode::Capture), - ); - } - builder.build() - } else { - PVisor::builder() - .executors(vec![Arc::new(ProcessExecutor::default())]) - .build() - }; - let handle = match pvisor.run(spec).await { - Ok(handle) => handle, - Err(error) => return Err(error.into()), - }; - let agentctl = handle.agentctl(); - let cancellation = handle.cancellation(); - let wait = handle.wait(); - tokio::pin!(wait); - let result = tokio::select! { - result = &mut wait => result?, - _ = delegated_shutdown_signal() => { - cancellation.cancel(); - wait.await? - } - }; - let output = crate::delegated::DelegatedRunOutput { - agentctl: agentctl.snapshot(), - result, - }; - let write_result = crate::delegated::write_result(&result_path, &output) - .with_context(|| format!("write delegated RunResult to {}", result_path.display())); - let cleanup_result = stage_guard - .as_mut() - .map(|guard| cleanup_temporary_stage(&guard.path, output.result.run_id.as_str(), true)) - .transpose()?; - if let Some(guard) = stage_guard.as_mut() { - guard.disarm(); - } - write_result?; - let _ = cleanup_result; - Ok(match output.result.state { - RunState::Completed => output.result.exit_code.unwrap_or(0), - RunState::Cancelled => 130, - _ => output.result.exit_code.unwrap_or(1), - }) -} - -fn cleanup_temporary_stage(path: &Path, run_id: &str, successful: bool) -> anyhow::Result<()> { - let owner = std::fs::read(path.join(STAGE_OWNER_FILE)).ok(); - let owned = owner.as_deref() == Some(run_id.as_bytes()); - if !owned { - let error = anyhow::anyhow!( - "temporary stage ownership marker is missing or does not match: {}", - path.display() - ); - if successful { - return Err(error); - } - eprintln!( - "pVisor warning: refusing to remove unowned temporary stage {}", - path.display() - ); - return Ok(()); - } - if let Err(error) = std::fs::remove_dir_all(path) { - if successful { - return Err(error) - .with_context(|| format!("remove temporary stage {}", path.display())); - } - eprintln!( - "pVisor warning: failed to remove temporary stage {}: {error}", - path.display() - ); - } - Ok(()) -} - -struct TemporaryStageGuard { - path: PathBuf, - run_id: String, - armed: bool, -} - -impl TemporaryStageGuard { - fn new(path: PathBuf, run_id: String) -> Self { - Self { - path, - run_id, - armed: true, - } - } - - fn disarm(&mut self) { - self.armed = false; - } -} - -impl Drop for TemporaryStageGuard { - fn drop(&mut self) { - if self.armed { - let _ = cleanup_temporary_stage(&self.path, &self.run_id, false); - } - } -} - -async fn delegated_shutdown_signal() { - #[cfg(unix)] - { - use tokio::signal::unix::{SignalKind, signal}; - let mut terminate = signal(SignalKind::terminate()).expect("install SIGTERM handler"); - tokio::select! { - _ = tokio::signal::ctrl_c() => {} - _ = terminate.recv() => {} - } - } - #[cfg(not(unix))] - { - let _ = tokio::signal::ctrl_c().await; - } -} - -pub async fn fork(args: ForkArgs) -> anyhow::Result { - let storage = args - .output_dir - .canonicalize() - .unwrap_or(args.output_dir.clone()); - let source = resolve_run(Some(&args.source), &storage)?; - let checkpoint = match args.checkpoint.as_deref() { - Some(id) => { - anyhow::ensure!( - !id.trim().is_empty() - && id != "." - && id != ".." - && !id.contains('/') - && !id.contains('\\'), - "checkpoint id must be one non-empty path-safe segment" - ); - LogicalCheckpoint::read(&source.stage_dir().join(crate::CHECKPOINTS_DIR).join(id))? - } - None => latest_logical_checkpoint(&source)?, - }; - anyhow::ensure!( - checkpoint.run_id == source.run_id, - "checkpoint {} belongs to Run {}, not {}", - checkpoint.checkpoint_id, - checkpoint.run_id, - source.run_id - ); - let fork_workspace = source - .workspace - .clone() - .unwrap_or_else(|| checkpoint.target.clone()); - let fork_workspace = resolve_workspace(&fork_workspace)?; - let run_id = format!("run-{}", uuid::Uuid::new_v4()); - let mut config = RunConfig::default(); - if source.executor.as_ref().is_some_and(|executor| { - executor.isolation == persisting_agentctl::IsolationKind::VirtualMachine - }) { - config.run.executor = RunExecutorKind::Vm; - config.vm.rootfs = Some(checkpoint.target.clone()); - config.vm.rootfs_immutable = checkpoint.protect_target; - } - config.run.workspace = Some(fork_workspace.clone()); - let (agent, command) = fork_command(&source.agent, &source.command, args.command); - config.run.agent = agent; - config.run.command = command; - config.overlayfs = Some(OverlayFsSettings { - base: Some(checkpoint.target.clone()), - target: None, - merged_dir: None, - compose: checkpoint - .lower_dirs - .iter() - .filter(|lower| *lower != &checkpoint.target) - .cloned() - .collect(), - stage: None, - stage_size_bytes: None, - backend: OverlayFsBackend::Directory, - commit: OverlayFsCommit::Manual, - }); - let stage = select_run_storage(&config, &fork_workspace, &run_id)?; - std::fs::create_dir_all(&stage)?; - let upper = stage.join("upper"); - if let Err(error) = restore_logical_checkpoint(&checkpoint, &upper) { - let _ = std::fs::remove_dir_all(&stage); - return Err(error); - } - config.overlayfs.as_mut().expect("configured above").stage = Some(stage); - apply_safe_defaults(&mut config)?; - execute_config( - config, - run_id, - true, - Some(RunLineage { - parent_run_id: source.run_id, - checkpoint_id: checkpoint.checkpoint_id, - }), - ) - .await -} - -fn fork_command( - source_agent: &str, - source_command: &[String], - command: Vec, -) -> (String, Vec) { - if command.is_empty() { - return (source_agent.to_owned(), source_command.to_vec()); - } - let agent = Path::new(&command[0]) - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or(source_agent) - .to_owned(); - (agent, command) -} - -async fn execute_config( - mut config: RunConfig, - run_id: String, - safe_profile_requested: bool, - lineage: Option, -) -> anyhow::Result { - validate_vm_rootfs_platform(&config)?; - let prepared_image = if config.run.executor == RunExecutorKind::Vm && config.vm.rootfs.is_none() - { - let image = config - .vm - .image - .clone() - .unwrap_or_else(|| crate::oci::DEFAULT_IMAGE.into()); - let store = config.vm.image_store.clone(); - eprintln!("pVisor image: resolving {image}"); - let prepared = tokio::task::spawn_blocking(move || { - crate::oci::ImageStore::new(store)?.prepare(&image) - }) - .await - .context("OCI image preparation task failed")??; - eprintln!( - "pVisor image: {} ({})", - prepared.digest, - prepared.rootfs.display() - ); - config.vm.rootfs = Some(prepared.rootfs.clone()); - config.vm.rootfs_immutable = true; - if config.run.command.is_empty() { - config.run.command = prepared.entrypoint.clone(); - config.run.command.extend(prepared.cmd.clone()); - } - Some(prepared) - } else { - None - }; - if config.run.executor == RunExecutorKind::Vm { - let (rootfs, workspace) = resolve_vm_layout(&config)?; - config.vm.rootfs = Some(rootfs.clone()); - config.run.workspace = Some(workspace.clone()); - let has_guest_overlay = config - .overlayfs - .as_ref() - .and_then(|overlay| overlay.target.as_ref()) - .is_some(); - if !has_guest_overlay { - let overlay = config - .overlayfs - .get_or_insert_with(OverlayFsSettings::default); - // Keep the host workspace path stable inside the guest. The - // workspace is a separate virtio-fs mount; using the rootfs as its - // base would make `cwd` point at a path that does not exist in an - // image guest and would bypass workspace staging. - overlay.base = Some(workspace.clone()); - overlay.target = Some(workspace.clone()); - overlay.commit = OverlayFsCommit::Manual; - } - if config.vm.library_dir.is_none() && crate::vm::bundled_firmware_dir().is_none() { - eprintln!( - "pVisor firmware: resolving libkrunfw {}", - crate::firmware::VERSION - ); - let directory = - tokio::task::spawn_blocking(|| crate::firmware::FirmwareStore::new()?.prepare()) - .await - .context("libkrunfw preparation task failed")??; - eprintln!("pVisor firmware: {}", directory.display()); - config.vm.library_dir = Some(directory); - } - } else { - if let Some(base) = config - .overlayfs - .as_ref() - .and_then(|overlay| overlay.base.as_ref()) - { - // The OverlayFS base is the project association for host and - // container runs when supplied by a config file. - config.run.workspace = Some(base.clone()); - } - // On host/container runs the path is a real host mount point visible - // to the Agent. VM runs keep `target` as the guest-visible path. - if let Some(overlay) = &mut config.overlayfs { - // The target is translated to a host merged mount below, after - // the workspace has been canonicalized. A missing path means the - // current workspace itself, preserving transparent cwd semantics. - if overlay.merged_dir.is_none() { - overlay.merged_dir = overlay.target.take(); - } - } - } - validate(&config)?; - - if config.overlaynet.mode == OverlayNetMode::Proxy { - eprintln!( - "pVisor OverlayNet boundary: explicit cooperative proxy; direct sockets remain ambient" - ); - } else if config.run.executor == RunExecutorKind::Vm - && config.overlaynet.mode == OverlayNetMode::Auto - { - eprintln!( - "pVisor OverlayNet boundary: non-bypassable libkrun virtio-net → smoltcp IPv4 TCP/DNS" - ); - } - - let workspace = config - .run - .workspace - .as_deref() - .map(Path::to_path_buf) - .unwrap_or(std::env::current_dir()?); - let workspace = resolve_workspace(&workspace)?; - let storage = resolve_run_storage(&select_run_storage(&config, &workspace, &run_id)?)?; - let mut overlay = resolve_overlay(&config, &workspace, &storage, &run_id)?; - if config.run.executor == RunExecutorKind::Vm - && config.vm.rootfs_immutable - && config - .overlayfs - .as_ref() - .and_then(|overlay| overlay.target.as_ref()) - .is_none() - && let Some(overlay) = &mut overlay - { - overlay.protect_target = true; - } - let overlay_enabled = overlay.is_some(); - let resolved_stage_for_limit = overlay.as_ref().and_then(|hint| hint.stage_dir.clone()); - let proxy = resolve_proxy(&config)?; - - if config.gateway.debug { - persisting_gateway::runtime::debug::enable_debug(&storage)?; - } - - let remote_json = config.record.format == RecordFormat::Json - && config - .record - .destination - .as_ref() - .is_some_and(|path| path.to_string_lossy().contains("://")); - let use_chronicle = config.record.format == RecordFormat::Lance || remote_json; - let mut json_writer = None; - let (sink, event_sink, writer, chronicle_control): ChronicleSinks = if use_chronicle { - let dir = config - .record - .destination - .clone() - .unwrap_or_else(|| storage.join("warehouse")); - let chronicle_format = if config.record.format == RecordFormat::Json { - TrajectoryFormat::Json - } else { - TrajectoryFormat::Lance - }; - let (sink, event_sink, writer, control) = chronicle_sink( - &dir, - &config.run.agent, - &run_id, - &config.chronicle.binary, - chronicle_format, - ) - .await?; - (sink, event_sink, Some(writer), Some(control)) - } else if config.gateway.mode == GatewayMode::Capture || config.record.destination.is_some() { - let destination = config - .record - .destination - .clone() - .unwrap_or_else(|| storage.join(".capture")); - let writer = JsonlWriter::open(&destination).with_context(|| { - format!("open JSONL recording destination {}", destination.display()) - })?; - let sink = jsonl_capture_sink(&writer, &config.run.agent); - let event_sink = Arc::new(JsonlEventSink::new(writer.clone())) as Arc; - json_writer = Some(writer); - (sink, event_sink, None, None) - } else { - ( - Arc::new(persisting_gateway::sink::SeqOnlySink::new()), - Arc::new(crate::NoopEventSink), - None, - None, - ) - }; - - let executor: Arc = match config.run.executor { - #[cfg(target_os = "linux")] - RunExecutorKind::Host if safe_profile_requested => { - if crate::process::rootless_runtime_available() { - match ProcessExecutor::rootless_with_launcher(std::env::current_exe()?) { - Ok(executor) => Arc::new(executor), - Err(error) => { - eprintln!( - "pVisor safe-best-effort: rootless launcher unavailable ({error}); falling back to host process" - ); - Arc::new(ProcessExecutor::default()) - } - } - } else { - eprintln!( - "pVisor safe-best-effort: user/mount/PID namespaces unavailable; falling back to host process" - ); - Arc::new(ProcessExecutor::default()) - } - } - #[cfg(target_os = "macos")] - RunExecutorKind::Host if safe_profile_requested => { - match ProcessExecutor::seatbelt_with_launcher(std::env::current_exe()?) { - Ok(executor) => Arc::new(executor), - Err(error) => { - eprintln!( - "pVisor safe-best-effort: Seatbelt unavailable ({error}); falling back to host process" - ); - Arc::new(ProcessExecutor::default()) - } - } - } - #[cfg(not(any(target_os = "linux", target_os = "macos")))] - RunExecutorKind::Host if safe_profile_requested => Arc::new(ProcessExecutor::default()), - RunExecutorKind::Host => Arc::new(ProcessExecutor::default()), - RunExecutorKind::Container => Arc::new(ContainerExecutor::new(config.container.clone())?), - RunExecutorKind::Vm => Arc::new(VmExecutor::new(config.vm.clone())?), - }; - let mut builder = PVisor::builder() - .storage(&storage) - .trajectory_sink(sink) - .event_sink(event_sink) - .pchronicle_binary(config.chronicle.binary.clone()) - .executors(vec![executor]) - .network(NetworkDriverConfig::new( - config.overlaynet.mode, - NetworkConfig { - mode: match config.overlaynet.policy { - OverlayNetPolicy::Public => NetworkMode::Public, - OverlayNetPolicy::Deny => NetworkMode::NoNetwork, - OverlayNetPolicy::Allowlist => NetworkMode::Allowlist, - }, - allowed_hosts: config.overlaynet.allow.clone(), - rules: config.overlaynet.rules.clone(), - deny_rules: config.overlaynet.deny.clone(), - limits: config.overlaynet.limits.clone(), - }, - )); - if let Some(control) = chronicle_control { - builder = builder.chronicle_control(control); - } - if let Some(proxy) = proxy { - builder = builder.gateway( - GatewayDriverConfig::new(proxy) - .output_dir(&storage) - .stream_markdown(config.gateway.stream_markdown) - .gateway_enabled(config.gateway.mode == GatewayMode::Capture), - ); - } - if let Some(overlay) = overlay { - builder = builder.overlay(overlay); - } - let pvisor = builder.build(); - - let (program, program_args) = config - .run - .command - .split_first() - .context("missing Agent command; pass it after `--` or set run.command")?; - let mut spec = RunSpec::process(run_id.as_str(), &config.run.agent, program); - let RunInvocation::Process(process) = &mut spec.invocation; - process.args = program_args.to_vec(); - process.stdin = StdioMode::Inherit; - process.stdout = match config.run.stdio { - RunStdio::Inherit => StdioMode::Inherit, - RunStdio::Capture => StdioMode::Capture, - }; - process.stderr = process.stdout; - process.inherit_env = config.run.inherit_env; - if let Some(image) = &prepared_image { - process.inherit_env = false; - process.env.extend(image.env.clone()); - } - if !process.inherit_env { - project_safe_baseline_environment(&mut process.env); - } - for key in &config.run.pass_env { - anyhow::ensure!( - valid_environment_name(key), - "--pass-env requires a valid environment variable name, got {key:?}" - ); - if let Ok(value) = std::env::var(key) { - process.env.insert(key.clone(), value); - } - } - if !overlay_enabled { - process.cwd = Some(workspace.display().to_string()); - } - spec.runtime.timeout_ms = config.run.timeout_ms; - spec.runtime.resource_limits = config.run.resource_limits.clone(); - spec.metadata.insert( - "pvisor.environment".into(), - serde_json::json!({ - "inherits_host": process.inherit_env, - "projected_keys": process.env.keys().cloned().collect::>(), - }), - ); - spec.metadata.insert( - "pvisor.workspace".into(), - serde_json::Value::String(workspace.display().to_string()), - ); - spec.metadata.insert( - "pvisor.stage".into(), - serde_json::json!({ - "scope": "whole-rootfs", - "path": config.overlayfs.as_ref().and_then(|overlay| overlay.stage.as_ref()).map(|path| path.display().to_string()), - "size_limit_bytes": config.overlayfs.as_ref().and_then(|overlay| overlay.stage_size_bytes), - }), - ); - if config.run.executor == RunExecutorKind::Vm { - if let Some(target) = config - .overlayfs - .as_ref() - .and_then(|overlay| overlay.target.as_ref()) - { - spec.metadata.insert( - "pvisor.vm.overlay_target".into(), - serde_json::Value::String(target.display().to_string()), - ); - spec.metadata.insert( - "pvisor.vm.guest_cwd".into(), - serde_json::Value::String(target.display().to_string()), - ); - } else { - spec.metadata.insert( - "pvisor.vm.guest_cwd".into(), - serde_json::Value::String("/".into()), - ); - } - if let Some(image) = &prepared_image { - spec.metadata.insert( - "pvisor.vm.image_digest".into(), - serde_json::Value::String(image.digest.clone()), - ); - } - } - if config.run.policy == RunPolicy::Enforce { - spec.runtime.policy_mode = PolicyMode::Enforce; - } - if let Some(lineage) = &lineage { - spec.metadata - .insert("pvisor.lineage".into(), serde_json::to_value(lineage)?); - } - if safe_profile_requested { - spec.metadata - .insert("pvisor.safe".into(), serde_json::Value::Bool(true)); - } - - if safe_profile_requested { - let network_boundary = if config.run.executor == RunExecutorKind::Vm - && config.overlaynet.mode == OverlayNetMode::Auto - { - "non-bypassable smoltcp IPv4 TCP/DNS" - } else if cfg!(any(target_os = "linux", target_os = "macos")) - && config.run.executor == RunExecutorKind::Host - && config.overlaynet.policy == OverlayNetPolicy::Deny - { - if cfg!(target_os = "linux") { - "private deny-all network namespace" - } else { - "Seatbelt deny-all socket policy" - } - } else { - "cooperative network review" - }; - if overlay_enabled { - eprintln!("pVisor safe profile: staged workspace + {network_boundary}"); - } else { - eprintln!( - "pVisor safe profile: best-effort isolation + {network_boundary}; workspace writes are not staged (pass --stage for COW/review)" - ); - } - eprintln!("workspace: {}", workspace.display()); - eprintln!("Run storage: {}", storage.display()); - match config.run.executor { - RunExecutorKind::Host => { - #[cfg(target_os = "linux")] - eprintln!( - "boundary: rootless user/mount/PID namespaces + PID 1 reaper + synthetic root + Landlock filesystem; network remains cooperative unless explicitly denied" - ); - #[cfg(target_os = "macos")] - if overlay_enabled { - eprintln!( - "boundary: Seatbelt-enforced staged writes; reads and selective network policies remain ambient/cooperative" - ); - } else { - eprintln!( - "boundary: Seatbelt best-effort when available; workspace writes are not staged; reads and selective network policies remain ambient/cooperative" - ); - } - #[cfg(not(any(target_os = "linux", target_os = "macos")))] - eprintln!("boundary: review-only host process"); - } - RunExecutorKind::Container => eprintln!( - "boundary: OCI container process; direct sockets remain outside proxy enforcement" - ), - RunExecutorKind::Vm => { - eprintln!( - "boundary: libkrun Linux virtual machine (KVM/HVF); virtio-net is owned by pVisor smoltcp and Gateway capture uses a virtual guest route" - ) - } - } - } - let handle = pvisor.run(spec).await?; - let cancellation = handle.cancellation(); - let wait = handle.wait(); - tokio::pin!(wait); - let result = tokio::select! { - result = &mut wait => result?, - _ = delegated_shutdown_signal() => { - cancellation.cancel(); - wait.await? - } - }; - drop(pvisor); - if let Some(writer) = json_writer { - writer.finish()?; - } - if let Some(writer) = writer { - writer.finish()?; - } - if result.state == RunState::Completed - && let Some(limit) = config - .overlayfs - .as_ref() - .and_then(|overlay| overlay.stage_size_bytes) - && let Some(path) = resolved_stage_for_limit - && path.exists() - { - let actual = directory_size_bytes(&path).context("measure OverlayFS stage size")?; - if actual > limit { - bail!( - "stage size limit exceeded: {} uses {} bytes (limit {})", - path.display(), - actual, - limit - ); - } - } - let record = resolve_run(Some(Path::new(&run_id)), &storage) - .with_context(|| format!("load finalized Run record for {run_id}"))?; - let bundle = RunBundle::read(&record.stage_dir()).with_context(|| { - format!( - "load finalized Run Bundle from {}", - record.stage_dir().display() - ) - })?; - let bundle_path = RunBundle::path(&record.stage_dir()); - if safe_profile_requested { - eprintln!("Run Bundle: {}", bundle_path.display()); - eprintln!("Review: pvisor review {}", record.stage_dir().display()); - if bundle.filesystem.is_some() { - eprintln!( - "Decide: pvisor apply {} | pvisor drop {}", - record.stage_dir().display(), - record.stage_dir().display() - ); - } - } - if result.state != RunState::Completed { - if let Some(failure) = &result.failure { - eprintln!("pVisor Run failed: {:?}: {}", failure.kind, failure.message); - } - for warning in &result.warnings { - eprintln!("pVisor Run warning: {warning}"); - } - } - Ok(match result.state { - RunState::Completed => result.exit_code.unwrap_or(0), - RunState::Cancelled => 130, - _ => result.exit_code.unwrap_or(1), - }) -} - -fn project_safe_baseline_environment(env: &mut std::collections::BTreeMap) { - for key in [ - "PATH", - "HOME", - "USER", - "LOGNAME", - "SHELL", - "LANG", - "LC_ALL", - "LC_CTYPE", - "TERM", - "COLORTERM", - "TZ", - ] { - if let Ok(value) = std::env::var(key) { - env.entry(key.into()).or_insert(value); - } - } -} - -fn valid_environment_name(name: &str) -> bool { - !name.is_empty() - && name - .bytes() - .all(|byte| byte == b'_' || byte.is_ascii_alphanumeric()) - && !name.as_bytes()[0].is_ascii_digit() -} - -fn apply_safe_defaults(config: &mut RunConfig) -> anyhow::Result<()> { - config.run.inherit_env = false; - if let Some(overlayfs) = config.overlayfs.as_mut() { - overlayfs.commit = OverlayFsCommit::Manual; - } - if config.overlaynet.listen == OverlayNetSettings::default().listen { - config.overlaynet.listen = free_loopback_address()?; - } - if config.gateway.admin_listen == crate::GatewaySettings::default().admin_listen { - config.gateway.admin_listen = free_loopback_address()?; - } - if config.run.agent == "agent" - && let Some(program) = config.run.command.first() - { - config.run.agent = Path::new(program) - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or("agent") - .to_owned(); - } - Ok(()) -} - -fn free_loopback_address() -> anyhow::Result { - let listener = std::net::TcpListener::bind("127.0.0.1:0")?; - Ok(listener.local_addr()?.to_string()) -} - -fn apply_cli(config: &mut RunConfig, args: RunArgs) -> anyhow::Result<()> { - if let Some(stage) = args.stage.as_deref().map(parse_stage).transpose()? { - // Persistent paths can be applied while translating CLI overrides. - // `drop` is intentionally materialized by `run()` so ownership markers - // and cleanup are handled exactly once (and the temporary directory is - // not leaked by this pure configuration step). - let path = match stage { - StageSpec::Persistent(path) | StageSpec::TemporaryAt(path) => Some(path), - StageSpec::Temporary => None, - }; - if let Some(path) = path { - config - .overlayfs - .get_or_insert_with(OverlayFsSettings::default) - .stage = Some(path); - } - } - let explicit_executor = args.run.executor; - let explicit_overlaynet_mode = args.overlaynet.overlaynet; - let rootfs_source = args.vm.rootfs.clone(); - anyhow::ensure!( - !(args.vm.vm && explicit_executor.is_some_and(|executor| executor != RunExecutorKind::Vm)), - "--vm cannot be combined with a non-vm --executor" - ); - let host_rootfs = rootfs_source.as_deref() == Some("host"); - let container_rootfs = explicit_executor == Some(RunExecutorKind::Container) - || args.container.container_runtime.is_some() - || args.container.container_image.is_some() - || args.container.container_rootfs.is_some(); - if host_rootfs { - anyhow::ensure!( - cfg!(target_os = "linux"), - "--rootfs host is only supported on Linux" - ); - anyhow::ensure!( - explicit_executor - .is_none_or(|executor| container_rootfs || executor == RunExecutorKind::Vm), - "--rootfs requires --executor vm or container (or no explicit executor)" - ); - } - if let Some(ref source) = rootfs_source { - if let Some(path) = source.strip_prefix("image=") { - anyhow::ensure!(!path.is_empty(), "--rootfs image= requires a path"); - if container_rootfs { - config.container.image = path.to_string(); - config.container.rootfs = None; - } else { - config.vm.image = Some(path.to_string()); - config.vm.rootfs = None; - config.vm.rootfs_immutable = false; - } - } else if source == "host" && container_rootfs { - config.container.rootfs = Some(PathBuf::from("/")); - config.container.image.clear(); - } else if source != "host" { - if container_rootfs { - config.container.rootfs = Some(PathBuf::from(source)); - config.container.image.clear(); - } else { - config.vm.rootfs = Some(PathBuf::from(source)); - config.vm.image = None; - config.vm.rootfs_immutable = false; - } - } - } - if let Some(value) = args.run.name { - config.run.agent = value; - } - if let Some(value) = explicit_executor { - config.run.executor = value; - } - if args.vm.vm { - config.run.executor = RunExecutorKind::Vm; - } - if let Some(value) = args.run.timeout { - config.run.timeout_ms = Some(value.0); - } - if let Some(value) = args.run.stdio { - config.run.stdio = value; - } - if args.run.strict { - config.run.policy = RunPolicy::Enforce; - } - if !args.run.pass_env.is_empty() { - config.run.pass_env = args.run.pass_env; - } - if let Some(value) = args.run.memory { - config.run.resource_limits.memory_bytes = Some(value.0); - } - if let Some(value) = args.run.max_processes { - config.run.resource_limits.processes = Some(value); - } - if let Some(value) = args.run.max_cpu_time { - config.run.resource_limits.cpu_time_ms = Some(value.0); - } - if let Some(value) = args.run.max_open_files { - config.run.resource_limits.open_files = Some(value); - } - if let Some(value) = args.run.max_file_size { - config.run.resource_limits.file_size_bytes = Some(value.0); - } - if let Some(value) = args.run.max_stage_size { - config - .overlayfs - .get_or_insert_with(OverlayFsSettings::default) - .stage_size_bytes = Some(value.0); - } - if !args.command.is_empty() { - config.run.command = args.command; - } - - let enables_container = args.container.container_runtime.is_some() - || args.container.container_image.is_some() - || args.container.container_rootfs.is_some() - || args.container.container_pvisor_binary.is_some() - || args.container.container_platform.is_some() - || args.container.container_network.is_some() - || args.container.container_workdir.is_some() - || args.container.container_user.is_some() - || args.container.container_read_only_rootfs.is_some() - || !args.container.container_mount.is_empty(); - if let Some(value) = args.container.container_runtime { - config.container.runtime = value; - } - if let Some(value) = args.container.container_image { - config.container.image = value; - } - if let Some(value) = args.container.container_rootfs { - config.container.rootfs = Some(value); - } - if let Some(value) = args.container.container_pvisor_binary { - config.container.pvisor_binary = Some(value); - } - if let Some(value) = args.container.container_platform { - config.container.platform = Some(value); - } - if let Some(value) = args.container.container_network { - config.container.network = value; - } - if let Some(value) = args.container.container_workdir { - config.container.workdir = Some(value); - } - if let Some(value) = args.container.container_user { - config.container.user = Some(value); - } - if let Some(value) = args.container.container_read_only_rootfs { - config.container.read_only_rootfs = value; - } - if !args.container.container_mount.is_empty() { - config.container.mounts = args - .container - .container_mount - .into_iter() - .map(|mount| mount.0) - .collect(); - } - if enables_container && explicit_executor.is_none() { - config.run.executor = RunExecutorKind::Container; - } - - let enables_vm = rootfs_source.is_some() - || args.vm.vm_image_store.is_some() - || args.vm.vm_library_dir.is_some(); - if host_rootfs && !container_rootfs { - config.vm.rootfs = Some(PathBuf::from("/")); - config.vm.image = None; - config.vm.rootfs_immutable = false; - } - if let Some(value) = args.vm.vm_image_store { - config.vm.image_store = Some(value); - } - if let Some(value) = args.vm.vm_library_dir { - config.vm.library_dir = Some(value); - } - if let Some(value) = args.run.cpu { - config.vm.cpus = value; - } - if let Some(bytes) = args.run.memory { - let mib = bytes.0.div_ceil(1024 * 1024); - config.vm.memory_mib = u32::try_from(mib) - .map_err(|_| anyhow::anyhow!("--memory value is too large for VM memory"))?; - } - if enables_vm && explicit_executor.is_none() { - config.run.executor = RunExecutorKind::Vm; - } - - let enables_overlayfs = args.overlayfs.overlayfs_path.is_some() - || !args.overlayfs.overlayfs_compose.is_empty() - || args.overlayfs.overlayfs_backend.is_some() - || args.overlayfs.overlayfs_commit.is_some(); - if enables_overlayfs { - let overlayfs = config - .overlayfs - .get_or_insert_with(OverlayFsSettings::default); - if let Some(value) = args.overlayfs.overlayfs_path { - overlayfs.target = Some(value); - } - if !args.overlayfs.overlayfs_compose.is_empty() { - overlayfs.compose = args.overlayfs.overlayfs_compose; - } - if let Some(value) = args.overlayfs.overlayfs_backend { - overlayfs.backend = value; - } - if let Some(value) = args.overlayfs.overlayfs_commit { - overlayfs.commit = value; - } - } - - let enables_overlaynet = !args.overlaynet.overlaynet_allow.is_empty() - || !args.overlaynet.overlaynet_deny.is_empty() - || !args.overlaynet.overlaynet_limit.is_empty() - || !args.overlaynet.overlaynet_rule.is_empty() - || args.overlaynet.overlaynet_deny_all - || args.overlaynet.overlaynet_listen.is_some(); - if let Some(value) = explicit_overlaynet_mode { - config.overlaynet.mode = value; - } - if let Some(value) = args.overlaynet.overlaynet_listen { - config.overlaynet.listen = value; - } - if let Some(value) = args.overlaynet.overlaynet_policy { - config.overlaynet.policy = value; - } - if args.overlaynet.overlaynet_deny_all { - config.overlaynet.policy = OverlayNetPolicy::Deny; - config.overlaynet.allow.clear(); - config.overlaynet.rules.clear(); - config.overlaynet.deny.clear(); - config.overlaynet.limits.clear(); - } - if !args.overlaynet.overlaynet_allow.is_empty() { - config.overlaynet.policy = OverlayNetPolicy::Allowlist; - config.overlaynet.allow.clear(); - config.overlaynet.rules = args - .overlaynet - .overlaynet_allow - .into_iter() - .map(|target| target.0) - .collect(); - } - if !args.overlaynet.overlaynet_deny.is_empty() { - config.overlaynet.deny = args - .overlaynet - .overlaynet_deny - .into_iter() - .map(|target| target.0) - .collect(); - } - if !args.overlaynet.overlaynet_limit.is_empty() { - config.overlaynet.limits = args - .overlaynet - .overlaynet_limit - .into_iter() - .map(|limit| limit.0) - .collect(); - } - if !args.overlaynet.overlaynet_rule.is_empty() { - config.overlaynet.rules = args - .overlaynet - .overlaynet_rule - .into_iter() - .map(|rule| rule.0) - .collect(); - } - if enables_overlaynet && explicit_overlaynet_mode.is_none() { - config.overlaynet.mode = if config.run.executor == RunExecutorKind::Vm { - OverlayNetMode::Auto - } else { - OverlayNetMode::Proxy - }; - } - - if let Some(value) = args.gateway.gateway_mode { - config.gateway.mode = value; - if value == GatewayMode::Capture && explicit_overlaynet_mode.is_none() { - config.overlaynet.mode = if config.run.executor == RunExecutorKind::Vm { - OverlayNetMode::Auto - } else { - OverlayNetMode::Proxy - }; - } - } - if let Some(value) = args.gateway.gateway_admin_listen { - config.gateway.admin_listen = value; - } - if let Some(value) = args.gateway.gateway_level { - config.gateway.level = value.into(); - } - if let Some(value) = args.gateway.gateway_session_header { - config.gateway.session_header = value; - } - if let Some(value) = args.gateway.gateway_debug { - config.gateway.debug = value; - } - if let Some(value) = args.gateway.gateway_stream_markdown { - config.gateway.stream_markdown = value; - } - if !args.gateway.gateway_route.is_empty() { - config.gateway.routes = args - .gateway - .gateway_route - .into_iter() - .map(|route| route.0) - .collect(); - } - - if let Some(value) = args.record.record_format { - config.record.format = value; - } - if let Some(value) = args.record.record_destination { - config.record.destination = Some(value); - } - - Ok(()) -} - -fn validate_vm_rootfs_platform(config: &RunConfig) -> anyhow::Result<()> { - if config.run.executor == RunExecutorKind::Vm - && config.vm.rootfs.as_deref() == Some(Path::new("/")) - { - anyhow::ensure!( - cfg!(target_os = "linux"), - "the host root filesystem can only be used as a VM rootfs on Linux; use --rootfs image= or --rootfs with a prepared Linux rootfs" - ); - } - Ok(()) -} - -fn validate(config: &RunConfig) -> anyhow::Result<()> { - validate_vm_rootfs_platform(config)?; - if config.run.command.is_empty() { - bail!("missing Agent command; pass it after `--` or set run.command"); - } - let overlay_path = config - .overlayfs - .as_ref() - .and_then(|overlay| overlay.target.as_deref().or(overlay.merged_dir.as_deref())); - if let Some(path) = overlay_path { - anyhow::ensure!( - path.is_absolute(), - "--overlayfs-path must be an absolute Agent-visible path" - ); - anyhow::ensure!( - !path - .components() - .any(|component| matches!(component, std::path::Component::ParentDir)), - "--overlayfs-path must not contain .." - ); - } - if config.run.executor == RunExecutorKind::Container { - if config.overlaynet.mode == OverlayNetMode::Proxy - && config.container.network != ContainerNetwork::Host - { - bail!("the in-process OverlayNet/Gateway requires container.network = \"host\""); - } - ContainerExecutor::new(config.container.clone())?; - } - if config.run.executor == RunExecutorKind::Vm { - VmExecutor::new(config.vm.clone())?; - let rootfs = config - .vm - .rootfs - .as_deref() - .context("VM execution requires vm.rootfs or --rootfs ")?; - if overlay_path.is_none() { - anyhow::ensure!( - config - .overlayfs - .as_ref() - .and_then(|overlay| overlay.base.as_deref()) - == Some(rootfs), - "VM execution requires vm.rootfs as its OverlayFS base" - ); - } - anyhow::ensure!( - config.overlaynet.mode != OverlayNetMode::Proxy, - "libkrun uses the smoltcp driver; choose --overlaynet auto or off" - ); - } - if let Some(overlayfs) = &config.overlayfs - && overlayfs.commit == OverlayFsCommit::Apply - && !overlayfs.compose.is_empty() - { - bail!( - "--overlayfs-commit apply cannot be combined with --overlayfs-compose until composed layers can be materialized safely" - ); - } - if config.overlaynet.mode == OverlayNetMode::Off { - if config.overlaynet.policy != OverlayNetPolicy::Public - || !config.overlaynet.allow.is_empty() - || !config.overlaynet.rules.is_empty() - || !config.overlaynet.deny.is_empty() - || !config.overlaynet.limits.is_empty() - { - bail!("OverlayNet policy options require --overlaynet auto or proxy"); - } - if config.gateway.mode == GatewayMode::Capture { - bail!("--gateway-mode capture requires OverlayNet auto or proxy"); - } - } - if config.overlaynet.mode == OverlayNetMode::Proxy - || config.gateway.mode == GatewayMode::Capture - { - let listen: std::net::SocketAddr = config.overlaynet.listen.parse().with_context(|| { - format!( - "invalid OverlayNet listen address {}", - config.overlaynet.listen - ) - })?; - if listen.port() == 0 { - bail!("OverlayNet port 0 is not supported; choose an explicit free port"); - } - } - if config.overlaynet.policy != OverlayNetPolicy::Allowlist - && (!config.overlaynet.allow.is_empty() || !config.overlaynet.rules.is_empty()) - { - bail!("OverlayNet allow entries and rules require --overlaynet-policy allowlist"); - } - match config.gateway.mode { - GatewayMode::Off if !config.gateway.routes.is_empty() => { - bail!("Gateway routes require --gateway-mode capture"); - } - // Capture without explicit routes uses the Gateway's default route; - // this is required by internal pPilot delegation. - GatewayMode::Capture if config.gateway.routes.is_empty() => {} - _ => {} - } - Ok(()) -} - -fn resolve_workspace(workspace: &Path) -> anyhow::Result { - let workspace = workspace - .canonicalize() - .with_context(|| format!("resolve pVisor workspace {}", workspace.display()))?; - anyhow::ensure!( - workspace.is_dir(), - "pVisor workspace must be a directory: {}", - workspace.display() - ); - Ok(workspace) -} - -fn resolve_vm_layout(config: &RunConfig) -> anyhow::Result<(PathBuf, PathBuf)> { - let rootfs = config - .vm - .rootfs - .as_deref() - .context("VM execution requires vm.rootfs or --rootfs ")?; - let rootfs = resolve_directory(rootfs, "libkrun rootfs")?; - let workspace = config - .overlayfs - .as_ref() - .filter(|overlay| overlay.target.is_some()) - .and_then(|overlay| overlay.base.clone()) - .or_else(|| config.run.workspace.clone()) - .unwrap_or(std::env::current_dir()?); - let workspace = resolve_workspace(&workspace)?; - Ok((rootfs, workspace)) -} - -fn resolve_run_storage(storage: &Path) -> anyhow::Result { - std::fs::create_dir_all(storage) - .with_context(|| format!("create pVisor Run storage {}", storage.display()))?; - storage - .canonicalize() - .with_context(|| format!("resolve pVisor Run storage {}", storage.display())) -} - -fn select_run_storage( - config: &RunConfig, - workspace: &Path, - run_id: &str, -) -> anyhow::Result { - if let Some(stage) = config - .overlayfs - .as_ref() - .and_then(|overlay| overlay.stage.clone()) - { - return resolve_run_storage(&stage); - } - let run_home = default_run_home(); - let run_home = if run_home.is_absolute() { - run_home - } else { - std::env::current_dir()?.join(run_home) - }; - let preferred = run_home.join(run_id); - let Some(overlayfs) = &config.overlayfs else { - return Ok(preferred); - }; - let mut read_only_layers = Vec::with_capacity(overlayfs.compose.len() + 1); - for layer in &overlayfs.compose { - read_only_layers.push(resolve_directory(layer, "OverlayFS compose layer")?); - } - read_only_layers.push(resolve_directory( - overlayfs.base.as_deref().unwrap_or(workspace), - "OverlayFS base", - )?); - if read_only_layers - .iter() - .any(|layer| paths_overlap(layer, &preferred)) - { - Ok(std::env::temp_dir().join("persisting-runs").join(run_id)) - } else { - Ok(preferred) - } -} - -fn resolve_directory(path: &Path, description: &str) -> anyhow::Result { - let path = path - .canonicalize() - .with_context(|| format!("resolve {description} {}", path.display()))?; - anyhow::ensure!( - path.is_dir(), - "{description} must be a directory: {}", - path.display() - ); - Ok(path) -} - -fn resolve_overlay( - config: &RunConfig, - workspace: &Path, - storage: &Path, - run_id: &str, -) -> anyhow::Result> { - let Some(overlayfs) = &config.overlayfs else { - return Ok(None); - }; - let base = resolve_directory( - overlayfs.base.as_deref().unwrap_or(workspace), - "OverlayFS base", - )?; - let stage = overlayfs - .stage - .clone() - .unwrap_or_else(|| storage.to_path_buf()); - let stage = if stage.exists() { - stage - .canonicalize() - .with_context(|| format!("resolve OverlayFS stage {}", stage.display()))? - } else { - std::fs::create_dir_all(&stage) - .with_context(|| format!("create OverlayFS stage {}", stage.display()))?; - stage - .canonicalize() - .with_context(|| format!("resolve OverlayFS stage {}", stage.display()))? - }; - anyhow::ensure!( - base != stage && !base.starts_with(&stage), - "OverlayFS stage must not contain its base: base={}, stage={}", - base.display(), - stage.display() - ); - let mut compose = Vec::with_capacity(overlayfs.compose.len()); - for layer in overlayfs.compose.iter().rev() { - let layer = resolve_directory(layer, "OverlayFS compose layer")?; - anyhow::ensure!( - layer != stage && !layer.starts_with(&stage), - "OverlayFS stage must not contain a compose layer: compose={}, stage={}", - layer.display(), - stage.display() - ); - compose.push(layer); - } - // The overlay implementation expects highest-priority lowers first. The - // workspace/base is the implicit bottom layer beneath explicit compose - // entries. - compose.push(base); - let merged_dir = overlayfs.merged_dir.clone(); - Ok(Some(OverlayHint { - lower_dirs: compose, - stage_dir: Some(stage.clone()), - merged_dir, - backend: match overlayfs.backend { - OverlayFsBackend::Directory => OverlayBackend::Directory, - OverlayFsBackend::Jujutsu => OverlayBackend::Jujutsu, - }, - jujutsu_store_path: (overlayfs.backend == OverlayFsBackend::Jujutsu) - .then(|| stage.join("jujutsu")), - jujutsu_workspace: (overlayfs.backend == OverlayFsBackend::Jujutsu) - .then(|| run_id.to_owned()), - auto_apply: overlayfs.commit == OverlayFsCommit::Apply, - auto_discard: overlayfs.commit == OverlayFsCommit::Drop, - ..OverlayHint::default() - })) -} - -fn resolve_proxy(config: &RunConfig) -> anyhow::Result> { - // VM Auto uses smoltcp directly. A loopback HTTP listener is still needed - // only when the explicit Gateway capture sink is enabled. - if config.run.executor == RunExecutorKind::Vm && config.gateway.mode == GatewayMode::Off { - return Ok(None); - } - if config.overlaynet.mode != OverlayNetMode::Proxy - && config.gateway.mode != GatewayMode::Capture - { - return Ok(None); - } - let network = NetworkConfig { - mode: match config.overlaynet.policy { - OverlayNetPolicy::Public => NetworkMode::Public, - OverlayNetPolicy::Deny => NetworkMode::NoNetwork, - OverlayNetPolicy::Allowlist => NetworkMode::Allowlist, - }, - allowed_hosts: config.overlaynet.allow.clone(), - rules: config.overlaynet.rules.clone(), - deny_rules: config.overlaynet.deny.clone(), - limits: config.overlaynet.limits.clone(), - }; - let proxy = ProxyConfig { - listen: config.overlaynet.listen.clone(), - admin_listen: config.gateway.admin_listen.clone(), - agent_id: config.run.agent.clone(), - session_header: config.gateway.session_header.clone(), - capture_level: config.gateway.level, - debug: config.gateway.debug, - network, - overlay: OverlayConfig::default(), - models: if config.gateway.mode == GatewayMode::Capture { - config.gateway.routes.clone() - } else { - Vec::new() - }, - }; - proxy.validate()?; - Ok(Some(proxy)) -} - -fn paths_overlap(left: &Path, right: &Path) -> bool { - left.starts_with(right) || right.starts_with(left) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn stage_spec_parses_persistent_and_drop_forms() { - assert!( - matches!(parse_stage("runs/task").unwrap(), StageSpec::Persistent(path) if path == *"runs/task") - ); - assert!(matches!(parse_stage("drop").unwrap(), StageSpec::Temporary)); - assert!( - matches!(parse_stage("drop:/tmp/task").unwrap(), StageSpec::TemporaryAt(path) if path == *"/tmp/task") - ); - assert!(parse_stage("drop:").is_err()); - } - - #[test] - fn spec_format_is_detected_from_content() { - let temp = tempfile::tempdir().unwrap(); - let json = temp.path().join("config.toml"); - std::fs::write(&json, b" {\"run_id\": \"x\" }").unwrap(); - assert!(spec_is_json(&json).unwrap()); - std::fs::write(&json, b"[run]\nagent = \"x\"\n").unwrap(); - assert!(!spec_is_json(&json).unwrap()); - } - use clap::Parser; - use proptest::prelude::*; - - use crate::cli::Cli; - - #[test] - fn safe_profile_builds_a_reviewable_default_run() { - let crate::cli::Command::Run(args) = - Cli::try_parse_from(["pvisor", "run", "--", "/usr/bin/true"]) - .unwrap() - .command - else { - unreachable!() - }; - let mut config = RunConfig::default(); - apply_cli(&mut config, *args).unwrap(); - apply_safe_defaults(&mut config).unwrap(); - assert!(config.overlayfs.is_none(), "stage is opt-in"); - assert_eq!(config.overlaynet.mode, OverlayNetMode::Auto); - assert_eq!(config.run.agent, "true"); - assert_ne!( - config.overlaynet.listen, - OverlayNetSettings::default().listen - ); - } - - #[test] - fn fork_inherits_or_reidentifies_the_agent_with_its_command() { - let source = vec!["/bin/sh".into(), "-c".into(), "work".into()]; - assert_eq!( - fork_command("sh", &source, Vec::new()), - ("sh".into(), source) - ); - assert_eq!( - fork_command("sh", &[], vec!["/usr/local/bin/codex".into()]), - ("codex".into(), vec!["/usr/local/bin/codex".into()]) - ); - } - - #[test] - fn cli_can_express_all_driver_domains_without_a_config_file() { - Cli::try_parse_from([ - "pvisor", - "run", - "--overlayfs-compose", - "/tmp/lower", - "--overlaynet", - "proxy", - "--overlaynet-policy", - "allowlist", - "--overlaynet-allow", - "api.openai.com", - "--overlaynet-rule", - r#"host="api.openai.com", ports=[443], transports=["tcp_tunnel"]"#, - "--gateway-mode", - "capture", - "--gateway-route", - r#"name="openai", upstream="https://api.openai.com/v1""#, - "--record-format", - "lance", - "--record-destination", - "s3://trajectory-bucket/pvisor-runs", - "--", - "codex", - ]) - .expect("complete command line should parse"); - } - - #[test] - fn cli_record_options_are_the_only_persistence_selection() { - let _ = Cli::try_parse_from([ - "pvisor", - "run", - "--record-format", - "json", - "--record-destination", - "/tmp/events", - "--", - "codex", - ]) - .unwrap(); - let _ = Cli::try_parse_from([ - "pvisor", - "run", - "--record-format", - "lance", - "--record-destination", - "s3://warehouse/runs", - "--", - "codex", - ]) - .unwrap(); - } - - #[test] - fn cli_selects_and_configures_container_executor() { - let crate::cli::Command::Run(args) = Cli::try_parse_from([ - "pvisor", - "run", - "--container-runtime", - "podman", - "--container-image", - "example/agent:latest", - "--container-pvisor-binary", - "/opt/artifacts/pvisor-linux-amd64", - "--container-platform", - "linux/amd64", - "--container-network", - "none", - "--container-read-only-rootfs", - "--container-mount", - r#"source="/tmp", target="/workspace", read_only=true"#, - "--", - "agent", - ]) - .unwrap() - .command - else { - unreachable!() - }; - let mut config = RunConfig::default(); - apply_cli(&mut config, *args).unwrap(); - assert_eq!(config.run.executor, RunExecutorKind::Container); - assert_eq!(config.container.runtime, Path::new("podman")); - assert_eq!(config.container.image, "example/agent:latest"); - assert_eq!( - config.container.pvisor_binary.as_deref(), - Some(Path::new("/opt/artifacts/pvisor-linux-amd64")) - ); - assert_eq!( - config.container.platform, - Some(ContainerPlatform::LinuxAmd64) - ); - assert_eq!(config.container.network, ContainerNetwork::None); - assert!(config.container.read_only_rootfs); - assert_eq!(config.container.mounts.len(), 1); - assert!(config.container.mounts[0].read_only); - validate(&config).unwrap(); - } - - #[test] - fn proxy_requires_host_network_for_container_executor() { - let mut config = RunConfig::default(); - config.run.command = vec!["agent".into()]; - config.run.executor = RunExecutorKind::Container; - config.container.image = "example/agent:latest".into(); - config.container.network = ContainerNetwork::Bridge; - config.run.workspace = Some("/tmp/run".into()); - config.overlaynet.mode = OverlayNetMode::Proxy; - let error = validate(&config).unwrap_err(); - assert!(error.to_string().contains("container.network = \"host\"")); - } - - #[test] - fn cli_selects_and_configures_vm_executor() { - let temporary = tempfile::tempdir().unwrap(); - let libraries = temporary.path().join("lib"); - std::fs::create_dir(&libraries).unwrap(); - let crate::cli::Command::Run(args) = Cli::try_parse_from([ - "pvisor", - "run", - "--rootfs", - temporary.path().to_str().unwrap(), - "--vm-library-dir", - libraries.to_str().unwrap(), - "--memory", - "4294967296", - "--cpu", - "4", - "--", - "agent", - ]) - .unwrap() - .command - else { - unreachable!() - }; - let mut config = RunConfig::default(); - apply_cli(&mut config, *args).unwrap(); - assert_eq!(config.run.executor, RunExecutorKind::Vm); - assert_eq!(config.vm.rootfs.as_deref(), Some(temporary.path())); - assert_eq!(config.vm.library_dir.as_deref(), Some(libraries.as_path())); - assert_eq!(config.vm.memory_mib, 4096); - assert_eq!(config.vm.cpus, 4); - } - - #[test] - fn host_rootfs_obeys_the_linux_vm_boundary() { - let crate::cli::Command::Run(args) = - Cli::try_parse_from(["pvisor", "run", "--rootfs", "host", "--", "/bin/true"]) - .unwrap() - .command - else { - unreachable!() - }; - let mut config = RunConfig::default(); - let result = apply_cli(&mut config, *args); - - #[cfg(target_os = "linux")] - { - result.unwrap(); - assert_eq!(config.run.executor, RunExecutorKind::Vm); - assert_eq!(config.vm.rootfs.as_deref(), Some(Path::new("/"))); - assert!(config.vm.image.is_none()); - assert!(!config.vm.rootfs_immutable); - } - #[cfg(not(target_os = "linux"))] - assert!( - result - .unwrap_err() - .to_string() - .contains("only supported on Linux") - ); - } - - #[test] - fn host_rootfs_conflicts_with_other_vm_rootfs_sources() { - for rootfs_value in ["image=/tmp/rootfs-image", "/tmp/rootfs"] { - let error = Cli::try_parse_from([ - "pvisor", - "run", - "--rootfs", - "host", - "--rootfs", - rootfs_value, - "--", - "/bin/true", - ]) - .unwrap_err(); - assert_eq!(error.kind(), clap::error::ErrorKind::ArgumentConflict); - } - } - - #[cfg(target_os = "linux")] - #[test] - fn host_rootfs_rejects_an_explicit_non_vm_executor() { - let crate::cli::Command::Run(args) = Cli::try_parse_from([ - "pvisor", - "run", - "--executor", - "host", - "--rootfs", - "host", - "--", - "/bin/true", - ]) - .unwrap() - .command - else { - unreachable!() - }; - let error = apply_cli(&mut RunConfig::default(), *args).unwrap_err(); - assert!(error.to_string().contains("requires --executor vm")); - } - - #[test] - fn vm_rejects_the_host_only_explicit_proxy_mode() { - let temporary = tempfile::tempdir().unwrap(); - let mut config = RunConfig::default(); - config.run.command = vec!["agent".into()]; - config.run.executor = RunExecutorKind::Vm; - config.vm.rootfs = Some(temporary.path().to_path_buf()); - config.vm.library_dir = Some(temporary.path().to_path_buf()); - std::fs::write(temporary.path().join(crate::vm::firmware_name()), []).unwrap(); - config.overlayfs = Some(OverlayFsSettings { - base: Some(temporary.path().to_path_buf()), - ..OverlayFsSettings::default() - }); - config.overlaynet.mode = OverlayNetMode::Proxy; - let error = validate(&config).unwrap_err(); - assert!(error.to_string().contains("smoltcp driver")); - } - - #[test] - fn explicit_off_is_not_overridden_by_vm_policy_flags() { - let crate::cli::Command::Run(args) = Cli::try_parse_from([ - "pvisor", - "run", - "--executor", - "vm", - "--overlaynet", - "off", - "--overlaynet-deny-all", - "--", - "true", - ]) - .unwrap() - .command - else { - unreachable!() - }; - let mut config = RunConfig::default(); - apply_cli(&mut config, *args).unwrap(); - assert_eq!(config.overlaynet.mode, OverlayNetMode::Off); - assert_eq!(config.overlaynet.policy, OverlayNetPolicy::Deny); - } - - #[test] - fn vm_resolves_a_guest_overlay_separate_from_the_rootfs() { - let temporary = tempfile::tempdir().unwrap(); - let rootfs = temporary.path().join("rootfs"); - let project = temporary.path().join("project"); - std::fs::create_dir(&rootfs).unwrap(); - std::fs::create_dir(&project).unwrap(); - let mut config = RunConfig::default(); - config.vm.rootfs = Some(rootfs.clone()); - config.overlayfs = Some(OverlayFsSettings { - base: Some(project.clone()), - target: Some("/work/project".into()), - ..OverlayFsSettings::default() - }); - let (resolved_rootfs, resolved_workspace) = resolve_vm_layout(&config).unwrap(); - assert_eq!(resolved_rootfs, rootfs.canonicalize().unwrap()); - assert_eq!(resolved_workspace, project.canonicalize().unwrap()); - } - - #[test] - fn overlayfs_path_is_valid_for_vm_executor() { - let mut config = RunConfig::default(); - config.run.command = vec!["true".into()]; - config.overlayfs = Some(OverlayFsSettings { - base: Some("/tmp/project".into()), - target: Some("/workspace".into()), - ..OverlayFsSettings::default() - }); - config.vm.rootfs = Some(tempfile::tempdir().unwrap().keep()); - config.run.executor = RunExecutorKind::Vm; - assert!(validate(&config).is_ok()); - } - - #[test] - fn cli_exposes_overlay_path_and_ordered_compose_layers() { - let crate::cli::Command::Run(args) = Cli::try_parse_from([ - "pvisor", - "run", - "--rootfs", - "image=ubuntu:latest", - "--overlayfs-compose", - "/tmp/project", - "--overlayfs-path", - "/work/project", - "--stage", - "/tmp/stage", - "--", - "/bin/true", - ]) - .unwrap() - .command - else { - unreachable!() - }; - let mut config = RunConfig::default(); - apply_cli(&mut config, *args).unwrap(); - assert_eq!(config.run.executor, RunExecutorKind::Vm); - let overlay = config.overlayfs.unwrap(); - assert_eq!(overlay.base, None); - assert_eq!(overlay.target.as_deref(), Some(Path::new("/work/project"))); - assert_eq!(overlay.compose, vec![PathBuf::from("/tmp/project")]); - assert_eq!(overlay.stage.as_deref(), Some(Path::new("/tmp/stage"))); - } - - #[test] - fn image_selects_the_daemonless_vm_executor() { - let crate::cli::Command::Run(args) = Cli::try_parse_from([ - "pvisor", - "run", - "--rootfs", - "image=ubuntu:24.04", - "--image-store", - "/tmp/pvisor-images", - "--", - "/bin/true", - ]) - .unwrap() - .command - else { - unreachable!() - }; - let mut config = RunConfig::default(); - apply_cli(&mut config, *args).unwrap(); - assert_eq!(config.run.executor, RunExecutorKind::Vm); - assert_eq!(config.vm.image.as_deref(), Some("ubuntu:24.04")); - assert_eq!( - config.vm.image_store.as_deref(), - Some(Path::new("/tmp/pvisor-images")) - ); - assert!(config.vm.rootfs.is_none()); - } - - #[test] - fn cli_lists_replace_config_lists() { - let mut config = RunConfig::default(); - config.overlaynet.allow = vec!["old.example".into()]; - let crate::cli::Command::Run(args) = Cli::try_parse_from([ - "pvisor", - "run", - "--overlaynet-allow", - "new.example", - "--", - "true", - ]) - .unwrap() - .command - else { - unreachable!() - }; - apply_cli(&mut config, *args).unwrap(); - assert!(config.overlaynet.allow.is_empty()); - assert_eq!(config.overlaynet.rules.len(), 1); - assert_eq!(config.overlaynet.rules[0].host, "new.example"); - assert_eq!(config.overlaynet.mode, OverlayNetMode::Proxy); - assert_eq!(config.overlaynet.policy, OverlayNetPolicy::Allowlist); - } - - #[test] - fn safe_defaults_disable_inheritance_and_cli_maps_resource_limits() { - let crate::cli::Command::Run(args) = Cli::try_parse_from([ - "pvisor", - "run", - "--pass-env", - "EXPLICIT_TOKEN", - "--memory", - "1048576", - "--max-processes", - "8", - "--max-open-files", - "32", - "--max-stage-size", - "2MiB", - "--", - "true", - ]) - .unwrap() - .command - else { - unreachable!() - }; - let mut config = RunConfig::default(); - apply_cli(&mut config, *args).unwrap(); - apply_safe_defaults(&mut config).unwrap(); - assert!(!config.run.inherit_env); - assert_eq!(config.run.pass_env, ["EXPLICIT_TOKEN"]); - assert_eq!(config.run.resource_limits.memory_bytes, Some(1_048_576)); - assert_eq!(config.run.resource_limits.processes, Some(8)); - assert_eq!(config.run.resource_limits.open_files, Some(32)); - assert_eq!( - config - .overlayfs - .as_ref() - .and_then(|overlay| overlay.stage_size_bytes), - Some(2 * 1024 * 1024) - ); - } - - #[test] - fn simple_network_flags_repeat_and_infer_policy() { - let crate::cli::Command::Run(args) = Cli::try_parse_from([ - "pvisor", - "run", - "--overlaynet-allow", - "api.example.com:443", - "--overlaynet-allow", - "packages.example.com", - "--overlaynet-deny", - "169.254.0.0/16", - "--overlaynet-deny", - "bad.example.com:80", - "--overlaynet-limit", - "10mbps", - "--overlaynet-limit", - "api.example.com:443=2mbps", - "--", - "true", - ]) - .unwrap() - .command - else { - unreachable!() - }; - let mut config = RunConfig::default(); - apply_cli(&mut config, *args).unwrap(); - - assert_eq!(config.overlaynet.mode, OverlayNetMode::Proxy); - assert_eq!(config.overlaynet.policy, OverlayNetPolicy::Allowlist); - assert_eq!(config.overlaynet.rules.len(), 2); - assert_eq!(config.overlaynet.rules[0].ports, [443]); - assert_eq!(config.overlaynet.deny.len(), 2); - assert_eq!(config.overlaynet.deny[1].ports, [80]); - assert_eq!(config.overlaynet.limits.len(), 2); - assert_eq!(config.overlaynet.limits[0].bytes_per_second, 1_250_000); - assert_eq!( - config.overlaynet.limits[1].host.as_deref(), - Some("api.example.com") - ); - assert_eq!(config.overlaynet.limits[1].bytes_per_second, 250_000); - assert!(config.run.workspace.is_none()); - validate(&config).unwrap(); - } - - #[test] - fn overlaynet_without_value_defaults_to_proxy() { - let crate::cli::Command::Run(args) = - Cli::try_parse_from(["pvisor", "run", "--overlaynet", "--", "true"]) - .unwrap() - .command - else { - unreachable!() - }; - let mut config = RunConfig::default(); - apply_cli(&mut config, *args).unwrap(); - assert_eq!(config.overlaynet.mode, OverlayNetMode::Proxy); - } - - #[test] - fn help_exposes_driver_selection_and_the_simple_network_policy_surface() { - let error = Cli::try_parse_from(["pvisor", "run", "--help"]).unwrap_err(); - let help = error.to_string(); - assert!(help.contains("--overlaynet-allow")); - assert!(help.contains("--overlaynet-deny")); - assert!(help.contains("--overlaynet-limit")); - assert!(help.contains("--overlaynet-deny-all")); - assert!(help.contains("--overlaynet []")); - #[cfg(target_os = "linux")] - { - assert!(help.to_ascii_lowercase().contains("network")); - assert!(help.contains("private network namespace")); - } - #[cfg(target_os = "macos")] - assert!(help.contains("ambient host Unix sockets")); - assert!(help.contains("--overlayfs-path")); - assert!(help.contains("--rootfs")); - assert!(!help.contains("--workspace")); - assert!(!help.contains("--overlaynet-policy")); - assert!(!help.contains("--overlaynet-rule")); - } - - #[test] - fn macos_help_disclosures_are_rendered_on_every_platform() { - use clap::CommandFactory; - - let mut command = Cli::command() - .mut_subcommand("run", |run| run.long_about(MACOS_RUN_COMMAND_LONG_ABOUT)); - let help = command - .find_subcommand_mut("run") - .expect("run subcommand") - .render_long_help() - .to_string(); - // Terminal wrapping must not affect checks of the safety description. - let help = help.split_whitespace().collect::>().join(" "); - for disclosure in [ - "safe-best-effort", - "macFUSE", - "Seatbelt", - "Full-disk reads remain ambient", - "selective network policies remain cooperative", - "ambient host Unix sockets", - "Run-scoped Unix IPC", - "reported as warnings in best-effort mode", - "With --strict", - "fail before Agent execution", - ] { - assert!( - help.contains(disclosure), - "missing disclosure: {disclosure}" - ); - } - } - - #[test] - fn safe_help_describes_the_effective_platform_boundary() { - let help = Cli::try_parse_from(["pvisor", "run", "--help"]) - .unwrap_err() - .to_string(); - - #[cfg(target_os = "linux")] - { - assert!(help.contains("safe-best-effort")); - assert!(help.contains("Host execution uses safe-best-effort isolation")); - } - #[cfg(target_os = "macos")] - { - assert!(help.contains("macFUSE")); - assert!(help.contains("Seatbelt")); - assert!(help.contains("Full-disk reads remain ambient")); - assert!(help.contains("fail before Agent execution")); - } - } - - #[test] - fn help_exposes_compositional_overlayfs_without_a_mode_switch() { - let error = Cli::try_parse_from(["pvisor", "run", "--help"]).unwrap_err(); - let help = error.to_string(); - for option in [ - "--overlayfs-path", - "--overlayfs-compose", - "--stage", - "--overlayfs-backend", - "--overlayfs-commit", - ] { - assert!(help.contains(option), "missing {option}"); - } - for obsolete in ["--overlayfs-mode", "--overlayfs-lower"] { - assert!(!help.contains(obsolete), "obsolete option {obsolete}"); - } - } - - #[test] - fn deny_all_is_discoverable_and_replaces_configured_policy_details() { - let crate::cli::Command::Run(args) = - Cli::try_parse_from(["pvisor", "run", "--overlaynet-deny-all", "--", "true"]) - .unwrap() - .command - else { - unreachable!() - }; - let mut config = RunConfig::default(); - config.overlaynet.allow = vec!["old.example".into()]; - config.overlaynet.deny = vec![NetworkAccessRule { - host: "blocked.example".into(), - ports: Vec::new(), - transports: Vec::new(), - allow_private_ips: false, - }]; - config.overlaynet.limits = vec![NetworkBandwidthLimit { - host: None, - port: None, - bytes_per_second: 1_000, - }]; - - apply_cli(&mut config, *args).unwrap(); - - assert_eq!(config.overlaynet.mode, OverlayNetMode::Proxy); - assert_eq!(config.overlaynet.policy, OverlayNetPolicy::Deny); - assert!(config.overlaynet.allow.is_empty()); - assert!(config.overlaynet.rules.is_empty()); - assert!(config.overlaynet.deny.is_empty()); - assert!(config.overlaynet.limits.is_empty()); - } - - #[test] - fn gateway_capture_enables_overlaynet_without_a_driver_flag() { - let crate::cli::Command::Run(args) = Cli::try_parse_from([ - "pvisor", - "run", - "--gateway-mode", - "capture", - "--gateway-route", - r#"name="openai", upstream="https://api.openai.com/v1""#, - "--", - "true", - ]) - .unwrap() - .command - else { - unreachable!() - }; - let mut config = RunConfig::default(); - apply_cli(&mut config, *args).unwrap(); - assert_eq!(config.gateway.mode, GatewayMode::Capture); - assert_eq!(config.overlaynet.mode, OverlayNetMode::Proxy); - } - - #[test] - fn target_parser_handles_cidrs_portless_ipv6_and_malformed_inputs() { - let cidr = parse_overlaynet_target("10.0.0.0/8:8080").unwrap(); - assert_eq!(cidr.host, "10.0.0.0/8"); - assert_eq!(cidr.ports, [8080]); - - assert!( - parse_overlaynet_target("2001:db8::1") - .unwrap() - .ports - .is_empty() - ); - - for invalid in ["", "api.example.com:", "https://api.example.com", "[::1"] { - assert!( - parse_overlaynet_target(invalid).is_err(), - "accepted invalid target {invalid:?}" - ); - } - } - - proptest! { - #[test] - fn target_parser_preserves_valid_domain_ports( - label in "[a-z][a-z0-9]{0,12}", - port in 1u16..=u16::MAX, - ) { - let host = format!("api-{label}.example.com"); - let target = parse_overlaynet_target(&format!("{host}:{port}")) - .expect("generated domain target should parse"); - prop_assert_eq!(target.host, host); - prop_assert_eq!(target.ports, vec![port]); - prop_assert!(target.transports.is_empty()); - prop_assert!(!target.allow_private_ips); - } - - #[test] - fn target_parser_preserves_valid_ipv6_ports( - segment in 1u16..=u16::MAX, - port in 1u16..=u16::MAX, - ) { - let input = format!("[2001:db8::{segment:x}]:{port}"); - let target = parse_overlaynet_target(&input) - .expect("generated IPv6 target should parse"); - prop_assert_eq!(target.host, format!("2001:db8::{segment:x}")); - prop_assert_eq!(target.ports, vec![port]); - } - - #[test] - fn bandwidth_parser_matches_bit_and_byte_units( - amount in 1u64..=1_000_000u64, - unit in prop_oneof![ - Just(("bps", 1u64, true)), - Just(("kbps", 1_000u64, true)), - Just(("mbps", 1_000_000u64, true)), - Just(("b/s", 1u64, false)), - Just(("kb/s", 1_000u64, false)), - Just(("mb/s", 1_000_000u64, false)), - Just(("GB/S", 1_000_000_000u64, false)), - ], - ) { - let (suffix, multiplier, bits) = unit; - let parsed = parse_bandwidth(&format!("{amount}{suffix}")) - .expect("generated bandwidth should parse"); - let scaled = amount * multiplier; - let expected = if bits { scaled.div_ceil(8) } else { scaled }; - prop_assert_eq!(parsed, expected); - } - - #[test] - fn bandwidth_parser_rejects_zero_unsupported_and_overflow( - unit in prop_oneof![ - Just("bps"), - Just("kbps"), - Just("mbps"), - Just("gbps"), - Just("b/s"), - Just("kb/s"), - Just("mb/s"), - Just("gb/s"), - ], - amount in 1u64..=1_000_000u64, - unsupported_suffix in prop_oneof![Just(""), Just("fast"), Just("tbps")], - overflow_amount in 18_446_744_074u64..=u64::MAX, - ) { - let zero_input = format!("0{unit}"); - let unsupported_input = format!("{amount}{unsupported_suffix}"); - let overflow_input = format!("{overflow_amount}gbps"); - prop_assert!(parse_bandwidth(&zero_input).is_err()); - prop_assert!(parse_bandwidth(&unsupported_input).is_err()); - prop_assert!(parse_bandwidth(&overflow_input).is_err()); - prop_assert!(parse_bandwidth("").is_err()); - } - - #[test] - fn target_parser_rejects_zero_and_out_of_range_ports( - port in prop_oneof![Just(0u32), 65_536u32..=u32::MAX] - ) { - let input = format!("api.example.com:{port}"); - prop_assert!( - port == 0 || port > u32::from(u16::MAX), - "this property only generates invalid ports" - ); - prop_assert!(parse_overlaynet_target(&input).is_err()); - } - } - - #[test] - fn cli_structured_rules_replace_config_rules() { - let mut config = RunConfig::default(); - config.overlaynet.rules = vec![NetworkAccessRule { - host: "old.example".into(), - ports: vec![80], - transports: Vec::new(), - allow_private_ips: false, - }]; - let crate::cli::Command::Run(args) = Cli::try_parse_from([ - "pvisor", - "run", - "--overlaynet-rule", - r#"host="new.example", ports=[443], transports=["tcp_tunnel"]"#, - "--", - "true", - ]) - .unwrap() - .command - else { - unreachable!() - }; - apply_cli(&mut config, *args).unwrap(); - assert_eq!(config.overlaynet.rules.len(), 1); - assert_eq!(config.overlaynet.rules[0].host, "new.example"); - assert_eq!(config.overlaynet.rules[0].ports, [443]); - } - - #[test] - fn overlayfs_options_enable_the_driver_and_select_a_stage() { - let crate::cli::Command::Run(args) = Cli::try_parse_from([ - "pvisor", - "run", - "--stage", - "/tmp/pvisor-stage", - "--overlayfs-backend", - "jujutsu", - "--", - "true", - ]) - .unwrap() - .command - else { - unreachable!() - }; - let mut config = RunConfig::default(); - apply_cli(&mut config, *args).unwrap(); - let overlayfs = config.overlayfs.expect("OverlayFS should be enabled"); - assert_eq!(overlayfs.backend, OverlayFsBackend::Jujutsu); - assert_eq!( - overlayfs.stage.as_deref(), - Some(Path::new("/tmp/pvisor-stage")) - ); - } - - #[test] - fn overlayfs_defaults_base_to_workspace_and_stage_to_run_storage() { - let temporary = tempfile::tempdir().unwrap(); - let workspace = temporary.path().join("workspace"); - let compose = temporary.path().join("compose"); - let storage = temporary.path().join("run"); - std::fs::create_dir_all(&workspace).unwrap(); - std::fs::create_dir_all(&compose).unwrap(); - std::fs::create_dir_all(&storage).unwrap(); - let config = RunConfig { - overlayfs: Some(OverlayFsSettings { - compose: vec![compose.clone()], - ..OverlayFsSettings::default() - }), - ..RunConfig::default() - }; - - let hint = resolve_overlay(&config, &workspace, &storage, "run-test") - .unwrap() - .unwrap(); - assert_eq!( - hint.lower_dirs, - [ - compose.canonicalize().unwrap(), - workspace.canonicalize().unwrap() - ] - ); - assert_eq!( - hint.stage_dir.as_deref(), - Some(storage.canonicalize().unwrap().as_path()) - ); - } - - #[test] - fn overlayfs_compose_preserves_bottom_to_top_priority() { - let temporary = tempfile::tempdir().unwrap(); - let workspace = temporary.path().join("workspace"); - let bottom = temporary.path().join("bottom"); - let top = temporary.path().join("top"); - let storage = temporary.path().join("run"); - for path in [&workspace, &bottom, &top, &storage] { - std::fs::create_dir_all(path).unwrap(); - } - let config = RunConfig { - overlayfs: Some(OverlayFsSettings { - compose: vec![bottom.clone(), top.clone()], - ..OverlayFsSettings::default() - }), - ..RunConfig::default() - }; - let hint = resolve_overlay(&config, &workspace, &storage, "run-test") - .unwrap() - .unwrap(); - assert_eq!( - hint.lower_dirs, - [ - top.canonicalize().unwrap(), - bottom.canonicalize().unwrap(), - workspace.canonicalize().unwrap() - ] - ); - } - - #[test] - fn overlayfs_allows_hidden_stage_inside_base_or_compose() { - let temporary = tempfile::tempdir().unwrap(); - let workspace = temporary.path().join("workspace"); - let compose = temporary.path().join("compose"); - let storage = temporary.path().join("run"); - std::fs::create_dir_all(workspace.join("stage")).unwrap(); - std::fs::create_dir_all(compose.join("stage")).unwrap(); - std::fs::create_dir_all(&storage).unwrap(); - - for (base, layers, stage) in [ - (workspace.clone(), Vec::new(), workspace.join("stage")), - ( - workspace.clone(), - vec![compose.clone()], - compose.join("stage"), - ), - ] { - let config = RunConfig { - overlayfs: Some(OverlayFsSettings { - base: Some(base), - compose: layers, - stage: Some(stage), - ..OverlayFsSettings::default() - }), - ..RunConfig::default() - }; - assert!(resolve_overlay(&config, &workspace, &storage, "run-test").is_ok()); - } - - let config = RunConfig { - overlayfs: Some(OverlayFsSettings { - base: Some(workspace.clone()), - stage: Some(temporary.path().to_path_buf()), - ..OverlayFsSettings::default() - }), - ..RunConfig::default() - }; - assert!(resolve_overlay(&config, &workspace, &storage, "run-test").is_err()); - } - - #[test] - fn composed_layers_cannot_be_auto_applied() { - let config = RunConfig { - run: crate::config::RunSettings { - command: vec!["true".into()], - ..crate::config::RunSettings::default() - }, - overlayfs: Some(OverlayFsSettings { - compose: vec!["/tmp/layer".into()], - commit: OverlayFsCommit::Apply, - ..OverlayFsSettings::default() - }), - ..RunConfig::default() - }; - assert!( - validate(&config) - .unwrap_err() - .to_string() - .contains("cannot be combined") - ); - } - - #[test] - fn compose_replaces_configured_layers_and_enables_overlayfs() { - let crate::cli::Command::Run(args) = Cli::try_parse_from([ - "pvisor", - "run", - "--overlayfs-compose", - "/tmp/first", - "--overlayfs-compose", - "/tmp/second", - "--", - "true", - ]) - .unwrap() - .command - else { - unreachable!() - }; - let mut config = RunConfig { - overlayfs: Some(OverlayFsSettings { - compose: vec!["/tmp/old".into()], - ..OverlayFsSettings::default() - }), - ..RunConfig::default() - }; - apply_cli(&mut config, *args).unwrap(); - assert_eq!( - config.overlayfs.unwrap().compose, - [PathBuf::from("/tmp/first"), PathBuf::from("/tmp/second")] - ); - } -} diff --git a/crates/persisting-pvisor/src/cli/runtime.rs b/crates/persisting-pvisor/src/cli/runtime.rs deleted file mode 100644 index 6efae0650..000000000 --- a/crates/persisting-pvisor/src/cli/runtime.rs +++ /dev/null @@ -1,435 +0,0 @@ -use std::path::{Path, PathBuf}; -use std::process::Command; - -use anyhow::{Context, bail}; -use clap::Args; - -use crate::runtime::{ - ApplySelection, OverlayState, ReadOnlyOverlayMount, RunLease, RunRecord, - apply_overlay_selected, control_mount_inspect, control_overlay_status, control_ping, - control_unmount_inspect, discard_overlay, is_live, load_apply_records, - mount_overlay_record_read_only, overlay_status, resolve_run, -}; - -const DEFAULT_STORAGE: &str = ".persisting/capture"; - -#[derive(Debug, Clone, Args)] -pub struct StatusArgs { - /// Run id, stage directory, upper directory, or workspace path. - pub selector: Option, - #[arg(long, short = 'o', default_value = DEFAULT_STORAGE)] - pub output_dir: PathBuf, - #[arg(long)] - pub json: bool, -} - -#[derive(Debug, Clone, Args)] -pub struct InspectArgs { - /// Run id, stage directory, upper directory, or workspace path. - pub selector: Option, - #[arg(long, short = 'o', default_value = DEFAULT_STORAGE)] - pub output_dir: PathBuf, - /// Command to run in the read-only view; defaults to $SHELL or /bin/bash. - #[arg(last = true, allow_hyphen_values = true)] - pub command: Vec, -} - -#[derive(Debug, Clone, Args)] -pub struct SelectArgs { - /// Run id, stage directory, upper directory, or workspace path. - pub selector: Option, - #[arg(long, short = 'o', default_value = DEFAULT_STORAGE)] - pub output_dir: PathBuf, -} - -#[derive(Debug, Clone, Args)] -pub struct ApplyArgs { - /// Run id, stage directory, upper directory, or workspace path. - pub selector: Option, - #[arg(long, short = 'o', default_value = DEFAULT_STORAGE)] - pub output_dir: PathBuf, - /// Apply staged changes here instead of the target recorded by the Run. - #[arg(long, value_name = "PATH")] - pub target: Option, - /// Apply this relative path and its descendants. Repeatable. - #[arg(long = "path", value_name = "RELATIVE_PATH")] - pub paths: Vec, - /// Include staged paths matching this glob. Repeatable. - #[arg(long, value_name = "GLOB")] - pub include: Vec, - /// Exclude staged paths matching this glob. Repeatable. - #[arg(long, value_name = "GLOB")] - pub exclude: Vec, - /// Explicitly apply every remaining staged change. - #[arg(long)] - pub all: bool, -} - -pub fn status(args: StatusArgs) -> anyhow::Result<()> { - let record = selected(args.selector.as_deref(), &args.output_dir)?; - let live = control_ping(&record.stage_dir()) || is_live(&record.stage_dir())?; - let apply_history = load_apply_records(&record.stage_dir())?; - let fs = record - .overlay - .as_ref() - .map(|overlay| { - if control_ping(&record.stage_dir()) { - control_overlay_status(&record.stage_dir()).map(|status| FsSummary { - changed_files: status.changed_files, - whiteouts: status.whiteouts, - sample_paths: status.sample_paths, - }) - } else { - overlay_status(overlay) - .map(|status| FsSummary { - changed_files: status.changed_files, - whiteouts: status.whiteouts, - sample_paths: status.sample_paths, - }) - .map_err(Into::into) - } - }) - .transpose()?; - if args.json { - println!( - "{}", - serde_json::to_string_pretty(&serde_json::json!({ - "run": record, - "live": live, - "apply_history": apply_history, - "filesystem": fs.as_ref().map(|status| serde_json::json!({ - "state": record.overlay.as_ref().map(|overlay| overlay.state), - "changed_files": status.changed_files, - "whiteouts": status.whiteouts, - "sample_paths": status.sample_paths, - })), - }))? - ); - return Ok(()); - } - - println!("run: {}", record.run_id); - println!("session: {}", record.session_id); - let state = if live { - "running" - } else if record.state == "running" { - "stale" - } else { - &record.state - }; - println!("state: {state}"); - println!( - "pid: {}{}", - record.pid, - if live { " (live)" } else { " (offline)" } - ); - println!("agent: {}", record.agent); - println!("command: {}", shell_join(&record.command)); - println!("stage: {}", record.stage_dir().display()); - if !apply_history.is_empty() { - println!("apply batches: {}", apply_history.len()); - } - println!("net: {}", serde_json::to_string(&record.network)?); - println!( - "overlaynet: {}", - record.overlaynet_listen.as_deref().unwrap_or("disabled") - ); - if let Some(interception) = &record.network_interception { - println!( - "network interception: {:?} ({:?}, enforcing={})", - interception.driver, - interception.strength, - interception.is_enforcing() - ); - } - println!( - "gateway: {}", - record.gateway_listen.as_deref().unwrap_or("disabled") - ); - if let Some(overlay) = &record.overlay { - let fs = fs.context("OverlayFS status missing")?; - println!("fs: {:?} (read-only inspect available)", overlay.state); - println!("target: {}", overlay.target.display()); - println!("upper: {}", overlay.upper.path().display()); - println!( - "changes: {} paths, {} whiteouts", - fs.changed_files, fs.whiteouts - ); - } else { - println!("fs: host view (no OverlayFS workspace)"); - } - Ok(()) -} - -struct FsSummary { - changed_files: usize, - whiteouts: usize, - sample_paths: Vec, -} - -pub fn inspect(args: InspectArgs) -> anyhow::Result { - let record = selected(args.selector.as_deref(), &args.output_dir)?; - let overlay = record - .overlay - .as_ref() - .context("this Run has no OverlayFS workspace to inspect")?; - let lowers = if record.overlay_lowers.is_empty() { - vec![overlay.target.clone()] - } else { - record.overlay_lowers.clone() - }; - let stage = record.stage_dir(); - let mount = if control_ping(&stage) { - let (id, mountpoint) = control_mount_inspect(&stage)?; - InspectMount::Remote { - stage, - id, - mountpoint, - } - } else { - let inspect_root = overlay.stage_dir.join("inspect").join(format!( - "{}-{}", - std::process::id(), - uuid::Uuid::new_v4() - )); - let mountpoint = inspect_root.join("merged"); - let session = mount_overlay_record_read_only(overlay, &lowers, &mountpoint) - .with_context(|| format!("mount read-only Run view at {}", mountpoint.display()))?; - InspectMount::Local { - inspect_root, - session, - } - }; - - let command = if args.command.is_empty() { - vec![std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".into())] - } else { - args.command - }; - let (program, command_args) = command.split_first().context("missing inspect command")?; - let status = Command::new(program) - .args(command_args) - .current_dir(mount.mountpoint()) - .env("PERSISTING_INSPECT", "1") - .env("PERSISTING_RUN_ID", &record.run_id) - .env("PERSISTING_OVERLAY_STAGE", &overlay.stage_dir) - .status() - .with_context(|| format!("execute inspect command `{program}`")); - mount.close()?; - let status = status?; - Ok(status.code().unwrap_or(1)) -} - -enum InspectMount { - Remote { - stage: PathBuf, - id: String, - mountpoint: PathBuf, - }, - Local { - inspect_root: PathBuf, - session: ReadOnlyOverlayMount, - }, -} - -impl InspectMount { - fn mountpoint(&self) -> &Path { - match self { - Self::Remote { mountpoint, .. } => mountpoint, - Self::Local { session, .. } => session.mountpoint(), - } - } - - fn close(self) -> anyhow::Result<()> { - match self { - Self::Remote { stage, id, .. } => control_unmount_inspect(&stage, id), - Self::Local { - inspect_root, - session, - } => { - session.unmount()?; - let _ = std::fs::remove_dir(inspect_root); - Ok(()) - } - } - } -} - -pub fn apply(args: ApplyArgs) -> anyhow::Result<()> { - if args.all && (!args.paths.is_empty() || !args.include.is_empty() || !args.exclude.is_empty()) - { - bail!("--all cannot be combined with --path, --include, or --exclude"); - } - let selection = ApplySelection { - paths: args.paths, - includes: args.include, - excludes: args.exclude, - }; - let select = SelectArgs { - selector: args.selector, - output_dir: args.output_dir, - }; - mutate(select, true, args.target.as_deref(), Some(&selection)) -} - -pub fn drop_overlay(args: SelectArgs) -> anyhow::Result<()> { - mutate(args, false, None, None) -} - -fn mutate( - args: SelectArgs, - apply: bool, - target: Option<&Path>, - selection: Option<&ApplySelection>, -) -> anyhow::Result<()> { - let mut record = selected(args.selector.as_deref(), &args.output_dir)?; - if is_live(&record.stage_dir())? { - bail!( - "Run {} is still running; its upper cannot be {}", - record.run_id, - if apply { "applied" } else { "dropped" } - ); - } - let _lease = RunLease::acquire(&record.stage_dir())?; - let mut overlay = record - .overlay - .take() - .context("this Run has no OverlayFS workspace")?; - if apply - && record - .overlay_lowers - .iter() - .any(|lower| lower != &overlay.target) - { - bail!( - "Run {} composes read-only layers above its base; apply is disabled until pVisor can materialize the complete merged diff", - record.run_id - ); - } - match (apply, overlay.state) { - (true, OverlayState::Applied) => { - println!( - "already applied {} → {}", - record.run_id, - overlay.target.display() - ); - return Ok(()); - } - (false, OverlayState::Discarded) => { - println!( - "remaining staged changes already dropped for {}", - record.run_id - ); - return Ok(()); - } - (false, OverlayState::Applied) => { - bail!( - "Run {} was already applied; drop cannot undo changes written to {}", - record.run_id, - overlay.target.display() - ); - } - (true, OverlayState::Discarded) => { - bail!( - "Run {} was already dropped; apply cannot recover discarded changes", - record.run_id - ); - } - _ => {} - } - if apply { - if overlay.target == Path::new("/") { - bail!( - "Run {} is a full-root libkrun changeset; checkpoint/fork it or drop it instead of applying it to the host root", - record.run_id - ); - } - if let Some(target) = target { - let target = resolve_apply_target(target, &record.stage_dir())?; - overlay.target = target.clone(); - if let Some(primary_lower) = record.overlay_lowers.first_mut() { - *primary_lower = target; - } else { - record.overlay_lowers.push(target); - } - } - let lower_dirs = if record.overlay_lowers.is_empty() { - vec![overlay.target.clone()] - } else { - record.overlay_lowers.clone() - }; - let outcome = apply_overlay_selected( - &mut overlay, - &lower_dirs, - selection.expect("apply always supplies a selection"), - )?; - println!( - "applied {} changes from {} → {} (apply_id={}, remaining={})", - outcome.applied.len(), - record.run_id, - overlay.target.display(), - outcome.apply_id, - outcome.remaining.len() - ); - } else { - discard_overlay(&mut overlay)?; - println!("dropped remaining staged changes for {}", record.run_id); - } - record.overlay = Some(overlay); - record.write()?; - Ok(()) -} - -fn resolve_apply_target(target: &Path, stage: &Path) -> anyhow::Result { - std::fs::create_dir_all(target) - .with_context(|| format!("create apply target {}", target.display()))?; - let target = target - .canonicalize() - .with_context(|| format!("resolve apply target {}", target.display()))?; - let stage = stage.canonicalize().unwrap_or_else(|_| stage.to_path_buf()); - if target.starts_with(&stage) || stage.starts_with(&target) { - bail!( - "apply target must not overlap the pVisor stage: target={}, stage={}", - target.display(), - stage.display() - ); - } - Ok(target) -} - -fn selected(selector: Option<&Path>, output_dir: &Path) -> anyhow::Result { - let storage = output_dir - .canonicalize() - .unwrap_or_else(|_| output_dir.to_path_buf()); - resolve_run(selector, &storage) -} - -fn shell_join(parts: &[String]) -> String { - parts - .iter() - .map(|part| { - if part - .chars() - .all(|ch| ch.is_ascii_alphanumeric() || "-._/".contains(ch)) - { - part.clone() - } else { - format!("{:?}", part) - } - }) - .collect::>() - .join(" ") -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn shell_join_quotes_only_when_needed() { - assert_eq!( - shell_join(&["rg".into(), "hello world".into()]), - "rg \"hello world\"" - ); - } -} diff --git a/crates/persisting-pvisor/src/cli/trajectory.rs b/crates/persisting-pvisor/src/cli/trajectory.rs deleted file mode 100644 index c4ff44707..000000000 --- a/crates/persisting-pvisor/src/cli/trajectory.rs +++ /dev/null @@ -1,326 +0,0 @@ -//! pChronicle sidecar adapter for pVisor lifecycle and Gateway events. -//! -//! pVisor owns only the shared event contract and this lightweight control -//! client. The sidecar process owns Lance, DataFusion, and object-store code. - -use std::fs::{File, OpenOptions, create_dir_all}; -use std::io::{BufWriter, Write}; -use std::path::{Path, PathBuf}; -use std::sync::{Arc, Mutex, mpsc}; - -use async_trait::async_trait; -use persisting_events::EventRecord; -use persisting_events::{ - ChronicleControl, ChronicleServeProcessClient, TrajectoryAppendRequest, TrajectoryFormat, -}; -use persisting_gateway::session::storage::CaptureRoute; -use persisting_gateway::sink::CallbackSink; - -use crate::{EventAppendErrorKind, EventSink, TrajectoryEventSink}; - -type ChronicleSinks = ( - Arc, - Arc, - ChronicleWriter, - Arc, -); - -/// Append-only JSONL writer used by the lightweight pVisor recording path. -/// The serialized value is the complete EventRecord, not a Markdown or -/// dialogue projection, so HTTP request/response wire bodies remain available. -#[derive(Clone)] -pub struct JsonlWriter { - path: PathBuf, - file: Arc>>, -} - -impl std::fmt::Debug for JsonlWriter { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("JsonlWriter") - .field("path", &self.path) - .finish() - } -} - -impl JsonlWriter { - pub fn open(destination: &Path) -> anyhow::Result { - let path = if destination.extension().is_some() { - destination.to_path_buf() - } else { - destination.join("events.jsonl") - }; - if let Some(parent) = path.parent() { - create_dir_all(parent)?; - } - let file = OpenOptions::new().create(true).append(true).open(&path)?; - Ok(Self { - path, - file: Arc::new(Mutex::new(BufWriter::new(file))), - }) - } - - pub fn append(&self, event: &EventRecord) -> anyhow::Result<()> { - let mut file = self - .file - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - serde_json::to_writer(&mut *file, event)?; - file.write_all(b"\n")?; - file.flush()?; - Ok(()) - } - - pub fn finish(self) -> anyhow::Result<()> { - let mut file = self - .file - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - file.flush()?; - file.get_ref().sync_all()?; - Ok(()) - } -} - -pub struct JsonlEventSink { - writer: JsonlWriter, -} - -impl JsonlEventSink { - pub fn new(writer: JsonlWriter) -> Self { - Self { writer } - } -} - -#[async_trait] -impl EventSink for JsonlEventSink { - async fn append(&self, event: &EventRecord) -> anyhow::Result<()> { - self.writer.append(event) - } - - fn classify_append_error(&self, _error: &anyhow::Error) -> EventAppendErrorKind { - EventAppendErrorKind::Rejected - } -} - -pub fn jsonl_capture_sink( - writer: &JsonlWriter, - default_agent_id: &str, -) -> Arc { - let writer = writer.clone(); - Arc::new(CallbackSink::new( - default_agent_id, - move |_route, _agent, record| writer.append(&record), - )) -} - -const APPEND_QUEUE_CAPACITY: usize = 256; - -#[derive(Debug, thiserror::Error)] -enum SidecarAppendError { - #[error("pChronicle sidecar append queue is full")] - Full, - #[error("pChronicle sidecar append worker is closed")] - Closed, - #[error("pChronicle sidecar append failed: {0}")] - Write(String), -} - -struct AppendCommand { - request: TrajectoryAppendRequest, - ack: mpsc::SyncSender>, -} - -#[derive(Clone)] -struct SidecarAppendSender { - tx: mpsc::SyncSender, - storage: String, - format: TrajectoryFormat, -} - -impl SidecarAppendSender { - fn append_durable( - &self, - agent_id: String, - session_id: String, - root_session_id: Option, - record: EventRecord, - ) -> anyhow::Result<()> { - let (ack, response) = mpsc::sync_channel(1); - let command = AppendCommand { - request: TrajectoryAppendRequest { - storage: self.storage.clone(), - agent_id, - session_id, - format: self.format, - root_session_id, - records: vec![record], - }, - ack, - }; - match self.tx.try_send(command) { - Ok(()) => {} - Err(mpsc::TrySendError::Full(_)) => return Err(SidecarAppendError::Full.into()), - Err(mpsc::TrySendError::Disconnected(_)) => { - return Err(SidecarAppendError::Closed.into()); - } - } - response.recv().map_err(|_| SidecarAppendError::Closed)??; - Ok(()) - } -} - -struct ChronicleEventSink { - tx: SidecarAppendSender, - run_id: String, - agent_id: String, -} - -#[async_trait] -impl EventSink for ChronicleEventSink { - async fn append(&self, event: &EventRecord) -> anyhow::Result<()> { - let tx = self.tx.clone(); - let agent_id = self.agent_id.clone(); - let run_id = self.run_id.clone(); - let event = event.clone(); - tokio::task::spawn_blocking(move || { - tx.append_durable(agent_id, run_id.clone(), Some(run_id), event) - }) - .await - .map_err(|error| anyhow::anyhow!("pChronicle append task failed: {error}"))??; - Ok(()) - } - - fn classify_append_error(&self, error: &anyhow::Error) -> EventAppendErrorKind { - match error.downcast_ref::() { - Some(SidecarAppendError::Full | SidecarAppendError::Closed) => { - EventAppendErrorKind::Rejected - } - Some(SidecarAppendError::Write(_)) | None => EventAppendErrorKind::Unknown, - } - } -} - -pub struct ChronicleWriter { - sender: Option, - worker: Option>>, -} - -impl ChronicleWriter { - pub fn finish(mut self) -> anyhow::Result<()> { - self.sender.take(); - let Some(worker) = self.worker.take() else { - return Ok(()); - }; - worker - .join() - .map_err(|_| anyhow::anyhow!("pChronicle append worker panicked"))? - } -} - -pub async fn chronicle_sink( - storage: &Path, - default_agent_id: &str, - run_id: &str, - binary: &Path, - format: TrajectoryFormat, -) -> anyhow::Result { - let storage = storage.display().to_string(); - let control: Arc = - Arc::new(ChronicleServeProcessClient::spawn(binary, storage.clone()).await?); - let (tx, rx) = mpsc::sync_channel::(APPEND_QUEUE_CAPACITY); - let worker_control = Arc::clone(&control); - let worker = std::thread::Builder::new() - .name("pvisor-pchronicle-append".into()) - .spawn(move || { - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build()?; - for command in rx { - let outcome = runtime - .block_on(worker_control.append_trajectory(command.request)) - .map(|_| ()) - .map_err(|error| SidecarAppendError::Write(format!("{error:#}"))); - let _ = command.ack.send(outcome); - } - Ok(()) - })?; - let sender = SidecarAppendSender { - tx, - storage: storage.clone(), - format, - }; - - let trajectory_tx = sender.clone(); - let callback = CallbackSink::new( - default_agent_id, - move |route: &CaptureRoute, agent_id, record: EventRecord| { - trajectory_tx.append_durable( - agent_id.to_string(), - route.storage_session_id.clone(), - route.append_root_session(), - record, - ) - }, - ); - let lifecycle = ChronicleEventSink { - tx: sender.clone(), - run_id: run_id.to_string(), - agent_id: default_agent_id.to_string(), - }; - Ok(( - Arc::new(callback), - Arc::new(lifecycle), - ChronicleWriter { - sender: Some(sender), - worker: Some(worker), - }, - control, - )) -} - -#[cfg(test)] -mod tests { - use super::*; - use persisting_events::EventIdentity; - - #[test] - fn jsonl_writer_preserves_complete_http_payload() { - let dir = tempfile::tempdir().unwrap(); - let writer = JsonlWriter::open(dir.path()).unwrap(); - let event = EventRecord { - identity: EventIdentity::default(), - seq: 0, - source: "gateway".into(), - kind: "llm.request".into(), - timestamp: None, - session_id: Some("session".into()), - agent_id: Some("agent".into()), - parent_uuid: None, - trace_id: None, - call_id: None, - subagent_id: None, - parent_agent_id: None, - branch: None, - parent_call_id: None, - payload: serde_json::json!({ - "http": { - "method": "POST", - "request_body": {"messages": [{"role": "user", "content": "full"}]}, - "response_body": {"choices": [{"message": {"content": "reply"}}]} - } - }), - }; - writer.append(&event).unwrap(); - writer.finish().unwrap(); - let line = std::fs::read_to_string(dir.path().join("events.jsonl")).unwrap(); - let decoded: EventRecord = serde_json::from_str(line.trim()).unwrap(); - assert_eq!( - decoded.payload["http"]["request_body"]["messages"][0]["content"], - "full" - ); - assert_eq!( - decoded.payload["http"]["response_body"]["choices"][0]["message"]["content"], - "reply" - ); - } -} diff --git a/crates/persisting-pvisor/src/config.rs b/crates/persisting-pvisor/src/config.rs deleted file mode 100644 index d376918f4..000000000 --- a/crates/persisting-pvisor/src/config.rs +++ /dev/null @@ -1,627 +0,0 @@ -//! Canonical pVisor Run configuration. -//! -//! TOML and the `pvisor run` command line both resolve into [`RunConfig`]. -//! Runtime drivers only consume the resolved value and do not read config -//! files themselves. - -use std::path::{Path, PathBuf}; - -use persisting_agentctl::ResourceLimits; -use persisting_gateway::config::{CaptureLevel, ModelRoute, ProxyConfig}; -use persisting_overlaynet::{NetworkAccessRule, NetworkBandwidthLimit}; -use serde::{Deserialize, Serialize}; - -use crate::runtime::OverlayHint; - -#[derive(Debug, Clone, Default, Deserialize, Serialize)] -#[serde(default, deny_unknown_fields)] -pub struct RunConfig { - pub run: RunSettings, - pub container: ContainerSettings, - #[serde(alias = "kvm")] - pub vm: VmSettings, - /// Transactional filesystem configuration. Absence means host filesystem access. - pub overlayfs: Option, - pub overlaynet: OverlayNetSettings, - pub gateway: GatewaySettings, - /// Simplified durable recording selection. JSON is the lightweight local - /// pVisor format; Lance is delegated to the full pChronicle warehouse path. - pub record: RecordSettings, - pub chronicle: ChronicleSettings, -} - -impl RunConfig { - pub fn from_file(path: &Path) -> anyhow::Result { - let source = std::fs::read_to_string(path)?; - Ok(toml::from_str(&source)?) - } -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(default, deny_unknown_fields)] -pub struct RunSettings { - /// Internally resolved project association; not a user-facing configuration parameter. - #[serde(skip)] - pub workspace: Option, - pub agent: String, - pub executor: RunExecutorKind, - pub timeout_ms: Option, - pub stdio: RunStdio, - pub policy: RunPolicy, - /// Inherit the complete supervisor environment. Safe CLI runs override - /// this to false and project only baseline plus explicitly passed keys. - pub inherit_env: bool, - /// Host environment variables projected by name when `inherit_env=false`. - pub pass_env: Vec, - pub resource_limits: ResourceLimits, - pub command: Vec, -} - -impl Default for RunSettings { - fn default() -> Self { - Self { - workspace: None, - agent: "agent".into(), - executor: RunExecutorKind::Host, - timeout_ms: None, - stdio: RunStdio::Inherit, - policy: RunPolicy::Observe, - inherit_env: true, - pass_env: Vec::new(), - resource_limits: ResourceLimits::default(), - command: Vec::new(), - } - } -} - -#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq, clap::ValueEnum)] -#[serde(rename_all = "kebab-case")] -pub enum RunExecutorKind { - #[default] - Host, - Container, - #[serde(alias = "kvm")] - Vm, -} - -/// OCI CLI configuration used by [`crate::ContainerExecutor`]. -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] -#[serde(default, deny_unknown_fields)] -pub struct ContainerSettings { - /// Native OCI runtime executable (runc or crun). - pub runtime: PathBuf, - /// Image reference used for the Agent process. - pub image: String, - /// Prepared OCI rootfs directory. When omitted, `image` is prepared by pVisor. - pub rootfs: Option, - /// Target-specific pVisor injected into the container. Defaults to the - /// running executable; set it when the guest ABI differs from the host. - pub pvisor_binary: Option, - /// Explicit OCI platform. Required for packaged artifact auto-discovery - /// when the image platform cannot be inspected locally. - pub platform: Option, - /// Container network namespace mode. - pub network: ContainerNetwork, - /// Container-native working directory used when the Run has no mounted cwd. - pub workdir: Option, - /// Optional container user (`uid`, `uid:gid`, or a named user). - pub user: Option, - /// Mount the image root filesystem read-only. - pub read_only_rootfs: bool, - /// Additional explicit bind mounts. The runtime automatically mounts the - /// injected pVisor, delegated control directory, final Run cwd, and capture - /// configuration when present. - pub mounts: Vec, -} - -impl Default for ContainerSettings { - fn default() -> Self { - Self { - runtime: PathBuf::from("crun"), - image: String::new(), - rootfs: None, - pvisor_binary: None, - platform: None, - network: ContainerNetwork::Host, - workdir: None, - user: None, - read_only_rootfs: false, - mounts: Vec::new(), - } - } -} - -#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -pub enum ContainerPlatform { - LinuxAmd64, - LinuxArm64, -} - -impl std::str::FromStr for ContainerPlatform { - type Err = String; - - fn from_str(value: &str) -> Result { - match value { - "linux/amd64" | "linux-amd64" | "amd64" | "x86_64" => Ok(Self::LinuxAmd64), - "linux/arm64" | "linux-arm64" | "arm64" | "aarch64" => Ok(Self::LinuxArm64), - _ => Err(format!( - "unsupported container platform `{value}`; expected linux/amd64 or linux/arm64" - )), - } - } -} - -/// libkrun process isolation over a pVisor-provided Linux rootfs OverlayFS. -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] -#[serde(default, deny_unknown_fields)] -pub struct VmSettings { - /// Linux root filesystem exported to the libkrun guest. - pub rootfs: Option, - /// OCI image used when no explicit rootfs is supplied. - pub image: Option, - /// Content-addressed OCI cache. The platform cache directory is used when omitted. - pub image_store: Option, - /// Reject apply operations that would mutate the configured rootfs lower. - pub rootfs_immutable: bool, - /// Optional directory containing libkrunfw. Packaged builds discover it - /// next to pVisor; source builds use a verified per-user download cache. - pub library_dir: Option, - pub memory_mib: u32, - pub cpus: u16, -} - -impl Default for VmSettings { - fn default() -> Self { - Self { - rootfs: None, - image: Some(crate::oci::DEFAULT_IMAGE.into()), - image_store: None, - rootfs_immutable: false, - library_dir: None, - memory_mib: 2048, - cpus: 2, - } - } -} - -#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq, clap::ValueEnum)] -#[serde(rename_all = "kebab-case")] -pub enum ContainerNetwork { - /// Share the runtime host network. This keeps an in-process Gateway and - /// OverlayNet proxy reachable at their injected loopback addresses. - #[default] - Host, - Bridge, - None, -} - -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] -#[serde(deny_unknown_fields)] -pub struct ContainerMount { - pub source: PathBuf, - pub target: PathBuf, - #[serde(default)] - pub read_only: bool, -} - -#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq, clap::ValueEnum)] -#[serde(rename_all = "kebab-case")] -pub enum RunStdio { - #[default] - Inherit, - Capture, -} - -#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq, clap::ValueEnum)] -#[serde(rename_all = "kebab-case")] -pub enum RunPolicy { - #[default] - Observe, - Enforce, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(default, deny_unknown_fields)] -pub struct OverlayFsSettings { - /// Optional host base layer and default apply destination (normally the workspace). - pub base: Option, - /// Absolute path where the staged overlay is exposed inside a libkrun guest. - #[serde(rename = "path")] - pub target: Option, - /// Host mount point used as the Agent-visible overlay view. - pub merged_dir: Option, - /// Read-only host layers, listed bottom-to-top as supplied on the CLI. - pub compose: Vec, - /// Durable writable stage root. Defaults to the generated per-Run storage directory. - pub stage: Option, - /// Aggregate byte budget for the whole staged filesystem. - pub stage_size_bytes: Option, - pub backend: OverlayFsBackend, - pub commit: OverlayFsCommit, -} - -impl Default for OverlayFsSettings { - fn default() -> Self { - Self { - base: None, - target: None, - merged_dir: None, - compose: Vec::new(), - stage: None, - stage_size_bytes: None, - backend: OverlayFsBackend::Directory, - commit: OverlayFsCommit::Manual, - } - } -} - -#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq, clap::ValueEnum)] -#[serde(rename_all = "kebab-case")] -pub enum OverlayFsBackend { - #[default] - Directory, - Jujutsu, -} - -#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq, clap::ValueEnum)] -#[serde(rename_all = "kebab-case")] -pub enum OverlayFsCommit { - #[default] - Manual, - Apply, - Drop, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(default, deny_unknown_fields)] -pub struct OverlayNetSettings { - pub mode: OverlayNetMode, - pub listen: String, - pub policy: OverlayNetPolicy, - pub allow: Vec, - /// Structured grants for port-, transport-, and address-scoped policy. - pub rules: Vec, - pub deny: Vec, - pub limits: Vec, -} - -impl Default for OverlayNetSettings { - fn default() -> Self { - Self { - mode: OverlayNetMode::Auto, - listen: "127.0.0.1:19081".into(), - policy: OverlayNetPolicy::Public, - allow: Vec::new(), - rules: Vec::new(), - deny: Vec::new(), - limits: Vec::new(), - } - } -} - -#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq, clap::ValueEnum)] -#[serde(rename_all = "kebab-case")] -pub enum OverlayNetMode { - #[default] - Auto, - Off, - Proxy, -} - -#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq, clap::ValueEnum)] -#[serde(rename_all = "kebab-case")] -pub enum OverlayNetPolicy { - #[default] - Public, - Deny, - Allowlist, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(default, deny_unknown_fields)] -pub struct GatewaySettings { - pub mode: GatewayMode, - pub admin_listen: String, - pub level: CaptureLevel, - pub session_header: String, - pub debug: bool, - pub stream_markdown: bool, - pub routes: Vec, -} - -impl Default for GatewaySettings { - fn default() -> Self { - Self { - mode: GatewayMode::Off, - admin_listen: "127.0.0.1:9876".into(), - level: CaptureLevel::Dialogue, - session_header: "x-persisting-session-id".into(), - debug: false, - stream_markdown: false, - routes: Vec::new(), - } - } -} - -#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq, clap::ValueEnum)] -#[serde(rename_all = "kebab-case")] -pub enum GatewayMode { - #[default] - Off, - Capture, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(default, deny_unknown_fields)] -pub struct ChronicleSettings { - pub mode: ChronicleMode, - pub dir: Option, - pub binary: PathBuf, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(default, deny_unknown_fields)] -pub struct RecordSettings { - pub format: RecordFormat, - /// Local directory/file for JSON, or a warehouse URI/directory for Lance. - pub destination: Option, -} - -impl Default for RecordSettings { - fn default() -> Self { - Self { - format: RecordFormat::Json, - destination: None, - } - } -} - -#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq, clap::ValueEnum)] -#[serde(rename_all = "kebab-case")] -pub enum RecordFormat { - /// Full EventRecord JSONL written directly by pVisor. - #[default] - #[serde(alias = "jsonl")] - #[value(alias = "jsonl")] - Json, - /// Full pChronicle warehouse path (canonical Lance storage). - Lance, -} - -impl Default for ChronicleSettings { - fn default() -> Self { - Self { - mode: ChronicleMode::Off, - dir: None, - binary: "pchronicle".into(), - } - } -} - -#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq, clap::ValueEnum)] -#[serde(rename_all = "kebab-case")] -pub enum ChronicleMode { - #[default] - Off, - /// Spawn a pChronicle sidecar that owns durable trajectory storage. - Spawn, - /// Compatibility spelling for the former embedded Lance mode. This now - /// has the same sidecar semantics as [`Self::Spawn`]. - Lance, -} - -/// Resolved configuration for the internal OverlayNet + optional Gateway sink. -#[derive(Debug, Clone)] -pub struct GatewayDriverConfig { - pub proxy: ProxyConfig, - pub output_dir: PathBuf, - pub stream_markdown: bool, - pub gateway_enabled: bool, -} - -/// Programmatic network-driver configuration. `Auto` selects smoltcp for a -/// libkrun VM and otherwise remains inactive unless Gateway/proxy is requested. -#[derive(Debug, Clone)] -pub struct NetworkDriverConfig { - pub mode: OverlayNetMode, - pub network: persisting_overlaynet::NetworkConfig, -} - -impl Default for NetworkDriverConfig { - fn default() -> Self { - Self { - mode: OverlayNetMode::Auto, - network: persisting_overlaynet::NetworkConfig::default(), - } - } -} - -impl NetworkDriverConfig { - pub fn new(mode: OverlayNetMode, network: persisting_overlaynet::NetworkConfig) -> Self { - Self { mode, network } - } -} - -impl GatewayDriverConfig { - pub fn new(proxy: ProxyConfig) -> Self { - Self { - proxy, - output_dir: PathBuf::from(".persisting/run"), - stream_markdown: false, - gateway_enabled: true, - } - } - - pub fn output_dir(mut self, output_dir: impl Into) -> Self { - self.output_dir = output_dir.into(); - self - } - - pub fn stream_markdown(mut self, enabled: bool) -> Self { - self.stream_markdown = enabled; - self - } - - pub fn gateway_enabled(mut self, enabled: bool) -> Self { - self.gateway_enabled = enabled; - self - } -} - -/// Programmatic pVisor driver assembly configuration. -#[derive(Debug, Clone, Default)] -pub struct PVisorConfig { - pub gateway: Option, - pub network: NetworkDriverConfig, - pub overlay: OverlayHint, -} - -impl PVisorConfig { - pub fn with_gateway(mut self, gateway: GatewayDriverConfig) -> Self { - self.gateway = Some(gateway); - self - } - - pub fn with_overlay(mut self, overlay: OverlayHint) -> Self { - self.overlay = overlay; - self - } - - pub fn with_network(mut self, network: NetworkDriverConfig) -> Self { - self.network = network; - self - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn run_config_toml_roundtrip() { - let config: RunConfig = toml::from_str( - r#" -[run] -executor = "container" -command = ["codex"] - -[container] -runtime = "podman" -image = "example/agent:latest" -network = "none" - -[overlayfs] -path = "/workspace" -compose = ["/tmp/lower"] - -[overlaynet] -mode = "proxy" -policy = "allowlist" - -[[overlaynet.rules]] -host = "api.openai.com" -ports = [443] -transports = ["tcp_tunnel"] - -[[overlaynet.deny]] -host = "169.254.0.0/16" - -[[overlaynet.limits]] -bytes_per_second = 1250000 - -[gateway] -mode = "capture" - -[record] -format = "json" -destination = "/tmp/events" - -[[gateway.routes]] -name = "openai" -upstream = "https://api.openai.com/v1" -"#, - ) - .unwrap(); - assert_eq!( - config - .overlayfs - .as_ref() - .and_then(|overlay| overlay.target.as_deref()), - Some(Path::new("/workspace")) - ); - assert_eq!( - config.overlayfs.as_ref().unwrap().compose, - [PathBuf::from("/tmp/lower")] - ); - assert_eq!(config.run.executor, RunExecutorKind::Container); - assert_eq!(config.container.runtime, Path::new("podman")); - assert_eq!(config.container.image, "example/agent:latest"); - assert_eq!(config.container.network, ContainerNetwork::None); - assert_eq!(config.overlaynet.mode, OverlayNetMode::Proxy); - assert_eq!(config.overlaynet.rules.len(), 1); - assert_eq!(config.overlaynet.rules[0].ports, [443]); - assert_eq!(config.overlaynet.deny.len(), 1); - assert_eq!(config.overlaynet.limits[0].bytes_per_second, 1_250_000); - assert_eq!(config.gateway.routes.len(), 1); - assert_eq!(config.record.format, RecordFormat::Json); - assert_eq!( - config.record.destination.as_deref(), - Some(Path::new("/tmp/events")) - ); - assert_eq!(config.run.command, ["codex"]); - } - - #[test] - fn vm_config_toml_roundtrip() { - let config: RunConfig = toml::from_str( - r#" -[run] -executor = "vm" -command = ["agent"] - -[vm] -rootfs = "/opt/rootfs" -library_dir = "/opt/libkrun/lib" -memory_mib = 4096 -cpus = 4 -"#, - ) - .unwrap(); - assert_eq!(config.run.executor, RunExecutorKind::Vm); - assert_eq!(config.vm.rootfs.as_deref(), Some(Path::new("/opt/rootfs"))); - assert_eq!( - config.vm.library_dir.as_deref(), - Some(Path::new("/opt/libkrun/lib")) - ); - assert_eq!(config.vm.memory_mib, 4096); - assert_eq!(config.vm.cpus, 4); - let encoded = toml::to_string_pretty(&config).unwrap(); - let decoded: RunConfig = toml::from_str(&encoded).unwrap(); - assert_eq!(decoded.vm, config.vm); - } - - #[test] - fn overlaynet_defaults_to_auto_for_executor_specific_selection() { - let config = RunConfig::default(); - assert_eq!(config.overlaynet.mode, OverlayNetMode::Auto); - assert_eq!(PVisorConfig::default().network.mode, OverlayNetMode::Auto); - } - - #[test] - fn legacy_kvm_config_deserializes_as_vm() { - let config: RunConfig = toml::from_str( - r#" -[run] -executor = "kvm" -command = ["agent"] - -[kvm] -rootfs = "/opt/rootfs" -"#, - ) - .unwrap(); - assert_eq!(config.run.executor, RunExecutorKind::Vm); - assert_eq!(config.vm.rootfs.as_deref(), Some(Path::new("/opt/rootfs"))); - } -} diff --git a/crates/persisting-pvisor/src/container.rs b/crates/persisting-pvisor/src/container.rs deleted file mode 100644 index af0b75181..000000000 --- a/crates/persisting-pvisor/src/container.rs +++ /dev/null @@ -1,799 +0,0 @@ -//! Native OCI runtime transport. pVisor materializes an OCI bundle and invokes -//! runc/crun; no Docker or Podman daemon is required. - -use crate::artifact::resolve_pvisor_binary; -use crate::config::{ContainerMount, ContainerPlatform, ContainerSettings}; -use crate::delegated::{DelegatedRunFiles, RESULT_FILENAME, SPEC_FILENAME}; -use crate::executor::{AttemptContext, RunExecutor}; -use async_trait::async_trait; -use persisting_agentctl::{ - ExecutorDescriptor, ExecutorKind, IsolationKind, ProcessOutput, RunFailure, RunFailureKind, - RunInvocation, RunResult, RunSpec, RunState, StdioMode, -}; -use std::collections::BTreeMap; -use std::fs; -use std::path::{Path, PathBuf}; -use std::process::Stdio; -use std::time::Duration; -use tokio::io::{AsyncRead, AsyncReadExt}; -use tokio::process::{Child, Command}; - -const CAPTURE_CONFIG_ENV: &str = "PERSISTING_CAPTURE_CONFIG"; -const GUEST_PVISOR: &str = "/opt/persisting/pvisor"; -const GUEST_CONTROL_DIR: &str = "/run/persisting"; - -#[derive(Debug, Clone)] -pub struct ContainerExecutor { - settings: ContainerSettings, -} - -#[derive(Debug)] -struct Captured { - text: String, - truncated: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct BindMount { - source: PathBuf, - target: PathBuf, - read_only: bool, -} - -impl ContainerExecutor { - pub fn new(settings: ContainerSettings) -> anyhow::Result { - anyhow::ensure!( - !settings.runtime.as_os_str().is_empty(), - "container runtime must not be empty" - ); - anyhow::ensure!( - !settings.image.trim().is_empty() || settings.rootfs.is_some(), - "container requires an image or an explicit rootfs" - ); - anyhow::ensure!( - settings.network != crate::config::ContainerNetwork::Bridge, - "container.network=bridge requires CNI and is not supported by native OCI runner; use host or none" - ); - if let Some(workdir) = &settings.workdir { - anyhow::ensure!( - workdir.is_absolute(), - "container workdir must be absolute: {}", - workdir.display() - ); - } - for mount in &settings.mounts { - validate_mount(mount)?; - } - Ok(Self { settings }) - } - - pub fn settings(&self) -> &ContainerSettings { - &self.settings - } - - fn build_command( - &self, - spec: &RunSpec, - attempt_id: &str, - _platform: ContainerPlatform, - pvisor_binary: &Path, - files: &DelegatedRunFiles, - ) -> anyhow::Result { - let RunInvocation::Process(invocation) = &spec.invocation; - let run_id = spec.run_id.as_str(); - let limits = &spec.runtime.resource_limits; - let mut mounts = BTreeMap::::new(); - for mount in &self.settings.mounts { - add_mount( - &mut mounts, - bind_mount(&mount.source, &mount.target, mount.read_only)?, - )?; - } - add_mount( - &mut mounts, - bind_mount(pvisor_binary, Path::new(GUEST_PVISOR), true)?, - )?; - let control_dir = files - .spec_path - .parent() - .ok_or_else(|| anyhow::anyhow!("delegated RunSpec has no parent directory"))?; - add_mount( - &mut mounts, - bind_mount(control_dir, Path::new(GUEST_CONTROL_DIR), false)?, - )?; - - let workdir = invocation - .cwd - .as_deref() - .map(PathBuf::from) - .or_else(|| self.settings.workdir.clone()); - if let Some(path) = invocation.cwd.as_deref().map(Path::new) { - if path.exists() { - let target = absolute_container_path(path)?; - add_mount(&mut mounts, bind_mount(path, &target, false)?)?; - } else { - anyhow::ensure!( - path.is_absolute(), - "container-native cwd must be absolute when it is not a host path: {}", - path.display() - ); - } - } - if let Some(value) = invocation.env.get(CAPTURE_CONFIG_ENV) { - let path = Path::new(value); - if path.is_absolute() && path.exists() { - add_mount(&mut mounts, bind_mount(path, path, true)?)?; - } - } - - let control_dir = files.spec_path.parent().unwrap(); - let bundle = control_dir.join(format!("oci-bundle-{}", attempt_id)); - fs::create_dir_all(&bundle)?; - let configured_rootfs = self - .settings - .rootfs - .clone() - .unwrap_or_else(|| PathBuf::from("/")); - let rootfs = if configured_rootfs == Path::new("/") { - // A host-root container gets a private synthetic root directory. - // Standard host directories are mounted read-only into it, so OCI - // mountpoint creation never mutates the real host `/`. - let synthetic = bundle.join("rootfs"); - fs::create_dir_all(&synthetic)?; - fs::create_dir_all(synthetic.join("tmp"))?; - fs::create_dir_all(synthetic.join("dev"))?; - fs::create_dir_all(synthetic.join("proc"))?; - fs::create_dir_all(synthetic.join("sys"))?; - for path in ["bin", "usr", "lib", "lib64", "sbin", "etc", "var"] { - let source = PathBuf::from(format!("/{path}")); - if source.is_dir() { - add_mount( - &mut mounts, - bind_mount(&source, Path::new(&format!("/{path}")), true)?, - )?; - } - } - synthetic - } else { - anyhow::ensure!( - configured_rootfs.is_dir(), - "OCI rootfs does not exist: {}", - configured_rootfs.display() - ); - configured_rootfs - }; - let _ = fs::create_dir_all(rootfs.join("opt/persisting")); - let _ = fs::create_dir_all(rootfs.join("run/persisting")); - // A read-only image still needs a standard writable scratch location - // for the injected pVisor and AgentCtl setup. - let _ = fs::create_dir_all(rootfs.join("tmp")); - for mount in mounts.values() { - if let Ok(relative) = mount.target.strip_prefix("/") { - let target = rootfs.join(relative); - if mount.source.is_dir() { - let _ = fs::create_dir_all(target); - } else if let Some(parent) = target.parent() { - let _ = fs::create_dir_all(parent); - let _ = fs::File::create(target); - } - } - } - let config = bundle.join("config.json"); - let mut namespaces = vec![ - serde_json::json!({"type":"pid"}), - serde_json::json!({"type":"ipc"}), - serde_json::json!({"type":"uts"}), - serde_json::json!({"type":"mount"}), - ]; - if self.settings.network != crate::config::ContainerNetwork::Host { - namespaces.push(serde_json::json!({"type":"network"})); - } - let mut mounts_json = Vec::new(); - mounts_json.push(serde_json::json!({"destination":"/dev","type":"tmpfs","source":"tmpfs","options":["nosuid","noexec","nodev","mode=755"]})); - mounts_json.push(serde_json::json!({"destination":"/dev/shm","type":"tmpfs","source":"shm","options":["nosuid","noexec","nodev"]})); - mounts_json.push(serde_json::json!({"destination":"/proc","type":"proc","source":"proc","options":["nosuid","noexec","nodev"]})); - mounts_json.push(serde_json::json!({"destination":"/tmp","type":"tmpfs","source":"tmpfs","options":["nosuid","nodev","mode=1777"]})); - // Bind mounts follow the standard pseudo-filesystem mounts. - for m in mounts.values() { - mounts_json.push(serde_json::json!({"destination":m.target,"type":"bind","source":m.source,"options":if m.read_only { vec!["rbind","ro"] } else { vec!["rbind","rw"] }})); - } - let requested_user = parse_user(self.settings.user.as_deref())?; - let host_uid = unsafe { libc::geteuid() }; - let host_gid = unsafe { libc::getegid() }; - // Without subordinate ID ranges, rootless runtimes can only map the - // caller's identity. Keep the container runnable (best effort) by - // falling back to container root for an explicitly requested user. - // pVisor currently uses a single-identity rootless mapping. A - // non-root container user would require configured subordinate ID - // ranges and cannot be represented safely otherwise. - let process_user = if requested_user != (0, 0) { - eprintln!( - "pVisor container: subordinate UID/GID mapping is unavailable; running as container root" - ); - (0, 0) - } else { - requested_user - }; - namespaces.insert(0, serde_json::json!({"type":"user"})); - let resources = serde_json::json!({"memory": limits.memory_bytes.map(|v| serde_json::json!({"limit":v})), "pids": limits.processes.map(|v| serde_json::json!({"limit":v}))}); - let mut env_json = Vec::new(); - for (key, value) in &invocation.env { - env_json.push(format!("{key}={value}")); - } - if invocation.inherit_env { - for (key, value) in std::env::vars() { - if valid_env_name(&key) && !invocation.env.contains_key(&key) { - env_json.push(format!("{key}={value}")); - } - } - } - let devices = [ - ("/dev/null", 1, 3), ("/dev/zero", 1, 5), ("/dev/random", 1, 8), - ("/dev/urandom", 1, 9), ("/dev/tty", 5, 0), - ].into_iter().map(|(path, major, minor)| serde_json::json!({"path":path,"type":"c","major":major,"minor":minor,"fileMode":438,"uid":0,"gid":0})).collect::>(); - // Rootless runtimes require a mapping for UID/GID 0 whenever a user - // namespace is enabled (even when the requested process user is not - // root). Map the caller's host identity to container root, and add a - // separate mapping for an explicitly requested non-root user. - // A single contiguous range avoids duplicate host IDs (which Linux - // rejects when writing uid_map/gid_map) while covering arbitrary - // explicit container users such as 1000:1000. - let mapping_size = 1u32; - let uid_mappings = - vec![serde_json::json!({"containerID":0,"hostID":host_uid,"size":mapping_size})]; - let gid_mappings = - vec![serde_json::json!({"containerID":0,"hostID":host_gid,"size":mapping_size})]; - let cfg = serde_json::json!({"ociVersion":"1.0.2","process":{"terminal":false,"cwd":workdir.as_deref().unwrap_or(Path::new("/")),"args":[GUEST_PVISOR,"run","--executor","host","--stdio","capture","--spec",format!("{GUEST_CONTROL_DIR}/{SPEC_FILENAME}"),"--result-file",format!("{GUEST_CONTROL_DIR}/{RESULT_FILENAME}" )],"env":env_json,"user":{"uid":process_user.0,"gid":process_user.1}},"root":{"path":rootfs,"readonly":self.settings.read_only_rootfs},"mounts":mounts_json,"linux":{"namespaces":namespaces,"resources":resources,"devices":devices,"uidMappings":uid_mappings,"gidMappings":gid_mappings},"annotations":{"io.persisting.run_id":run_id,"io.persisting.attempt_id":attempt_id}}); - fs::write(&config, serde_json::to_vec_pretty(&cfg)?)?; - let state = control_dir.join("oci-state"); - fs::create_dir_all(&state)?; - let mut command = Command::new(&self.settings.runtime); - command - .arg("--root") - .arg(state) - .arg("run") - .arg("--bundle") - .arg(bundle) - .arg(container_name(run_id, attempt_id)); - - command - .stdin(stdio(invocation.stdin)) - .stdout(stdio(invocation.stdout)) - .stderr(stdio(invocation.stderr)) - .kill_on_drop(true); - Ok(command) - } - - async fn terminate( - &self, - child: &mut Child, - container_name: &str, - grace_ms: u64, - ) -> Option { - let stop = tokio::time::timeout( - Duration::from_millis(grace_ms.saturating_add(2_000)), - Command::new(&self.settings.runtime) - .arg("kill") - .arg(container_name) - .arg("TERM") - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(), - ) - .await; - if tokio::time::timeout(Duration::from_millis(2_000), child.wait()) - .await - .is_ok() - { - return match stop { - Ok(Ok(status)) if status.success() => None, - Ok(Ok(_)) => Some("container stop reported failure".into()), - Ok(Err(error)) => Some(format!("failed to execute container stop: {error}")), - Err(_) => Some("container stop timed out".into()), - }; - } - let kill = Command::new(&self.settings.runtime) - .arg("kill") - .arg(container_name) - .arg("KILL") - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .await; - let _ = child.kill().await; - let _ = child.wait().await; - let _ = Command::new(&self.settings.runtime) - .arg("delete") - .arg("--force") - .arg(container_name) - .status() - .await; - match kill { - Ok(status) if status.success() => None, - Ok(_) => Some("container runtime could not kill the delegated pVisor".into()), - Err(error) => Some(format!("failed to execute container kill: {error}")), - } - } -} - -#[async_trait] -impl RunExecutor for ContainerExecutor { - fn descriptor(&self) -> ExecutorDescriptor { - ExecutorDescriptor { - name: "oci-pvisor".into(), - kind: ExecutorKind::Container, - isolation: IsolationKind::Container, - capability_enforcement: Default::default(), - supports_checkpoint: false, - supports_migration: false, - } - } - - fn supports(&self, invocation: &RunInvocation) -> bool { - matches!(invocation, RunInvocation::Process(_)) - } - - async fn execute(&self, context: AttemptContext) -> RunResult { - let spec = context.spec().clone(); - let started_at = crate::util::unix_now_ms(); - context - .transition( - RunState::Starting, - Some("injecting pVisor into OCI container".into()), - ) - .await; - - let prepared = async { - let platform = self - .settings - .platform - .unwrap_or(ContainerPlatform::LinuxAmd64); - let binary = resolve_pvisor_binary(self.settings.pvisor_binary.as_deref())?; - let files = DelegatedRunFiles::new_with_stdio(&spec, true)?; - let mut executor = self.clone(); - if executor.settings.rootfs.is_none() { - anyhow::ensure!( - !executor.settings.image.trim().is_empty(), - "container requires --container-rootfs or --container-image" - ); - let image = executor.settings.image.clone(); - let prepared = tokio::task::spawn_blocking(move || { - crate::oci::ImageStore::new(None)?.prepare(&image) - }) - .await??; - executor.settings.rootfs = Some(prepared.rootfs); - } - let command = executor.build_command( - &spec, - context.attempt_id().as_str(), - platform, - &binary, - &files, - )?; - Ok::<_, anyhow::Error>((files, command)) - } - .await; - let (files, mut command) = match prepared { - Ok(prepared) => prepared, - Err(error) => { - return failed_to_start(&spec, context.attempt_id(), started_at, error.to_string()); - } - }; - let name = container_name(spec.run_id.as_str(), context.attempt_id().as_str()); - let mut child = match command.spawn() { - Ok(child) => child, - Err(error) => { - return failed_to_start(&spec, context.attempt_id(), started_at, error.to_string()); - } - }; - let stdout_task = child.stdout.take().map(|stdout| { - let limit = spec.runtime.max_output_bytes; - tokio::spawn(async move { read_limited(stdout, limit).await }) - }); - let stderr_task = child.stderr.take().map(|stderr| { - let limit = spec.runtime.max_output_bytes; - tokio::spawn(async move { read_limited(stderr, limit).await }) - }); - context.transition(RunState::Running, None).await; - - enum End { - Exited(std::io::Result), - Cancelled, - Watchdog, - } - let cancellation = context.cancellation(); - let watchdog_ms = spec.runtime.timeout_ms.map(|timeout| { - timeout - .saturating_add(spec.runtime.termination_grace_ms) - .saturating_add(10_000) - }); - let end = if let Some(watchdog_ms) = watchdog_ms { - tokio::select! { - biased; - status = child.wait() => End::Exited(status), - _ = cancellation.cancelled() => End::Cancelled, - _ = tokio::time::sleep(Duration::from_millis(watchdog_ms)) => End::Watchdog, - } - } else { - tokio::select! { - biased; - status = child.wait() => End::Exited(status), - _ = cancellation.cancelled() => End::Cancelled, - } - }; - - let mut warnings = Vec::new(); - if matches!(end, End::Cancelled | End::Watchdog) { - if matches!(end, End::Cancelled) { - context - .transition(RunState::Cancelling, Some("cancellation requested".into())) - .await; - } - if let Some(warning) = self - .terminate(&mut child, &name, spec.runtime.termination_grace_ms) - .await - { - warnings.push(warning); - } - } - - let transport_stdout = join_capture(stdout_task).await; - let transport_stderr = join_capture(stderr_task).await; - if matches!(end, End::Exited(_)) && files.result_path.is_file() { - match files.read_result(&spec.run_id, context.attempt_id(), spec.lease_epoch) { - Ok(mut output) => { - context.import_delegated_agentctl(output.agentctl); - output.result.warnings.extend(warnings); - return output.result; - } - Err(error) => warnings.push(format!("decode delegated pVisor result: {error}")), - } - } - - let mut output = ProcessOutput::default(); - if let Some(captured) = transport_stdout { - output.stdout = Some(captured.text); - output.stdout_truncated = captured.truncated; - } - if let Some(captured) = transport_stderr { - output.stderr = Some(captured.text); - output.stderr_truncated = captured.truncated; - } - let (state, exit_code, failure) = match end { - End::Cancelled => (RunState::Cancelled, None, None), - End::Watchdog => ( - RunState::Failed, - None, - Some(RunFailure { - kind: RunFailureKind::DeadlineExceeded, - message: "delegated pVisor did not finish before the transport watchdog".into(), - retryable: false, - }), - ), - End::Exited(Ok(status)) if status.code() == Some(125) => ( - RunState::Failed, - status.code(), - Some(RunFailure { - kind: RunFailureKind::Infrastructure, - message: "container runtime failed before injected pVisor started".into(), - retryable: false, - }), - ), - End::Exited(Ok(status)) => ( - RunState::Failed, - status.code(), - Some(RunFailure { - kind: RunFailureKind::Infrastructure, - message: "injected pVisor exited without a valid RunResult".into(), - retryable: false, - }), - ), - End::Exited(Err(error)) => ( - RunState::Failed, - None, - Some(RunFailure { - kind: RunFailureKind::Infrastructure, - message: error.to_string(), - retryable: true, - }), - ), - }; - RunResult { - run_id: spec.run_id, - attempt_id: context.attempt_id().clone(), - lease_epoch: spec.lease_epoch, - state, - started_at_unix_ms: started_at, - finished_at_unix_ms: crate::util::unix_now_ms(), - exit_code, - failure, - output, - value: None, - metrics: Default::default(), - artifacts: Vec::new(), - event_stream_ref: None, - warnings, - } - } -} - -fn failed_to_start( - spec: &persisting_agentctl::RunSpec, - attempt_id: &persisting_agentctl::AttemptId, - started_at: u64, - message: String, -) -> RunResult { - RunResult { - run_id: spec.run_id.clone(), - attempt_id: attempt_id.clone(), - lease_epoch: spec.lease_epoch, - state: RunState::Failed, - started_at_unix_ms: started_at, - finished_at_unix_ms: crate::util::unix_now_ms(), - exit_code: None, - failure: Some(RunFailure { - kind: RunFailureKind::Spawn, - message, - retryable: false, - }), - output: ProcessOutput::default(), - value: None, - metrics: Default::default(), - artifacts: Vec::new(), - event_stream_ref: None, - warnings: Vec::new(), - } -} - -fn validate_mount(mount: &ContainerMount) -> anyhow::Result<()> { - anyhow::ensure!( - !mount.source.as_os_str().is_empty(), - "container mount source must not be empty" - ); - anyhow::ensure!( - mount.target.is_absolute(), - "container mount target must be absolute: {}", - mount.target.display() - ); - validate_mount_path(&mount.target) -} - -fn bind_mount(source: &Path, target: &Path, read_only: bool) -> anyhow::Result { - let source = source.canonicalize().map_err(|error| { - anyhow::anyhow!("resolve container mount {}: {error}", source.display()) - })?; - anyhow::ensure!( - target.is_absolute(), - "container mount target must be absolute" - ); - validate_mount_path(&source)?; - validate_mount_path(target)?; - Ok(BindMount { - source, - target: target.to_path_buf(), - read_only, - }) -} - -fn add_mount(mounts: &mut BTreeMap, mount: BindMount) -> anyhow::Result<()> { - if let Some(existing) = mounts.get(&mount.target) { - anyhow::ensure!( - existing == &mount, - "conflicting container mounts for {}", - mount.target.display() - ); - return Ok(()); - } - mounts.insert(mount.target.clone(), mount); - Ok(()) -} - -fn validate_mount_path(path: &Path) -> anyhow::Result<()> { - let value = path - .to_str() - .ok_or_else(|| anyhow::anyhow!("container mount path is not UTF-8"))?; - anyhow::ensure!( - !value.contains([',', '\n', '\r']), - "container mount path contains an unsupported delimiter: {}", - path.display() - ); - Ok(()) -} - -fn absolute_container_path(path: &Path) -> anyhow::Result { - if path.is_absolute() { - return Ok(path.to_path_buf()); - } - Ok(std::env::current_dir()?.join(path)) -} - -fn valid_env_name(key: &str) -> bool { - !key.is_empty() && !key.contains(['=', '\0']) -} - -fn parse_user(value: Option<&str>) -> anyhow::Result<(u32, u32)> { - let Some(value) = value else { - return Ok((0, 0)); - }; - let mut parts = value.split(':'); - let uid: u32 = parts - .next() - .unwrap_or("0") - .parse() - .map_err(|_| anyhow::anyhow!("container user must be uid[:gid]"))?; - let gid: u32 = parts - .next() - .unwrap_or("0") - .parse() - .map_err(|_| anyhow::anyhow!("container user must be uid[:gid]"))?; - anyhow::ensure!(parts.next().is_none(), "container user must be uid[:gid]"); - Ok((uid, gid)) -} - -fn container_name(run_id: &str, attempt_id: &str) -> String { - fn clean(value: &str) -> String { - value - .chars() - .map(|ch| { - if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.') { - ch - } else { - '-' - } - }) - .collect() - } - let run = clean(run_id).chars().take(32).collect::(); - let attempt = clean(attempt_id); - let suffix = attempt - .chars() - .rev() - .take(24) - .collect::() - .chars() - .rev() - .collect::(); - format!("pvisor-{run}-{suffix}") -} - -fn stdio(mode: StdioMode) -> Stdio { - match mode { - StdioMode::Inherit => Stdio::inherit(), - StdioMode::Capture => Stdio::piped(), - StdioMode::Null => Stdio::null(), - } -} - -async fn read_limited( - mut reader: R, - limit: usize, -) -> std::io::Result { - let mut retained = Vec::with_capacity(limit.min(8192)); - let mut buffer = [0_u8; 8192]; - let mut truncated = false; - loop { - let read = reader.read(&mut buffer).await?; - if read == 0 { - break; - } - let keep = limit.saturating_sub(retained.len()).min(read); - retained.extend_from_slice(&buffer[..keep]); - truncated |= keep < read; - } - Ok(Captured { - text: String::from_utf8_lossy(&retained).into_owned(), - truncated, - }) -} - -async fn join_capture( - task: Option>>, -) -> Option { - match task { - Some(task) => task.await.ok().and_then(Result::ok), - None => None, - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::config::{ContainerNetwork, ContainerPlatform}; - use persisting_agentctl::ResourceLimits; - use std::ffi::OsStr; - #[cfg(unix)] - use std::os::unix::fs::PermissionsExt; - - #[cfg(unix)] - fn executable(path: &Path) { - std::fs::write(path, b"runtime").unwrap(); - std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).unwrap(); - } - - #[cfg(unix)] - #[tokio::test] - async fn command_injects_pvisor_and_never_executes_agent_directly() { - let temporary = tempfile::tempdir().unwrap(); - let runtime = temporary.path().join("pvisor"); - executable(&runtime); - let cwd = temporary.path().join("workspace"); - std::fs::create_dir(&cwd).unwrap(); - let executor = ContainerExecutor::new(ContainerSettings { - image: "example/agent:latest".into(), - pvisor_binary: Some(runtime.clone()), - platform: Some(ContainerPlatform::LinuxAmd64), - network: ContainerNetwork::None, - ..ContainerSettings::default() - }) - .unwrap(); - let mut spec = persisting_agentctl::RunSpec::process("run-one", "agent", "secret-agent"); - spec.runtime.resource_limits = ResourceLimits { - memory_bytes: Some(1_048_576), - processes: Some(8), - open_files: Some(32), - ..ResourceLimits::default() - }; - let RunInvocation::Process(invocation) = &mut spec.invocation; - invocation.cwd = Some(cwd.display().to_string()); - invocation.inherit_env = false; - let files = DelegatedRunFiles::new(&spec).unwrap(); - let command = executor - .build_command( - &spec, - "attempt-one", - ContainerPlatform::LinuxAmd64, - &runtime, - &files, - ) - .unwrap(); - let args = command - .as_std() - .get_args() - .map(|arg| arg.to_string_lossy().into_owned()) - .collect::>(); - assert!( - args.iter() - .any(|arg| arg.contains("oci-bundle-attempt-one")) - ); - assert!(!args.iter().any(|arg| arg == "secret-agent")); - } - - #[test] - fn descriptor_reports_container_without_overclaiming_enforcement() { - let executor = ContainerExecutor::new(ContainerSettings { - image: "example/agent:latest".into(), - ..ContainerSettings::default() - }) - .unwrap(); - let descriptor = executor.descriptor(); - assert_eq!(descriptor.name, "oci-pvisor"); - assert_eq!(descriptor.kind, ExecutorKind::Container); - assert_eq!(descriptor.isolation, IsolationKind::Container); - assert!(descriptor.capability_enforcement.dimensions.is_empty()); - } - - #[test] - fn accepts_numeric_container_user() { - assert!( - ContainerExecutor::new(ContainerSettings { - image: "agent".into(), - user: Some("1000".into()), - ..ContainerSettings::default() - }) - .is_ok() - ); - } - - #[test] - fn runtime_name_is_path_safe_and_retains_attempt_entropy() { - let name = container_name("run/unsafe", "attempt:1234567890"); - assert!(!name.contains('/')); - assert!(!name.contains(':')); - assert!(name.ends_with("1234567890")); - assert_ne!( - container_name("run", "attempt-one"), - container_name("run", "attempt-two") - ); - assert_ne!(OsStr::new(&name), OsStr::new("")); - } -} diff --git a/crates/persisting-pvisor/src/control.rs b/crates/persisting-pvisor/src/control.rs deleted file mode 100644 index 8d95a9fe9..000000000 --- a/crates/persisting-pvisor/src/control.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Re-export the shared runtime control protocol. - -pub use persisting_agentctl::{ - ControlController, ControlEffect, ControlMachine, ControlReason, ControlRequest, ControlState, - ControlTransition, NetworkGuard, NetworkHostRule, NetworkRule, PolicyControlController, - host_matches, is_public_egress_ip, normalize_host, parse_network_rule, -}; diff --git a/crates/persisting-pvisor/src/delegated.rs b/crates/persisting-pvisor/src/delegated.rs deleted file mode 100644 index 21dd97807..000000000 --- a/crates/persisting-pvisor/src/delegated.rs +++ /dev/null @@ -1,123 +0,0 @@ -//! Files and result hand-off for a pVisor delegated through Docker or KVM. - -use persisting_agentctl::{AttemptId, RunInvocation, RunResult, RunSpec}; -use std::path::{Path, PathBuf}; - -pub(crate) const SPEC_FILENAME: &str = "run-spec.json"; -pub(crate) const RESULT_FILENAME: &str = "run-result.json"; - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -pub(crate) struct DelegatedRunOutput { - pub(crate) result: RunResult, - #[serde(alias = "agentctl")] - pub(crate) agentctl: crate::AgentCtlSnapshot, -} - -pub(crate) struct DelegatedRunFiles { - _temporary: tempfile::TempDir, - pub(crate) spec_path: PathBuf, - pub(crate) result_path: PathBuf, -} - -impl DelegatedRunFiles { - #[cfg(test)] - pub(crate) fn new(spec: &RunSpec) -> anyhow::Result { - Self::new_with_stdio(spec, false) - } - - /// Create delegated files while forcing the injected pVisor to use pipes. - /// The outer transport owns the real terminal; inheriting it in the nested - /// process makes rootless OCI runs attempt tty process-group operations. - pub(crate) fn new_with_stdio(spec: &RunSpec, capture: bool) -> anyhow::Result { - let temporary = tempfile::Builder::new() - .prefix("pvisor-delegated-") - .tempdir()?; - let spec_path = temporary.path().join(SPEC_FILENAME); - let result_path = temporary.path().join(RESULT_FILENAME); - let mut delegated = spec.clone(); - delegated.metadata.remove("pvisor.executor"); - let RunInvocation::Process(process) = &mut delegated.invocation; - process.env.retain(|key, _| { - !key.starts_with("PERSISTING_AGENTCTL_") && !key.starts_with("PERSISTING_AGENTCTL_") - }); - if capture { - // pVisor v1 does not support captured stdin. Null stdin also - // prevents the nested host executor from attempting tty control. - process.stdin = persisting_agentctl::StdioMode::Null; - process.stdout = persisting_agentctl::StdioMode::Capture; - process.stderr = persisting_agentctl::StdioMode::Capture; - } - write_private_json(&spec_path, &delegated)?; - Ok(Self { - _temporary: temporary, - spec_path, - result_path, - }) - } - - pub(crate) fn read_result( - &self, - run_id: &persisting_agentctl::RunId, - attempt_id: &AttemptId, - lease_epoch: u64, - ) -> anyhow::Result { - let mut output: DelegatedRunOutput = - serde_json::from_slice(&std::fs::read(&self.result_path)?)?; - output.result.run_id = run_id.clone(); - output.result.attempt_id = attempt_id.clone(); - output.result.lease_epoch = lease_epoch; - output.agentctl.run_id = run_id.to_string(); - output.agentctl.attempt_id = attempt_id.to_string(); - Ok(output) - } -} - -pub(crate) fn write_result(path: &Path, output: &DelegatedRunOutput) -> anyhow::Result<()> { - let parent = path - .parent() - .ok_or_else(|| anyhow::anyhow!("result path has no parent: {}", path.display()))?; - std::fs::create_dir_all(parent)?; - let temporary = parent.join(format!( - ".{}.{}.tmp", - path.file_name() - .and_then(|name| name.to_str()) - .unwrap_or("run-result"), - uuid::Uuid::new_v4().simple() - )); - write_private_json(&temporary, output)?; - std::fs::rename(temporary, path)?; - Ok(()) -} - -fn write_private_json(path: &Path, value: &impl serde::Serialize) -> anyhow::Result<()> { - let body = serde_json::to_vec_pretty(value)?; - std::fs::write(path, body)?; - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?; - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn delegated_spec_drops_host_agentctl_and_normalizes_result_identity() { - let mut spec = RunSpec::process("run-one", "agent", "true"); - let RunInvocation::Process(process) = &mut spec.invocation; - process.env.insert( - "PERSISTING_AGENTCTL_ENDPOINT".into(), - "/tmp/host.sock".into(), - ); - process.env.insert("KEEP".into(), "yes".into()); - let files = DelegatedRunFiles::new(&spec).unwrap(); - let delegated: RunSpec = - serde_json::from_slice(&std::fs::read(&files.spec_path).unwrap()).unwrap(); - let RunInvocation::Process(process) = delegated.invocation; - assert!(!process.env.contains_key("PERSISTING_AGENTCTL_ENDPOINT")); - assert_eq!(process.env.get("KEEP").map(String::as_str), Some("yes")); - } -} diff --git a/crates/persisting-pvisor/src/event.rs b/crates/persisting-pvisor/src/event.rs deleted file mode 100644 index 8d378fd3a..000000000 --- a/crates/persisting-pvisor/src/event.rs +++ /dev/null @@ -1,212 +0,0 @@ -use anyhow::Result; -use async_trait::async_trait; -use persisting_agentctl::{AttemptId, RunId}; -use persisting_events::{EventIdentity, EventRecord}; -use serde_json::Value; -use std::sync::{Arc, Mutex}; -use tokio::sync::Mutex as AsyncMutex; -use tokio::sync::broadcast; - -/// Whether an append error proves that the event was not persisted. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum EventAppendErrorKind { - /// The sink guarantees that this event was not committed. - Rejected, - /// The caller cannot know whether the sink committed the event. - Unknown, -} - -#[async_trait] -pub trait EventSink: Send + Sync { - async fn append(&self, event: &EventRecord) -> Result<()>; - - /// Errors are ambiguous by default. A sink may opt into `Rejected` only - /// when it can prove the append had no durable effect. - fn classify_append_error(&self, _error: &anyhow::Error) -> EventAppendErrorKind { - EventAppendErrorKind::Unknown - } -} - -#[derive(Debug, Default)] -pub struct NoopEventSink; - -#[async_trait] -impl EventSink for NoopEventSink { - async fn append(&self, _event: &EventRecord) -> Result<()> { - Ok(()) - } -} - -/// In-memory sink intended for embedding, tests, and early integrations. -#[derive(Debug, Default)] -pub struct MemoryEventSink { - events: Mutex>, -} - -impl MemoryEventSink { - pub fn events(&self) -> Vec { - self.events - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - .clone() - } -} - -#[async_trait] -impl EventSink for MemoryEventSink { - async fn append(&self, event: &EventRecord) -> Result<()> { - self.events - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - .push(event.clone()); - Ok(()) - } -} - -/// Assigns one monotonic event sequence to an Attempt and fans events out to -/// the canonical sink plus live subscribers. -#[derive(Clone)] -pub struct RunEventPublisher { - run_id: RunId, - attempt_id: AttemptId, - producer: String, - next_seq: Arc>, - sink: Arc, - live: broadcast::Sender, -} - -impl RunEventPublisher { - pub(crate) fn new( - run_id: RunId, - attempt_id: AttemptId, - producer: impl Into, - sink: Arc, - live: broadcast::Sender, - ) -> Self { - Self { - run_id, - attempt_id, - producer: producer.into(), - next_seq: Arc::new(AsyncMutex::new(0)), - sink, - live, - } - } - - pub fn subscribe(&self) -> broadcast::Receiver { - self.live.subscribe() - } - - pub(crate) fn classify_append_error(&self, error: &anyhow::Error) -> EventAppendErrorKind { - self.sink.classify_append_error(error) - } - - pub async fn publish( - &self, - kind: impl Into, - source: impl Into, - payload: Value, - ) -> Result { - // Persistence and sequence assignment form one per-Attempt critical - // section. A definitely rejected append reuses its sequence; an - // ambiguous failure consumes it because the sink may have committed. - let mut next_seq = self.next_seq.lock().await; - let seq = *next_seq; - let following_seq = seq.checked_add(1).ok_or_else(|| { - anyhow::anyhow!("event sequence exhausted for Attempt {}", self.attempt_id) - })?; - let (timestamp, timestamp_unix_ms) = crate::util::now_rfc3339_and_unix_ms(); - let event = EventRecord { - identity: EventIdentity { - event_id: Some(format!("event-{}", uuid::Uuid::new_v4())), - run_id: Some(self.run_id.to_string()), - attempt_id: Some(self.attempt_id.to_string()), - timestamp_unix_ms: Some(timestamp_unix_ms), - producer: Some(self.producer.clone()), - ..EventIdentity::default() - }, - seq, - kind: kind.into(), - source: source.into(), - timestamp: Some(timestamp), - session_id: None, - agent_id: None, - parent_uuid: None, - trace_id: None, - call_id: None, - subagent_id: None, - parent_agent_id: None, - branch: None, - parent_call_id: None, - payload, - }; - if let Err(error) = self.sink.append(&event).await { - if self.sink.classify_append_error(&error) == EventAppendErrorKind::Unknown { - *next_seq = following_seq; - } - return Err(error); - } - *next_seq = following_seq; - drop(next_seq); - // Live observers only see events accepted by the canonical sink. - let _ = self.live.send(event.clone()); - Ok(event) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - struct FailFirstSink { - kind: EventAppendErrorKind, - sequences: Mutex>, - } - - #[async_trait] - impl EventSink for FailFirstSink { - async fn append(&self, event: &EventRecord) -> Result<()> { - let mut sequences = self.sequences.lock().unwrap(); - sequences.push(event.seq); - if sequences.len() == 1 { - anyhow::bail!("first append failed"); - } - Ok(()) - } - - fn classify_append_error(&self, _error: &anyhow::Error) -> EventAppendErrorKind { - self.kind - } - } - - fn publisher(sink: Arc) -> RunEventPublisher { - let (live, _) = broadcast::channel(4); - RunEventPublisher::new("run".into(), "attempt".into(), "test", sink, live) - } - - #[tokio::test] - async fn rejected_append_reuses_sequence() { - let sink = Arc::new(FailFirstSink { - kind: EventAppendErrorKind::Rejected, - sequences: Mutex::new(Vec::new()), - }); - let events = publisher(sink.clone()); - assert!(events.publish("first", "test", Value::Null).await.is_err()); - let accepted = events.publish("retry", "test", Value::Null).await.unwrap(); - assert_eq!(accepted.seq, 0); - assert_eq!(*sink.sequences.lock().unwrap(), vec![0, 0]); - } - - #[tokio::test] - async fn ambiguous_append_consumes_sequence() { - let sink = Arc::new(FailFirstSink { - kind: EventAppendErrorKind::Unknown, - sequences: Mutex::new(Vec::new()), - }); - let events = publisher(sink.clone()); - assert!(events.publish("first", "test", Value::Null).await.is_err()); - let accepted = events.publish("retry", "test", Value::Null).await.unwrap(); - assert_eq!(accepted.seq, 1); - assert_eq!(*sink.sequences.lock().unwrap(), vec![0, 1]); - } -} diff --git a/crates/persisting-pvisor/src/executor.rs b/crates/persisting-pvisor/src/executor.rs deleted file mode 100644 index 058ca0b0f..000000000 --- a/crates/persisting-pvisor/src/executor.rs +++ /dev/null @@ -1,134 +0,0 @@ -use crate::event::RunEventPublisher; -use async_trait::async_trait; -use persisting_agentctl::{ - AttemptId, ExecutorDescriptor, RunInvocation, RunResult, RunSpec, RunState, RunStatus, -}; -use serde_json::json; -use std::sync::Arc; -use tokio::sync::watch; -use tokio_util::sync::CancellationToken; - -#[derive(Clone, Default)] -pub(crate) struct AttemptAttachments { - pub vm_network: Option>>>, -} - -#[derive(Clone)] -pub struct AttemptContext { - spec: Arc, - attempt_id: AttemptId, - cancel: CancellationToken, - status: watch::Sender, - events: RunEventPublisher, - agentctl: crate::AgentCtlControl, - attachments: AttemptAttachments, -} - -impl AttemptContext { - pub(crate) fn new( - spec: Arc, - attempt_id: AttemptId, - cancel: CancellationToken, - status: watch::Sender, - events: RunEventPublisher, - agentctl: crate::AgentCtlControl, - attachments: AttemptAttachments, - ) -> Self { - Self { - spec, - attempt_id, - cancel, - status, - events, - agentctl, - attachments, - } - } - - pub fn spec(&self) -> &RunSpec { - &self.spec - } - - pub fn attempt_id(&self) -> &AttemptId { - &self.attempt_id - } - - pub fn cancellation(&self) -> CancellationToken { - self.cancel.clone() - } - - pub(crate) fn take_vm_network( - &self, - ) -> anyhow::Result> { - let Some(attachment) = &self.attachments.vm_network else { - return Ok(None); - }; - let mut attachment = attachment - .lock() - .map_err(|_| anyhow::anyhow!("VM network attachment lock poisoned"))?; - Ok(attachment.take()) - } - - pub fn events(&self) -> &RunEventPublisher { - &self.events - } - - pub(crate) fn import_delegated_agentctl(&self, snapshot: crate::AgentCtlSnapshot) { - self.agentctl.import_delegated_snapshot(snapshot); - } - - pub async fn transition(&self, state: RunState, message: impl Into>) { - let now = crate::util::unix_now_ms(); - let message = message.into(); - self.status.send_modify(|status| { - status.state = state; - status.updated_at_unix_ms = now; - status.message = message.clone(); - if matches!(state, RunState::Starting | RunState::Running) - && status.attempt.started_at_unix_ms.is_none() - { - status.attempt.started_at_unix_ms = Some(now); - } - if state.is_terminal() { - status.attempt.finished_at_unix_ms = Some(now); - } - }); - let _ = self - .events - .publish( - "run.state_changed", - "runtime", - json!({ - "state": state, - "message": message, - }), - ) - .await; - } - - /// Make a terminal status visible after finalization and terminal-event commit. - pub(crate) fn finish(&self, state: RunState, message: Option) { - let now = crate::util::unix_now_ms(); - self.status.send_modify(|status| { - status.state = state; - status.updated_at_unix_ms = now; - status.message = message.clone(); - status.attempt.finished_at_unix_ms = Some(now); - }); - } -} - -#[async_trait] -pub trait RunExecutor: Send + Sync { - fn descriptor(&self) -> ExecutorDescriptor; - fn supports(&self, invocation: &RunInvocation) -> bool; - /// Whether this executor consumes pVisor's VM network attachment. - /// - /// A virtual-machine descriptor alone is not sufficient to claim that the - /// Attempt network is non-bypassable: pluggable executors must explicitly - /// opt into the transport handoff contract. - fn supports_vm_network_attachment(&self) -> bool { - false - } - async fn execute(&self, context: AttemptContext) -> RunResult; -} diff --git a/crates/persisting-pvisor/src/firmware.rs b/crates/persisting-pvisor/src/firmware.rs deleted file mode 100644 index e6cc7e741..000000000 --- a/crates/persisting-pvisor/src/firmware.rs +++ /dev/null @@ -1,254 +0,0 @@ -//! Verified libkrunfw download and per-user cache. - -use anyhow::{Context, bail}; -use flate2::read::GzDecoder; -use fs2::FileExt; -use reqwest::blocking::Client; -use sha2::{Digest, Sha256}; -use std::fs::{self, OpenOptions}; -use std::io::{Read, Write}; -use std::path::{Path, PathBuf}; -#[cfg(target_os = "macos")] -use std::process::Command; -use std::time::Duration; - -pub(crate) const VERSION: &str = "5.5.0"; -#[cfg(target_os = "macos")] -const ABI_VERSION: &str = "5"; -const MAX_ARCHIVE_BYTES: usize = 64 * 1024 * 1024; - -#[derive(Debug, Clone, Copy)] -struct ReleaseAsset { - url: &'static str, - sha256: &'static str, - archive_member: &'static str, -} - -#[derive(Debug, Clone)] -pub struct FirmwareStore { - root: PathBuf, - client: Client, -} - -impl FirmwareStore { - pub fn new() -> anyhow::Result { - let root = dirs::cache_dir() - .context("platform cache directory is unavailable")? - .join("persisting/pvisor/firmware"); - fs::create_dir_all(&root)?; - let client = Client::builder() - .user_agent(concat!("pvisor/", env!("CARGO_PKG_VERSION"))) - .connect_timeout(Duration::from_secs(30)) - .timeout(Duration::from_secs(300)) - .build() - .context("build libkrunfw download client")?; - Ok(Self { root, client }) - } - - pub fn prepare(&self) -> anyhow::Result { - let platform = platform_name()?; - let directory = self.root.join(VERSION).join(platform); - let firmware = directory.join(crate::vm::firmware_name()); - if firmware.is_file() { - return Ok(directory); - } - - fs::create_dir_all(&directory)?; - let lock = OpenOptions::new() - .create(true) - .truncate(false) - .read(true) - .write(true) - .open(self.root.join(format!("{VERSION}-{platform}.lock")))?; - lock.lock_exclusive()?; - if firmware.is_file() { - FileExt::unlock(&lock)?; - return Ok(directory); - } - - let result = self.install(&directory, &firmware); - FileExt::unlock(&lock)?; - result?; - Ok(directory) - } - - fn install(&self, directory: &Path, firmware: &Path) -> anyhow::Result<()> { - let asset = release_asset()?; - let archive = self.load_archive(directory, asset)?; - - let temporary = tempfile::Builder::new() - .prefix(".libkrunfw-") - .tempdir_in(directory)?; - let payload = temporary.path().join("kernel.c"); - extract_member(&archive, asset.archive_member, &payload)?; - let built = temporary.path().join(crate::vm::firmware_name()); - build_platform_firmware(&payload, &built)?; - let mut permissions = fs::metadata(&built)?.permissions(); - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - permissions.set_mode(0o755); - } - fs::set_permissions(&built, permissions)?; - fs::rename(&built, firmware) - .with_context(|| format!("install cached firmware at {}", firmware.display()))?; - fs::write( - directory.join("source.sha256"), - format!("{} {}\n", asset.sha256, asset.url), - )?; - Ok(()) - } - - fn load_archive(&self, directory: &Path, asset: ReleaseAsset) -> anyhow::Result> { - let cached = directory.join("source.tgz"); - if cached.is_file() { - let archive = fs::read(&cached)?; - verify_archive(&archive, asset.sha256)?; - return Ok(archive); - } - let mut response = self - .client - .get(asset.url) - .send() - .with_context(|| format!("download libkrunfw {VERSION}"))? - .error_for_status() - .context("libkrunfw release download failed")?; - if let Some(length) = response.content_length() { - anyhow::ensure!( - length <= MAX_ARCHIVE_BYTES as u64, - "libkrunfw archive is unexpectedly large: {length} bytes" - ); - } - let mut archive = Vec::new(); - response - .by_ref() - .take(MAX_ARCHIVE_BYTES as u64 + 1) - .read_to_end(&mut archive)?; - verify_archive(&archive, asset.sha256)?; - let mut temporary = tempfile::NamedTempFile::new_in(directory)?; - temporary.write_all(&archive)?; - temporary.flush()?; - match temporary.persist_noclobber(&cached) { - Ok(_) => {} - Err(error) if cached.is_file() => drop(error), - Err(error) => return Err(error.error.into()), - } - Ok(archive) - } -} - -fn verify_archive(archive: &[u8], expected: &str) -> anyhow::Result<()> { - anyhow::ensure!( - archive.len() <= MAX_ARCHIVE_BYTES, - "libkrunfw archive exceeds {} bytes", - MAX_ARCHIVE_BYTES - ); - let actual = encode_hex(&Sha256::digest(archive)); - anyhow::ensure!( - actual == expected, - "libkrunfw archive digest mismatch: expected {expected}, got {actual}" - ); - Ok(()) -} - -fn platform_name() -> anyhow::Result<&'static str> { - match (std::env::consts::OS, std::env::consts::ARCH) { - ("macos", "aarch64") => Ok("macos-aarch64"), - ("linux", "aarch64") => Ok("linux-aarch64"), - ("linux", "x86_64") => Ok("linux-x86_64"), - (os, arch) => bail!("automatic libkrunfw installation is unsupported on {os}/{arch}"), - } -} - -fn release_asset() -> anyhow::Result { - match (std::env::consts::OS, std::env::consts::ARCH) { - ("macos", "aarch64") => Ok(ReleaseAsset { - url: "https://github.com/libkrun/libkrunfw/releases/download/v5.5.0/libkrunfw-prebuilt-aarch64.tgz", - sha256: "5bfae6efee63dbdf04a8fac2a69d772d9f900af2f54c4429b4acdfd6d86b9979", - archive_member: "libkrunfw/kernel.c", - }), - ("linux", "aarch64") => Ok(ReleaseAsset { - url: "https://github.com/libkrun/libkrunfw/releases/download/v5.5.0/libkrunfw-aarch64.tgz", - sha256: "b04c9a5520a1ea52b5b35d87559566872246145961c4b6978034c9b9be54b89b", - archive_member: "lib64/libkrunfw.so.5.5.0", - }), - ("linux", "x86_64") => Ok(ReleaseAsset { - url: "https://github.com/libkrun/libkrunfw/releases/download/v5.5.0/libkrunfw-x86_64.tgz", - sha256: "c169206b01c89fbe134f1728bf4f988702bc7f73b4cf73e6fdece447d6fceca1", - archive_member: "lib64/libkrunfw.so.5.5.0", - }), - (os, arch) => bail!("automatic libkrunfw installation is unsupported on {os}/{arch}"), - } -} - -fn extract_member(archive: &[u8], expected: &str, destination: &Path) -> anyhow::Result<()> { - let decoder = GzDecoder::new(archive); - let mut archive = tar::Archive::new(decoder); - for entry in archive.entries()? { - let mut entry = entry?; - if entry.path()?.as_ref() != Path::new(expected) { - continue; - } - anyhow::ensure!( - entry.header().entry_type().is_file(), - "libkrunfw archive member {expected} is not a regular file" - ); - let mut output = OpenOptions::new() - .create_new(true) - .write(true) - .open(destination)?; - std::io::copy(&mut entry, &mut output)?; - output.flush()?; - return Ok(()); - } - bail!("libkrunfw archive is missing {expected}") -} - -#[cfg(target_os = "macos")] -fn build_platform_firmware(source: &Path, destination: &Path) -> anyhow::Result<()> { - let output = Command::new("/usr/bin/cc") - .arg("-fPIC") - .arg(format!("-DABI_VERSION={ABI_VERSION}")) - .arg("-shared") - .arg("-Wl,-install_name,@rpath/libkrunfw.5.dylib") - .arg("-o") - .arg(destination) - .arg(source) - .output() - .context("compile downloaded libkrunfw payload with /usr/bin/cc")?; - anyhow::ensure!( - output.status.success(), - "compile downloaded libkrunfw payload: {}", - String::from_utf8_lossy(&output.stderr).trim() - ); - Ok(()) -} - -#[cfg(target_os = "linux")] -fn build_platform_firmware(source: &Path, destination: &Path) -> anyhow::Result<()> { - fs::copy(source, destination)?; - Ok(()) -} - -fn encode_hex(bytes: &[u8]) -> String { - const HEX: &[u8; 16] = b"0123456789abcdef"; - let mut output = String::with_capacity(bytes.len() * 2); - for byte in bytes { - output.push(HEX[(byte >> 4) as usize] as char); - output.push(HEX[(byte & 0x0f) as usize] as char); - } - output -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn release_digest_is_pinned_sha256() { - let asset = release_asset().unwrap(); - assert_eq!(asset.sha256.len(), 64); - assert!(asset.sha256.bytes().all(|byte| byte.is_ascii_hexdigit())); - assert!(asset.url.contains(&format!("/v{VERSION}/"))); - } -} diff --git a/crates/persisting-pvisor/src/lib.rs b/crates/persisting-pvisor/src/lib.rs deleted file mode 100644 index 74c7d9300..000000000 --- a/crates/persisting-pvisor/src/lib.rs +++ /dev/null @@ -1,78 +0,0 @@ -//! pVisor — foreground Agent Run manager and portable execution runtime. -//! -//! pVisor is a top-level Persisting component alongside pPilot and pChronicle. -//! Hosts call [`PVisor::run`] directly; pVisor assembles execution, control, -//! network, filesystem, and the optional internal Gateway driver. -//! -//! pChronicle is a peer history service, not a child module. When persistence -//! is requested, pVisor starts the Control component of `pchronicle serve` and -//! talks to it over the lightweight versioned client protocol. - -pub mod cli; -mod runtime; - -mod agentctl; -mod artifact; -mod bundle; -mod checkpoint; -mod config; -mod container; -mod control; -mod delegated; -mod event; -mod executor; -mod firmware; -mod oci; -mod process; -mod pvisor; -#[doc(hidden)] -pub mod sandbox; -mod supervisor; -mod util; -mod vm; - -pub use agentctl::{ - AGENTCTL_MAX_SESSIONS, AgentClientSnapshot, AgentCtlControl, AgentCtlServer, AgentCtlSnapshot, -}; -pub use bundle::{ - BundleArtifact, BundleRun, FilesystemSummary, NetworkSummary, RUN_BUNDLE_FILENAME, - RUN_BUNDLE_SCHEMA_VERSION, ResourceSummary, RunBundle, SafetySummary, -}; -pub use checkpoint::{ - CHECKPOINTS_DIR, CheckpointConsistency, LogicalCheckpoint, create_logical_checkpoint, - latest_logical_checkpoint, restore_logical_checkpoint, -}; -pub use config::{ - ChronicleMode, ChronicleSettings, ContainerMount, ContainerNetwork, ContainerPlatform, - ContainerSettings, GatewayDriverConfig, GatewayMode, GatewaySettings, NetworkDriverConfig, - OverlayFsBackend, OverlayFsCommit, OverlayFsSettings, OverlayNetMode, OverlayNetPolicy, - OverlayNetSettings, PVisorConfig, RecordFormat, RecordSettings, RunConfig, RunExecutorKind, - RunPolicy, RunSettings, RunStdio, VmSettings, -}; -pub use container::ContainerExecutor; -pub use control::{ - ControlController, ControlEffect, ControlMachine, ControlReason, ControlRequest, ControlState, - ControlTransition, NetworkGuard, NetworkHostRule, NetworkRule, PolicyControlController, - host_matches, is_public_egress_ip, normalize_host, parse_network_rule, -}; -pub use event::{ - EventAppendErrorKind, EventSink, MemoryEventSink, NoopEventSink, RunEventPublisher, -}; -pub use executor::{AttemptContext, RunExecutor}; -pub use persisting_agentctl::{ - AGENTCTL_ENDPOINT_ENV, AGENTCTL_MAX_FRAME_BYTES, AGENTCTL_TOKEN_ENV, AGENTCTL_TRANSPORT_ENV, - AGENTCTL_VERSION, AGENTCTL_VERSION_ENV, AgentDirective, AgentErrorCode, AgentRequest, - AgentResponse, AgentState, SUPERVISOR_PROTOCOL_VERSION, SupervisorClientMessage, - SupervisorDirective, SupervisorDirectiveAck, SupervisorDirectiveEnvelope, SupervisorHeartbeat, - SupervisorNetworkQuotaGrant, SupervisorRegistration, SupervisorServerMessage, -}; -pub use persisting_gateway::sink::CaptureEventSink as TrajectoryEventSink; -pub use process::ProcessExecutor; -pub use pvisor::{PVisor, PVisorBuilder, PVisorError, RunCancellation, RunEventStream, RunHandle}; -pub use runtime::{ - ChangeEntry, ChangeEntryType, ChangeKind, ImplantPlan, OverlayHint, RunLineage, - RuntimeCapabilities, -}; -pub use util::unix_now_ms; -pub use vm::VmExecutor; -pub use vm::run_internal_if_requested as run_krun_internal_if_requested; diff --git a/crates/persisting-pvisor/src/oci.rs b/crates/persisting-pvisor/src/oci.rs deleted file mode 100644 index c8c936737..000000000 --- a/crates/persisting-pvisor/src/oci.rs +++ /dev/null @@ -1,964 +0,0 @@ -//! Minimal OCI Distribution client and content-addressed rootfs store. -//! -//! This deliberately owns the small public-image path instead of shelling out -//! to Docker, Podman, or Buildah. The on-disk layout is private to pVisor; OCI -//! digests remain the source of truth for blobs and prepared root filesystems. - -use anyhow::{Context, bail}; -use flate2::read::GzDecoder; -use fs2::FileExt; -use reqwest::blocking::{Client, Response}; -use reqwest::header::{ACCEPT, AUTHORIZATION, WWW_AUTHENTICATE}; -use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; -use std::collections::BTreeMap; -use std::fs::{self, File, OpenOptions}; -use std::io::{Read, Write}; -use std::path::{Component, Path, PathBuf}; - -pub const DEFAULT_IMAGE: &str = "ubuntu:latest"; - -const MANIFEST_ACCEPT: &str = concat!( - "application/vnd.oci.image.index.v1+json, ", - "application/vnd.docker.distribution.manifest.list.v2+json, ", - "application/vnd.oci.image.manifest.v1+json, ", - "application/vnd.docker.distribution.manifest.v2+json" -); - -#[derive(Debug, Clone, PartialEq, Eq)] -struct ImageReference { - registry: String, - repository: String, - reference: String, -} - -#[derive(Debug, Clone)] -pub struct PreparedImage { - pub rootfs: PathBuf, - pub digest: String, - pub env: BTreeMap, - pub entrypoint: Vec, - pub cmd: Vec, -} - -#[derive(Debug, Clone)] -pub struct ImageStore { - root: PathBuf, - client: Client, -} - -#[derive(Debug, Clone, Deserialize)] -struct Descriptor { - #[serde(rename = "mediaType", default)] - media_type: String, - digest: String, - #[serde(default)] - size: Option, - #[serde(default)] - platform: Option, -} - -#[derive(Debug, Clone, Deserialize)] -struct Platform { - architecture: String, - os: String, - #[serde(default)] - variant: Option, -} - -#[derive(Debug, Deserialize)] -struct ImageIndex { - manifests: Vec, -} - -#[derive(Debug, Deserialize)] -struct ImageManifest { - config: Descriptor, - layers: Vec, -} - -#[derive(Debug, Default, Deserialize)] -struct ImageConfiguration { - #[serde(default)] - config: RuntimeConfiguration, -} - -#[derive(Debug, Default, Deserialize)] -#[serde(rename_all = "PascalCase")] -struct RuntimeConfiguration { - #[serde(default)] - env: Option>, - #[serde(default)] - entrypoint: Option>, - #[serde(default)] - cmd: Option>, -} - -#[derive(Debug, Deserialize)] -struct TokenResponse { - #[serde(default)] - token: String, - #[serde(default)] - access_token: String, -} - -#[derive(Debug, Serialize)] -struct RootfsMetadata<'a> { - image: &'a str, - manifest_digest: &'a str, -} - -impl ImageStore { - pub fn new(root: Option) -> anyhow::Result { - let root = match root { - Some(root) => root, - None => default_store_dir()?, - }; - fs::create_dir_all(root.join("blobs/sha256"))?; - fs::create_dir_all(root.join("rootfs-v3/sha256"))?; - fs::create_dir_all(root.join("metadata/sha256"))?; - fs::create_dir_all(root.join("locks"))?; - let client = Client::builder() - .user_agent(concat!("pvisor/", env!("CARGO_PKG_VERSION"))) - .build() - .context("build OCI registry client")?; - Ok(Self { root, client }) - } - - pub fn prepare(&self, image: &str) -> anyhow::Result { - let image_ref = ImageReference::parse(image)?; - let mut registry = RegistryClient::new(&self.client, image_ref.clone()); - let (mut body, mut manifest_digest) = registry.fetch_manifest(&image_ref.reference)?; - let value: serde_json::Value = serde_json::from_slice(&body) - .with_context(|| format!("decode OCI manifest for {image}"))?; - if value.get("manifests").is_some() { - let index: ImageIndex = serde_json::from_value(value)?; - let descriptor = select_platform(&index.manifests)?; - let fetched = registry.fetch_manifest(&descriptor.digest)?; - body = fetched.0; - manifest_digest = fetched.1; - verify_digest(&descriptor.digest, &body)?; - } - let manifest: ImageManifest = serde_json::from_slice(&body) - .with_context(|| format!("decode image manifest for {image}"))?; - let config_path = self.fetch_blob(&mut registry, &manifest.config)?; - let config: ImageConfiguration = serde_json::from_reader(File::open(config_path)?) - .with_context(|| format!("decode image configuration for {image}"))?; - - let digest_hex = digest_hex(&manifest_digest)?; - let rootfs = self.root.join("rootfs-v3/sha256").join(digest_hex); - let lock_path = self.root.join("locks").join(format!("{digest_hex}.lock")); - let lock = OpenOptions::new() - .create(true) - .truncate(false) - .read(true) - .write(true) - .open(&lock_path)?; - lock.lock_exclusive()?; - if !rootfs.is_dir() { - let partial = self - .root - .join("rootfs-v3/sha256") - .join(format!(".{digest_hex}.partial-{}", uuid::Uuid::new_v4())); - fs::create_dir(&partial)?; - let extraction = (|| -> anyhow::Result<()> { - for layer in &manifest.layers { - let blob = self.fetch_blob(&mut registry, layer)?; - apply_layer(&blob, &layer.media_type, &partial)?; - } - fs::rename(&partial, &rootfs)?; - Ok(()) - })(); - if extraction.is_err() { - let _ = fs::remove_dir_all(&partial); - } - extraction?; - let metadata = RootfsMetadata { - image, - manifest_digest: &manifest_digest, - }; - fs::write( - self.root - .join("metadata/sha256") - .join(format!("{digest_hex}.json")), - serde_json::to_vec_pretty(&metadata)?, - )?; - } - lock.unlock()?; - - Ok(PreparedImage { - rootfs, - digest: manifest_digest, - env: parse_env(config.config.env.unwrap_or_default()), - entrypoint: config.config.entrypoint.unwrap_or_default(), - cmd: config.config.cmd.unwrap_or_default(), - }) - } - - fn fetch_blob( - &self, - registry: &mut RegistryClient<'_>, - descriptor: &Descriptor, - ) -> anyhow::Result { - let digest_hex = digest_hex(&descriptor.digest)?; - let destination = self.root.join("blobs/sha256").join(digest_hex); - if destination.is_file() { - verify_file_digest(&descriptor.digest, &destination)?; - return Ok(destination); - } - let mut response = registry.get( - &format!( - "/v2/{}/blobs/{}", - registry.image.repository, descriptor.digest - ), - None, - )?; - let mut temporary = tempfile::NamedTempFile::new_in(self.root.join("blobs/sha256"))?; - let mut hasher = Sha256::new(); - let mut size = 0_u64; - let mut buffer = [0_u8; 64 * 1024]; - loop { - let read = response.read(&mut buffer)?; - if read == 0 { - break; - } - hasher.update(&buffer[..read]); - temporary.write_all(&buffer[..read])?; - size += read as u64; - } - let actual = format!("sha256:{}", encode_hex(&hasher.finalize())); - anyhow::ensure!( - actual == descriptor.digest, - "OCI blob digest mismatch: expected {}, got {actual}", - descriptor.digest - ); - if let Some(expected) = descriptor.size { - anyhow::ensure!( - expected == size, - "OCI blob size mismatch for {}: expected {expected}, got {size}", - descriptor.digest - ); - } - match temporary.persist_noclobber(&destination) { - Ok(_) => {} - Err(error) if destination.is_file() => drop(error), - Err(error) => return Err(error.error.into()), - } - Ok(destination) - } -} - -struct RegistryClient<'a> { - client: &'a Client, - image: ImageReference, - token: Option, -} - -impl<'a> RegistryClient<'a> { - fn new(client: &'a Client, image: ImageReference) -> Self { - Self { - client, - image, - token: None, - } - } - - fn fetch_manifest(&mut self, reference: &str) -> anyhow::Result<(Vec, String)> { - let path = format!("/v2/{}/manifests/{reference}", self.image.repository); - let mut response = self.get(&path, Some(MANIFEST_ACCEPT))?; - let mut body = Vec::new(); - response.read_to_end(&mut body)?; - let digest = format!("sha256:{}", encode_hex(&Sha256::digest(&body))); - if reference.starts_with("sha256:") { - verify_digest(reference, &body)?; - } - Ok((body, digest)) - } - - fn get(&mut self, path: &str, accept: Option<&str>) -> anyhow::Result { - let url = format!("https://{}{}", self.image.registry, path); - let send = |token: Option<&str>| { - let mut request = self.client.get(&url); - if let Some(accept) = accept { - request = request.header(ACCEPT, accept); - } - if let Some(token) = token { - request = request.header(AUTHORIZATION, format!("Bearer {token}")); - } - request.send() - }; - let mut response = send(self.token.as_deref())?; - if response.status() == reqwest::StatusCode::UNAUTHORIZED { - let challenge = response - .headers() - .get(WWW_AUTHENTICATE) - .and_then(|value| value.to_str().ok()) - .context("OCI registry omitted the Bearer authentication challenge")?; - let token = self.fetch_token(challenge)?; - self.token = Some(token); - response = send(self.token.as_deref())?; - } - response - .error_for_status() - .with_context(|| format!("request OCI registry URL {url}")) - } - - fn fetch_token(&self, challenge: &str) -> anyhow::Result { - let fields = parse_bearer_challenge(challenge)?; - let realm = fields - .get("realm") - .context("Bearer challenge has no realm")?; - let mut request = self.client.get(realm); - if let Some(service) = fields.get("service") { - request = request.query(&[("service", service)]); - } - let scope = fields - .get("scope") - .cloned() - .unwrap_or_else(|| format!("repository:{}:pull", self.image.repository)); - request = request.query(&[("scope", &scope)]); - let response: TokenResponse = request.send()?.error_for_status()?.json()?; - let token = if response.token.is_empty() { - response.access_token - } else { - response.token - }; - anyhow::ensure!( - !token.is_empty(), - "registry token response contained no token" - ); - Ok(token) - } -} - -impl ImageReference { - fn parse(value: &str) -> anyhow::Result { - let value = value - .strip_prefix("docker://") - .or_else(|| value.strip_prefix("oci://")) - .unwrap_or(value); - anyhow::ensure!(!value.trim().is_empty(), "OCI image reference is empty"); - let (name, reference) = if let Some((name, digest)) = value.rsplit_once('@') { - (name, digest.to_owned()) - } else { - let last_slash = value.rfind('/'); - let last_colon = value.rfind(':'); - if last_colon.is_some_and(|colon| last_slash.is_none_or(|slash| colon > slash)) { - let colon = last_colon.expect("checked above"); - (&value[..colon], value[colon + 1..].to_owned()) - } else { - (value, "latest".to_owned()) - } - }; - anyhow::ensure!( - !name.is_empty() && !reference.is_empty(), - "invalid OCI image reference `{value}`" - ); - let mut parts = name.split('/'); - let first = parts.next().expect("non-empty above"); - let explicit_registry = first.contains('.') || first.contains(':') || first == "localhost"; - let (registry, mut repository) = if explicit_registry { - (first.to_owned(), parts.collect::>().join("/")) - } else { - ("registry-1.docker.io".to_owned(), name.to_owned()) - }; - anyhow::ensure!( - !repository.is_empty(), - "image reference `{value}` has no repository" - ); - let registry = match registry.as_str() { - "docker.io" | "index.docker.io" => "registry-1.docker.io".to_owned(), - _ => registry, - }; - if registry == "registry-1.docker.io" && !repository.contains('/') { - repository = format!("library/{repository}"); - } - Ok(Self { - registry, - repository, - reference, - }) - } -} - -fn default_store_dir() -> anyhow::Result { - if let Some(value) = std::env::var_os("PERSISTING_PVISOR_IMAGE_STORE") { - return Ok(PathBuf::from(value)); - } - Ok(dirs::cache_dir() - .unwrap_or(std::env::current_dir()?.join(".persisting/cache")) - .join("persisting/pvisor/images")) -} - -fn select_platform(manifests: &[Descriptor]) -> anyhow::Result<&Descriptor> { - let architecture = match std::env::consts::ARCH { - "aarch64" => "arm64", - "x86_64" => "amd64", - other => bail!("libkrun OCI images are unsupported on host architecture {other}"), - }; - manifests - .iter() - .find(|descriptor| { - descriptor.platform.as_ref().is_some_and(|platform| { - platform.os == "linux" - && platform.architecture == architecture - && (architecture != "arm64" - || platform - .variant - .as_deref() - .is_none_or(|value| value == "v8")) - }) - }) - .with_context(|| format!("image has no linux/{architecture} manifest")) -} - -fn parse_env(values: Vec) -> BTreeMap { - values - .into_iter() - .filter_map(|value| { - let (key, value) = value.split_once('=')?; - Some((key.to_owned(), value.to_owned())) - }) - .collect() -} - -fn parse_bearer_challenge(value: &str) -> anyhow::Result> { - let value = value - .strip_prefix("Bearer ") - .or_else(|| value.strip_prefix("bearer ")) - .context("OCI registry requested unsupported authentication")?; - let mut result = BTreeMap::new(); - for field in value.split(',') { - let (key, value) = field - .trim() - .split_once('=') - .context("invalid Bearer challenge")?; - result.insert(key.to_owned(), value.trim_matches('"').to_owned()); - } - Ok(result) -} - -fn digest_hex(digest: &str) -> anyhow::Result<&str> { - let value = digest - .strip_prefix("sha256:") - .context("pVisor v1 only supports sha256 OCI digests")?; - anyhow::ensure!( - value.len() == 64 && value.bytes().all(|byte| byte.is_ascii_hexdigit()), - "invalid sha256 digest `{digest}`" - ); - Ok(value) -} - -fn verify_digest(expected: &str, body: &[u8]) -> anyhow::Result<()> { - digest_hex(expected)?; - let actual = format!("sha256:{}", encode_hex(&Sha256::digest(body))); - anyhow::ensure!( - actual == expected, - "digest mismatch: expected {expected}, got {actual}" - ); - Ok(()) -} - -fn verify_file_digest(expected: &str, path: &Path) -> anyhow::Result<()> { - digest_hex(expected)?; - let mut file = File::open(path)?; - let mut hasher = Sha256::new(); - let mut buffer = [0_u8; 64 * 1024]; - loop { - let read = file.read(&mut buffer)?; - if read == 0 { - break; - } - hasher.update(&buffer[..read]); - } - let actual = format!("sha256:{}", encode_hex(&hasher.finalize())); - anyhow::ensure!( - actual == expected, - "cached OCI blob {} is corrupt", - path.display() - ); - Ok(()) -} - -fn encode_hex(bytes: &[u8]) -> String { - const HEX: &[u8; 16] = b"0123456789abcdef"; - let mut output = String::with_capacity(bytes.len() * 2); - for byte in bytes { - output.push(HEX[(byte >> 4) as usize] as char); - output.push(HEX[(byte & 0x0f) as usize] as char); - } - output -} - -fn apply_layer(blob: &Path, media_type: &str, rootfs: &Path) -> anyhow::Result<()> { - let tar_file = tempfile::NamedTempFile::new()?; - { - let input = File::open(blob)?; - let mut output = tar_file.reopen()?; - if media_type.ends_with("+gzip") { - std::io::copy(&mut GzDecoder::new(input), &mut output)?; - } else if media_type.ends_with("+zstd") { - std::io::copy(&mut zstd::stream::read::Decoder::new(input)?, &mut output)?; - } else if media_type.ends_with(".tar") - || media_type == "application/octet-stream" - || media_type.is_empty() - { - std::io::copy(&mut std::io::BufReader::new(input), &mut output)?; - } else { - bail!("unsupported OCI layer media type `{media_type}`"); - } - } - - let mut archive = tar::Archive::new(tar_file.reopen()?); - for entry in archive.entries()? { - let entry = entry?; - let relative = clean_relative(&entry.path()?)?; - if let Some(whiteout) = whiteout(&relative) { - match whiteout { - Whiteout::Remove(path) => remove_relative(rootfs, &path)?, - Whiteout::Opaque(path) => clear_relative_directory(rootfs, &path)?, - } - } - } - - let mut archive = tar::Archive::new(tar_file.reopen()?); - archive.set_preserve_permissions(true); - #[cfg(unix)] - let mut deferred_directory_modes = BTreeMap::new(); - for entry in archive.entries()? { - let mut entry = entry?; - let relative = clean_relative(&entry.path()?)?; - if whiteout(&relative).is_some() { - continue; - } - #[cfg(unix)] - make_ancestor_directories_writable(rootfs, &relative, &mut deferred_directory_modes)?; - // A rootless extractor cannot preserve an OCI entry's uid/gid. On - // Linux, retaining setuid/setgid would therefore grant the host - // user's identity inside the guest instead of the image owner. pVisor - // VM workloads currently start as guest root, so stripping the bits - // preserves execution while avoiding that incorrect privilege shift. - #[cfg(target_os = "linux")] - let sanitized_mode = entry.header().mode()? & !0o6000; - #[cfg(unix)] - let directory_mode = if entry.header().entry_type().is_dir() { - Some({ - #[cfg(target_os = "linux")] - { - sanitized_mode - } - #[cfg(not(target_os = "linux"))] - { - entry.header().mode()? - } - }) - } else { - None - }; - #[cfg(target_os = "macos")] - let linux_owner = ( - entry.header().uid().unwrap_or(0), - entry.header().gid().unwrap_or(0), - ); - let unpacked = entry.unpack_in(rootfs)?; - anyhow::ensure!( - unpacked, - "OCI layer entry escaped rootfs: {}", - relative.display() - ); - #[cfg(target_os = "linux")] - if !entry.header().entry_type().is_dir() && !entry.header().entry_type().is_symlink() { - use std::os::unix::fs::PermissionsExt; - fs::set_permissions( - rootfs.join(&relative), - fs::Permissions::from_mode(sanitized_mode), - )?; - } - #[cfg(target_os = "macos")] - { - use std::os::unix::fs::{MetadataExt, PermissionsExt}; - let path = rootfs.join(&relative); - let metadata = fs::symlink_metadata(&path)?; - let mode = metadata.mode(); - let temporarily_writable = !metadata.file_type().is_symlink() && mode & 0o200 == 0; - if temporarily_writable { - fs::set_permissions(&path, fs::Permissions::from_mode(mode | 0o200))?; - } - let override_stat = format!("{}:{}:0{:o}", linux_owner.0, linux_owner.1, mode); - let xattr_result = persisting_overlay_core::sys::set_xattr( - &path, - std::ffi::OsStr::new("user.containers.override_stat"), - override_stat.as_bytes(), - 0, - ); - if temporarily_writable { - fs::set_permissions(&path, fs::Permissions::from_mode(mode))?; - } - xattr_result?; - } - #[cfg(unix)] - if let Some(mode) = directory_mode { - use std::os::unix::fs::PermissionsExt; - deferred_directory_modes.insert(relative.clone(), mode); - fs::set_permissions( - rootfs.join(&relative), - fs::Permissions::from_mode(mode | 0o700), - )?; - } - } - #[cfg(unix)] - restore_directory_modes(rootfs, deferred_directory_modes)?; - Ok(()) -} - -#[cfg(unix)] -fn make_ancestor_directories_writable( - rootfs: &Path, - relative: &Path, - deferred_modes: &mut BTreeMap, -) -> anyhow::Result<()> { - use std::os::unix::fs::PermissionsExt; - - let mut relative_directory = PathBuf::new(); - for component in relative - .parent() - .unwrap_or_else(|| Path::new("")) - .components() - { - relative_directory.push(component.as_os_str()); - let directory = rootfs.join(&relative_directory); - let metadata = match fs::symlink_metadata(&directory) { - Ok(metadata) => metadata, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => break, - Err(error) => return Err(error.into()), - }; - anyhow::ensure!( - metadata.is_dir() && !metadata.file_type().is_symlink(), - "OCI layer entry traverses non-directory {}", - directory.display() - ); - let mode = metadata.permissions().mode() & 0o7777; - deferred_modes - .entry(relative_directory.clone()) - .or_insert(mode); - if mode & 0o700 != 0o700 { - fs::set_permissions(&directory, fs::Permissions::from_mode(mode | 0o700))?; - } - } - Ok(()) -} - -#[cfg(unix)] -fn restore_directory_modes( - rootfs: &Path, - deferred_modes: BTreeMap, -) -> anyhow::Result<()> { - use std::os::unix::fs::PermissionsExt; - - let mut directories = deferred_modes.into_iter().collect::>(); - directories.sort_by_key(|(path, _)| std::cmp::Reverse(path.components().count())); - for (relative, mode) in directories { - let directory = rootfs.join(relative); - match fs::symlink_metadata(&directory) { - Ok(metadata) if metadata.is_dir() && !metadata.file_type().is_symlink() => { - fs::set_permissions(directory, fs::Permissions::from_mode(mode))?; - } - Ok(_) => {} - Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} - Err(error) => return Err(error.into()), - } - } - Ok(()) -} - -enum Whiteout { - Remove(PathBuf), - Opaque(PathBuf), -} - -fn whiteout(path: &Path) -> Option { - let name = path.file_name()?.to_str()?; - if name == ".wh..wh..opq" { - return Some(Whiteout::Opaque( - path.parent().unwrap_or_else(|| Path::new("")).to_path_buf(), - )); - } - name.strip_prefix(".wh.") - .map(|target| Whiteout::Remove(path.parent().unwrap_or_else(|| Path::new("")).join(target))) -} - -fn clean_relative(path: &Path) -> anyhow::Result { - let mut clean = PathBuf::new(); - for component in path.components() { - match component { - Component::Normal(value) => clean.push(value), - Component::CurDir => {} - Component::ParentDir | Component::RootDir | Component::Prefix(_) => { - bail!("unsafe OCI layer path {}", path.display()) - } - } - } - Ok(clean) -} - -fn ensure_no_symlink_ancestors(root: &Path, relative: &Path) -> anyhow::Result<()> { - let mut current = root.to_path_buf(); - for component in relative.components() { - current.push(component.as_os_str()); - match fs::symlink_metadata(¤t) { - Ok(metadata) if metadata.file_type().is_symlink() => { - bail!("OCI whiteout traverses symlink {}", current.display()) - } - Ok(_) => {} - Err(error) if error.kind() == std::io::ErrorKind::NotFound => break, - Err(error) => return Err(error.into()), - } - } - Ok(()) -} - -fn remove_relative(root: &Path, relative: &Path) -> anyhow::Result<()> { - ensure_no_symlink_ancestors(root, relative.parent().unwrap_or_else(|| Path::new("")))?; - let target = root.join(relative); - match fs::symlink_metadata(&target) { - Ok(metadata) if metadata.is_dir() && !metadata.file_type().is_symlink() => { - fs::remove_dir_all(target)? - } - Ok(_) => fs::remove_file(target)?, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} - Err(error) => return Err(error.into()), - } - Ok(()) -} - -fn clear_relative_directory(root: &Path, relative: &Path) -> anyhow::Result<()> { - ensure_no_symlink_ancestors(root, relative)?; - let target = root.join(relative); - match fs::symlink_metadata(&target) { - Ok(metadata) if metadata.is_dir() && !metadata.file_type().is_symlink() => { - for entry in fs::read_dir(target)? { - let path = entry?.path(); - let metadata = fs::symlink_metadata(&path)?; - if metadata.is_dir() && !metadata.file_type().is_symlink() { - fs::remove_dir_all(path)?; - } else { - fs::remove_file(path)?; - } - } - } - Ok(_) => bail!( - "opaque OCI whiteout targets a non-directory: {}", - target.display() - ), - Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} - Err(error) => return Err(error.into()), - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parses_docker_style_references() { - assert_eq!( - ImageReference::parse("ubuntu").unwrap(), - ImageReference { - registry: "registry-1.docker.io".into(), - repository: "library/ubuntu".into(), - reference: "latest".into(), - } - ); - assert_eq!( - ImageReference::parse("ghcr.io/acme/agent:v1").unwrap(), - ImageReference { - registry: "ghcr.io".into(), - repository: "acme/agent".into(), - reference: "v1".into(), - } - ); - assert_eq!( - ImageReference::parse("docker.io/ubuntu:24.04") - .unwrap() - .registry, - "registry-1.docker.io" - ); - } - - #[test] - fn nullable_docker_runtime_fields_decode_as_empty() { - let config: ImageConfiguration = - serde_json::from_str(r#"{"config":{"Env":null,"Entrypoint":null,"Cmd":null}}"#) - .unwrap(); - assert!(config.config.env.is_none()); - assert!(config.config.entrypoint.is_none()); - assert!(config.config.cmd.is_none()); - } - - #[test] - fn bearer_challenge_is_parsed() { - let fields = parse_bearer_challenge( - r#"Bearer realm="https://auth.example/token",service="registry.example",scope="repository:acme/app:pull""#, - ) - .unwrap(); - assert_eq!(fields["service"], "registry.example"); - assert_eq!(fields["scope"], "repository:acme/app:pull"); - } - - #[test] - fn whiteouts_remove_previous_layer_entries() { - let root = tempfile::tempdir().unwrap(); - fs::create_dir_all(root.path().join("etc/sub")).unwrap(); - fs::write(root.path().join("etc/old"), b"old").unwrap(); - fs::write(root.path().join("etc/sub/old"), b"old").unwrap(); - remove_relative(root.path(), Path::new("etc/old")).unwrap(); - clear_relative_directory(root.path(), Path::new("etc/sub")).unwrap(); - assert!(!root.path().join("etc/old").exists()); - assert!( - fs::read_dir(root.path().join("etc/sub")) - .unwrap() - .next() - .is_none() - ); - } - - #[test] - fn applies_an_uncompressed_oci_layer() { - let root = tempfile::tempdir().unwrap(); - fs::create_dir(root.path().join("etc")).unwrap(); - fs::write(root.path().join("etc/old"), b"old").unwrap(); - let layer = tempfile::NamedTempFile::new().unwrap(); - { - let mut archive = tar::Builder::new(layer.reopen().unwrap()); - let mut whiteout = tar::Header::new_gnu(); - whiteout.set_mode(0o600); - whiteout.set_size(0); - whiteout.set_cksum(); - archive - .append_data(&mut whiteout, "etc/.wh.old", std::io::empty()) - .unwrap(); - let body = b"new"; - let mut file = tar::Header::new_gnu(); - file.set_mode(0o644); - file.set_size(body.len() as u64); - file.set_cksum(); - archive - .append_data(&mut file, "etc/new", body.as_slice()) - .unwrap(); - archive.finish().unwrap(); - } - apply_layer( - layer.path(), - "application/vnd.oci.image.layer.v1.tar", - root.path(), - ) - .unwrap(); - assert!(!root.path().join("etc/old").exists()); - assert_eq!(fs::read(root.path().join("etc/new")).unwrap(), b"new"); - } - - #[cfg(unix)] - #[test] - fn layer_can_populate_a_read_only_directory() { - use std::os::unix::fs::PermissionsExt; - - let root = tempfile::tempdir().unwrap(); - let layer = tempfile::NamedTempFile::new().unwrap(); - { - let mut archive = tar::Builder::new(layer.reopen().unwrap()); - let mut directory = tar::Header::new_gnu(); - directory.set_entry_type(tar::EntryType::Directory); - directory.set_mode(0o555); - directory.set_size(0); - directory.set_cksum(); - archive - .append_data(&mut directory, "certs", std::io::empty()) - .unwrap(); - - let body = b"certificate"; - let mut file = tar::Header::new_gnu(); - file.set_mode(0o444); - file.set_size(body.len() as u64); - file.set_cksum(); - archive - .append_data(&mut file, "certs/root.pem", body.as_slice()) - .unwrap(); - - let mut symlink = tar::Header::new_gnu(); - symlink.set_entry_type(tar::EntryType::Symlink); - symlink.set_mode(0o777); - symlink.set_size(0); - archive - .append_link(&mut symlink, "certs/hash.0", "root.pem") - .unwrap(); - archive.finish().unwrap(); - } - - apply_layer( - layer.path(), - "application/vnd.oci.image.layer.v1.tar", - root.path(), - ) - .unwrap(); - - assert_eq!( - fs::read_link(root.path().join("certs/hash.0")).unwrap(), - Path::new("root.pem") - ); - assert_eq!( - fs::metadata(root.path().join("certs")) - .unwrap() - .permissions() - .mode() - & 0o7777, - 0o555 - ); - } - - #[cfg(target_os = "linux")] - #[test] - fn rootless_layer_strips_setuid_and_setgid_bits() { - use std::os::unix::fs::PermissionsExt; - - let root = tempfile::tempdir().unwrap(); - let layer = tempfile::NamedTempFile::new().unwrap(); - { - let mut archive = tar::Builder::new(layer.reopen().unwrap()); - let body = b"executable"; - let mut file = tar::Header::new_gnu(); - file.set_mode(0o6755); - file.set_size(body.len() as u64); - file.set_cksum(); - archive - .append_data(&mut file, "bin/tool", body.as_slice()) - .unwrap(); - archive.finish().unwrap(); - } - - apply_layer( - layer.path(), - "application/vnd.oci.image.layer.v1.tar", - root.path(), - ) - .unwrap(); - - assert_eq!( - fs::metadata(root.path().join("bin/tool")) - .unwrap() - .permissions() - .mode() - & 0o7777, - 0o755 - ); - } - - #[test] - fn layer_paths_cannot_escape_root() { - assert!(clean_relative(Path::new("../../host")).is_err()); - assert_eq!( - clean_relative(Path::new("./etc/passwd")).unwrap(), - Path::new("etc/passwd") - ); - } -} diff --git a/crates/persisting-pvisor/src/process.rs b/crates/persisting-pvisor/src/process.rs deleted file mode 100644 index e2544fd5a..000000000 --- a/crates/persisting-pvisor/src/process.rs +++ /dev/null @@ -1,1370 +0,0 @@ -use crate::executor::{AttemptContext, RunExecutor}; -#[cfg(any(target_os = "linux", target_os = "macos"))] -use crate::sandbox::{INTERNAL_SANDBOX_ARG, NetworkIsolation}; -#[cfg(target_os = "macos")] -use crate::sandbox::{MACOS_SANDBOX_EXEC, SEATBELT_ATTESTATION, SeatbeltPlan, seatbelt_profile}; -#[cfg(target_os = "linux")] -use crate::sandbox::{ROOTLESS_ATTESTATION, SandboxPlan, landlock_runtime_available}; -use crate::sandbox::{SANDBOX_PLAN_ENV, SANDBOX_SETUP_FAILED_WARNING}; -use async_trait::async_trait; -use persisting_agentctl::{ - CapabilityDimension, CapabilityEnforcementEvidence, ExecutorDescriptor, ExecutorKind, - IsolationKind, ProcessInvocation, ProcessOutput, ResourceLimits, RunFailure, RunFailureKind, - RunInvocation, RunResult, RunSpec, RunState, StdioMode, -}; -#[cfg(any(target_os = "linux", target_os = "macos"))] -use persisting_agentctl::{FilesystemAccess, NetworkCapability}; -#[cfg(any(target_os = "linux", target_os = "macos"))] -use std::path::Path; -use std::path::PathBuf; -#[cfg(target_os = "linux")] -use std::process::Command as StdCommand; -use std::process::Stdio; -use tokio::io::{AsyncRead, AsyncReadExt}; -use tokio::process::{Child, Command}; - -#[cfg(target_os = "linux")] -struct ResourceCgroup { - path: PathBuf, -} - -#[cfg(target_os = "linux")] -fn validate_cgroup_relative_path(relative: &Path) -> std::io::Result<()> { - if relative - .components() - .all(|component| matches!(component, std::path::Component::Normal(_))) - { - Ok(()) - } else { - Err(std::io::Error::other("unsafe cgroup v2 membership path")) - } -} - -#[cfg(target_os = "linux")] -impl ResourceCgroup { - fn prepare(limits: &ResourceLimits) -> std::io::Result> { - if limits.memory_bytes.is_none() && limits.processes.is_none() { - return Ok(None); - } - let membership = std::fs::read_to_string("/proc/self/cgroup")?; - let relative = membership - .lines() - .find_map(|line| line.strip_prefix("0::")) - .ok_or_else(|| std::io::Error::other("unified cgroup v2 membership is unavailable"))?; - let relative = Path::new(relative.trim_start_matches('/')); - validate_cgroup_relative_path(relative)?; - let parent = Path::new("/sys/fs/cgroup").join(relative); - let path = parent.join(format!("persisting-{}", uuid::Uuid::new_v4().simple())); - std::fs::create_dir(&path)?; - let configure = (|| { - if let Some(bytes) = limits.memory_bytes { - std::fs::write(path.join("memory.max"), bytes.to_string())?; - } - if let Some(processes) = limits.processes { - std::fs::write(path.join("pids.max"), processes.to_string())?; - } - Ok::<_, std::io::Error>(()) - })(); - if let Err(error) = configure { - let _ = std::fs::remove_dir(&path); - return Err(error); - } - Ok(Some(Self { path })) - } - - fn install(&self, command: &mut Command) -> std::io::Result<()> { - use std::os::fd::AsRawFd; - use std::os::unix::process::CommandExt; - - let membership = std::fs::OpenOptions::new() - .write(true) - .open(self.path.join("cgroup.procs"))?; - // SAFETY: the pre-exec hook performs one async-signal-safe write to a - // cgroup.procs file opened by the parent. Writing `0` moves the calling - // child into the prepared cgroup before Agent code executes. - unsafe { - command.as_std_mut().pre_exec(move || { - let fd = membership.as_raw_fd(); - let moved = libc::write(fd, b"0".as_ptr().cast(), 1); - if moved == 1 { - Ok(()) - } else { - Err(std::io::Error::last_os_error()) - } - }); - } - Ok(()) - } -} - -#[cfg(target_os = "linux")] -impl Drop for ResourceCgroup { - fn drop(&mut self) { - let _ = std::fs::remove_dir(&self.path); - } -} - -#[cfg(unix)] -struct ForegroundProcessGroup { - terminal_fd: libc::c_int, - original_pgrp: libc::pid_t, -} - -#[cfg(unix)] -impl ForegroundProcessGroup { - fn give_to(child: &Child, invocation: &ProcessInvocation) -> std::io::Result> { - if invocation.stdin != StdioMode::Inherit - || unsafe { libc::isatty(libc::STDIN_FILENO) } != 1 - { - return Ok(None); - } - let Some(pid) = child.id() else { - return Ok(None); - }; - let terminal_fd = libc::STDIN_FILENO; - let original_pgrp = unsafe { libc::tcgetpgrp(terminal_fd) }; - if original_pgrp < 0 { - return Err(std::io::Error::last_os_error()); - } - set_terminal_pgrp(terminal_fd, pid as libc::pid_t)?; - // The child may have attempted a terminal read between spawn and - // tcsetpgrp and received SIGTTIN. Resume its whole process group. - unsafe { - libc::kill(-(pid as libc::pid_t), libc::SIGCONT); - } - Ok(Some(Self { - terminal_fd, - original_pgrp, - })) - } -} - -#[cfg(unix)] -impl Drop for ForegroundProcessGroup { - fn drop(&mut self) { - let _ = set_terminal_pgrp(self.terminal_fd, self.original_pgrp); - } -} - -/// Change the terminal foreground group without letting a background caller -/// stop itself with SIGTTOU. Signal masking is thread-local and restored before -/// returning, so it is safe inside the multi-threaded Tokio runtime. -#[cfg(unix)] -fn set_terminal_pgrp(fd: libc::c_int, pgrp: libc::pid_t) -> std::io::Result<()> { - unsafe { - let mut blocked: libc::sigset_t = std::mem::zeroed(); - let mut previous: libc::sigset_t = std::mem::zeroed(); - libc::sigemptyset(&mut blocked); - libc::sigaddset(&mut blocked, libc::SIGTTOU); - let mask_error = libc::pthread_sigmask(libc::SIG_BLOCK, &blocked, &mut previous); - if mask_error != 0 { - return Err(std::io::Error::from_raw_os_error(mask_error)); - } - let result = libc::tcsetpgrp(fd, pgrp); - let error = (result != 0).then(std::io::Error::last_os_error); - libc::pthread_sigmask(libc::SIG_SETMASK, &previous, std::ptr::null_mut()); - match error { - Some(error) => Err(error), - None => Ok(()), - } - } -} - -#[derive(Debug, Clone, Default)] -pub struct ProcessExecutor { - /// `Some` selects the platform sandbox launcher. The normal library - /// default intentionally remains the compatibility host process. - sandbox_launcher: Option, -} - -struct PreparedCommand { - command: Command, - resources: SandboxResources, -} - -enum SandboxResources { - None, - #[cfg(target_os = "linux")] - Linux { - root: PathBuf, - attestation: tempfile::NamedTempFile, - }, - #[cfg(target_os = "macos")] - MacOS { - scratch: tempfile::TempDir, - attestation: tempfile::NamedTempFile, - }, -} - -impl SandboxResources { - fn none() -> Self { - Self::None - } - - #[cfg(target_os = "linux")] - fn create() -> std::io::Result { - use std::os::unix::fs::PermissionsExt; - - let path = std::env::temp_dir().join(format!( - ".pvisor-rootfs-{}-{}", - std::process::id(), - uuid::Uuid::new_v4() - )); - std::fs::create_dir(&path)?; - std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700))?; - let attestation = tempfile::Builder::new() - .prefix("pvisor-rootless-attestation-") - .tempfile()?; - Ok(Self::Linux { - root: path, - attestation, - }) - } - - #[cfg(target_os = "linux")] - fn path(&self) -> Option<&std::path::Path> { - match self { - Self::Linux { root, .. } => Some(root), - Self::None => None, - } - } - - #[cfg(target_os = "linux")] - fn attestation_path(&self) -> Option<&Path> { - match self { - Self::Linux { attestation, .. } => Some(attestation.path()), - Self::None => None, - } - } - - #[cfg(target_os = "macos")] - fn create() -> std::io::Result { - let scratch = tempfile::Builder::new() - .prefix("pvisor-seatbelt-scratch-") - .tempdir()?; - let attestation = tempfile::Builder::new() - .prefix("pvisor-seatbelt-attestation-") - .tempfile()?; - Ok(Self::MacOS { - scratch, - attestation, - }) - } - - #[cfg(target_os = "macos")] - fn scratch_path(&self) -> Option<&Path> { - match self { - Self::MacOS { scratch, .. } => Some(scratch.path()), - Self::None => None, - } - } - - #[cfg(target_os = "macos")] - fn attestation_path(&self) -> Option<&Path> { - match self { - Self::MacOS { attestation, .. } => Some(attestation.path()), - Self::None => None, - } - } - - #[cfg(target_os = "macos")] - fn setup_attested(&mut self) -> bool { - use std::io::{Read, Seek}; - - let Self::MacOS { attestation, .. } = self else { - return true; - }; - let file = attestation.as_file_mut(); - if file.rewind().is_err() { - return false; - } - let mut contents = Vec::new(); - file.read_to_end(&mut contents).is_ok() && contents == SEATBELT_ATTESTATION - } - - #[cfg(target_os = "linux")] - fn setup_attested(&mut self) -> bool { - use std::io::{Read, Seek}; - - let Self::Linux { attestation, .. } = self else { - return true; - }; - let file = attestation.as_file_mut(); - if file.rewind().is_err() { - return false; - } - let mut contents = Vec::new(); - file.read_to_end(&mut contents).is_ok() && contents == ROOTLESS_ATTESTATION - } - - #[cfg(not(any(target_os = "linux", target_os = "macos")))] - fn setup_attested(&mut self) -> bool { - true - } -} - -impl Drop for SandboxResources { - fn drop(&mut self) { - #[cfg(target_os = "linux")] - if let Self::Linux { root: path, .. } = self { - // Never recurse over a security-sensitive path. A successful - // launcher leaves an empty mountpoint; a non-empty directory is - // retained for diagnosis instead of being removed destructively. - let _ = std::fs::remove_dir(path); - } - } -} - -#[derive(Debug)] -struct Captured { - text: String, - truncated: bool, -} - -async fn read_limited( - mut reader: R, - limit: usize, -) -> std::io::Result { - let mut retained = Vec::with_capacity(limit.min(8192)); - let mut buf = [0_u8; 8192]; - let mut truncated = false; - loop { - let read = reader.read(&mut buf).await?; - if read == 0 { - break; - } - let remaining = limit.saturating_sub(retained.len()); - let keep = remaining.min(read); - retained.extend_from_slice(&buf[..keep]); - truncated |= keep < read; - } - Ok(Captured { - text: String::from_utf8_lossy(&retained).into_owned(), - truncated, - }) -} - -fn stdio(mode: StdioMode) -> Stdio { - match mode { - StdioMode::Inherit => Stdio::inherit(), - StdioMode::Capture => Stdio::piped(), - StdioMode::Null => Stdio::null(), - } -} - -#[cfg(any(target_os = "linux", target_os = "macos"))] -fn network_isolation(spec: &RunSpec) -> NetworkIsolation { - if matches!(spec.capabilities.network, NetworkCapability::Deny) { - NetworkIsolation::LoopbackOnly - } else { - NetworkIsolation::Ambient - } -} - -fn resolve_host_program(program: &str) -> std::path::PathBuf { - if program.contains(std::path::MAIN_SEPARATOR) { - return program.into(); - } - let Some(path) = std::env::var_os("PATH") else { - return program.into(); - }; - std::env::split_paths(&path) - .map(|directory| directory.join(program)) - .find(|candidate| is_executable(candidate)) - .unwrap_or_else(|| program.into()) -} - -#[cfg(unix)] -fn is_executable(path: &std::path::Path) -> bool { - use std::os::unix::fs::PermissionsExt; - path.metadata() - .is_ok_and(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0) -} - -#[cfg(not(unix))] -fn is_executable(path: &std::path::Path) -> bool { - path.is_file() -} - -impl ProcessExecutor { - /// Build a Linux rootless executor using `launcher` for the trusted - /// namespace/Landlock setup stage. - /// - /// The launcher must dispatch [`crate::sandbox::run_internal_if_requested`] - /// before starting threads or an async runtime. The `pvisor` binary is the - /// canonical launcher and uses this path automatically for default host Runs. - #[cfg(target_os = "linux")] - pub fn rootless_with_launcher(launcher: impl Into) -> std::io::Result { - let launcher = launcher.into().canonicalize()?; - if !is_executable(&launcher) { - return Err(std::io::Error::new( - std::io::ErrorKind::PermissionDenied, - format!( - "rootless sandbox launcher is not executable: {}", - launcher.display() - ), - )); - } - Ok(Self { - sandbox_launcher: Some(launcher), - }) - } - - #[cfg(not(target_os = "linux"))] - pub fn rootless_with_launcher(launcher: impl Into) -> std::io::Result { - let _ = launcher.into(); - Err(std::io::Error::new( - std::io::ErrorKind::Unsupported, - "the rootless local process executor is only available on Linux", - )) - } - - /// Build a macOS executor that installs a generated Seatbelt profile - /// before entering the hidden launcher and executing Agent code. - #[cfg(target_os = "macos")] - pub fn seatbelt_with_launcher(launcher: impl Into) -> std::io::Result { - let launcher = launcher.into().canonicalize()?; - if !is_executable(&launcher) { - return Err(std::io::Error::new( - std::io::ErrorKind::PermissionDenied, - format!( - "Seatbelt sandbox launcher is not executable: {}", - launcher.display() - ), - )); - } - if !is_executable(Path::new(MACOS_SANDBOX_EXEC)) { - return Err(std::io::Error::new( - std::io::ErrorKind::NotFound, - format!("required Seatbelt launcher is unavailable: {MACOS_SANDBOX_EXEC}"), - )); - } - Ok(Self { - sandbox_launcher: Some(launcher), - }) - } - - #[cfg(not(target_os = "macos"))] - pub fn seatbelt_with_launcher(launcher: impl Into) -> std::io::Result { - let _ = launcher.into(); - Err(std::io::Error::new( - std::io::ErrorKind::Unsupported, - "the Seatbelt local process sandbox is only available on macOS", - )) - } - - pub fn is_rootless(&self) -> bool { - cfg!(target_os = "linux") && self.sandbox_launcher.is_some() - } - - pub fn is_seatbelt(&self) -> bool { - cfg!(target_os = "macos") && self.sandbox_launcher.is_some() - } - - pub fn is_sandboxed(&self) -> bool { - self.sandbox_launcher.is_some() - } - - fn spawn_command( - &self, - spec: &RunSpec, - invocation: &ProcessInvocation, - ) -> std::io::Result { - // Resolve a bare command against the host PATH before changing cwd to - // an OverlayFS merged root. The executable belongs to the host-process - // executor and need not exist inside the projected lower filesystem. - let program = resolve_host_program(&invocation.program); - let (mut command, sandbox_plan, resources) = if let Some(launcher) = &self.sandbox_launcher - { - platform_launcher_command(launcher, spec, invocation, &program)? - } else { - let mut command = Command::new(program); - command.args(&invocation.args); - (command, None, SandboxResources::none()) - }; - command - .stdin(stdio(invocation.stdin)) - .stdout(stdio(invocation.stdout)) - .stderr(stdio(invocation.stderr)) - .kill_on_drop(true); - #[cfg(unix)] - { - use std::os::unix::process::CommandExt; - command.as_std_mut().process_group(0); - let mut limits = spec.runtime.resource_limits.clone(); - // RLIMIT_NPROC must be applied after the rootless launcher has - // created its private PID namespace and reaper. Applying it to - // the launcher itself can make setup fail with EAGAIN when the - // host user already has more processes than the requested cap. - if sandbox_plan.is_some() { - limits.processes = None; - } - install_resource_limit_hook(&mut command, limits); - } - if let Some(cwd) = &invocation.cwd { - command.current_dir(cwd); - } - if !invocation.inherit_env { - command.env_clear(); - } - command.envs(&invocation.env); - // This is a reserved supervisor-to-launcher capability. Apply it last - // so an untrusted Run environment cannot remove or replace the policy. - if let Some(sandbox_plan) = sandbox_plan { - command.env(SANDBOX_PLAN_ENV, sandbox_plan); - } - #[cfg(target_os = "macos")] - if let Some(scratch) = resources.scratch_path() { - // A Run-owned temporary directory avoids granting the Agent the - // shared /tmp or per-user Darwin temporary hierarchy. - command.env("TMPDIR", scratch); - } - Ok(PreparedCommand { command, resources }) - } -} - -/// Probe the namespace primitives used by the default Linux launcher without -/// mutating the pVisor process itself. A short-lived `unshare` child keeps the -/// probe safe in a multithreaded Tokio process and distinguishes an unavailable -/// host capability from a later Agent failure. -#[cfg(target_os = "linux")] -pub(crate) fn rootless_runtime_available() -> bool { - landlock_runtime_available() - && StdCommand::new("unshare") - .args(["--user", "--mount", "--pid", "--fork", "true"]) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .is_ok_and(|status| status.success()) -} - -#[cfg(unix)] -fn install_resource_limit_hook(command: &mut Command, limits: ResourceLimits) { - if limits.is_empty() { - return; - } - use std::os::unix::process::CommandExt; - // SAFETY: the hook only invokes async-signal-safe getrlimit/setrlimit calls - // and does not allocate or acquire locks between fork and exec. - unsafe { - command - .as_std_mut() - .pre_exec(move || apply_resource_limits(&limits)); - } -} - -#[cfg(unix)] -fn apply_resource_limits(limits: &ResourceLimits) -> std::io::Result<()> { - macro_rules! set_limit { - ($resource:expr, $value:expr) => {{ - let mut current = libc::rlimit { - rlim_cur: 0, - rlim_max: 0, - }; - if unsafe { libc::getrlimit($resource, &mut current) } != 0 { - return Err(std::io::Error::last_os_error()); - } - let requested = $value as libc::rlim_t; - let effective = requested.min(current.rlim_max); - let limit = libc::rlimit { - rlim_cur: effective, - rlim_max: effective, - }; - if unsafe { libc::setrlimit($resource, &limit) } != 0 { - return Err(std::io::Error::last_os_error()); - } - }}; - } - - #[cfg(not(target_os = "macos"))] - if let Some(bytes) = limits.memory_bytes { - set_limit!(libc::RLIMIT_AS, bytes); - } - if let Some(processes) = limits.processes { - set_limit!(libc::RLIMIT_NPROC, processes); - } - if let Some(milliseconds) = limits.cpu_time_ms { - let seconds = milliseconds - .saturating_add(999) - .checked_div(1_000) - .unwrap_or(0) - .max(1); - set_limit!(libc::RLIMIT_CPU, seconds); - } - if let Some(open_files) = limits.open_files { - set_limit!(libc::RLIMIT_NOFILE, open_files); - } - if let Some(bytes) = limits.file_size_bytes { - set_limit!(libc::RLIMIT_FSIZE, bytes); - } - Ok(()) -} - -#[cfg(target_os = "linux")] -fn platform_launcher_command( - launcher: &Path, - spec: &RunSpec, - invocation: &ProcessInvocation, - program: &Path, -) -> std::io::Result<(Command, Option, SandboxResources)> { - let program = program.canonicalize().map_err(|error| { - std::io::Error::new( - error.kind(), - format!("resolve Agent executable {}: {error}", program.display()), - ) - })?; - let sandbox_root = SandboxResources::create()?; - let network = network_isolation(spec); - let plan = rootless_plan( - spec, - invocation, - &program, - sandbox_root - .path() - .expect("created sandbox root") - .to_owned(), - sandbox_root - .attestation_path() - .expect("created rootless attestation") - .to_owned(), - network, - )?; - let encoded = serde_json::to_string(&plan).map_err(std::io::Error::other)?; - let mut command = Command::new(launcher); - command - .arg(INTERNAL_SANDBOX_ARG) - .arg("--") - .arg(&program) - .args(&invocation.args); - Ok((command, Some(encoded), sandbox_root)) -} - -#[cfg(target_os = "macos")] -fn platform_launcher_command( - launcher: &Path, - spec: &RunSpec, - invocation: &ProcessInvocation, - program: &Path, -) -> std::io::Result<(Command, Option, SandboxResources)> { - let program = program.canonicalize().map_err(|error| { - std::io::Error::new( - error.kind(), - format!("resolve Agent executable {}: {error}", program.display()), - ) - })?; - let resources = SandboxResources::create()?; - let cwd = invocation - .cwd - .as_deref() - .map(PathBuf::from) - .unwrap_or(std::env::current_dir()?); - let cwd = cwd.canonicalize()?; - let mut writable_paths = vec![ - cwd.clone(), - resources - .scratch_path() - .expect("created Seatbelt scratch directory") - .to_owned(), - resources - .attestation_path() - .expect("created Seatbelt attestation") - .to_owned(), - ]; - for path in ["/dev/null", "/dev/zero", "/dev/tty", "/dev/fd"] { - push_existing(&mut writable_paths, Path::new(path)); - } - for capability in &spec.capabilities.filesystem { - if capability.access != FilesystemAccess::ReadWrite { - continue; - } - let path = PathBuf::from(&capability.path); - let path = if path.is_absolute() { - path - } else { - cwd.join(path) - }; - if !path.exists() { - return Err(std::io::Error::new( - std::io::ErrorKind::NotFound, - format!( - "filesystem capability path does not exist: {}", - path.display() - ), - )); - } - writable_paths.push(path); - } - - let network = network_isolation(spec); - let (allowed_unix_sockets, local_socket_roots) = if network.is_loopback_only() { - ( - invocation - .env - .get(crate::AGENTCTL_ENDPOINT_ENV) - .map(PathBuf::from) - .filter(|path| path.exists()) - .into_iter() - .collect::>(), - vec![ - cwd, - resources - .scratch_path() - .expect("created Seatbelt scratch directory") - .to_owned(), - ], - ) - } else { - (Vec::new(), Vec::new()) - }; - let (profile, parameters) = seatbelt_profile( - &writable_paths, - &allowed_unix_sockets, - &local_socket_roots, - network, - )?; - let plan = SeatbeltPlan { - attestation: resources - .attestation_path() - .expect("created Seatbelt attestation") - .to_owned(), - network, - }; - let encoded = serde_json::to_string(&plan).map_err(std::io::Error::other)?; - - let mut command = Command::new(MACOS_SANDBOX_EXEC); - command.arg("-p").arg(profile); - for (key, path) in parameters { - let path = path.to_str().ok_or_else(|| { - std::io::Error::new( - std::io::ErrorKind::InvalidInput, - format!( - "Seatbelt parameter path is not valid UTF-8: {}", - path.display() - ), - ) - })?; - command.arg(format!("-D{key}={path}")); - } - command - .arg("--") - .arg(launcher) - .arg(INTERNAL_SANDBOX_ARG) - .arg("--") - .arg(program) - .args(&invocation.args); - Ok((command, Some(encoded), resources)) -} - -#[cfg(not(any(target_os = "linux", target_os = "macos")))] -fn platform_launcher_command( - _launcher: &std::path::Path, - _spec: &RunSpec, - _invocation: &ProcessInvocation, - _program: &std::path::Path, -) -> std::io::Result<(Command, Option, SandboxResources)> { - Err(std::io::Error::new( - std::io::ErrorKind::Unsupported, - "the local process sandbox is not available on this platform", - )) -} - -#[cfg(target_os = "linux")] -fn rootless_plan( - spec: &RunSpec, - invocation: &ProcessInvocation, - program: &Path, - root: PathBuf, - attestation: PathBuf, - network: NetworkIsolation, -) -> std::io::Result { - let cwd = invocation - .cwd - .as_deref() - .map(PathBuf::from) - .unwrap_or(std::env::current_dir()?); - let cwd = cwd.canonicalize()?; - let mut read_only = Vec::new(); - let mut read_write = vec![cwd.clone()]; - - // A broad but immutable OS runtime keeps arbitrary local executables and - // dynamic language runtimes working while excluding user data by default. - for path in ["/bin", "/sbin", "/usr", "/lib", "/lib64", "/etc"] { - let path = PathBuf::from(path); - if path.exists() { - // Preserve compatibility aliases such as /bin and /lib64 inside - // the synthetic root. Canonicalizing them would project only - // /usr/bin or /usr/lib and break ELF interpreter paths. - read_only.push(path); - } - } - // On systemd-resolved hosts this follows /etc/resolv.conf into /run, - // whose containing hierarchy is intentionally not otherwise projected. - push_existing(&mut read_only, Path::new("/etc/resolv.conf")); - read_only.push(program.to_path_buf()); - for path in [ - "/dev/null", - "/dev/zero", - "/dev/full", - "/dev/random", - "/dev/urandom", - "/dev/tty", - ] { - push_existing(&mut read_write, Path::new(path)); - } - - // The Run-scoped AgentCtl and an explicitly supplied SSH agent are - // capabilities represented by their exact socket inode, not by /tmp. - // Merely inheriting the host environment must not project signing - // authority into a safe Run. - for key in [crate::AGENTCTL_ENDPOINT_ENV, "SSH_AUTH_SOCK"] { - if let Some(path) = invocation.env.get(key) { - push_existing(&mut read_write, Path::new(path)); - } - } - - for capability in &spec.capabilities.filesystem { - let path = PathBuf::from(&capability.path); - let path = if path.is_absolute() { - path - } else { - cwd.join(path) - }; - if !path.exists() { - return Err(std::io::Error::new( - std::io::ErrorKind::NotFound, - format!( - "filesystem capability path does not exist: {}", - path.display() - ), - )); - } - match capability.access { - FilesystemAccess::Read => push_existing(&mut read_only, &path), - FilesystemAccess::ReadWrite => push_existing(&mut read_write, &path), - } - } - - read_only.sort_unstable(); - read_only.dedup(); - read_write.sort_unstable(); - read_write.dedup(); - Ok(SandboxPlan { - root, - cwd, - attestation, - read_only, - read_write, - network, - process_limit: spec.runtime.resource_limits.processes, - }) -} - -#[cfg(any(target_os = "linux", target_os = "macos"))] -fn push_existing(paths: &mut Vec, path: &Path) { - if let Ok(path) = path.canonicalize() { - paths.push(path); - } -} - -async fn terminate_process_tree(child: &mut Child, grace_ms: u64) { - #[cfg(unix)] - { - if let Some(pid) = child.id() { - // The child is the leader of the process group configured above. - let process_group = -(pid as i32); - unsafe { - libc::kill(process_group, libc::SIGTERM); - } - if tokio::time::timeout(std::time::Duration::from_millis(grace_ms), child.wait()) - .await - .is_ok() - { - return; - } - unsafe { - libc::kill(process_group, libc::SIGKILL); - } - let _ = child.wait().await; - return; - } - } - - let _ = child.kill().await; - let _ = child.wait().await; -} - -#[async_trait] -impl RunExecutor for ProcessExecutor { - fn descriptor(&self) -> ExecutorDescriptor { - let (name, isolation) = if self.is_rootless() { - ("local-rootless-v1", IsolationKind::RootlessProcess) - } else if self.is_seatbelt() { - ("local-seatbelt-v1", IsolationKind::SandboxedProcess) - } else { - ("local-process-v1", IsolationKind::HostProcess) - }; - let mut capability_enforcement = CapabilityEnforcementEvidence::default() - .enforced(CapabilityDimension::Resources, "posix-rlimit"); - if self.is_rootless() { - capability_enforcement = capability_enforcement - .enforced( - CapabilityDimension::FilesystemRead, - "linux-synthetic-root-landlock", - ) - .enforced( - CapabilityDimension::FilesystemWrite, - "linux-synthetic-root-landlock", - ); - } else if self.is_seatbelt() { - capability_enforcement = capability_enforcement.enforced( - CapabilityDimension::FilesystemWrite, - "macos-seatbelt-write-policy", - ); - } - ExecutorDescriptor { - name: name.into(), - kind: ExecutorKind::Process, - isolation, - capability_enforcement, - supports_checkpoint: false, - supports_migration: false, - } - } - - fn supports(&self, invocation: &RunInvocation) -> bool { - matches!(invocation, RunInvocation::Process(_)) - } - - async fn execute(&self, context: AttemptContext) -> RunResult { - let spec = context.spec().clone(); - let RunInvocation::Process(invocation) = &spec.invocation; - let started_at = crate::util::unix_now_ms(); - context - .transition(RunState::Starting, Some("spawning local process".into())) - .await; - - let PreparedCommand { - mut command, - mut resources, - } = match self.spawn_command(&spec, invocation) { - Ok(command) => command, - Err(error) => { - return RunResult { - run_id: spec.run_id, - attempt_id: context.attempt_id().clone(), - lease_epoch: spec.lease_epoch, - state: RunState::Failed, - started_at_unix_ms: started_at, - finished_at_unix_ms: crate::util::unix_now_ms(), - exit_code: None, - failure: Some(RunFailure { - kind: RunFailureKind::Spawn, - message: error.to_string(), - retryable: false, - }), - output: ProcessOutput::default(), - value: None, - metrics: Default::default(), - artifacts: Vec::new(), - event_stream_ref: None, - warnings: Vec::new(), - }; - } - }; - let mut warnings = Vec::new(); - #[cfg(target_os = "linux")] - let mut metrics = std::collections::BTreeMap::new(); - #[cfg(not(target_os = "linux"))] - let metrics = std::collections::BTreeMap::new(); - #[cfg(target_os = "linux")] - let _resource_cgroup = match ResourceCgroup::prepare(&spec.runtime.resource_limits) { - Ok(Some(cgroup)) => match cgroup.install(&mut command) { - Ok(()) => { - metrics.insert("resource.cgroup_v2".into(), 1.0); - Some(cgroup) - } - Err(error) => { - warnings.push(format!( - "cgroup v2 resource controller unavailable; using inherited rlimits: {error}" - )); - None - } - }, - Ok(None) => None, - Err(error) => { - warnings.push(format!( - "cgroup v2 resource controller unavailable; using inherited rlimits: {error}" - )); - None - } - }; - let mut child = match command.spawn() { - Ok(child) => child, - Err(error) => { - return RunResult { - run_id: spec.run_id, - attempt_id: context.attempt_id().clone(), - lease_epoch: spec.lease_epoch, - state: RunState::Failed, - started_at_unix_ms: started_at, - finished_at_unix_ms: crate::util::unix_now_ms(), - exit_code: None, - failure: Some(RunFailure { - kind: RunFailureKind::Spawn, - message: error.to_string(), - retryable: false, - }), - output: ProcessOutput::default(), - value: None, - metrics: Default::default(), - artifacts: Vec::new(), - event_stream_ref: None, - warnings: Vec::new(), - }; - } - }; - #[cfg(unix)] - let _foreground = match ForegroundProcessGroup::give_to(&child, invocation) { - Ok(foreground) => foreground, - Err(error) => { - terminate_process_tree(&mut child, spec.runtime.termination_grace_ms).await; - return RunResult { - run_id: spec.run_id, - attempt_id: context.attempt_id().clone(), - lease_epoch: spec.lease_epoch, - state: RunState::Failed, - started_at_unix_ms: started_at, - finished_at_unix_ms: crate::util::unix_now_ms(), - exit_code: None, - failure: Some(RunFailure { - kind: RunFailureKind::Infrastructure, - message: format!("failed to give terminal to child process: {error}"), - retryable: false, - }), - output: ProcessOutput::default(), - value: None, - metrics: Default::default(), - artifacts: Vec::new(), - event_stream_ref: None, - warnings: Vec::new(), - }; - } - }; - - let stdout_task = child.stdout.take().map(|stdout| { - let limit = spec.runtime.max_output_bytes; - tokio::spawn(async move { read_limited(stdout, limit).await }) - }); - let stderr_task = child.stderr.take().map(|stderr| { - let limit = spec.runtime.max_output_bytes; - tokio::spawn(async move { read_limited(stderr, limit).await }) - }); - - context.transition(RunState::Running, None).await; - - enum End { - Exited(std::io::Result), - Cancelled, - Deadline, - } - - let cancellation = context.cancellation(); - let end = if let Some(timeout_ms) = spec.runtime.timeout_ms { - tokio::select! { - biased; - status = child.wait() => End::Exited(status), - _ = cancellation.cancelled() => End::Cancelled, - _ = tokio::time::sleep(std::time::Duration::from_millis(timeout_ms)) => End::Deadline, - } - } else { - tokio::select! { - biased; - status = child.wait() => End::Exited(status), - _ = cancellation.cancelled() => End::Cancelled, - } - }; - - if matches!(end, End::Cancelled | End::Deadline) { - if matches!(end, End::Cancelled) { - context - .transition(RunState::Cancelling, Some("cancellation requested".into())) - .await; - } - terminate_process_tree(&mut child, spec.runtime.termination_grace_ms).await; - } - - let mut output = ProcessOutput::default(); - if let Some(task) = stdout_task - && let Ok(Ok(captured)) = task.await - { - output.stdout = Some(captured.text); - output.stdout_truncated = captured.truncated; - } - if let Some(task) = stderr_task - && let Ok(Ok(captured)) = task.await - { - output.stderr = Some(captured.text); - output.stderr_truncated = captured.truncated; - } - - let finished_at = crate::util::unix_now_ms(); - let sandbox_attested = resources.setup_attested(); - let sandbox_setup_failed = self.is_sandboxed() && !sandbox_attested; - let (state, exit_code, failure) = if sandbox_setup_failed { - ( - RunState::Failed, - None, - Some(RunFailure { - kind: RunFailureKind::Infrastructure, - message: "local sandbox setup failed before Agent execution".into(), - retryable: false, - }), - ) - } else { - match end { - End::Exited(Ok(status)) if status.success() => { - (RunState::Completed, status.code(), None) - } - End::Exited(Ok(status)) => ( - RunState::Failed, - status.code(), - Some(RunFailure { - kind: RunFailureKind::ProcessExit, - message: match status.code() { - Some(code) => format!("process exited with code {code}"), - None => "process terminated without an exit code".into(), - }, - retryable: false, - }), - ), - End::Exited(Err(error)) => ( - RunState::Failed, - None, - Some(RunFailure { - kind: RunFailureKind::Infrastructure, - message: error.to_string(), - retryable: true, - }), - ), - End::Cancelled => (RunState::Cancelled, None, None), - End::Deadline => ( - RunState::Failed, - None, - Some(RunFailure { - kind: RunFailureKind::DeadlineExceeded, - message: format!( - "attempt exceeded {} ms deadline", - spec.runtime.timeout_ms.unwrap_or_default() - ), - retryable: false, - }), - ), - } - }; - - RunResult { - run_id: spec.run_id, - attempt_id: context.attempt_id().clone(), - lease_epoch: spec.lease_epoch, - state, - started_at_unix_ms: started_at, - finished_at_unix_ms: finished_at, - exit_code, - failure, - output, - value: None, - metrics, - artifacts: Vec::new(), - event_stream_ref: None, - warnings: { - if sandbox_setup_failed { - warnings.push(SANDBOX_SETUP_FAILED_WARNING.into()); - } - warnings - }, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[cfg(target_os = "linux")] - #[test] - fn cgroup_membership_path_rejects_non_normal_components() { - assert!(validate_cgroup_relative_path(Path::new("user.slice/session.scope")).is_ok()); - assert!(validate_cgroup_relative_path(Path::new("")).is_ok()); - assert!(validate_cgroup_relative_path(Path::new("../escape")).is_err()); - assert!(validate_cgroup_relative_path(Path::new("/absolute")).is_err()); - } - - #[cfg(unix)] - #[tokio::test] - async fn child_process_receives_requested_open_file_limit() { - let mut command = Command::new("/bin/sh"); - command.args(["-c", "ulimit -n"]); - command.stdout(Stdio::piped()); - install_resource_limit_hook( - &mut command, - ResourceLimits { - open_files: Some(32), - ..ResourceLimits::default() - }, - ); - let output = command.output().await.unwrap(); - assert!(output.status.success()); - assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "32"); - } - - #[cfg(target_os = "linux")] - #[test] - fn rootless_executor_reports_an_honest_partial_boundary() { - let executor = - ProcessExecutor::rootless_with_launcher(std::env::current_exe().unwrap()).unwrap(); - let descriptor = executor.descriptor(); - assert_eq!(descriptor.name, "local-rootless-v1"); - assert_eq!(descriptor.isolation, IsolationKind::RootlessProcess); - assert!( - descriptor - .capability_enforcement - .is_enforced(CapabilityDimension::FilesystemRead) - ); - assert!( - descriptor - .capability_enforcement - .is_enforced(CapabilityDimension::FilesystemWrite) - ); - assert!( - !descriptor - .capability_enforcement - .is_enforced(CapabilityDimension::Network) - ); - } - - #[cfg(target_os = "macos")] - #[test] - fn seatbelt_executor_reports_write_confinement_without_overclaiming_capabilities() { - let executor = - ProcessExecutor::seatbelt_with_launcher(std::env::current_exe().unwrap()).unwrap(); - let descriptor = executor.descriptor(); - assert_eq!(descriptor.name, "local-seatbelt-v1"); - assert_eq!(descriptor.isolation, IsolationKind::SandboxedProcess); - assert!( - !descriptor - .capability_enforcement - .is_enforced(CapabilityDimension::FilesystemRead) - ); - assert!( - descriptor - .capability_enforcement - .is_enforced(CapabilityDimension::FilesystemWrite) - ); - } - - #[cfg(target_os = "macos")] - #[test] - fn seatbelt_plan_and_scratch_override_an_untrusted_environment() { - let temporary = tempfile::tempdir().unwrap(); - let mut spec = RunSpec::process("run", "agent", "/usr/bin/true"); - { - let RunInvocation::Process(invocation) = &mut spec.invocation; - invocation.cwd = Some(temporary.path().display().to_string()); - invocation.inherit_env = false; - invocation - .env - .insert(SANDBOX_PLAN_ENV.into(), r#"{"attestation":"/"}"#.into()); - invocation.env.insert("TMPDIR".into(), "/".into()); - } - - let executor = - ProcessExecutor::seatbelt_with_launcher(std::env::current_exe().unwrap()).unwrap(); - let RunInvocation::Process(invocation) = &spec.invocation; - let prepared = executor.spawn_command(&spec, invocation).unwrap(); - let environment = prepared - .command - .as_std() - .get_envs() - .map(|(key, value)| { - ( - key.to_string_lossy().into_owned(), - value.unwrap().to_string_lossy().into_owned(), - ) - }) - .collect::>(); - let plan: SeatbeltPlan = - serde_json::from_str(environment.get(SANDBOX_PLAN_ENV).unwrap()).unwrap(); - assert_ne!(plan.attestation, PathBuf::from("/")); - assert_ne!(environment.get("TMPDIR").map(String::as_str), Some("/")); - assert!(prepared.resources.scratch_path().unwrap().is_dir()); - } - - #[cfg(target_os = "linux")] - #[test] - fn rootless_plan_is_reserved_even_when_the_run_clears_or_poisons_its_environment() { - let temporary = tempfile::tempdir().unwrap(); - let mut spec = RunSpec::process("run", "agent", "/bin/true"); - { - let RunInvocation::Process(invocation) = &mut spec.invocation; - invocation.cwd = Some(temporary.path().display().to_string()); - invocation.inherit_env = false; - invocation - .env - .insert(SANDBOX_PLAN_ENV.into(), r#"{"read_write":["/"]}"#.into()); - } - - let executor = - ProcessExecutor::rootless_with_launcher(std::env::current_exe().unwrap()).unwrap(); - let RunInvocation::Process(invocation) = &spec.invocation; - let command = executor.spawn_command(&spec, invocation).unwrap(); - let encoded = command - .command - .as_std() - .get_envs() - .find_map(|(key, value)| { - (key == SANDBOX_PLAN_ENV).then(|| value.unwrap().to_string_lossy().into_owned()) - }) - .expect("trusted sandbox plan must survive env_clear"); - let plan: SandboxPlan = serde_json::from_str(&encoded).unwrap(); - assert_eq!(plan.cwd, temporary.path().canonicalize().unwrap()); - assert_ne!(encoded, r#"{"read_write":["/"]}"#); - } - - #[cfg(not(target_os = "linux"))] - #[test] - fn rootless_executor_fails_closed_off_linux() { - let error = ProcessExecutor::rootless_with_launcher("pvisor").unwrap_err(); - assert_eq!(error.kind(), std::io::ErrorKind::Unsupported); - } - - #[cfg(unix)] - #[test] - fn resolves_bare_program_before_overlay_cwd_is_applied() { - let resolved = resolve_host_program("sh"); - assert!( - resolved.is_absolute(), - "resolved path: {}", - resolved.display() - ); - assert!( - is_executable(&resolved), - "resolved path: {}", - resolved.display() - ); - assert_eq!( - resolved.file_name().and_then(|name| name.to_str()), - Some("sh") - ); - let path = std::env::var_os("PATH").expect("test requires PATH"); - assert!( - std::env::split_paths(&path).any(|directory| directory.join("sh") == resolved), - "{} was not resolved from PATH", - resolved.display() - ); - assert_eq!( - resolve_host_program("./agent-script"), - std::path::PathBuf::from("./agent-script") - ); - } -} diff --git a/crates/persisting-pvisor/src/pvisor.rs b/crates/persisting-pvisor/src/pvisor.rs deleted file mode 100644 index a88a3db1b..000000000 --- a/crates/persisting-pvisor/src/pvisor.rs +++ /dev/null @@ -1,1506 +0,0 @@ -//! pVisor — foreground Agent Run manager and portable execution runtime. -//! -//! Callers configure a [`PVisor`] and invoke [`PVisor::run`]. There is no -//! separate control plane: CLI / pPilot talk to this API directly. - -use crate::TrajectoryEventSink; -use crate::config::{GatewayDriverConfig, NetworkDriverConfig, PVisorConfig}; -use crate::event::{EventSink, NoopEventSink, RunEventPublisher}; -use crate::executor::{AttemptContext, RunExecutor}; -use crate::process::ProcessExecutor; -use crate::runtime::{ - AttemptTeardown, ImplantPlan, OverlayHint, RuntimeCapabilities, RuntimeSupervisor, - RuntimeSupervisorBuilder, -}; -use crate::util::unix_now_ms; -use crate::{AGENTCTL_VERSION, AgentCtlServer}; -use persisting_agentctl::ControlController; -use persisting_agentctl::{ - AttemptId, AttemptInfo, CapabilityDimension, CapabilityEnforcementEvidence, EnforcementLevel, - ExecutorDescriptor, IsolationKind, NetworkCapability, PolicyMode, RUNTIME_SCHEMA_VERSION, - RunFailure, RunFailureKind, RunInvocation, RunResult, RunSpec, RunState, RunStatus, -}; -use persisting_events::{ChronicleControl, ChronicleServeProcessClient, EventRecord}; -use serde_json::json; -use std::sync::Arc; -use tokio::sync::{broadcast, watch}; -use tokio::task::JoinHandle; -use tokio_util::sync::CancellationToken; - -#[derive(Debug, thiserror::Error)] -pub enum PVisorError { - #[error("invalid RunSpec: {0}")] - InvalidSpec(String), - #[error("runtime prepare failed: {0}")] - Prepare(#[source] anyhow::Error), - #[error("AgentCtl setup failed: {0}")] - AgentCtl(#[source] anyhow::Error), - #[error("no executor supports this invocation")] - UnsupportedInvocation, - #[error( - "executor `{executor}` lacks enforced evidence for requested capability dimensions: {dimensions}" - )] - UnsupportedPolicy { - executor: String, - dimensions: String, - }, - #[error("event sink rejected run creation: {0}")] - EventSink(#[source] anyhow::Error), - #[error("durable Attempt registration failed: {0}")] - AttemptRegistry(#[source] anyhow::Error), - #[error("run task failed to join: {0}")] - Join(#[from] tokio::task::JoinError), -} - -pub type RunEventStream = broadcast::Receiver; - -/// Cloneable, provider-independent cancellation capability for an in-flight Run. -#[derive(Clone)] -pub struct RunCancellation { - token: CancellationToken, -} - -impl RunCancellation { - pub fn cancel(&self) { - self.token.cancel(); - } - - pub fn is_cancelled(&self) -> bool { - self.token.is_cancelled() - } -} - -/// Handle for one in-flight Run: status, cancel, wait, event subscribe. -pub struct RunHandle { - run_id: persisting_agentctl::RunId, - attempt_id: AttemptId, - status: watch::Receiver, - cancellation: CancellationToken, - events: RunEventPublisher, - agentctl: crate::AgentCtlControl, - checkpoint_record: Option, - join: JoinHandle, -} - -impl RunHandle { - pub fn run_id(&self) -> &persisting_agentctl::RunId { - &self.run_id - } - - pub fn attempt_id(&self) -> &AttemptId { - &self.attempt_id - } - - pub fn status(&self) -> RunStatus { - self.status.borrow().clone() - } - - pub async fn status_changed(&mut self) -> Option { - self.status.changed().await.ok()?; - Some(self.status()) - } - - pub fn subscribe_events(&self) -> RunEventStream { - self.events.subscribe() - } - - /// Run-scoped cooperative AgentCtl desired-state and observation surface. - pub fn agentctl(&self) -> crate::AgentCtlControl { - self.agentctl.clone() - } - - /// Cooperatively quiesce every connected AgentCtl client and snapshot the upper. - pub async fn checkpoint( - &self, - checkpoint_id: &str, - timeout: std::time::Duration, - ) -> anyhow::Result { - anyhow::ensure!( - !checkpoint_id.trim().is_empty() - && checkpoint_id != "." - && checkpoint_id != ".." - && !checkpoint_id.contains('/') - && !checkpoint_id.contains('\\'), - "checkpoint id must be one non-empty path-safe segment" - ); - let record = self - .checkpoint_record - .as_ref() - .ok_or_else(|| anyhow::anyhow!("Run {} has no OverlayFS stage", self.run_id))?; - let deadline = crate::unix_now_ms().saturating_add(timeout.as_millis() as u64); - let checkpoint = self - .agentctl - .begin_checkpoint(checkpoint_id.to_owned(), Some(deadline))?; - loop { - if let Some(captured) = checkpoint.try_capture(|| { - crate::checkpoint::create_agent_quiesced_checkpoint(record, checkpoint_id) - })? { - return Ok(captured); - } - tokio::time::sleep(std::time::Duration::from_millis(20)).await; - } - } - - /// Cooperative cancel followed by executor-specific termination. - pub fn cancel(&self) { - self.cancellation.cancel(); - } - - pub fn cancellation(&self) -> RunCancellation { - RunCancellation { - token: self.cancellation.clone(), - } - } - - pub async fn wait(self) -> Result { - Ok(self.join.await?) - } -} - -/// Builder for a configured [`PVisor`]. -#[derive(Clone, Default)] -pub struct PVisorBuilder { - runtime: RuntimeSupervisorBuilder, - event_sink: Option>, - executors: Option>>, - chronicle_control: Option>, - pchronicle_binary: std::path::PathBuf, -} - -impl std::fmt::Debug for PVisorBuilder { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("PVisorBuilder") - .field("runtime", &self.runtime) - .field( - "event_sink", - &self.event_sink.as_ref().map(|_| ""), - ) - .field("executors", &self.executors.as_ref().map(|e| e.len())) - .field( - "chronicle_control", - &self - .chronicle_control - .as_ref() - .map(|_| ""), - ) - .field("pchronicle_binary", &self.pchronicle_binary) - .finish() - } -} - -impl PVisorBuilder { - pub fn new() -> Self { - Self::default() - } - - /// Apply the top-level pVisor configuration. - pub fn config(mut self, config: PVisorConfig) -> Self { - if let Some(gateway) = config.gateway { - self.runtime = self.runtime.gateway(gateway); - } - self.runtime = self.runtime.overlay(config.overlay); - self.runtime = self.runtime.network(config.network); - self - } - - /// Enable pVisor's built-in Agent protocol Gateway driver. - pub fn gateway(mut self, gateway: GatewayDriverConfig) -> Self { - self.runtime = self.runtime.gateway(gateway); - self - } - - /// Configure the Attempt network policy and interception-driver selection. - pub fn network(mut self, network: NetworkDriverConfig) -> Self { - self.runtime = self.runtime.network(network); - self - } - - /// Inject the structured trajectory output port used by the Gateway driver. - pub fn trajectory_sink(mut self, sink: Arc) -> Self { - self.runtime = self.runtime.trajectory_sink(sink); - self - } - - pub fn overlay(mut self, overlay: OverlayHint) -> Self { - self.runtime = self.runtime.overlay(overlay); - self - } - - /// Set durable Run storage independently of the optional Gateway. - pub fn storage(mut self, storage: impl Into) -> Self { - self.runtime = self.runtime.storage(storage.into()); - self - } - - pub fn control_controller(mut self, controller: Arc) -> Self { - self.runtime = self.runtime.control_controller(controller); - self - } - - pub fn event_sink(mut self, event_sink: Arc) -> Self { - self.event_sink = Some(event_sink); - self - } - - /// Inject the lightweight pChronicle control-plane port used for durable - /// Attempt registration. Storage engines remain outside the pVisor process. - pub fn chronicle_control(mut self, control: Arc) -> Self { - self.chronicle_control = Some(control); - self - } - - /// Select the sidecar executable used when a Run requests durable Attempt - /// registration without an injected control connection. - pub fn pchronicle_binary(mut self, binary: impl Into) -> Self { - self.pchronicle_binary = binary.into(); - self - } - - pub fn executors(mut self, executors: Vec>) -> Self { - self.executors = Some(executors); - self - } - - pub fn build(self) -> PVisor { - PVisor { - executors: Arc::new(self.executors.unwrap_or_else(|| { - vec![Arc::new(ProcessExecutor::default()) as Arc] - })), - event_sink: self - .event_sink - .unwrap_or_else(|| Arc::new(NoopEventSink) as Arc), - runtime: self.runtime.build(), - chronicle_control: self.chronicle_control, - pchronicle_binary: if self.pchronicle_binary.as_os_str().is_empty() { - "pchronicle".into() - } else { - self.pchronicle_binary - }, - } - } -} - -/// Portable Agent execution runtime. -/// -/// Owns Attempt prepare (capture / network / overlay), process execution, and -/// Run lifecycle. Hosts call [`Self::run`] directly — no forwarding control plane. -#[derive(Clone)] -pub struct PVisor { - executors: Arc>>, - event_sink: Arc, - runtime: RuntimeSupervisor, - chronicle_control: Option>, - pchronicle_binary: std::path::PathBuf, -} - -impl Default for PVisor { - fn default() -> Self { - Self::new() - } -} - -impl PVisor { - pub fn new() -> Self { - Self::builder().build() - } - - pub fn builder() -> PVisorBuilder { - PVisorBuilder::new() - } - - pub fn capabilities(&self) -> RuntimeCapabilities { - self.runtime.capabilities() - } - - /// Dry-run implant plan (env / network markers) without starting capture. - pub fn plan_for(&self, spec: &RunSpec) -> ImplantPlan { - self.runtime.plan_for(spec) - } - - /// Start one Run: prepare controls → execute → teardown on completion. - pub async fn run(&self, mut spec: RunSpec) -> Result { - validate_spec(&spec)?; - let executor = self - .executors - .iter() - .find(|executor| executor.supports(&spec.invocation)) - .cloned() - .ok_or(PVisorError::UnsupportedInvocation)?; - let mut descriptor = executor.descriptor(); - let vm_executor = descriptor.kind == persisting_agentctl::ExecutorKind::VirtualMachine; - let vm_network_executor = vm_executor && executor.supports_vm_network_attachment(); - if self.runtime.vm_network_is_requested() - && descriptor.isolation == persisting_agentctl::IsolationKind::VirtualMachine - && !vm_network_executor - { - return Err(PVisorError::InvalidSpec(format!( - "executor `{}` reports virtual-machine isolation but does not support pVisor VM network attachments", - descriptor.name - ))); - } - self.runtime.apply_network_capability(&mut spec); - let capability_enforcement = effective_capability_enforcement( - &descriptor, - &spec, - self.runtime.proxy_network_is_configured(), - vm_network_executor && self.runtime.vm_network_is_enforcing(), - ); - if spec.runtime.policy_mode == PolicyMode::Enforce { - let missing = capability_enforcement - .missing_dimensions(&spec.capabilities, &spec.runtime.resource_limits); - if !missing.is_empty() { - return Err(PVisorError::UnsupportedPolicy { - executor: descriptor.name, - dimensions: missing - .iter() - .map(ToString::to_string) - .collect::>() - .join(", "), - }); - } - } - // Persist the effective, Run-specific evidence in Attempt status and - // Run Bundle descriptors, including enforcement supplied by drivers. - descriptor.capability_enforcement = capability_enforcement.clone(); - spec.metadata.insert( - "pvisor.executor".into(), - serde_json::to_value(&descriptor).map_err(|error| { - PVisorError::InvalidSpec(format!("serialize executor descriptor: {error}")) - })?, - ); - spec.metadata.insert( - "pvisor.capability_enforcement".into(), - serde_json::to_value(&capability_enforcement).map_err(|error| { - PVisorError::InvalidSpec(format!("serialize capability enforcement: {error}")) - })?, - ); - let attempt_id = AttemptId::new(format!("attempt-{}", uuid::Uuid::new_v4())); - let cancellation = CancellationToken::new(); - let supervisor = crate::supervisor::connect_optional( - spec.supervisor.as_ref(), - &spec.run_id, - &attempt_id, - spec.lease_epoch, - cancellation.clone(), - ) - .await; - if let Some(connected) = supervisor.connected { - spec.metadata.insert( - "persisting.ppilot.supervisor.connected".into(), - json!(connected), - ); - } - if let Some(controller_epoch) = supervisor.controller_epoch { - spec.metadata.insert( - "persisting.ppilot.supervisor.controller_epoch".into(), - json!(controller_epoch), - ); - } - let mut session = self - .runtime - .prepare( - &mut spec, - &supervisor.initial_limits, - vm_network_executor, - &attempt_id, - ) - .map_err(PVisorError::Prepare)?; - let attachments = session - .as_ref() - .map(|session| session.attachments()) - .unwrap_or_default(); - let checkpoint_record = session - .as_ref() - .and_then(|session| session.checkpoint_record()); - let safe_profile_requested = spec - .metadata - .get("pvisor.safe") - .and_then(serde_json::Value::as_bool) - .unwrap_or(false); - let agentctl_server = match AgentCtlServer::start(&spec.run_id, &attempt_id) { - Ok(server) => server, - Err(error) => { - if let Some(session) = session.take() { - let snapshot = empty_agentctl_snapshot(&spec.run_id, &attempt_id); - if let Err(cleanup_error) = session.abort_startup( - &attempt_id, - spec.lease_epoch, - snapshot, - safe_profile_requested, - format!("AgentCtl setup failed: {error:#}"), - ) { - tracing::warn!(%cleanup_error, "persist pVisor startup failure"); - } - } - return Err(PVisorError::AgentCtl(error)); - } - }; - let agentctl = agentctl_server.control(); - let bundle_agentctl = agentctl.clone(); - let RunInvocation::Process(process) = &mut spec.invocation; - process.env.extend(agentctl_server.environment()); - spec.metadata.insert( - "pvisor.agentctl".into(), - json!({ - "version": AGENTCTL_VERSION, - "transport": "unix", - "endpoint": agentctl.endpoint(), - }), - ); - - let run_id = spec.run_id.clone(); - let now = unix_now_ms(); - let initial = RunStatus { - run_id: run_id.clone(), - state: RunState::Created, - attempt: AttemptInfo { - attempt_id: attempt_id.clone(), - lease_epoch: spec.lease_epoch, - number: 0, - executor: descriptor.clone(), - started_at_unix_ms: None, - finished_at_unix_ms: None, - }, - updated_at_unix_ms: now, - message: None, - }; - let (status_tx, status_rx) = watch::channel(initial); - let (live_tx, _) = broadcast::channel(256); - let events = RunEventPublisher::new( - run_id.clone(), - attempt_id.clone(), - "persisting-pvisor", - Arc::clone(&self.event_sink), - live_tx, - ); - if let Err(error) = events - .publish( - "run.created", - "runtime", - json!({ - "agent": spec.agent, - "task_id": spec.task_id, - "executor": descriptor, - "policy_mode": spec.runtime.policy_mode, - "capture_session": session.as_ref().map(|session| session.root_session()), - "agentctl_version": AGENTCTL_VERSION, - }), - ) - .await - { - if let Some(session) = session.take() - && let Err(cleanup_error) = session.abort_startup( - &attempt_id, - spec.lease_epoch, - agentctl.snapshot(), - safe_profile_requested, - format!("event sink rejected run creation: {error:#}"), - ) - { - tracing::warn!(%cleanup_error, "persist pVisor startup failure"); - } - return Err(PVisorError::EventSink(error)); - } - - let attempt_ttl_ms = spec - .supervisor - .as_ref() - .map(|bootstrap| bootstrap.attempt_ttl_ms.max(1_000)) - .unwrap_or(15_000); - let attempt_registry: Option> = match spec - .supervisor - .as_ref() - .and_then(|bootstrap| bootstrap.attempt_registry_uri.as_deref()) - { - Some(root) => { - let registry = match self - .chronicle_control - .as_ref() - .filter(|control| control.root_uri() == root) - { - Some(control) => Arc::clone(control), - None => Arc::new( - ChronicleServeProcessClient::spawn(&self.pchronicle_binary, root) - .await - .map_err(PVisorError::AttemptRegistry)?, - ) as Arc, - }; - let registered = registry - .publish_attempt_active( - run_id.as_str(), - attempt_id.as_str(), - spec.lease_epoch, - attempt_ttl_ms, - ) - .await - .map_err(PVisorError::AttemptRegistry)?; - if !registered { - return Err(PVisorError::AttemptRegistry(anyhow::anyhow!( - "Run {} lease epoch {} was fenced before execution", - run_id, - spec.lease_epoch - ))); - } - Some(registry) - } - None => None, - }; - let attempt_heartbeat_stop = CancellationToken::new(); - if let Some(registry) = attempt_registry.as_ref().map(Arc::clone) { - let heartbeat_run_id = run_id.to_string(); - let heartbeat_attempt_id = attempt_id.to_string(); - let heartbeat_epoch = spec.lease_epoch; - let heartbeat_stop = attempt_heartbeat_stop.clone(); - let heartbeat_cancel = cancellation.clone(); - tokio::spawn(async move { - let period_ms = (attempt_ttl_ms / 3).max(250); - let mut interval = - tokio::time::interval(std::time::Duration::from_millis(period_ms)); - let mut last_success = tokio::time::Instant::now(); - loop { - tokio::select! { - _ = heartbeat_stop.cancelled() => break, - _ = interval.tick() => { - match registry - .heartbeat_attempt( - &heartbeat_run_id, - &heartbeat_attempt_id, - heartbeat_epoch, - attempt_ttl_ms, - ) - .await - { - Ok(true) => last_success = tokio::time::Instant::now(), - Ok(false) => { - tracing::warn!( - run_id = %heartbeat_run_id, - attempt_id = %heartbeat_attempt_id, - "durable Attempt was fenced; cancelling workload" - ); - heartbeat_cancel.cancel(); - break; - } - Err(error) => { - tracing::warn!( - run_id = %heartbeat_run_id, - attempt_id = %heartbeat_attempt_id, - %error, - "durable Attempt heartbeat failed" - ); - if last_success.elapsed() - >= std::time::Duration::from_millis(attempt_ttl_ms) - { - heartbeat_cancel.cancel(); - break; - } - } - } - } - } - } - }); - } - - let context = AttemptContext::new( - Arc::new(spec), - attempt_id.clone(), - cancellation.clone(), - status_tx, - events.clone(), - agentctl.clone(), - attachments, - ); - let supervisor_warning = supervisor.warning; - let supervisor_session = supervisor.session; - let join = tokio::spawn(async move { - // Keep the Run-scoped endpoint alive until executor finalization finishes. - let _agentctl_server = agentctl_server; - let _supervisor_session = supervisor_session; - let mut result = executor.execute(context.clone()).await; - if let Some(warning) = supervisor_warning { - result.warnings.push(warning); - } - // The owning pVisor, not a pluggable executor, is authoritative for - // the scheduling generation attached to this Attempt. - result.lease_epoch = context.spec().lease_epoch; - let mut teardown = session.map(|session| session.teardown(result.exit_code)); - if let Some(error) = teardown - .as_ref() - .and_then(|teardown| teardown.error_message()) - { - fail_finalization(&mut result, format!("attempt teardown failed: {error}")); - } - if let Some(teardown) = teardown.as_mut() - && let Err(error) = teardown.commit_state(result.state) - { - fail_finalization( - &mut result, - format!("commit local Run record failed: {error:#}"), - ); - if let Err(error) = teardown.commit_state(RunState::Failed) { - result.warnings.push(format!( - "commit failed Run record after finalization error: {error:#}" - )); - } - } - if let Some(teardown) = teardown.as_mut() { - let bundle_result = crate::RunBundle::capture( - teardown.run_record(), - &result, - bundle_agentctl.snapshot(), - safe_profile_requested, - ) - .and_then(|bundle| bundle.write(&teardown.run_record().stage_dir())); - if let Err(error) = bundle_result { - fail_finalization( - &mut result, - format!("write durable Run Bundle failed: {error:#}"), - ); - persist_failed_local_state( - teardown, - &mut result, - &bundle_agentctl, - safe_profile_requested, - false, - ); - } - } - let kind = match result.state { - RunState::Completed => "run.completed", - RunState::Cancelled => "run.cancelled", - _ => "run.failed", - }; - if let Err(error) = context - .events() - .publish(kind, "runtime", terminal_payload(&result)) - .await - { - let append_error_kind = context.events().classify_append_error(&error); - fail_finalization( - &mut result, - format!("terminal event sink failed: {error:#}"), - ); - if append_error_kind == crate::EventAppendErrorKind::Unknown { - result.warnings.push( - "terminal event append outcome is unknown; a replacement terminal event was suppressed" - .into(), - ); - } - if let Some(teardown) = teardown.as_mut() { - persist_failed_local_state( - teardown, - &mut result, - &bundle_agentctl, - safe_profile_requested, - true, - ); - } - if append_error_kind == crate::EventAppendErrorKind::Rejected - && let Err(error) = context - .events() - .publish("run.failed", "runtime", terminal_payload(&result)) - .await - { - result.warnings.push(format!( - "publish finalization failure event failed: {error:#}" - )); - if let Some(teardown) = teardown.as_mut() { - persist_failed_local_state( - teardown, - &mut result, - &bundle_agentctl, - safe_profile_requested, - true, - ); - } - } - } - context.finish( - result.state, - result.failure.as_ref().map(|f| f.message.clone()), - ); - if let Some(registry) = attempt_registry { - match serde_json::to_value(&result) { - Ok(value) => match registry - .publish_attempt_terminal( - result.run_id.as_str(), - result.attempt_id.as_str(), - result.lease_epoch, - value, - ) - .await - { - Ok(true) => {} - Ok(false) => result - .warnings - .push("durable Attempt terminal result was fenced".into()), - Err(error) => result.warnings.push(format!( - "publish durable Attempt terminal result failed: {error:#}" - )), - }, - Err(error) => result.warnings.push(format!( - "encode durable Attempt terminal result failed: {error}" - )), - } - } - attempt_heartbeat_stop.cancel(); - result - }); - - Ok(RunHandle { - run_id, - attempt_id, - status: status_rx, - cancellation, - events, - agentctl, - checkpoint_record, - join, - }) - } -} - -fn empty_agentctl_snapshot( - run_id: &persisting_agentctl::RunId, - attempt_id: &AttemptId, -) -> crate::AgentCtlSnapshot { - crate::AgentCtlSnapshot { - run_id: run_id.as_str().to_owned(), - attempt_id: attempt_id.as_str().to_owned(), - directive: crate::AgentDirective::Continue, - clients: Vec::new(), - } -} - -fn effective_capability_enforcement( - descriptor: &ExecutorDescriptor, - spec: &RunSpec, - proxy_network_configured: bool, - vm_network_enforcing: bool, -) -> CapabilityEnforcementEvidence { - let mut evidence = descriptor.capability_enforcement.clone(); - if proxy_network_configured { - evidence.record( - CapabilityDimension::Network, - EnforcementLevel::Cooperative, - "explicit-proxy-environment", - ); - } - if matches!(spec.capabilities.network, NetworkCapability::Deny) { - match descriptor.isolation { - IsolationKind::RootlessProcess => evidence.record( - CapabilityDimension::Network, - EnforcementLevel::Enforced, - "linux-network-namespace", - ), - IsolationKind::SandboxedProcess => evidence.record( - CapabilityDimension::Network, - EnforcementLevel::Enforced, - "macos-seatbelt-network-deny", - ), - _ => {} - } - } - if vm_network_enforcing { - evidence.record( - CapabilityDimension::Network, - EnforcementLevel::Enforced, - "vm-smoltcp-network-boundary", - ); - } - evidence -} - -fn terminal_payload(result: &RunResult) -> serde_json::Value { - json!({ - "state": result.state, - "lease_epoch": result.lease_epoch, - "exit_code": result.exit_code, - "failure": result.failure, - "started_at_unix_ms": result.started_at_unix_ms, - "finished_at_unix_ms": result.finished_at_unix_ms, - }) -} - -fn persist_failed_local_state( - teardown: &mut AttemptTeardown, - result: &mut RunResult, - agentctl: &crate::AgentCtlControl, - safe_profile_requested: bool, - invalidate_stale_bundle: bool, -) { - if let Err(error) = teardown.commit_state(RunState::Failed) { - result - .warnings - .push(format!("commit failed Run record: {error:#}")); - } - let bundle_result = crate::RunBundle::capture( - teardown.run_record(), - result, - agentctl.snapshot(), - safe_profile_requested, - ) - .and_then(|bundle| bundle.write(&teardown.run_record().stage_dir())); - if let Err(error) = bundle_result { - result - .warnings - .push(format!("persist failed Run Bundle: {error:#}")); - if invalidate_stale_bundle - && let Err(error) = crate::RunBundle::invalidate(&teardown.run_record().stage_dir()) - { - result - .warnings - .push(format!("invalidate stale Run Bundle: {error:#}")); - } - } -} - -fn fail_finalization(result: &mut RunResult, message: String) { - result.warnings.push(message.clone()); - result.state = RunState::Failed; - result.finished_at_unix_ms = unix_now_ms(); - result.failure = Some(RunFailure { - kind: RunFailureKind::Infrastructure, - message, - retryable: true, - }); -} - -fn validate_spec(spec: &RunSpec) -> Result<(), PVisorError> { - if spec.schema_version != RUNTIME_SCHEMA_VERSION { - return Err(PVisorError::InvalidSpec(format!( - "unsupported schema_version {}; expected {}", - spec.schema_version, RUNTIME_SCHEMA_VERSION - ))); - } - if spec.run_id.is_empty() { - return Err(PVisorError::InvalidSpec("run_id must not be empty".into())); - } - let run_id = spec.run_id.as_str().trim(); - if run_id == "." || run_id == ".." || run_id.contains('/') || run_id.contains('\\') { - return Err(PVisorError::InvalidSpec( - "run_id must be one non-empty path-safe segment".into(), - )); - } - if spec.agent.name.trim().is_empty() { - return Err(PVisorError::InvalidSpec( - "agent.name must not be empty".into(), - )); - } - let persisting_agentctl::RunInvocation::Process(process) = &spec.invocation; - if process.program.trim().is_empty() { - return Err(PVisorError::InvalidSpec( - "process program must not be empty".into(), - )); - } - if process.stdin == persisting_agentctl::StdioMode::Capture { - return Err(PVisorError::InvalidSpec( - "captured stdin is not supported in pVisor v1".into(), - )); - } - if spec.runtime.max_output_bytes == 0 { - return Err(PVisorError::InvalidSpec( - "runtime.max_output_bytes must be greater than zero".into(), - )); - } - let limits = &spec.runtime.resource_limits; - if [ - limits.memory_bytes, - limits.processes, - limits.cpu_time_ms, - limits.open_files, - limits.file_size_bytes, - ] - .into_iter() - .flatten() - .any(|value| value == 0) - { - return Err(PVisorError::InvalidSpec( - "runtime resource limits must be greater than zero when configured".into(), - )); - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{EventSink, MemoryEventSink}; - use async_trait::async_trait; - use persisting_agentctl::SupervisorBootstrap; - use persisting_agentctl::{NetworkCapability, RunFailureKind, RunInvocation, StdioMode}; - use persisting_events::{AttemptRecordState, MemoryChronicleControl}; - use std::sync::Mutex; - - #[derive(Default)] - struct RejectCompletedSink { - kinds: Mutex>, - } - - #[async_trait] - impl EventSink for RejectCompletedSink { - async fn append(&self, event: &EventRecord) -> anyhow::Result<()> { - if event.kind == "run.completed" { - anyhow::bail!("simulated terminal commit failure"); - } - self.kinds.lock().unwrap().push(event.kind.clone()); - Ok(()) - } - - fn classify_append_error(&self, _error: &anyhow::Error) -> crate::EventAppendErrorKind { - crate::EventAppendErrorKind::Rejected - } - } - - #[derive(Default)] - struct CommitThenLoseAcknowledgementSink { - kinds: Mutex>, - } - - #[async_trait] - impl EventSink for CommitThenLoseAcknowledgementSink { - async fn append(&self, event: &EventRecord) -> anyhow::Result<()> { - self.kinds.lock().unwrap().push(event.kind.clone()); - if event.kind == "run.completed" { - anyhow::bail!("simulated acknowledgement loss after commit"); - } - Ok(()) - } - } - - struct RejectAllTerminalEventsSink; - - #[async_trait] - impl EventSink for RejectAllTerminalEventsSink { - async fn append(&self, event: &EventRecord) -> anyhow::Result<()> { - if matches!( - event.kind.as_str(), - "run.completed" | "run.cancelled" | "run.failed" - ) { - anyhow::bail!("simulated terminal rejection"); - } - Ok(()) - } - - fn classify_append_error(&self, _error: &anyhow::Error) -> crate::EventAppendErrorKind { - crate::EventAppendErrorKind::Rejected - } - } - - struct RejectCreatedSink; - - #[async_trait] - impl EventSink for RejectCreatedSink { - async fn append(&self, event: &EventRecord) -> anyhow::Result<()> { - if event.kind == "run.created" { - anyhow::bail!("simulated creation rejection"); - } - Ok(()) - } - - fn classify_append_error(&self, _error: &anyhow::Error) -> crate::EventAppendErrorKind { - crate::EventAppendErrorKind::Rejected - } - } - - #[cfg(unix)] - #[tokio::test] - async fn rejected_creation_finalizes_prepared_run_storage() { - let temporary = tempfile::tempdir().unwrap(); - let storage = temporary.path().join("storage"); - let runtime = PVisor::builder() - .storage(&storage) - .event_sink(Arc::new(RejectCreatedSink)) - .build(); - let spec = RunSpec::process("run-created-rejected", "test-agent", "/bin/true"); - - let error = match runtime.run(spec).await { - Ok(_) => panic!("run creation unexpectedly succeeded"), - Err(error) => error, - }; - assert!(matches!(error, PVisorError::EventSink(_))); - - let record = crate::runtime::RunRecord::read(&storage).unwrap(); - assert_eq!(record.state, "failed"); - assert!(record.finished_at_unix_ms.is_some()); - let bundle = crate::RunBundle::read(&storage).unwrap(); - assert_eq!(bundle.run.state, RunState::Failed); - assert_eq!( - bundle.run.failure.as_ref().map(|failure| failure.kind), - Some(RunFailureKind::Infrastructure) - ); - assert!( - bundle - .run - .failure - .as_ref() - .unwrap() - .message - .contains("event sink rejected run creation") - ); - assert!(!storage.join("control.sock").exists()); - let _lease = crate::runtime::RunLease::acquire(&storage).unwrap(); - } - - #[cfg(unix)] - #[tokio::test] - async fn process_run_completes_and_emits_lifecycle() { - let sink = Arc::new(MemoryEventSink::default()); - let runtime = PVisor::builder().event_sink(sink.clone()).build(); - let mut spec = RunSpec::process("run-success", "test-agent", "/bin/sh"); - let RunInvocation::Process(process) = &mut spec.invocation; - process.args = vec!["-c".into(), "printf pvisor".into()]; - process.stdout = StdioMode::Capture; - process.stderr = StdioMode::Capture; - - let handle = runtime.run(spec).await.unwrap(); - assert_eq!(handle.status().state, RunState::Created); - let result = handle.wait().await.unwrap(); - assert_eq!(result.state, RunState::Completed); - assert_eq!(result.output.stdout.as_deref(), Some("pvisor")); - - let emitted = sink.events(); - assert!(emitted.iter().all(|event| { - event.identity.event_id.is_some() - && event.identity.run_id.as_deref() == Some("run-success") - && event.identity.attempt_id.is_some() - && event.identity.producer.as_deref() == Some("persisting-pvisor") - })); - let kinds: Vec<_> = emitted.into_iter().map(|event| event.kind).collect(); - assert_eq!(kinds.first().map(String::as_str), Some("run.created")); - assert_eq!(kinds.last().map(String::as_str), Some("run.completed")); - assert!(kinds.iter().any(|kind| kind == "run.state_changed")); - } - - #[cfg(unix)] - #[tokio::test] - async fn durable_attempt_registry_receives_terminal_run_result() { - let dir = tempfile::tempdir().unwrap(); - let control = Arc::new(MemoryChronicleControl::new( - dir.path().display().to_string(), - )); - let mut spec = RunSpec::process("run-durable-registry", "test-agent", "/bin/sh"); - spec.lease_epoch = 7; - spec.supervisor = Some(SupervisorBootstrap { - endpoint: "tcp://127.0.0.1:9".into(), - token: "unavailable".into(), - controller_epoch: 1, - connect_timeout_ms: 25, - attempt_registry_uri: Some(dir.path().display().to_string()), - attempt_ttl_ms: 1_000, - }); - let RunInvocation::Process(process) = &mut spec.invocation; - process.args = vec!["-c".into(), "printf durable".into()]; - process.stdout = StdioMode::Capture; - - let result = PVisor::builder() - .chronicle_control(control.clone()) - .build() - .run(spec) - .await - .unwrap() - .wait() - .await - .unwrap(); - assert_eq!(result.state, RunState::Completed); - let record = control - .get_attempt("run-durable-registry") - .await - .unwrap() - .unwrap(); - assert_eq!(record.state, AttemptRecordState::Terminal); - assert_eq!(record.lease_epoch, 7); - let recovered: RunResult = serde_json::from_value(record.terminal_result.unwrap()).unwrap(); - assert_eq!(recovered.attempt_id, result.attempt_id); - assert_eq!(recovered.state, RunState::Completed); - } - - #[cfg(unix)] - #[tokio::test] - async fn process_receives_live_agentctl_endpoint() { - let runtime = PVisor::new(); - let mut spec = RunSpec::process("run-agentctl", "test-agent", "/bin/sh"); - let RunInvocation::Process(process) = &mut spec.invocation; - process.args = vec![ - "-c".into(), - "test -S \"$PERSISTING_AGENTCTL_ENDPOINT\" && \ - test -n \"$PERSISTING_AGENTCTL_TOKEN\" && \ - test \"$PERSISTING_AGENTCTL_VERSION\" = 1 && \ - test \"$PERSISTING_AGENTCTL_TRANSPORT\" = unix" - .into(), - ]; - - let handle = runtime.run(spec).await.unwrap(); - assert_eq!(handle.agentctl().snapshot().run_id, "run-agentctl"); - let result = handle.wait().await.unwrap(); - assert_eq!(result.state, RunState::Completed); - } - - #[cfg(unix)] - #[tokio::test] - async fn terminal_sink_failure_prevents_completed_result() { - let sink = Arc::new(RejectCompletedSink::default()); - let runtime = PVisor::builder().event_sink(sink.clone()).build(); - let mut spec = RunSpec::process("run-terminal-failure", "test-agent", "/bin/sh"); - let RunInvocation::Process(process) = &mut spec.invocation; - process.args = vec!["-c".into(), "exit 0".into()]; - - let result = runtime.run(spec).await.unwrap().wait().await.unwrap(); - assert_eq!(result.state, RunState::Failed); - assert_eq!( - result.failure.as_ref().map(|failure| failure.kind), - Some(RunFailureKind::Infrastructure) - ); - assert!( - result - .failure - .as_ref() - .unwrap() - .message - .contains("terminal event sink failed") - ); - let kinds = sink.kinds.lock().unwrap().clone(); - assert!(!kinds.iter().any(|kind| kind == "run.completed")); - assert_eq!(kinds.last().map(String::as_str), Some("run.failed")); - } - - #[cfg(unix)] - #[tokio::test] - async fn unknown_terminal_append_does_not_publish_a_conflicting_terminal() { - let sink = Arc::new(CommitThenLoseAcknowledgementSink::default()); - let runtime = PVisor::builder().event_sink(sink.clone()).build(); - let mut spec = RunSpec::process("run-terminal-unknown", "test-agent", "/bin/sh"); - let RunInvocation::Process(process) = &mut spec.invocation; - process.args = vec!["-c".into(), "exit 0".into()]; - - let result = runtime.run(spec).await.unwrap().wait().await.unwrap(); - assert_eq!(result.state, RunState::Failed); - assert!( - result - .warnings - .iter() - .any(|warning| warning.contains("outcome is unknown")) - ); - let terminal_kinds = sink - .kinds - .lock() - .unwrap() - .iter() - .filter(|kind| { - matches!( - kind.as_str(), - "run.completed" | "run.cancelled" | "run.failed" - ) - }) - .cloned() - .collect::>(); - assert_eq!(terminal_kinds, vec!["run.completed"]); - } - - #[cfg(unix)] - #[tokio::test] - async fn replacement_terminal_failure_is_reported_in_the_result() { - let runtime = PVisor::builder() - .event_sink(Arc::new(RejectAllTerminalEventsSink)) - .build(); - let mut spec = RunSpec::process("run-terminal-double-reject", "test-agent", "/bin/sh"); - let RunInvocation::Process(process) = &mut spec.invocation; - process.args = vec!["-c".into(), "exit 0".into()]; - - let result = runtime.run(spec).await.unwrap().wait().await.unwrap(); - assert_eq!(result.state, RunState::Failed); - assert!( - result - .warnings - .iter() - .any(|warning| warning.contains("publish finalization failure event failed")) - ); - } - - #[cfg(unix)] - #[tokio::test] - async fn bundle_failure_is_published_as_the_only_terminal_result() { - let temporary = tempfile::tempdir().unwrap(); - let storage = temporary.path().join("storage"); - let sink = Arc::new(MemoryEventSink::default()); - let runtime = PVisor::builder() - .storage(&storage) - .event_sink(sink.clone()) - .build(); - let mut spec = RunSpec::process("run-bundle-failure", "test-agent", "/bin/sh"); - let RunInvocation::Process(process) = &mut spec.invocation; - process.args = vec![ - "-c".into(), - "mkdir -p \"$1\" && mkdir \"$1/run-bundle.json\"".into(), - "sh".into(), - storage.display().to_string(), - ]; - - let result = runtime.run(spec).await.unwrap().wait().await.unwrap(); - assert_eq!(result.state, RunState::Failed); - assert!( - result - .failure - .as_ref() - .unwrap() - .message - .contains("write durable Run Bundle failed") - ); - let terminal_kinds = sink - .events() - .into_iter() - .map(|event| event.kind) - .filter(|kind| { - matches!( - kind.as_str(), - "run.completed" | "run.cancelled" | "run.failed" - ) - }) - .collect::>(); - assert_eq!(terminal_kinds, vec!["run.failed"]); - } - - #[cfg(unix)] - #[tokio::test] - async fn storage_only_run_persists_a_bundle_without_network_drivers() { - let temporary = tempfile::tempdir().unwrap(); - let storage = temporary.path().join("storage"); - let runtime = PVisor::builder().storage(&storage).build(); - let mut spec = RunSpec::process("run-storage-only", "test-agent", "/bin/sh"); - let RunInvocation::Process(process) = &mut spec.invocation; - process.args = vec!["-c".into(), "exit 0".into()]; - - let result = runtime.run(spec).await.unwrap().wait().await.unwrap(); - assert_eq!(result.state, RunState::Completed); - assert_eq!( - crate::RunBundle::read(&storage).unwrap().run.state, - RunState::Completed - ); - assert_eq!( - crate::runtime::RunRecord::read(&storage).unwrap().state, - "completed" - ); - } - - #[cfg(unix)] - #[tokio::test] - async fn durable_run_metadata_records_environment_keys_without_secret_values() { - const SECRET: &str = "pvisor-secret-value-must-not-be-persisted"; - let temporary = tempfile::tempdir().unwrap(); - let storage = temporary.path().join("storage"); - let runtime = PVisor::builder().storage(&storage).build(); - let mut spec = RunSpec::process("run-secret-projection", "test-agent", "/bin/sh"); - let RunInvocation::Process(process) = &mut spec.invocation; - process.args = vec!["-c".into(), "exit 0".into()]; - process.inherit_env = false; - process - .env - .insert("PRIVATE_API_TOKEN".into(), SECRET.into()); - - let result = runtime.run(spec).await.unwrap().wait().await.unwrap(); - assert_eq!(result.state, RunState::Completed); - - let bundle_raw = std::fs::read_to_string(storage.join(crate::RUN_BUNDLE_FILENAME)).unwrap(); - let record_raw = std::fs::read_to_string(storage.join("run.json")).unwrap(); - assert!(!bundle_raw.contains(SECRET)); - assert!(!record_raw.contains(SECRET)); - let bundle = crate::RunBundle::read(&storage).unwrap(); - assert!(!bundle.environment.inherits_host); - assert!( - bundle - .environment - .projected_keys - .iter() - .any(|key| key == "PRIVATE_API_TOKEN") - ); - } - - #[tokio::test] - async fn run_id_must_be_a_capture_safe_path_segment() { - for invalid in ["../escape", "nested/run", r"nested\run", ".", ".."] { - let spec = RunSpec::process(invalid, "agent", "echo"); - let error = match PVisor::new().run(spec).await { - Ok(_) => panic!("invalid run id was accepted: {invalid}"), - Err(error) => error, - }; - assert!(matches!(error, PVisorError::InvalidSpec(_))); - } - } - - #[cfg(unix)] - #[tokio::test] - async fn process_run_cancels_the_process_tree() { - let runtime = PVisor::new(); - let mut spec = RunSpec::process("run-cancel", "test-agent", "/bin/sh"); - let RunInvocation::Process(process) = &mut spec.invocation; - process.args = vec!["-c".into(), "sleep 30 & wait".into()]; - process.stdout = StdioMode::Capture; - process.stderr = StdioMode::Capture; - - let handle = runtime.run(spec).await.unwrap(); - tokio::time::sleep(std::time::Duration::from_millis(30)).await; - handle.cancel(); - let result = tokio::time::timeout(std::time::Duration::from_secs(3), handle.wait()) - .await - .expect("process tree did not terminate") - .unwrap(); - assert_eq!(result.state, RunState::Cancelled); - } - - #[cfg(unix)] - #[tokio::test] - async fn process_deadline_is_a_typed_failure() { - let runtime = PVisor::new(); - let mut spec = RunSpec::process("run-timeout", "test-agent", "/bin/sleep"); - let RunInvocation::Process(process) = &mut spec.invocation; - process.args = vec!["30".into()]; - process.stdout = StdioMode::Null; - process.stderr = StdioMode::Null; - spec.runtime.timeout_ms = Some(20); - - let result = runtime.run(spec).await.unwrap().wait().await.unwrap(); - assert_eq!(result.state, RunState::Failed); - assert_eq!( - result.failure.unwrap().kind, - RunFailureKind::DeadlineExceeded - ); - } - - #[tokio::test] - async fn host_process_refuses_enforced_policy() { - let runtime = PVisor::new(); - let mut spec = RunSpec::process("run-enforce", "test-agent", "echo"); - spec.runtime.policy_mode = PolicyMode::Enforce; - spec.capabilities.network = NetworkCapability::Deny; - let error = match runtime.run(spec).await { - Ok(_) => panic!("host process must not claim capability enforcement"), - Err(error) => error, - }; - assert!(matches!( - error, - PVisorError::UnsupportedPolicy { dimensions, .. } - if dimensions == "network, subprocess" - )); - } - - #[test] - fn ambient_network_requires_an_enforced_boundary() { - let spec = RunSpec::process("run-ambient", "test-agent", "echo"); - assert_eq!( - persisting_agentctl::requested_enforcement_dimensions( - &spec.capabilities, - &spec.runtime.resource_limits, - ), - vec![ - CapabilityDimension::Network, - CapabilityDimension::Subprocess - ] - ); - } - - #[tokio::test] - async fn gateway_driver_does_not_elevate_host_process_enforcement() { - let proxy = persisting_gateway::config::ProxyConfig::from_toml_str( - r#" -listen = "127.0.0.1:19081" -admin_listen = "127.0.0.1:9876" -agent_id = "test" - -[[models]] -name = "*" -upstream = "https://example.com" -"#, - ) - .unwrap(); - let runtime = PVisor::builder() - .gateway(GatewayDriverConfig::new(proxy)) - .build(); - let mut spec = RunSpec::process("run-enforce-capture", "test-agent", "echo"); - spec.runtime.policy_mode = PolicyMode::Enforce; - spec.capabilities.network = NetworkCapability::Deny; - let plan = runtime.plan_for(&spec); - assert_eq!( - plan.env - .get("PERSISTING_OVERLAYNET_DRIVER") - .map(String::as_str), - Some("explicit-proxy") - ); - assert_eq!( - plan.env - .get("PERSISTING_OVERLAYNET_STRENGTH") - .map(String::as_str), - Some("cooperative") - ); - let error = match runtime.run(spec).await { - Ok(_) => panic!("explicit proxy capture cannot enforce host process capabilities"), - Err(error) => error, - }; - assert!(matches!( - error, - PVisorError::UnsupportedPolicy { dimensions, .. } - if dimensions == "network, subprocess" - )); - } - - #[cfg(target_os = "macos")] - #[tokio::test] - #[ignore = "requires an enabled macFUSE kernel extension"] - async fn overlay_run_does_not_require_gateway() { - let temporary = tempfile::tempdir().unwrap(); - let target = temporary.path().join("target"); - let storage = temporary.path().join("storage"); - std::fs::create_dir_all(&target).unwrap(); - std::fs::write(target.join("base.txt"), b"base").unwrap(); - - let pvisor = PVisor::builder() - .storage(&storage) - .overlay(OverlayHint { - lower_dirs: vec![target.clone()], - ..OverlayHint::default() - }) - .build(); - let mut spec = RunSpec::process("run-overlay-only", "agent", "/bin/sh"); - let RunInvocation::Process(process) = &mut spec.invocation; - process.args = vec![ - "-c".into(), - "test \"$(cat base.txt)\" = base && printf changed > base.txt && printf new > new.txt" - .into(), - ]; - - let result = pvisor.run(spec).await.unwrap().wait().await.unwrap(); - assert_eq!(result.state, RunState::Completed); - assert_eq!(std::fs::read(target.join("base.txt")).unwrap(), b"base"); - assert!(!target.join("new.txt").exists()); - - let record = - crate::runtime::resolve_run(Some(std::path::Path::new("run-overlay-only")), &storage) - .unwrap(); - assert!(record.gateway_listen.is_none()); - let mut overlay = record.overlay.unwrap(); - assert_eq!(format!("{:?}", overlay.state), "Staged"); - crate::runtime::apply_overlay(&mut overlay).unwrap(); - assert_eq!(std::fs::read(target.join("base.txt")).unwrap(), b"changed"); - assert_eq!(std::fs::read(target.join("new.txt")).unwrap(), b"new"); - } - - #[test] - fn builder_injects_runtime_and_network_markers() { - let pvisor = PVisor::builder().build(); - let mut spec = RunSpec::process("run-implant", "agent", "echo"); - spec.capabilities.network = NetworkCapability::Deny; - let plan = pvisor.plan_for(&spec); - assert_eq!( - plan.env - .get("PERSISTING_PVISOR_RUNTIME") - .map(String::as_str), - Some("1") - ); - assert_eq!( - plan.env - .get("PERSISTING_NETWORK_POLICY") - .map(String::as_str), - Some("deny") - ); - assert!(plan.notes.iter().any(|note| note.contains("network"))); - } -} diff --git a/crates/persisting-pvisor/src/runtime/attempt.rs b/crates/persisting-pvisor/src/runtime/attempt.rs deleted file mode 100644 index 2324d6dc0..000000000 --- a/crates/persisting-pvisor/src/runtime/attempt.rs +++ /dev/null @@ -1,1348 +0,0 @@ -//! Attempt-scoped Gateway + OverlayFS session owned by pVisor. - -use super::implant::{ImplantPlan, OverlayHint}; -use super::overlay::{ - OverlayMount, OverlayRecord, apply_overlay, discard_overlay, hint_from_record, - lower_stack_from_config, mount_overlay_record, prepare_overlay_record_mountless, - resolve_overlay_workspace, stage_overlay_record, -}; -use super::registry::{EnvironmentProjection, RunControlServer, RunLease, RunLineage, RunRecord}; -use crate::TrajectoryEventSink; -use anyhow::Context as _; -use persisting_agentctl::ControlController; -use persisting_agentctl::{NetworkCapability, ProcessInvocation, RunInvocation, RunSpec, RunState}; -use persisting_gateway::config::ProxyConfig; -use persisting_gateway::injection::{ - client_gateway_config_args, proxy_environment_with_local_auth, -}; -use persisting_gateway::lifecycle::{ - CaptureMode, append_lifecycle, root_session_route, session_ended_record, session_started_record, -}; -use persisting_gateway::runtime::in_process::{InProcessCapture, InProcessRuntime}; -use persisting_gateway::runtime::run_config::snapshot_proxy_config; -use persisting_gateway::runtime::run_env::write_run_session; -use persisting_gateway::sink::SeqOnlySink; -use persisting_overlaynet::{ - BandwidthRegistry, EgressContext, EgressRuntime, InterceptionMetrics, NetworkConfig, - NetworkPolicy, -}; -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use std::time::Instant; - -/// Live controls for one Attempt: capture proxy + optional overlay mount. -pub(crate) struct AttemptSession { - root_session: String, - agent_id: String, - /// Staging record retained after unmount (for apply / discard). - overlay_record: Option, - gateway: Option, - vm_network: Option>>>, - network_metrics: Option, - overlay: Option, - sink: Option>, - started_at: Instant, - run_record: RunRecord, - _control: Option, - _lease: RunLease, -} - -impl AttemptSession { - pub(crate) fn root_session(&self) -> &str { - &self.root_session - } - - pub(crate) fn attachments(&self) -> crate::executor::AttemptAttachments { - crate::executor::AttemptAttachments { - vm_network: self.vm_network.clone(), - } - } - pub(crate) fn checkpoint_record(&self) -> Option { - self.overlay_record - .as_ref() - .map(|_| self.run_record.clone()) - } - - pub(crate) fn teardown(mut self, exit_code: Option) -> AttemptTeardown { - let mut errors = Vec::new(); - let duration_ms = self.started_at.elapsed().as_millis() as u64; - if let Some(sink) = &self.sink - && let Err(err) = append_lifecycle( - sink.as_ref(), - &root_session_route(&self.root_session), - &self.agent_id, - session_ended_record( - Some(self.root_session.clone()), - Some(self.agent_id.clone()), - CaptureMode::Run, - "child_exit", - exit_code, - Some(duration_ms), - ), - ) - { - errors.push(format!("append session.ended: {err:#}")); - } - - let mut record = if let Some(mount) = self.overlay.take() { - let fallback = self.overlay_record.take(); - match mount.unmount() { - Ok(record) => Some(record), - Err(err) => { - errors.push(format!("unmount OverlayFS: {err:#}")); - fallback - } - } - } else { - let mut record = self.overlay_record.take(); - if let Some(record) = record.as_mut() - && let Err(err) = stage_overlay_record(record) - { - errors.push(format!("stage OverlayFS: {err:#}")); - } - record - }; - - if let Some(ref mut rec) = record { - if rec.auto_discard { - if let Err(err) = discard_overlay(rec) { - errors.push(format!("discard OverlayFS staging: {err:#}")); - } - } else if rec.auto_apply { - if let Err(err) = apply_overlay(rec) { - errors.push(format!("apply OverlayFS staging: {err:#}")); - } else { - tracing::info!( - id = %rec.id, - target = %rec.target.display(), - "overlay auto-applied onto target" - ); - } - } else { - tracing::info!( - id = %rec.id, - stage = %rec.stage_dir.display(), - target = %rec.target.display(), - "overlay staged — review then: \ - `pvisor status {}` then `pvisor apply {}` or `pvisor drop {}`", - rec.id, - rec.id, - rec.id, - ); - } - } - self.overlay_record = record; - - if let Some(metrics) = &self.network_metrics { - self.run_record.network_interception_metrics = Some(metrics.snapshot()); - } - if let Some(network) = self.vm_network.take() { - match network.lock() { - Ok(mut attachment) => { - if let Some(attachment) = attachment.take() { - match attachment.shutdown() { - Ok(snapshot) => { - self.run_record.network_interception_metrics = Some(snapshot) - } - Err(err) => errors.push(format!("shutdown VM OverlayNet: {err:#}")), - } - } - } - Err(_) => errors.push("shutdown VM OverlayNet: attachment lock poisoned".into()), - } - } - if let Some(gateway) = self.gateway.take() - && let Err(err) = gateway.shutdown() - { - errors.push(format!("shutdown Gateway: {err:#}")); - } - self.run_record.finished_at_unix_ms = Some(crate::util::unix_now_ms()); - self.run_record.overlay = self.overlay_record.clone(); - AttemptTeardown { - run_record: self.run_record, - errors, - } - } - - /// Finalize durable state when pVisor prepared the Attempt drivers but - /// could not hand the Attempt to an executor. - /// - /// Preparation writes a live RunRecord and may start an overlay, Gateway, - /// or VM network attachment. A later AgentCtl or event-publisher failure - /// must therefore take the same teardown path as an executed Attempt - /// instead of leaving a stale `running` record behind. - pub(crate) fn abort_startup( - self, - attempt_id: &persisting_agentctl::AttemptId, - lease_epoch: u64, - agentctl: crate::AgentCtlSnapshot, - safe_profile_requested: bool, - message: String, - ) -> anyhow::Result<()> { - let run_id = persisting_agentctl::RunId::new(self.run_record.run_id.clone()); - let started_at_unix_ms = self.run_record.started_at_unix_ms; - let mut teardown = self.teardown(None); - let mut warnings = Vec::new(); - if let Some(error) = teardown.error_message() { - warnings.push(format!("attempt teardown after startup failure: {error}")); - } - let result = persisting_agentctl::RunResult { - run_id, - attempt_id: attempt_id.clone(), - lease_epoch, - state: RunState::Failed, - started_at_unix_ms, - finished_at_unix_ms: crate::util::unix_now_ms(), - exit_code: None, - failure: Some(persisting_agentctl::RunFailure { - kind: persisting_agentctl::RunFailureKind::Infrastructure, - message, - retryable: true, - }), - output: Default::default(), - value: None, - metrics: Default::default(), - artifacts: Vec::new(), - event_stream_ref: None, - warnings, - }; - teardown.commit_state(RunState::Failed)?; - crate::RunBundle::capture( - teardown.run_record(), - &result, - agentctl, - safe_profile_requested, - )? - .write(&teardown.run_record().stage_dir())?; - Ok(()) - } -} - -pub(crate) struct AttemptTeardown { - run_record: RunRecord, - errors: Vec, -} - -impl AttemptTeardown { - pub(crate) fn run_record(&self) -> &RunRecord { - &self.run_record - } - - pub(crate) fn error_message(&self) -> Option { - (!self.errors.is_empty()).then(|| self.errors.join("; ")) - } - - pub(crate) fn commit_state(&mut self, state: RunState) -> anyhow::Result<()> { - self.run_record.state = match state { - RunState::Completed => "completed", - RunState::Cancelled => "cancelled", - RunState::Failed => "failed", - _ => "terminated", - } - .into(); - self.run_record.write() - } -} - -pub(crate) struct AttemptPrepareOpts<'a> { - pub config: &'a ProxyConfig, - /// Durable pVisor Run storage and default OverlayFS stage. - pub storage: &'a Path, - /// Gateway capture and session configuration storage. - pub capture_storage: &'a Path, - pub sink: Option>, - pub stream_markdown: bool, - /// Extra overlay hint from CLI (overrides paths when set). - pub overlay_override: OverlayHint, - pub controller: Arc, - pub gateway_enabled: bool, - pub vm_network: bool, - pub attempt_id: &'a str, -} - -pub(crate) struct OverlayAttemptPrepareOpts<'a> { - pub storage: &'a Path, - pub overlay: OverlayHint, - pub vm_network: Option, -} - -#[derive(Clone)] -pub(crate) struct VmNetworkPrepareOpts { - pub network: NetworkConfig, - pub controller: Arc, - pub attempt_id: String, -} - -pub(crate) struct VmNetworkAttachment { - guest_stream: std::os::unix::net::UnixStream, - backend: persisting_overlaynet::vm::VmNetwork, -} - -impl VmNetworkAttachment { - pub(crate) fn guest_stream(&self) -> &std::os::unix::net::UnixStream { - &self.guest_stream - } - - /// Close the peer first so a backend blocked on socket I/O can observe EOF - /// before we join its thread. - pub(crate) fn shutdown(self) -> anyhow::Result { - let Self { - backend, - guest_stream, - } = self; - drop(guest_stream); - backend.shutdown() - } -} - -fn mark_vm_network(plan: &mut ImplantPlan) { - plan.env - .insert("PERSISTING_OVERLAYNET_DRIVER".into(), "vm-smoltcp".into()); - plan.env.insert( - "PERSISTING_OVERLAYNET_STRENGTH".into(), - "non-bypassable".into(), - ); - plan.notes.push( - "network interception: libkrun virtio-net → smoltcp (non-bypassable IPv4 TCP + DNS)".into(), - ); -} - -struct PreparedVmNetwork { - attachment: Arc>>, - metrics: InterceptionMetrics, - policy: serde_json::Value, -} - -struct PreparedOverlay { - mount: Option, - hint: OverlayHint, - record: Option, - lowers: Vec, -} - -/// Start pVisor's configured Gateway and OverlayFS drivers, then enrich `spec`. -pub(crate) fn prepare_attempt( - spec: &mut RunSpec, - opts: AttemptPrepareOpts<'_>, -) -> anyhow::Result { - let config = opts.config.clone(); - spec.agent.name = config.agent_id.clone(); - let storage = opts - .storage - .canonicalize() - .unwrap_or_else(|_| opts.storage.to_path_buf()); - let capture_storage = opts - .capture_storage - .canonicalize() - .unwrap_or_else(|_| opts.capture_storage.to_path_buf()); - - let sink = opts - .sink - .unwrap_or_else(|| Arc::new(SeqOnlySink::new()) as Arc); - - let network_metrics = InterceptionMetrics::default(); - let bandwidth_registry = BandwidthRegistry::default(); - let gateway = InProcessCapture::start_with_runtime( - config.clone(), - capture_storage.clone(), - Arc::clone(&sink), - opts.stream_markdown, - InProcessRuntime { - controller: Arc::clone(&opts.controller), - interception_metrics: network_metrics.clone(), - bandwidth_registry: bandwidth_registry.clone(), - attempt_id: Some(opts.attempt_id.to_owned()), - gateway_enabled: opts.gateway_enabled, - }, - )?; - - // A Run has one top-level identity across pVisor, Gateway and pChronicle. - // Subagent sessions remain separate Storylines beneath this root. - let root_session = spec.run_id.as_str().to_string(); - write_run_session(&capture_storage, &root_session)?; - let config_snapshot = snapshot_proxy_config(&capture_storage, &root_session, &config)?; - - let mut overlay_cfg = config.overlay.clone(); - apply_overlay_override(&mut overlay_cfg, &opts.overlay_override); - - let prepared_overlay = prepare_overlay( - &overlay_cfg, - &storage, - &root_session, - uses_krun_executor(spec), - )?; - let PreparedOverlay { - mount: overlay_mount, - hint: overlay_hint, - record: overlay_record, - lowers: overlay_lowers, - } = prepared_overlay; - - let RunInvocation::Process(process) = &spec.invocation; - let command = std::iter::once(process.program.clone()) - .chain(process.args.iter().cloned()) - .collect::>(); - let stage_dir = overlay_record - .as_ref() - .map(|record| record.stage_dir.clone()) - .unwrap_or_else(|| storage.clone()); - let lease = RunLease::acquire(&stage_dir)?; - let vm_network = opts - .vm_network - .then(|| { - start_vm_network( - spec, - VmNetworkPrepareOpts { - network: config.network.clone(), - controller: Arc::clone(&opts.controller), - attempt_id: opts.attempt_id.to_owned(), - }, - Some((&gateway.listen, opts.gateway_enabled)), - network_metrics.clone(), - bandwidth_registry, - ) - }) - .transpose()?; - let mut run_record = RunRecord { - schema_version: 1, - run_id: spec.run_id.as_str().to_string(), - parent_run_id: spec.parent_run_id.as_ref().map(ToString::to_string), - task_id: spec.task_id.clone(), - session_id: root_session.clone(), - agent: config.agent_id.clone(), - pid: std::process::id(), - command, - executor: executor_from_spec(spec), - state: "running".into(), - started_at_unix_ms: crate::util::unix_now_ms(), - finished_at_unix_ms: None, - storage: storage.clone(), - workspace: workspace_from_spec(spec), - overlaynet_listen: Some(gateway.listen.clone()), - network_interception: Some(if opts.vm_network { - persisting_overlaynet::InterceptionProfile::vm_smoltcp() - } else { - persisting_overlaynet::InterceptionProfile::explicit_proxy() - }), - network_interception_metrics: None, - gateway_listen: opts.gateway_enabled.then(|| gateway.listen.clone()), - network: serde_json::to_value(&spec.capabilities.network)?, - network_policy: Some(serde_json::to_value(&config.network)?), - environment: environment_from_spec(spec), - resource_limits: spec.runtime.resource_limits.clone(), - overlay: overlay_record.clone(), - overlay_lowers, - lineage: lineage_from_spec(spec), - orchestration: orchestration_from_spec(spec), - }; - run_record.write()?; - let control = RunControlServer::start(&run_record)?; - - let RunInvocation::Process(ref process) = spec.invocation; - let program = process.program.clone(); - append_lifecycle( - sink.as_ref(), - &root_session_route(&root_session), - &config.agent_id, - session_started_record( - Some(root_session.clone()), - Some(config.agent_id.clone()), - CaptureMode::Run, - Some(&gateway.listen), - Some(program.as_str()), - ), - )?; - - let implant = enrich_with_session( - spec, - SessionImplantOpts { - listen: &gateway.listen, - root_session: &root_session, - overlay: &overlay_hint, - overlay_record: overlay_record.as_ref(), - run_storage: &storage, - capture_storage: &capture_storage, - config_path: &config_snapshot, - gateway_enabled: opts.gateway_enabled, - local_gateway_auth: opts.gateway_enabled - && config - .models - .iter() - .any(|route| route.api_key.is_some() || route.api_key_env.is_some()), - }, - )?; - run_record.environment.runtime_injected_keys = implant.env.keys().cloned().collect(); - run_record.write()?; - if opts.vm_network && opts.gateway_enabled { - rewrite_vm_gateway_implant(spec, &gateway.listen); - } - inject_krun_overlay_metadata(spec, &overlay_hint, overlay_record.as_ref()); - - Ok(AttemptSession { - root_session, - agent_id: config.agent_id.clone(), - overlay_record, - gateway: Some(gateway), - vm_network, - network_metrics: Some(network_metrics), - overlay: overlay_mount, - sink: Some(sink), - started_at: Instant::now(), - run_record, - _control: control, - _lease: lease, - }) -} - -/// Prepare a durable OverlayFS Run without enabling the optional Gateway. -pub(crate) fn prepare_overlay_attempt( - spec: &mut RunSpec, - opts: OverlayAttemptPrepareOpts<'_>, -) -> anyhow::Result { - let storage = opts - .storage - .canonicalize() - .unwrap_or_else(|_| opts.storage.to_path_buf()); - let root_session = spec.run_id.as_str().to_string(); - let mut overlay_cfg = persisting_gateway::config::OverlayConfig::default(); - apply_overlay_override(&mut overlay_cfg, &opts.overlay); - let prepared_overlay = prepare_overlay( - &overlay_cfg, - &storage, - &root_session, - uses_krun_executor(spec), - )?; - let PreparedOverlay { - mount: overlay_mount, - hint: overlay_hint, - record: overlay_record, - lowers: overlay_lowers, - } = prepared_overlay; - let overlay_record = overlay_record.ok_or_else(|| { - anyhow::anyhow!("overlay preparation requested without a target or lower directory") - })?; - - let RunInvocation::Process(process) = &spec.invocation; - let command = std::iter::once(process.program.clone()) - .chain(process.args.iter().cloned()) - .collect::>(); - let lease = RunLease::acquire(&overlay_record.stage_dir)?; - let prepared_network = opts - .vm_network - .map(|network| prepare_vm_network(spec, network, None)) - .transpose()?; - let vm_network = prepared_network - .as_ref() - .map(|network| Arc::clone(&network.attachment)); - let network_metrics = prepared_network - .as_ref() - .map(|network| network.metrics.clone()); - let network_policy = prepared_network.map(|network| network.policy); - let mut run_record = RunRecord { - schema_version: 1, - run_id: spec.run_id.as_str().to_string(), - parent_run_id: spec.parent_run_id.as_ref().map(ToString::to_string), - task_id: spec.task_id.clone(), - session_id: root_session.clone(), - agent: spec.agent.name.clone(), - pid: std::process::id(), - command, - executor: executor_from_spec(spec), - state: "running".into(), - started_at_unix_ms: crate::util::unix_now_ms(), - finished_at_unix_ms: None, - storage: storage.clone(), - workspace: workspace_from_spec(spec), - overlaynet_listen: None, - network_interception: vm_network - .as_ref() - .map(|_| persisting_overlaynet::InterceptionProfile::vm_smoltcp()), - network_interception_metrics: None, - gateway_listen: None, - network: serde_json::to_value(&spec.capabilities.network)?, - network_policy, - environment: environment_from_spec(spec), - resource_limits: spec.runtime.resource_limits.clone(), - overlay: Some(overlay_record.clone()), - overlay_lowers, - lineage: lineage_from_spec(spec), - orchestration: orchestration_from_spec(spec), - }; - run_record.write()?; - let control = RunControlServer::start(&run_record)?; - - let mut plan = ImplantPlan { - env: ImplantPlan::marker_env(), - cwd: overlay_hint.merged_dir.clone(), - overlay: overlay_hint, - notes: vec![format!( - "filesystem: overlay target={} staging={} (apply later unless auto_apply)", - overlay_record.target.display(), - overlay_record.stage_dir.display() - )], - }; - plan.env - .insert("PERSISTING_RUN_ID".into(), spec.run_id.as_str().to_string()); - plan.env - .insert("PERSISTING_AGENT".into(), spec.agent.name.clone()); - plan.env.insert( - "PERSISTING_PVISOR_STORAGE".into(), - storage.display().to_string(), - ); - plan.env.insert( - "PERSISTING_OVERLAY_TARGET".into(), - overlay_record.target.display().to_string(), - ); - plan.env.insert( - "PERSISTING_OVERLAY_STAGE".into(), - overlay_record.stage_dir.display().to_string(), - ); - plan.env - .insert("PERSISTING_OVERLAY_ID".into(), overlay_record.id.clone()); - if vm_network.is_some() { - mark_vm_network(&mut plan); - } - match &overlay_record.upper { - super::overlay::OverlayUpper::Directory { upper_dir, .. } => { - plan.env.insert( - "PERSISTING_OVERLAY_UPPER".into(), - upper_dir.display().to_string(), - ); - } - super::overlay::OverlayUpper::Jujutsu { - store_path, - workspace, - upper_dir, - } => { - plan.env.insert( - "PERSISTING_OVERLAY_UPPER".into(), - upper_dir.display().to_string(), - ); - plan.env.insert( - "PERSISTING_OVERLAY_JUJUTSU_STORE".into(), - store_path.display().to_string(), - ); - plan.env.insert( - "PERSISTING_OVERLAY_JUJUTSU_WORKSPACE".into(), - workspace.clone(), - ); - } - } - let RunInvocation::Process(ref mut process) = spec.invocation; - apply_implant(process, &plan); - run_record.environment.runtime_injected_keys = plan.env.keys().cloned().collect(); - run_record.write()?; - spec.metadata - .insert("pvisor.runtime.implant".into(), plan.as_metadata_json()); - inject_krun_overlay_metadata(spec, &plan.overlay, Some(&overlay_record)); - - Ok(AttemptSession { - root_session, - agent_id: spec.agent.name.clone(), - overlay_record: Some(overlay_record), - gateway: None, - vm_network, - network_metrics, - overlay: overlay_mount, - sink: None, - started_at: Instant::now(), - run_record, - _control: control, - _lease: lease, - }) -} - -/// Prepare metadata-only durable Run storage without Gateway or OverlayFS. -pub(crate) fn prepare_storage_attempt( - spec: &mut RunSpec, - storage: &Path, - vm_network_opts: Option, -) -> anyhow::Result { - let storage = storage - .canonicalize() - .unwrap_or_else(|_| storage.to_path_buf()); - let root_session = spec.run_id.as_str().to_string(); - let RunInvocation::Process(process) = &spec.invocation; - let command = std::iter::once(process.program.clone()) - .chain(process.args.iter().cloned()) - .collect::>(); - let lease = RunLease::acquire(&storage)?; - let prepared_network = vm_network_opts - .map(|network| prepare_vm_network(spec, network, None)) - .transpose()?; - let vm_network = prepared_network - .as_ref() - .map(|network| Arc::clone(&network.attachment)); - let network_metrics = prepared_network - .as_ref() - .map(|network| network.metrics.clone()); - let network_policy = prepared_network.map(|network| network.policy); - let mut run_record = RunRecord { - schema_version: 1, - run_id: root_session.clone(), - parent_run_id: spec.parent_run_id.as_ref().map(ToString::to_string), - task_id: spec.task_id.clone(), - session_id: root_session.clone(), - agent: spec.agent.name.clone(), - pid: std::process::id(), - command, - executor: executor_from_spec(spec), - state: "running".into(), - started_at_unix_ms: crate::util::unix_now_ms(), - finished_at_unix_ms: None, - storage: storage.clone(), - workspace: workspace_from_spec(spec), - overlaynet_listen: None, - network_interception: vm_network - .as_ref() - .map(|_| persisting_overlaynet::InterceptionProfile::vm_smoltcp()), - network_interception_metrics: None, - gateway_listen: None, - network: serde_json::to_value(&spec.capabilities.network)?, - network_policy, - environment: environment_from_spec(spec), - resource_limits: spec.runtime.resource_limits.clone(), - overlay: None, - overlay_lowers: Vec::new(), - lineage: lineage_from_spec(spec), - orchestration: orchestration_from_spec(spec), - }; - run_record.write()?; - let control = RunControlServer::start(&run_record)?; - - let mut plan = ImplantPlan { - env: ImplantPlan::marker_env(), - cwd: None, - overlay: OverlayHint::default(), - notes: vec![format!("durable Run storage: {}", storage.display())], - }; - plan.env - .insert("PERSISTING_RUN_ID".into(), root_session.clone()); - plan.env - .insert("PERSISTING_AGENT".into(), spec.agent.name.clone()); - plan.env.insert( - "PERSISTING_PVISOR_STORAGE".into(), - storage.display().to_string(), - ); - if vm_network.is_some() { - mark_vm_network(&mut plan); - } - let RunInvocation::Process(ref mut process) = spec.invocation; - apply_implant(process, &plan); - run_record.environment.runtime_injected_keys = plan.env.keys().cloned().collect(); - run_record.write()?; - spec.metadata - .insert("pvisor.runtime.implant".into(), plan.as_metadata_json()); - - Ok(AttemptSession { - root_session, - agent_id: spec.agent.name.clone(), - overlay_record: None, - gateway: None, - vm_network, - network_metrics, - overlay: None, - sink: None, - started_at: Instant::now(), - run_record, - _control: control, - _lease: lease, - }) -} - -fn start_vm_network( - spec: &mut RunSpec, - opts: VmNetworkPrepareOpts, - gateway: Option<(&str, bool)>, - metrics: InterceptionMetrics, - bandwidth_registry: BandwidthRegistry, -) -> anyhow::Result>>> { - let policy = NetworkPolicy::compile(&opts.network)?; - let egress = - EgressRuntime::with_bandwidth_registry(policy, opts.controller, bandwidth_registry); - let mut config = persisting_overlaynet::vm::VmNetworkConfig::new( - egress, - EgressContext { - run_id: Some(spec.run_id.as_str().to_owned()), - attempt_id: Some(opts.attempt_id), - storyline_id: None, - }, - ); - config.metrics = metrics; - if let Some((listen, _)) = gateway.filter(|(_, enabled)| *enabled) { - let host: std::net::SocketAddr = listen - .strip_prefix("http://") - .or_else(|| listen.strip_prefix("https://")) - .unwrap_or(listen) - .parse() - .with_context(|| format!("parse Attempt Gateway listen address `{listen}`"))?; - config.gateway = Some(persisting_overlaynet::vm::VmGatewayRoute { - guest_port: host.port(), - host, - }); - } - let (backend, guest_stream) = persisting_overlaynet::vm::VmNetwork::start(config)?; - spec.metadata.insert( - "pvisor.network.driver".into(), - serde_json::Value::String("vm-smoltcp".into()), - ); - spec.metadata.insert( - "pvisor.network.guest_ipv4".into(), - serde_json::Value::String(persisting_overlaynet::vm::GUEST_IPV4.to_string()), - ); - Ok(Arc::new(std::sync::Mutex::new(Some(VmNetworkAttachment { - guest_stream, - backend, - })))) -} - -fn prepare_vm_network( - spec: &mut RunSpec, - opts: VmNetworkPrepareOpts, - gateway: Option<(&str, bool)>, -) -> anyhow::Result { - let metrics = InterceptionMetrics::default(); - let policy = serde_json::to_value(&opts.network)?; - let attachment = start_vm_network( - spec, - opts, - gateway, - metrics.clone(), - BandwidthRegistry::default(), - )?; - Ok(PreparedVmNetwork { - attachment, - metrics, - policy, - }) -} - -fn rewrite_vm_gateway_implant(spec: &mut RunSpec, listen: &str) { - let listen = listen.trim_end_matches('/'); - let listen_authority = listen - .strip_prefix("http://") - .or_else(|| listen.strip_prefix("https://")) - .unwrap_or(listen); - let gateway_port = listen_authority - .rsplit_once(':') - .and_then(|(_, port)| port.parse::().ok()) - .expect("Gateway listen address was validated before implant rewriting"); - let virtual_base = format!( - "http://{}:{gateway_port}", - persisting_overlaynet::vm::ROUTER_IPV4 - ); - let mut source_bases = vec![ - format!("http://{listen_authority}"), - format!("https://{listen_authority}"), - ]; - if listen_authority.starts_with("127.0.0.1:") { - source_bases.push(format!("http://localhost:{gateway_port}")); - source_bases.push(format!("https://localhost:{gateway_port}")); - } else if listen_authority.starts_with("localhost:") { - source_bases.push(format!("http://127.0.0.1:{gateway_port}")); - source_bases.push(format!("https://127.0.0.1:{gateway_port}")); - } - let RunInvocation::Process(process) = &mut spec.invocation; - for value in process.env.values_mut() { - for source in &source_bases { - if value.contains(source) { - *value = value.replace(source, &virtual_base); - } - } - } - for argument in &mut process.args { - for source in &source_bases { - if argument.contains(source) { - *argument = argument.replace(source, &virtual_base); - } - } - } - let no_proxy = format!( - "127.0.0.1,localhost,{}", - persisting_overlaynet::vm::ROUTER_IPV4 - ); - process.env.insert("NO_PROXY".into(), no_proxy.clone()); - process.env.insert("no_proxy".into(), no_proxy); - process - .env - .insert("PERSISTING_GATEWAY_VIRTUAL_ADDR".into(), virtual_base); -} - -fn lineage_from_spec(spec: &RunSpec) -> Option { - spec.metadata - .get("pvisor.lineage") - .cloned() - .and_then(|value| serde_json::from_value(value).ok()) -} - -fn orchestration_from_spec( - spec: &RunSpec, -) -> std::collections::BTreeMap { - spec.metadata - .iter() - .filter(|(key, _)| key.starts_with("ppilot.") || key.starts_with("persisting.ppilot.")) - .map(|(key, value)| (key.clone(), value.clone())) - .collect() -} - -fn environment_from_spec(spec: &RunSpec) -> EnvironmentProjection { - if let Some(value) = spec.metadata.get("pvisor.environment") { - let inherits_host = value - .get("inherits_host") - .and_then(serde_json::Value::as_bool) - .unwrap_or(false); - let projected_keys = value - .get("projected_keys") - .and_then(serde_json::Value::as_array) - .into_iter() - .flatten() - .filter_map(serde_json::Value::as_str) - .map(str::to_owned) - .collect(); - return EnvironmentProjection { - inherits_host, - projected_keys, - runtime_injected_keys: Vec::new(), - }; - } - let RunInvocation::Process(process) = &spec.invocation; - EnvironmentProjection { - inherits_host: process.inherit_env, - projected_keys: process.env.keys().cloned().collect(), - runtime_injected_keys: Vec::new(), - } -} - -fn workspace_from_spec(spec: &RunSpec) -> Option { - spec.metadata - .get("pvisor.workspace") - .and_then(serde_json::Value::as_str) - .map(PathBuf::from) -} - -fn executor_from_spec(spec: &RunSpec) -> Option { - spec.metadata - .get("pvisor.executor") - .cloned() - .and_then(|value| serde_json::from_value(value).ok()) -} - -fn apply_overlay_override( - overlay_cfg: &mut persisting_gateway::config::OverlayConfig, - overlay_override: &OverlayHint, -) { - overlay_cfg.backend = overlay_override.backend; - overlay_cfg.auto_apply = overlay_override.auto_apply; - overlay_cfg.auto_discard = overlay_override.auto_discard; - overlay_cfg.protect_target = overlay_override.protect_target; - if let Some(stage) = &overlay_override.stage_dir { - overlay_cfg.stage_dir = Some(stage.display().to_string()); - overlay_cfg.enabled = true; - } - if let Some(merged) = &overlay_override.merged_dir { - overlay_cfg.merged_dir = Some(merged.display().to_string()); - overlay_cfg.enabled = true; - } - if let Some(upper) = &overlay_override.upper_dir { - overlay_cfg.upper_dir = Some(upper.display().to_string()); - overlay_cfg.backend = persisting_gateway::config::OverlayBackend::Directory; - overlay_cfg.jujutsu_store_path = None; - overlay_cfg.jujutsu_workspace = None; - } - if let Some(work) = &overlay_override.work_dir { - overlay_cfg.work_dir = Some(work.display().to_string()); - overlay_cfg.backend = persisting_gateway::config::OverlayBackend::Directory; - overlay_cfg.jujutsu_store_path = None; - overlay_cfg.jujutsu_workspace = None; - } - if let Some(store) = &overlay_override.jujutsu_store_path { - overlay_cfg.jujutsu_store_path = Some(store.display().to_string()); - overlay_cfg.backend = persisting_gateway::config::OverlayBackend::Jujutsu; - overlay_cfg.upper_dir = None; - overlay_cfg.work_dir = None; - } - if let Some(workspace) = &overlay_override.jujutsu_workspace { - overlay_cfg.jujutsu_workspace = Some(workspace.clone()); - } - if !overlay_override.lower_dirs.is_empty() { - // The final lower is the base/apply target; preceding entries are - // read-only compose layers ordered from highest to lowest priority. - if overlay_cfg.target.is_none() { - let (target, compose) = overlay_override - .lower_dirs - .split_last() - .expect("non-empty lower stack"); - overlay_cfg.target = Some(target.display().to_string()); - overlay_cfg.lower_dirs = compose.iter().map(|p| p.display().to_string()).collect(); - } else { - overlay_cfg.lower_dirs = overlay_override - .lower_dirs - .iter() - .map(|p| p.display().to_string()) - .collect(); - } - overlay_cfg.enabled = true; - } -} - -fn prepare_overlay( - overlay_cfg: &persisting_gateway::config::OverlayConfig, - storage: &Path, - root_session: &str, - mountless: bool, -) -> anyhow::Result { - if !overlay_cfg.enabled && overlay_cfg.target.is_none() { - return Ok(PreparedOverlay { - mount: None, - hint: OverlayHint::default(), - record: None, - lowers: Vec::new(), - }); - } - match resolve_overlay_workspace(overlay_cfg, storage, root_session)? { - Some(record) => { - if let Ok(existing) = RunRecord::read(&record.stage_dir) { - anyhow::bail!( - "OverlayFS stage {} already belongs to Run {}; choose a unique stage_dir", - record.stage_dir.display(), - existing.run_id - ); - } - let lowers = lower_stack_from_config(overlay_cfg, storage, &record.target); - let (mount, record) = if mountless { - (None, prepare_overlay_record_mountless(&record, &lowers)?) - } else { - let mount = mount_overlay_record(&record, &lowers)?; - let record = mount.record().clone(); - (Some(mount), record) - }; - let mut hint = hint_from_record(&record, lowers.clone()); - if mountless { - hint.merged_dir = None; - } - Ok(PreparedOverlay { - mount, - hint, - record: Some(record), - lowers, - }) - } - None => Ok(PreparedOverlay { - mount: None, - hint: OverlayHint::default(), - record: None, - lowers: Vec::new(), - }), - } -} - -fn uses_krun_executor(spec: &RunSpec) -> bool { - executor_from_spec(spec).is_some_and(|executor| executor.name.starts_with("libkrun-")) -} - -fn inject_krun_overlay_metadata( - spec: &mut RunSpec, - hint: &OverlayHint, - record: Option<&OverlayRecord>, -) { - if !uses_krun_executor(spec) { - return; - } - let Some(record) = record else { - return; - }; - let (upper, work) = match &record.upper { - super::overlay::OverlayUpper::Directory { - upper_dir, - work_dir, - } => (upper_dir.clone(), Some(work_dir.clone())), - super::overlay::OverlayUpper::Jujutsu { upper_dir, .. } => (upper_dir.clone(), None), - }; - spec.metadata.insert( - "pvisor.vm.workspace_overlay".into(), - serde_json::json!({ - "lowers": hint.lower_dirs, - "upper": upper, - "work": work, - "preimages": record.stage_dir.join("preimages"), - "excluded": record.excluded_paths, - }), - ); -} - -struct SessionImplantOpts<'a> { - listen: &'a str, - root_session: &'a str, - overlay: &'a OverlayHint, - overlay_record: Option<&'a OverlayRecord>, - run_storage: &'a Path, - capture_storage: &'a Path, - config_path: &'a Path, - gateway_enabled: bool, - local_gateway_auth: bool, -} - -fn enrich_with_session( - spec: &mut RunSpec, - opts: SessionImplantOpts<'_>, -) -> anyhow::Result { - let SessionImplantOpts { - listen, - root_session, - overlay, - overlay_record, - run_storage, - capture_storage, - config_path, - gateway_enabled, - local_gateway_auth, - } = opts; - let mut plan = ImplantPlan { - env: ImplantPlan::marker_env(), - cwd: overlay.merged_dir.clone(), - overlay: overlay.clone(), - notes: Vec::new(), - }; - - plan.env - .insert("PERSISTING_RUN_ID".into(), spec.run_id.as_str().to_string()); - plan.env - .insert("PERSISTING_AGENT".into(), spec.agent.name.clone()); - plan.env.insert( - "PERSISTING_CAPTURE_CONFIG".into(), - config_path.display().to_string(), - ); - plan.env.insert( - "PERSISTING_CAPTURE_STORAGE".into(), - capture_storage.display().to_string(), - ); - plan.env.insert( - "PERSISTING_PVISOR_STORAGE".into(), - run_storage.display().to_string(), - ); - plan.notes - .push("network service: in-process HTTP proxy started".into()); - - for (key, value) in proxy_environment_with_local_auth(listen, root_session, local_gateway_auth) - { - plan.env.insert(key, value); - } - if !gateway_enabled { - for key in [ - "OPENAI_BASE_URL", - "OPENAI_API_BASE", - "AZURE_OPENAI_ENDPOINT", - "ANTHROPIC_BASE_URL", - "GEMINI_API_BASE", - ] { - plan.env.remove(key); - } - } - plan.notes - .push(format!("network service: proxy env → http://{listen}")); - if uses_krun_executor(spec) { - mark_vm_network(&mut plan); - } else { - plan.env.insert( - "PERSISTING_OVERLAYNET_DRIVER".into(), - "explicit-proxy".into(), - ); - plan.env.insert( - "PERSISTING_OVERLAYNET_STRENGTH".into(), - "cooperative".into(), - ); - plan.notes.push( - "network interception: explicit proxy (cooperative; direct sockets remain ambient)" - .into(), - ); - } - - match &spec.capabilities.network { - NetworkCapability::Ambient => { - plan.env - .insert("PERSISTING_NETWORK_POLICY".into(), "ambient".into()); - plan.notes - .push("network: ambient (from capture config)".into()); - } - NetworkCapability::Deny => { - plan.env - .insert("PERSISTING_NETWORK_POLICY".into(), "deny".into()); - plan.notes.push(if uses_krun_executor(spec) { - "network: deny on the non-bypassable VM data plane".into() - } else { - "network: deny for traffic intercepted by the proxy".into() - }); - } - NetworkCapability::AllowList { hosts, rules } => { - plan.env - .insert("PERSISTING_NETWORK_POLICY".into(), "allowlist".into()); - plan.env - .insert("PERSISTING_NETWORK_ALLOWLIST".into(), hosts.join(",")); - if let Ok(serialized) = serde_json::to_string(rules) { - plan.env - .insert("PERSISTING_NETWORK_RULES".into(), serialized); - } - plan.notes.push(format!( - "network: allowlist ({} legacy hosts, {} structured rules, applied to {} traffic)", - hosts.len(), - rules.len(), - if uses_krun_executor(spec) { - "VM" - } else { - "intercepted proxy" - }, - )); - } - NetworkCapability::Policy { - default_action, - allow, - deny, - limits, - } => { - plan.env.insert( - "PERSISTING_NETWORK_POLICY".into(), - match default_action { - persisting_agentctl::NetworkDefaultAction::Allow => "default-allow", - persisting_agentctl::NetworkDefaultAction::Deny => "default-deny", - } - .into(), - ); - for (key, value) in [ - ("PERSISTING_NETWORK_RULES", allow), - ("PERSISTING_NETWORK_DENY", deny), - ] { - if let Ok(serialized) = serde_json::to_string(value) { - plan.env.insert(key.into(), serialized); - } - } - if let Ok(serialized) = serde_json::to_string(limits) { - plan.env - .insert("PERSISTING_NETWORK_LIMITS".into(), serialized); - } - plan.notes.push(format!( - "network: policy ({} allow, {} deny, {} bandwidth limits, applied to {} traffic)", - allow.len(), - deny.len(), - limits.len(), - if uses_krun_executor(spec) { - "VM" - } else { - "intercepted proxy" - }, - )); - } - } - - if let Some(rec) = overlay_record { - plan.env.insert( - "PERSISTING_OVERLAY_TARGET".into(), - rec.target.display().to_string(), - ); - match &rec.upper { - super::overlay::OverlayUpper::Directory { upper_dir, .. } => { - plan.env.insert( - "PERSISTING_OVERLAY_UPPER".into(), - upper_dir.display().to_string(), - ); - } - super::overlay::OverlayUpper::Jujutsu { - store_path, - workspace, - upper_dir, - } => { - plan.env.insert( - "PERSISTING_OVERLAY_UPPER".into(), - upper_dir.display().to_string(), - ); - plan.env.insert( - "PERSISTING_OVERLAY_JUJUTSU_STORE".into(), - store_path.display().to_string(), - ); - plan.env.insert( - "PERSISTING_OVERLAY_JUJUTSU_WORKSPACE".into(), - workspace.clone(), - ); - } - } - plan.env.insert( - "PERSISTING_OVERLAY_STAGE".into(), - rec.stage_dir.display().to_string(), - ); - plan.env - .insert("PERSISTING_OVERLAY_ID".into(), rec.id.clone()); - plan.notes.push(format!( - "filesystem: overlay target={} staging={} (apply later unless auto_apply)", - rec.target.display(), - rec.stage_dir.display() - )); - } else if overlay.merged_dir.is_some() { - plan.notes - .push("filesystem: embedded overlay merged root as cwd".into()); - } else { - plan.notes.push("filesystem: host view (no overlay)".into()); - } - - let RunInvocation::Process(ref mut process) = spec.invocation; - apply_implant(process, &plan); - if gateway_enabled { - inject_gateway_args(process, listen); - } - spec.metadata - .insert("pvisor.runtime.implant".into(), plan.as_metadata_json()); - Ok(plan) -} - -fn inject_gateway_args(process: &mut ProcessInvocation, listen: &str) { - let extra = client_gateway_config_args(&process.program, listen); - if extra.is_empty() { - return; - } - let mut args = extra; - args.append(&mut process.args); - process.args = args; -} - -pub(crate) fn apply_implant(process: &mut ProcessInvocation, plan: &ImplantPlan) { - for (key, value) in &plan.env { - process - .env - .entry(key.clone()) - .or_insert_with(|| value.clone()); - } - if process.cwd.is_none() - && let Some(cwd) = &plan.cwd - { - process.cwd = Some(cwd.display().to_string()); - } -} - -#[cfg(test)] -mod vm_network_tests { - use super::rewrite_vm_gateway_implant; - use persisting_agentctl::{RunInvocation, RunSpec}; - - #[test] - fn gateway_loopback_urls_and_embedded_arguments_are_rewritten() { - let mut spec = RunSpec::process("run-1", "agent", "codex"); - let RunInvocation::Process(process) = &mut spec.invocation; - process - .env - .insert("OPENAI_BASE_URL".into(), "http://127.0.0.1:19081/v1".into()); - process - .args - .push("openai_base_url=\"http://127.0.0.1:19081/v1\"".into()); - - rewrite_vm_gateway_implant(&mut spec, "127.0.0.1:19081"); - - let RunInvocation::Process(process) = &spec.invocation; - assert_eq!( - process.env.get("OPENAI_BASE_URL").map(String::as_str), - Some("http://192.0.2.1:19081/v1") - ); - assert_eq!( - process.args.last().map(String::as_str), - Some("openai_base_url=\"http://192.0.2.1:19081/v1\"") - ); - assert!(process.env["NO_PROXY"].contains("192.0.2.1")); - } -} diff --git a/crates/persisting-pvisor/src/runtime/implant.rs b/crates/persisting-pvisor/src/runtime/implant.rs deleted file mode 100644 index ac4e245c4..000000000 --- a/crates/persisting-pvisor/src/runtime/implant.rs +++ /dev/null @@ -1,60 +0,0 @@ -use serde_json::json; -use std::collections::BTreeMap; -use std::path::PathBuf; - -use persisting_gateway::config::OverlayBackend; - -/// Optional in-process FUSE overlay root for one Attempt. -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct OverlayHint { - /// Shared read-only lower layers (host paths). - pub lower_dirs: Vec, - /// Durable staging root containing upper storage and the merged mount. - pub stage_dir: Option, - /// Writable upper directory for this Attempt. - pub upper_dir: Option, - /// Work directory required by overlay implementations. - pub work_dir: Option, - /// Shared Jujutsu repository root for all OverlayFS forks. - pub jujutsu_store_path: Option, - /// Jujutsu workspace/fork name within the shared repository. - pub jujutsu_workspace: Option, - /// Merged mount point visible to the Agent as cwd/root when set. - pub merged_dir: Option, - /// Writable staging representation. - pub backend: OverlayBackend, - /// Apply staged changes when the Run exits successfully or unsuccessfully. - pub auto_apply: bool, - /// Discard staged changes when the Run exits. - pub auto_discard: bool, - /// Reject apply so an immutable image/cache lower cannot be mutated. - pub protect_target: bool, -} - -/// Environment + cwd plan injected beside the Agent process. -#[derive(Debug, Clone, Default)] -pub struct ImplantPlan { - pub env: BTreeMap, - pub cwd: Option, - pub overlay: OverlayHint, - pub notes: Vec, -} - -impl ImplantPlan { - pub fn marker_env() -> BTreeMap { - let mut env = BTreeMap::new(); - env.insert("PERSISTING_PVISOR_RUNTIME".into(), "1".into()); - env.insert("PERSISTING_PVISOR_ROLE".into(), "supervisor".into()); - env - } - - pub fn as_metadata_json(&self) -> serde_json::Value { - json!({ - "env_keys": self.env.keys().cloned().collect::>(), - "cwd": self.cwd.as_ref().map(|p| p.display().to_string()), - "overlay_merged": self.overlay.merged_dir.as_ref().map(|p| p.display().to_string()), - "overlay_stage": self.overlay.stage_dir.as_ref().map(|p| p.display().to_string()), - "notes": self.notes, - }) - } -} diff --git a/crates/persisting-pvisor/src/runtime/mod.rs b/crates/persisting-pvisor/src/runtime/mod.rs deleted file mode 100644 index 3b89d9b52..000000000 --- a/crates/persisting-pvisor/src/runtime/mod.rs +++ /dev/null @@ -1,32 +0,0 @@ -//! Attempt preparation for pVisor-owned runtime drivers. -//! -//! pVisor assembles the optional Gateway/OverlayNet driver, network policy, and -//! embedded OverlayFS before the Agent process starts. - -mod attempt; -mod implant; -mod overlay; -mod registry; -mod supervisor; - -pub(crate) use attempt::AttemptTeardown; -pub(crate) use attempt::VmNetworkAttachment; -pub(crate) use supervisor::RuntimeSupervisor; -pub(crate) use supervisor::RuntimeSupervisorBuilder; - -pub use implant::{ImplantPlan, OverlayHint}; -#[cfg(all(test, target_os = "macos"))] -pub use overlay::apply_overlay; -pub use overlay::{ - ApplySelection, ChangeEntry, ChangeEntryType, ChangeKind, OverlayRecord, OverlayState, - OverlayUpper, ReadOnlyOverlayMount, apply_overlay_selected, discard_overlay, - load_apply_records, load_overlay_record, mount_overlay_record, mount_overlay_record_read_only, - overlay_changes, overlay_status, restore_overlay_upper, snapshot_overlay_upper, - write_overlay_record, -}; -pub use registry::{ - EnvironmentProjection, RunLease, RunLineage, RunRecord, all_runs, control_mount_inspect, - control_overlay_status, control_ping, control_unmount_inspect, default_run_home, is_live, - resolve_run, -}; -pub use supervisor::RuntimeCapabilities; diff --git a/crates/persisting-pvisor/src/runtime/overlay.rs b/crates/persisting-pvisor/src/runtime/overlay.rs deleted file mode 100644 index 9d460c9a1..000000000 --- a/crates/persisting-pvisor/src/runtime/overlay.rs +++ /dev/null @@ -1,3299 +0,0 @@ -//! In-process FUSE overlay mount + staging apply/discard. -//! -//! ```text -//! target (RO lower / apply destination) -//! + -//! staging/upper (writable deltas) -//! → -//! staging/merged (Agent cwd) -//! -//! After the Attempt: unmount, keep staging. -//! Review → apply_overlay (upper → target) | discard_overlay -//! ``` -//! -use super::implant::OverlayHint; -use crate::util::{atomic_write, create_dir_all_durable}; -use globset::{GlobBuilder, GlobSet, GlobSetBuilder}; -use persisting_gateway::config::{OverlayBackend, OverlayConfig}; -use persisting_overlay_core::{ - PathFingerprint, PathPreimage, fingerprint_at, load_preimages, preimage_journal_is_complete, - remove_preimages, -}; -use persisting_overlayfs::{ - OverlayMountConfig, OverlaySession, jujutsu_upper_dir, mount as mount_embedded_overlay, - snapshot_jujutsu_upper, -}; -use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; -use std::collections::{BTreeSet, HashMap}; -use std::ffi::{CString, OsStr}; -use std::fs::{self, File}; -use std::io; -use std::os::unix::ffi::OsStrExt; -use std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt}; -use std::path::{Component, Path, PathBuf}; -use std::time::Duration; - -const META_FILENAME: &str = "overlay.json"; -const APPLY_LEDGER_FILENAME: &str = "apply-ledger.json"; -const APPLY_LEDGER_SCHEMA_VERSION: u32 = 1; -const WHITEOUT_PREFIX: &str = ".wh."; -const OPAQUE_WHITEOUT: &str = ".wh..wh..opq"; -const OPAQUE_XATTRS: [&[u8]; 3] = [ - b"trusted.overlay.opaque", - b"user.overlay.opaque", - b"user.fuseoverlayfs.opaque", -]; - -#[derive(Debug, thiserror::Error)] -pub enum OverlayError { - #[error("overlay enabled but no target / lower_dirs configured")] - MissingTarget, - #[error("invalid overlay upper configuration: {0}")] - InvalidConfig(String), - #[error("overlay meta missing or invalid at {0}")] - Meta(String), - #[error("failed to prepare overlay directories: {0}")] - Prepare(#[source] std::io::Error), - #[error("embedded FUSE mount failed: {0}")] - Mount(String), - #[error("merged mount point not ready: {0}")] - NotReady(String), - #[error("overlay apply failed: {0}")] - Apply(String), - #[error("{0}")] - InvalidState(String), - #[error("overlay metadata update failed: {0}")] - Persist(String), - #[error("overlay finalization failed: {0}")] - Finalize(String), - #[error("io: {0}")] - Io(#[from] std::io::Error), -} - -/// Durable record of one overlay staging workspace (survives Attempt teardown). -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OverlayRecord { - pub id: String, - /// Monotonic reusable-environment generation. Terminal overlays are never - /// reopened; a reset creates the next generation over the same stage. - #[serde(default)] - pub generation: u64, - /// Target filesystem (apply destination + primary lower). - pub target: PathBuf, - pub upper: OverlayUpper, - pub merged_dir: PathBuf, - pub stage_dir: PathBuf, - /// Paths relative to the overlay root that are inaccessible through the - /// merged view. Root overlays use this to hide their own backing state. - #[serde(default)] - pub excluded_paths: Vec, - pub auto_apply: bool, - #[serde(default)] - pub auto_discard: bool, - /// Immutable lower targets (for example OCI cache entries) reject apply. - #[serde(default)] - pub protect_target: bool, - pub state: OverlayState, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum OverlayUpper { - Directory { - upper_dir: PathBuf, - work_dir: PathBuf, - }, - Jujutsu { - store_path: PathBuf, - workspace: String, - upper_dir: PathBuf, - }, -} - -impl OverlayUpper { - pub fn path(&self) -> &Path { - match self { - Self::Directory { upper_dir, .. } => upper_dir, - Self::Jujutsu { upper_dir, .. } => upper_dir, - } - } -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] -#[serde(rename_all = "snake_case")] -pub enum OverlayState { - /// Mounted / Agent may write. - Active, - /// Unmounted; upper retained for review. - Staged, - /// Upper applied onto target. - Applied, - /// Upper discarded. - Discarded, -} - -/// Summary of files present in upper (not a full recursive diff vs target). -#[derive(Debug, Clone)] -pub struct OverlayStatus { - pub changed_files: usize, - pub whiteouts: usize, - pub sample_paths: Vec, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] -#[serde(rename_all = "snake_case")] -pub enum ChangeKind { - Added, - Modified, - Deleted, - TypeChanged, - Opaque, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum ChangeEntryType { - File, - Directory, - Symlink, - Other, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct ChangeEntry { - pub path: String, - pub kind: ChangeKind, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub old_type: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub new_type: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub size_bytes: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub mode: Option, -} - -/// User-facing selection for one staged apply operation. Exact paths select -/// the path and its descendants; include/exclude values use git-style glob -/// matching against slash-separated paths relative to the overlay root. -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] -pub struct ApplySelection { - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub paths: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub includes: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub excludes: Vec, -} - -impl ApplySelection { - pub fn is_all(&self) -> bool { - self.paths.is_empty() && self.includes.is_empty() && self.excludes.is_empty() - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct ApplyRecord { - pub schema_version: u32, - pub apply_id: String, - pub created_at_unix_ms: u64, - pub overlay_id: String, - #[serde(default)] - pub overlay_generation: u64, - pub target: PathBuf, - pub selection: ApplySelection, - pub changes: Vec, - /// Exact dependency-closed paths selected by the prepared transaction. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub planned_paths: Vec, - /// Target state captured when each path was first mutated in the overlay. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub preimages: Vec, - /// Old ledgers contain only successful records and therefore deserialize - /// as committed. - #[serde(default = "committed_apply_state")] - pub state: ApplyRecordState, - pub remaining_changes: usize, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum ApplyRecordState { - Prepared, - TargetApplied, - Committed, -} - -fn committed_apply_state() -> ApplyRecordState { - ApplyRecordState::Committed -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ApplyOutcome { - pub apply_id: String, - pub applied: Vec, - pub remaining: Vec, -} - -#[derive(Debug, Clone)] -pub struct ApplyPlan { - pub selected: Vec, - selected_paths: BTreeSet, -} - -#[derive(Debug, Default, Serialize, Deserialize)] -struct ApplyLedger { - #[serde(default = "apply_ledger_schema_version")] - schema_version: u32, - #[serde(default)] - records: Vec, -} - -fn apply_ledger_schema_version() -> u32 { - APPLY_LEDGER_SCHEMA_VERSION -} - -/// Live in-process FUSE mount; unmounted on [`Self::unmount`] / Drop. -/// Staging directories are **not** deleted on unmount. -pub struct OverlayMount { - record: OverlayRecord, - session: Option, -} - -/// Independent kernel-enforced read-only view used by `pvisor inspect`. -pub struct ReadOnlyOverlayMount { - session: Option, - mountpoint: PathBuf, -} - -struct TargetApplyLock { - file: File, -} - -impl TargetApplyLock { - fn acquire(target: &Path) -> Result { - let directory = std::env::temp_dir() - .join(format!("persisting-pvisor-apply-locks-{}", unsafe { - libc::geteuid() - })); - fs::create_dir_all(&directory)?; - fs::set_permissions(&directory, fs::Permissions::from_mode(0o700))?; - let identity = fs::canonicalize(target).unwrap_or_else(|_| target.to_path_buf()); - let path = directory.join(format!("{}.lock", path_digest(&identity))); - let file = fs::OpenOptions::new() - .read(true) - .write(true) - .create(true) - .truncate(false) - .mode(0o600) - .open(path)?; - fs2::FileExt::lock_exclusive(&file)?; - Ok(Self { file }) - } -} - -impl Drop for TargetApplyLock { - fn drop(&mut self) { - let _ = fs2::FileExt::unlock(&self.file); - } -} - -fn path_digest(path: &Path) -> String { - use std::fmt::Write as _; - - let digest = Sha256::digest(path.as_os_str().as_bytes()); - let mut encoded = String::with_capacity(digest.len() * 2); - for byte in digest { - let _ = write!(&mut encoded, "{byte:02x}"); - } - encoded -} - -impl ReadOnlyOverlayMount { - pub fn mountpoint(&self) -> &Path { - &self.mountpoint - } - - pub fn unmount(mut self) -> anyhow::Result<()> { - self.unmount_inner() - } - - fn unmount_inner(&mut self) -> anyhow::Result<()> { - if let Some(session) = self.session.take() { - session.unmount()?; - } - if self.mountpoint.is_dir() { - let _ = fs::remove_dir(&self.mountpoint); - } - Ok(()) - } -} - -impl Drop for ReadOnlyOverlayMount { - fn drop(&mut self) { - let _ = self.unmount_inner(); - } -} - -impl OverlayMount { - pub fn mountpoint(&self) -> &Path { - &self.record.merged_dir - } - - pub fn record(&self) -> &OverlayRecord { - &self.record - } - - /// Unmount and mark staging as [`OverlayState::Staged`] (keep upper). - pub fn unmount(mut self) -> anyhow::Result { - self.unmount_inner()?; - self.record.state = OverlayState::Staged; - write_overlay_record(&self.record)?; - Ok(self.record.clone()) - } - - fn unmount_inner(&mut self) -> anyhow::Result<()> { - if let Some(session) = self.session.take() { - session.unmount()?; - } - if !self.record.merged_dir.starts_with(&self.record.stage_dir) - && self.record.merged_dir.is_dir() - { - fs::remove_dir(&self.record.merged_dir)?; - if let Some(parent) = self.record.merged_dir.parent() { - let _ = fs::remove_dir(parent); - } - } - Ok(()) - } -} - -impl Drop for OverlayMount { - fn drop(&mut self) { - let _ = self.unmount_inner(); - if self.record.state == OverlayState::Active { - self.record.state = OverlayState::Staged; - let _ = write_overlay_record(&self.record); - } - } -} - -/// Resolve config into concrete paths (target + staging layout). -pub fn resolve_overlay_workspace( - cfg: &OverlayConfig, - storage: &Path, - session_id: &str, -) -> Result, OverlayError> { - if !cfg.enabled && cfg.target.is_none() && cfg.lower_dirs.is_empty() { - return Ok(None); - } - match cfg.backend { - OverlayBackend::Directory - if cfg.jujutsu_store_path.is_some() || cfg.jujutsu_workspace.is_some() => - { - return Err(OverlayError::InvalidConfig( - "directory cannot be combined with Jujutsu options".into(), - )); - } - OverlayBackend::Jujutsu if cfg.upper_dir.is_some() || cfg.work_dir.is_some() => { - return Err(OverlayError::InvalidConfig( - "jujutsu cannot be combined with upper_dir or work_dir".into(), - )); - } - _ => {} - } - - let resolve = |p: &str| -> PathBuf { - let path = PathBuf::from(p); - if path.is_absolute() { - path - } else { - storage.join(path) - } - }; - - let target = if let Some(t) = &cfg.target { - resolve(t) - } else if let Some(first) = cfg.lower_dirs.first() { - resolve(first) - } else { - return Err(OverlayError::MissingTarget); - }; - - let stage_dir = cfg - .stage_dir - .as_deref() - .map(resolve) - .unwrap_or_else(|| storage.join(".overlay").join(session_id)); - - let upper = match cfg.backend { - OverlayBackend::Directory => OverlayUpper::Directory { - upper_dir: cfg - .upper_dir - .as_deref() - .map(resolve) - .unwrap_or_else(|| stage_dir.join("upper")), - work_dir: cfg - .work_dir - .as_deref() - .map(resolve) - .unwrap_or_else(|| stage_dir.join("work")), - }, - OverlayBackend::Jujutsu => { - let store_path = cfg - .jujutsu_store_path - .as_deref() - .map(resolve) - .unwrap_or_else(|| storage.join(".overlay").join("jujutsu")); - let workspace = cfg - .jujutsu_workspace - .clone() - .unwrap_or_else(|| session_id.to_owned()); - let upper_dir = jujutsu_upper_dir(&store_path, &workspace) - .map_err(|error| OverlayError::InvalidConfig(error.to_string()))?; - OverlayUpper::Jujutsu { - store_path, - workspace, - upper_dir, - } - } - }; - let resolved_lowers = cfg - .lower_dirs - .iter() - .map(|path| resolve(path)) - .collect::>(); - let stage_is_nested = target != Path::new("/") - && std::iter::once(&target) - .chain(resolved_lowers.iter()) - .any(|lower| stage_dir.as_path() != lower.as_path() && stage_dir.starts_with(lower)); - let merged = cfg.merged_dir.as_deref().map(resolve).unwrap_or_else(|| { - if stage_is_nested { - storage - .join(".overlay-mounts") - .join(session_id) - .join("merged") - } else { - stage_dir.join("merged") - } - }); - - let mut backing_paths = vec![stage_dir.clone(), merged.clone()]; - match &upper { - OverlayUpper::Directory { - upper_dir, - work_dir, - } => { - backing_paths.push(upper_dir.clone()); - backing_paths.push(work_dir.clone()); - } - OverlayUpper::Jujutsu { store_path, .. } => backing_paths.push(store_path.clone()), - } - backing_paths.extend(resolved_lowers); - let mut excluded_paths = backing_paths - .into_iter() - .filter_map(|path| { - path.strip_prefix(&target) - .ok() - .filter(|relative| !relative.as_os_str().is_empty()) - .map(Path::to_path_buf) - }) - .collect::>(); - excluded_paths.sort_by_key(|path| path.components().count()); - let mut minimal_exclusions = Vec::::new(); - for path in excluded_paths { - if !minimal_exclusions - .iter() - .any(|parent| path.starts_with(parent)) - { - minimal_exclusions.push(path); - } - } - let excluded_paths = minimal_exclusions; - - Ok(Some(OverlayRecord { - id: session_id.to_string(), - generation: 0, - target, - upper, - merged_dir: merged, - stage_dir, - excluded_paths, - auto_apply: cfg.auto_apply, - auto_discard: cfg.auto_discard, - protect_target: cfg.protect_target, - state: OverlayState::Active, - })) -} - -/// Build an [`OverlayHint`] from a resolved record + full lower stack. -pub fn hint_from_record(record: &OverlayRecord, lower_dirs: Vec) -> OverlayHint { - let (upper_dir, work_dir, jujutsu_store_path, jujutsu_workspace) = match &record.upper { - OverlayUpper::Directory { - upper_dir, - work_dir, - } => (Some(upper_dir.clone()), Some(work_dir.clone()), None, None), - OverlayUpper::Jujutsu { - store_path, - workspace, - .. - } => ( - None, - None, - Some(store_path.clone()), - Some(workspace.clone()), - ), - }; - OverlayHint { - lower_dirs, - stage_dir: Some(record.stage_dir.clone()), - upper_dir, - work_dir, - jujutsu_store_path, - jujutsu_workspace, - merged_dir: Some(record.merged_dir.clone()), - backend: match &record.upper { - OverlayUpper::Directory { .. } => OverlayBackend::Directory, - OverlayUpper::Jujutsu { .. } => OverlayBackend::Jujutsu, - }, - auto_apply: record.auto_apply, - auto_discard: record.auto_discard, - protect_target: record.protect_target, - } -} - -/// Lower stack for mount: compose layers first (top), then the base target. -pub fn lower_stack_from_config(cfg: &OverlayConfig, storage: &Path, target: &Path) -> Vec { - let resolve = |p: &str| -> PathBuf { - let path = PathBuf::from(p); - if path.is_absolute() { - path - } else { - storage.join(path) - } - }; - let mut lowers: Vec = cfg.lower_dirs.iter().map(|p| resolve(p)).collect(); - lowers.retain(|p| p != target); - lowers.push(target.to_path_buf()); - lowers -} - -/// Mount the overlay in-process; pVisor becomes the FUSE userspace server. -pub fn mount_overlay_record( - record: &OverlayRecord, - lower_dirs: &[PathBuf], -) -> Result { - if lower_dirs.is_empty() { - return Err(OverlayError::MissingTarget); - } - for dir in lower_dirs - .iter() - .chain([&record.merged_dir, &record.stage_dir]) - { - create_dir_all_durable(dir) - .map_err(|error| OverlayError::Prepare(io::Error::other(error)))?; - } - match &record.upper { - OverlayUpper::Directory { - upper_dir, - work_dir, - } => { - create_dir_all_durable(upper_dir) - .map_err(|error| OverlayError::Prepare(io::Error::other(error)))?; - create_dir_all_durable(work_dir) - .map_err(|error| OverlayError::Prepare(io::Error::other(error)))?; - } - OverlayUpper::Jujutsu { store_path, .. } => { - create_dir_all_durable(store_path) - .map_err(|error| OverlayError::Prepare(io::Error::other(error)))?; - } - } - - let mut config = match &record.upper { - OverlayUpper::Directory { - upper_dir, - work_dir, - } => OverlayMountConfig::new( - lower_dirs.to_vec(), - upper_dir.clone(), - Some(work_dir.clone()), - record.merged_dir.clone(), - ), - OverlayUpper::Jujutsu { - store_path, - workspace, - .. - } => OverlayMountConfig::new_jujutsu( - lower_dirs.to_vec(), - store_path.clone(), - workspace.clone(), - record.merged_dir.clone(), - ), - }; - config.fsname = format!("pvisor-{}", record.id); - config.excluded_paths = record.excluded_paths.clone(); - config.preimage_dir = Some(record.stage_dir.join("preimages")); - let session = mount_embedded_overlay(config).map_err(embedded_mount_error)?; - wait_merged_ready(&record.merged_dir, &session)?; - - let mut record = record.clone(); - record.state = OverlayState::Active; - write_overlay_record(&record)?; - - Ok(OverlayMount { - record, - session: Some(session), - }) -} - -/// Prepare durable overlay backing directories for a consumer that serves the -/// union itself (currently libkrun virtio-fs), without creating a host mount. -pub(crate) fn prepare_overlay_record_mountless( - record: &OverlayRecord, - lower_dirs: &[PathBuf], -) -> Result { - if lower_dirs.is_empty() { - return Err(OverlayError::MissingTarget); - } - for dir in lower_dirs.iter().chain([&record.stage_dir]) { - create_dir_all_durable(dir) - .map_err(|error| OverlayError::Prepare(io::Error::other(error)))?; - } - create_dir_all_durable(&record.stage_dir.join("preimages/entries")) - .map_err(|error| OverlayError::Prepare(io::Error::other(error)))?; - match &record.upper { - OverlayUpper::Directory { - upper_dir, - work_dir, - } => { - create_dir_all_durable(upper_dir) - .map_err(|error| OverlayError::Prepare(io::Error::other(error)))?; - create_dir_all_durable(work_dir) - .map_err(|error| OverlayError::Prepare(io::Error::other(error)))?; - } - OverlayUpper::Jujutsu { - store_path, - workspace, - upper_dir, - } => { - create_dir_all_durable(store_path) - .map_err(|error| OverlayError::Prepare(io::Error::other(error)))?; - persisting_overlayfs::prepare_jujutsu_upper(store_path, workspace) - .map_err(OverlayError::Prepare)?; - create_dir_all_durable(upper_dir) - .map_err(|error| OverlayError::Prepare(io::Error::other(error)))?; - } - } - let mut record = record.clone(); - record.state = OverlayState::Active; - write_overlay_record(&record)?; - Ok(record) -} - -pub(crate) fn stage_overlay_record(record: &mut OverlayRecord) -> anyhow::Result<()> { - if record.state == OverlayState::Active { - record.state = OverlayState::Staged; - if let OverlayUpper::Jujutsu { - store_path, - workspace, - .. - } = &record.upper - { - snapshot_jujutsu_upper(store_path, workspace)?; - } - write_overlay_record(record)?; - } - Ok(()) -} - -/// Mount the same lower/upper projection without permitting any mutation. -/// The kernel's read-only FUSE mount rejects writes before they reach the -/// writable overlay implementation. -pub fn mount_overlay_record_read_only( - record: &OverlayRecord, - lower_dirs: &[PathBuf], - mountpoint: &Path, -) -> Result { - if lower_dirs.is_empty() { - return Err(OverlayError::MissingTarget); - } - fs::create_dir_all(mountpoint).map_err(OverlayError::Prepare)?; - let mut config = match &record.upper { - OverlayUpper::Directory { - upper_dir, - work_dir, - } => OverlayMountConfig::new( - lower_dirs.to_vec(), - upper_dir.clone(), - Some(work_dir.clone()), - mountpoint.to_path_buf(), - ), - OverlayUpper::Jujutsu { - store_path, - workspace, - .. - } => OverlayMountConfig::new_jujutsu( - lower_dirs.to_vec(), - store_path.clone(), - workspace.clone(), - mountpoint.to_path_buf(), - ), - }; - config.fsname = format!("pvisor-inspect-{}", record.id); - config.excluded_paths = record.excluded_paths.clone(); - config.read_only = true; - let session = mount_embedded_overlay(config).map_err(embedded_mount_error)?; - wait_merged_ready(mountpoint, &session)?; - Ok(ReadOnlyOverlayMount { - session: Some(session), - mountpoint: mountpoint.to_path_buf(), - }) -} - -fn embedded_mount_error(error: anyhow::Error) -> OverlayError { - #[cfg(target_os = "macos")] - { - OverlayError::Mount(format!( - "{error}; macOS staged workspaces require macFUSE 5 to be installed and enabled (brew install --cask macfuse)" - )) - } - #[cfg(not(target_os = "macos"))] - { - OverlayError::Mount(error.to_string()) - } -} - -pub fn overlay_meta_path(stage_dir: &Path) -> PathBuf { - stage_dir.join(META_FILENAME) -} - -pub fn write_overlay_record(record: &OverlayRecord) -> Result<(), OverlayError> { - let path = overlay_meta_path(&record.stage_dir); - let body = serde_json::to_string_pretty(record) - .map_err(|e| OverlayError::Persist(format!("serialize meta: {e}")))?; - atomic_write(&path, body.as_bytes(), 0o600) - .map_err(|error| OverlayError::Persist(format!("{}: {error:#}", path.display())))?; - Ok(()) -} - -pub fn load_overlay_record(stage_dir: &Path) -> Result { - let path = overlay_meta_path(stage_dir); - let raw = - fs::read_to_string(&path).map_err(|_| OverlayError::Meta(path.display().to_string()))?; - serde_json::from_str(&raw).map_err(|e| OverlayError::Meta(format!("{}: {e}", path.display()))) -} - -pub fn overlay_status(record: &OverlayRecord) -> Result { - let upper_dir = match &record.upper { - OverlayUpper::Directory { upper_dir, .. } | OverlayUpper::Jujutsu { upper_dir, .. } => { - upper_dir - } - }; - let mut changed = 0usize; - let mut whiteouts = 0usize; - let mut sample = Vec::new(); - if upper_dir.is_dir() { - walk_upper(upper_dir, upper_dir, &mut |rel, is_wh| { - if is_wh { - whiteouts += 1; - } else { - changed += 1; - } - if sample.len() < 32 { - sample.push(rel.display().to_string()); - } - Ok(()) - })?; - } - Ok(OverlayStatus { - changed_files: changed, - whiteouts, - sample_paths: sample, - }) -} - -/// Build the complete classified upper-layer changeset without reading file -/// contents. `lower_dirs` use overlay priority order (highest first). -pub fn overlay_changes( - record: &OverlayRecord, - lower_dirs: &[PathBuf], -) -> Result, OverlayError> { - let upper_dir = record.upper.path(); - let mut changes = Vec::new(); - if !upper_dir.is_dir() { - return Ok(changes); - } - walk_upper(upper_dir, upper_dir, &mut |rel, is_whiteout| { - let upper_path = upper_dir.join(&rel); - if is_whiteout { - let name = rel.file_name().unwrap_or_default(); - let parent = rel.parent().unwrap_or_else(|| Path::new("")); - if name == OPAQUE_WHITEOUT { - changes.push(ChangeEntry { - path: parent.display().to_string(), - kind: ChangeKind::Opaque, - old_type: Some(ChangeEntryType::Directory), - new_type: Some(ChangeEntryType::Directory), - size_bytes: None, - mode: None, - }); - } else if let Some(victim) = whiteout_target(name) { - let path = parent.join(victim); - let old = lower_metadata(lower_dirs, &path); - changes.push(ChangeEntry { - path: path.display().to_string(), - kind: ChangeKind::Deleted, - old_type: old.as_ref().map(metadata_type), - new_type: None, - size_bytes: None, - mode: None, - }); - } - return Ok(()); - } - - let new = fs::symlink_metadata(&upper_path)?; - let old = lower_metadata(lower_dirs, &rel); - let old_type = old.as_ref().map(metadata_type); - let new_type = metadata_type(&new); - let kind = match old_type { - None => ChangeKind::Added, - Some(old_type) if old_type != new_type => ChangeKind::TypeChanged, - Some(_) => ChangeKind::Modified, - }; - changes.push(ChangeEntry { - path: rel.display().to_string(), - kind, - old_type, - new_type: Some(new_type), - size_bytes: new.is_file().then_some(new.len()), - mode: Some(new.permissions().mode() & 0o7777), - }); - Ok(()) - })?; - changes.sort_by(|left, right| left.path.cmp(&right.path).then(left.kind.cmp(&right.kind))); - Ok(changes) -} - -fn lower_metadata(lower_dirs: &[PathBuf], relative: &Path) -> Option { - lower_dirs - .iter() - .find_map(|lower| fs::symlink_metadata(lower.join(relative)).ok()) -} - -fn metadata_type(metadata: &fs::Metadata) -> ChangeEntryType { - let file_type = metadata.file_type(); - if file_type.is_file() { - ChangeEntryType::File - } else if file_type.is_dir() { - ChangeEntryType::Directory - } else if file_type.is_symlink() { - ChangeEntryType::Symlink - } else { - ChangeEntryType::Other - } -} - -/// Copy the raw upper tree without interpreting whiteouts or opaque markers. -/// The destination is replaced and can later seed another directory upper. -pub fn snapshot_overlay_upper( - record: &OverlayRecord, - destination: &Path, -) -> Result<(), OverlayError> { - restore_overlay_upper(record.upper.path(), destination) -} - -/// Restore a raw upper snapshot into a directory upper. -pub fn restore_overlay_upper(source: &Path, destination: &Path) -> Result<(), OverlayError> { - if path_exists(destination) { - remove_path(destination)?; - } - fs::create_dir_all(destination)?; - if !source.is_dir() { - return Ok(()); - } - let mut hard_links = HashMap::new(); - snapshot_directory_raw(source, destination, &mut hard_links, &|| false)?; - Ok(()) -} - -struct CompiledApplySelection { - paths: Vec, - includes: GlobSet, - excludes: GlobSet, - has_positive: bool, -} - -impl CompiledApplySelection { - fn compile(selection: &ApplySelection) -> Result { - let paths = selection - .paths - .iter() - .map(|path| normalize_selection_path(path)) - .collect::, _>>()?; - Ok(Self { - has_positive: !paths.is_empty() || !selection.includes.is_empty(), - paths, - includes: compile_globs(&selection.includes, "include")?, - excludes: compile_globs(&selection.excludes, "exclude")?, - }) - } - - fn requested(&self, path: &Path) -> bool { - !self.has_positive - || self - .paths - .iter() - .any(|selected| path == selected || path.starts_with(selected)) - || self.includes.is_match(path) - } - - fn excluded(&self, path: &Path) -> bool { - let mut current = Some(path); - while let Some(candidate) = current { - if self.excludes.is_match(candidate) { - return true; - } - current = candidate.parent(); - } - false - } -} - -fn normalize_selection_path(path: &Path) -> Result { - let mut normalized = PathBuf::new(); - for component in path.components() { - match component { - Component::CurDir => {} - Component::Normal(value) => normalized.push(value), - Component::ParentDir | Component::RootDir | Component::Prefix(_) => { - return Err(OverlayError::InvalidConfig(format!( - "apply path must be relative and cannot contain `..`: {}", - path.display() - ))); - } - } - } - Ok(normalized) -} - -fn compile_globs(patterns: &[String], label: &str) -> Result { - let mut builder = GlobSetBuilder::new(); - for pattern in patterns { - let glob = GlobBuilder::new(pattern) - .literal_separator(true) - .build() - .map_err(|error| { - OverlayError::InvalidConfig(format!( - "invalid apply {label} glob `{pattern}`: {error}" - )) - })?; - builder.add(glob); - } - builder.build().map_err(|error| { - OverlayError::InvalidConfig(format!("compile apply {label} globs: {error}")) - }) -} - -fn at_or_below(path: &Path, root: &Path) -> bool { - path == root || path.starts_with(root) -} - -/// Resolve a filtered selection into a dependency-closed apply plan. -/// -/// Directory ancestors and hard-link siblings are included automatically. -/// Opaque directories remain atomic: callers must select the opaque directory, -/// rather than only a child whose application would implicitly delete siblings. -pub fn plan_overlay_apply( - record: &OverlayRecord, - lower_dirs: &[PathBuf], - selection: &ApplySelection, -) -> Result { - let changes = overlay_changes(record, lower_dirs)?; - let compiled = CompiledApplySelection::compile(selection)?; - let mut selected_paths = changes - .iter() - .map(|change| PathBuf::from(&change.path)) - .filter(|path| compiled.requested(path) && !compiled.excluded(path)) - .collect::>(); - - if !selection.is_all() && selected_paths.is_empty() { - return Err(OverlayError::Apply( - "no staged changes matched the apply selection".into(), - )); - } - - let opaque_dirs = changes - .iter() - .filter(|change| change.kind == ChangeKind::Opaque) - .map(|change| PathBuf::from(&change.path)) - .collect::>(); - let hard_link_groups = upper_hard_link_groups(record.upper.path())?; - - loop { - let before = selected_paths.len(); - - for opaque in &opaque_dirs { - let has_selected_child = selected_paths - .iter() - .any(|path| path != opaque && at_or_below(path, opaque)); - if has_selected_child && !selected_paths.contains(opaque) { - return Err(OverlayError::Apply(format!( - "{} is inside opaque directory {}; select the directory as one atomic unit", - selected_paths - .iter() - .find(|path| *path != opaque && at_or_below(path, opaque)) - .map_or_else(|| "selected path".into(), |path| path.display().to_string()), - if opaque.as_os_str().is_empty() { - ".".into() - } else { - opaque.display().to_string() - } - ))); - } - if selected_paths.contains(opaque) { - for change in &changes { - let path = PathBuf::from(&change.path); - if at_or_below(&path, opaque) { - if compiled.excluded(&path) { - return Err(OverlayError::Apply(format!( - "cannot exclude {} from atomic opaque directory {}", - path.display(), - opaque.display() - ))); - } - selected_paths.insert(path); - } - } - } - } - - for group in &hard_link_groups { - if group.iter().any(|path| selected_paths.contains(path)) { - for path in group { - if compiled.excluded(path) { - return Err(OverlayError::Apply(format!( - "cannot exclude hard-link sibling {} from the selected apply batch", - path.display() - ))); - } - selected_paths.insert(path.clone()); - } - } - } - - let selected_snapshot = selected_paths.iter().cloned().collect::>(); - for path in selected_snapshot { - let mut parent = path.parent(); - while let Some(ancestor) = parent { - if changes.iter().any(|change| { - Path::new(&change.path) == ancestor - && change.new_type == Some(ChangeEntryType::Directory) - }) { - if compiled.excluded(ancestor) { - return Err(OverlayError::Apply(format!( - "cannot exclude ancestor {} required by selected path {}", - ancestor.display(), - path.display() - ))); - } - selected_paths.insert(ancestor.to_path_buf()); - } - parent = ancestor.parent(); - } - } - - if selected_paths.len() == before { - break; - } - } - - let selected = changes - .iter() - .filter(|change| selected_paths.contains(Path::new(&change.path))) - .cloned() - .collect::>(); - Ok(ApplyPlan { - selected, - selected_paths, - }) -} - -fn upper_hard_link_groups(upper: &Path) -> Result>, OverlayError> { - let mut groups = HashMap::<(u64, u64), Vec>::new(); - if !upper.is_dir() { - return Ok(Vec::new()); - } - walk_upper(upper, upper, &mut |rel, is_whiteout| { - if !is_whiteout { - let metadata = fs::symlink_metadata(upper.join(&rel))?; - if metadata.is_file() && metadata.nlink() > 1 { - groups - .entry((metadata.dev(), metadata.ino())) - .or_default() - .push(rel); - } - } - Ok(()) - })?; - Ok(groups - .into_values() - .filter(|paths| paths.len() > 1) - .collect()) -} - -/// Apply one dependency-closed subset and retain all unselected changes for a -/// later apply or drop decision. -pub fn apply_overlay_selected( - record: &mut OverlayRecord, - lower_dirs: &[PathBuf], - selection: &ApplySelection, -) -> Result { - if record.protect_target { - return Err(OverlayError::Apply(format!( - "target is an immutable image rootfs: {}", - record.target.display() - ))); - } - let _target_lock = TargetApplyLock::acquire(&record.target)?; - recover_pending_applies_locked(record, lower_dirs)?; - match record.state { - OverlayState::Applied if selection.is_all() => { - return Ok(ApplyOutcome { - apply_id: String::new(), - applied: Vec::new(), - remaining: Vec::new(), - }); - } - OverlayState::Applied => { - return Err(OverlayError::InvalidState(format!( - "overlay {} was already fully applied", - record.id - ))); - } - OverlayState::Discarded => { - return Err(OverlayError::InvalidState(format!( - "overlay {} was already dropped; apply cannot recover discarded changes", - record.id - ))); - } - OverlayState::Active | OverlayState::Staged => {} - } - let plan = plan_overlay_apply(record, lower_dirs, selection)?; - let preimages = prepare_apply_preimages(record, &plan.selected_paths)?; - validate_target_preimages(record, &preimages, &plan.selected, false)?; - let apply_id = uuid::Uuid::new_v4().to_string(); - append_apply_record( - record, - ApplyRecord { - schema_version: APPLY_LEDGER_SCHEMA_VERSION, - apply_id: apply_id.clone(), - created_at_unix_ms: crate::util::unix_now_ms(), - overlay_id: record.id.clone(), - overlay_generation: record.generation, - target: record.target.clone(), - selection: selection.clone(), - changes: plan.selected.clone(), - planned_paths: plan.selected_paths.iter().cloned().collect(), - preimages: preimages.clone(), - state: ApplyRecordState::Prepared, - remaining_changes: 0, - }, - )?; - apply_prepared_target( - record, - &plan.selected_paths, - &preimages, - &plan.selected, - false, - )?; - mark_apply_target_applied(record, &apply_id)?; - let remaining = complete_target_applied(record, lower_dirs, &plan.selected_paths)?; - consume_applied_preimages(record, &plan.selected_paths)?; - mark_apply_committed(record, &apply_id, remaining.len())?; - Ok(ApplyOutcome { - apply_id, - applied: plan.selected, - remaining, - }) -} - -/// Complete any transaction whose durable intent was written before a crash. -/// `TargetApplied` is persisted before pruning starts, so recovery never tries -/// to reinterpret a partially-pruned opaque upper as a fresh target mutation. -#[cfg(test)] -fn recover_pending_applies( - record: &mut OverlayRecord, - lower_dirs: &[PathBuf], -) -> Result, OverlayError> { - let _target_lock = TargetApplyLock::acquire(&record.target)?; - recover_pending_applies_locked(record, lower_dirs) -} - -fn recover_pending_applies_locked( - record: &mut OverlayRecord, - lower_dirs: &[PathBuf], -) -> Result, OverlayError> { - let pending = load_apply_records(&record.stage_dir)? - .into_iter() - .filter(|apply| apply.state != ApplyRecordState::Committed) - .collect::>(); - let mut recovered = Vec::with_capacity(pending.len()); - for apply in pending { - if apply.overlay_id != record.id - || apply.overlay_generation != record.generation - || apply.target != record.target - { - return Err(OverlayError::InvalidState(format!( - "prepared apply {} belongs to overlay {} generation {} target {}, not overlay {} generation {} target {}", - apply.apply_id, - apply.overlay_id, - apply.overlay_generation, - apply.target.display(), - record.id, - record.generation, - record.target.display() - ))); - } - let selected_paths = apply - .planned_paths - .iter() - .map(|path| normalize_selection_path(path)) - .collect::, _>>()?; - if selected_paths.is_empty() && !apply.changes.is_empty() { - return Err(OverlayError::InvalidState(format!( - "prepared apply {} has changes but no recovery paths", - apply.apply_id - ))); - } - if apply.state == ApplyRecordState::Prepared { - apply_prepared_target( - record, - &selected_paths, - &apply.preimages, - &apply.changes, - true, - )?; - mark_apply_target_applied(record, &apply.apply_id)?; - } - let remaining = complete_target_applied(record, lower_dirs, &selected_paths)?; - consume_applied_preimages(record, &selected_paths)?; - mark_apply_committed(record, &apply.apply_id, remaining.len())?; - recovered.push(apply.apply_id); - } - Ok(recovered) -} - -fn consume_applied_preimages( - record: &OverlayRecord, - selected_paths: &BTreeSet, -) -> Result<(), OverlayError> { - let paths = selected_paths.iter().cloned().collect::>(); - remove_preimages(&record.stage_dir.join("preimages"), &paths).map_err(OverlayError::Io) -} - -fn apply_prepared_target( - record: &OverlayRecord, - selected_paths: &BTreeSet, - preimages: &[PathPreimage], - changes: &[ChangeEntry], - recovering: bool, -) -> Result<(), OverlayError> { - validate_target_preimages(record, preimages, changes, recovering)?; - let upper_dir = record.upper.path(); - if upper_dir.is_dir() && !selected_paths.is_empty() { - apply_selected_upper(upper_dir, &record.target, selected_paths)?; - } - Ok(()) -} - -fn prepare_apply_preimages( - record: &OverlayRecord, - selected_paths: &BTreeSet, -) -> Result, OverlayError> { - let journal_directory = record.stage_dir.join("preimages"); - let complete = preimage_journal_is_complete(&journal_directory); - let mut journal = load_preimages(&journal_directory)? - .into_iter() - .map(|preimage| (preimage.relative_path(), preimage)) - .collect::>(); - let mut preimages = Vec::with_capacity(selected_paths.len()); - for path in selected_paths { - if let Some(preimage) = journal.remove(path) { - preimages.push(preimage); - } else if complete { - return Err(OverlayError::InvalidState(format!( - "complete preimage journal is missing selected path {}; refusing an unverified apply", - path.display() - ))); - } else { - // Backward compatibility for stages produced before first-touch - // journaling existed. These paths get an apply-time preimage, but - // only complete-v1 stages claim run-time conflict detection. - preimages.push(PathPreimage { - path: path.as_os_str().as_bytes().to_vec(), - state: fingerprint_at(&record.target, path)?, - }); - } - } - Ok(preimages) -} - -fn recovery_fingerprint_matches(current: &PathFingerprint, expected: &PathFingerprint) -> bool { - if current == expected { - return true; - } - matches!( - (current, expected), - ( - PathFingerprint::Directory { - mode: current_mode, - uid: current_uid, - gid: current_gid, - .. - }, - PathFingerprint::Directory { - mode: expected_mode, - uid: expected_uid, - gid: expected_gid, - .. - } - ) if current_mode == expected_mode - && current_uid == expected_uid - && current_gid == expected_gid - ) -} - -fn desired_fingerprint( - record: &OverlayRecord, - path: &Path, - changes: &[ChangeEntry], -) -> Result, OverlayError> { - let Some(change) = changes - .iter() - .find(|change| Path::new(&change.path) == path) - else { - return Ok(None); - }; - if change.kind == ChangeKind::Deleted { - return Ok(Some(PathFingerprint::Absent)); - } - Ok(Some(fingerprint_at(record.upper.path(), path)?)) -} - -fn validate_target_preimages( - record: &OverlayRecord, - preimages: &[PathPreimage], - changes: &[ChangeEntry], - recovering: bool, -) -> Result<(), OverlayError> { - for preimage in preimages { - let path = preimage.relative_path(); - let current = fingerprint_at(&record.target, &path)?; - if current == preimage.state { - continue; - } - if recovering - && desired_fingerprint(record, &path, changes)? - .is_some_and(|desired| recovery_fingerprint_matches(¤t, &desired)) - { - continue; - } - return Err(OverlayError::Apply(format!( - "target changed after staging at {}; refusing to overwrite concurrent changes", - record.target.join(&path).display() - ))); - } - Ok(()) -} - -fn complete_target_applied( - record: &mut OverlayRecord, - lower_dirs: &[PathBuf], - selected_paths: &BTreeSet, -) -> Result, OverlayError> { - let upper_dir = record.upper.path().to_path_buf(); - if upper_dir.is_dir() && !selected_paths.is_empty() { - prune_selected_upper(&upper_dir, selected_paths)?; - } - - if let OverlayUpper::Jujutsu { - store_path, - workspace, - .. - } = &record.upper - { - snapshot_jujutsu_upper(store_path, workspace) - .map_err(|error| OverlayError::Finalize(error.to_string()))?; - } - - let remaining = overlay_changes(record, lower_dirs)?; - if remaining.is_empty() { - cleanup_terminal_overlay_data(record)?; - record.state = OverlayState::Applied; - } else { - record.state = OverlayState::Staged; - } - write_overlay_record(record)?; - Ok(remaining) -} - -/// Merge the complete staging upper onto `target`. -pub fn apply_overlay(record: &mut OverlayRecord) -> Result<(), OverlayError> { - let lower_dirs = vec![record.target.clone()]; - apply_overlay_selected(record, &lower_dirs, &ApplySelection::default()).map(|_| ()) -} - -/// Drop staging upper (and optionally the whole stage dir contents except meta). -pub fn discard_overlay(record: &mut OverlayRecord) -> Result<(), OverlayError> { - match record.state { - OverlayState::Discarded => return Ok(()), - OverlayState::Applied => { - return Err(OverlayError::InvalidState(format!( - "overlay {} was already applied; drop cannot undo applied changes", - record.id - ))); - } - OverlayState::Active | OverlayState::Staged => {} - } - match &record.upper { - OverlayUpper::Directory { .. } => {} - OverlayUpper::Jujutsu { - store_path, - workspace, - upper_dir, - } => { - clear_path(upper_dir)?; - snapshot_jujutsu_upper(store_path, workspace) - .map_err(|error| OverlayError::Finalize(error.to_string()))?; - clear_path(upper_dir)?; - } - } - cleanup_terminal_overlay_data(record)?; - record.state = OverlayState::Discarded; - write_overlay_record(record)?; - Ok(()) -} - -fn cleanup_terminal_overlay_data(record: &OverlayRecord) -> Result<(), OverlayError> { - match &record.upper { - OverlayUpper::Directory { - upper_dir, - work_dir, - } => { - clear_path(upper_dir)?; - clear_path(work_dir)?; - } - OverlayUpper::Jujutsu { upper_dir, .. } => clear_path(upper_dir)?, - } - - // Never recursively remove a mountpoint: after a clean teardown this is - // either absent or an empty placeholder. A non-empty directory is retained - // for inspection instead of risking traversal into a stale mount. - if record.merged_dir.starts_with(&record.stage_dir) { - match fs::remove_dir(&record.merged_dir) { - Ok(()) => {} - Err(error) - if matches!( - error.kind(), - io::ErrorKind::NotFound | io::ErrorKind::DirectoryNotEmpty - ) => {} - Err(error) => return Err(error.into()), - } - } - Ok(()) -} - -fn clear_path(path: &Path) -> Result<(), OverlayError> { - if path_exists(path) { - remove_path(path)?; - } - Ok(()) -} - -#[cfg(test)] -fn apply_upper_onto_target(upper: &Path, target: &Path) -> Result<(), OverlayError> { - ensure_directory(target)?; - let mut hard_links = HashMap::new(); - apply_directory(upper, target, &mut hard_links, false) -} - -fn apply_selected_upper( - upper: &Path, - target: &Path, - selected: &BTreeSet, -) -> Result<(), OverlayError> { - ensure_directory(target)?; - let mut hard_links = HashMap::new(); - apply_selected_directory(upper, target, Path::new(""), selected, &mut hard_links) -} - -fn apply_selected_directory( - source: &Path, - destination: &Path, - relative: &Path, - selected: &BTreeSet, - hard_links: &mut HashMap<(u64, u64), PathBuf>, -) -> Result<(), OverlayError> { - ensure_directory(destination)?; - let opaque = path_exists(&source.join(OPAQUE_WHITEOUT)) || has_opaque_xattr(source); - if opaque && selected.contains(relative) { - for entry in fs::read_dir(destination)? { - remove_path(&entry?.path())?; - } - } - - let entries = fs::read_dir(source)?.collect::, _>>()?; - for entry in &entries { - let name = entry.file_name(); - if name == OPAQUE_WHITEOUT { - continue; - } - if let Some(victim) = whiteout_target(&name) { - let logical = relative.join(victim); - if selected.contains(&logical) { - remove_path(&destination.join(victim))?; - } - } - } - - for entry in entries { - let name = entry.file_name(); - if name.as_bytes().starts_with(WHITEOUT_PREFIX.as_bytes()) { - continue; - } - let logical = relative.join(&name); - let source_path = entry.path(); - let metadata = fs::symlink_metadata(&source_path)?; - if metadata.is_dir() { - let has_selected_descendant = selected - .iter() - .any(|path| path != &logical && path.starts_with(&logical)); - if selected.contains(&logical) || has_selected_descendant { - let target_path = destination.join(&name); - ensure_directory(&target_path)?; - apply_selected_directory( - &source_path, - &target_path, - &logical, - selected, - hard_links, - )?; - if selected.contains(&logical) { - copy_host_metadata(&source_path, &target_path)?; - } - } - } else if selected.contains(&logical) { - copy_upper_entry(&source_path, &destination.join(name), hard_links)?; - } - } - Ok(()) -} - -fn prune_selected_upper(upper: &Path, selected: &BTreeSet) -> Result<(), OverlayError> { - prune_selected_directory(upper, Path::new(""), selected) -} - -fn prune_selected_directory( - directory: &Path, - relative: &Path, - selected: &BTreeSet, -) -> Result<(), OverlayError> { - if selected.contains(relative) { - clear_opaque_xattrs(directory)?; - } - let entries = fs::read_dir(directory)?.collect::, _>>()?; - for entry in entries { - let name = entry.file_name(); - if name == OPAQUE_WHITEOUT { - if selected.contains(relative) { - remove_path(&entry.path())?; - } - continue; - } - if let Some(victim) = whiteout_target(&name) { - if selected.contains(&relative.join(victim)) { - remove_path(&entry.path())?; - } - continue; - } - - let path = entry.path(); - let logical = relative.join(&name); - let metadata = fs::symlink_metadata(&path)?; - if metadata.is_dir() { - let has_selected_descendant = selected.iter().any(|selected_path| { - selected_path != &logical && selected_path.starts_with(&logical) - }); - if selected.contains(&logical) || has_selected_descendant { - prune_selected_directory(&path, &logical, selected)?; - } - if selected.contains(&logical) && fs::read_dir(&path)?.next().is_none() { - fs::remove_dir(&path)?; - } - } else if selected.contains(&logical) { - remove_path(&path)?; - } - } - Ok(()) -} - -fn apply_ledger_path(stage_dir: &Path) -> PathBuf { - stage_dir.join(APPLY_LEDGER_FILENAME) -} - -pub fn load_apply_records(stage_dir: &Path) -> Result, OverlayError> { - let path = apply_ledger_path(stage_dir); - let raw = match fs::read(&path) { - Ok(raw) => raw, - Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()), - Err(error) => return Err(error.into()), - }; - let ledger = serde_json::from_slice::(&raw) - .map_err(|error| OverlayError::Meta(format!("{}: {error}", path.display())))?; - if ledger.schema_version != APPLY_LEDGER_SCHEMA_VERSION { - return Err(OverlayError::Meta(format!( - "{}: unsupported apply ledger schema {}", - path.display(), - ledger.schema_version - ))); - } - Ok(ledger.records) -} - -fn append_apply_record( - overlay: &OverlayRecord, - apply_record: ApplyRecord, -) -> Result<(), OverlayError> { - let mut ledger = ApplyLedger { - schema_version: APPLY_LEDGER_SCHEMA_VERSION, - records: load_apply_records(&overlay.stage_dir)?, - }; - ledger.records.push(apply_record); - let path = apply_ledger_path(&overlay.stage_dir); - let body = serde_json::to_vec_pretty(&ledger) - .map_err(|error| OverlayError::Persist(format!("serialize apply ledger: {error}")))?; - atomic_write(&path, &body, 0o600) - .map_err(|error| OverlayError::Persist(format!("{}: {error:#}", path.display()))) -} - -fn mark_apply_committed( - overlay: &OverlayRecord, - apply_id: &str, - remaining_changes: usize, -) -> Result<(), OverlayError> { - update_apply_state( - overlay, - apply_id, - ApplyRecordState::Committed, - Some(remaining_changes), - ) -} - -fn mark_apply_target_applied(overlay: &OverlayRecord, apply_id: &str) -> Result<(), OverlayError> { - update_apply_state(overlay, apply_id, ApplyRecordState::TargetApplied, None) -} - -fn update_apply_state( - overlay: &OverlayRecord, - apply_id: &str, - state: ApplyRecordState, - remaining_changes: Option, -) -> Result<(), OverlayError> { - let mut ledger = ApplyLedger { - schema_version: APPLY_LEDGER_SCHEMA_VERSION, - records: load_apply_records(&overlay.stage_dir)?, - }; - let record = ledger - .records - .iter_mut() - .find(|record| record.apply_id == apply_id) - .ok_or_else(|| { - OverlayError::Persist(format!("prepared apply {apply_id} disappeared from ledger")) - })?; - record.state = state; - if let Some(remaining_changes) = remaining_changes { - record.remaining_changes = remaining_changes; - } - let path = apply_ledger_path(&overlay.stage_dir); - let body = serde_json::to_vec_pretty(&ledger) - .map_err(|error| OverlayError::Persist(format!("serialize apply ledger: {error}")))?; - atomic_write(&path, &body, 0o600) - .map_err(|error| OverlayError::Persist(format!("{}: {error:#}", path.display()))) -} - -fn path_exists(path: &Path) -> bool { - fs::symlink_metadata(path).is_ok() -} - -fn remove_path(path: &Path) -> io::Result<()> { - match fs::symlink_metadata(path) { - Ok(metadata) if metadata.is_dir() => fs::remove_dir_all(path), - Ok(_) => fs::remove_file(path), - Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()), - Err(error) => Err(error), - } -} - -fn ensure_directory(path: &Path) -> io::Result<()> { - match fs::symlink_metadata(path) { - Ok(metadata) if metadata.is_dir() => Ok(()), - Ok(_) => { - remove_path(path)?; - fs::create_dir(path) - } - Err(error) if error.kind() == io::ErrorKind::NotFound => fs::create_dir_all(path), - Err(error) => Err(error), - } -} - -fn whiteout_target(name: &OsStr) -> Option<&OsStr> { - let bytes = name.as_bytes(); - bytes - .strip_prefix(WHITEOUT_PREFIX.as_bytes()) - .filter(|stripped| !stripped.is_empty()) - .map(OsStr::from_bytes) -} - -fn apply_directory( - source: &Path, - destination: &Path, - hard_links: &mut HashMap<(u64, u64), PathBuf>, - preserve_metadata: bool, -) -> Result<(), OverlayError> { - ensure_directory(destination)?; - let opaque = path_exists(&source.join(OPAQUE_WHITEOUT)) || has_opaque_xattr(source); - if opaque { - for entry in fs::read_dir(destination)? { - remove_path(&entry?.path())?; - } - } - - let entries = fs::read_dir(source)?.collect::, _>>()?; - // Whiteouts are processed first, independent of host readdir order. - for entry in &entries { - let name = entry.file_name(); - if name == OPAQUE_WHITEOUT { - continue; - } - if let Some(victim) = whiteout_target(&name) { - remove_path(&destination.join(victim))?; - } - } - for entry in entries { - let name = entry.file_name(); - if name.as_bytes().starts_with(WHITEOUT_PREFIX.as_bytes()) { - continue; - } - copy_upper_entry(&entry.path(), &destination.join(name), hard_links)?; - } - if preserve_metadata { - copy_host_metadata(source, destination)?; - } - Ok(()) -} - -fn copy_upper_entry( - source: &Path, - destination: &Path, - hard_links: &mut HashMap<(u64, u64), PathBuf>, -) -> Result<(), OverlayError> { - let metadata = fs::symlink_metadata(source)?; - let kind = metadata.file_type(); - if kind.is_dir() { - ensure_directory(destination)?; - return apply_directory(source, destination, hard_links, true); - } - let parent = destination.parent().ok_or_else(|| { - OverlayError::Apply(format!( - "apply destination has no parent: {}", - destination.display() - )) - })?; - fs::create_dir_all(parent)?; - // The deterministic reserved name lets a Prepared transaction clean up - // its own interrupted copy before retrying. TargetApplyLock serializes all - // pVisor writers for this target while the entry exists. - let temporary = parent.join(format!(".pvisor-apply-{}", path_digest(destination))); - remove_path(&temporary)?; - let identity = (metadata.dev(), metadata.ino()); - let result = (|| { - if kind.is_symlink() { - std::os::unix::fs::symlink(fs::read_link(source)?, &temporary)?; - } else if kind.is_file() { - if metadata.nlink() > 1 { - if let Some(existing) = hard_links.get(&identity) { - fs::hard_link(existing, &temporary)?; - } else { - fs::copy(source, &temporary)?; - } - } else { - fs::copy(source, &temporary)?; - } - } else { - let path = c_path(&temporary)?; - // SAFETY: path is NUL terminated and points to valid storage for this call. - let rc = unsafe { - libc::mknod( - path.as_ptr(), - metadata.mode() as libc::mode_t, - metadata.rdev() as libc::dev_t, - ) - }; - if rc != 0 { - return Err(io::Error::last_os_error().into()); - } - } - copy_host_metadata(source, &temporary)?; - if kind.is_file() { - File::open(&temporary)?.sync_all()?; - } - if fs::symlink_metadata(destination).is_ok_and(|metadata| metadata.is_dir()) { - remove_path(destination)?; - } - fs::rename(&temporary, destination)?; - File::open(parent)?.sync_all()?; - if kind.is_file() && metadata.nlink() > 1 { - hard_links.insert(identity, destination.to_path_buf()); - } - Ok::<_, OverlayError>(()) - })(); - if result.is_err() { - let _ = remove_path(&temporary); - } - result -} - -fn snapshot_directory_raw( - source: &Path, - destination: &Path, - hard_links: &mut HashMap<(u64, u64), PathBuf>, - cancelled: &dyn Fn() -> bool, -) -> Result<(), OverlayError> { - if cancelled() { - return Err(io::Error::new(io::ErrorKind::Interrupted, "materialization cancelled").into()); - } - ensure_directory(destination)?; - for entry in fs::read_dir(source)? { - if cancelled() { - return Err( - io::Error::new(io::ErrorKind::Interrupted, "materialization cancelled").into(), - ); - } - let entry = entry?; - snapshot_entry_raw( - &entry.path(), - &destination.join(entry.file_name()), - hard_links, - cancelled, - )?; - } - copy_snapshot_metadata(source, destination)?; - Ok(()) -} - -fn snapshot_entry_raw( - source: &Path, - destination: &Path, - hard_links: &mut HashMap<(u64, u64), PathBuf>, - cancelled: &dyn Fn() -> bool, -) -> Result<(), OverlayError> { - if cancelled() { - return Err(io::Error::new(io::ErrorKind::Interrupted, "materialization cancelled").into()); - } - let metadata = fs::symlink_metadata(source)?; - let kind = metadata.file_type(); - if kind.is_dir() { - return snapshot_directory_raw(source, destination, hard_links, cancelled); - } - if let Some(parent) = destination.parent() { - fs::create_dir_all(parent)?; - } - remove_path(destination)?; - if kind.is_symlink() { - std::os::unix::fs::symlink(fs::read_link(source)?, destination)?; - } else if kind.is_file() { - let identity = (metadata.dev(), metadata.ino()); - if metadata.nlink() > 1 - && let Some(existing) = hard_links.get(&identity) - { - fs::hard_link(existing, destination)?; - return Ok(()); - } - fs::copy(source, destination)?; - if metadata.nlink() > 1 { - hard_links.insert(identity, destination.to_path_buf()); - } - } else { - let path = c_path(destination)?; - // SAFETY: the C path and metadata remain valid for this call. - let rc = unsafe { - libc::mknod( - path.as_ptr(), - metadata.mode() as libc::mode_t, - metadata.rdev() as libc::dev_t, - ) - }; - if rc != 0 { - return Err(io::Error::last_os_error().into()); - } - } - copy_snapshot_metadata(source, destination)?; - Ok(()) -} - -fn copy_snapshot_metadata(source: &Path, destination: &Path) -> io::Result<()> { - copy_host_metadata(source, destination)?; - let source_c = c_path(source)?; - let destination_c = c_path(destination)?; - for name in OPAQUE_XATTRS { - let name = CString::new(name).map_err(|_| io::Error::from_raw_os_error(libc::EINVAL))?; - if let Ok(value) = get_host_xattr(&source_c, &name) - && let Err(error) = set_host_xattr(&destination_c, &name, &value) - && !matches!( - error.raw_os_error(), - Some(libc::EPERM) | Some(libc::EACCES) | Some(libc::ENOTSUP) - ) - { - return Err(error); - } - } - Ok(()) -} - -fn c_path(path: &Path) -> io::Result { - CString::new(path.as_os_str().as_bytes()) - .map_err(|_| io::Error::from_raw_os_error(libc::EINVAL)) -} - -fn copy_host_metadata(source: &Path, destination: &Path) -> io::Result<()> { - let metadata = fs::symlink_metadata(source)?; - let nofollow = metadata.file_type().is_symlink(); - let source = c_path(source)?; - let destination_c = c_path(destination)?; - - // Preserve ownership where permitted. An unprivileged apply still preserves - // all metadata it is allowed to own instead of failing the whole transaction. - let flags = if nofollow { - libc::AT_SYMLINK_NOFOLLOW - } else { - 0 - }; - // SAFETY: both C strings and syscall arguments remain valid for each call. - let chown_rc = unsafe { - libc::fchownat( - libc::AT_FDCWD, - destination_c.as_ptr(), - metadata.uid(), - metadata.gid(), - flags, - ) - }; - if chown_rc != 0 { - let error = io::Error::last_os_error(); - if !matches!(error.raw_os_error(), Some(libc::EPERM) | Some(libc::EACCES)) { - return Err(error); - } - } - if !nofollow { - fs::set_permissions( - destination, - fs::Permissions::from_mode(metadata.mode() & 0o7777), - )?; - } - copy_host_xattrs(&source, &destination_c)?; - - let times = [ - libc::timespec { - tv_sec: metadata.atime(), - tv_nsec: metadata.atime_nsec(), - }, - libc::timespec { - tv_sec: metadata.mtime(), - tv_nsec: metadata.mtime_nsec(), - }, - ]; - // SAFETY: destination and times are valid for the duration of the call. - let rc = unsafe { - libc::utimensat( - libc::AT_FDCWD, - destination_c.as_ptr(), - times.as_ptr(), - flags, - ) - }; - if rc == 0 { - Ok(()) - } else { - let error = io::Error::last_os_error(); - if nofollow - && matches!( - error.raw_os_error(), - Some(libc::ENOTSUP) | Some(libc::EPERM) - ) - { - Ok(()) - } else { - Err(error) - } - } -} - -fn copy_host_xattrs(source: &CString, destination: &CString) -> io::Result<()> { - let names = list_host_xattrs(source)?; - let destination_names = list_host_xattrs(destination)?; - for name in destination_names - .split(|byte| *byte == 0) - .filter(|name| !name.is_empty()) - { - if OPAQUE_XATTRS.contains(&name) - || !names - .split(|byte| *byte == 0) - .any(|source_name| source_name == name) - { - let name = - CString::new(name).map_err(|_| io::Error::from_raw_os_error(libc::EINVAL))?; - if let Err(error) = remove_host_xattr(destination, &name) - && !matches!( - error.raw_os_error(), - Some(libc::EPERM) | Some(libc::EACCES) | Some(libc::ENOTSUP) - ) - { - return Err(error); - } - } - } - for name in names - .split(|byte| *byte == 0) - .filter(|name| !name.is_empty() && !OPAQUE_XATTRS.contains(name)) - { - let name = CString::new(name).map_err(|_| io::Error::from_raw_os_error(libc::EINVAL))?; - let value = get_host_xattr(source, &name)?; - if let Err(error) = set_host_xattr(destination, &name, &value) - && !matches!( - error.raw_os_error(), - Some(libc::EPERM) | Some(libc::EACCES) | Some(libc::ENOTSUP) - ) - { - return Err(error); - } - } - Ok(()) -} - -fn has_opaque_xattr(path: &Path) -> bool { - let Ok(path) = c_path(path) else { - return false; - }; - OPAQUE_XATTRS.iter().any(|name| { - let Ok(name) = CString::new(*name) else { - return false; - }; - get_host_xattr(&path, &name).is_ok_and(|value| value == b"y") - }) -} - -fn clear_opaque_xattrs(path: &Path) -> Result<(), OverlayError> { - let path = c_path(path)?; - for name in OPAQUE_XATTRS { - let name = CString::new(name).map_err(|_| io::Error::from_raw_os_error(libc::EINVAL))?; - if get_host_xattr(&path, &name).is_ok_and(|value| value == b"y") { - remove_host_xattr(&path, &name)?; - } - } - Ok(()) -} - -fn remove_host_xattr(path: &CString, name: &CString) -> io::Result<()> { - #[cfg(target_os = "macos")] - // SAFETY: both strings are NUL terminated and valid for this call. - let rc = unsafe { libc::removexattr(path.as_ptr(), name.as_ptr(), libc::XATTR_NOFOLLOW) }; - #[cfg(not(target_os = "macos"))] - // SAFETY: both strings are NUL terminated and valid for this call. - let rc = unsafe { libc::lremovexattr(path.as_ptr(), name.as_ptr()) }; - if rc == 0 { - Ok(()) - } else { - Err(io::Error::last_os_error()) - } -} - -fn list_host_xattrs(path: &CString) -> io::Result> { - #[cfg(target_os = "macos")] - let list = |buffer: *mut libc::c_char, size| unsafe { - libc::listxattr(path.as_ptr(), buffer, size, libc::XATTR_NOFOLLOW) - }; - #[cfg(not(target_os = "macos"))] - let list = - |buffer: *mut libc::c_char, size| unsafe { libc::llistxattr(path.as_ptr(), buffer, size) }; - let needed = list(std::ptr::null_mut(), 0); - if needed < 0 { - let error = io::Error::last_os_error(); - if matches!(error.raw_os_error(), Some(libc::ENOTSUP)) { - return Ok(Vec::new()); - } - return Err(error); - } - let mut names = vec![0; needed as usize]; - if !names.is_empty() { - let actual = list(names.as_mut_ptr().cast(), names.len()); - if actual < 0 { - return Err(io::Error::last_os_error()); - } - names.truncate(actual as usize); - } - Ok(names) -} - -fn get_host_xattr(path: &CString, name: &CString) -> io::Result> { - #[cfg(target_os = "macos")] - let get = |buffer: *mut libc::c_void, size| unsafe { - libc::getxattr( - path.as_ptr(), - name.as_ptr(), - buffer, - size, - 0, - libc::XATTR_NOFOLLOW, - ) - }; - #[cfg(not(target_os = "macos"))] - let get = |buffer: *mut libc::c_void, size| unsafe { - libc::lgetxattr(path.as_ptr(), name.as_ptr(), buffer, size) - }; - let needed = get(std::ptr::null_mut(), 0); - if needed < 0 { - return Err(io::Error::last_os_error()); - } - let mut value = vec![0; needed as usize]; - if !value.is_empty() { - let actual = get(value.as_mut_ptr().cast(), value.len()); - if actual < 0 { - return Err(io::Error::last_os_error()); - } - value.truncate(actual as usize); - } - Ok(value) -} - -fn set_host_xattr(path: &CString, name: &CString, value: &[u8]) -> io::Result<()> { - #[cfg(target_os = "macos")] - let rc = unsafe { - libc::setxattr( - path.as_ptr(), - name.as_ptr(), - value.as_ptr().cast(), - value.len(), - 0, - libc::XATTR_NOFOLLOW, - ) - }; - #[cfg(not(target_os = "macos"))] - let rc = unsafe { - libc::lsetxattr( - path.as_ptr(), - name.as_ptr(), - value.as_ptr().cast(), - value.len(), - 0, - ) - }; - if rc == 0 { - Ok(()) - } else { - Err(io::Error::last_os_error()) - } -} - -fn walk_upper( - root: &Path, - dir: &Path, - visit: &mut dyn FnMut(PathBuf, bool) -> Result<(), OverlayError>, -) -> Result<(), OverlayError> { - let entries = match fs::read_dir(dir) { - Ok(e) => e, - Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()), - Err(err) => return Err(err.into()), - }; - for entry in entries { - let entry = entry?; - let path = entry.path(); - let rel = path - .strip_prefix(root) - .map_err(|e| OverlayError::Apply(e.to_string()))? - .to_path_buf(); - let is_wh = path - .file_name() - .is_some_and(|name| name.as_bytes().starts_with(WHITEOUT_PREFIX.as_bytes())); - if fs::symlink_metadata(&path)?.is_dir() && !is_wh { - visit(rel.clone(), false)?; - walk_upper(root, &path, visit)?; - } else { - visit(rel, is_wh)?; - } - } - Ok(()) -} - -fn wait_merged_ready(merged: &Path, session: &OverlaySession) -> Result<(), OverlayError> { - for _ in 0..50 { - if session.has_exited() { - return Err(OverlayError::Mount( - "embedded FUSE request loop exited before mount became ready".into(), - )); - } - if merged_root_is_ready(merged) { - return Ok(()); - } - std::thread::sleep(Duration::from_millis(50)); - } - if merged_root_is_ready(merged) { - return Ok(()); - } - Err(OverlayError::NotReady(merged.display().to_string())) -} - -fn merged_root_is_ready(path: &Path) -> bool { - if !is_mountpoint(path) { - return false; - } - // macFUSE may publish the mountpoint before its request loop can serve the - // root directory. Probe opendir/readdir so the Agent never races the mount. - match fs::read_dir(path) { - Ok(mut entries) => entries.next().is_none_or(|entry| entry.is_ok()), - Err(_) => false, - } -} - -fn is_mountpoint(path: &Path) -> bool { - let Ok(metadata) = fs::metadata(path) else { - return false; - }; - let Some(parent) = path.parent() else { - return true; - }; - let Ok(parent_metadata) = fs::metadata(parent) else { - return false; - }; - // `dev`/`ino` differ (or `ino` matches for hardlinks) across the parent - // boundary means this path is a distinct mount. On Linux there is an - // additional `/proc/self/mounts` probe that the early return cannot fold - // into, so the platforms are branched explicitly to stay clippy-clean. - #[cfg(not(target_os = "linux"))] - { - metadata.dev() != parent_metadata.dev() - || (metadata.dev() == parent_metadata.dev() && metadata.ino() == parent_metadata.ino()) - } - - #[cfg(target_os = "linux")] - { - if metadata.dev() != parent_metadata.dev() - || (metadata.dev() == parent_metadata.dev() && metadata.ino() == parent_metadata.ino()) - { - return true; - } - let target = path.display().to_string(); - if let Ok(mounts) = fs::read_to_string("/proc/self/mounts") { - return mounts.lines().any(|line| { - line.split_whitespace() - .nth(1) - .is_some_and(|mount| mount == target) - }); - } - false - } -} - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::tempdir; - - #[test] - fn resolve_uses_target_and_default_stage() { - let cfg = OverlayConfig { - enabled: true, - target: Some("/proj".into()), - ..OverlayConfig::default() - }; - let rec = resolve_overlay_workspace(&cfg, Path::new("/tmp/store"), "run-1") - .unwrap() - .unwrap(); - assert_eq!(rec.target, PathBuf::from("/proj")); - assert_eq!(rec.stage_dir, PathBuf::from("/tmp/store/.overlay/run-1")); - assert_eq!( - rec.upper, - OverlayUpper::Directory { - upper_dir: PathBuf::from("/tmp/store/.overlay/run-1/upper"), - work_dir: PathBuf::from("/tmp/store/.overlay/run-1/work") - } - ); - } - - #[test] - fn resolve_rejects_parallel_upper_backends() { - let cfg = OverlayConfig { - enabled: true, - target: Some("/proj".into()), - jujutsu_store_path: Some("/shared/jj".into()), - ..OverlayConfig::default() - }; - assert!(matches!( - resolve_overlay_workspace(&cfg, Path::new("/tmp/store"), "run-1"), - Err(OverlayError::InvalidConfig(_)) - )); - } - - #[test] - fn root_overlay_hides_its_stage_and_compose_backing_paths() { - let cfg = OverlayConfig { - enabled: true, - target: Some("/".into()), - stage_dir: Some("/tmp/persisting-runs/run-one".into()), - lower_dirs: vec!["/var/lib/persisting/layers/base".into()], - ..OverlayConfig::default() - }; - let record = resolve_overlay_workspace(&cfg, Path::new("/unused"), "run-one") - .unwrap() - .unwrap(); - assert_eq!(record.target, Path::new("/")); - assert_eq!( - record.excluded_paths, - [ - PathBuf::from("tmp/persisting-runs/run-one"), - PathBuf::from("var/lib/persisting/layers/base"), - ] - ); - } - - #[test] - fn nested_stage_is_hidden_from_a_non_root_overlay() { - let cfg = OverlayConfig { - enabled: true, - target: Some("/Users/example/workspace".into()), - stage_dir: Some("/Users/example/workspace/project/tmp".into()), - ..OverlayConfig::default() - }; - let record = resolve_overlay_workspace(&cfg, Path::new("/unused"), "run-one") - .unwrap() - .unwrap(); - assert_eq!(record.excluded_paths, [PathBuf::from("project/tmp")]); - assert_eq!( - record.merged_dir, - PathBuf::from("/unused/.overlay-mounts/run-one/merged") - ); - assert!(!record.merged_dir.starts_with(&record.target)); - } - - #[test] - fn mountless_preparation_creates_backing_state_without_a_merged_mount() { - let tmp = tempdir().unwrap(); - let lower = tmp.path().join("lower"); - let stage = tmp.path().join("stage"); - fs::create_dir_all(&lower).unwrap(); - let record = OverlayRecord { - id: "mountless".into(), - generation: 0, - target: lower.clone(), - upper: OverlayUpper::Directory { - upper_dir: stage.join("upper"), - work_dir: stage.join("work"), - }, - merged_dir: stage.join("merged"), - stage_dir: stage.clone(), - excluded_paths: Vec::new(), - auto_apply: false, - auto_discard: false, - protect_target: false, - state: OverlayState::Active, - }; - let prepared = prepare_overlay_record_mountless(&record, &[lower]).unwrap(); - assert!(prepared.upper.path().is_dir()); - assert!(stage.join("work").is_dir()); - assert!(!prepared.merged_dir.exists()); - assert_eq!( - load_overlay_record(&stage).unwrap().state, - OverlayState::Active - ); - } - - #[test] - fn jujutsu_sessions_share_store_but_get_distinct_workspaces() { - let storage = Path::new("/tmp/store"); - let cfg = OverlayConfig { - enabled: true, - target: Some("/proj".into()), - backend: OverlayBackend::Jujutsu, - ..OverlayConfig::default() - }; - let first = resolve_overlay_workspace(&cfg, storage, "fork-a") - .unwrap() - .unwrap(); - let second = resolve_overlay_workspace(&cfg, storage, "fork-b") - .unwrap() - .unwrap(); - let OverlayUpper::Jujutsu { - store_path: first_store, - workspace: first_workspace, - upper_dir: first_upper, - } = first.upper - else { - panic!("expected Jujutsu upper") - }; - let OverlayUpper::Jujutsu { - store_path: second_store, - workspace: second_workspace, - upper_dir: second_upper, - } = second.upper - else { - panic!("expected Jujutsu upper") - }; - assert_eq!(first_store, second_store); - assert_eq!(first_store, PathBuf::from("/tmp/store/.overlay/jujutsu")); - assert_eq!(first_workspace, "fork-a"); - assert_eq!(second_workspace, "fork-b"); - assert_ne!(first_upper, second_upper); - } - - #[test] - fn lower_stack_keeps_target_as_bottom_base_layer() { - let cfg = OverlayConfig { - lower_dirs: vec!["extra-a".into(), "extra-b".into()], - ..OverlayConfig::default() - }; - assert_eq!( - lower_stack_from_config(&cfg, Path::new("/store"), Path::new("/target")), - vec![ - PathBuf::from("/store/extra-a"), - PathBuf::from("/store/extra-b"), - PathBuf::from("/target"), - ] - ); - } - - #[cfg(target_os = "macos")] - #[test] - #[ignore = "requires an enabled macFUSE kernel extension"] - fn embedded_mount_roundtrip() { - let tmp = tempdir().unwrap(); - let lower = tmp.path().join("lower"); - let stage = tmp.path().join("stage"); - let upper = stage.join("upper"); - let work = stage.join("work"); - let merged = stage.join("merged"); - fs::create_dir_all(&lower).unwrap(); - fs::write(lower.join("lower-file"), b"lower").unwrap(); - fs::write(lower.join("deleted-file"), b"delete me").unwrap(); - let record = OverlayRecord { - id: "embedded-e2e".into(), - generation: 0, - target: lower.clone(), - upper: OverlayUpper::Directory { - upper_dir: upper.clone(), - work_dir: work, - }, - merged_dir: merged.clone(), - stage_dir: stage.clone(), - excluded_paths: Vec::new(), - auto_apply: false, - auto_discard: false, - protect_target: false, - state: OverlayState::Staged, - }; - - let mount = mount_overlay_record(&record, std::slice::from_ref(&lower)).unwrap(); - assert_eq!(fs::read(merged.join("lower-file")).unwrap(), b"lower"); - fs::write(merged.join("lower-file"), b"copied-up").unwrap(); - fs::remove_file(merged.join("deleted-file")).unwrap(); - fs::write(merged.join("created"), b"upper").unwrap(); - fs::hard_link(merged.join("created"), merged.join("created-link")).unwrap(); - fs::create_dir(merged.join("new-dir")).unwrap(); - fs::write(merged.join("new-dir/before-rename"), b"nested").unwrap(); - fs::rename( - merged.join("new-dir/before-rename"), - merged.join("new-dir/after-rename"), - ) - .unwrap(); - std::os::unix::fs::symlink("created", merged.join("created-symlink")).unwrap(); - assert!(upper.is_dir()); - let mut record = mount.unmount().unwrap(); - assert_eq!(record.state, OverlayState::Staged); - assert!(!is_mountpoint(&merged)); - let status = overlay_status(&record).unwrap(); - assert!(status.changed_files >= 5); - assert_eq!(status.whiteouts, 1); - apply_overlay(&mut record).unwrap(); - assert_eq!(record.state, OverlayState::Applied); - assert_eq!(fs::read(lower.join("lower-file")).unwrap(), b"copied-up"); - assert!(!lower.join("deleted-file").exists()); - assert_eq!(fs::read(lower.join("created")).unwrap(), b"upper"); - assert_eq!( - fs::read(lower.join("new-dir/after-rename")).unwrap(), - b"nested" - ); - assert_eq!( - fs::read_link(lower.join("created-symlink")).unwrap(), - PathBuf::from("created") - ); - assert_eq!( - fs::metadata(lower.join("created")).unwrap().ino(), - fs::metadata(lower.join("created-link")).unwrap().ino() - ); - assert!(!upper.exists()); - assert!(!stage.join("work").exists()); - assert!(stage.join(META_FILENAME).is_file()); - } - - #[test] - fn apply_copies_and_honors_whiteout() { - let tmp = tempdir().unwrap(); - let target = tmp.path().join("target"); - let upper = tmp.path().join("upper"); - fs::create_dir_all(target.join("keep")).unwrap(); - fs::write(target.join("keep/a.txt"), b"old").unwrap(); - fs::write(target.join("gone.txt"), b"x").unwrap(); - fs::create_dir_all(upper.join("keep")).unwrap(); - fs::write(upper.join("keep/a.txt"), b"new").unwrap(); - fs::write(upper.join("keep/b.txt"), b"added").unwrap(); - fs::write(upper.join(".wh.gone.txt"), b"").unwrap(); - - let work = tmp.path().join("work"); - fs::create_dir_all(&work).unwrap(); - fs::write(work.join("scratch"), b"temporary").unwrap(); - let mut rec = OverlayRecord { - id: "t".into(), - generation: 0, - target: target.clone(), - upper: OverlayUpper::Directory { - upper_dir: upper.clone(), - work_dir: work.clone(), - }, - merged_dir: tmp.path().join("merged"), - stage_dir: tmp.path().to_path_buf(), - excluded_paths: Vec::new(), - auto_apply: false, - auto_discard: false, - protect_target: false, - state: OverlayState::Staged, - }; - apply_overlay(&mut rec).unwrap(); - assert_eq!( - fs::read_to_string(target.join("keep/a.txt")).unwrap(), - "new" - ); - assert_eq!( - fs::read_to_string(target.join("keep/b.txt")).unwrap(), - "added" - ); - assert!(!target.join("gone.txt").exists()); - assert_eq!(rec.state, OverlayState::Applied); - assert!(!upper.exists()); - assert!(!work.exists()); - assert!(tmp.path().join(META_FILENAME).is_file()); - } - - #[test] - fn selective_apply_retains_pending_changes_and_can_repeat() { - let tmp = tempdir().unwrap(); - let target = tmp.path().join("target"); - let stage = tmp.path().join("stage"); - let upper = stage.join("upper"); - fs::create_dir_all(target.join("src")).unwrap(); - fs::create_dir_all(upper.join("src")).unwrap(); - fs::write(target.join("src/a.txt"), b"old-a").unwrap(); - fs::write(target.join("src/b.txt"), b"old-b").unwrap(); - fs::write(target.join("gone.txt"), b"old-gone").unwrap(); - fs::write(upper.join("src/a.txt"), b"new-a").unwrap(); - fs::write(upper.join("src/b.txt"), b"new-b").unwrap(); - fs::write(upper.join(".wh.gone.txt"), b"").unwrap(); - let mut record = OverlayRecord { - generation: 0, - id: "selective".into(), - target: target.clone(), - upper: OverlayUpper::Directory { - upper_dir: upper.clone(), - work_dir: stage.join("work"), - }, - merged_dir: stage.join("merged"), - stage_dir: stage.clone(), - excluded_paths: Vec::new(), - auto_apply: false, - auto_discard: false, - protect_target: false, - state: OverlayState::Staged, - }; - - let first = apply_overlay_selected( - &mut record, - std::slice::from_ref(&target), - &ApplySelection { - paths: vec!["src/a.txt".into()], - ..ApplySelection::default() - }, - ) - .unwrap(); - assert_eq!(fs::read(target.join("src/a.txt")).unwrap(), b"new-a"); - assert_eq!(fs::read(target.join("src/b.txt")).unwrap(), b"old-b"); - assert!(target.join("gone.txt").exists()); - assert_eq!(record.state, OverlayState::Staged); - assert!( - first - .remaining - .iter() - .any(|change| change.path == "src/b.txt") - ); - assert!(upper.join("src/b.txt").is_file()); - assert!(upper.join(".wh.gone.txt").is_file()); - - apply_overlay_selected( - &mut record, - std::slice::from_ref(&target), - &ApplySelection { - paths: vec!["gone.txt".into()], - ..ApplySelection::default() - }, - ) - .unwrap(); - assert!(!target.join("gone.txt").exists()); - assert_eq!(record.state, OverlayState::Staged); - - let final_apply = apply_overlay_selected( - &mut record, - std::slice::from_ref(&target), - &ApplySelection { - paths: vec!["src".into()], - ..ApplySelection::default() - }, - ) - .unwrap(); - assert_eq!(fs::read(target.join("src/b.txt")).unwrap(), b"new-b"); - assert!(final_apply.remaining.is_empty()); - assert_eq!(record.state, OverlayState::Applied); - assert!(!upper.exists()); - - let ledger = load_apply_records(&stage).unwrap(); - assert_eq!(ledger.len(), 3); - assert!( - ledger - .iter() - .all(|record| record.state == ApplyRecordState::Committed) - ); - assert_eq!(ledger[0].remaining_changes, first.remaining.len()); - assert_eq!(ledger[2].remaining_changes, 0); - } - - #[test] - fn prepared_apply_recovers_before_or_after_target_mutation() { - for target_already_mutated in [false, true] { - let tmp = tempdir().unwrap(); - let target = tmp.path().join("target"); - let stage = tmp.path().join("stage"); - let upper = stage.join("upper"); - fs::create_dir_all(&target).unwrap(); - fs::create_dir_all(&upper).unwrap(); - fs::write(target.join("value.txt"), b"old").unwrap(); - fs::write(upper.join("value.txt"), b"new").unwrap(); - let mut record = OverlayRecord { - generation: 0, - id: format!("recover-{target_already_mutated}"), - target: target.clone(), - upper: OverlayUpper::Directory { - upper_dir: upper.clone(), - work_dir: stage.join("work"), - }, - merged_dir: stage.join("merged"), - stage_dir: stage.clone(), - excluded_paths: Vec::new(), - auto_apply: false, - auto_discard: false, - protect_target: false, - state: OverlayState::Staged, - }; - let selection = ApplySelection::default(); - let plan = - plan_overlay_apply(&record, std::slice::from_ref(&target), &selection).unwrap(); - let preimages = prepare_apply_preimages(&record, &plan.selected_paths).unwrap(); - let apply_id = format!("prepared-{target_already_mutated}"); - append_apply_record( - &record, - ApplyRecord { - schema_version: APPLY_LEDGER_SCHEMA_VERSION, - apply_id: apply_id.clone(), - created_at_unix_ms: crate::util::unix_now_ms(), - overlay_id: record.id.clone(), - overlay_generation: record.generation, - target: target.clone(), - selection, - changes: plan.selected.clone(), - planned_paths: plan.selected_paths.iter().cloned().collect(), - preimages, - state: ApplyRecordState::Prepared, - remaining_changes: 0, - }, - ) - .unwrap(); - - let mut next_generation = record.clone(); - next_generation.generation += 1; - let stale = - recover_pending_applies(&mut next_generation, std::slice::from_ref(&target)) - .unwrap_err(); - assert!(stale.to_string().contains("generation")); - - if target_already_mutated { - apply_selected_upper(&upper, &target, &plan.selected_paths).unwrap(); - assert_eq!(fs::read(target.join("value.txt")).unwrap(), b"new"); - assert!(upper.join("value.txt").is_file()); - } - - assert_eq!( - recover_pending_applies(&mut record, std::slice::from_ref(&target)).unwrap(), - vec![apply_id.clone()] - ); - assert_eq!(fs::read(target.join("value.txt")).unwrap(), b"new"); - assert_eq!(record.state, OverlayState::Applied); - assert!(!upper.exists()); - let ledger = load_apply_records(&stage).unwrap(); - assert_eq!(ledger.len(), 1); - assert_eq!(ledger[0].apply_id, apply_id); - assert_eq!(ledger[0].state, ApplyRecordState::Committed); - assert_eq!(ledger[0].remaining_changes, 0); - } - } - - #[test] - fn apply_rejects_a_target_changed_after_first_touch() { - let temporary = tempdir().unwrap(); - let target = temporary.path().join("target"); - let stage = temporary.path().join("stage"); - let upper = stage.join("upper"); - let work = stage.join("work"); - fs::create_dir_all(&target).unwrap(); - fs::write(target.join("value.txt"), b"original").unwrap(); - let core = persisting_overlay_core::OverlayCore::new_with_exclusions_and_preimages( - vec![target.clone()], - upper.clone(), - Some(work.clone()), - Vec::new(), - Some(stage.join("preimages")), - ) - .unwrap(); - core.copy_up(Path::new("value.txt")).unwrap(); - fs::write(upper.join("value.txt"), b"staged").unwrap(); - fs::write(target.join("value.txt"), b"concurrent").unwrap(); - - let mut record = OverlayRecord { - generation: 0, - id: "conflicting-apply".into(), - target: target.clone(), - upper: OverlayUpper::Directory { - upper_dir: upper.clone(), - work_dir: work, - }, - merged_dir: stage.join("merged"), - stage_dir: stage.clone(), - excluded_paths: Vec::new(), - auto_apply: false, - auto_discard: false, - protect_target: false, - state: OverlayState::Staged, - }; - - let error = apply_overlay(&mut record).unwrap_err(); - assert!(error.to_string().contains("target changed after staging")); - assert_eq!(fs::read(target.join("value.txt")).unwrap(), b"concurrent"); - assert_eq!(fs::read(upper.join("value.txt")).unwrap(), b"staged"); - assert!(load_apply_records(&stage).unwrap().is_empty()); - } - - #[test] - fn target_applied_recovery_only_finishes_partially_pruned_opaque_upper() { - let tmp = tempdir().unwrap(); - let target = tmp.path().join("target"); - let stage = tmp.path().join("stage"); - let upper = stage.join("upper"); - fs::create_dir_all(target.join("replaced")).unwrap(); - fs::create_dir_all(upper.join("replaced")).unwrap(); - fs::write(target.join("replaced/old"), b"old").unwrap(); - fs::write(upper.join("replaced").join(OPAQUE_WHITEOUT), b"").unwrap(); - fs::write(upper.join("replaced/new"), b"new").unwrap(); - let mut record = OverlayRecord { - generation: 0, - id: "opaque-recovery".into(), - target: target.clone(), - upper: OverlayUpper::Directory { - upper_dir: upper.clone(), - work_dir: stage.join("work"), - }, - merged_dir: stage.join("merged"), - stage_dir: stage.clone(), - excluded_paths: Vec::new(), - auto_apply: false, - auto_discard: false, - protect_target: false, - state: OverlayState::Staged, - }; - let selection = ApplySelection { - paths: vec!["replaced".into()], - ..ApplySelection::default() - }; - let plan = plan_overlay_apply(&record, std::slice::from_ref(&target), &selection).unwrap(); - let apply_id = "opaque-partial-prune".to_string(); - append_apply_record( - &record, - ApplyRecord { - schema_version: APPLY_LEDGER_SCHEMA_VERSION, - apply_id: apply_id.clone(), - created_at_unix_ms: crate::util::unix_now_ms(), - overlay_id: record.id.clone(), - overlay_generation: record.generation, - target: target.clone(), - selection, - changes: plan.selected.clone(), - planned_paths: plan.selected_paths.iter().cloned().collect(), - preimages: Vec::new(), - state: ApplyRecordState::Prepared, - remaining_changes: 0, - }, - ) - .unwrap(); - - apply_prepared_target(&record, &plan.selected_paths, &[], &plan.selected, false).unwrap(); - mark_apply_target_applied(&record, &apply_id).unwrap(); - assert!(!target.join("replaced/old").exists()); - assert_eq!(fs::read(target.join("replaced/new")).unwrap(), b"new"); - - // Simulate a crash after opaque metadata was pruned but before the - // selected upper entries and ledger were finalized. - fs::remove_file(upper.join("replaced").join(OPAQUE_WHITEOUT)).unwrap(); - assert_eq!( - load_apply_records(&stage).unwrap()[0].state, - ApplyRecordState::TargetApplied - ); - - assert_eq!( - recover_pending_applies(&mut record, std::slice::from_ref(&target)).unwrap(), - vec![apply_id] - ); - assert_eq!(record.state, OverlayState::Applied); - assert!(!upper.exists()); - assert_eq!( - load_apply_records(&stage).unwrap()[0].state, - ApplyRecordState::Committed - ); - } - - #[test] - fn glob_excludes_leave_matching_subtrees_staged() { - let tmp = tempdir().unwrap(); - let target = tmp.path().join("target"); - let stage = tmp.path().join("stage"); - let upper = stage.join("upper"); - fs::create_dir_all(&target).unwrap(); - fs::create_dir_all(upper.join("src/generated")).unwrap(); - fs::write(upper.join("src/lib.rs"), b"accepted").unwrap(); - fs::write(upper.join("src/generated/code.rs"), b"pending").unwrap(); - let mut record = OverlayRecord { - generation: 0, - id: "glob".into(), - target: target.clone(), - upper: OverlayUpper::Directory { - upper_dir: upper.clone(), - work_dir: stage.join("work"), - }, - merged_dir: stage.join("merged"), - stage_dir: stage, - excluded_paths: Vec::new(), - auto_apply: false, - auto_discard: false, - protect_target: false, - state: OverlayState::Staged, - }; - - let outcome = apply_overlay_selected( - &mut record, - std::slice::from_ref(&target), - &ApplySelection { - includes: vec!["src/**".into()], - excludes: vec!["src/generated/**".into()], - ..ApplySelection::default() - }, - ) - .unwrap(); - assert_eq!(fs::read(target.join("src/lib.rs")).unwrap(), b"accepted"); - assert!(!target.join("src/generated/code.rs").exists()); - assert!(upper.join("src/generated/code.rs").is_file()); - assert!( - outcome - .remaining - .iter() - .any(|change| change.path == "src/generated/code.rs") - ); - assert_eq!(record.state, OverlayState::Staged); - } - - #[test] - fn opaque_directory_requires_atomic_selection() { - let tmp = tempdir().unwrap(); - let target = tmp.path().join("target"); - let stage = tmp.path().join("stage"); - let upper = stage.join("upper"); - fs::create_dir_all(target.join("replaced")).unwrap(); - fs::create_dir_all(upper.join("replaced")).unwrap(); - fs::write(target.join("replaced/old"), b"old").unwrap(); - fs::write(upper.join("replaced").join(OPAQUE_WHITEOUT), b"").unwrap(); - fs::write(upper.join("replaced/new"), b"new").unwrap(); - let mut record = OverlayRecord { - generation: 0, - id: "opaque-select".into(), - target: target.clone(), - upper: OverlayUpper::Directory { - upper_dir: upper, - work_dir: stage.join("work"), - }, - merged_dir: stage.join("merged"), - stage_dir: stage, - excluded_paths: Vec::new(), - auto_apply: false, - auto_discard: false, - protect_target: false, - state: OverlayState::Staged, - }; - - let error = plan_overlay_apply( - &record, - std::slice::from_ref(&target), - &ApplySelection { - paths: vec!["replaced/new".into()], - ..ApplySelection::default() - }, - ) - .unwrap_err(); - assert!(error.to_string().contains("opaque directory replaced")); - - apply_overlay_selected( - &mut record, - std::slice::from_ref(&target), - &ApplySelection { - paths: vec!["replaced".into()], - ..ApplySelection::default() - }, - ) - .unwrap(); - assert!(!target.join("replaced/old").exists()); - assert_eq!(fs::read(target.join("replaced/new")).unwrap(), b"new"); - } - - #[test] - fn selective_apply_expands_hard_link_groups() { - let tmp = tempdir().unwrap(); - let target = tmp.path().join("target"); - let stage = tmp.path().join("stage"); - let upper = stage.join("upper"); - fs::create_dir_all(&target).unwrap(); - fs::create_dir_all(&upper).unwrap(); - fs::write(upper.join("first"), b"linked").unwrap(); - fs::hard_link(upper.join("first"), upper.join("second")).unwrap(); - let mut record = OverlayRecord { - generation: 0, - id: "hard-links".into(), - target: target.clone(), - upper: OverlayUpper::Directory { - upper_dir: upper, - work_dir: stage.join("work"), - }, - merged_dir: stage.join("merged"), - stage_dir: stage, - excluded_paths: Vec::new(), - auto_apply: false, - auto_discard: false, - protect_target: false, - state: OverlayState::Staged, - }; - - let outcome = apply_overlay_selected( - &mut record, - std::slice::from_ref(&target), - &ApplySelection { - paths: vec!["first".into()], - ..ApplySelection::default() - }, - ) - .unwrap(); - assert!(outcome.applied.iter().any(|change| change.path == "first")); - assert!(outcome.applied.iter().any(|change| change.path == "second")); - assert_eq!( - fs::metadata(target.join("first")).unwrap().ino(), - fs::metadata(target.join("second")).unwrap().ino() - ); - assert_eq!(record.state, OverlayState::Applied); - } - - #[test] - fn apply_selection_rejects_parent_traversal() { - let tmp = tempdir().unwrap(); - let target = tmp.path().join("target"); - let upper = tmp.path().join("upper"); - fs::create_dir_all(&target).unwrap(); - fs::create_dir_all(&upper).unwrap(); - fs::write(upper.join("value"), b"value").unwrap(); - let record = OverlayRecord { - generation: 0, - id: "invalid-selection".into(), - target: target.clone(), - upper: OverlayUpper::Directory { - upper_dir: upper, - work_dir: tmp.path().join("work"), - }, - merged_dir: tmp.path().join("merged"), - stage_dir: tmp.path().to_path_buf(), - excluded_paths: Vec::new(), - auto_apply: false, - auto_discard: false, - protect_target: false, - state: OverlayState::Staged, - }; - let error = plan_overlay_apply( - &record, - std::slice::from_ref(&target), - &ApplySelection { - paths: vec!["../outside".into()], - ..ApplySelection::default() - }, - ) - .unwrap_err(); - assert!(error.to_string().contains("cannot contain `..`")); - } - - #[test] - fn changeset_classifies_added_modified_deleted_and_opaque_entries() { - let tmp = tempdir().unwrap(); - let target = tmp.path().join("target"); - let upper = tmp.path().join("upper"); - fs::create_dir_all(target.join("dir")).unwrap(); - fs::write(target.join("modified.txt"), b"old").unwrap(); - fs::write(target.join("deleted.txt"), b"gone").unwrap(); - fs::write(target.join("dir/lower.txt"), b"lower").unwrap(); - fs::create_dir_all(upper.join("dir")).unwrap(); - fs::write(upper.join("modified.txt"), b"new").unwrap(); - fs::write(upper.join("added.txt"), b"added").unwrap(); - fs::write(upper.join(".wh.deleted.txt"), b"").unwrap(); - fs::write(upper.join("dir/.wh..wh..opq"), b"").unwrap(); - let record = OverlayRecord { - generation: 0, - id: "changes".into(), - target: target.clone(), - upper: OverlayUpper::Directory { - upper_dir: upper, - work_dir: tmp.path().join("work"), - }, - merged_dir: tmp.path().join("merged"), - stage_dir: tmp.path().to_path_buf(), - excluded_paths: Vec::new(), - auto_apply: false, - auto_discard: false, - protect_target: false, - state: OverlayState::Staged, - }; - - let changes = overlay_changes(&record, std::slice::from_ref(&target)).unwrap(); - assert!( - changes - .iter() - .any(|change| change.path == "added.txt" && change.kind == ChangeKind::Added) - ); - assert!(changes.iter().any(|change| { - change.path == "modified.txt" && change.kind == ChangeKind::Modified - })); - assert!( - changes.iter().any(|change| { - change.path == "deleted.txt" && change.kind == ChangeKind::Deleted - }) - ); - assert!( - changes - .iter() - .any(|change| change.path == "dir" && change.kind == ChangeKind::Opaque) - ); - } - - #[test] - fn apply_rejects_an_immutable_image_target() { - let tmp = tempdir().unwrap(); - let target = tmp.path().join("target"); - let upper = tmp.path().join("upper"); - fs::create_dir_all(&target).unwrap(); - fs::create_dir_all(&upper).unwrap(); - fs::write(target.join("system"), b"cached").unwrap(); - fs::write(upper.join("system"), b"changed").unwrap(); - let mut record = OverlayRecord { - generation: 0, - id: "immutable".into(), - target: target.clone(), - upper: OverlayUpper::Directory { - upper_dir: upper, - work_dir: tmp.path().join("work"), - }, - merged_dir: tmp.path().join("merged"), - stage_dir: tmp.path().to_path_buf(), - excluded_paths: Vec::new(), - auto_apply: false, - auto_discard: false, - protect_target: true, - state: OverlayState::Staged, - }; - let error = apply_overlay(&mut record).unwrap_err(); - assert!(error.to_string().contains("immutable image rootfs")); - assert_eq!(fs::read(target.join("system")).unwrap(), b"cached"); - assert_eq!(record.state, OverlayState::Staged); - } - - #[test] - fn apply_honors_opaque_before_children_and_preserves_posix_types() { - let tmp = tempdir().unwrap(); - let target = tmp.path().join("target"); - let upper = tmp.path().join("upper"); - fs::create_dir_all(target.join("replaced")).unwrap(); - fs::write(target.join("replaced/old"), b"old").unwrap(); - fs::create_dir_all(upper.join("replaced")).unwrap(); - fs::write(upper.join("replaced").join(OPAQUE_WHITEOUT), b"").unwrap(); - fs::write(upper.join("replaced/new"), b"new").unwrap(); - fs::set_permissions( - upper.join("replaced/new"), - fs::Permissions::from_mode(0o751), - ) - .unwrap(); - std::os::unix::fs::symlink("new", upper.join("replaced/link")).unwrap(); - fs::hard_link(upper.join("replaced/new"), upper.join("replaced/hard-link")).unwrap(); - - apply_upper_onto_target(&upper, &target).unwrap(); - - assert!(!target.join("replaced/old").exists()); - assert_eq!(fs::read(target.join("replaced/new")).unwrap(), b"new"); - assert_eq!( - fs::read_link(target.join("replaced/link")).unwrap(), - PathBuf::from("new") - ); - let original = fs::metadata(target.join("replaced/new")).unwrap(); - let linked = fs::metadata(target.join("replaced/hard-link")).unwrap(); - assert_eq!(original.ino(), linked.ino()); - assert_eq!(original.mode() & 0o777, 0o751); - } - - #[test] - fn status_does_not_follow_symlinked_directories() { - let tmp = tempdir().unwrap(); - let upper = tmp.path().join("upper"); - fs::create_dir_all(&upper).unwrap(); - std::os::unix::fs::symlink(tmp.path(), upper.join("loop")).unwrap(); - let record = OverlayRecord { - generation: 0, - id: "t".into(), - target: tmp.path().join("target"), - upper: OverlayUpper::Directory { - upper_dir: upper, - work_dir: tmp.path().join("work"), - }, - merged_dir: tmp.path().join("merged"), - stage_dir: tmp.path().to_path_buf(), - excluded_paths: Vec::new(), - auto_apply: false, - auto_discard: false, - protect_target: false, - state: OverlayState::Staged, - }; - let status = overlay_status(&record).unwrap(); - assert_eq!(status.changed_files, 1); - } - - #[test] - fn discard_clears_upper() { - let tmp = tempdir().unwrap(); - let upper = tmp.path().join("upper"); - fs::create_dir_all(&upper).unwrap(); - fs::write(upper.join("x"), b"1").unwrap(); - let mut rec = OverlayRecord { - id: "t".into(), - generation: 0, - target: tmp.path().join("target"), - upper: OverlayUpper::Directory { - upper_dir: upper.clone(), - work_dir: tmp.path().join("work"), - }, - merged_dir: tmp.path().join("merged"), - stage_dir: tmp.path().to_path_buf(), - excluded_paths: Vec::new(), - auto_apply: false, - auto_discard: false, - protect_target: false, - state: OverlayState::Staged, - }; - discard_overlay(&mut rec).unwrap(); - assert!(!upper.exists()); - assert_eq!(rec.state, OverlayState::Discarded); - } - - #[test] - fn terminal_decisions_are_idempotent_but_cannot_be_reversed() { - let applied_root = tempdir().unwrap(); - let applied_target = applied_root.path().join("target"); - let applied_upper = applied_root.path().join("upper"); - fs::create_dir_all(&applied_target).unwrap(); - fs::create_dir_all(&applied_upper).unwrap(); - fs::write(applied_upper.join("value"), b"applied").unwrap(); - let mut applied = OverlayRecord { - id: "applied-run".into(), - generation: 0, - target: applied_target, - upper: OverlayUpper::Directory { - upper_dir: applied_upper, - work_dir: applied_root.path().join("work"), - }, - merged_dir: applied_root.path().join("merged"), - stage_dir: applied_root.path().to_path_buf(), - excluded_paths: Vec::new(), - auto_apply: false, - auto_discard: false, - protect_target: false, - state: OverlayState::Staged, - }; - apply_overlay(&mut applied).unwrap(); - apply_overlay(&mut applied).unwrap(); - let error = discard_overlay(&mut applied).unwrap_err(); - assert_eq!( - error.to_string(), - "overlay applied-run was already applied; drop cannot undo applied changes" - ); - assert_eq!(applied.state, OverlayState::Applied); - - let dropped_root = tempdir().unwrap(); - let dropped_upper = dropped_root.path().join("upper"); - fs::create_dir_all(&dropped_upper).unwrap(); - fs::write(dropped_upper.join("value"), b"discarded").unwrap(); - let mut dropped = OverlayRecord { - id: "dropped-run".into(), - generation: 0, - target: dropped_root.path().join("target"), - upper: OverlayUpper::Directory { - upper_dir: dropped_upper, - work_dir: dropped_root.path().join("work"), - }, - merged_dir: dropped_root.path().join("merged"), - stage_dir: dropped_root.path().to_path_buf(), - excluded_paths: Vec::new(), - auto_apply: false, - auto_discard: false, - protect_target: false, - state: OverlayState::Staged, - }; - discard_overlay(&mut dropped).unwrap(); - discard_overlay(&mut dropped).unwrap(); - let error = apply_overlay(&mut dropped).unwrap_err(); - assert_eq!( - error.to_string(), - "overlay dropped-run was already dropped; apply cannot recover discarded changes" - ); - assert_eq!(dropped.state, OverlayState::Discarded); - } -} diff --git a/crates/persisting-pvisor/src/runtime/registry.rs b/crates/persisting-pvisor/src/runtime/registry.rs deleted file mode 100644 index f34c5c87f..000000000 --- a/crates/persisting-pvisor/src/runtime/registry.rs +++ /dev/null @@ -1,723 +0,0 @@ -//! Durable Run identity, project association, and liveness metadata. - -use super::overlay::{ - OverlayRecord, OverlayUpper, ReadOnlyOverlayMount, load_overlay_record, - mount_overlay_record_read_only, overlay_status, -}; -use crate::util::{atomic_write, create_dir_all_durable}; -use anyhow::Context; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::fs::{self, File, OpenOptions}; -use std::os::fd::AsRawFd; -use std::os::unix::fs::PermissionsExt; -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::thread::JoinHandle; -use std::time::Duration; - -use persisting_agentctl::{ExecutorDescriptor, ResourceLimits}; - -pub const RUN_META_FILENAME: &str = "run.json"; -pub const LEASE_FILENAME: &str = "lease.lock"; -pub const CONTROL_FILENAME: &str = "control.sock"; - -pub fn default_run_home() -> PathBuf { - if let Some(root) = std::env::var_os("PERSISTING_RUN_HOME") { - return PathBuf::from(root); - } - if let Some(home) = std::env::var_os("HOME") { - return PathBuf::from(home).join(".persisting").join("runs"); - } - std::env::temp_dir().join("persisting-runs") -} - -/// Provenance for a Run started from a logical checkpoint. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct RunLineage { - pub parent_run_id: String, - pub checkpoint_id: String, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] -pub struct EnvironmentProjection { - #[serde(default)] - pub inherits_host: bool, - #[serde(default)] - pub projected_keys: Vec, - #[serde(default)] - pub runtime_injected_keys: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RunRecord { - pub schema_version: u32, - pub run_id: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub parent_run_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub task_id: Option, - pub session_id: String, - pub agent: String, - pub pid: u32, - pub command: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub executor: Option, - pub state: String, - pub started_at_unix_ms: u64, - pub finished_at_unix_ms: Option, - pub storage: PathBuf, - /// Reusable project workspace associated with this Run. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub workspace: Option, - #[serde(default)] - pub overlaynet_listen: Option, - #[serde(default)] - pub network_interception: Option, - #[serde(default)] - pub network_interception_metrics: Option, - pub gateway_listen: Option, - pub network: serde_json::Value, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub network_policy: Option, - #[serde(default)] - pub environment: EnvironmentProjection, - #[serde(default)] - pub resource_limits: ResourceLimits, - pub overlay: Option, - #[serde(default)] - pub overlay_lowers: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub lineage: Option, - #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")] - pub orchestration: std::collections::BTreeMap, -} - -impl RunRecord { - pub fn stage_dir(&self) -> PathBuf { - self.overlay - .as_ref() - .map(|record| record.stage_dir.clone()) - .unwrap_or_else(|| self.storage.clone()) - } - - pub fn write(&self) -> anyhow::Result<()> { - let stage = self.stage_dir(); - let path = stage.join(RUN_META_FILENAME); - atomic_write(&path, &serde_json::to_vec_pretty(self)?, 0o600)?; - - let index_dir = self.storage.join(".pvisor").join("runs"); - atomic_write( - &index_dir.join(format!("{}.json", encode_id(&self.run_id))), - &serde_json::to_vec_pretty(&RunIndex { - run_id: self.run_id.clone(), - stage_dir: stage, - })?, - 0o600, - )?; - Ok(()) - } - - pub fn read(stage: &Path) -> anyhow::Result { - let path = stage.join(RUN_META_FILENAME); - Ok(serde_json::from_slice(&fs::read(&path)?)?) - } - - pub fn remove_index(&self) -> anyhow::Result<()> { - let path = self - .storage - .join(".pvisor") - .join("runs") - .join(format!("{}.json", encode_id(&self.run_id))); - match fs::remove_file(path) { - Ok(()) => Ok(()), - Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), - Err(error) => Err(error.into()), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct RunIndex { - run_id: String, - stage_dir: PathBuf, -} - -/// Exclusive process-lifetime lease. Its file is intentionally retained. -pub struct RunLease { - file: File, -} - -impl RunLease { - pub fn acquire(stage_dir: &Path) -> anyhow::Result { - create_dir_all_durable(stage_dir)?; - let file = OpenOptions::new() - .create(true) - .read(true) - .write(true) - .truncate(false) - .open(stage_dir.join(LEASE_FILENAME))?; - let result = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) }; - if result != 0 { - anyhow::bail!("Run storage is already leased: {}", stage_dir.display()); - } - Ok(Self { file }) - } -} - -impl Drop for RunLease { - fn drop(&mut self) { - let _ = unsafe { libc::flock(self.file.as_raw_fd(), libc::LOCK_UN) }; - } -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(tag = "op", rename_all = "snake_case")] -enum ControlRequest { - Ping, - OverlayStatus, - MountInspect, - UnmountInspect { id: String }, -} - -#[derive(Debug, Serialize, Deserialize)] -struct ControlResponse { - ok: bool, - id: Option, - mountpoint: Option, - error: Option, - overlay_status: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ControlOverlayStatus { - pub changed_files: usize, - pub whiteouts: usize, - pub sample_paths: Vec, -} - -/// Attempt-scoped local control endpoint. The owning pVisor creates read-only -/// views so a second CLI process never interferes with a live writable mount. -pub struct RunControlServer { - stop: Arc, - join: Option>, - socket_path: PathBuf, - locator_path: PathBuf, -} - -impl RunControlServer { - pub fn start(record: &RunRecord) -> anyhow::Result> { - let Some(overlay) = record.overlay.clone() else { - return Ok(None); - }; - let lowers = if record.overlay_lowers.is_empty() { - vec![overlay.target.clone()] - } else { - record.overlay_lowers.clone() - }; - let locator_path = record.stage_dir().join(CONTROL_FILENAME); - if locator_path.exists() || locator_path.is_symlink() { - fs::remove_file(&locator_path)?; - } - // macOS sockaddr_un paths are short. Bind in the fixed, short `/tmp` - // directory rather than `std::env::temp_dir()` (which can point at a deep - // per-user path) and expose a stable stage-local symlink for discovery. - let socket_path = - Path::new("/tmp").join(format!("pvisor-{}.sock", uuid::Uuid::new_v4().simple())); - let listener = std::os::unix::net::UnixListener::bind(&socket_path)?; - fs::set_permissions(&socket_path, fs::Permissions::from_mode(0o600))?; - std::os::unix::fs::symlink(&socket_path, &locator_path)?; - listener.set_nonblocking(true)?; - let stop = Arc::new(AtomicBool::new(false)); - let thread_stop = Arc::clone(&stop); - let stage = record.stage_dir(); - let join = std::thread::Builder::new() - .name(format!("pvisor-control-{}", record.run_id)) - .spawn(move || { - let mut mounts: HashMap = HashMap::new(); - while !thread_stop.load(Ordering::Acquire) { - match listener.accept() { - Ok((stream, _)) => { - serve_control(stream, &stage, &overlay, &lowers, &mut mounts); - } - Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { - std::thread::sleep(Duration::from_millis(20)); - } - Err(_) => break, - } - } - })?; - Ok(Some(Self { - stop, - join: Some(join), - socket_path, - locator_path, - })) - } -} - -impl Drop for RunControlServer { - fn drop(&mut self) { - self.stop.store(true, Ordering::Release); - let _ = std::os::unix::net::UnixStream::connect(&self.socket_path); - if let Some(join) = self.join.take() { - let _ = join.join(); - } - let _ = fs::remove_file(&self.socket_path); - let _ = fs::remove_file(&self.locator_path); - } -} - -fn serve_control( - mut stream: std::os::unix::net::UnixStream, - stage: &Path, - overlay: &OverlayRecord, - lowers: &[PathBuf], - mounts: &mut HashMap, -) { - use std::io::{BufRead, Write}; - let request = (|| -> anyhow::Result { - let mut line = String::new(); - std::io::BufReader::new(&stream).read_line(&mut line)?; - Ok(serde_json::from_str(&line)?) - })(); - let response = match request { - Ok(ControlRequest::Ping) => ControlResponse { - ok: true, - id: None, - mountpoint: None, - error: None, - overlay_status: None, - }, - Ok(ControlRequest::OverlayStatus) => match overlay_status(overlay) { - Ok(status) => ControlResponse { - ok: true, - id: None, - mountpoint: None, - error: None, - overlay_status: Some(ControlOverlayStatus { - changed_files: status.changed_files, - whiteouts: status.whiteouts, - sample_paths: status.sample_paths, - }), - }, - Err(error) => control_error(error), - }, - Ok(ControlRequest::MountInspect) => { - let id = uuid::Uuid::new_v4().to_string(); - let mountpoint = stage.join("inspect").join(&id).join("merged"); - match mount_overlay_record_read_only(overlay, lowers, &mountpoint) { - Ok(mount) => { - mounts.insert(id.clone(), mount); - ControlResponse { - ok: true, - id: Some(id), - mountpoint: Some(mountpoint), - error: None, - overlay_status: None, - } - } - Err(error) => control_error(error), - } - } - Ok(ControlRequest::UnmountInspect { id }) => { - if let Some(mount) = mounts.remove(&id) { - match mount.unmount() { - Ok(()) => ControlResponse { - ok: true, - id: None, - mountpoint: None, - error: None, - overlay_status: None, - }, - Err(error) => control_error(error), - } - } else { - control_error(anyhow::anyhow!("unknown inspect session {id}")) - } - } - Err(error) => control_error(error), - }; - if let Ok(mut body) = serde_json::to_vec(&response) { - body.push(b'\n'); - let _ = stream.write_all(&body); - } -} - -fn control_error(error: impl std::fmt::Display) -> ControlResponse { - ControlResponse { - ok: false, - id: None, - mountpoint: None, - error: Some(error.to_string()), - overlay_status: None, - } -} - -fn control_request(stage: &Path, request: &ControlRequest) -> anyhow::Result { - use std::io::{BufRead, Write}; - let mut stream = std::os::unix::net::UnixStream::connect(stage.join(CONTROL_FILENAME))?; - serde_json::to_writer(&mut stream, request)?; - stream.write_all(b"\n")?; - let mut line = String::new(); - std::io::BufReader::new(stream).read_line(&mut line)?; - let response: ControlResponse = serde_json::from_str(&line)?; - if !response.ok { - anyhow::bail!( - "pVisor control request failed: {}", - response.error.as_deref().unwrap_or("unknown error") - ); - } - Ok(response) -} - -pub fn control_ping(stage: &Path) -> bool { - control_request(stage, &ControlRequest::Ping).is_ok() -} - -pub fn control_mount_inspect(stage: &Path) -> anyhow::Result<(String, PathBuf)> { - let response = control_request(stage, &ControlRequest::MountInspect)?; - Ok(( - response.id.context("control response missing inspect id")?, - response - .mountpoint - .context("control response missing inspect mountpoint")?, - )) -} - -pub fn control_overlay_status(stage: &Path) -> anyhow::Result { - control_request(stage, &ControlRequest::OverlayStatus)? - .overlay_status - .context("control response missing OverlayFS status") -} - -pub fn control_unmount_inspect(stage: &Path, id: String) -> anyhow::Result<()> { - control_request(stage, &ControlRequest::UnmountInspect { id })?; - Ok(()) -} - -pub fn is_live(stage_dir: &Path) -> anyhow::Result { - let path = stage_dir.join(LEASE_FILENAME); - if !path.exists() { - return Ok(false); - } - let file = OpenOptions::new().read(true).write(true).open(path)?; - let result = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) }; - if result == 0 { - let _ = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_UN) }; - Ok(false) - } else { - let error = std::io::Error::last_os_error(); - if error - .raw_os_error() - .is_some_and(|code| code == libc::EWOULDBLOCK || code == libc::EAGAIN) - { - Ok(true) - } else { - Err(error.into()) - } - } -} - -/// Resolve a Run from a run id, stage, upper directory, database, or a path -/// inside the target/merged workspace. -pub fn resolve_run(selector: Option<&Path>, storage: &Path) -> anyhow::Result { - if let Some(selector) = selector { - if selector == Path::new("last") { - return resolve_last(storage, std::env::current_dir().ok().as_deref()); - } - if selector.exists() || selector.components().count() > 1 { - return resolve_path(selector); - } - let id = selector.to_string_lossy(); - let index = storage - .join(".pvisor") - .join("runs") - .join(format!("{}.json", encode_id(&id))); - if index.exists() { - let index: RunIndex = serde_json::from_slice(&fs::read(index)?)?; - return RunRecord::read(&index.stage_dir); - } - if let Some(record) = default_runs()? - .into_iter() - .find(|record| record.run_id == id) - { - return Ok(record); - } - anyhow::bail!("pVisor Run not found: {}", selector.display()); - } - - if let Ok(current) = std::env::current_dir() { - if let Ok(record) = resolve_path(¤t) { - return Ok(record); - } - if let Ok(record) = latest_workspace_run(¤t) { - return Ok(record); - } - } - latest_run(storage).or_else(|_| latest_default_run()) -} - -fn resolve_last(storage: &Path, current: Option<&Path>) -> anyhow::Result { - if let Some(current) = current { - if let Ok(record) = resolve_path(current) { - return Ok(record); - } - if let Ok(record) = latest_workspace_run(current) { - return Ok(record); - } - } - latest_run(storage).or_else(|_| latest_default_run()) -} - -fn default_runs() -> anyhow::Result> { - let mut records = Vec::new(); - let mut roots = vec![ - default_run_home(), - std::env::temp_dir().join("persisting-runs"), - ]; - roots.sort(); - roots.dedup(); - for root in roots { - if !root.is_dir() { - continue; - } - for entry in fs::read_dir(root)? { - let storage = entry?.path(); - if let Ok(record) = RunRecord::read(&storage) { - records.push(record); - } else { - records.extend(all_runs(&storage)?); - } - } - } - records.sort_by_key(|record| std::cmp::Reverse(record.started_at_unix_ms)); - Ok(records) -} - -fn latest_default_run() -> anyhow::Result { - default_runs()?.into_iter().next().ok_or_else(|| { - anyhow::anyhow!( - "no pVisor Runs found under {}", - default_run_home().display() - ) - }) -} - -fn latest_workspace_run(workspace: &Path) -> anyhow::Result { - let workspace = workspace - .canonicalize() - .unwrap_or_else(|_| workspace.to_path_buf()); - default_runs()? - .into_iter() - .find(|record| { - record.workspace.as_ref().is_some_and(|root| { - let root = root.canonicalize().unwrap_or_else(|_| root.clone()); - workspace.starts_with(root) - }) - }) - .ok_or_else(|| { - anyhow::anyhow!("no pVisor Runs found for workspace {}", workspace.display()) - }) -} - -fn resolve_path(path: &Path) -> anyhow::Result { - let absolute = path.canonicalize().unwrap_or_else(|_| path.to_path_buf()); - if absolute.join(".pvisor").join("runs").is_dir() { - return latest_run(&absolute); - } - let mut candidates = Vec::new(); - if absolute.is_file() { - if absolute - .file_name() - .is_some_and(|name| name == RUN_META_FILENAME) - { - candidates.push(absolute.parent().unwrap_or(Path::new(".")).to_path_buf()); - } else if let Some(parent) = absolute.parent() { - candidates.push(parent.to_path_buf()); - } - } else { - candidates.push(absolute.clone()); - if absolute.file_name().is_some_and(|name| name == "upper") - && let Some(parent) = absolute.parent() - { - candidates.push(parent.to_path_buf()); - } - } - candidates.extend(absolute.ancestors().map(Path::to_path_buf)); - for stage in candidates { - if stage.join(RUN_META_FILENAME).is_file() { - return RunRecord::read(&stage); - } - if stage.join("overlay.json").is_file() { - let overlay = load_overlay_record(&stage)?; - if let Ok(record) = RunRecord::read(&overlay.stage_dir) { - return Ok(record); - } - } - } - - if let Ok(record) = latest_workspace_run(&absolute) { - return Ok(record); - } - - // A target or merged path is not necessarily below stage_dir. Scan the - // nearest project storage and compare canonical roots. - for ancestor in absolute.ancestors() { - let storage = ancestor.join(".persisting").join("capture"); - if storage.is_dir() { - for record in all_runs(&storage)? { - if record.overlay.as_ref().is_some_and(|overlay| { - path_within(&absolute, &overlay.target) - || path_within(&absolute, &overlay.merged_dir) - || match &overlay.upper { - OverlayUpper::Directory { upper_dir, .. } => { - path_within(&absolute, upper_dir) - } - OverlayUpper::Jujutsu { - store_path, - upper_dir, - .. - } => { - path_within(&absolute, upper_dir) - || path_within(&absolute, store_path) - } - } - }) { - return Ok(record); - } - } - } - } - anyhow::bail!("no pVisor Run metadata found for {}", path.display()) -} - -fn path_within(path: &Path, root: &Path) -> bool { - let root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf()); - path.starts_with(root) -} - -pub fn all_runs(storage: &Path) -> anyhow::Result> { - let dir = storage.join(".pvisor").join("runs"); - if !dir.is_dir() { - return Ok(Vec::new()); - } - let mut records = Vec::new(); - for entry in fs::read_dir(dir)? { - let path = entry?.path(); - if path - .extension() - .is_some_and(|extension| extension == "json") - { - let index: RunIndex = match serde_json::from_slice(&fs::read(path)?) { - Ok(index) => index, - Err(_) => continue, - }; - if let Ok(record) = RunRecord::read(&index.stage_dir) { - records.push(record); - } - } - } - records.sort_by_key(|record| std::cmp::Reverse(record.started_at_unix_ms)); - Ok(records) -} - -fn latest_run(storage: &Path) -> anyhow::Result { - all_runs(storage)? - .into_iter() - .next() - .ok_or_else(|| anyhow::anyhow!("no pVisor Runs found under {}", storage.display())) -} - -fn encode_id(id: &str) -> String { - let mut encoded = String::with_capacity(id.len() * 2); - for byte in id.as_bytes() { - use std::fmt::Write; - let _ = write!(encoded, "{byte:02x}"); - } - encoded -} - -#[cfg(test)] -mod tests { - use super::*; - - fn record(storage: &Path, stage: &Path, upper: &Path) -> RunRecord { - RunRecord { - schema_version: 1, - run_id: "run-test".into(), - parent_run_id: None, - task_id: None, - session_id: "session-test".into(), - agent: "test".into(), - pid: 1, - command: vec!["true".into()], - executor: None, - state: "completed".into(), - started_at_unix_ms: 1, - finished_at_unix_ms: Some(2), - storage: storage.to_path_buf(), - workspace: None, - overlaynet_listen: None, - network_interception: None, - network_interception_metrics: None, - gateway_listen: None, - network: serde_json::json!({"mode": "ambient"}), - network_policy: None, - environment: Default::default(), - resource_limits: Default::default(), - overlay: Some(OverlayRecord { - id: "session-test".into(), - generation: 0, - target: storage.join("target"), - upper: OverlayUpper::Directory { - upper_dir: upper.to_path_buf(), - work_dir: stage.join("work"), - }, - merged_dir: stage.join("merged"), - stage_dir: stage.to_path_buf(), - excluded_paths: Vec::new(), - auto_apply: false, - auto_discard: false, - protect_target: false, - state: super::super::overlay::OverlayState::Staged, - }), - overlay_lowers: vec![storage.join("target")], - lineage: None, - orchestration: Default::default(), - } - } - - #[test] - fn lease_reports_live_only_while_held() { - let temp = tempfile::tempdir().unwrap(); - assert!(!is_live(temp.path()).unwrap()); - let lease = RunLease::acquire(temp.path()).unwrap(); - assert!(is_live(temp.path()).unwrap()); - drop(lease); - assert!(!is_live(temp.path()).unwrap()); - } - - #[test] - fn run_resolves_from_id_stage_and_upper() { - let temp = tempfile::tempdir().unwrap(); - let storage = temp.path().join("store"); - let stage = storage.join(".overlay/session-test"); - let upper = stage.join("upper"); - fs::create_dir_all(&upper).unwrap(); - let record = record(&storage, &stage, &upper); - record.write().unwrap(); - - assert_eq!( - resolve_run(Some(Path::new("run-test")), &storage) - .unwrap() - .run_id, - "run-test" - ); - assert_eq!(resolve_path(&stage).unwrap().run_id, "run-test"); - assert_eq!(resolve_path(&upper).unwrap().run_id, "run-test"); - assert_eq!(resolve_path(&storage).unwrap().run_id, "run-test"); - assert_eq!(resolve_last(&storage, None).unwrap().run_id, "run-test"); - } -} diff --git a/crates/persisting-pvisor/src/runtime/supervisor.rs b/crates/persisting-pvisor/src/runtime/supervisor.rs deleted file mode 100644 index c5f041025..000000000 --- a/crates/persisting-pvisor/src/runtime/supervisor.rs +++ /dev/null @@ -1,590 +0,0 @@ -use super::attempt::{ - AttemptPrepareOpts, AttemptSession, OverlayAttemptPrepareOpts, apply_implant, prepare_attempt, - prepare_overlay_attempt, prepare_storage_attempt, -}; -use super::implant::{ImplantPlan, OverlayHint}; -use crate::TrajectoryEventSink; -use crate::{GatewayDriverConfig, NetworkDriverConfig, OverlayNetMode}; -use persisting_agentctl::{AttemptId, NetworkCapability, RunSpec}; -use persisting_agentctl::{ControlController, PolicyControlController}; -use persisting_gateway::config::ProxyConfig; -use std::path::PathBuf; -use std::sync::Arc; - -/// Runtime features and strong capability enforcement available for one Attempt. -/// -/// `network` and `filesystem` report non-bypassable policy enforcement, not -/// proxy injection or a staged filesystem projection. -#[derive(Debug, Clone)] -pub struct RuntimeCapabilities { - pub agentctl: bool, - pub gateway: bool, - pub network: bool, - pub filesystem: bool, - pub providers: Vec<&'static str>, - /// Network interception support available to a VM Attempt. This is not a - /// claim that every configured executor is currently enforcing it. - pub vm_network: bool, -} - -impl Default for RuntimeCapabilities { - fn default() -> Self { - let vm_network = vm_network_supported(); - let mut providers = vec![ - "local-process", - "agentctl-unix-v1", - "in-process-capture", - "overlaynet-explicit-proxy", - "fs-overlay-staging", - ]; - if vm_network { - providers.push("overlaynet-vm-smoltcp"); - } - Self { - agentctl: true, - gateway: true, - network: false, - filesystem: false, - providers, - vm_network, - } - } -} - -fn vm_network_supported() -> bool { - cfg!(any( - target_os = "linux", - all(target_os = "macos", target_arch = "aarch64") - )) -} - -fn network_config_from_capability( - capability: &NetworkCapability, -) -> persisting_overlaynet::NetworkConfig { - use persisting_agentctl::NetworkDefaultAction; - use persisting_overlaynet::NetworkMode; - - match capability { - NetworkCapability::Ambient => persisting_overlaynet::NetworkConfig::default(), - NetworkCapability::Deny => persisting_overlaynet::NetworkConfig { - mode: NetworkMode::NoNetwork, - ..Default::default() - }, - NetworkCapability::AllowList { hosts, rules } => persisting_overlaynet::NetworkConfig { - mode: NetworkMode::Allowlist, - allowed_hosts: hosts.clone(), - rules: rules.clone(), - ..Default::default() - }, - NetworkCapability::Policy { - default_action, - allow, - deny, - limits, - } => persisting_overlaynet::NetworkConfig { - mode: match default_action { - NetworkDefaultAction::Allow => NetworkMode::Public, - NetworkDefaultAction::Deny => NetworkMode::Allowlist, - }, - allowed_hosts: Vec::new(), - rules: allow.clone(), - deny_rules: deny.clone(), - limits: limits.clone(), - }, - } -} - -/// Builder for Attempt prepare options (capture / overlay). Crate-private; -/// public configuration goes through [`crate::PVisorBuilder`]. -#[derive(Clone, Default)] -pub struct RuntimeSupervisorBuilder { - proxy: Option, - gateway_output_dir: Option, - gateway_enabled: bool, - storage: Option, - stream_markdown: bool, - sink: Option>, - overlay: OverlayHint, - controller: Option>, - network: Option, -} - -impl std::fmt::Debug for RuntimeSupervisorBuilder { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("RuntimeSupervisorBuilder") - .field("proxy", &self.proxy.as_ref().map(|_| "")) - .field("gateway_output_dir", &self.gateway_output_dir) - .field("storage", &self.storage) - .field("stream_markdown", &self.stream_markdown) - .field("sink", &self.sink.as_ref().map(|_| "")) - .field("overlay", &self.overlay) - .field("network", &self.network) - .finish_non_exhaustive() - } -} - -impl RuntimeSupervisorBuilder { - pub fn new() -> Self { - Self::default() - } - - pub fn gateway(mut self, gateway: GatewayDriverConfig) -> Self { - self.proxy = Some(gateway.proxy); - self.gateway_output_dir = Some(gateway.output_dir); - self.stream_markdown = gateway.stream_markdown; - self.gateway_enabled = gateway.gateway_enabled; - self - } - - pub fn storage(mut self, storage: PathBuf) -> Self { - self.storage = Some(storage); - self - } - - pub fn trajectory_sink(mut self, sink: Arc) -> Self { - self.sink = Some(sink); - self - } - - pub fn overlay(mut self, overlay: OverlayHint) -> Self { - self.overlay = overlay; - self - } - - pub fn control_controller(mut self, controller: Arc) -> Self { - self.controller = Some(controller); - self - } - - pub fn network(mut self, network: NetworkDriverConfig) -> Self { - self.network = Some(network); - self - } - - pub fn build(self) -> RuntimeSupervisor { - RuntimeSupervisor { - proxy: self.proxy, - gateway_output_dir: self.gateway_output_dir, - gateway_enabled: self.gateway_enabled, - storage: self.storage, - stream_markdown: self.stream_markdown, - sink: self.sink, - overlay: self.overlay, - controller: self - .controller - .unwrap_or_else(|| Arc::new(PolicyControlController)), - network: self.network, - } - } -} - -/// Capture / network / overlay prepare options for one Attempt. -#[derive(Clone)] -pub struct RuntimeSupervisor { - proxy: Option, - gateway_output_dir: Option, - gateway_enabled: bool, - storage: Option, - stream_markdown: bool, - sink: Option>, - overlay: OverlayHint, - controller: Arc, - network: Option, -} - -impl Default for RuntimeSupervisor { - fn default() -> Self { - RuntimeSupervisorBuilder::new().build() - } -} - -impl RuntimeSupervisor { - fn network_mode(&self) -> OverlayNetMode { - self.network - .as_ref() - .map_or(OverlayNetMode::Auto, |network| network.mode) - } - - fn effective_network_config(&self, spec: &RunSpec) -> persisting_overlaynet::NetworkConfig { - self.network - .as_ref() - .map(|network| network.network.clone()) - .or_else(|| self.proxy.as_ref().map(|proxy| proxy.network.clone())) - .unwrap_or_else(|| network_config_from_capability(&spec.capabilities.network)) - } - - pub(crate) fn vm_network_is_enforcing(&self) -> bool { - self.network_mode() == OverlayNetMode::Auto && vm_network_supported() - } - - pub(crate) fn vm_network_is_requested(&self) -> bool { - self.network_mode() == OverlayNetMode::Auto - } - - pub(crate) fn proxy_network_is_configured(&self) -> bool { - self.proxy.is_some() - } - - pub(crate) fn apply_network_capability(&self, spec: &mut RunSpec) { - let network = self.effective_network_config(spec); - spec.capabilities.network = persisting_overlaynet::policy::network_capability(&network); - } - - fn vm_network_options( - &self, - mut network: persisting_overlaynet::NetworkConfig, - supervisor_limits: &[persisting_agentctl::NetworkBandwidthLimit], - attempt_id: &AttemptId, - ) -> super::attempt::VmNetworkPrepareOpts { - network.limits.extend_from_slice(supervisor_limits); - super::attempt::VmNetworkPrepareOpts { - network, - controller: Arc::clone(&self.controller), - attempt_id: attempt_id.to_string(), - } - } - - pub fn capabilities(&self) -> RuntimeCapabilities { - RuntimeCapabilities::default() - } - - /// Start configured pVisor drivers and merge their implant into `spec`. - pub fn prepare( - &self, - spec: &mut RunSpec, - supervisor_limits: &[persisting_agentctl::NetworkBandwidthLimit], - vm_executor: bool, - attempt_id: &AttemptId, - ) -> anyhow::Result> { - let network_mode = self.network_mode(); - let network = self.effective_network_config(spec); - let vm_network = vm_executor && network_mode == OverlayNetMode::Auto; - if vm_executor && network_mode == OverlayNetMode::Proxy { - anyhow::bail!( - "overlaynet mode `proxy` is only valid for host/container execution; use `auto` for VM smoltcp networking" - ); - } - if vm_executor && network_mode == OverlayNetMode::Off && self.proxy.is_some() { - anyhow::bail!( - "overlaynet mode `off` makes the VM offline and cannot be combined with Gateway/proxy configuration" - ); - } - if let Some(proxy) = &self.proxy { - let mut proxy = proxy.clone(); - // NetworkDriverConfig is the one Attempt policy source. ProxyConfig - // retains its field for standalone Gateway use only. - if vm_network { - proxy.network = self - .vm_network_options(network, supervisor_limits, attempt_id) - .network; - } else { - proxy.network = network; - proxy.network.limits.extend_from_slice(supervisor_limits); - } - let storage = self - .storage - .clone() - .unwrap_or_else(|| PathBuf::from(".persisting/capture")); - let capture_storage = self - .gateway_output_dir - .clone() - .unwrap_or_else(|| storage.clone()); - let session = prepare_attempt( - spec, - AttemptPrepareOpts { - config: &proxy, - storage: &storage, - capture_storage: &capture_storage, - sink: self.sink.clone(), - stream_markdown: self.stream_markdown, - overlay_override: self.overlay.clone(), - controller: Arc::clone(&self.controller), - gateway_enabled: self.gateway_enabled, - vm_network, - attempt_id: attempt_id.as_str(), - }, - )?; - return Ok(Some(session)); - } - - if !self.overlay.lower_dirs.is_empty() - || self.overlay.stage_dir.is_some() - || self.overlay.upper_dir.is_some() - || self.overlay.work_dir.is_some() - || self.overlay.merged_dir.is_some() - { - let storage = self - .storage - .clone() - .unwrap_or_else(|| PathBuf::from(".persisting/capture")); - let session = prepare_overlay_attempt( - spec, - OverlayAttemptPrepareOpts { - storage: &storage, - overlay: self.overlay.clone(), - vm_network: vm_network - .then(|| self.vm_network_options(network, supervisor_limits, attempt_id)), - }, - )?; - return Ok(Some(session)); - } - - if let Some(storage) = &self.storage { - let session = prepare_storage_attempt( - spec, - storage, - vm_network.then(|| self.vm_network_options(network, supervisor_limits, attempt_id)), - )?; - return Ok(Some(session)); - } - - if vm_network { - let storage = super::registry::default_run_home().join(spec.run_id.as_str()); - std::fs::create_dir_all(&storage)?; - let session = prepare_storage_attempt( - spec, - &storage, - Some(self.vm_network_options(network, supervisor_limits, attempt_id)), - )?; - return Ok(Some(session)); - } - - let _ = self.enrich_spec(spec); - Ok(None) - } - - /// Build the implant plan and merge it into a process RunSpec (env markers only). - pub fn enrich_spec(&self, spec: &mut RunSpec) -> ImplantPlan { - let plan = self.plan_for(spec); - let persisting_agentctl::RunInvocation::Process(ref mut process) = spec.invocation; - apply_implant(process, &plan); - spec.metadata - .insert("pvisor.runtime.implant".into(), plan.as_metadata_json()); - plan - } - - pub fn plan_for(&self, spec: &RunSpec) -> ImplantPlan { - let mut plan = ImplantPlan { - env: ImplantPlan::marker_env(), - cwd: self.overlay.merged_dir.clone(), - overlay: self.overlay.clone(), - notes: Vec::new(), - }; - - plan.env - .insert("PERSISTING_RUN_ID".into(), spec.run_id.as_str().to_string()); - plan.env - .insert("PERSISTING_AGENT".into(), spec.agent.name.clone()); - - if self.proxy.is_some() { - plan.notes - .push("network: in-process OverlayNet proxy configured".into()); - plan.env.insert( - "PERSISTING_OVERLAYNET_DRIVER".into(), - "explicit-proxy".into(), - ); - plan.env.insert( - "PERSISTING_OVERLAYNET_STRENGTH".into(), - "cooperative".into(), - ); - } - if let Some(path) = &self.gateway_output_dir { - plan.env.insert( - "PERSISTING_CAPTURE_STORAGE".into(), - path.display().to_string(), - ); - plan.notes.push("capture: storage path exported".into()); - } - - match &spec.capabilities.network { - NetworkCapability::Ambient => { - plan.env - .insert("PERSISTING_NETWORK_POLICY".into(), "ambient".into()); - plan.notes.push("network: ambient".into()); - } - NetworkCapability::Deny => { - plan.env - .insert("PERSISTING_NETWORK_POLICY".into(), "deny".into()); - plan.notes.push( - "network: deny requested; only intercepted proxy traffic is controlled".into(), - ); - } - NetworkCapability::AllowList { hosts, rules } => { - plan.env - .insert("PERSISTING_NETWORK_POLICY".into(), "allowlist".into()); - plan.env - .insert("PERSISTING_NETWORK_ALLOWLIST".into(), hosts.join(",")); - if let Ok(serialized) = serde_json::to_string(rules) { - plan.env - .insert("PERSISTING_NETWORK_RULES".into(), serialized); - } - plan.notes.push(format!( - "network: allowlist ({} legacy hosts, {} structured rules)", - hosts.len(), - rules.len() - )); - } - NetworkCapability::Policy { - default_action, - allow, - deny, - limits, - } => { - plan.env.insert( - "PERSISTING_NETWORK_POLICY".into(), - match default_action { - persisting_agentctl::NetworkDefaultAction::Allow => "default-allow", - persisting_agentctl::NetworkDefaultAction::Deny => "default-deny", - } - .into(), - ); - for (key, value) in [ - ("PERSISTING_NETWORK_RULES", allow), - ("PERSISTING_NETWORK_DENY", deny), - ] { - if let Ok(serialized) = serde_json::to_string(value) { - plan.env.insert(key.into(), serialized); - } - } - if let Ok(serialized) = serde_json::to_string(limits) { - plan.env - .insert("PERSISTING_NETWORK_LIMITS".into(), serialized); - } - plan.notes.push(format!( - "network: policy ({} allow, {} deny, {} bandwidth limits)", - allow.len(), - deny.len(), - limits.len() - )); - } - } - - if self.overlay.merged_dir.is_some() { - plan.notes - .push("filesystem: merged overlay root selected as cwd".into()); - } else { - plan.notes - .push("filesystem: host view (no overlay merged_dir)".into()); - } - - plan - } -} - -#[cfg(test)] -mod tests { - use super::*; - use persisting_gateway::config::ProxyConfig; - - fn test_proxy() -> ProxyConfig { - ProxyConfig::from_toml_str( - r#" -listen = "127.0.0.1:19081" -admin_listen = "127.0.0.1:19876" -agent_id = "test" -models = [] -"#, - ) - .unwrap() - } - - #[test] - fn capabilities_report_vm_smoltcp_only_on_supported_hosts() { - let capabilities = RuntimeCapabilities::default(); - assert_eq!(capabilities.vm_network, vm_network_supported()); - assert_eq!( - capabilities.providers.contains(&"overlaynet-vm-smoltcp"), - vm_network_supported() - ); - } - - #[test] - fn explicit_network_config_is_the_attempt_policy_source() { - let mut proxy = test_proxy(); - proxy.network.mode = persisting_overlaynet::NetworkMode::Public; - let supervisor = RuntimeSupervisorBuilder::new() - .gateway(GatewayDriverConfig::new(proxy)) - .network(NetworkDriverConfig::new( - OverlayNetMode::Proxy, - persisting_overlaynet::NetworkConfig { - mode: persisting_overlaynet::NetworkMode::NoNetwork, - ..Default::default() - }, - )) - .build(); - let mut spec = RunSpec::process("configured-policy", "test", "true"); - - supervisor.apply_network_capability(&mut spec); - - assert_eq!(spec.capabilities.network, NetworkCapability::Deny); - } - - #[test] - fn absent_network_config_preserves_the_run_spec_policy() { - let supervisor = RuntimeSupervisorBuilder::new().build(); - let mut spec = RunSpec::process("spec-policy", "test", "true"); - spec.capabilities.network = NetworkCapability::Deny; - - supervisor.apply_network_capability(&mut spec); - - assert_eq!(spec.capabilities.network, NetworkCapability::Deny); - } - - #[test] - fn network_capability_roundtrips_into_driver_config() { - let cases = [ - NetworkCapability::Ambient, - NetworkCapability::Deny, - NetworkCapability::AllowList { - hosts: vec!["api.example.com".into()], - rules: Vec::new(), - }, - NetworkCapability::Policy { - default_action: persisting_agentctl::NetworkDefaultAction::Deny, - allow: vec![persisting_agentctl::NetworkAccessRule { - host: "api.example.com".into(), - ports: vec![443], - transports: vec![persisting_agentctl::NetworkTransport::TcpTunnel], - allow_private_ips: false, - }], - deny: vec![persisting_agentctl::NetworkAccessRule { - host: "metadata.internal".into(), - ports: Vec::new(), - transports: Vec::new(), - allow_private_ips: false, - }], - limits: Vec::new(), - }, - ]; - - for capability in cases { - let config = network_config_from_capability(&capability); - assert_eq!( - persisting_overlaynet::policy::network_capability(&config), - capability - ); - } - } - - #[test] - fn offline_vm_rejects_gateway_configuration() { - let supervisor = RuntimeSupervisorBuilder::new() - .network(NetworkDriverConfig::new( - OverlayNetMode::Off, - Default::default(), - )) - .gateway(GatewayDriverConfig::new(test_proxy())) - .build(); - let mut spec = RunSpec::process("offline-vm", "test", "true"); - let error = - match supervisor.prepare(&mut spec, &[], true, &AttemptId::new("attempt-offline")) { - Ok(_) => panic!("offline VM accepted Gateway configuration"), - Err(error) => error, - }; - assert!( - error - .to_string() - .contains("mode `off` makes the VM offline") - ); - } -} diff --git a/crates/persisting-pvisor/src/sandbox.rs b/crates/persisting-pvisor/src/sandbox.rs deleted file mode 100644 index d092db7a8..000000000 --- a/crates/persisting-pvisor/src/sandbox.rs +++ /dev/null @@ -1,1277 +0,0 @@ -//! Platform sandbox launchers used by the local process executor. -//! -//! The launcher is a hidden self-exec mode of the `pvisor` binary. On Linux it -//! installs namespaces and Landlock before Agent code starts. On macOS it is -//! entered only after `/usr/bin/sandbox-exec` has installed a generated -//! Seatbelt profile and records an attestation before replacing itself with -//! the Agent. - -#[cfg(any(target_os = "linux", target_os = "macos"))] -use serde::{Deserialize, Serialize}; -#[cfg(target_os = "linux")] -use std::os::fd::AsRawFd; -#[cfg(any(target_os = "linux", target_os = "macos"))] -use std::path::PathBuf; - -pub(crate) const INTERNAL_SANDBOX_ARG: &str = "__pvisor-sandbox-exec"; -pub(crate) const SANDBOX_PLAN_ENV: &str = "PERSISTING_INTERNAL_SANDBOX_PLAN"; -/// Reserved launcher exit status: setup failed before the Agent was executed. -#[doc(hidden)] -pub const SANDBOX_SETUP_EXIT_CODE: i32 = 125; -pub(crate) const SANDBOX_SETUP_FAILED_WARNING: &str = "pvisor.sandbox.setup_failed"; - -#[cfg(target_os = "macos")] -pub(crate) const MACOS_SANDBOX_EXEC: &str = "/usr/bin/sandbox-exec"; -#[cfg(target_os = "macos")] -pub(crate) const SEATBELT_ATTESTATION: &[u8] = b"pvisor-seatbelt-ready-v1\n"; -#[cfg(target_os = "linux")] -pub(crate) const ROOTLESS_ATTESTATION: &[u8] = b"pvisor-rootless-ready-v1\n"; - -#[cfg(target_os = "linux")] -const LANDLOCK_ACCESS_FS_EXECUTE: u64 = 1 << 0; -#[cfg(target_os = "linux")] -const LANDLOCK_ACCESS_FS_WRITE_FILE: u64 = 1 << 1; -#[cfg(target_os = "linux")] -const LANDLOCK_ACCESS_FS_READ_FILE: u64 = 1 << 2; -#[cfg(target_os = "linux")] -const LANDLOCK_ACCESS_FS_READ_DIR: u64 = 1 << 3; -#[cfg(target_os = "linux")] -const LANDLOCK_ACCESS_FS_TRUNCATE: u64 = 1 << 14; -#[cfg(target_os = "linux")] -const LANDLOCK_ACCESS_FS_V1: u64 = (1 << 13) - 1; -#[cfg(target_os = "linux")] -const LANDLOCK_ACCESS_FS_V2: u64 = (1 << 14) - 1; -#[cfg(target_os = "linux")] -const LANDLOCK_ACCESS_FS_V3: u64 = (1 << 15) - 1; -#[cfg(target_os = "linux")] -const LANDLOCK_ACCESS_FS_READ: u64 = - LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR; - -#[cfg(any(target_os = "linux", target_os = "macos"))] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub(crate) enum NetworkIsolation { - Ambient, - LoopbackOnly, -} - -#[cfg(any(target_os = "linux", target_os = "macos"))] -impl NetworkIsolation { - pub(crate) const fn is_loopback_only(self) -> bool { - matches!(self, Self::LoopbackOnly) - } -} - -#[cfg(target_os = "linux")] -#[derive(Debug, Clone, Serialize, Deserialize)] -pub(crate) struct SandboxPlan { - pub root: PathBuf, - pub cwd: PathBuf, - pub attestation: PathBuf, - pub read_only: Vec, - pub read_write: Vec, - pub network: NetworkIsolation, - /// Applied after the private PID namespace is initialized so the trusted - /// launcher itself can still create its init/reaper process. - #[serde(default)] - pub process_limit: Option, -} - -#[cfg(target_os = "macos")] -#[derive(Debug, Clone, Serialize, Deserialize)] -pub(crate) struct SeatbeltPlan { - pub attestation: PathBuf, - pub network: NetworkIsolation, -} - -/// Enter the hidden launcher when the first argument is the internal marker. -/// -/// Returns `Ok(false)` for an ordinary pVisor invocation. A successful -/// sandbox invocation never returns because it supervises or replaces itself -/// with the Agent. -#[doc(hidden)] -pub fn run_internal_if_requested() -> anyhow::Result { - if std::env::args_os().nth(1).as_deref() != Some(std::ffi::OsStr::new(INTERNAL_SANDBOX_ARG)) { - return Ok(false); - } - run_internal()?; - Ok(true) -} - -#[cfg(target_os = "linux")] -fn run_internal() -> anyhow::Result<()> { - use anyhow::{Context, bail}; - - let encoded = std::env::var(SANDBOX_PLAN_ENV).context("missing rootless sandbox plan")?; - let plan: SandboxPlan = - serde_json::from_str(&encoded).context("decode rootless sandbox plan")?; - let mut arguments = std::env::args_os().skip(2); - if arguments.next().as_deref() != Some(std::ffi::OsStr::new("--")) { - bail!("invalid internal rootless sandbox invocation"); - } - let program = arguments - .next() - .context("rootless sandbox invocation is missing the Agent executable")?; - let arguments = arguments.collect::>(); - - enter_rootless_namespaces(plan.network) - .context("initialize rootless user and mount namespaces")?; - enter_child_pid_namespace().context("initialize private PID namespace")?; - if let Some(limit) = plan.process_limit { - apply_process_limit(limit).context("apply Agent process limit")?; - } - // Open the parent-owned inode before chroot/Landlock. The descriptor is - // retained only by trusted setup code and closed before Agent execution, - // so no attestation pathname needs to be projected into the sandbox. - let attestation = std::fs::OpenOptions::new() - .write(true) - .open(&plan.attestation) - .with_context(|| { - format!( - "open rootless setup attestation {}", - plan.attestation.display() - ) - })?; - enter_synthetic_root(&plan).context("construct private sandbox root")?; - // The private tmpfs created by `enter_synthetic_root` is writable by the - // Agent, but must also be present in the Landlock allowlist. This uses the - // host-side mount path because rules are installed before chroot. - let mut plan = plan; - plan.read_write.push(PathBuf::from("/tmp")); - std::env::set_current_dir(&plan.cwd) - .with_context(|| format!("enter sandbox workspace {}", plan.cwd.display()))?; - - // Enumerating /proc/self/fd must happen before Landlock intentionally - // removes access to the host procfs tree. - close_unexpected_file_descriptors(Some(attestation.as_raw_fd())) - .context("close inherited file descriptors")?; - let landlock_abi = install_landlock(&plan).context("install Landlock filesystem policy")?; - drop_process_capabilities().context("drop namespace capabilities")?; - // The child process is configuring its environment immediately before - // exec; no concurrent environment mutation occurs in this scope. - unsafe { - std::env::remove_var(SANDBOX_PLAN_ENV); - std::env::set_var("PERSISTING_SANDBOX_FILESYSTEM", "landlock"); - std::env::set_var("PERSISTING_SANDBOX_LANDLOCK_ABI", landlock_abi.to_string()); - std::env::set_var("PERSISTING_SANDBOX_USER_NAMESPACE", "1"); - std::env::set_var( - "PERSISTING_SANDBOX_NETWORK", - if plan.network.is_loopback_only() { - "deny" - } else { - "ambient" - }, - ); - } - - supervise_pid_namespace(program, arguments, attestation) -} - -#[cfg(target_os = "macos")] -fn run_internal() -> anyhow::Result<()> { - use anyhow::{Context, bail}; - use std::io::Write; - use std::os::unix::process::CommandExt; - - let encoded = std::env::var(SANDBOX_PLAN_ENV).context("missing Seatbelt sandbox plan")?; - let plan: SeatbeltPlan = - serde_json::from_str(&encoded).context("decode Seatbelt sandbox plan")?; - let mut arguments = std::env::args_os().skip(2); - if arguments.next().as_deref() != Some(std::ffi::OsStr::new("--")) { - bail!("invalid internal Seatbelt sandbox invocation"); - } - let program = arguments - .next() - .context("Seatbelt sandbox invocation is missing the Agent executable")?; - let arguments = arguments.collect::>(); - - // The parent keeps the already-open inode and checks these bytes after the - // process exits. Unlinking before Agent execution keeps the random path and - // its narrow write grant out of the Agent-visible filesystem namespace. - let mut attestation = std::fs::OpenOptions::new() - .write(true) - .open(&plan.attestation) - .with_context(|| { - format!( - "open Seatbelt setup attestation {}", - plan.attestation.display() - ) - })?; - attestation - .write_all(SEATBELT_ATTESTATION) - .context("write Seatbelt setup attestation")?; - attestation - .sync_data() - .context("sync Seatbelt setup attestation")?; - drop(attestation); - std::fs::remove_file(&plan.attestation).with_context(|| { - format!( - "unlink Seatbelt setup attestation {}", - plan.attestation.display() - ) - })?; - - // The child process is configuring its environment immediately before - // exec; no concurrent environment mutation occurs in this scope. - unsafe { - std::env::remove_var(SANDBOX_PLAN_ENV); - std::env::set_var("PERSISTING_SANDBOX_FILESYSTEM", "seatbelt-write"); - std::env::set_var( - "PERSISTING_SANDBOX_NETWORK", - if plan.network.is_loopback_only() { - "deny" - } else { - "ambient" - }, - ); - } - - Err(std::process::Command::new(program) - .args(arguments) - .exec() - .into()) -} - -#[cfg(target_os = "linux")] -pub(crate) fn landlock_runtime_available() -> bool { - const CREATE_RULESET_VERSION: libc::c_uint = 1; - let abi = unsafe { - libc::syscall( - libc::SYS_landlock_create_ruleset, - std::ptr::null::(), - 0, - CREATE_RULESET_VERSION, - ) - }; - abi >= 1 -} - -#[cfg(target_os = "linux")] -fn install_landlock(plan: &SandboxPlan) -> std::io::Result { - use std::io::{Error, ErrorKind}; - - // Calling the small stable kernel ABI directly keeps this launcher - // dependency-free. Each kernel must only receive the access bits introduced - // by the ABI it implements: v2 adds REFER and v3 adds TRUNCATE. - const CREATE_RULESET_VERSION: libc::c_uint = 1; - const RULE_PATH_BENEATH: libc::c_int = 1; - #[repr(C)] - struct RulesetAttr { - handled_access_fs: u64, - } - - let abi = unsafe { - libc::syscall( - libc::SYS_landlock_create_ruleset, - std::ptr::null::(), - 0, - CREATE_RULESET_VERSION, - ) - }; - if abi < 0 { - return Err(Error::last_os_error()); - } - if abi < 1 { - return Err(Error::new( - ErrorKind::Unsupported, - format!("Landlock ABI v1 or newer is required; kernel provides v{abi}"), - )); - } - - let handled_access_fs = landlock_access_fs_for_abi(abi as u32); - - let attr = RulesetAttr { handled_access_fs }; - let ruleset_fd = unsafe { - libc::syscall( - libc::SYS_landlock_create_ruleset, - &attr, - std::mem::size_of::(), - 0, - ) - } as libc::c_int; - if ruleset_fd < 0 { - return Err(Error::last_os_error()); - } - let ruleset = OwnedFd(ruleset_fd); - - for path in &plan.read_only { - add_landlock_path_rule(ruleset.0, path, LANDLOCK_ACCESS_FS_READ, RULE_PATH_BENEATH) - .map_err(|error| { - Error::new( - error.kind(), - format!("add read-only rule for {}: {error}", path.display()), - ) - })?; - } - for path in &plan.read_write { - add_landlock_path_rule(ruleset.0, path, handled_access_fs, RULE_PATH_BENEATH).map_err( - |error| { - Error::new( - error.kind(), - format!("add read-write rule for {}: {error}", path.display()), - ) - }, - )?; - } - - if unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) } != 0 { - return Err(Error::last_os_error()); - } - if unsafe { libc::syscall(libc::SYS_landlock_restrict_self, ruleset.0, 0) } != 0 { - return Err(Error::last_os_error()); - } - Ok(abi as u32) -} - -#[cfg(target_os = "linux")] -const fn landlock_access_fs_for_abi(abi: u32) -> u64 { - match abi { - 1 => LANDLOCK_ACCESS_FS_V1, - 2 => LANDLOCK_ACCESS_FS_V2, - _ => LANDLOCK_ACCESS_FS_V3, - } -} - -/// Confine the libkrun VMM process while leaving the pVisor FUSE server in the -/// trusted parent. The VMM gets a private network and mount namespace, may -/// access only its virtio-fs root plus KVM/runtime files, and retains no -/// namespace capabilities after setup. -#[cfg(target_os = "linux")] -pub(crate) fn restrict_krun_runner( - overlay_read_only: Vec, - overlay_read_write: Vec, - library_dir: Option, -) -> anyhow::Result { - use anyhow::Context; - - enter_rootless_namespaces(NetworkIsolation::LoopbackOnly) - .context("initialize libkrun user, mount, and network namespaces")?; - let mut read_only = [ - "/usr/lib", - "/usr/lib64", - "/lib", - "/lib64", - "/proc/self", - "/dev/urandom", - ] - .into_iter() - .map(PathBuf::from) - .filter(|path| path.exists()) - .collect::>(); - if let Some(directory) = library_dir { - read_only.push(directory); - } - read_only.extend(overlay_read_only); - let mut read_write = overlay_read_write; - if PathBuf::from("/dev/kvm").exists() { - read_write.push(PathBuf::from("/dev/kvm")); - } - let plan = SandboxPlan { - root: PathBuf::from("/"), - cwd: PathBuf::from("/"), - attestation: PathBuf::from("/dev/null"), - read_only, - read_write, - network: NetworkIsolation::LoopbackOnly, - process_limit: None, - }; - let abi = install_landlock(&plan).context("install libkrun Landlock policy")?; - drop_process_capabilities().context("drop libkrun namespace capabilities")?; - Ok(abi) -} - -#[cfg(target_os = "linux")] -fn add_landlock_path_rule( - ruleset_fd: libc::c_int, - path: &std::path::Path, - allowed_access: u64, - rule_type: libc::c_int, -) -> std::io::Result<()> { - use std::io::{Error, ErrorKind}; - use std::os::unix::ffi::OsStrExt; - use std::os::unix::fs::FileTypeExt; - - #[repr(C, packed)] - struct PathBeneathAttr { - allowed_access: u64, - parent_fd: libc::c_int, - } - - // Landlock rejects directory-only access bits on a non-directory anchor. - // Filter the requested access against the anchor's inode type before - // adding the rule. Pathname Unix sockets are not governed by Landlock's - // filesystem rights, so there is no useful rule to add for them. - let file_type = std::fs::metadata(path)?.file_type(); - let allowed_access = if file_type.is_dir() { - allowed_access - } else if file_type.is_file() { - allowed_access - & (LANDLOCK_ACCESS_FS_EXECUTE - | LANDLOCK_ACCESS_FS_WRITE_FILE - | LANDLOCK_ACCESS_FS_READ_FILE - | LANDLOCK_ACCESS_FS_TRUNCATE) - } else if file_type.is_socket() { - return Ok(()); - } else { - allowed_access & (LANDLOCK_ACCESS_FS_WRITE_FILE | LANDLOCK_ACCESS_FS_READ_FILE) - }; - if allowed_access == 0 { - return Ok(()); - } - - let encoded = std::ffi::CString::new(path.as_os_str().as_bytes()).map_err(|_| { - Error::new( - ErrorKind::InvalidInput, - format!("sandbox path contains a NUL byte: {}", path.display()), - ) - })?; - let path_fd = unsafe { libc::open(encoded.as_ptr(), libc::O_PATH | libc::O_CLOEXEC) }; - if path_fd < 0 { - return Err(Error::last_os_error()); - } - let path_fd = OwnedFd(path_fd); - let attr = PathBeneathAttr { - allowed_access, - parent_fd: path_fd.0, - }; - if unsafe { libc::syscall(libc::SYS_landlock_add_rule, ruleset_fd, rule_type, &attr, 0) } != 0 { - return Err(Error::last_os_error()); - } - Ok(()) -} - -#[cfg(target_os = "linux")] -struct OwnedFd(libc::c_int); - -#[cfg(target_os = "linux")] -impl Drop for OwnedFd { - fn drop(&mut self) { - unsafe { - libc::close(self.0); - } - } -} - -#[cfg(all(test, target_os = "linux"))] -mod linux_tests { - use super::*; - - #[test] - fn landlock_access_mask_matches_negotiated_abi() { - assert_eq!(landlock_access_fs_for_abi(1), LANDLOCK_ACCESS_FS_V1); - assert_eq!(landlock_access_fs_for_abi(2), LANDLOCK_ACCESS_FS_V2); - assert_eq!(landlock_access_fs_for_abi(3), LANDLOCK_ACCESS_FS_V3); - assert_eq!(landlock_access_fs_for_abi(99), LANDLOCK_ACCESS_FS_V3); - assert_eq!(LANDLOCK_ACCESS_FS_V2, LANDLOCK_ACCESS_FS_V1 | (1 << 13)); - assert_eq!( - LANDLOCK_ACCESS_FS_V3, - LANDLOCK_ACCESS_FS_V2 | LANDLOCK_ACCESS_FS_TRUNCATE - ); - } - - #[test] - fn namespace_errors_preserve_stage_and_os_error() { - let error = with_io_context( - "unshare mount namespace", - std::io::Error::from_raw_os_error(libc::EPERM), - ); - assert_eq!(error.kind(), std::io::ErrorKind::PermissionDenied); - assert_eq!( - error.to_string(), - "unshare mount namespace: Operation not permitted (os error 1)" - ); - } -} - -#[cfg(not(any(target_os = "linux", target_os = "macos")))] -fn run_internal() -> anyhow::Result<()> { - anyhow::bail!("the local process sandbox is not available on this platform") -} - -/// Generate a compatibility-oriented Seatbelt profile. -/// -/// Reads remain ambient so ordinary developer toolchains keep working. Every -/// pathname write outside `writable_paths` is denied by Seatbelt. A -/// network-isolated Run starts from `deny default` and admits loopback IP, -/// exact Run-scoped Unix sockets, and sockets rooted in Run-owned directories. -#[cfg(target_os = "macos")] -pub(crate) fn seatbelt_profile( - writable_paths: &[PathBuf], - allowed_unix_sockets: &[PathBuf], - local_socket_roots: &[PathBuf], - network: NetworkIsolation, -) -> std::io::Result<(String, Vec<(String, PathBuf)>)> { - use std::io::{Error, ErrorKind}; - - let writable_paths = canonical_seatbelt_paths(writable_paths, "writable")?; - if writable_paths.is_empty() { - return Err(Error::new( - ErrorKind::InvalidInput, - "Seatbelt requires at least one writable path", - )); - } - if writable_paths - .iter() - .any(|path| path == std::path::Path::new("/")) - { - return Err(Error::new( - ErrorKind::InvalidInput, - "the host root cannot be granted as a Seatbelt writable path", - )); - } - let mut parameters = Vec::with_capacity(writable_paths.len()); - for (index, path) in writable_paths.iter().enumerate() { - let key = format!("PVISOR_WRITABLE_{index}"); - parameters.push((key, path.clone())); - } - - if network.is_loopback_only() { - let allowed_unix_sockets = canonical_seatbelt_paths(allowed_unix_sockets, "Unix socket")?; - let local_socket_roots = canonical_seatbelt_paths(local_socket_roots, "local socket root")?; - parameters.reserve(allowed_unix_sockets.len() + local_socket_roots.len()); - for (index, path) in allowed_unix_sockets.iter().enumerate() { - parameters.push((format!("PVISOR_UNIX_SOCKET_{index}"), path.clone())); - } - for (index, path) in local_socket_roots.iter().enumerate() { - parameters.push((format!("PVISOR_SOCKET_ROOT_{index}"), path.clone())); - } - - // Deny by default for a network-isolated Run. The allowlist below is - // intentionally small and mirrors the system services required by - // shells, language runtimes, PTYs, and read-only preferences. Socket - // operations are admitted only so the filtered denies below can retain - // Run-local Unix IPC while rejecting non-loopback IP and ambient host - // Unix sockets. - let mut profile = String::from( - "(version 1)\n\ - (deny default)\n\ - (allow process-exec)\n\ - (allow process-fork)\n\ - (allow signal (target same-sandbox))\n\ - (allow process-info* (target same-sandbox))\n\ - (allow file-read* file-test-existence file-map-executable)\n\ - (allow sysctl-read)\n\ - (allow system-mac-syscall (mac-policy-name \"vnguard\"))\n\ - (allow system-mac-syscall\n\ - (require-all (mac-policy-name \"Sandbox\") (mac-syscall-number 67)))\n\ - (allow system-fsctl)\n\ - (allow iokit-open (iokit-registry-entry-class \"RootDomainUserClient\"))\n\ - (allow ipc-posix-sem)\n\ - (allow ipc-posix-shm-read*)\n\ - (allow pseudo-tty)\n\ - (allow user-preference-read)\n\ - (allow mach-lookup\n\ - (global-name \"com.apple.system.opendirectoryd.libinfo\")\n\ - (global-name \"com.apple.system.opendirectoryd.membership\")\n\ - (global-name \"com.apple.cfprefsd.daemon\")\n\ - (global-name \"com.apple.cfprefsd.agent\")\n\ - (local-name \"com.apple.cfprefsd.agent\")\n\ - (global-name \"com.apple.PowerManagement.control\"))\n\ - (allow file-ioctl (regex #\"^/dev/ttys[0-9]+$\"))\n\ - (allow system-socket (socket-domain AF_UNIX))\n\ - (allow network*)\n\ - (deny network-bind (local ip))\n\ - (deny network-inbound (local ip))\n\ - (deny network-outbound\n\ - (require-all\n\ - (remote ip)\n\ - (require-not (remote ip \"localhost:*\"))))\n\ - (allow network-outbound (remote ip \"localhost:*\"))\n", - ); - profile.push_str("(allow file-write*\n"); - for index in 0..writable_paths.len() { - profile.push_str(&format!( - " (literal (param \"PVISOR_WRITABLE_{index}\"))\n\ - (subpath (param \"PVISOR_WRITABLE_{index}\"))\n" - )); - } - profile.push_str(")\n"); - profile.push_str("(deny network-outbound\n (require-all\n (remote unix-socket)\n"); - for index in 0..allowed_unix_sockets.len() { - profile.push_str(&format!( - " (require-not (remote unix-socket\n\ - (literal (param \"PVISOR_UNIX_SOCKET_{index}\"))))\n" - )); - } - for index in 0..local_socket_roots.len() { - profile.push_str(&format!( - " (require-not (remote unix-socket\n\ - (subpath (param \"PVISOR_SOCKET_ROOT_{index}\"))))\n" - )); - } - profile.push_str(" )\n)\n"); - return Ok((profile, parameters)); - } - - // Starting from `allow default` preserves compatibility with local macOS - // toolchains. The filtered deny is fail-closed for writes: it matches only - // when a target is neither an exact writable root nor beneath one. - let mut profile = String::from( - "(version 1)\n\ - (allow default)\n\ - (deny file-write*\n\ - (require-all\n", - ); - for index in 0..writable_paths.len() { - profile.push_str(&format!( - " (require-not (literal (param \"PVISOR_WRITABLE_{index}\")))\n\ - (require-not (subpath (param \"PVISOR_WRITABLE_{index}\")))\n" - )); - } - profile.push_str(" )\n)\n"); - Ok((profile, parameters)) -} - -#[cfg(target_os = "macos")] -fn canonical_seatbelt_paths(paths: &[PathBuf], kind: &str) -> std::io::Result> { - use std::io::{Error, ErrorKind}; - - let mut canonical = paths - .iter() - .map(|path| { - path.canonicalize().map_err(|error| { - Error::new( - error.kind(), - format!( - "canonicalize Seatbelt {kind} path {}: {error}", - path.display() - ), - ) - }) - }) - .collect::>>()?; - canonical.sort_unstable(); - canonical.dedup(); - if canonical.iter().any(|path| path.to_str().is_none()) { - return Err(Error::new( - ErrorKind::InvalidInput, - format!("Seatbelt {kind} paths must be valid UTF-8"), - )); - } - Ok(canonical) -} - -#[cfg(target_os = "linux")] -fn enter_rootless_namespaces(network: NetworkIsolation) -> std::io::Result<()> { - let uid = unsafe { libc::getuid() }; - let gid = unsafe { libc::getgid() }; - if unsafe { libc::unshare(libc::CLONE_NEWUSER) } != 0 { - return Err(namespace_stage_error("unshare user namespace")); - } - - // A one-ID identity mapping is sufficient for a local Agent executable and - // avoids /etc/subuid, newuidmap, and a privileged setup helper. - match std::fs::write("/proc/self/setgroups", b"deny\n") { - Ok(()) => {} - Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} - Err(error) => { - return Err(with_io_context( - "disable setgroups in user namespace", - error, - )); - } - } - std::fs::write("/proc/self/uid_map", format!("{uid} {uid} 1\n")) - .map_err(|error| with_io_context("write user namespace UID map", error))?; - std::fs::write("/proc/self/gid_map", format!("{gid} {gid} 1\n")) - .map_err(|error| with_io_context("write user namespace GID map", error))?; - - if unsafe { libc::unshare(libc::CLONE_NEWNS) } != 0 { - return Err(namespace_stage_error("unshare mount namespace")); - } - if network.is_loopback_only() && unsafe { libc::unshare(libc::CLONE_NEWNET) } != 0 { - return Err(namespace_stage_error("unshare network namespace")); - } - if network.is_loopback_only() { - bring_loopback_up() - .map_err(|error| with_io_context("enable network namespace loopback", error))?; - } - - // Never propagate mounts performed by the child back into the host mount - // namespace. Landlock later prevents the Agent from changing topology. - if unsafe { - libc::mount( - std::ptr::null(), - c"/".as_ptr(), - std::ptr::null(), - libc::MS_REC | libc::MS_PRIVATE, - std::ptr::null(), - ) - } != 0 - { - return Err(namespace_stage_error( - "set mount namespace root propagation to private", - )); - } - Ok(()) -} - -#[cfg(target_os = "linux")] -fn bring_loopback_up() -> std::io::Result<()> { - // A newly-created network namespace starts with only `lo`, administratively - // down. Enable that interface before dropping capabilities; no route or - // non-loopback device is created, so children cannot reach the host network. - #[repr(C)] - struct Ifreq { - name: [libc::c_char; libc::IFNAMSIZ], - flags: libc::c_short, - _pad: [u8; 22], - } - let fd = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM | libc::SOCK_CLOEXEC, 0) }; - if fd < 0 { - return Err(std::io::Error::last_os_error()); - } - let _guard = OwnedFd(fd); - let mut ifreq = Ifreq { - name: [0; libc::IFNAMSIZ], - flags: 0, - _pad: [0; 22], - }; - ifreq.name[0] = b'l' as libc::c_char; - ifreq.name[1] = b'o' as libc::c_char; - if unsafe { libc::ioctl(fd, libc::SIOCGIFFLAGS, &mut ifreq) } != 0 { - return Err(std::io::Error::last_os_error()); - } - ifreq.flags |= libc::IFF_UP as libc::c_short | libc::IFF_RUNNING as libc::c_short; - if unsafe { libc::ioctl(fd, libc::SIOCSIFFLAGS, &ifreq) } != 0 { - return Err(std::io::Error::last_os_error()); - } - Ok(()) -} - -#[cfg(target_os = "linux")] -fn namespace_stage_error(stage: &str) -> std::io::Error { - with_io_context(stage, std::io::Error::last_os_error()) -} - -#[cfg(target_os = "linux")] -fn with_io_context(stage: &str, error: std::io::Error) -> std::io::Error { - std::io::Error::new(error.kind(), format!("{stage}: {error}")) -} - -#[cfg(target_os = "linux")] -fn enter_child_pid_namespace() -> std::io::Result<()> { - if unsafe { libc::unshare(libc::CLONE_NEWPID) } != 0 { - return Err(std::io::Error::last_os_error()); - } - Ok(()) -} - -#[cfg(target_os = "linux")] -fn apply_process_limit(processes: u64) -> std::io::Result<()> { - let mut current = libc::rlimit { - rlim_cur: 0, - rlim_max: 0, - }; - if unsafe { libc::getrlimit(libc::RLIMIT_NPROC, &mut current) } != 0 { - return Err(std::io::Error::last_os_error()); - } - let requested = processes as libc::rlim_t; - let effective = requested.min(current.rlim_max); - let limit = libc::rlimit { - rlim_cur: effective, - rlim_max: effective, - }; - if unsafe { libc::setrlimit(libc::RLIMIT_NPROC, &limit) } != 0 { - return Err(std::io::Error::last_os_error()); - } - Ok(()) -} - -#[cfg(target_os = "linux")] -fn write_rootless_attestation(attestation: &mut std::fs::File) -> std::io::Result<()> { - use std::io::Write; - - attestation.write_all(ROOTLESS_ATTESTATION)?; - attestation.sync_data() -} - -#[cfg(target_os = "linux")] -fn enter_synthetic_root(plan: &SandboxPlan) -> std::io::Result<()> { - use std::io::{Error, ErrorKind}; - - if !plan.root.is_absolute() || plan.root == std::path::Path::new("/") { - return Err(Error::new( - ErrorKind::InvalidInput, - format!( - "sandbox root must be a non-root absolute path: {}", - plan.root.display() - ), - )); - } - if !plan.root.is_dir() { - return Err(Error::new( - ErrorKind::NotFound, - format!("sandbox root does not exist: {}", plan.root.display()), - )); - } - - let root = path_cstring(&plan.root)?; - if unsafe { - libc::mount( - c"tmpfs".as_ptr(), - root.as_ptr(), - c"tmpfs".as_ptr(), - libc::MS_NOSUID | libc::MS_NODEV, - c"mode=0755,size=16m".as_ptr().cast(), - ) - } != 0 - { - return Err(Error::last_os_error()); - } - - // Give the Agent a private temporary directory. Binding the host /tmp - // would let a staged Run mutate unrelated host state, while omitting it - // breaks ordinary tools that need a scratch directory. This tmpfs is - // intentionally ephemeral and is not part of the durable workspace - // OverlayFS stage. - let tmp = plan.root.join("tmp"); - std::fs::create_dir(&tmp)?; - let tmp = path_cstring(&tmp)?; - if unsafe { - libc::mount( - c"tmpfs".as_ptr(), - tmp.as_ptr(), - c"tmpfs".as_ptr(), - libc::MS_NOSUID | libc::MS_NODEV, - c"mode=1777,size=64m".as_ptr().cast(), - ) - } != 0 - { - return Err(Error::last_os_error()); - } - - // procfs is needed by the trusted launcher for FD cleanup. Landlock does - // not admit it to the Agent, including magic-link escape paths. - bind_path_into_root(&plan.root, std::path::Path::new("/proc"))?; - - let mut paths = plan - .read_only - .iter() - .chain(&plan.read_write) - .collect::>(); - paths.sort_unstable_by(|left, right| { - left.components() - .count() - .cmp(&right.components().count()) - .then_with(|| left.cmp(right)) - }); - paths.dedup(); - for path in paths { - if path == std::path::Path::new("/") { - return Err(Error::new( - ErrorKind::InvalidInput, - "the host root cannot be granted to a rootless sandbox", - )); - } - bind_path_into_root(&plan.root, path)?; - } - - // chroot is safe here because the process has a private mount namespace, - // no Agent code has run, every non-stdio FD is closed immediately below, - // and all namespace capabilities are dropped before exec. - if unsafe { libc::chroot(root.as_ptr()) } != 0 { - return Err(Error::last_os_error()); - } - if unsafe { libc::chdir(c"/".as_ptr()) } != 0 { - return Err(Error::last_os_error()); - } - Ok(()) -} - -#[cfg(target_os = "linux")] -fn bind_path_into_root(root: &std::path::Path, source: &std::path::Path) -> std::io::Result<()> { - use std::io::{Error, ErrorKind}; - - let relative = source.strip_prefix("/").map_err(|_| { - Error::new( - ErrorKind::InvalidInput, - format!("sandbox path must be absolute: {}", source.display()), - ) - })?; - let target = root.join(relative); - if std::fs::symlink_metadata(&target).is_ok() { - // A parent hierarchy (for example /usr or /proc) already projects the - // same absolute source path into the synthetic root. - return Ok(()); - } - let metadata = std::fs::metadata(source)?; - if metadata.is_dir() { - std::fs::create_dir_all(&target)?; - } else { - let parent = target.parent().ok_or_else(|| { - Error::new( - ErrorKind::InvalidInput, - format!("sandbox target has no parent: {}", target.display()), - ) - })?; - std::fs::create_dir_all(parent)?; - std::fs::OpenOptions::new() - .write(true) - .create_new(true) - .open(&target)?; - } - - let source = path_cstring(source)?; - let target = path_cstring(&target)?; - let flags = libc::MS_BIND | if metadata.is_dir() { libc::MS_REC } else { 0 }; - if unsafe { - libc::mount( - source.as_ptr(), - target.as_ptr(), - std::ptr::null(), - flags, - std::ptr::null(), - ) - } != 0 - { - return Err(Error::last_os_error()); - } - Ok(()) -} - -#[cfg(target_os = "linux")] -fn path_cstring(path: &std::path::Path) -> std::io::Result { - use std::io::{Error, ErrorKind}; - use std::os::unix::ffi::OsStrExt; - - std::ffi::CString::new(path.as_os_str().as_bytes()).map_err(|_| { - Error::new( - ErrorKind::InvalidInput, - format!("sandbox path contains a NUL byte: {}", path.display()), - ) - }) -} - -#[cfg(target_os = "linux")] -fn drop_process_capabilities() -> std::io::Result<()> { - use std::io::Error; - - const LINUX_CAPABILITY_VERSION_3: u32 = 0x2008_0522; - #[repr(C)] - struct CapabilityHeader { - version: u32, - pid: i32, - } - #[repr(C)] - #[derive(Clone, Copy)] - struct CapabilityData { - effective: u32, - permitted: u32, - inheritable: u32, - } - - let mut header = CapabilityHeader { - version: LINUX_CAPABILITY_VERSION_3, - pid: 0, - }; - let mut data = [CapabilityData { - effective: 0, - permitted: 0, - inheritable: 0, - }; 2]; - if unsafe { libc::syscall(libc::SYS_capset, &mut header, data.as_mut_ptr()) } != 0 { - return Err(Error::last_os_error()); - } - if unsafe { - libc::prctl( - libc::PR_CAP_AMBIENT, - libc::PR_CAP_AMBIENT_CLEAR_ALL, - 0, - 0, - 0, - ) - } != 0 - { - return Err(Error::last_os_error()); - } - Ok(()) -} - -#[cfg(target_os = "linux")] -extern "C" fn forward_namespace_signal(signal: libc::c_int) { - // PID 1 is excluded from kill(-1, ...), so this forwards cancellation to - // every Agent descendant even after setsid(2) or a double fork. - unsafe { - libc::kill(-1, signal); - } -} - -#[cfg(target_os = "linux")] -static NAMESPACE_INIT_PID: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); - -#[cfg(target_os = "linux")] -extern "C" fn forward_launcher_signal(signal: libc::c_int) { - let pid = NAMESPACE_INIT_PID.load(std::sync::atomic::Ordering::Relaxed); - if pid > 0 { - unsafe { - libc::kill(pid, signal); - } - } -} - -#[cfg(target_os = "linux")] -fn install_namespace_signal_handlers(handler: libc::sighandler_t) -> std::io::Result<()> { - for signal in [libc::SIGTERM, libc::SIGINT, libc::SIGHUP, libc::SIGQUIT] { - let mut action = unsafe { std::mem::zeroed::() }; - action.sa_sigaction = handler; - action.sa_flags = libc::SA_RESTART; - unsafe { - libc::sigemptyset(&mut action.sa_mask); - } - if unsafe { libc::sigaction(signal, &action, std::ptr::null_mut()) } != 0 { - return Err(std::io::Error::last_os_error()); - } - } - Ok(()) -} - -#[cfg(target_os = "linux")] -fn exit_with_wait_status(status: libc::c_int) -> ! { - if libc::WIFEXITED(status) { - unsafe { libc::_exit(libc::WEXITSTATUS(status)) }; - } - if libc::WIFSIGNALED(status) { - let signal = libc::WTERMSIG(status); - unsafe { - libc::signal(signal, libc::SIG_DFL); - libc::kill(libc::getpid(), signal); - libc::_exit(128 + signal); - } - } - unsafe { libc::_exit(SANDBOX_SETUP_EXIT_CODE) }; -} - -/// Run a tiny trusted PID-namespace supervisor. The first child after -/// CLONE_NEWPID becomes namespace PID 1; when it exits, the kernel kills all -/// remaining processes in that namespace, including daemonized descendants. -#[cfg(target_os = "linux")] -fn supervise_pid_namespace( - program: std::ffi::OsString, - arguments: Vec, - mut attestation: std::fs::File, -) -> anyhow::Result<()> { - use anyhow::Context; - use std::os::unix::process::CommandExt; - - let mut ready_pipe = [0; 2]; - let mut release_pipe = [0; 2]; - if unsafe { libc::pipe2(ready_pipe.as_mut_ptr(), libc::O_CLOEXEC) } != 0 { - return Err(std::io::Error::last_os_error()).context("create PID supervisor ready pipe"); - } - if unsafe { libc::pipe2(release_pipe.as_mut_ptr(), libc::O_CLOEXEC) } != 0 { - let error = std::io::Error::last_os_error(); - unsafe { - libc::close(ready_pipe[0]); - libc::close(ready_pipe[1]); - } - return Err(error).context("create PID supervisor release pipe"); - } - - let namespace_init = unsafe { libc::fork() }; - if namespace_init < 0 { - unsafe { - libc::close(ready_pipe[0]); - libc::close(ready_pipe[1]); - libc::close(release_pipe[0]); - libc::close(release_pipe[1]); - } - return Err(std::io::Error::last_os_error()).context("fork PID namespace init"); - } - if namespace_init > 0 { - unsafe { - libc::close(ready_pipe[1]); - libc::close(release_pipe[0]); - } - NAMESPACE_INIT_PID.store(namespace_init, std::sync::atomic::Ordering::Relaxed); - if let Err(error) = install_namespace_signal_handlers( - forward_launcher_signal as *const () as libc::sighandler_t, - ) { - unsafe { - libc::kill(namespace_init, libc::SIGKILL); - libc::waitpid(namespace_init, std::ptr::null_mut(), 0); - } - return Err(error).context("install PID namespace launcher signal handlers"); - } - let mut ready = 0_u8; - let ready_count = unsafe { libc::read(ready_pipe[0], (&mut ready as *mut u8).cast(), 1) }; - unsafe { - libc::close(ready_pipe[0]); - } - if ready_count != 1 || ready != 1 { - unsafe { - libc::kill(namespace_init, libc::SIGKILL); - libc::waitpid(namespace_init, std::ptr::null_mut(), 0); - libc::close(release_pipe[1]); - } - return Err(std::io::Error::other( - "PID namespace Agent setup did not attest", - )) - .context("initialize PID namespace supervisor"); - } - if let Err(error) = write_rootless_attestation(&mut attestation) { - unsafe { - libc::kill(namespace_init, libc::SIGKILL); - libc::waitpid(namespace_init, std::ptr::null_mut(), 0); - libc::close(release_pipe[1]); - } - return Err(error).context("record installed rootless sandbox controls"); - } - let release = 1_u8; - let released = unsafe { libc::write(release_pipe[1], (&release as *const u8).cast(), 1) }; - unsafe { - libc::close(release_pipe[1]); - } - if released != 1 { - let error = std::io::Error::last_os_error(); - let _ = attestation.set_len(0); - let _ = attestation.sync_data(); - unsafe { - libc::kill(namespace_init, libc::SIGKILL); - libc::waitpid(namespace_init, std::ptr::null_mut(), 0); - } - return Err(error).context("release attested Agent executable"); - } - drop(attestation); - let mut status = 0; - loop { - let waited = unsafe { libc::waitpid(namespace_init, &mut status, 0) }; - if waited == namespace_init { - exit_with_wait_status(status); - } - let error = std::io::Error::last_os_error(); - if error.kind() != std::io::ErrorKind::Interrupted { - return Err(error).context("wait for PID namespace init"); - } - } - } - - unsafe { - libc::close(ready_pipe[0]); - libc::close(release_pipe[1]); - } - drop(attestation); - - // If the outer launcher is terminated before it can forward a signal, - // killing PID 1 still gives the kernel an authoritative cleanup point. - if unsafe { libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL) } != 0 { - unsafe { libc::_exit(SANDBOX_SETUP_EXIT_CODE) }; - } - if install_namespace_signal_handlers( - forward_namespace_signal as *const () as libc::sighandler_t, - ) - .is_err() - { - unsafe { libc::_exit(SANDBOX_SETUP_EXIT_CODE) }; - } - - let agent = unsafe { libc::fork() }; - if agent < 0 { - unsafe { libc::_exit(SANDBOX_SETUP_EXIT_CODE) }; - } - if agent == 0 { - let supervisor = unsafe { libc::getppid() }; - if unsafe { libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL) } != 0 - || unsafe { libc::getppid() } != supervisor - || install_namespace_signal_handlers(libc::SIG_DFL).is_err() - { - unsafe { libc::_exit(SANDBOX_SETUP_EXIT_CODE) }; - } - let ready = 1_u8; - let ready_count = unsafe { libc::write(ready_pipe[1], (&ready as *const u8).cast(), 1) }; - unsafe { - libc::close(ready_pipe[1]); - } - if ready_count != 1 { - unsafe { libc::_exit(SANDBOX_SETUP_EXIT_CODE) }; - } - let mut release = 0_u8; - let release_count = - unsafe { libc::read(release_pipe[0], (&mut release as *mut u8).cast(), 1) }; - unsafe { - libc::close(release_pipe[0]); - } - if release_count != 1 || release != 1 { - unsafe { libc::_exit(SANDBOX_SETUP_EXIT_CODE) }; - } - let error = std::process::Command::new(program).args(arguments).exec(); - eprintln!("pvisor: execute sandboxed Agent: {error}"); - unsafe { libc::_exit(SANDBOX_SETUP_EXIT_CODE) }; - } - - unsafe { - libc::close(ready_pipe[1]); - libc::close(release_pipe[0]); - } - - // Reap all descendants while the Agent is alive. Orphans are reparented - // to namespace PID 1, so they cannot accumulate as unreaped zombies. - loop { - let mut status = 0; - let waited = unsafe { libc::waitpid(-1, &mut status, 0) }; - if waited == agent { - exit_with_wait_status(status); - } - if waited < 0 { - let error = std::io::Error::last_os_error(); - if error.kind() != std::io::ErrorKind::Interrupted { - unsafe { libc::_exit(SANDBOX_SETUP_EXIT_CODE) }; - } - } - } -} - -#[cfg(target_os = "linux")] -fn close_unexpected_file_descriptors(retain: Option) -> std::io::Result<()> { - let mut descriptors = Vec::new(); - for entry in std::fs::read_dir("/proc/self/fd")? { - let entry = entry?; - let Some(name) = entry.file_name().to_str().map(str::to_owned) else { - continue; - }; - let Ok(fd) = name.parse::() else { - continue; - }; - if fd > libc::STDERR_FILENO && Some(fd) != retain { - descriptors.push(fd); - } - } - descriptors.sort_unstable(); - descriptors.dedup(); - for fd in descriptors { - unsafe { - libc::close(fd); - } - } - Ok(()) -} - -#[cfg(all(test, target_os = "macos"))] -mod tests { - use super::*; - - #[test] - fn seatbelt_profile_uses_parameters_and_rejects_a_writable_host_root() { - let temporary = tempfile::Builder::new() - .prefix("pvisor-\")-(deny-default-") - .tempdir() - .unwrap(); - let canonical = temporary.path().canonicalize().unwrap(); - let (profile, parameters) = seatbelt_profile( - &[temporary.path().to_owned()], - &[], - &[], - NetworkIsolation::LoopbackOnly, - ) - .unwrap(); - - assert!(!profile.contains(canonical.to_str().unwrap())); - assert_eq!(parameters, [("PVISOR_WRITABLE_0".into(), canonical)]); - assert!(profile.contains("(deny default)")); - assert!(profile.contains("(remote ip \"localhost:*\")")); - assert!(profile.contains("(allow network-outbound (remote ip \"localhost:*\"))")); - - let error = seatbelt_profile(&[PathBuf::from("/")], &[], &[], NetworkIsolation::Ambient) - .unwrap_err(); - assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput); - } -} diff --git a/crates/persisting-pvisor/src/supervisor.rs b/crates/persisting-pvisor/src/supervisor.rs deleted file mode 100644 index 67d682b33..000000000 --- a/crates/persisting-pvisor/src/supervisor.rs +++ /dev/null @@ -1,297 +0,0 @@ -//! Optional pPilot Supervisor client. The Run data plane never depends on it. - -use crate::util::unix_now_ms; -#[cfg(test)] -use persisting_agentctl::SupervisorNetworkQuotaGrant; -use persisting_agentctl::{AttemptId, NetworkBandwidthLimit, RunId, SupervisorBootstrap}; -use persisting_agentctl::{ - SUPERVISOR_PROTOCOL_VERSION, SupervisorClientMessage, SupervisorDirective, - SupervisorDirectiveAck, SupervisorDirectiveEnvelope, SupervisorHeartbeat, - SupervisorRegistration, SupervisorServerMessage, -}; -use std::sync::Arc; -use std::time::Duration; -use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; -use tokio::net::TcpStream; -use tokio::sync::Mutex; -use tokio::task::JoinHandle; -use tokio_util::sync::CancellationToken; - -const MAX_FRAME_BYTES: usize = 64 * 1024; -const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(1); - -pub(crate) struct SupervisorConnectOutcome { - pub(crate) connected: Option, - pub(crate) controller_epoch: Option, - pub(crate) initial_limits: Vec, - pub(crate) warning: Option, - pub(crate) session: Option, -} - -pub(crate) struct SupervisorSession { - stop: CancellationToken, - _join: JoinHandle<()>, -} - -impl Drop for SupervisorSession { - fn drop(&mut self) { - self.stop.cancel(); - } -} - -pub(crate) async fn connect_optional( - bootstrap: Option<&SupervisorBootstrap>, - run_id: &RunId, - attempt_id: &AttemptId, - lease_epoch: u64, - run_cancellation: CancellationToken, -) -> SupervisorConnectOutcome { - let Some(bootstrap) = bootstrap else { - return SupervisorConnectOutcome { - connected: None, - controller_epoch: None, - initial_limits: Vec::new(), - warning: None, - session: None, - }; - }; - match connect(bootstrap, run_id, attempt_id, lease_epoch, run_cancellation).await { - Ok(outcome) => outcome, - Err(error) => SupervisorConnectOutcome { - connected: Some(false), - controller_epoch: None, - initial_limits: Vec::new(), - warning: Some(format!( - "optional pPilot Supervisor unavailable; continuing standalone: {error:#}" - )), - session: None, - }, - } -} - -async fn connect( - bootstrap: &SupervisorBootstrap, - run_id: &RunId, - attempt_id: &AttemptId, - lease_epoch: u64, - run_cancellation: CancellationToken, -) -> anyhow::Result { - anyhow::ensure!( - bootstrap.controller_epoch > 0, - "Supervisor controller epoch must be non-zero" - ); - let address = bootstrap - .endpoint - .strip_prefix("tcp://") - .ok_or_else(|| anyhow::anyhow!("unsupported Supervisor endpoint {}", bootstrap.endpoint))?; - let timeout = Duration::from_millis(bootstrap.connect_timeout_ms.max(1)); - let stream = tokio::time::timeout(timeout, TcpStream::connect(address)) - .await - .map_err(|_| anyhow::anyhow!("Supervisor connect timed out after {timeout:?}"))??; - let (read, mut write) = stream.into_split(); - let registration = SupervisorClientMessage::Register(SupervisorRegistration { - protocol_version: SUPERVISOR_PROTOCOL_VERSION, - token: bootstrap.token.clone(), - run_id: run_id.clone(), - attempt_id: attempt_id.clone(), - lease_epoch, - }); - write_client_message(&mut write, ®istration).await?; - let mut lines = BufReader::new(read).lines(); - let line = tokio::time::timeout(timeout, lines.next_line()) - .await - .map_err(|_| anyhow::anyhow!("Supervisor registration timed out after {timeout:?}"))?? - .ok_or_else(|| anyhow::anyhow!("Supervisor closed during registration"))?; - anyhow::ensure!( - line.len() <= MAX_FRAME_BYTES, - "Supervisor registration response exceeds {MAX_FRAME_BYTES} bytes" - ); - let response: SupervisorServerMessage = serde_json::from_str(&line)?; - let (controller_epoch, directives) = match response { - SupervisorServerMessage::Registered { - controller_epoch, - directives, - } => (controller_epoch, directives), - SupervisorServerMessage::Error { message } => anyhow::bail!(message), - SupervisorServerMessage::Directive(_) => { - anyhow::bail!("Supervisor sent a directive before registration completed") - } - }; - anyhow::ensure!( - controller_epoch >= bootstrap.controller_epoch, - "stale Supervisor controller epoch {controller_epoch}; expected at least {}", - bootstrap.controller_epoch - ); - - let mut initial_limits = Vec::new(); - let mut last_applied = 0; - for directive in &directives { - if validate_directive(directive, controller_epoch, lease_epoch, last_applied).is_err() { - continue; - } - if let SupervisorDirective::GrantNetworkQuota(grant) = &directive.directive - && grant.valid_until_unix_ms >= unix_now_ms() - && grant.limit.bytes_per_second > 0 - { - initial_limits.push(grant.limit.clone()); - last_applied = directive.directive_seq; - write_client_message( - &mut write, - &SupervisorClientMessage::Ack(SupervisorDirectiveAck { - directive_seq: directive.directive_seq, - applied: true, - }), - ) - .await?; - } - } - - let stop = CancellationToken::new(); - let task_stop = stop.clone(); - let last_applied = Arc::new(Mutex::new(last_applied)); - let task_last_applied = Arc::clone(&last_applied); - let join = tokio::spawn(async move { - let mut heartbeat = tokio::time::interval(HEARTBEAT_INTERVAL); - heartbeat.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); - loop { - let result: anyhow::Result<()> = tokio::select! { - _ = task_stop.cancelled() => break, - _ = heartbeat.tick() => { - let seq = *task_last_applied.lock().await; - write_client_message( - &mut write, - &SupervisorClientMessage::Heartbeat(SupervisorHeartbeat { - last_applied_directive_seq: seq, - }), - ).await - } - line = lines.next_line() => { - match line { - Ok(Some(line)) if line.len() <= MAX_FRAME_BYTES => { - match serde_json::from_str::(&line) { - Ok(SupervisorServerMessage::Directive(directive)) => { - let current = *task_last_applied.lock().await; - let result = apply_live_directive( - &directive, - controller_epoch, - lease_epoch, - current, - &run_cancellation, - ); - if result.0 { - *task_last_applied.lock().await = directive.directive_seq; - } - write_client_message( - &mut write, - &SupervisorClientMessage::Ack(SupervisorDirectiveAck { - directive_seq: directive.directive_seq, - applied: result.0, - }), - ).await - } - Ok(_) => Ok(()), - Err(error) => Err(error.into()), - } - } - Ok(Some(_)) => Err(anyhow::anyhow!("Supervisor frame exceeds {MAX_FRAME_BYTES} bytes")), - Ok(None) => break, - Err(error) => Err(error.into()), - } - } - }; - if let Err(error) = result { - tracing::debug!(%error, "optional pPilot Supervisor disconnected; Run continues standalone"); - break; - } - } - }); - - Ok(SupervisorConnectOutcome { - connected: Some(true), - controller_epoch: Some(controller_epoch), - initial_limits, - warning: None, - session: Some(SupervisorSession { stop, _join: join }), - }) -} - -fn validate_directive( - directive: &SupervisorDirectiveEnvelope, - controller_epoch: u64, - lease_epoch: u64, - last_applied: u64, -) -> anyhow::Result<()> { - anyhow::ensure!( - directive.controller_epoch == controller_epoch, - "Supervisor directive controller epoch mismatch" - ); - anyhow::ensure!( - directive.lease_epoch == lease_epoch, - "Supervisor directive lease epoch mismatch" - ); - anyhow::ensure!( - directive.directive_seq > last_applied, - "Supervisor directive is stale or duplicated" - ); - Ok(()) -} - -fn apply_live_directive( - directive: &SupervisorDirectiveEnvelope, - controller_epoch: u64, - lease_epoch: u64, - last_applied: u64, - run_cancellation: &CancellationToken, -) -> (bool, Option) { - if let Err(error) = validate_directive(directive, controller_epoch, lease_epoch, last_applied) { - return (false, Some(error.to_string())); - } - match &directive.directive { - SupervisorDirective::Cancel => { - run_cancellation.cancel(); - (true, None) - } - SupervisorDirective::GrantNetworkQuota(_) => ( - false, - Some("live quota replacement is not supported by this pVisor version".into()), - ), - } -} - -async fn write_client_message( - write: &mut tokio::net::tcp::OwnedWriteHalf, - message: &SupervisorClientMessage, -) -> anyhow::Result<()> { - let encoded = serde_json::to_vec(message)?; - write.write_all(&encoded).await?; - write.write_all(b"\n").await?; - write.flush().await?; - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn wire_messages_roundtrip_as_json() { - let message = SupervisorServerMessage::Directive(SupervisorDirectiveEnvelope { - controller_epoch: 4, - lease_epoch: 9, - directive_seq: 2, - directive: SupervisorDirective::GrantNetworkQuota(SupervisorNetworkQuotaGrant { - grant_id: "grant-1".into(), - quota_epoch: 3, - valid_until_unix_ms: 100, - limit: NetworkBandwidthLimit { - host: None, - port: None, - bytes_per_second: 32_768, - }, - }), - }); - let encoded = serde_json::to_vec(&message).unwrap(); - let decoded: SupervisorServerMessage = serde_json::from_slice(&encoded).unwrap(); - assert_eq!(decoded, message); - } -} diff --git a/crates/persisting-pvisor/src/util.rs b/crates/persisting-pvisor/src/util.rs deleted file mode 100644 index 6191dcd2b..000000000 --- a/crates/persisting-pvisor/src/util.rs +++ /dev/null @@ -1,108 +0,0 @@ -//! Shared helpers for pVisor. - -use anyhow::Context; -pub use persisting_events::unix_now_ms; -use std::fs::{self, OpenOptions}; -use std::io::Write; -use std::os::unix::fs::PermissionsExt; -use std::path::Path; - -pub fn now_rfc3339_and_unix_ms() -> (String, u64) { - let now = chrono::Utc::now(); - (now.to_rfc3339(), now.timestamp_millis().max(0) as u64) -} - -pub(crate) fn sync_directory(path: &Path) -> anyhow::Result<()> { - fs::File::open(path)? - .sync_all() - .with_context(|| format!("sync directory {}", path.display())) -} - -/// Create a directory tree and sync every newly created directory entry. -pub(crate) fn create_dir_all_durable(path: &Path) -> anyhow::Result<()> { - let mut missing = Vec::new(); - let mut cursor = path; - while !cursor.exists() { - missing.push(cursor.to_path_buf()); - let Some(parent) = cursor.parent() else { - break; - }; - if parent.as_os_str().is_empty() { - break; - } - cursor = parent; - } - fs::create_dir_all(path) - .with_context(|| format!("create directory tree {}", path.display()))?; - for directory in missing.iter().rev() { - let parent = directory - .parent() - .filter(|parent| !parent.as_os_str().is_empty()) - .unwrap_or_else(|| Path::new(".")); - sync_directory(parent)?; - sync_directory(directory)?; - } - Ok(()) -} - -/// Atomically replace a file after syncing both its contents and parent directory. -pub(crate) fn atomic_write(path: &Path, contents: &[u8], mode: u32) -> anyhow::Result<()> { - let parent = path - .parent() - .ok_or_else(|| anyhow::anyhow!("{} has no parent directory", path.display()))?; - create_dir_all_durable(parent)?; - - let file_name = path - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or("persisting"); - let temporary = parent.join(format!(".{file_name}.{}.tmp", uuid::Uuid::new_v4())); - let result = (|| -> anyhow::Result<()> { - let mut file = OpenOptions::new() - .create_new(true) - .write(true) - .open(&temporary) - .with_context(|| format!("create temporary file {}", temporary.display()))?; - file.set_permissions(fs::Permissions::from_mode(mode))?; - file.write_all(contents)?; - file.sync_all() - .with_context(|| format!("sync temporary file {}", temporary.display()))?; - fs::rename(&temporary, path) - .with_context(|| format!("replace {} with {}", path.display(), temporary.display()))?; - sync_directory(parent)?; - Ok(()) - })(); - if result.is_err() { - let _ = fs::remove_file(&temporary); - } - result -} - -#[cfg(test)] -mod tests { - use super::*; - use std::os::unix::fs::PermissionsExt; - - #[test] - fn atomic_write_replaces_private_file() { - let temp = tempfile::tempdir().unwrap(); - let path = temp.path().join("record.json"); - atomic_write(&path, b"first", 0o600).unwrap(); - atomic_write(&path, b"second", 0o600).unwrap(); - - assert_eq!(fs::read(&path).unwrap(), b"second"); - assert_eq!( - fs::metadata(path).unwrap().permissions().mode() & 0o777, - 0o600 - ); - } - - #[test] - fn durable_directory_creation_handles_nested_paths() { - let temp = tempfile::tempdir().unwrap(); - let path = temp.path().join("one/two/three"); - - create_dir_all_durable(&path).unwrap(); - assert!(path.is_dir()); - } -} diff --git a/crates/persisting-pvisor/src/vm.rs b/crates/persisting-pvisor/src/vm.rs deleted file mode 100644 index c78b79dd1..000000000 --- a/crates/persisting-pvisor/src/vm.rs +++ /dev/null @@ -1,1118 +0,0 @@ -//! libkrun VM process isolation over a pVisor-provided root OverlayFS. - -use crate::config::VmSettings; -use crate::executor::{AttemptContext, RunExecutor}; -use anyhow::Context as _; -use async_trait::async_trait; -use persisting_agentctl::{ - CapabilityDimension, CapabilityEnforcementEvidence, ExecutorDescriptor, ExecutorKind, - IsolationKind, ProcessOutput, ResourceLimits, RunFailure, RunFailureKind, RunInvocation, - RunResult, RunState, StdioMode, -}; -use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; -use std::ffi::CString; -use std::os::fd::{AsRawFd, RawFd}; -use std::path::{Path, PathBuf}; -use std::process::Stdio; -use std::time::Duration; -use tokio::io::{AsyncRead, AsyncReadExt}; -use tokio::process::Command; - -const RUNNER_SPEC_ENV: &str = "PERSISTING_KRUN_RUNNER_SPEC"; -const WORKSPACE_TAG: &str = "pvisor-workspace"; -const NETWORK_FD_ENV: &str = "PERSISTING_KRUN_NETWORK_FD"; -const NETWORK_CHILD_FD: RawFd = 198; -const NET_FLAG_DHCP_CLIENT: u32 = 1 << 1; - -#[derive(Debug, Clone)] -pub struct VmExecutor { - settings: VmSettings, -} - -#[derive(Debug)] -struct Captured { - text: String, - truncated: bool, -} - -#[derive(Debug, Serialize, Deserialize)] -struct RunnerSpec { - root: OverlayDeviceSpec, - workspace: Option, - workspace_target: Option, - mount_helper: Option, - guest: GuestSpec, - cpus: u8, - memory_mib: u32, - library_dir: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct OverlayDeviceSpec { - lowers: Vec, - upper: PathBuf, - work: Option, - #[serde(default)] - preimages: Option, - #[serde(default)] - excluded: Vec, -} - -#[derive(Debug, Serialize, Deserialize)] -struct GuestSpec { - program: String, - args: Vec, - env: BTreeMap, - cwd: PathBuf, -} - -impl VmExecutor { - pub fn new(mut settings: VmSettings) -> anyhow::Result { - anyhow::ensure!(settings.memory_mib > 0, "vm.memory_mib must be positive"); - anyhow::ensure!(settings.cpus > 0, "vm.cpus must be positive"); - anyhow::ensure!(settings.cpus <= 8, "libkrunfw supports at most 8 vCPUs"); - let rootfs = settings - .rootfs - .as_deref() - .ok_or_else(|| anyhow::anyhow!("vm.rootfs must be configured"))?; - anyhow::ensure!( - rootfs.is_dir(), - "vm.rootfs is not a directory: {}", - rootfs.display() - ); - if let Some(directory) = &settings.library_dir { - anyhow::ensure!( - directory.is_dir(), - "vm.library_dir is not a directory: {}", - directory.display() - ); - anyhow::ensure!( - directory.join(firmware_name()).is_file(), - "vm.library_dir does not contain {}: {}", - firmware_name(), - directory.display() - ); - } else if let Some(directory) = bundled_firmware_dir() { - settings.library_dir = Some(directory); - } - Ok(Self { settings }) - } - - pub fn settings(&self) -> &VmSettings { - &self.settings - } -} - -pub(crate) fn bundled_firmware_dir() -> Option { - let directory = std::env::current_exe().ok()?.parent()?.to_path_buf(); - directory - .join(firmware_name()) - .is_file() - .then_some(directory) -} - -pub(crate) const fn firmware_name() -> &'static str { - #[cfg(target_os = "macos")] - { - "libkrunfw.5.dylib" - } - #[cfg(not(target_os = "macos"))] - { - "libkrunfw.so.5" - } -} - -#[async_trait] -impl RunExecutor for VmExecutor { - fn descriptor(&self) -> ExecutorDescriptor { - ExecutorDescriptor { - name: "libkrun-root-overlay-v1".into(), - kind: ExecutorKind::VirtualMachine, - isolation: IsolationKind::VirtualMachine, - capability_enforcement: CapabilityEnforcementEvidence::default() - .enforced( - CapabilityDimension::FilesystemRead, - "libkrun-guest-kernel-virtiofs-root", - ) - .enforced( - CapabilityDimension::FilesystemWrite, - "libkrun-guest-kernel-virtiofs-overlay", - ), - supports_checkpoint: true, - supports_migration: false, - } - } - - fn supports(&self, invocation: &RunInvocation) -> bool { - matches!(invocation, RunInvocation::Process(_)) - } - - fn supports_vm_network_attachment(&self) -> bool { - true - } - - async fn execute(&self, context: AttemptContext) -> RunResult { - let mut spec = context.spec().clone(); - let started_at = crate::util::unix_now_ms(); - let cancellation = context.cancellation(); - context - .transition( - RunState::Starting, - Some("starting libkrun guest over pVisor root OverlayFS".into()), - ) - .await; - if !cfg!(any( - target_os = "linux", - all(target_os = "macos", target_arch = "aarch64") - )) { - return failed_to_start( - &spec, - context.attempt_id(), - started_at, - "libkrun execution requires Linux/KVM or Apple Silicon macOS/HVF".into(), - ); - } - - let RunInvocation::Process(invocation) = &mut spec.invocation; - let overlay_target = spec - .metadata - .get("pvisor.vm.overlay_target") - .and_then(serde_json::Value::as_str) - .map(PathBuf::from); - let root = self - .settings - .rootfs - .clone() - .expect("validated by VmExecutor::new"); - if !root.is_dir() { - return failed_to_start( - &spec, - context.attempt_id(), - started_at, - format!("prepared root OverlayFS is not mounted: {}", root.display()), - ); - } - let guest_cwd = spec - .metadata - .get("pvisor.vm.guest_cwd") - .and_then(serde_json::Value::as_str) - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from("/")); - let workspace = spec - .metadata - .get("pvisor.vm.workspace_overlay") - .cloned() - .map(serde_json::from_value::) - .transpose(); - let configured_overlay = match workspace { - Ok(workspace) => workspace, - Err(error) => { - return failed_to_start( - &spec, - context.attempt_id(), - started_at, - format!("invalid libkrun workspace overlay metadata: {error}"), - ); - } - }; - let (root_overlay, workspace) = if overlay_target.is_none() { - ( - configured_overlay.unwrap_or_else(|| OverlayDeviceSpec { - lowers: vec![root.clone()], - upper: PathBuf::new(), - work: None, - preimages: None, - excluded: Vec::new(), - }), - None, - ) - } else { - ( - OverlayDeviceSpec { - lowers: vec![root.clone()], - upper: PathBuf::new(), - work: None, - preimages: None, - excluded: Vec::new(), - }, - configured_overlay, - ) - }; - let workspace_target = workspace.as_ref().and(overlay_target.clone()); - let mut env = if invocation.inherit_env { - std::env::vars().collect::>() - } else { - BTreeMap::new() - }; - for key in [ - crate::AGENTCTL_ENDPOINT_ENV, - crate::AGENTCTL_TOKEN_ENV, - crate::AGENTCTL_TRANSPORT_ENV, - crate::AGENTCTL_VERSION_ENV, - ] { - env.remove(key); - } - for key in [ - "DYLD_LIBRARY_PATH", - "DYLD_FALLBACK_LIBRARY_PATH", - "LD_LIBRARY_PATH", - ] { - env.remove(key); - } - if !invocation.env.contains_key("PATH") { - env.insert( - "PATH".into(), - "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".into(), - ); - } - if !invocation.env.contains_key("HOME") { - env.insert("HOME".into(), "/root".into()); - } - if !invocation.env.contains_key("TMPDIR") { - env.insert("TMPDIR".into(), "/tmp".into()); - } - env.extend(invocation.env.clone()); - - let temporary = match tempfile::Builder::new().prefix("pvisor-krun-").tempdir() { - Ok(value) => value, - Err(error) => { - return failed_to_start(&spec, context.attempt_id(), started_at, error.to_string()); - } - }; - let executable = match std::env::current_exe() { - Ok(path) if path.is_absolute() => path, - Ok(path) => { - return failed_to_start( - &spec, - context.attempt_id(), - started_at, - format!("pVisor executable is not absolute: {}", path.display()), - ); - } - Err(error) => { - return failed_to_start(&spec, context.attempt_id(), started_at, error.to_string()); - } - }; - let runner_root = root.clone(); - let root_upper = temporary.path().join("root-upper"); - let root_work = temporary.path().join("root-work"); - if let Err(error) = - std::fs::create_dir_all(&root_upper).and_then(|()| std::fs::create_dir_all(&root_work)) - { - return failed_to_start( - &spec, - context.attempt_id(), - started_at, - format!("prepare libkrun root overlay: {error}"), - ); - } - let mut root_overlay = if root_overlay.upper.as_os_str().is_empty() { - OverlayDeviceSpec { - lowers: root_overlay.lowers, - upper: root_upper.clone(), - work: Some(root_work.clone()), - preimages: root_overlay.preimages, - excluded: root_overlay.excluded, - } - } else { - root_overlay - }; - let vm_network_enabled = context - .spec() - .metadata - .get("pvisor.network.driver") - .and_then(serde_json::Value::as_str) - == Some("vm-smoltcp"); - if vm_network_enabled { - let network_lower = temporary.path().join("network-lower"); - let resolver = network_lower.join("etc/resolv.conf"); - if let Err(error) = std::fs::create_dir_all(resolver.parent().expect("resolver parent")) - .and_then(|()| { - std::fs::write( - &resolver, - b"nameserver 192.0.2.1\noptions timeout:2 attempts:2\n", - ) - }) - { - return failed_to_start( - &spec, - context.attempt_id(), - started_at, - format!("prepare VM synthetic resolver: {error}"), - ); - } - root_overlay.lowers.insert(0, network_lower); - } - if let Some(workspace) = &workspace { - if workspace.lowers.is_empty() - || workspace.lowers.iter().any(|lower| !lower.is_dir()) - || !workspace.upper.is_dir() - || workspace.work.as_ref().is_some_and(|work| !work.is_dir()) - { - return failed_to_start( - &spec, - context.attempt_id(), - started_at, - "libkrun workspace overlay contains a missing backing directory".into(), - ); - } - let target = workspace_target - .as_deref() - .expect("workspace is only configured with an overlay target"); - let mountpoint = match guest_path_in_root(&runner_root, target) { - Ok(path) => path, - Err(error) => { - return failed_to_start( - &spec, - context.attempt_id(), - started_at, - error.to_string(), - ); - } - }; - match std::fs::symlink_metadata(&mountpoint) { - Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_dir() => { - return failed_to_start( - &spec, - context.attempt_id(), - started_at, - format!( - "guest overlay target must be a directory: {}", - target.display() - ), - ); - } - Ok(_) => {} - Err(error) if error.kind() == std::io::ErrorKind::NotFound => { - let upper_mountpoint = - guest_path_in_root(&root_upper, target).expect("validated guest target"); - if let Err(error) = std::fs::create_dir_all(&upper_mountpoint) { - return failed_to_start( - &spec, - context.attempt_id(), - started_at, - format!("create guest overlay target: {error}"), - ); - } - } - Err(error) => { - return failed_to_start( - &spec, - context.attempt_id(), - started_at, - error.to_string(), - ); - } - } - } - let guest = GuestSpec { - program: invocation.program.clone(), - args: invocation.args.clone(), - env, - cwd: guest_cwd, - }; - let mount_helper_guest = if workspace_target.is_some() { - let source = match guest_mount_program(&root) { - Some(path) => path, - None => { - return failed_to_start( - &spec, - context.attempt_id(), - started_at, - format!( - "libkrun guest rootfs does not contain mount: {}", - root.display() - ), - ); - } - }; - let name = format!(".pvisor-mount-{}", uuid::Uuid::new_v4().simple()); - let host = root_overlay.upper.join(&name); - if let Err(error) = copy_guest_mount_program(&source, &host) { - return failed_to_start( - &spec, - context.attempt_id(), - started_at, - format!("prepare guest mount helper: {error:#}"), - ); - } - Some(Path::new("/").join(name)) - } else { - None - }; - let helper_name = format!(".pvisor-exec-{}.sh", uuid::Uuid::new_v4().simple()); - let helper_host = root_overlay.upper.join(&helper_name); - if let Err(error) = write_guest_helper( - &helper_host, - workspace_target.as_deref(), - mount_helper_guest.as_deref(), - &guest, - &spec.runtime.resource_limits, - ) { - return failed_to_start( - &spec, - context.attempt_id(), - started_at, - format!("create guest execution helper: {error:#}"), - ); - } - let helper_guest = Path::new("/").join(helper_name); - let requested_memory_mib = spec - .runtime - .resource_limits - .memory_bytes - .map(|bytes| bytes.div_ceil(1024 * 1024).max(1)) - .and_then(|mib| u32::try_from(mib).ok()); - let runner = RunnerSpec { - root: root_overlay, - workspace, - workspace_target: workspace_target.clone(), - mount_helper: Some(helper_guest), - guest, - cpus: self.settings.cpus as u8, - memory_mib: requested_memory_mib - .map(|requested| requested.min(self.settings.memory_mib)) - .unwrap_or(self.settings.memory_mib), - library_dir: self.settings.library_dir.clone(), - }; - let runner_path = temporary.path().join("runner.json"); - if let Err(error) = write_private_json(&runner_path, &runner) { - return failed_to_start(&spec, context.attempt_id(), started_at, error.to_string()); - } - - let mut vm_network = match context.take_vm_network() { - Ok(network) => network, - Err(error) => { - return failed_to_start(&spec, context.attempt_id(), started_at, error.to_string()); - } - }; - if vm_network_enabled && vm_network.is_none() { - return failed_to_start( - &spec, - context.attempt_id(), - started_at, - "pVisor VM network attachment is missing".into(), - ); - } - let mut command = Command::new(executable); - command - .env(RUNNER_SPEC_ENV, &runner_path) - .stdin(stdio(invocation.stdin)) - .stdout(stdio(invocation.stdout)) - .stderr(stdio(invocation.stderr)) - .kill_on_drop(true); - if let Some(network) = &vm_network { - let source_fd = network.guest_stream().as_raw_fd(); - command.env(NETWORK_FD_ENV, NETWORK_CHILD_FD.to_string()); - // The socketpair has CLOEXEC. Duplicate it to one fixed inherited - // descriptor after fork and before exec; the JSON runner spec never - // contains a process-local FD number. - unsafe { - command.pre_exec(move || { - if libc::dup2(source_fd, NETWORK_CHILD_FD) < 0 { - return Err(std::io::Error::last_os_error()); - } - Ok(()) - }); - } - } - // libkrun's x86_64 KVM path can otherwise race guest workqueue - // creation and halt before init runs. The upstream compatibility - // switch is still required on the Fedora 43 / Linux 6.17 host used by - // pVisor's Linux validation, not only the older kernels named in the - // vendored libkrun comment. - #[cfg(all(target_os = "linux", target_arch = "x86_64"))] - command.env("KRUN_ENOMEM_WORKAROUND", "1"); - if let Some(directory) = &self.settings.library_dir { - #[cfg(target_os = "linux")] - command.env("LD_LIBRARY_PATH", directory); - #[cfg(target_os = "macos")] - command.env("DYLD_LIBRARY_PATH", directory); - } - let mut child = match command.spawn() { - Ok(child) => child, - Err(error) => { - return failed_to_start(&spec, context.attempt_id(), started_at, error.to_string()); - } - }; - let stdout_task = child.stdout.take().map(|stdout| { - let limit = spec.runtime.max_output_bytes; - tokio::spawn(async move { read_limited(stdout, limit).await }) - }); - let stderr_task = child.stderr.take().map(|stderr| { - let limit = spec.runtime.max_output_bytes; - tokio::spawn(async move { read_limited(stderr, limit).await }) - }); - context.transition(RunState::Running, None).await; - - enum End { - Exited(std::io::Result), - Cancelled, - Watchdog, - } - let watchdog_ms = spec.runtime.timeout_ms.map(|timeout| { - timeout - .saturating_add(spec.runtime.termination_grace_ms) - .saturating_add(10_000) - }); - let end = if let Some(watchdog_ms) = watchdog_ms { - tokio::select! { - biased; - status = child.wait() => End::Exited(status), - _ = cancellation.cancelled() => End::Cancelled, - _ = tokio::time::sleep(Duration::from_millis(watchdog_ms)) => End::Watchdog, - } - } else { - tokio::select! { - biased; - status = child.wait() => End::Exited(status), - _ = cancellation.cancelled() => End::Cancelled, - } - }; - if matches!(end, End::Cancelled | End::Watchdog) { - if matches!(end, End::Cancelled) { - context - .transition(RunState::Cancelling, Some("cancellation requested".into())) - .await; - } - let _ = child.kill().await; - let _ = child.wait().await; - } - let transport_stdout = join_capture(stdout_task).await; - let transport_stderr = join_capture(stderr_task).await; - let mut output = ProcessOutput::default(); - if let Some(captured) = transport_stdout { - output.stdout = Some(captured.text); - output.stdout_truncated = captured.truncated; - } - if let Some(captured) = transport_stderr { - output.stderr = Some(captured.text); - output.stderr_truncated = captured.truncated; - } - let (state, exit_code, failure) = match end { - End::Cancelled => (RunState::Cancelled, None, None), - End::Watchdog => ( - RunState::Failed, - None, - Some(RunFailure { - kind: RunFailureKind::DeadlineExceeded, - message: "libkrun guest exceeded the transport watchdog".into(), - retryable: false, - }), - ), - End::Exited(Ok(status)) if status.code().is_some() => { - (RunState::Completed, status.code(), None) - } - End::Exited(Ok(status)) => ( - RunState::Failed, - None, - Some(RunFailure { - kind: RunFailureKind::Infrastructure, - message: format!("libkrun runner terminated by {status}"), - retryable: false, - }), - ), - End::Exited(Err(error)) => ( - RunState::Failed, - None, - Some(RunFailure { - kind: RunFailureKind::Infrastructure, - message: error.to_string(), - retryable: true, - }), - ), - }; - let mut warnings = Vec::new(); - if let Some(network) = vm_network.take() - && let Err(error) = network.shutdown() - { - tracing::warn!(%error, "failed to stop VM smoltcp backend"); - warnings.push(format!("failed to stop VM smoltcp backend: {error:#}")); - } - RunResult { - run_id: spec.run_id, - attempt_id: context.attempt_id().clone(), - lease_epoch: spec.lease_epoch, - state, - started_at_unix_ms: started_at, - finished_at_unix_ms: crate::util::unix_now_ms(), - exit_code, - failure, - output, - value: None, - metrics: BTreeMap::from([( - "resource.vm_memory_bytes".into(), - f64::from(runner.memory_mib) * 1024.0 * 1024.0, - )]), - artifacts: Vec::new(), - event_stream_ref: None, - warnings, - } - } -} - -/// Handle the self-exec libkrun runner. -/// Returns `true` when the current process was consumed by an internal mode. -pub fn run_internal_if_requested() -> anyhow::Result { - if let Some(path) = std::env::var_os(RUNNER_SPEC_ENV) { - let spec: RunnerSpec = serde_json::from_slice(&std::fs::read(&path)?)?; - run_runner(spec)?; - return Ok(true); - } - Ok(false) -} - -fn run_runner(spec: RunnerSpec) -> anyhow::Result<()> { - #[cfg(target_os = "linux")] - { - let mut read_only = spec.root.lowers.clone(); - let mut read_write = vec![spec.root.upper.clone()]; - read_write.extend(spec.root.work.iter().cloned()); - if let Some(workspace) = &spec.workspace { - read_only.extend(workspace.lowers.iter().cloned()); - read_write.push(workspace.upper.clone()); - read_write.extend(workspace.work.iter().cloned()); - read_write.extend(workspace.preimages.iter().cloned()); - } - crate::sandbox::restrict_krun_runner(read_only, read_write, spec.library_dir.clone())?; - } - run_linked_krun(spec) -} - -fn run_linked_krun(spec: RunnerSpec) -> anyhow::Result<()> { - if std::env::var_os("PERSISTING_KRUN_LOG").is_some() { - check_krun(krun::krun_set_log_level(5), "krun_set_log_level")?; - } - let workspace_tag = CString::new(WORKSPACE_TAG)?; - let helper = spec - .mount_helper - .as_deref() - .ok_or_else(|| anyhow::anyhow!("libkrun guest execution helper is missing"))?; - let program = path_cstring(helper)?; - let workdir = CString::new("/")?; - // libkrun 1.19 serializes argv and env through the kernel command line - // without escaping embedded quotes. The helper contains the exact - // invocation instead, so only its quote-free path crosses that boundary. - let argv = Vec::::new(); - let mut argv_ptrs = argv.iter().map(|value| value.as_ptr()).collect::>(); - argv_ptrs.push(std::ptr::null()); - let env = Vec::::new(); - let mut env_ptrs = env.iter().map(|value| value.as_ptr()).collect::>(); - env_ptrs.push(std::ptr::null()); - - let ctx = check_ctx(krun::krun_create_ctx(), "krun_create_ctx")?; - check_krun( - krun::krun_set_vm_config(ctx, spec.cpus, spec.memory_mib), - "krun_set_vm_config", - )?; - add_krun_overlay(ctx, "/dev/root", &spec.root, 1 << 29)?; - if let Some(workspace) = &spec.workspace { - add_krun_overlay(ctx, workspace_tag.to_str()?, workspace, 0)?; - } - if let Some(fd) = std::env::var_os(NETWORK_FD_ENV) { - let fd = fd - .to_str() - .ok_or_else(|| anyhow::anyhow!("invalid {NETWORK_FD_ENV}"))? - .parse::() - .with_context(|| format!("parse {NETWORK_FD_ENV}"))?; - check_krun( - unsafe { - krun::krun_add_net_unixstream( - ctx, - std::ptr::null(), - fd, - persisting_overlaynet::vm::VM_MAC.as_ptr(), - 0, - NET_FLAG_DHCP_CLIENT, - ) - }, - "krun_add_net_unixstream", - )?; - } - // Contexts start with an implicit vsock whose heuristic enables TSI when - // there is no virtio-net device. Replace it with an explicit zero-feature - // device so ordinary guest sockets cannot escape through the host stack. - check_krun( - krun::krun_disable_implicit_vsock(ctx), - "krun_disable_implicit_vsock", - )?; - check_krun(krun::krun_add_vsock(ctx, 0), "krun_add_vsock")?; - check_krun( - unsafe { krun::krun_set_workdir(ctx, workdir.as_ptr()) }, - "krun_set_workdir", - )?; - check_krun( - unsafe { - krun::krun_set_exec(ctx, program.as_ptr(), argv_ptrs.as_ptr(), env_ptrs.as_ptr()) - }, - "krun_set_exec", - )?; - let started = krun::krun_start_enter(ctx); - #[cfg(target_os = "macos")] - if started == -libc::EINVAL { - anyhow::bail!( - "krun_start_enter failed with errno 22; source-built macOS binaries must be signed \ - with crates/persisting-pvisor/macos-hypervisor.entitlements" - ); - } - check_krun(started, "krun_start_enter")?; - Ok(()) -} - -fn add_krun_overlay( - ctx: u32, - tag: &str, - overlay: &OverlayDeviceSpec, - shm_size: u64, -) -> anyhow::Result<()> { - anyhow::ensure!( - !overlay.lowers.is_empty(), - "libkrun overlay requires a lower directory" - ); - let tag = CString::new(tag)?; - let lowers = overlay - .lowers - .iter() - .map(|path| path_cstring(path)) - .collect::>>()?; - let lower_ptrs = lowers.iter().map(|path| path.as_ptr()).collect::>(); - let upper = path_cstring(&overlay.upper)?; - let work = overlay.work.as_deref().map(path_cstring).transpose()?; - let preimages = overlay.preimages.as_deref().map(path_cstring).transpose()?; - let excluded = overlay - .excluded - .iter() - .map(|path| path_cstring(path)) - .collect::>>()?; - let excluded_ptrs = excluded - .iter() - .map(|path| path.as_ptr()) - .collect::>(); - check_krun( - unsafe { - krun::krun_add_virtiofs_overlay( - ctx, - tag.as_ptr(), - lower_ptrs.as_ptr(), - lower_ptrs.len(), - upper.as_ptr(), - work.as_ref().map_or(std::ptr::null(), |path| path.as_ptr()), - preimages - .as_ref() - .map_or(std::ptr::null(), |path| path.as_ptr()), - excluded_ptrs.as_ptr(), - excluded_ptrs.len(), - shm_size, - ) - }, - "krun_add_virtiofs_overlay", - ) -} - -fn write_guest_helper( - path: &Path, - workspace_target: Option<&Path>, - mount_helper: Option<&Path>, - guest: &GuestSpec, - limits: &ResourceLimits, -) -> anyhow::Result<()> { - use std::os::unix::fs::PermissionsExt; - - let mut script = String::from("#!/bin/sh\nset -eu\n"); - if let Some(target) = workspace_target { - let mount_helper = - mount_helper.ok_or_else(|| anyhow::anyhow!("workspace mount helper is missing"))?; - script.push_str(&shell_quote(&mount_helper.to_string_lossy())?); - script.push_str(" -t virtiofs pvisor-workspace "); - script.push_str(&shell_quote(&target.to_string_lossy())?); - script.push('\n'); - } - if let Some(bytes) = limits.memory_bytes { - script.push_str(&format!("ulimit -v {}\n", bytes.div_ceil(1024))); - } - if let Some(processes) = limits.processes { - script.push_str(&format!("ulimit -u {processes}\n")); - } - if let Some(milliseconds) = limits.cpu_time_ms { - script.push_str(&format!("ulimit -t {}\n", milliseconds.div_ceil(1_000))); - } - if let Some(open_files) = limits.open_files { - script.push_str(&format!("ulimit -n {open_files}\n")); - } - if let Some(bytes) = limits.file_size_bytes { - script.push_str(&format!("ulimit -f {}\n", bytes.div_ceil(512))); - } - script.push_str("rm -f /init.krun \"$0\""); - if let Some(mount_helper) = mount_helper { - script.push(' '); - script.push_str(&shell_quote(&mount_helper.to_string_lossy())?); - } - script.push_str("\ncd "); - script.push_str(&shell_quote(&guest.cwd.to_string_lossy())?); - script.push_str("\nexec env -i"); - for (key, value) in &guest.env { - script.push(' '); - script.push_str(&shell_quote(&format!("{key}={value}"))?); - } - script.push(' '); - script.push_str(&shell_quote(&guest.program)?); - for argument in &guest.args { - script.push(' '); - script.push_str(&shell_quote(argument)?); - } - script.push('\n'); - std::fs::write(path, script)?; - std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))?; - Ok(()) -} - -fn guest_mount_program(root: &Path) -> Option { - ["bin/mount", "usr/bin/mount", "sbin/mount", "usr/sbin/mount"] - .into_iter() - .map(|relative| root.join(relative)) - .find(|path| path.is_file()) -} - -fn copy_guest_mount_program(source: &Path, destination: &Path) -> anyhow::Result<()> { - use std::os::unix::fs::PermissionsExt; - - std::fs::copy(source, destination)?; - // Host distributions commonly install mount setuid-root. The rootless - // passthrough cannot preserve its owner, so executing that file would - // switch guest root to the mapped host uid. A private non-setuid copy - // keeps the already-root guest credentials and can perform the mount. - std::fs::set_permissions(destination, std::fs::Permissions::from_mode(0o700))?; - Ok(()) -} - -fn shell_quote(value: &str) -> anyhow::Result { - anyhow::ensure!( - !value.as_bytes().contains(&0), - "guest command and environment cannot contain NUL bytes" - ); - Ok(format!("'{}'", value.replace('\'', "'\"'\"'"))) -} - -fn stdio(mode: StdioMode) -> Stdio { - match mode { - StdioMode::Inherit => Stdio::inherit(), - StdioMode::Capture => Stdio::piped(), - StdioMode::Null => Stdio::null(), - } -} - -async fn read_limited( - mut reader: R, - limit: usize, -) -> std::io::Result { - let mut retained = Vec::with_capacity(limit.min(8192)); - let mut buffer = [0_u8; 8192]; - let mut truncated = false; - loop { - let read = reader.read(&mut buffer).await?; - if read == 0 { - break; - } - let keep = limit.saturating_sub(retained.len()).min(read); - retained.extend_from_slice(&buffer[..keep]); - truncated |= keep < read; - } - Ok(Captured { - text: String::from_utf8_lossy(&retained).into_owned(), - truncated, - }) -} - -async fn join_capture( - task: Option>>, -) -> Option { - match task { - Some(task) => task.await.ok().and_then(Result::ok), - None => None, - } -} - -fn write_private_json(path: &Path, value: &impl Serialize) -> anyhow::Result<()> { - use std::os::unix::fs::PermissionsExt; - std::fs::write(path, serde_json::to_vec(value)?)?; - std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?; - Ok(()) -} - -fn failed_to_start( - spec: &persisting_agentctl::RunSpec, - attempt_id: &persisting_agentctl::AttemptId, - started_at: u64, - message: String, -) -> RunResult { - RunResult { - run_id: spec.run_id.clone(), - attempt_id: attempt_id.clone(), - lease_epoch: spec.lease_epoch, - state: RunState::Failed, - started_at_unix_ms: started_at, - finished_at_unix_ms: crate::util::unix_now_ms(), - exit_code: None, - failure: Some(RunFailure { - kind: RunFailureKind::Spawn, - message, - retryable: false, - }), - output: ProcessOutput::default(), - value: None, - metrics: Default::default(), - artifacts: Vec::new(), - event_stream_ref: None, - warnings: Vec::new(), - } -} - -fn path_cstring(path: &Path) -> anyhow::Result { - use std::os::unix::ffi::OsStrExt; - Ok(CString::new(path.as_os_str().as_bytes())?) -} - -fn guest_path_in_root(root: &Path, target: &Path) -> anyhow::Result { - anyhow::ensure!( - target.is_absolute() && target != Path::new("/"), - "libkrun guest overlay target must be an absolute path other than /" - ); - anyhow::ensure!( - !target - .components() - .any(|component| matches!(component, std::path::Component::ParentDir)), - "libkrun guest overlay target must not contain .." - ); - let relative = target.strip_prefix(Path::new("/"))?; - let mut resolved = root.to_path_buf(); - for component in relative.components() { - let std::path::Component::Normal(component) = component else { - continue; - }; - resolved.push(component); - match std::fs::symlink_metadata(&resolved) { - Ok(metadata) => anyhow::ensure!( - !metadata.file_type().is_symlink(), - "libkrun guest overlay target traverses a symlink: {}", - target.display() - ), - Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} - Err(error) => return Err(error.into()), - } - } - Ok(resolved) -} - -fn check_ctx(value: i32, operation: &str) -> anyhow::Result { - if value < 0 { - anyhow::bail!("{operation} failed with errno {}", -value); - } - Ok(value as u32) -} - -fn check_krun(value: i32, operation: &str) -> anyhow::Result<()> { - if value < 0 { - anyhow::bail!("{operation} failed with errno {}", -value); - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn settings_validate_resource_limits() { - let rootfs = tempfile::tempdir().unwrap(); - let settings = VmSettings { - rootfs: Some(rootfs.path().to_path_buf()), - ..VmSettings::default() - }; - assert!(VmExecutor::new(settings.clone()).is_ok()); - assert!(VmExecutor::new(VmSettings::default()).is_err()); - assert!( - VmExecutor::new(VmSettings { - cpus: 9, - ..settings - }) - .is_err() - ); - } - - #[test] - fn guest_helper_preserves_quoted_arguments_and_environment() { - let temporary = tempfile::tempdir().unwrap(); - let helper = temporary.path().join("guest-helper.sh"); - let environment_value = "space ' single \" double\nnewline"; - let argument_value = "argument ' with \" quotes"; - let guest = GuestSpec { - program: "/bin/sh".into(), - args: vec![ - "-c".into(), - "printf '%s\\n%s' \"$COMPLEX\" \"$0\" > result".into(), - argument_value.into(), - ], - env: BTreeMap::from([("COMPLEX".into(), environment_value.into())]), - cwd: temporary.path().to_path_buf(), - }; - write_guest_helper(&helper, None, None, &guest, &ResourceLimits::default()).unwrap(); - - let status = std::process::Command::new(&helper).status().unwrap(); - assert!(status.success()); - assert_eq!( - std::fs::read_to_string(temporary.path().join("result")).unwrap(), - format!("{environment_value}\n{argument_value}") - ); - } - - #[test] - fn guest_helper_emits_requested_resource_limits() { - let temporary = tempfile::tempdir().unwrap(); - let helper = temporary.path().join("guest-helper.sh"); - let guest = GuestSpec { - program: "/bin/true".into(), - args: Vec::new(), - env: BTreeMap::new(), - cwd: PathBuf::from("/"), - }; - write_guest_helper( - &helper, - None, - None, - &guest, - &ResourceLimits { - memory_bytes: Some(2 * 1024 * 1024), - processes: Some(8), - cpu_time_ms: Some(1_500), - open_files: Some(32), - file_size_bytes: Some(1024), - }, - ) - .unwrap(); - let script = std::fs::read_to_string(helper).unwrap(); - assert!(script.contains("ulimit -v 2048")); - assert!(script.contains("ulimit -u 8")); - assert!(script.contains("ulimit -t 2")); - assert!(script.contains("ulimit -n 32")); - assert!(script.contains("ulimit -f 2")); - } - - #[test] - fn guest_mount_copy_strips_privilege_bits() { - use std::os::unix::fs::{MetadataExt, PermissionsExt}; - - let temporary = tempfile::tempdir().unwrap(); - let source = temporary.path().join("mount"); - let destination = temporary.path().join("mount-helper"); - std::fs::write(&source, b"mount").unwrap(); - std::fs::set_permissions(&source, std::fs::Permissions::from_mode(0o4755)).unwrap(); - - copy_guest_mount_program(&source, &destination).unwrap(); - - assert_eq!( - std::fs::metadata(destination).unwrap().mode() & 0o7777, - 0o700 - ); - } -} diff --git a/crates/persisting-pvisor/tests/agentctl_contract.rs b/crates/persisting-pvisor/tests/agentctl_contract.rs deleted file mode 100644 index dc24e9856..000000000 --- a/crates/persisting-pvisor/tests/agentctl_contract.rs +++ /dev/null @@ -1,181 +0,0 @@ -use persisting_agentctl::{ - AgentCtlClient, AgentCtlClientConfig, AgentCtlResponseError, AgentDirective, AgentErrorCode, - AgentState, AttemptId, RunId, -}; -use persisting_pvisor::AgentCtlServer; -use std::time::Duration; - -fn client(server: &AgentCtlServer, client_id: &str) -> AgentCtlClient { - AgentCtlClient::new( - AgentCtlClientConfig::from_environment(&server.environment(), client_id) - .unwrap() - .unwrap(), - ) -} - -fn response_code(error: anyhow::Error) -> AgentErrorCode { - error.downcast::().unwrap().code -} - -#[test] -fn all_runtime_clients_must_sync_the_checkpoint_boundary() { - let server = AgentCtlServer::start(&RunId::new("run-1"), &AttemptId::new("attempt-1")).unwrap(); - let mut first = client(&server, "first"); - let mut second = client(&server, "second"); - - assert_eq!(first.connect().unwrap(), AgentDirective::Continue); - assert_eq!(second.connect().unwrap(), AgentDirective::Continue); - assert_eq!( - first.sync(AgentState::Active).unwrap(), - AgentDirective::Continue - ); - assert_eq!( - second.sync(AgentState::Active).unwrap(), - AgentDirective::Continue - ); - - server - .control() - .request_quiesce("checkpoint-1", None) - .unwrap(); - assert!(matches!( - first.sync(AgentState::Idle).unwrap(), - AgentDirective::Quiesce { - ref checkpoint_id, - .. - } if checkpoint_id == "checkpoint-1" - )); - first - .sync(AgentState::Quiesced { - checkpoint_id: "checkpoint-1".into(), - }) - .unwrap(); - - let snapshot = server.control().snapshot(); - assert!(matches!( - snapshot - .clients - .iter() - .find(|client| client.client_id == "first") - .unwrap() - .state, - AgentState::Quiesced { ref checkpoint_id } if checkpoint_id == "checkpoint-1" - )); - assert!(matches!( - snapshot - .clients - .iter() - .find(|client| client.client_id == "second") - .unwrap() - .state, - AgentState::Active - )); - - assert!(matches!( - second.sync(AgentState::Active).unwrap(), - AgentDirective::Quiesce { .. } - )); - second - .sync(AgentState::Quiesced { - checkpoint_id: "checkpoint-1".into(), - }) - .unwrap(); - assert!(server.control().snapshot().clients.iter().all(|client| { - matches!( - &client.state, - AgentState::Quiesced { checkpoint_id } if checkpoint_id == "checkpoint-1" - ) - })); - - server.control().continue_execution(); - assert_eq!( - first - .sync(AgentState::Quiesced { - checkpoint_id: "checkpoint-1".into(), - }) - .unwrap(), - AgentDirective::Continue - ); -} - -#[test] -fn invalid_token_and_duplicate_live_client_have_typed_errors() { - let server = AgentCtlServer::start(&RunId::new("run-1"), &AttemptId::new("attempt-1")).unwrap(); - let mut bad_config = AgentCtlClientConfig::from_environment(&server.environment(), "bad-token") - .unwrap() - .unwrap(); - bad_config.token = "wrong".into(); - assert_eq!( - response_code(AgentCtlClient::new(bad_config).connect().unwrap_err()), - AgentErrorCode::Unauthorized - ); - - let mut first = client(&server, "same-client"); - let mut duplicate = client(&server, "same-client"); - first.connect().unwrap(); - assert_eq!( - response_code(duplicate.connect().unwrap_err()), - AgentErrorCode::Conflict - ); -} - -#[test] -fn checkpoint_rejects_new_sessions_and_mismatched_acknowledgements() { - let server = AgentCtlServer::start(&RunId::new("run-1"), &AttemptId::new("attempt-1")).unwrap(); - let mut participant = client(&server, "participant"); - participant.connect().unwrap(); - server.control().request_quiesce("cp", None).unwrap(); - - let mut late = client(&server, "late"); - assert_eq!( - response_code(late.connect().unwrap_err()), - AgentErrorCode::Conflict - ); - assert_eq!( - response_code( - participant - .sync(AgentState::Quiesced { - checkpoint_id: "wrong".into(), - }) - .unwrap_err() - ), - AgentErrorCode::Conflict - ); - - let state = AgentState::Quiesced { - checkpoint_id: "cp".into(), - }; - assert!(matches!( - participant.sync(state.clone()).unwrap(), - AgentDirective::Quiesce { .. } - )); - assert!(matches!( - participant.sync(state).unwrap(), - AgentDirective::Quiesce { .. } - )); -} - -#[test] -fn stale_session_can_be_replaced_outside_checkpoint() { - let server = AgentCtlServer::start(&RunId::new("run-1"), &AttemptId::new("attempt-1")).unwrap(); - let mut stale = client(&server, "worker"); - stale.connect().unwrap(); - std::thread::sleep(Duration::from_millis(3_100)); - - let mut replacement = client(&server, "worker"); - assert_eq!(replacement.connect().unwrap(), AgentDirective::Continue); -} - -#[test] -fn checkpoint_keeps_a_disconnected_participant_in_the_frozen_set() { - let server = AgentCtlServer::start(&RunId::new("run-1"), &AttemptId::new("attempt-1")).unwrap(); - let mut participant = client(&server, "participant"); - participant.connect().unwrap(); - server.control().request_quiesce("cp", None).unwrap(); - std::thread::sleep(Duration::from_millis(3_100)); - - let snapshot = server.control().snapshot(); - assert_eq!(snapshot.clients.len(), 1); - assert_eq!(snapshot.clients[0].client_id, "participant"); - assert!(snapshot.clients[0].stale); -} diff --git a/crates/persisting-pvisor/tests/fixtures/bundles/v1-minimal.json b/crates/persisting-pvisor/tests/fixtures/bundles/v1-minimal.json deleted file mode 100644 index ce99338f5..000000000 --- a/crates/persisting-pvisor/tests/fixtures/bundles/v1-minimal.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "schema_version": 1, - "generated_at_unix_ms": 20, - "run": { - "run_id": "run-v1-fixture", - "attempt_id": "attempt-v1-fixture", - "session_id": "run-v1-fixture", - "agent": "fixture-agent", - "command": ["/bin/true"], - "state": "completed", - "started_at_unix_ms": 10, - "finished_at_unix_ms": 20, - "duration_ms": 10, - "exit_code": 0 - }, - "safety": { - "safe_profile_requested": false, - "host_process": true, - "filesystem_changes_staged": false, - "filesystem_non_bypassable": false, - "network_non_bypassable": false, - "warnings": [] - }, - "network": { - "policy": { - "mode": "ambient" - } - }, - "agentctl": { - "run_id": "run-v1-fixture", - "attempt_id": "attempt-v1-fixture", - "directive": { - "kind": "continue" - }, - "clients": [] - } -} diff --git a/crates/persisting-pvisor/tests/macos_safe.rs b/crates/persisting-pvisor/tests/macos_safe.rs deleted file mode 100644 index 378ec8720..000000000 --- a/crates/persisting-pvisor/tests/macos_safe.rs +++ /dev/null @@ -1,258 +0,0 @@ -#![cfg(target_os = "macos")] - -use persisting_agentctl::IsolationKind; -use persisting_pvisor::RunBundle; -use std::fs; -use std::net::TcpListener; -use std::os::unix::net::UnixListener; -use std::path::{Path, PathBuf}; -use std::process::Command; - -fn macfuse_is_installed() -> bool { - Path::new("/Library/Filesystems/macfuse.fs").is_dir() -} - -fn only_run(root: &Path) -> PathBuf { - let runs = fs::read_dir(root) - .expect("read Run root") - .filter_map(Result::ok) - .map(|entry| entry.path()) - .filter(|path| path.join("run-bundle.json").is_file()) - .collect::>(); - assert_eq!(runs.len(), 1, "expected one finalized Run in {root:?}"); - runs.into_iter().next().unwrap() -} - -#[test] -fn safe_profile_stages_reviews_and_applies_on_macos() { - if !macfuse_is_installed() { - eprintln!( - "skipping macOS safe-profile smoke test: install macFUSE to exercise staged writes" - ); - return; - } - - let temporary = tempfile::Builder::new() - .prefix("pvmac") - .tempdir_in("/tmp") - .expect("create short macOS fixture path"); - let workspace = temporary.path().join("workspace"); - let run_home = temporary.path().join("runs"); - let outside = temporary.path().join("outside.txt"); - let outside_secret = temporary.path().join("outside-secret.txt"); - fs::create_dir(&workspace).unwrap(); - fs::write(&outside_secret, "read-compatible").unwrap(); - - let mut command = Command::new(env!("CARGO_BIN_EXE_pvisor")); - command - .env("PERSISTING_RUN_HOME", &run_home) - .args(["run", "--stdio", "capture", "--overlayfs-compose"]) - .arg(&workspace) - .args([ - "--", - "/bin/sh", - "-c", - r#" - test "$PERSISTING_SANDBOX_FILESYSTEM" = seatbelt-write || exit 38 - test "$PERSISTING_SANDBOX_NETWORK" = ambient || exit 39 - test "$(cat "$2")" = read-compatible || exit 40 - if printf escaped > "$1" 2>/dev/null; then exit 41; fi - ln -s "$1" outside-link - if printf escaped > outside-link 2>/dev/null; then exit 42; fi - if ln "$2" outside-hardlink 2>/dev/null; then - printf mutated > outside-hardlink 2>/dev/null || true - rm -f outside-hardlink - fi - test "$(cat "$2")" = read-compatible || exit 43 - printf scratch > "$TMPDIR/probe" - printf staged > macos-staged.txt - printf macos-ok - "#, - "pvisor-macos-test", - ]) - .arg(&outside) - .arg(&outside_secret); - let output = command.output().expect("run macOS safe profile"); - if !output.status.success() - && String::from_utf8_lossy(&output.stderr).contains("file system is not available") - { - eprintln!("skipping macOS safe-profile smoke test: macFUSE is installed but unavailable"); - return; - } - assert!( - output.status.success(), - "stdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - assert!(!workspace.join("macos-staged.txt").exists()); - assert!( - !outside.exists(), - "Seatbelt allowed a write outside the stage" - ); - assert_eq!( - fs::read_to_string(&outside_secret).unwrap(), - "read-compatible" - ); - - let run = only_run(&run_home); - let bundle = RunBundle::read(&run).unwrap(); - assert_eq!( - bundle - .run - .executor - .as_ref() - .map(|executor| executor.isolation), - Some(IsolationKind::SandboxedProcess) - ); - assert!(bundle.safety.safe_profile_requested); - assert!(bundle.safety.filesystem_changes_staged); - assert!(!bundle.safety.filesystem_non_bypassable); - assert!(!bundle.safety.filesystem_read_non_bypassable); - assert!(bundle.safety.filesystem_write_non_bypassable); - assert!(!bundle.safety.network_non_bypassable); - assert!( - bundle - .run - .output - .stdout - .as_deref() - .is_some_and(|stdout| stdout == "macos-ok") - ); - let filesystem = bundle.filesystem.as_ref().expect("filesystem summary"); - assert_eq!(filesystem.changed_files, 2); - assert!(filesystem.upper.join("macos-staged.txt").is_file()); - assert!(filesystem.upper.join("outside-link").is_symlink()); - - let review = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .env("PERSISTING_RUN_HOME", &run_home) - .args(["review", "--json"]) - .arg(&run) - .output() - .expect("review macOS Run"); - assert!( - review.status.success(), - "review failed: {}", - String::from_utf8_lossy(&review.stderr) - ); - let reviewed: serde_json::Value = serde_json::from_slice(&review.stdout).unwrap(); - assert_eq!(reviewed["safety"]["filesystem_non_bypassable"], false); - assert_eq!(reviewed["safety"]["filesystem_write_non_bypassable"], true); - - let apply = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .env("PERSISTING_RUN_HOME", &run_home) - .arg("apply") - .arg(&run) - .output() - .expect("apply macOS Run"); - assert!( - apply.status.success(), - "apply failed: {}", - String::from_utf8_lossy(&apply.stderr) - ); - assert_eq!( - fs::read_to_string(workspace.join("macos-staged.txt")).unwrap(), - "staged" - ); -} - -#[test] -fn deny_all_blocks_ip_and_host_unix_sockets_on_macos() { - if !macfuse_is_installed() { - eprintln!("skipping macOS Seatbelt network test: macFUSE is not installed"); - return; - } - - let temporary = tempfile::Builder::new() - .prefix("pvmacnet") - .tempdir_in("/tmp") - .expect("create short macOS network fixture path"); - let workspace = temporary.path().join("workspace"); - let run_home = temporary.path().join("runs"); - let outside_socket = temporary.path().join("host.sock"); - let loopback_listener = TcpListener::bind("127.0.0.1:0").unwrap(); - let loopback_port = loopback_listener.local_addr().unwrap().port(); - fs::create_dir(&workspace).unwrap(); - let _listener = UnixListener::bind(&outside_socket).unwrap(); - - let output = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .env("PERSISTING_RUN_HOME", &run_home) - .env("LOOPBACK_PORT", loopback_port.to_string()) - .args([ - "run", - "--overlaynet-deny-all", - "--stdio", - "capture", - "--overlayfs-compose", - ]) - .arg(&workspace) - .args(["--pass-env", "LOOPBACK_PORT"]) - .args([ - "--", - "/usr/bin/python3", - "-c", - r#"import errno, os, socket, sys -denied = (errno.EPERM, errno.EACCES) -assert os.environ["PERSISTING_SANDBOX_FILESYSTEM"] == "seatbelt-write" -assert os.environ["PERSISTING_SANDBOX_NETWORK"] == "deny" -agentctl = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) -agentctl.connect(os.environ["PERSISTING_AGENTCTL_ENDPOINT"]) -agentctl.close() - -try: - inet = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - inet_code = inet.connect_ex(("192.0.2.1", 9)) -except PermissionError as error: - inet_code = error.errno - -loopback = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -loopback_code = loopback.connect_ex(("127.0.0.1", int(os.environ["LOOPBACK_PORT"]))) - -try: - host = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - host_code = host.connect_ex(sys.argv[1]) -except PermissionError as error: - host_code = error.errno - -local = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) -local.bind(os.path.join(os.environ["TMPDIR"], "local.sock")) -local.close() -print(inet_code, loopback_code, host_code) -raise SystemExit(0 if inet_code in denied and loopback_code == 0 and host_code in denied else 1)"#, - ]) - .arg(&outside_socket) - .output() - .expect("run macOS deny-all profile"); - if !output.status.success() - && String::from_utf8_lossy(&output.stderr).contains("file system is not available") - { - eprintln!("skipping macOS deny-all test: macFUSE is installed but unavailable"); - return; - } - assert!( - output.status.success(), - "stdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - - let run = only_run(&run_home); - let bundle = RunBundle::read(&run).unwrap(); - assert_eq!( - bundle - .run - .executor - .as_ref() - .map(|executor| executor.isolation), - Some(IsolationKind::SandboxedProcess) - ); - assert!(bundle.safety.filesystem_write_non_bypassable); - assert!(bundle.safety.network_non_bypassable); - assert!( - bundle - .safety - .warnings - .iter() - .all(|warning| !warning.contains("direct sockets may bypass")) - ); -} diff --git a/crates/persisting-pvisor/tests/overlayfs_performance.rs b/crates/persisting-pvisor/tests/overlayfs_performance.rs deleted file mode 100644 index bd1ea1970..000000000 --- a/crates/persisting-pvisor/tests/overlayfs_performance.rs +++ /dev/null @@ -1,116 +0,0 @@ -#![cfg(all(target_os = "macos", not(debug_assertions)))] - -use std::fs::{self, File}; -use std::process::{Command, Stdio}; -use std::time::{Duration, Instant}; - -const DIRECTORY_COUNT: usize = 32; -const FILES_PER_DIRECTORY: usize = 32; -const LOWER_FILE_BYTES: u64 = 2 * 1024 * 1024; -const DEFAULT_MAX_ELAPSED: Duration = Duration::from_secs(10); - -/// End-to-end guard against making lower-file payload I/O part of readdir/getattr. -/// -/// Run manually with: -/// `cargo test --release -p persisting-pvisor --test overlayfs_performance -- --ignored --nocapture` -#[test] -#[ignore = "requires an enabled macFUSE kernel extension and measures wall-clock performance"] -fn recursive_lower_walk_does_not_materialize_file_payloads() { - let temporary = tempfile::tempdir().expect("create performance fixture root"); - let lower = temporary.path().join("lower"); - let stage = temporary.path().join("stage"); - create_large_lower_tree(&lower); - - let host_started = Instant::now(); - let host_output = Command::new("/usr/bin/find") - .arg(&lower) - .args(["-type", "f", "-print"]) - .stdout(Stdio::null()) - .stderr(Stdio::piped()) - .output() - .expect("execute host directory walk"); - let host_elapsed = host_started.elapsed(); - assert!( - host_output.status.success(), - "host find failed with {}: {}", - host_output.status, - String::from_utf8_lossy(&host_output.stderr) - ); - - let ambient_started = Instant::now(); - let ambient_output = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .args(["run", "--", "/usr/bin/find"]) - .arg(&lower) - .args(["-type", "f", "-print"]) - .stdout(Stdio::null()) - .stderr(Stdio::piped()) - .output() - .expect("execute pvisor host directory walk"); - let ambient_elapsed = ambient_started.elapsed(); - assert!( - ambient_output.status.success(), - "ambient pvisor failed with {}: {}", - ambient_output.status, - String::from_utf8_lossy(&ambient_output.stderr) - ); - - let started = Instant::now(); - let output = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .args(["run", "--overlayfs-compose"]) - .arg(&lower) - .arg("--stage") - .arg(&stage) - .args(["--", "/usr/bin/find", ".", "-type", "f", "-print"]) - .env("PERSISTING_RUN_HOME", temporary.path().join("runs")) - .stdout(Stdio::null()) - .stderr(Stdio::piped()) - .output() - .expect("execute pvisor overlay walk"); - let elapsed = started.elapsed(); - - assert!( - output.status.success(), - "pvisor failed with {}: {}", - output.status, - String::from_utf8_lossy(&output.stderr) - ); - let max_elapsed = std::env::var("PVISOR_PERF_MAX_MS") - .ok() - .and_then(|value| value.parse::().ok()) - .map(Duration::from_millis) - .unwrap_or(DEFAULT_MAX_ELAPSED); - assert!( - elapsed <= max_elapsed, - "recursive walk took {elapsed:?}, exceeding {max_elapsed:?}" - ); - - let upper_entries = fs::read_dir(stage.join("upper")) - .expect("read directory upper") - .count(); - assert!( - upper_entries == 0, - "read-only walk materialized {upper_entries} entries in the directory upper" - ); - assert!(lower.join("dir-00/file-0000.bin").is_file()); - - eprintln!( - "pvisor overlayfs perf: {} files, {} MiB apparent lower data, host={host_elapsed:?}, pvisor={ambient_elapsed:?}, overlay={elapsed:?}, pvisor_overhead={:?}, overlay_total_overhead={:?}", - DIRECTORY_COUNT * FILES_PER_DIRECTORY, - DIRECTORY_COUNT as u64 * FILES_PER_DIRECTORY as u64 * LOWER_FILE_BYTES / 1024 / 1024, - ambient_elapsed.saturating_sub(host_elapsed), - elapsed.saturating_sub(host_elapsed) - ); -} - -fn create_large_lower_tree(lower: &std::path::Path) { - for directory_index in 0..DIRECTORY_COUNT { - let directory = lower.join(format!("dir-{directory_index:02}")); - fs::create_dir_all(&directory).expect("create lower directory"); - for file_index in 0..FILES_PER_DIRECTORY { - let file = directory.join(format!("file-{file_index:04}.bin")); - File::create(file) - .and_then(|file| file.set_len(LOWER_FILE_BYTES)) - .expect("create sparse lower file"); - } - } -} diff --git a/crates/persisting-pvisor/tests/ppilot_supervisor.rs b/crates/persisting-pvisor/tests/ppilot_supervisor.rs deleted file mode 100644 index 03ecba25d..000000000 --- a/crates/persisting-pvisor/tests/ppilot_supervisor.rs +++ /dev/null @@ -1,61 +0,0 @@ -//! Cross-component Supervisor checks live with the execution plane so pPilot -//! never needs a compile-time dependency on pVisor. - -use persisting_agentctl::{RunInvocation, RunSpec, RunState, StdioMode}; -use persisting_ppilot::{EmbeddedSupervisor, EmbeddedSupervisorConfig}; -use persisting_pvisor::PVisor; -use std::time::Duration; - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn ppilot_supervisor_cancels_a_live_pvisor_attempt() { - let supervisor = EmbeddedSupervisor::start(EmbeddedSupervisorConfig::default()) - .await - .unwrap(); - let mut spec = RunSpec::process("supervisor-cancelled", "fixture-agent", "/bin/sh"); - spec.lease_epoch = 11; - spec.supervisor = Some(supervisor.bootstrap()); - let RunInvocation::Process(process) = &mut spec.invocation; - process.args = vec!["-c".into(), "sleep 10".into()]; - let handle = PVisor::new().run(spec).await.unwrap(); - - let deadline = tokio::time::Instant::now() + Duration::from_secs(2); - loop { - if supervisor - .registrations() - .await - .iter() - .any(|registration| registration.run_id.as_str() == "supervisor-cancelled") - { - break; - } - assert!(tokio::time::Instant::now() < deadline); - tokio::time::sleep(Duration::from_millis(10)).await; - } - supervisor - .cancel(&persisting_agentctl::RunId::new("supervisor-cancelled")) - .await - .unwrap(); - let result = tokio::time::timeout(Duration::from_secs(3), handle.wait()) - .await - .expect("Supervisor cancel did not stop the Run") - .unwrap(); - assert_eq!(result.state, RunState::Cancelled); - supervisor.shutdown().await.unwrap(); -} - -#[tokio::test] -async fn ppilot_supervisor_disconnect_does_not_abort_the_run() { - let supervisor = EmbeddedSupervisor::start(EmbeddedSupervisorConfig::default()) - .await - .unwrap(); - let mut spec = RunSpec::process("supervisor-disconnected", "fixture-agent", "/bin/sh"); - spec.supervisor = Some(supervisor.bootstrap()); - let RunInvocation::Process(process) = &mut spec.invocation; - process.args = vec!["-c".into(), "sleep 0.2; printf survived".into()]; - process.stdout = StdioMode::Capture; - let handle = PVisor::new().run(spec).await.unwrap(); - supervisor.shutdown().await.unwrap(); - let result = handle.wait().await.unwrap(); - assert_eq!(result.state, RunState::Completed); - assert_eq!(result.output.stdout.as_deref(), Some("survived")); -} diff --git a/crates/persisting-pvisor/tests/rootless_local.rs b/crates/persisting-pvisor/tests/rootless_local.rs deleted file mode 100644 index a3c522896..000000000 --- a/crates/persisting-pvisor/tests/rootless_local.rs +++ /dev/null @@ -1,838 +0,0 @@ -#![cfg(target_os = "linux")] - -use persisting_agentctl::{IsolationKind, RunFailureKind}; -use persisting_pvisor::RunBundle; -use persisting_pvisor::sandbox::SANDBOX_SETUP_EXIT_CODE; -use std::fs; -use std::fs::OpenOptions; -use std::net::{TcpListener, TcpStream}; -use std::os::fd::AsRawFd; -use std::os::unix::fs::PermissionsExt; -use std::os::unix::fs::symlink; -use std::os::unix::net::{UnixListener, UnixStream}; -use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; - -fn only_run(root: &Path) -> PathBuf { - if root.join("run-bundle.json").is_file() { - return root.to_path_buf(); - } - let runs = fs::read_dir(root) - .expect("read Run root") - .filter_map(Result::ok) - .map(|entry| entry.path()) - .filter(|path| path.join("run-bundle.json").is_file()) - .collect::>(); - assert_eq!(runs.len(), 1, "expected one finalized Run in {root:?}"); - runs.into_iter().next().unwrap() -} - -fn stage_root(run_home: &Path) -> PathBuf { - run_home - .parent() - .expect("temporary run root has a parent") - .join("stage") -} - -fn setup_failure(root: &Path) -> Option { - let root = if root.exists() { - root.to_path_buf() - } else { - stage_root(root) - }; - if root.join("run-bundle.json").is_file() { - return RunBundle::read(&root).ok()?.run.output.stderr; - } - let run = fs::read_dir(root) - .ok()? - .filter_map(Result::ok) - .map(|entry| entry.path()) - .find(|path| path.join("run-bundle.json").is_file())?; - let bundle = RunBundle::read(&run).ok()?; - bundle.run.output.stderr -} - -fn user_namespaces_are_unavailable(stderr: &str) -> bool { - const CONTEXT: &str = "initialize rootless user and mount namespaces: "; - stderr.lines().any(|line| { - let Some((_, error)) = line.split_once(CONTEXT) else { - return false; - }; - error == "unshare user namespace: Operation not permitted (os error 1)" - || error == "unshare user namespace: Permission denied (os error 13)" - }) -} - -fn skip_if_user_namespaces_are_explicitly_optional( - run_home: &Path, - output: &std::process::Output, -) -> bool { - if std::env::var_os("PERSISTING_TEST_ALLOW_NO_USERNS").is_none() || output.status.success() { - return false; - } - let combined = String::from_utf8_lossy(&output.stderr); - // Safe-best-effort intentionally falls back to the host process when the - // runner cannot create namespaces. Treat that capability result as a - // skippable environment condition, just like the launcher setup error. - if combined.contains("falling back to host process") { - eprintln!("skipping: the test host disables required namespaces"); - return true; - } - let Some(stderr) = setup_failure(run_home) else { - return false; - }; - if !user_namespaces_are_unavailable(&stderr) { - if !stderr.is_empty() { - eprintln!("rootless sandbox failure was not skippable: {stderr}"); - } - return false; - } - eprintln!("skipping: the test host disables unprivileged user namespaces: {stderr}"); - true -} - -fn skip_if_rootless_runtime_is_explicitly_optional() -> bool { - if std::env::var_os("PERSISTING_TEST_ALLOW_NO_USERNS").is_none() { - return false; - } - let available = Command::new("unshare") - .args(["--user", "--mount", "--pid", "--fork", "true"]) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .is_ok_and(|status| status.success()); - if !available { - eprintln!("skipping: the test host disables the rootless PID namespace"); - } - !available -} - -#[test] -fn safe_local_executable_cannot_escape_the_workspace() { - let temporary = tempfile::tempdir().unwrap(); - let workspace = temporary.path().join("workspace"); - let outside = temporary.path().join("outside"); - let run_home = temporary.path().join("runs"); - fs::create_dir_all(&workspace).unwrap(); - fs::create_dir_all(&outside).unwrap(); - fs::write(outside.join("secret.txt"), b"host-secret").unwrap(); - symlink(&outside, workspace.join("escape-link")).unwrap(); - - let script = r#" -set -eu -printf 'staged' > staged.txt -if cat "$OUTSIDE_SECRET" >/dev/null 2>&1; then - echo 'outside read unexpectedly succeeded' >&2 - exit 40 -fi -if printf 'escaped' > "$OUTSIDE_WRITE" 2>/dev/null; then - echo 'outside write unexpectedly succeeded' >&2 - exit 41 -fi -if cat escape-link/secret.txt >/dev/null 2>&1; then - echo 'symlink escape read unexpectedly succeeded' >&2 - exit 42 -fi -if printf 'escaped' > escape-link/symlink-escaped.txt 2>/dev/null; then - echo 'symlink escape write unexpectedly succeeded' >&2 - exit 43 -fi -if cat "/proc/self/root$OUTSIDE_SECRET" >/dev/null 2>&1; then - echo 'proc root escape unexpectedly succeeded' >&2 - exit 44 -fi -printf '%s:%s:%s\n' "$PERSISTING_SANDBOX_FILESYSTEM" "$PERSISTING_SANDBOX_LANDLOCK_ABI" "$PERSISTING_SANDBOX_USER_NAMESPACE" -"#; - let output = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .env("PERSISTING_RUN_HOME", &run_home) - .env("OUTSIDE_SECRET", outside.join("secret.txt")) - .env("OUTSIDE_WRITE", outside.join("escaped.txt")) - .args([ - "run", - "--stdio", - "capture", - "--stage", - temporary.path().join("stage").to_str().unwrap(), - "--pass-env", - "OUTSIDE_SECRET", - "--pass-env", - "OUTSIDE_WRITE", - ]) - .current_dir(&workspace) - .args(["--", "/bin/sh", "-c", script]) - .output() - .unwrap(); - - if skip_if_user_namespaces_are_explicitly_optional(&run_home, &output) { - return; - } - assert!( - output.status.success(), - "stdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - assert!(!outside.join("escaped.txt").exists()); - assert!(!outside.join("symlink-escaped.txt").exists()); - assert!(!workspace.join("staged.txt").exists()); - - let run = only_run(&stage_root(&run_home)); - let bundle = RunBundle::read(&run).unwrap(); - assert_eq!( - bundle - .run - .executor - .as_ref() - .map(|executor| executor.isolation), - Some(IsolationKind::RootlessProcess) - ); - assert!(bundle.safety.filesystem_non_bypassable); - assert!(bundle.safety.filesystem_changes_staged); - assert!(!bundle.safety.network_non_bypassable); - assert!( - bundle - .safety - .warnings - .iter() - .any(|warning| warning.contains("process-tree cleanup")) - ); - assert!( - bundle - .safety - .warnings - .iter() - .all(|warning| !warning.contains("host PID namespace")) - ); - let filesystem = bundle - .filesystem - .as_ref() - .expect("staged filesystem summary"); - assert!(filesystem.upper.join("staged.txt").is_file()); - assert!(filesystem.changed_files >= 1); - assert!( - bundle - .run - .output - .stdout - .as_deref() - .is_some_and(|stdout| stdout.contains("landlock:") && stdout.ends_with(":1\n")), - "captured output: {:?}", - bundle.run.output.stdout - ); -} - -#[test] -fn safe_local_executable_cannot_mutate_outside_metadata() { - let temporary = tempfile::tempdir().unwrap(); - let workspace = temporary.path().join("workspace"); - let outside = temporary.path().join("outside"); - let run_home = temporary.path().join("runs"); - fs::create_dir_all(&workspace).unwrap(); - fs::create_dir_all(&outside).unwrap(); - let protected = outside.join("protected.txt"); - fs::write(&protected, b"host-content").unwrap(); - fs::set_permissions(&protected, fs::Permissions::from_mode(0o640)).unwrap(); - - let script = r#" -set -eu -if chmod 000 "$OUTSIDE_FILE" 2>/dev/null; then exit 60; fi -if rm "$OUTSIDE_FILE" 2>/dev/null; then exit 61; fi -if mv "$OUTSIDE_FILE" stolen.txt 2>/dev/null; then exit 62; fi -if ln "$OUTSIDE_FILE" hardlink.txt 2>/dev/null; then exit 63; fi -if mkdir "$OUTSIDE_DIR/created" 2>/dev/null; then exit 64; fi -if touch -r "$OUTSIDE_FILE" timestamp-copy 2>/dev/null; then exit 65; fi -printf metadata-denied -"#; - let output = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .env("PERSISTING_RUN_HOME", &run_home) - .env("OUTSIDE_FILE", &protected) - .env("OUTSIDE_DIR", &outside) - .args([ - "run", - "--stdio", - "capture", - "--stage", - temporary.path().join("stage").to_str().unwrap(), - "--pass-env", - "OUTSIDE_FILE", - "--pass-env", - "OUTSIDE_DIR", - ]) - .current_dir(&workspace) - .args(["--", "/bin/sh", "-c", script]) - .output() - .unwrap(); - - if skip_if_user_namespaces_are_explicitly_optional(&run_home, &output) { - return; - } - assert!( - output.status.success(), - "stdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - assert_eq!(fs::read(&protected).unwrap(), b"host-content"); - assert_eq!( - fs::metadata(&protected).unwrap().permissions().mode() & 0o777, - 0o640 - ); - assert!(!outside.join("created").exists()); - assert!(!workspace.join("stolen.txt").exists()); - assert!(!workspace.join("hardlink.txt").exists()); - assert!(!workspace.join("timestamp-copy").exists()); - - let bundle = RunBundle::read(&only_run(&stage_root(&run_home))).unwrap(); - assert!(bundle.safety.filesystem_read_non_bypassable); - assert!(bundle.safety.filesystem_write_non_bypassable); - assert!( - bundle - .run - .output - .stdout - .as_deref() - .is_some_and(|stdout| stdout == "metadata-denied") - ); -} - -#[test] -fn safe_run_selectively_applies_then_drops_remaining_changes() { - let temporary = tempfile::tempdir().unwrap(); - let workspace = temporary.path().join("workspace"); - let run_home = temporary.path().join("runs"); - fs::create_dir_all(workspace.join("src")).unwrap(); - fs::create_dir_all(workspace.join("tests")).unwrap(); - - let output = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .env("PERSISTING_RUN_HOME", &run_home) - .args(["run", "--stdio", "capture", "--stage"]) - .arg(temporary.path().join("stage")) - .current_dir(&workspace) - .args([ - "--", - "/bin/sh", - "-c", - "printf promoted > src/promote.txt; printf deferred > tests/defer.txt", - ]) - .output() - .unwrap(); - - if skip_if_user_namespaces_are_explicitly_optional(&run_home, &output) { - return; - } - assert!( - output.status.success(), - "stdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - assert!(!workspace.join("src/promote.txt").exists()); - assert!(!workspace.join("tests/defer.txt").exists()); - - let run = only_run(&stage_root(&run_home)); - let apply = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .env("PERSISTING_RUN_HOME", &run_home) - .args(["apply"]) - .arg(&run) - .args(["--path", "src"]) - .output() - .unwrap(); - assert!( - apply.status.success(), - "selective apply failed: {}", - String::from_utf8_lossy(&apply.stderr) - ); - assert_eq!( - fs::read_to_string(workspace.join("src/promote.txt")).unwrap(), - "promoted" - ); - assert!(!workspace.join("tests/defer.txt").exists()); - - let status = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .env("PERSISTING_RUN_HOME", &run_home) - .args(["status"]) - .arg(&run) - .arg("--json") - .output() - .unwrap(); - assert!( - status.status.success(), - "status failed: {}", - String::from_utf8_lossy(&status.stderr) - ); - let status: serde_json::Value = serde_json::from_slice(&status.stdout).unwrap(); - assert_eq!(status["apply_history"].as_array().unwrap().len(), 1); - assert_eq!(status["filesystem"]["state"], "staged"); - assert!( - status["filesystem"]["sample_paths"] - .as_array() - .unwrap() - .iter() - .any(|path| path.as_str().is_some_and(|path| path.contains("defer.txt"))) - ); - - let drop = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .env("PERSISTING_RUN_HOME", &run_home) - .arg("drop") - .arg(&run) - .output() - .unwrap(); - assert!( - drop.status.success(), - "drop failed: {}", - String::from_utf8_lossy(&drop.stderr) - ); - assert!(!workspace.join("tests/defer.txt").exists()); - - let status = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .env("PERSISTING_RUN_HOME", &run_home) - .args(["status"]) - .arg(&run) - .arg("--json") - .output() - .unwrap(); - assert!(status.status.success()); - let status: serde_json::Value = serde_json::from_slice(&status.stdout).unwrap(); - assert_eq!(status["filesystem"]["state"], "discarded"); - assert_eq!(status["filesystem"]["changed_files"], 0); - assert_eq!(status["apply_history"].as_array().unwrap().len(), 1); -} - -#[test] -fn safe_apply_refuses_to_overwrite_a_concurrently_changed_target() { - let temporary = tempfile::tempdir().unwrap(); - let workspace = temporary.path().join("workspace"); - let run_home = temporary.path().join("runs"); - fs::create_dir_all(&workspace).unwrap(); - fs::write(workspace.join("value.txt"), b"original").unwrap(); - - let output = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .env("PERSISTING_RUN_HOME", &run_home) - .args(["run", "--stdio", "capture", "--stage"]) - .arg(temporary.path().join("stage")) - .current_dir(&workspace) - .args(["--", "/bin/sh", "-c", "printf staged > value.txt"]) - .output() - .unwrap(); - if skip_if_user_namespaces_are_explicitly_optional(&run_home, &output) { - return; - } - assert!(output.status.success()); - assert_eq!(fs::read(workspace.join("value.txt")).unwrap(), b"original"); - fs::write(workspace.join("value.txt"), b"concurrent").unwrap(); - - let run = only_run(&stage_root(&run_home)); - let apply = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .env("PERSISTING_RUN_HOME", &run_home) - .arg("apply") - .arg(&run) - .output() - .unwrap(); - assert!(!apply.status.success()); - assert!( - String::from_utf8_lossy(&apply.stderr).contains("target changed after staging"), - "stderr: {}", - String::from_utf8_lossy(&apply.stderr) - ); - assert_eq!( - fs::read(workspace.join("value.txt")).unwrap(), - b"concurrent" - ); - let bundle = RunBundle::read(&run).unwrap(); - assert_eq!( - fs::read(bundle.filesystem.as_ref().unwrap().upper.join("value.txt")).unwrap(), - b"staged" - ); -} - -#[test] -fn safe_launcher_closes_inherited_host_file_descriptors() { - let temporary = tempfile::tempdir().unwrap(); - let workspace = temporary.path().join("workspace"); - let run_home = temporary.path().join("runs"); - let secret_path = temporary.path().join("descriptor-secret.txt"); - fs::create_dir_all(&workspace).unwrap(); - fs::write(&secret_path, b"descriptor-secret").unwrap(); - - let secret = OpenOptions::new().read(true).open(&secret_path).unwrap(); - let leaked_fd = secret.as_raw_fd(); - let flags = unsafe { libc::fcntl(leaked_fd, libc::F_GETFD) }; - assert!(flags >= 0, "read descriptor flags"); - let cleared = unsafe { libc::fcntl(leaked_fd, libc::F_SETFD, flags & !libc::FD_CLOEXEC) }; - assert_eq!(cleared, 0, "make the fixture descriptor inheritable"); - - // An already-connected socket would bypass pathname and connect-time - // policy if it survived exec, so exercise it independently from the file. - let listener = TcpListener::bind("127.0.0.1:0").unwrap(); - let inherited_socket = TcpStream::connect(listener.local_addr().unwrap()).unwrap(); - let socket_fd = inherited_socket.as_raw_fd(); - let socket_flags = unsafe { libc::fcntl(socket_fd, libc::F_GETFD) }; - assert!(socket_flags >= 0, "read socket descriptor flags"); - let cleared = - unsafe { libc::fcntl(socket_fd, libc::F_SETFD, socket_flags & !libc::FD_CLOEXEC) }; - assert_eq!(cleared, 0, "make the fixture socket inheritable"); - - let output = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .env("PERSISTING_RUN_HOME", &run_home) - .env("PERSISTING_LEAKED_FD", leaked_fd.to_string()) - .env("PERSISTING_LEAKED_SOCKET_FD", socket_fd.to_string()) - .args([ - "run", - "--stdio", - "capture", - "--stage", - temporary.path().join("stage").to_str().unwrap(), - "--overlaynet-deny-all", - "--pass-env", - "PERSISTING_LEAKED_FD", - "--pass-env", - "PERSISTING_LEAKED_SOCKET_FD", - ]) - .current_dir(&workspace) - .arg("--") - .arg(std::env::current_exe().unwrap()) - .args([ - "--ignored", - "--exact", - "inherited_fd_probe_agent", - "--nocapture", - ]) - .output() - .unwrap(); - - if skip_if_user_namespaces_are_explicitly_optional(&run_home, &output) { - return; - } - assert!( - output.status.success(), - "stdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - let bundle = RunBundle::read(&only_run(&stage_root(&run_home))).unwrap(); - assert!(bundle.safety.filesystem_non_bypassable); - assert!( - bundle - .run - .output - .stdout - .as_deref() - .is_some_and(|stdout| stdout.contains("inherited descriptors closed")) - ); -} - -/// Re-enter this test binary as the untrusted Agent. The parent deliberately -/// passes an inheritable descriptor through pVisor; the trusted launcher must -/// close it before executing this function. -#[test] -#[ignore] -fn inherited_fd_probe_agent() { - for name in ["PERSISTING_LEAKED_FD", "PERSISTING_LEAKED_SOCKET_FD"] { - let fd = std::env::var(name) - .unwrap_or_else(|_| panic!("missing {name}")) - .parse::() - .expect("numeric descriptor"); - let result = unsafe { libc::fcntl(fd, libc::F_GETFD) }; - assert_eq!(result, -1, "host descriptor {fd} remained open"); - assert_eq!( - std::io::Error::last_os_error().raw_os_error(), - Some(libc::EBADF), - "closed descriptor should report EBADF" - ); - assert!( - !PathBuf::from(format!("/proc/self/fd/{fd}")).exists(), - "closed descriptor remained reachable through procfs" - ); - } - println!("inherited descriptors closed"); -} - -#[test] -fn denied_network_uses_a_private_network_namespace() { - let temporary = tempfile::tempdir().unwrap(); - let workspace = temporary.path().join("workspace"); - let run_home = temporary.path().join("runs"); - fs::create_dir_all(&workspace).unwrap(); - let listener = TcpListener::bind("127.0.0.1:0").unwrap(); - let host_port = listener.local_addr().unwrap().port().to_string(); - - let script = r#" -set -eu -test "$PERSISTING_SANDBOX_NETWORK" = deny -if exec 3<>"/dev/tcp/127.0.0.1/${HOST_PORT}"; then - echo 'host listener unexpectedly reachable' >&2 - exit 50 -fi -printf 'network:%s\n' "$PERSISTING_SANDBOX_NETWORK" -"#; - let output = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .env("PERSISTING_RUN_HOME", &run_home) - .env("HOST_PORT", host_port) - .args([ - "run", - "--stdio", - "capture", - "--stage", - temporary.path().join("stage").to_str().unwrap(), - "--overlaynet-deny-all", - "--pass-env", - "HOST_PORT", - ]) - .current_dir(&workspace) - .args(["--", "/bin/bash", "-c", script]) - .output() - .unwrap(); - - if skip_if_user_namespaces_are_explicitly_optional(&run_home, &output) { - return; - } - assert!( - output.status.success(), - "stdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - - let run = only_run(&stage_root(&run_home)); - let bundle = RunBundle::read(&run).unwrap(); - assert!(bundle.safety.network_non_bypassable); - assert!( - bundle - .run - .output - .stdout - .as_deref() - .is_some_and(|stdout| stdout.contains("network:deny")), - "captured output: {:?}", - bundle.run.output.stdout - ); - assert!( - bundle - .safety - .warnings - .iter() - .all(|warning| !warning.contains("direct sockets may bypass")), - "safety warnings: {:?}", - bundle.safety.warnings - ); -} - -#[test] -fn synthetic_root_hides_ungranted_host_unix_sockets() { - let temporary = tempfile::tempdir().unwrap(); - let workspace = temporary.path().join("workspace"); - let run_home = temporary.path().join("runs"); - fs::create_dir_all(&workspace).unwrap(); - let host_socket = temporary.path().join("host-control.sock"); - let _listener = UnixListener::bind(&host_socket).unwrap(); - - let output = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .env("PERSISTING_RUN_HOME", &run_home) - .env("PERSISTING_SOCKET_PROBE", &host_socket) - .env("SSH_AUTH_SOCK", &host_socket) - .args(["run", "--stdio", "capture", "--stage"]) - .arg(temporary.path().join("stage")) - .current_dir(&workspace) - .arg("--") - .arg(std::env::current_exe().unwrap()) - .args(["--ignored", "--exact", "unix_socket_probe_agent"]) - .output() - .unwrap(); - - if skip_if_user_namespaces_are_explicitly_optional(&run_home, &output) { - return; - } - assert!( - output.status.success(), - "stdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - let bundle = RunBundle::read(&only_run(&stage_root(&run_home))).unwrap(); - if !bundle.safety.filesystem_non_bypassable - && String::from_utf8_lossy(&output.stderr).contains("falling back to host process") - { - eprintln!("skipping: synthetic root unavailable on this test host"); - return; - } - assert!(bundle.safety.filesystem_non_bypassable); -} - -#[test] -fn safe_run_reaps_setsid_double_fork_descendants_after_success() { - if skip_if_rootless_runtime_is_explicitly_optional() { - return; - } - let temporary = tempfile::tempdir().unwrap(); - let workspace = temporary.path().join("workspace"); - let run_home = temporary.path().join("runs"); - fs::create_dir_all(&workspace).unwrap(); - - let output = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .env("PERSISTING_RUN_HOME", &run_home) - .args(["run", "--stdio", "capture", "--stage"]) - .arg(temporary.path().join("stage")) - .current_dir(&workspace) - .arg("--") - .arg(std::env::current_exe().unwrap()) - .args(["--ignored", "--exact", "daemon_escape_probe_agent"]) - .output() - .unwrap(); - - if skip_if_user_namespaces_are_explicitly_optional(&run_home, &output) { - return; - } - assert!( - output.status.success(), - "stdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - let bundle = RunBundle::read(&only_run(&stage_root(&run_home))).unwrap(); - let socket = bundle - .filesystem - .as_ref() - .expect("staged filesystem") - .upper - .join("daemon.sock"); - assert!(socket.exists(), "daemon did not publish its socket"); - let error = UnixStream::connect(&socket) - .expect_err("setsid/double-fork descendant survived successful Run completion"); - assert!(matches!( - error.kind(), - std::io::ErrorKind::ConnectionRefused | std::io::ErrorKind::NotFound - )); -} - -/// Re-enter this integration-test executable as an Agent that deliberately -/// escapes its process group and double-forks a long-lived Unix listener. -#[test] -#[ignore] -fn daemon_escape_probe_agent() { - let status = Command::new("/usr/bin/setsid") - .arg("--fork") - .arg(std::env::current_exe().unwrap()) - .args(["--ignored", "--exact", "daemon_listener_agent"]) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .expect("launch setsid daemon"); - assert!(status.success(), "setsid launcher failed: {status}"); - for _ in 0..500 { - if Path::new("daemon.ready").is_file() { - return; - } - std::thread::sleep(std::time::Duration::from_millis(10)); - } - panic!("daemon did not become ready"); -} - -#[test] -#[ignore] -fn daemon_listener_agent() { - let listener = UnixListener::bind("daemon.sock").expect("bind daemon socket"); - fs::write("daemon.ready", b"ready").expect("publish daemon readiness"); - loop { - match listener.accept() { - Ok((_stream, _address)) => {} - Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {} - Err(error) => panic!("daemon listener failed: {error}"), - } - } -} - -#[test] -fn sandboxed_agent_may_legitimately_exit_with_reserved_launcher_code() { - let temporary = tempfile::tempdir().unwrap(); - let workspace = temporary.path().join("workspace"); - let run_home = temporary.path().join("runs"); - fs::create_dir_all(&workspace).unwrap(); - - let output = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .env("PERSISTING_RUN_HOME", &run_home) - .args(["run", "--stdio", "capture", "--stage"]) - .arg(temporary.path().join("stage")) - .current_dir(&workspace) - .args(["--", "/bin/sh", "-c", "exit 125"]) - .output() - .unwrap(); - if skip_if_user_namespaces_are_explicitly_optional(&run_home, &output) { - return; - } - - assert!(!output.status.success()); - let bundle = RunBundle::read(&only_run(&stage_root(&run_home))).unwrap(); - assert_eq!(bundle.run.exit_code, Some(SANDBOX_SETUP_EXIT_CODE)); - assert_eq!( - bundle.run.failure.as_ref().map(|failure| failure.kind), - Some(RunFailureKind::ProcessExit) - ); - assert!( - bundle - .run - .warnings - .iter() - .all(|warning| warning != "pvisor.sandbox.setup_failed") - ); -} - -/// Re-enter this integration-test executable as the untrusted Agent so the -/// socket regression does not depend on Python, curl, socat, or netcat. -#[test] -#[ignore] -fn unix_socket_probe_agent() { - let Some(path) = std::env::var_os("PERSISTING_SOCKET_PROBE") else { - return; - }; - let error = UnixStream::connect(path).expect_err("ungranted host socket must be hidden"); - assert_eq!(error.kind(), std::io::ErrorKind::NotFound); - - let inherited_ssh_agent = - std::env::var_os("SSH_AUTH_SOCK").expect("the Agent should still see inherited metadata"); - let error = UnixStream::connect(inherited_ssh_agent) - .expect_err("ambient SSH signing authority must not be projected"); - assert_eq!(error.kind(), std::io::ErrorKind::NotFound); - - let agentctl = std::env::var_os(persisting_pvisor::AGENTCTL_ENDPOINT_ENV) - .expect("pVisor must inject its Run-scoped AgentCtl endpoint"); - UnixStream::connect(agentctl).expect("the explicitly projected AgentCtl socket must work"); -} - -#[test] -fn internal_launcher_reports_setup_failure_with_reserved_status() { - let output = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .arg("__pvisor-sandbox-exec") - .arg("--") - .arg("/bin/true") - .env("PERSISTING_INTERNAL_SANDBOX_PLAN", "not-json") - .output() - .unwrap(); - - assert_eq!(output.status.code(), Some(SANDBOX_SETUP_EXIT_CODE)); - assert!( - String::from_utf8_lossy(&output.stderr).contains("decode rootless sandbox plan"), - "stderr: {}", - String::from_utf8_lossy(&output.stderr) - ); -} - -#[test] -fn namespace_setup_failure_classifier_is_narrow() { - assert!(user_namespaces_are_unavailable( - "pVisor local sandbox setup failed: initialize rootless user and mount namespaces: unshare user namespace: Operation not permitted (os error 1)" - )); - assert!(user_namespaces_are_unavailable( - "pVisor local sandbox setup failed: initialize rootless user and mount namespaces: unshare user namespace: Permission denied (os error 13)" - )); - assert!(!user_namespaces_are_unavailable( - "apply Landlock rules: Permission denied (os error 13)" - )); - assert!(!user_namespaces_are_unavailable( - "initialize rootless user and mount namespaces: No such file or directory (os error 2)" - )); - assert!(!user_namespaces_are_unavailable( - "initialize rootless user and mount namespaces: unshare mount namespace: Operation not permitted (os error 1)" - )); -} diff --git a/crates/persisting-pvisor/tests/run_config_cli.rs b/crates/persisting-pvisor/tests/run_config_cli.rs deleted file mode 100644 index 96faccd4d..000000000 --- a/crates/persisting-pvisor/tests/run_config_cli.rs +++ /dev/null @@ -1,537 +0,0 @@ -use std::{net::TcpListener, process::Command}; - -use persisting_pvisor::{RecordFormat, RunBundle, RunConfig}; - -#[cfg(unix)] -use std::os::unix::fs::PermissionsExt; - -fn only_run_dir(run_home: &std::path::Path) -> std::path::PathBuf { - let runs = std::fs::read_dir(run_home) - .expect("list Run Home") - .map(|entry| entry.expect("read Run Home entry").path()) - .filter(|path| { - path.file_name() - .and_then(|name| name.to_str()) - .is_some_and(|name| name.starts_with("run-")) - }) - .collect::>(); - assert_eq!(runs.len(), 1, "expected exactly one Run directory"); - runs.into_iter().next().unwrap() -} - -#[test] -fn network_run_uses_the_current_workspace_and_external_run_home() { - let temporary = tempfile::Builder::new() - .prefix("pv") - .tempdir_in("/tmp") - .expect("create short temporary run root"); - let listener = TcpListener::bind("127.0.0.1:0").expect("reserve a loopback port"); - let listen = listener.local_addr().unwrap().to_string(); - let run_home = temporary.path().join("runs"); - drop(listener); - let output = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .args(["run", "--overlaynet-listen"]) - .arg(&listen) - .args(["--overlaynet-deny-all", "--", "/usr/bin/true"]) - .env("PERSISTING_RUN_HOME", &run_home) - .output() - .expect("execute network-only pvisor without an explicit workspace"); - assert!( - output.status.success(), - "pvisor failed: {}", - String::from_utf8_lossy(&output.stderr) - ); - - let run_dir = only_run_dir(&run_home); - - let record: serde_json::Value = serde_json::from_slice( - &std::fs::read(run_dir.join("run.json")).expect("read finalized Run record"), - ) - .expect("decode finalized Run record"); - assert_eq!(record["state"], "completed"); - assert_eq!(record["command"][0], "/usr/bin/true"); - let bundle = RunBundle::read(&run_dir).expect("read generated Run Bundle"); - assert_eq!(bundle.run.exit_code, Some(0)); - assert!(bundle.network.interception.is_some()); -} - -#[test] -fn toml_and_cli_share_one_run_configuration() { - let temporary = tempfile::tempdir().expect("create CLI fixture"); - let workspace = temporary.path().join("workspace"); - std::fs::create_dir(&workspace).unwrap(); - let run_home = temporary.path().join("runs"); - let config_path = temporary.path().join("run.toml"); - - let mut config = RunConfig::default(); - config.run.agent = "from-toml".into(); - config.run.command = vec!["/usr/bin/false".into()]; - std::fs::write(&config_path, toml::to_string_pretty(&config).unwrap()).unwrap(); - - let output = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .args(["run", "--spec"]) - .arg(&config_path) - .args(["--name", "from-cli", "--", "/usr/bin/true"]) - .current_dir(&workspace) - .env("PERSISTING_RUN_HOME", &run_home) - .output() - .expect("execute pvisor from TOML plus CLI overrides"); - assert!( - output.status.success(), - "pvisor failed: {}", - String::from_utf8_lossy(&output.stderr) - ); - - let run_dir = only_run_dir(&run_home); - let record: serde_json::Value = - serde_json::from_slice(&std::fs::read(run_dir.join("run.json")).unwrap()).unwrap(); - assert_eq!(record["agent"], "from-cli"); - assert_eq!(record["command"][0], "/usr/bin/true"); - assert_eq!(record["stage_dir"], serde_json::Value::Null); - - assert_eq!( - record["workspace"], - workspace.canonicalize().unwrap().display().to_string() - ); - let bundle = RunBundle::read(&run_dir).expect("read generated Run Bundle"); - assert_eq!(bundle.run.run_id, record["run_id"]); - assert_eq!(bundle.run.agent, "from-cli"); - assert!(bundle.safety.safe_profile_requested); - - let review = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .args(["review", "--json"]) - .arg(&workspace) - .env("PERSISTING_RUN_HOME", &run_home) - .output() - .expect("review generated Run Bundle"); - assert!( - review.status.success(), - "pvisor review failed: {}", - String::from_utf8_lossy(&review.stderr) - ); - let reviewed: serde_json::Value = serde_json::from_slice(&review.stdout).unwrap(); - assert_eq!(reviewed["schema_version"], 2); - assert_eq!(reviewed["run"]["agent"], "from-cli"); -} - -#[test] -fn one_workspace_accepts_multiple_independent_runs() { - let temporary = tempfile::tempdir().expect("create CLI fixture"); - let workspace = temporary.path().join("workspace"); - let run_home = temporary.path().join("runs"); - std::fs::create_dir(&workspace).unwrap(); - - for _ in 0..2 { - let output = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .args(["run", "--", "/usr/bin/true"]) - .current_dir(&workspace) - .env("PERSISTING_RUN_HOME", &run_home) - .output() - .expect("execute pVisor Run"); - assert!( - output.status.success(), - "pvisor failed: {}", - String::from_utf8_lossy(&output.stderr) - ); - } - - let records = std::fs::read_dir(&run_home) - .unwrap() - .filter_map(Result::ok) - .filter(|entry| entry.path().join("run.json").is_file()) - .count(); - assert_eq!(records, 2); - - let status = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .args(["status"]) - .current_dir(&workspace) - .env("PERSISTING_RUN_HOME", &run_home) - .output() - .expect("resolve latest Run from reusable workspace"); - assert!( - status.status.success(), - "status failed: {}", - String::from_utf8_lossy(&status.stderr) - ); - - let review = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .args(["review", "last"]) - .current_dir(&workspace) - .env("PERSISTING_RUN_HOME", &run_home) - .output() - .expect("resolve last Run in reusable workspace"); - assert!( - review.status.success(), - "review failed: {}", - String::from_utf8_lossy(&review.stderr) - ); -} - -#[test] -fn current_directory_selects_the_host_process_working_directory() { - let temporary = tempfile::tempdir().expect("create CLI fixture"); - let workspace = temporary.path().join("workspace"); - let run_home = temporary.path().join("runs"); - std::fs::create_dir(&workspace).unwrap(); - - let output = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .args(["run", "--", "/bin/pwd"]) - .current_dir(&workspace) - .env("PERSISTING_RUN_HOME", &run_home) - .output() - .expect("execute pVisor in selected workspace"); - assert!(output.status.success()); - assert_eq!( - String::from_utf8_lossy(&output.stdout).trim(), - workspace.canonicalize().unwrap().display().to_string() - ); -} - -#[test] -fn record_destination_survives_toml_round_trip() { - let mut config = RunConfig::default(); - config.record.format = RecordFormat::Lance; - config.record.destination = Some("s3://trajectory-bucket/pvisor/轨迹".into()); - - let encoded = toml::to_string_pretty(&config).expect("serialize RunConfig"); - let decoded: RunConfig = toml::from_str(&encoded).expect("deserialize RunConfig"); - assert_eq!(decoded.record.format, RecordFormat::Lance); - assert_eq!( - decoded.record.destination.as_deref(), - Some(std::path::Path::new("s3://trajectory-bucket/pvisor/轨迹")) - ); -} - -#[test] -#[ignore = "requires a built pchronicle sidecar binary"] -fn run_accepts_portable_object_store_chronicle_sink() { - let temporary = tempfile::tempdir().expect("create CLI fixture"); - let workspace = temporary.path().join("workspace"); - std::fs::create_dir(&workspace).unwrap(); - let run_home = temporary.path().join("runs"); - let uri = format!( - "shared-memory://pvisor-chronicle-{}-{}/runs", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_nanos() - ); - - let output = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .args(["run", "--record-format", "lance", "--record-destination"]) - .arg(&uri) - .args(["--", "/usr/bin/true"]) - .current_dir(&workspace) - .env("PERSISTING_RUN_HOME", &run_home) - .output() - .expect("execute pvisor with object-store pChronicle sink"); - assert!( - output.status.success(), - "pvisor failed: {}", - String::from_utf8_lossy(&output.stderr) - ); - - let run_dir = only_run_dir(&run_home); - let bundle = RunBundle::read(&run_dir).expect("read generated Run Bundle"); - assert_eq!(bundle.run.exit_code, Some(0)); - assert!(bundle.run.failure.is_none()); - let record: serde_json::Value = - serde_json::from_slice(&std::fs::read(run_dir.join("run.json")).unwrap()).unwrap(); - assert_eq!(record["state"], "completed"); -} - -#[cfg(unix)] -#[test] -#[cfg_attr( - target_os = "macos", - ignore = "temporary OCI control mount is not visible to nested pVisor on macOS" -)] -fn container_executor_runs_through_an_oci_compatible_control_surface() { - let temporary = tempfile::tempdir().expect("create CLI fixture"); - let workspace = temporary.path().join("workspace"); - std::fs::create_dir(&workspace).unwrap(); - let run_home = temporary.path().join("runs"); - let runtime = temporary.path().join("fake-oci"); - std::fs::write( - &runtime, - r#"#!/bin/sh -set -eu -bundle="" -while [ "$#" -gt 0 ]; do - case "$1" in - --bundle) bundle="$2"; shift 2 ;; - *) shift ;; - esac -done -spec=$(find "$(dirname "$bundle")" -name run-spec.json -print -quit) -control=$(dirname "$spec") -exec "$PERSISTING_TEST_PVISOR" run --executor host \ - --spec "$control/run-spec.json" \ - --result-file "$control/run-result.json" -"#, - ) - .unwrap(); - std::fs::set_permissions(&runtime, std::fs::Permissions::from_mode(0o755)).unwrap(); - - let output = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .args(["run", "--executor", "container", "--container-runtime"]) - .arg(&runtime) - .args(["--container-pvisor-binary", env!("CARGO_BIN_EXE_pvisor")]) - .args(["--rootfs"]) - .arg(&workspace) - .args([ - "--container-platform", - "linux/amd64", - "--container-network", - "none", - "--", - "/bin/sh", - "-c", - "test \"$PERSISTING_PVISOR_RUNTIME\" = 1 && printf container-ok", - ]) - .current_dir(&workspace) - .env("PERSISTING_TEST_PVISOR", env!("CARGO_BIN_EXE_pvisor")) - .env("PERSISTING_RUN_HOME", &run_home) - .output() - .expect("execute pvisor with fake OCI runtime"); - assert!( - output.status.success(), - "pvisor failed: {}", - String::from_utf8_lossy(&output.stderr) - ); - // The fake runtime validates the OCI control surface; delegated stdout is - // intentionally persisted in the nested Run Bundle rather than forwarded - // by this transport fixture. - - let run_dir = only_run_dir(&run_home); - let record: serde_json::Value = - serde_json::from_slice(&std::fs::read(run_dir.join("run.json")).unwrap()).unwrap(); - assert_eq!(record["executor"]["kind"], "container"); - assert_eq!(record["executor"]["isolation"], "container"); - let bundle = RunBundle::read(&run_dir).unwrap(); - assert!(!bundle.safety.host_process); - assert_eq!( - bundle - .run - .executor - .as_ref() - .map(|executor| executor.name.as_str()), - Some("oci-pvisor") - ); -} - -#[cfg(unix)] -#[test] -fn container_executor_deadline_stops_the_runtime_client() { - let temporary = tempfile::tempdir().expect("create CLI fixture"); - let workspace = temporary.path().join("workspace"); - std::fs::create_dir(&workspace).unwrap(); - let run_home = temporary.path().join("runs"); - let runtime = temporary.path().join("fake-oci"); - std::fs::write( - &runtime, - r#"#!/bin/sh -if [ "$1" = "run" ]; then - shift - control="" - while [ "$1" != "fixture-image" ]; do - if [ "$1" = "--mount" ]; then - shift - case "$1" in - *target=/run/persisting*) - control=$(printf '%s' "$1" | sed -e 's/^.*source=//' -e 's/,target=.*$//') - ;; - esac - fi - shift - done - shift - exec "$PERSISTING_TEST_PVISOR" run --executor host \ - --spec "$control/run-spec.json" \ - --result-file "$control/run-result.json" -fi -exit 0 -"#, - ) - .unwrap(); - std::fs::set_permissions(&runtime, std::fs::Permissions::from_mode(0o755)).unwrap(); - - let started = std::time::Instant::now(); - let output = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .args(["run", "--timeout", "20ms", "--container-runtime"]) - .arg(&runtime) - .args(["--container-pvisor-binary", env!("CARGO_BIN_EXE_pvisor")]) - .args([ - "--container-image", - "fixture-image", - "--container-platform", - "linux/amd64", - "--container-network", - "none", - "--", - "/bin/sleep", - "30", - ]) - .current_dir(&workspace) - .env("PERSISTING_TEST_PVISOR", env!("CARGO_BIN_EXE_pvisor")) - .env("PERSISTING_RUN_HOME", &run_home) - .output() - .expect("execute deadline-bound container Run"); - assert!(!output.status.success()); - assert!( - started.elapsed() < std::time::Duration::from_secs(20), - "container deadline cleanup took {:?}", - started.elapsed() - ); - - let run_dir = only_run_dir(&run_home); - let bundle = RunBundle::read(&run_dir).unwrap(); - assert_eq!(bundle.run.state, persisting_agentctl::RunState::Failed); - let failure_kind = bundle.run.failure.as_ref().map(|failure| failure.kind); - // Sandboxed CI runners may prohibit executing helper scripts from the - // temporary directory; that setup failure is still a valid transport - // termination result for this fixture. - if failure_kind == Some(persisting_agentctl::RunFailureKind::Spawn) { - // accepted when the runner blocks temporary executable files - } else { - assert_eq!( - failure_kind, - Some(persisting_agentctl::RunFailureKind::DeadlineExceeded) - ); - } - assert!(!bundle.safety.host_process); -} - -#[test] -fn every_public_run_option_is_accepted_by_the_real_cli_parser() { - let cases: &[&[&str]] = &[ - &["--spec", "config.toml"], - &["--result-file", "result.json"], - &["--stage", "runs/task"], - &["--stage", "drop"], - &["--stage", "drop:/tmp/task"], - &["--name", "smoke"], - &["--executor", "host"], - &["--executor", "container"], - &["--executor", "vm"], - &["--vm"], - &["--rootfs", "host"], - &["--rootfs", "/tmp/rootfs"], - &["--rootfs", "image=/tmp/image"], - &["--image-store", "/tmp/images"], - &["--vm-library-dir", "/tmp/libkrunfw"], - &["--memory", "256MiB"], - &["--mem", "256MiB"], - &["--cpu", "2"], - &["--strict"], - &["--timeout", "1s"], - &["--stdio", "capture"], - &["--pass-env", "PATH"], - &["--max-processes", "8"], - &["--max-cpu-time", "5s"], - &["--max-open-files", "32"], - &["--max-file-size", "1MiB"], - &["--max-stage-size", "2GiB"], - &["--container-runtime", "runc"], - &["--container-image", "alpine:latest"], - &["--container-rootfs", "/tmp/rootfs"], - &["--container-pvisor-binary", "/tmp/pvisor"], - &["--container-platform", "linux/amd64"], - &["--container-network", "none"], - &["--container-workdir", "/workspace"], - &["--container-user", "1000:1000"], - &["--container-read-only-rootfs"], - &["--container-mount", "source=\"/tmp\",target=\"/workspace\""], - &["--overlayfs-path", "/workspace"], - &["--overlayfs-compose", "/tmp/lower"], - &["--overlayfs-backend", "directory"], - &["--overlayfs-commit", "manual"], - &["--overlaynet", "proxy"], - &["--overlaynet", "auto"], - &["--overlaynet"], - &["--overlaynet-listen", "127.0.0.1:18080"], - &["--overlaynet-allow", "example.com:443"], - &["--overlaynet-deny", "10.0.0.0/8"], - &["--overlaynet-limit", "example.com=1mbps"], - &["--overlaynet-deny-all"], - &["--gateway-mode", "capture"], - &["--gateway-admin-listen", "127.0.0.1:19090"], - &["--gateway-level", "full"], - &["--gateway-session-header", "X-Session-ID"], - &["--gateway-debug"], - &["--gateway-stream-markdown"], - &[ - "--gateway-route", - "name=\"default\",upstream=\"https://example.com\"", - ], - &["--record-format", "json"], - &["--record-destination", "/tmp/events.jsonl"], - ]; - // `--help` short-circuits before execution, so this exercises the parser - // over the whole public surface without starting a Run. - for options in cases { - let output = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .arg("run") - .args(*options) - .arg("--help") - .output() - .expect("run pvisor CLI parser"); - assert!( - output.status.success(), - "CLI rejected {:?}: {}", - options, - String::from_utf8_lossy(&output.stderr) - ); - } -} - -fn advertised_run_options() -> std::collections::BTreeSet { - let output = Command::new(env!("CARGO_BIN_EXE_pvisor")) - .args(["run", "--help"]) - .output() - .expect("render pvisor run --help"); - assert!(output.status.success()); - let help = String::from_utf8_lossy(&output.stdout); - let options = help - .split(|character: char| !character.is_ascii_alphanumeric() && character != '-') - .filter(|token| token.starts_with("--") && token.len() > 2) - .map(str::to_owned) - .collect::>(); - for anchor in ["--executor", "--stage", "--strict"] { - assert!( - options.contains(anchor), - "help scraping is broken: {anchor} is missing from {options:?}" - ); - } - options -} - -#[test] -fn removed_run_options_stay_off_the_cli_surface() { - // `run` takes a trailing var arg that allows hyphen values, so an unknown - // `--flag` joins the Agent command instead of failing to parse. Exit codes - // cannot separate a removed option from a resurrected one; the rendered - // option list can. - let advertised = advertised_run_options(); - for option in [ - "--safe", - "--workspace", - "--config", - "--run-spec", - "--run-home", - "--agent", - "--host-rootfs", - "--max-file-size-bytes", - "--timeout-ms", - "--max-cpu-time-ms", - "--overlaynet-mode", - "--overlayfs-base", - "--overlayfs-target", - ] { - assert!( - !advertised.contains(option), - "`pvisor run --help` advertises the removed option {option} again" - ); - } -} diff --git a/crates/persisting-replay/Cargo.toml b/crates/persisting-replay/Cargo.toml deleted file mode 100644 index e43ccdd9c..000000000 --- a/crates/persisting-replay/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "persisting-replay" -version.workspace = true -edition.workspace = true -authors.workspace = true -license.workspace = true -description = "Agent-native sandbox replay and continuation for pVisor" - -[dependencies] -anyhow.workspace = true -chrono.workspace = true -fs2.workspace = true -libc.workspace = true -axum = { workspace = true, features = ["http1", "json", "tokio"] } -reqwest = { workspace = true, features = ["json", "rustls-tls", "stream"] } -serde = { workspace = true, features = ["derive"] } -serde_json.workspace = true -sha2.workspace = true -tokio = { workspace = true, features = ["net", "rt-multi-thread", "sync", "time"] } -toml.workspace = true -uuid = { workspace = true, features = ["v4"] } - -[dev-dependencies] -tempfile.workspace = true diff --git a/crates/persisting-replay/assets/mini_swe_agent_runner.py b/crates/persisting-replay/assets/mini_swe_agent_runner.py deleted file mode 100644 index e44362b99..000000000 --- a/crates/persisting-replay/assets/mini_swe_agent_runner.py +++ /dev/null @@ -1,249 +0,0 @@ -"""Pinned mini-swe-agent 2.4.6 replay bridge, launched by the Rust engine.""" - -from __future__ import annotations - -import copy -import json -import os -import sys -import time -from pathlib import Path -from typing import Any - -SESSION_HEADER_NAME = "X-LiteLLM-Session-ID" - - -def _load(path: Path) -> dict[str, Any]: - value = json.loads(path.read_text(encoding="utf-8")) - if not isinstance(value, dict): - raise TypeError(f"{path} must contain an object") - return value - - -def _action_messages(messages: list[dict[str, Any]]) -> list[tuple[int, dict[str, Any]]]: - result = [] - for index, message in enumerate(messages): - extra = message.get("extra") - if isinstance(extra, dict) and isinstance(extra.get("actions"), list) and extra["actions"]: - result.append((index, message)) - return result - - -def _has_model_response(message: dict[str, Any]) -> bool: - extra = message.get("extra") - return isinstance(extra, dict) and isinstance(extra.get("response"), dict) - - -def _preserved_messages_between_actions( - messages: list[dict[str, Any]], - *, - previous_index: int, - next_index: int, - observation_count: int, -) -> list[dict[str, Any]]: - preserved: list[dict[str, Any]] = [] - skipped_observations = 0 - for message in messages[previous_index + 1 : next_index]: - extra = message.get("extra") - is_interrupt = isinstance(extra, dict) and bool(extra.get("interrupt_type")) - is_observation = ( - message.get("role") in {"tool", "user"} - or message.get("type") == "function_call_output" - ) - if skipped_observations < observation_count and is_observation and not is_interrupt: - skipped_observations += 1 - continue - preserved.append(copy.deepcopy(message)) - if skipped_observations != observation_count: - raise ValueError("native trajectory does not contain all observations for the previous action") - return preserved - - -def _fresh_observation( - action: dict[str, Any], output: dict[str, Any], duration_ms: int -) -> dict[str, Any]: - return { - "call_id": str(action.get("tool_call_id") or ""), - "content": output.get("output", ""), - "is_error": bool(output.get("exception_info")) or output.get("returncode") != 0, - "return_code": output.get("returncode"), - "duration_ms": duration_ms, - } - - -def _continue(agent: Any, output_path: Path) -> None: - from minisweagent.exceptions import FormatError, InterruptAgentFlow - - while not agent.messages or agent.messages[-1].get("role") != "exit": - try: - agent.step() - agent.n_consecutive_format_errors = 0 - except FormatError as exc: - agent.cost += exc.messages[0].get("extra", {}).get("cost", 0.0) - agent.n_consecutive_format_errors += 1 - limit = agent.config.max_consecutive_format_errors - if 0 < limit <= agent.n_consecutive_format_errors: - agent.add_messages( - *exc.messages, - { - "role": "exit", - "content": "RepeatedFormatError", - "extra": {"exit_status": "RepeatedFormatError", "submission": ""}, - }, - ) - else: - agent.add_messages(*exc.messages) - except InterruptAgentFlow as exc: - agent.add_messages(*exc.messages) - except Exception as exc: - agent.handle_uncaught_exception(exc) - raise - finally: - agent.save(output_path) - - -def run(request: dict[str, Any]) -> None: - from minisweagent.agents import get_agent - from minisweagent.environments import get_environment - from minisweagent.models import get_model - - source = _load(Path(request["source"])) - mode = str(request["mode"]) - if mode not in {"replay_only", "replay_and_continue"}: - raise ValueError(f"unsupported replay mode: {mode}") - after_step = int(request["after_step"]) - max_steps = request.get("max_steps") - if max_steps is not None: - max_steps = int(max_steps) - if max_steps < after_step or (mode == "replay_and_continue" and max_steps == after_step): - raise ValueError("max_steps does not leave the steps required by replay mode") - info = source["info"] - config = copy.deepcopy(info["config"]) - messages = source["messages"] - selected = _action_messages(messages)[:after_step] - if len(selected) != after_step: - raise ValueError("native trajectory does not contain the requested replay prefix") - - model_config = config["model"] - model_type = config.get("model_type") - if model_type: - model_config["model_class"] = model_type - if os.environ.get("MODEL_NAME"): - model_config["model_name"] = os.environ["MODEL_NAME"] - model_config["cost_tracking"] = "ignore_errors" - model_kwargs = model_config.setdefault("model_kwargs", {}) - headers = { - str(key): str(value) - for key, value in (model_kwargs.get("extra_headers") or {}).items() - if str(key).lower() != SESSION_HEADER_NAME.lower() - } - headers[SESSION_HEADER_NAME] = request["session_id"] - model_kwargs["extra_headers"] = headers - - environment_config = config["environment"] - environment_type = config.get("environment_type") - if environment_type: - environment_config["environment_class"] = environment_type - environment_config["cwd"] = request["workspace"] - - agent_config = config["agent"] - agent_type = config.get("agent_type") - if agent_type: - agent_config["agent_class"] = agent_type - agent_config["output_path"] = request["continued"] - if "mode" in agent_config: - agent_config["mode"] = "yolo" - if "confirm_exit" in agent_config: - agent_config["confirm_exit"] = False - if max_steps is not None: - agent_config["step_limit"] = max_steps - agent_config["cost_limit"] = 0 - - model = get_model(config=model_config) - environment = get_environment(environment_config, default_type="local") - agent = get_agent(model, environment, agent_config, default_type="default") - - first_action_index = selected[0][0] - agent.messages = copy.deepcopy(messages[:first_action_index]) - fresh: list[dict[str, Any]] = [] - previous: tuple[int, dict[str, Any]] | None = None - for message_index, original in selected: - if previous is not None: - previous_index, previous_message = previous - agent.add_messages( - *_preserved_messages_between_actions( - messages, - previous_index=previous_index, - next_index=message_index, - observation_count=len(previous_message["extra"]["actions"]), - ) - ) - assistant = copy.deepcopy(original) - agent.add_messages(assistant) - outputs = [] - for action in assistant["extra"]["actions"]: - started = time.monotonic() - output = environment.execute(action) - outputs.append(output) - fresh.append(_fresh_observation(action, output, int((time.monotonic() - started) * 1000))) - agent.add_messages( - *model.format_observation_messages(assistant, outputs, agent.get_template_vars()) - ) - previous = (message_index, original) - - source_prefix = messages[: selected[-1][0] + 1] - agent.n_calls = sum(_has_model_response(message) for message in source_prefix) - prefix_calls = agent.n_calls - agent.cost = sum(float((message.get("extra") or {}).get("cost") or 0) for message in source_prefix) - Path(request["observations"]).write_text( - json.dumps(fresh, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" - ) - reconstructed_path = Path(request["reconstructed"]) - continued_path = Path(request["continued"]) - agent.save(reconstructed_path) - boundary_user_prompt_injected = False - if mode == "replay_only": - phase = "replayed" - agent_status = "not_started" - continued_steps = 0 - trajectory_path = reconstructed_path - else: - boundary_user_prompt = request.get("boundary_user_prompt") - if boundary_user_prompt: - agent.add_messages({"role": "user", "content": str(boundary_user_prompt)}) - boundary_user_prompt_injected = True - _continue(agent, continued_path) - phase = "continued" - continued_steps = max(0, int(agent.n_calls) - int(prefix_calls)) - exit_status = "" - if agent.messages: - exit_status = str((agent.messages[-1].get("extra") or {}).get("exit_status") or "") - agent_status = ( - "max_steps" - if exit_status in {"LimitsExceeded", "StepLimitExceeded"} - else "completed" - ) - trajectory_path = continued_path - Path(request["result"]).write_text( - json.dumps( - { - "phase": phase, - "agent_status": agent_status, - "replayed_steps": len(selected), - "continued_steps": continued_steps, - "trajectory": str(trajectory_path), - "reconstructed": str(reconstructed_path), - "boundary_user_prompt_injected": boundary_user_prompt_injected, - }, - ensure_ascii=False, - indent=2, - ) - + "\n", - encoding="utf-8", - ) - - -if __name__ == "__main__": - if len(sys.argv) != 2: - raise SystemExit("runner expects one JSON request path") - run(_load(Path(sys.argv[1]))) diff --git a/crates/persisting-replay/assets/pi_agent_runner.mjs b/crates/persisting-replay/assets/pi_agent_runner.mjs deleted file mode 100644 index f353ab113..000000000 --- a/crates/persisting-replay/assets/pi_agent_runner.mjs +++ /dev/null @@ -1,305 +0,0 @@ -/** Pinned Pi 0.83.0 replay bridge, launched by the Rust engine. */ - -import fs from "node:fs"; -import path from "node:path"; -import { pathToFileURL } from "node:url"; - -// Pi runtimes may be launched with Node 16/18 in older sandboxes where the -// global structuredClone helper is unavailable. JSON is sufficient for the -// native event objects we copy here and keeps replay portable across runtimes. -const clone = globalThis.structuredClone ?? ((value) => JSON.parse(JSON.stringify(value))); - -function load(filename) { - return JSON.parse(fs.readFileSync(filename, "utf8")); -} - -function writeJson(filename, value) { - fs.mkdirSync(path.dirname(filename), { recursive: true, mode: 0o700 }); - fs.writeFileSync(filename, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 }); -} - -function writeJsonl(filename, values) { - fs.mkdirSync(path.dirname(filename), { recursive: true, mode: 0o700 }); - const body = values.map((value) => JSON.stringify(value)).join("\n"); - fs.writeFileSync(filename, body ? `${body}\n` : "", { mode: 0o600 }); -} - -function toolCalls(message) { - return (Array.isArray(message?.content) ? message.content : []).filter( - (part) => part?.type === "toolCall", - ); -} - -function assistantConfig(events) { - for (const event of events) { - if (event?.type === "turn_end" && event?.message?.role === "assistant") { - return event.message; - } - } - throw new Error("Pi source trajectory has no assistant turn"); -} - -function safeHeaders() { - const raw = process.env.PVISOR_PI_HEADERS_JSON; - if (!raw) return {}; - const headers = JSON.parse(raw); - if (!headers || typeof headers !== "object" || Array.isArray(headers)) { - throw new Error("PVISOR_PI_HEADERS_JSON must contain an object"); - } - for (const [name, value] of Object.entries(headers)) { - if (/authorization|api-key|x-api-key/i.test(name) || /[\0\r\n]/.test(`${name}${value}`)) { - throw new Error("PVISOR_PI_HEADERS_JSON contains an unsafe header"); - } - } - return Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, String(value)])); -} - -function modelSettings(request, events) { - const original = assistantConfig(events); - const model = process.env.MODEL_NAME || original.model; - if (!model) throw new Error("Pi Replay requires MODEL_NAME or a model in the source trajectory"); - const api = process.env.PVISOR_PI_API || original.api || "openai-completions"; - const thinking = request.disable_thinking - ? "off" - : process.env.PVISOR_PI_THINKING_LEVEL || "medium"; - const definition = { - id: model, - name: model, - reasoning: thinking !== "off", - input: ["text"], - }; - if (process.env.PVISOR_PI_CONTEXT_WINDOW) { - definition.contextWindow = Number.parseInt(process.env.PVISOR_PI_CONTEXT_WINDOW, 10); - } - if (process.env.PVISOR_PI_MAX_OUTPUT_TOKENS) { - definition.maxTokens = Number.parseInt(process.env.PVISOR_PI_MAX_OUTPUT_TOKENS, 10); - } - const headers = safeHeaders(); - if (request.session_id) headers["X-LiteLLM-Session-ID"] = request.session_id; - return { provider: "sweeval", model, api, thinking, definition, headers }; -} - -async function importPi(packageJson) { - const manifest = load(packageJson); - const exported = manifest?.exports?.["."]?.import; - if (manifest?.name !== "@earendil-works/pi-coding-agent" || typeof exported !== "string") { - throw new Error("Pi runtime package manifest has no ESM root export"); - } - const packageRoot = path.dirname(packageJson); - const entrypoint = path.resolve(packageRoot, exported); - if (!entrypoint.startsWith(`${packageRoot}${path.sep}`) || !fs.statSync(entrypoint).isFile()) { - throw new Error("Pi runtime ESM root export escapes the package or is not a file"); - } - return import(pathToFileURL(entrypoint).href); -} - -function freshToolResult(call, result, isError) { - return { - role: "toolResult", - toolCallId: call.id, - toolName: call.name, - content: result?.content ?? [], - details: result?.details ?? {}, - ...(result?.usage !== undefined ? { usage: result.usage } : {}), - ...(result?.addedToolNames?.length ? { addedToolNames: result.addedToolNames } : {}), - isError, - timestamp: Date.now(), - }; -} - -async function executeCall(tools, call) { - const tool = tools.get(call.name); - if (!tool) throw new Error(`Pi Replay profile does not support tool ${JSON.stringify(call.name)}`); - const started = Date.now(); - let result; - let isError = false; - try { - result = await tool.execute(call.id, call.arguments ?? {}, undefined, () => {}); - } catch (error) { - isError = true; - result = { - content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], - details: {}, - }; - } - const message = freshToolResult(call, result, isError); - const returnCode = - Number.isInteger(message.details?.exitCode) ? message.details.exitCode - : Number.isInteger(message.details?.exit_code) ? message.details.exit_code - : null; - return { - message, - observation: { - call_id: call.id, - content: message.content, - is_error: isError, - return_code: returnCode, - duration_ms: Date.now() - started, - details: message.details, - }, - }; -} - -async function run(request) { - const events = load(request.source); - if (!Array.isArray(events)) throw new Error("Pi replay source must contain an event array"); - if (!new Set(["replay_only", "replay_and_continue"]).has(request.mode)) { - throw new Error(`unsupported Pi replay mode: ${request.mode}`); - } - const pi = await importPi(request.package_json); - const settings = modelSettings(request, events); - const baseUrl = process.env.OPENAI_BASE_URL || process.env.OPENAI_API_BASE || process.env.LLM_BASE_URL; - const apiKey = process.env.OPENAI_API_KEY || process.env.LLM_API_KEY; - if (request.mode === "replay_and_continue" && (!baseUrl || !apiKey)) { - throw new Error("Pi Replay continuation requires an OpenAI-compatible base URL and API key"); - } - - fs.mkdirSync(request.config_dir, { recursive: true, mode: 0o700 }); - fs.mkdirSync(request.session_dir, { recursive: true, mode: 0o700 }); - if (baseUrl && apiKey) { - writeJson(path.join(request.config_dir, "models.json"), { - providers: { - [settings.provider]: { - baseUrl, - api: settings.api, - apiKey: "$OPENAI_API_KEY", - headers: settings.headers, - models: [settings.definition], - }, - }, - }); - } - writeJson(path.join(request.config_dir, "settings.json"), { - defaultProvider: settings.provider, - defaultModel: settings.model, - defaultThinkingLevel: settings.thinking, - }); - - const nativeTools = new Map(pi.createCodingTools(request.workspace).map((tool) => [tool.name, tool])); - const sessionManager = pi.SessionManager.create(request.workspace, request.session_dir, { - id: "pvisor-replay", - }); - const reconstructedEvents = []; - const observations = []; - let replayedBatches = 0; - let prefixTurns = 0; - - for (const event of events) { - if (event?.type === "message_end" && event?.message?.role === "user") { - sessionManager.appendMessage(clone(event.message)); - reconstructedEvents.push(clone(event)); - continue; - } - if (event?.type !== "turn_end" || event?.message?.role !== "assistant") continue; - const calls = toolCalls(event.message); - if (calls.length > 0 && replayedBatches >= request.after_step) break; - prefixTurns += 1; - const assistant = clone(event.message); - sessionManager.appendMessage(assistant); - const freshResults = []; - for (const call of calls) { - const fresh = await executeCall(nativeTools, call); - freshResults.push(fresh.message); - observations.push(fresh.observation); - sessionManager.appendMessage(fresh.message); - } - reconstructedEvents.push({ ...clone(event), toolResults: freshResults }); - if (calls.length > 0) { - replayedBatches += 1; - if (replayedBatches === request.after_step) break; - } - } - if (replayedBatches !== request.after_step || prefixTurns !== request.prefix_model_turns) { - throw new Error("Pi replay runner reconstructed a different boundary than the Rust plan"); - } - writeJsonl(request.reconstructed, reconstructedEvents); - writeJson(request.observations, observations); - - if (request.mode === "replay_only") { - writeJson(request.result, { - phase: "replayed", - agent_status: "not_started", - replayed_steps: replayedBatches, - continued_steps: 0, - trajectory: request.reconstructed, - reconstructed: request.reconstructed, - boundary_user_prompt_injected: false, - }); - return; - } - - const modelRuntime = await pi.ModelRuntime.create({ - authPath: path.join(request.config_dir, "auth.json"), - modelsPath: path.join(request.config_dir, "models.json"), - }); - const model = modelRuntime.getModel(settings.provider, settings.model); - if (!model) throw new Error(`Pi Replay could not resolve model ${settings.provider}/${settings.model}`); - const settingsManager = pi.SettingsManager.create(request.workspace, request.config_dir); - const resourceLoader = new pi.DefaultResourceLoader({ - cwd: request.workspace, - agentDir: request.config_dir, - settingsManager, - noExtensions: true, - noSkills: true, - noPromptTemplates: true, - noThemes: true, - noContextFiles: true, - }); - await resourceLoader.reload(); - const created = await pi.createAgentSession({ - cwd: request.workspace, - agentDir: request.config_dir, - modelRuntime, - model, - thinkingLevel: settings.thinking, - tools: ["read", "bash", "edit", "write"], - sessionManager, - settingsManager, - resourceLoader, - }); - const session = created.session; - const liveEvents = []; - let continuedSteps = 0; - let reachedLimit = false; - let terminalError = null; - const remaining = request.max_steps == null ? null : request.max_steps - prefixTurns; - const unsubscribe = session.subscribe((event) => { - liveEvents.push(clone(event)); - if (event.type === "turn_end") { - continuedSteps += 1; - if (event.message?.stopReason === "error") { - terminalError = event.message.errorMessage || "Pi model request failed"; - } - if (remaining !== null && continuedSteps >= remaining) { - reachedLimit = true; - session.agent.abort(); - } - } - }); - const boundaryPrompt = request.boundary_user_prompt; - if (boundaryPrompt) { - await session.prompt(String(boundaryPrompt), { - expandPromptTemplates: false, - source: "rpc", - }); - } else { - await session.agent.continue(); - } - unsubscribe(); - session.dispose(); - if (terminalError) throw new Error(terminalError); - if (continuedSteps === 0) throw new Error("Pi produced no live continuation turn"); - writeJsonl(request.continued, [...reconstructedEvents, ...liveEvents]); - writeJson(request.result, { - phase: "continued", - agent_status: reachedLimit ? "max_steps" : "completed", - replayed_steps: replayedBatches, - continued_steps: continuedSteps, - trajectory: request.continued, - reconstructed: request.reconstructed, - boundary_user_prompt_injected: Boolean(boundaryPrompt), - }); -} - -if (process.argv.length !== 3) throw new Error("Pi runner expects one JSON request path"); -await run(load(process.argv[2])); diff --git a/crates/persisting-replay/assets/swe_agent_runner.py b/crates/persisting-replay/assets/swe_agent_runner.py deleted file mode 100644 index a2eeacb2f..000000000 --- a/crates/persisting-replay/assets/swe_agent_runner.py +++ /dev/null @@ -1,174 +0,0 @@ -"""Pinned SWE-agent 1.1.0 replay-then-live bridge, launched by Rust.""" - -from __future__ import annotations - -import json -import os -import sys -from pathlib import Path -from typing import Any - -from sweagent.agent.agents import DefaultAgent -from sweagent.environment.swe_env import SWEEnv -from sweagent.run.run_single import RunSingleConfig -from swerex.deployment.config import get_deployment - - -class ReplayThenLiveModel: - def __init__( - self, - prefix: list[dict[str, Any]], - live_model: Any, - boundary_user_prompt: str | None, - ) -> None: - self.prefix = prefix - self.live_model = live_model - self.boundary_user_prompt = boundary_user_prompt - self.boundary_user_prompt_injected = False - self.index = 0 - self.live_calls = 0 - - @property - def stats(self) -> Any: - return self.live_model.stats - - @property - def config(self) -> Any: - return self.live_model.config - - def query(self, history: list[dict[str, Any]], **kwargs: Any) -> dict[str, Any]: - if self.index < len(self.prefix): - value = self.prefix[self.index] - self.index += 1 - result: dict[str, Any] = {"message": str(value.get("content") or "")} - if value.get("tool_calls") is not None: - result["tool_calls"] = value["tool_calls"] - if value.get("thinking_blocks") is not None: - result["thinking_blocks"] = value["thinking_blocks"] - return result - if self.boundary_user_prompt and not self.boundary_user_prompt_injected: - history.append({"role": "user", "content": self.boundary_user_prompt}) - self.boundary_user_prompt_injected = True - self.live_calls += 1 - return self.live_model.query(history, **kwargs) - - def __getattr__(self, name: str) -> Any: - return getattr(self.live_model, name) - - -def _config(raw: Any, workspace: str) -> RunSingleConfig: - if isinstance(raw, str): - raw = json.loads(raw) - if not isinstance(raw, dict): - raise TypeError("trajectory replay_config must be an object or encoded object") - value = json.loads(json.dumps(raw)) - value.setdefault("env", {}) - value["env"]["deployment"] = {"type": "local"} - value["env"]["repo"] = {"type": "preexisting", "repo_name": workspace} - model = value.setdefault("agent", {}).setdefault("model", {}) - model["api_key"] = os.environ["OPENAI_API_KEY"] - model["api_base"] = os.environ["OPENAI_BASE_URL"] - model_name = os.environ.get("MODEL_NAME") or os.environ.get("LLM_MODEL") - if model_name: - model["name"] = model_name - return RunSingleConfig.model_validate(value) - - -def main() -> int: - if len(sys.argv) != 2: - raise SystemExit("runner expects one JSON request path") - request = json.loads(Path(sys.argv[1]).read_text()) - mode = str(request["mode"]) - if mode not in {"replay_only", "replay_and_continue"}: - raise ValueError(f"unsupported replay mode: {mode}") - after_step = int(request["after_step"]) - max_steps = request.get("max_steps") - if max_steps is not None: - max_steps = int(max_steps) - if max_steps < after_step or (mode == "replay_and_continue" and max_steps == after_step): - raise ValueError("max_steps does not leave the steps required by replay mode") - source = json.loads(Path(request["trajectory"]).read_text()) - config = _config(source.get("replay_config"), request["workspace"]) - if getattr(config.agent, "type", None) != "default": - raise ValueError("SWE-agent retry configurations are unsupported for deterministic replay") - assistant = [item for item in source["history"] if item.get("role") == "assistant"] - prefix = assistant[:after_step] - if len(prefix) != after_step: - raise ValueError("SWE-agent history has fewer assistant actions than the cutoff") - - agent = DefaultAgent.from_config(config.agent) - live_model = agent.model - agent.model = ReplayThenLiveModel( - prefix, - live_model, - request.get("boundary_user_prompt"), - ) - agent.replay_config = config - environment = SWEEnv( - deployment=get_deployment(config.env.deployment), - repo=config.env.repo, - post_startup_commands=[], - ) - output_dir = Path(request["output_dir"]) - agent.setup( - env=environment, - problem_statement=config.problem_statement, - output_dir=output_dir, - ) - agent.replay_config = config - total_steps = 0 - done = False - while True: - if mode == "replay_only" and total_steps >= after_step: - break - if max_steps is not None and total_steps >= max_steps: - break - step_output = agent.step() - total_steps += 1 - agent.save_trajectory() - if total_steps == after_step: - Path(request["reconstructed"]).write_text( - json.dumps(agent.get_trajectory_data(), indent=2) + "\n" - ) - if total_steps >= after_step and step_output.done: - done = True - break - if agent.model.index != after_step: - raise RuntimeError( - f"SWE-agent replay consumed {agent.model.index} source actions, expected {after_step}" - ) - if mode == "replay_only" and agent.model.live_calls != 0: - raise RuntimeError("SWE-agent replay-only unexpectedly queried the live model") - data = agent.get_trajectory_data() - agent.save_trajectory() - continued_steps = max(0, total_steps - after_step) - if mode == "replay_only": - phase = "replayed" - agent_status = "not_started" - trajectory_path = Path(request["reconstructed"]) - else: - phase = "continued" - agent_status = "completed" if done else "max_steps" - trajectory_path = Path(request["continued"]) - trajectory_path.write_text(json.dumps(data, indent=2) + "\n") - Path(request["result"]).write_text( - json.dumps( - { - "phase": phase, - "agent_status": agent_status, - "replayed_steps": after_step, - "continued_steps": continued_steps, - "trajectory": str(trajectory_path), - "reconstructed": str(request["reconstructed"]), - "trajectory_steps": len(data["trajectory"]), - "boundary_user_prompt_injected": agent.model.boundary_user_prompt_injected, - }, - indent=2, - ) - + "\n" - ) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/crates/persisting-replay/src/adapter/claude_code.rs b/crates/persisting-replay/src/adapter/claude_code.rs deleted file mode 100644 index 5974cd522..000000000 --- a/crates/persisting-replay/src/adapter/claude_code.rs +++ /dev/null @@ -1,2592 +0,0 @@ -use std::collections::{BTreeMap, BTreeSet}; -use std::fs; -use std::path::{Path, PathBuf}; -use std::process::Command; -use std::time::{Duration, Instant}; - -use serde_json::{Value, json}; - -use super::{ - MAX_TOOL_OUTPUT_BYTES, RunContext, agent_command, check_boundary, sanitized_environment, - with_boundary_user_prompt_metadata, -}; -use crate::claude_bridge::ClaudeBridgeHandle; -use crate::claude_resume::ResumeTransportManifest; -use crate::error::{ReplayError, ReplayErrorKind, ResultExt}; -use crate::io::{atomic_write, atomic_write_json, canonicalize, read_regular_file, sha256}; -use crate::journal::Journal; -use crate::model::{ - AdapterPlan, FreshObservation, PlaybackRequest, ReplayMode, ReplayOutcome, ReplayPlan, - ToolBatch, ToolCall, -}; -use crate::process::{ProcessSpec, run_process}; - -const FRESH_CLAUDE_TOOLS: &[&str] = &["Bash", "Edit", "Glob", "Grep", "MultiEdit", "Read", "Write"]; -const STALE_CLAUDE_TOOLS: &[&str] = &[ - "Agent", - "TaskCreate", - "TaskGet", - "TaskList", - "TaskOutput", - "TaskUpdate", - "TodoWrite", -]; - -fn required_str<'a>(value: &'a Value, field: &str, context: &str) -> Result<&'a str, ReplayError> { - value - .get(field) - .and_then(Value::as_str) - .filter(|value| !value.is_empty()) - .ok_or_else(|| ReplayError::trajectory(format!("{context} has no {field}"))) -} - -pub(super) fn build(request: &PlaybackRequest) -> Result { - build_claude_plan(request).map(AdapterPlan::ClaudeCode) -} - -pub(super) fn execute( - plan: &ReplayPlan, - context: &RunContext<'_>, - journal: &mut Journal, -) -> Result { - run_claude(plan, context, journal) -} - -fn claude_boundary_tool_use_ids(plan: &ReplayPlan) -> Vec { - let Some(last_batch) = plan.batches.last() else { - return Vec::new(); - }; - let Some(events) = plan.native.get("events").and_then(Value::as_array) else { - return last_batch - .tool_calls - .iter() - .map(|call| call.call_id.clone()) - .collect(); - }; - let last_message_id = last_batch - .native - .get("assistant_index") - .and_then(Value::as_u64) - .and_then(|index| events.get(index as usize)) - .and_then(|event| event.get("message")) - .and_then(|message| message.get("id")) - .and_then(Value::as_str); - let mut grouped = plan - .batches - .iter() - .rev() - .take_while(|batch| { - batch - .native - .get("assistant_index") - .and_then(Value::as_u64) - .and_then(|index| events.get(index as usize)) - .and_then(|event| event.get("message")) - .and_then(|message| message.get("id")) - .and_then(Value::as_str) - == last_message_id - }) - .collect::>(); - grouped.reverse(); - grouped - .into_iter() - .flat_map(|batch| batch.tool_calls.iter().map(|call| call.call_id.clone())) - .collect() -} - -fn claude_canonical_messages(canonical: &str) -> Result, ReplayError> { - let events = canonical - .lines() - .enumerate() - .map(|(index, line)| { - serde_json::from_str::(line).replay_context( - ReplayErrorKind::Trajectory, - format!("invalid rebuilt Claude JSONL at line {}", index + 1), - ) - }) - .collect::, _>>()?; - let (chain_indices, _) = claude_active_chain(&events)?; - let source_prompt = chain_indices - .iter() - .map(|index| &events[*index]) - .find_map(|event| { - if event.get("type").and_then(Value::as_str) != Some("user") { - return None; - } - let content = event.get("message")?.get("content")?; - if content.as_array().is_some_and(|blocks| { - blocks - .iter() - .any(|block| block.get("type").and_then(Value::as_str) == Some("tool_result")) - }) { - return None; - } - Some(claude_render_text(content)) - }) - .ok_or_else(|| ReplayError::trajectory("Claude canonical chain has no initial prompt"))?; - let (turns, _) = parse_claude_turns(&events)?; - let mut messages = vec![json!({"role": "user", "content": source_prompt})]; - let mut index = 0; - while index < turns.len() { - let turn = &turns[index]; - let batch = turn.batch.as_ref().ok_or_else(|| { - ReplayError::trajectory("text-only Claude turn precedes the canonical replay boundary") - })?; - let message_id = batch - .native - .get("message_id") - .and_then(Value::as_str) - .ok_or_else(|| ReplayError::trajectory("Claude canonical turn has no message ID"))?; - let mut grouped = vec![(turn, batch)]; - while let Some(next) = turns.get(index + grouped.len()) { - let Some(next_batch) = next.batch.as_ref() else { - break; - }; - if next_batch.native.get("message_id").and_then(Value::as_str) != Some(message_id) { - break; - } - grouped.push((next, next_batch)); - } - - let mut assistant_blocks = Vec::new(); - let mut result_blocks = Vec::new(); - for (grouped_turn, grouped_batch) in &grouped { - assistant_blocks.extend(claude_canonical_assistant_content( - &events, - grouped_turn, - grouped_batch, - )?); - for call in &grouped_batch.tool_calls { - let mut result = json!({ - "type": "tool_result", - "tool_use_id": call.call_id, - "content": call.original_observation, - }); - if call.original_is_error { - result["is_error"] = Value::Bool(true); - } - result_blocks.push(result); - } - } - messages.push(json!({"role": "assistant", "content": assistant_blocks})); - messages.push(json!({"role": "user", "content": result_blocks})); - index += grouped.len(); - } - if messages.len() < 3 { - return Err(ReplayError::trajectory( - "Claude canonical messages do not end in a replayed tool result", - )); - } - Ok(messages) -} - -fn claude_canonical_assistant_content( - events: &[Value], - turn: &ParsedClaudeTurn, - batch: &ToolBatch, -) -> Result, ReplayError> { - let indices = batch - .native - .get("assistant_indices") - .and_then(Value::as_array) - .ok_or_else(|| ReplayError::trajectory("Claude canonical turn lost assistant events"))?; - let mut first_text = None; - let mut first_thinking = None; - let mut tools = BTreeMap::new(); - for index in indices { - let index = index - .as_u64() - .ok_or_else(|| ReplayError::trajectory("Claude assistant index is not an integer"))? - as usize; - for raw in events - .get(index) - .and_then(|event| event.get("message")) - .and_then(|message| message.get("content")) - .and_then(Value::as_array) - .into_iter() - .flatten() - { - match raw.get("type").and_then(Value::as_str) { - Some("text") if first_text.is_none() => first_text = Some(raw.clone()), - Some("thinking") if first_thinking.is_none() => first_thinking = Some(raw.clone()), - Some("tool_use") => { - if let Some(call_id) = raw.get("id").and_then(Value::as_str) { - tools - .entry(call_id.to_owned()) - .or_insert_with(|| raw.clone()); - } - } - Some("server_tool_use") => { - return Err(ReplayError::new( - ReplayErrorKind::UnsupportedVersion, - "Claude server_tool_use is not replayable", - )); - } - _ => {} - } - } - } - let reasoning = turn - .signature - .get("reasoning") - .and_then(Value::as_str) - .unwrap_or_default(); - let text = turn - .signature - .get("text") - .and_then(Value::as_str) - .unwrap_or_default(); - let mut output = Vec::new(); - if first_thinking.is_some() || !reasoning.is_empty() { - let mut block = first_thinking.unwrap_or_else(|| json!({"type": "thinking"})); - block["thinking"] = Value::String(reasoning.to_owned()); - output.push(block); - } - if first_text.is_some() || !text.is_empty() { - let mut block = first_text.unwrap_or_else(|| json!({"type": "text"})); - block["text"] = Value::String(text.to_owned()); - if !text.trim().is_empty() { - output.push(block); - } - } - for call in &batch.tool_calls { - output.push(tools.remove(&call.call_id).unwrap_or_else(|| { - json!({ - "type": "tool_use", - "id": call.call_id, - "name": call.name, - "input": call.arguments, - }) - })); - } - if output.is_empty() { - return Err(ReplayError::trajectory( - "Claude canonical assistant turn has no model-visible content", - )); - } - Ok(output) -} - -fn claude_render_text(content: &Value) -> String { - if let Some(text) = content.as_str() { - return text.to_owned(); - } - content - .as_array() - .into_iter() - .flatten() - .filter(|block| block.get("type").and_then(Value::as_str) == Some("text")) - .filter_map(|block| block.get("text").and_then(Value::as_str)) - .collect::>() - .join("\n") -} - -fn build_claude_plan(request: &PlaybackRequest) -> Result { - let raw = read_regular_file(&request.trajectory)?; - let text = std::str::from_utf8(&raw).replay_context( - ReplayErrorKind::Trajectory, - "Claude trajectory is not UTF-8", - )?; - let mut events = Vec::new(); - for (index, line) in text.lines().enumerate() { - if line.trim().is_empty() { - return Err(ReplayError::trajectory(format!( - "blank Claude native line at {}", - index + 1 - ))); - } - let value: Value = serde_json::from_str(line).replay_context( - ReplayErrorKind::Trajectory, - format!("invalid Claude native JSONL at line {}", index + 1), - )?; - if !value.is_object() { - return Err(ReplayError::trajectory(format!( - "Claude native line {} is not an object", - index + 1 - ))); - } - events.push(value); - } - let versions: BTreeSet<_> = events - .iter() - .filter_map(|event| event.get("version").and_then(Value::as_str)) - .collect(); - if versions != BTreeSet::from(["2.1.220"]) { - return Err(ReplayError::new( - ReplayErrorKind::UnsupportedVersion, - format!("Claude trajectory requires exact version 2.1.220; got {versions:?}"), - )); - } - let session_ids: BTreeSet<_> = events - .iter() - .filter_map(|event| event.get("sessionId").and_then(Value::as_str)) - .collect(); - if session_ids.len() != 1 { - return Err(ReplayError::trajectory( - "Claude trajectory must contain exactly one session ID", - )); - } - let session_id = session_ids.iter().next().unwrap().to_string(); - let (turns, active_chain_uuids) = parse_claude_turns(&events)?; - let mut batches = Vec::new(); - for (turn_index, turn) in turns.iter().enumerate() { - let Some(batch) = &turn.batch else { - continue; - }; - let mut batch = batch.clone(); - batch.ordinal = batches.len() + 1; - batch.native["turn_index"] = json!(turn_index); - batches.push(batch); - } - check_boundary(request.after_step, batches.len())?; - for batch in batches.iter().take(request.after_step) { - for call in &batch.tool_calls { - let allow_stale = - request.mode == ReplayMode::PrepareOnly || request.allow_stale_observations; - validate_claude_tool_policy(call, allow_stale)?; - } - } - let boundary_turn_index = batches[request.after_step - 1].native["turn_index"] - .as_u64() - .ok_or_else(|| ReplayError::trajectory("Claude batch lost its logical turn index"))? - as usize; - let original_next_action = turns - .get(boundary_turn_index + 1) - .map(|turn| turn.signature.clone()); - batches.truncate(request.after_step); - let boundary_result_index = batches - .last() - .and_then(|batch| batch.native.get("terminal_result_index")) - .and_then(Value::as_u64) - .ok_or_else(|| ReplayError::trajectory("Claude boundary has no terminal result"))? - as usize; - Ok(ReplayPlan { - agent: request.agent, - source_path: canonicalize( - &request.trajectory, - ReplayErrorKind::Trajectory, - "trajectory", - )?, - source_sha256: sha256(&raw), - after_step: request.after_step, - prefix_model_turns: batches.len(), - batches, - native: json!({ - "events": events, - "session_id": session_id, - "active_chain_uuids": active_chain_uuids, - "boundary_result_index": boundary_result_index, - }), - original_next_action, - }) -} - -#[derive(Debug)] -struct ParsedClaudeTurn { - signature: Value, - batch: Option, -} - -fn parse_claude_turns( - events: &[Value], -) -> Result<(Vec, Vec), ReplayError> { - let (chain_indices, chain_uuids) = claude_active_chain(events)?; - let chain_positions: BTreeMap<_, _> = chain_uuids - .iter() - .enumerate() - .map(|(position, uuid)| (uuid.clone(), position)) - .collect(); - let result_index = claude_tool_result_index(events)?; - let mut seen_call_ids = BTreeSet::new(); - let mut turns = Vec::new(); - let mut cursor = 0; - - while cursor < chain_indices.len() { - let event_index = chain_indices[cursor]; - let event = &events[event_index]; - if event.get("type").and_then(Value::as_str) != Some("assistant") { - cursor += 1; - continue; - } - let message = event.get("message").unwrap_or(&Value::Null); - if !matches!( - message.get("role").and_then(Value::as_str), - None | Some("assistant") - ) { - return Err(ReplayError::trajectory(format!( - "Claude assistant event {event_index} has an invalid role" - ))); - } - let message_id = required_str(message, "id", "Claude assistant message")?.to_owned(); - let mut assistant_indices: Vec = Vec::new(); - while cursor < chain_indices.len() { - let candidate_index = chain_indices[cursor]; - let candidate = &events[candidate_index]; - if candidate.get("type").and_then(Value::as_str) != Some("assistant") { - if assistant_indices.is_empty() { - break; - } - let previous = &events[*assistant_indices.last().unwrap()]; - let response_incomplete = previous - .get("message") - .and_then(|value| value.get("stop_reason")) - .is_none_or(Value::is_null); - if !response_incomplete { - break; - } - let mut lookahead = cursor; - while lookahead < chain_indices.len() - && events[chain_indices[lookahead]] - .get("type") - .and_then(Value::as_str) - != Some("assistant") - { - lookahead += 1; - } - if lookahead >= chain_indices.len() - || events[chain_indices[lookahead]] - .get("message") - .and_then(|value| value.get("id")) - .and_then(Value::as_str) - != Some(message_id.as_str()) - { - break; - } - cursor = lookahead; - continue; - } - if candidate - .get("message") - .and_then(|value| value.get("id")) - .and_then(Value::as_str) - != Some(message_id.as_str()) - { - break; - } - assistant_indices.push(candidate_index); - cursor += 1; - } - - let mut text = String::new(); - let mut reasoning = String::new(); - let mut calls = Vec::new(); - let mut terminal_results: BTreeMap = BTreeMap::new(); - let mut assistant_chain_positions = Vec::new(); - for &assistant_index in &assistant_indices { - let assistant_event = &events[assistant_index]; - let assistant_uuid = required_str(assistant_event, "uuid", "Claude assistant event")?; - if let Some(position) = chain_positions.get(assistant_uuid) { - assistant_chain_positions.push(*position); - } - for block in assistant_event - .get("message") - .and_then(|value| value.get("content")) - .and_then(Value::as_array) - .into_iter() - .flatten() - { - match block.get("type").and_then(Value::as_str) { - Some("thinking") => merge_claude_streamed_text( - &mut reasoning, - block - .get("thinking") - .and_then(Value::as_str) - .unwrap_or_default(), - ), - Some("text") => merge_claude_streamed_text( - &mut text, - block - .get("text") - .and_then(Value::as_str) - .unwrap_or_default(), - ), - Some("server_tool_use") => { - return Err(ReplayError::new( - ReplayErrorKind::UnsupportedVersion, - "Claude server_tool_use is not replayable", - )); - } - Some("tool_use") => { - let call_id = required_str(block, "id", "Claude tool_use")?.to_owned(); - if !seen_call_ids.insert(call_id.clone()) { - return Err(ReplayError::trajectory(format!( - "duplicate Claude tool_use id {call_id}" - ))); - } - // Keep later malformed/unsupported calls in the parsed timeline so a - // valid earlier boundary remains replayable. Prefix validation above - // still rejects them if the user selects one for execution. - let name = block - .get("name") - .and_then(Value::as_str) - .unwrap_or_default() - .to_owned(); - let arguments = block - .get("input") - .filter(|input| input.is_object()) - .cloned() - .ok_or_else(|| { - ReplayError::trajectory(format!( - "Claude tool input for {call_id} is not an object" - )) - })?; - let (result_event_index, result) = - result_index.get(&call_id).ok_or_else(|| { - ReplayError::trajectory(format!( - "active Claude tool_use {call_id} has no tool_result" - )) - })?; - let result_event = &events[*result_event_index]; - let result_uuid = - required_str(result_event, "uuid", "Claude tool_result event")?; - if required_str(result_event, "parentUuid", "Claude tool_result event")? - != assistant_uuid - || required_str( - result_event, - "sourceToolAssistantUUID", - "Claude tool_result event", - )? != assistant_uuid - { - return Err(ReplayError::trajectory(format!( - "Claude tool_result {call_id} does not point to its tool_use event" - ))); - } - if let Some(position) = chain_positions.get(result_uuid) { - terminal_results - .entry(result_uuid.to_owned()) - .or_insert((*position, *result_event_index)); - } - calls.push(ToolCall { - ordinal: calls.len() + 1, - call_id, - name, - arguments, - original_observation: result - .get("content") - .cloned() - .unwrap_or(Value::Null), - original_is_error: result - .get("is_error") - .and_then(Value::as_bool) - .unwrap_or(false), - native: json!({ - "assistant_index": assistant_index, - "assistant_uuid": assistant_uuid, - "result_index": result_event_index, - "result_uuid": result_uuid, - }), - }); - } - _ => {} - } - } - } - - let signature = json!({ - "text": text, - "reasoning": reasoning, - "tools": calls.iter().map(|call| json!({ - "name": call.name, - "arguments": call.arguments, - })).collect::>(), - }); - let batch = if calls.is_empty() { - None - } else { - if terminal_results.is_empty() { - return Err(ReplayError::trajectory(format!( - "Claude assistant turn {message_id} has no tool_result on its active chain" - ))); - } - let mut ordered_results = terminal_results.values().copied().collect::>(); - ordered_results.sort_unstable(); - for (result_position, _) in ordered_results.iter().take(ordered_results.len() - 1) { - if !assistant_chain_positions - .iter() - .any(|assistant_position| assistant_position > result_position) - { - return Err(ReplayError::trajectory(format!( - "Claude assistant turn {message_id} has ambiguous terminal results" - ))); - } - } - let terminal_result_index = ordered_results.last().unwrap().1; - Some(ToolBatch { - ordinal: 0, - native_locator: format!("event:{}", assistant_indices[0]), - assistant_text: signature["text"].as_str().unwrap_or_default().to_owned(), - tool_calls: calls, - native: json!({ - "assistant_index": assistant_indices[0], - "assistant_indices": assistant_indices, - "message_id": message_id, - "terminal_result_index": terminal_result_index, - }), - }) - }; - turns.push(ParsedClaudeTurn { signature, batch }); - } - if turns.is_empty() { - return Err(ReplayError::trajectory( - "active Claude native chain contains no assistant turns", - )); - } - Ok((turns, chain_uuids)) -} - -fn claude_active_chain(events: &[Value]) -> Result<(Vec, Vec), ReplayError> { - let mut events_by_uuid = BTreeMap::new(); - for (index, event) in events.iter().enumerate() { - if !main_event(event) { - continue; - } - let uuid = required_str(event, "uuid", "Claude main event")?.to_owned(); - if events_by_uuid.insert(uuid.clone(), index).is_some() { - return Err(ReplayError::trajectory(format!( - "duplicate Claude main-chain UUID {uuid}" - ))); - } - } - let leaf_uuid = events - .iter() - .rev() - .find(|event| { - main_event(event) - && matches!( - event.get("type").and_then(Value::as_str), - Some("assistant" | "user") - ) - }) - .and_then(|event| event.get("uuid")) - .and_then(Value::as_str) - .ok_or_else(|| ReplayError::trajectory("Claude session has no active leaf"))? - .to_owned(); - let mut reversed_indices = Vec::new(); - let mut reversed_uuids = Vec::new(); - let mut seen = BTreeSet::new(); - let mut cursor = Some(leaf_uuid); - while let Some(uuid) = cursor { - if !seen.insert(uuid.clone()) { - return Err(ReplayError::trajectory(format!( - "cycle in Claude UUID parent chain at {uuid}" - ))); - } - let index = *events_by_uuid.get(&uuid).ok_or_else(|| { - ReplayError::trajectory(format!("Claude active chain lost parent {uuid}")) - })?; - reversed_indices.push(index); - reversed_uuids.push(uuid); - cursor = match events[index].get("parentUuid") { - None | Some(Value::Null) => None, - Some(Value::String(parent)) if !parent.is_empty() => Some(parent.clone()), - _ => { - return Err(ReplayError::trajectory(format!( - "Claude event {index} has an invalid parentUuid" - ))); - } - }; - } - reversed_indices.reverse(); - reversed_uuids.reverse(); - Ok((reversed_indices, reversed_uuids)) -} - -fn claude_tool_result_index( - events: &[Value], -) -> Result, ReplayError> { - let mut results = BTreeMap::new(); - for (event_index, event) in events.iter().enumerate() { - if !main_event(event) || event.get("type").and_then(Value::as_str) != Some("user") { - continue; - } - if !matches!( - event - .get("message") - .and_then(|message| message.get("role")) - .and_then(Value::as_str), - None | Some("user") - ) { - return Err(ReplayError::trajectory(format!( - "Claude user event {event_index} has an invalid role" - ))); - } - for block in event - .get("message") - .and_then(|message| message.get("content")) - .and_then(Value::as_array) - .into_iter() - .flatten() - { - if block.get("type").and_then(Value::as_str) != Some("tool_result") { - continue; - } - let call_id = required_str(block, "tool_use_id", "Claude tool_result")?.to_owned(); - if results - .insert(call_id.clone(), (event_index, block.clone())) - .is_some() - { - return Err(ReplayError::trajectory(format!( - "duplicate Claude tool_result for {call_id}" - ))); - } - } - } - Ok(results) -} - -fn merge_claude_streamed_text(existing: &mut String, incoming: &str) { - if incoming.is_empty() || incoming == existing || existing.starts_with(incoming) { - return; - } - if existing.is_empty() || incoming.starts_with(existing.as_str()) { - *existing = incoming.to_owned(); - } else { - existing.push_str("\n\n"); - existing.push_str(incoming); - } -} - -fn main_event(event: &Value) -> bool { - event.get("isSidechain").and_then(Value::as_bool) != Some(true) - && event.get("uuid").and_then(Value::as_str).is_some() -} - -fn run_claude( - plan: &ReplayPlan, - context: &RunContext<'_>, - journal: &mut Journal, -) -> Result { - let session_id = plan.native["session_id"] - .as_str() - .ok_or_else(|| ReplayError::trajectory("Claude plan lost its session ID"))?; - if context.request.mode == ReplayMode::PrepareOnly { - let replacements = plan - .calls() - .map(|call| { - ( - call.call_id.clone(), - FreshObservation { - call_id: call.call_id.clone(), - content: call.original_observation.clone(), - is_error: call.original_is_error, - return_code: None, - duration_ms: 0, - truncated: false, - metadata: BTreeMap::new(), - }, - ) - }) - .collect(); - let canonical = rebuild_claude(plan, &replacements)?; - let prepared = context.output_dir.join("native/prepared-prefix.jsonl"); - atomic_write(&prepared, canonical.as_bytes())?; - journal.append( - "session_prepared", - [("sha256".into(), json!(sha256(canonical.as_bytes())))], - )?; - return Ok(ReplayOutcome { - status: "prepared".into(), - reconstructed_path: Some(prepared), - continued_path: None, - observations: Vec::new(), - continued_steps: 0, - metadata: with_boundary_user_prompt_metadata( - json!({"native_session_id": session_id}), - context.request, - false, - ), - }); - } - let mut replacements = BTreeMap::new(); - let mut observations = Vec::new(); - let mut comparisons = Vec::new(); - let historical_logs = context.output_dir.join("logs/historical-tools"); - fs::create_dir_all(&historical_logs).replay_context( - ReplayErrorKind::Executor, - "create Claude historical tool log directory", - )?; - for batch in &plan.batches { - journal.append("batch_started", [("batch".into(), json!(batch.ordinal))])?; - for call in &batch.tool_calls { - journal.append( - "tool_started", - [ - ("batch".into(), json!(batch.ordinal)), - ("call_id".into(), json!(call.call_id)), - ("tool".into(), json!(call.name)), - ], - )?; - let bash_log = historical_logs.join(format!("bash-{}.log", call.ordinal)); - let fresh = execute_claude_tool_with_policy( - call, - &context.request.workspace, - context.request.allow_stale_observations, - Some(&bash_log), - )?; - replacements.insert(call.call_id.clone(), fresh.clone()); - comparisons.push(json!({ - "call_id": call.call_id, - "tool": call.name, - "exact": call.original_observation == fresh.content - && call.original_is_error == fresh.is_error - && !fresh.metadata.contains_key("opaque_source_observation"), - "original_is_error": call.original_is_error, - "replayed_is_error": fresh.is_error, - })); - journal.append( - "tool_finished", - [ - ("batch".into(), json!(batch.ordinal)), - ("call_id".into(), json!(call.call_id)), - ("return_code".into(), json!(fresh.return_code)), - ("is_error".into(), json!(fresh.is_error)), - ("duration_ms".into(), json!(fresh.duration_ms)), - ], - )?; - observations.push(fresh); - } - journal.append("batch_committed", [("batch".into(), json!(batch.ordinal))])?; - } - let canonical = rebuild_claude(plan, &replacements)?; - let reconstructed = context.output_dir.join("native/reconstructed-prefix.jsonl"); - atomic_write(&reconstructed, canonical.as_bytes())?; - atomic_write_json( - &context.output_dir.join("observation-comparison.json"), - &comparisons, - )?; - journal.append( - "session_rebuilt", - [("sha256".into(), json!(sha256(canonical.as_bytes())))], - )?; - if context.request.mode == ReplayMode::ReplayOnly { - return Ok(ReplayOutcome { - status: "replayed".into(), - reconstructed_path: Some(reconstructed), - continued_path: None, - observations, - continued_steps: 0, - metadata: with_boundary_user_prompt_metadata( - json!({"native_session_id": session_id}), - context.request, - false, - ), - }); - } - let launch = context - .launch - .ok_or_else(|| ReplayError::continuation("Claude continuation has no launch spec"))?; - if let Some(max_steps) = context.request.max_steps - && max_steps <= plan.prefix_model_turns - { - return Err(ReplayError::continuation( - "max-steps is exhausted by the replay prefix", - )); - } - let remaining_turns = context - .request - .max_steps - .map(|max_steps| max_steps - plan.prefix_model_turns); - let config_dir = context.state_dir.join("claude-config"); - let project_key: String = context - .request - .workspace - .to_string_lossy() - .chars() - .map(|character| { - if character.is_ascii_alphanumeric() { - character - } else { - '-' - } - }) - .collect(); - let native_path = config_dir - .join("projects") - .join(project_key) - .join(format!("{session_id}.jsonl")); - atomic_write(&native_path, canonical.as_bytes())?; - let canonical_messages = claude_canonical_messages(&canonical)?; - let manifest = ResumeTransportManifest::create( - session_id, - claude_boundary_tool_use_ids(plan), - canonical_messages, - context.nonce.to_owned(), - ) - .map_err(|error| { - ReplayError::trajectory(format!( - "construct Claude Resume Transport manifest: {error}" - )) - })?; - let bridge = ClaudeBridgeHandle::start( - manifest, - context.session_id, - context.request.disable_thinking, - context.request.boundary_user_prompt(), - )?; - journal.append("continuation_started", std::iter::empty())?; - let mut command = agent_command(&launch.entrypoint, context); - for (name, value) in bridge.child_environment() { - command.env(name, value); - } - command - .args(["--verbose", "--output-format=stream-json", "--resume"]) - .arg(session_id); - if let Some(remaining_turns) = remaining_turns { - command.args(["--max-turns", &remaining_turns.to_string()]); - } - if !context.request.disallowed_tools.is_empty() { - command.args([ - "--disallowedTools", - &context.request.disallowed_tools.join(","), - ]); - } - command.args(["--permission-mode", "bypassPermissions", "--print"]); - command.env("CLAUDE_CONFIG_DIR", &config_dir); - let log = context.output_dir.join("logs/claude-code.jsonl"); - let output = run_process(ProcessSpec { - command, - stdin: Some(context.nonce.as_bytes().to_vec()), - idle_timeout: None, - step_finish_limit: None, - stdout_redirect: None, - timeout: Duration::from_secs(24 * 60 * 60), - termination_grace: Duration::from_secs(2), - pipe_grace: Duration::from_millis(250), - retained_bytes: MAX_TOOL_OUTPUT_BYTES / 2, - log_path: log.clone(), - }) - .map_err(|error| ReplayError::new(ReplayErrorKind::Continuation, error.message))?; - let process_error = if output.timed_out - || (!output.status.success() - && !expected_claude_max_turn_exit(&output.stdout_tail, remaining_turns)) - { - let mut rendered = String::from_utf8_lossy(&output.stdout_tail).into_owned(); - if !output.stderr_tail.is_empty() { - rendered.push('\n'); - rendered.push_str(&String::from_utf8_lossy(&output.stderr_tail)); - } - Some(ReplayError::classify_continuation( - format!( - "Claude continuation exited {}; see {}", - output.status, - log.display() - ), - &rendered, - )) - } else { - None - }; - let bridge_result = bridge.finish(); - if let Some(mut process_error) = process_error { - if let Err(bridge_error) = bridge_result { - process_error.message = format!( - "{}; SandboxReplay bridge shutdown/validation also failed: {}", - process_error.message, bridge_error - ); - } - return Err(process_error); - } - let validated_model_requests = bridge_result?; - let prompt_injected = - context.request.boundary_user_prompt().is_some() && validated_model_requests > 0; - let raw_continued = String::from_utf8(read_regular_file(&native_path)?).replay_context( - ReplayErrorKind::Continuation, - "continued Claude session is not UTF-8", - )?; - let (cleaned, continued_steps) = - clean_claude_continuation(plan, context.nonce, &raw_continued)?; - let continued = context.output_dir.join("native/continued-session.jsonl"); - atomic_write(&continued, cleaned.as_bytes())?; - journal.append( - "continuation_finished", - [ - ("return_code".into(), json!(output.status.code())), - ("continued_steps".into(), json!(continued_steps)), - ( - "validated_model_requests".into(), - json!(validated_model_requests), - ), - ( - "boundary_user_prompt_injected".into(), - json!(prompt_injected), - ), - ], - )?; - Ok(ReplayOutcome { - status: "completed".into(), - reconstructed_path: Some(reconstructed), - continued_path: Some(continued), - observations, - continued_steps, - metadata: with_boundary_user_prompt_metadata( - json!({ - "native_session_id": session_id, - "validated_model_requests": validated_model_requests, - "model_transport": "sandbox-replay-claude-bridge", - }), - context.request, - prompt_injected, - ), - }) -} - -fn validate_claude_tool_policy( - call: &ToolCall, - allow_stale_observations: bool, -) -> Result<(), ReplayError> { - let unsupported = || { - ReplayError::new( - ReplayErrorKind::UnsupportedVersion, - format!( - "unsupported Claude replay tool call {}({}) inside the selected prefix", - call.name, call.call_id - ), - ) - }; - if call.name == "Find" - || (!FRESH_CLAUDE_TOOLS.contains(&call.name.as_str()) - && !STALE_CLAUDE_TOOLS.contains(&call.name.as_str())) - || (call.name == "Bash" - && call - .arguments - .get("run_in_background") - .and_then(Value::as_bool) - == Some(true)) - || (call.name == "Agent" - && call.arguments.get("subagent_type").and_then(Value::as_str) != Some("Explore")) - { - return Err(unsupported()); - } - let requires_stale = STALE_CLAUDE_TOOLS.contains(&call.name.as_str()) - || (call.original_is_error && claude_arguments_are_invalid(call)); - if requires_stale && !allow_stale_observations { - return Err(ReplayError::trajectory(format!( - "Claude tool call {}({}) can only reuse its source observation; pass --allow-stale-observations to opt into degraded replay", - call.name, call.call_id - ))); - } - Ok(()) -} - -fn execute_claude_tool_with_policy( - call: &ToolCall, - workspace: &Path, - allow_stale_observations: bool, - bash_log: Option<&Path>, -) -> Result { - validate_claude_tool_policy(call, allow_stale_observations)?; - let started = Instant::now(); - if call.original_is_error && claude_arguments_are_invalid(call) { - return replay_original_observation(call, started); - } - let (content, is_error, return_code) = match call.name.as_str() { - "Agent" => { - return replay_original_observation(call, started); - } - "TaskOutput" => { - return replay_original_observation(call, started); - } - "Bash" => { - let command = call - .arguments - .get("command") - .and_then(Value::as_str) - .unwrap_or_default(); - let timeout = call - .arguments - .get("timeout") - .and_then(Value::as_u64) - .map(|milliseconds| Duration::from_millis(milliseconds.clamp(1_000, 600_000))) - .unwrap_or(Duration::from_secs(120)); - let log = bash_log.ok_or_else(|| { - ReplayError::new( - ReplayErrorKind::Internal, - "Claude Bash replay requires a process log path", - ) - })?; - let (content, is_error, return_code, truncated) = - run_bash(command, workspace, timeout, log)?; - return observation_with_truncation( - call, - content, - is_error, - return_code, - started, - truncated, - ); - } - "Read" => { - let path = tool_path(&call.arguments, workspace, true)?; - let bytes = match fs::read(&path) { - Ok(bytes) => bytes, - Err(error) => { - return observation( - call, - format!("Read failed for {}: {error}", path.display()), - true, - Some(1), - started, - ); - } - }; - let text = String::from_utf8_lossy(&bytes); - let offset = call - .arguments - .get("offset") - .and_then(Value::as_u64) - .unwrap_or(1) - .max(1) as usize; - let limit = call - .arguments - .get("limit") - .and_then(Value::as_u64) - .unwrap_or(u64::MAX) as usize; - let value = text - .lines() - .skip(offset - 1) - .take(limit) - .collect::>() - .join("\n"); - (value, false, Some(0)) - } - "Write" => { - let path = tool_path(&call.arguments, workspace, true)?; - let content = call - .arguments - .get("content") - .and_then(Value::as_str) - .unwrap_or_default(); - fs::write(&path, content).replay_context( - ReplayErrorKind::Executor, - format!("write replay tool target {}", path.display()), - )?; - ( - format!("Wrote {} bytes to {}", content.len(), path.display()), - false, - Some(0), - ) - } - "Edit" => edit_tool(&call.arguments, workspace)?, - "MultiEdit" => { - let mut result = String::new(); - for edit in call - .arguments - .get("edits") - .and_then(Value::as_array) - .into_iter() - .flatten() - { - let mut arguments = edit.clone(); - if arguments.get("file_path").is_none() { - arguments["file_path"] = call - .arguments - .get("file_path") - .cloned() - .unwrap_or(Value::Null); - } - let (message, is_error, code) = edit_tool(&arguments, workspace)?; - if is_error { - return observation(call, message, true, code, started); - } - result.push_str(&message); - result.push('\n'); - } - (result.trim_end().to_owned(), false, Some(0)) - } - "Glob" => { - let pattern = call - .arguments - .get("pattern") - .and_then(Value::as_str) - .unwrap_or("*"); - let root = call - .arguments - .get("path") - .and_then(Value::as_str) - .map(PathBuf::from) - .unwrap_or_else(|| workspace.to_path_buf()); - let root = confined_path(workspace, &root, false)?; - let mut matches = Vec::new(); - walk_files(&root, &mut |path| { - let relative = path.strip_prefix(&root).unwrap_or(path).to_string_lossy(); - if wildcard_match(pattern, &relative) { - matches.push(path.display().to_string()); - } - })?; - (matches.join("\n"), false, Some(0)) - } - "Grep" => { - let Some(needle) = call - .arguments - .get("pattern") - .or_else(|| call.arguments.get("search")) - .and_then(Value::as_str) - .filter(|needle| !needle.is_empty()) - else { - return observation( - call, - "Grep failed: pattern/search is missing or empty".into(), - true, - Some(1), - started, - ); - }; - let root = call - .arguments - .get("path") - .or_else(|| call.arguments.get("files")) - .and_then(Value::as_str) - .map(PathBuf::from) - .unwrap_or_else(|| workspace.to_path_buf()); - let root = confined_path(workspace, &root, false)?; - let mut matches = Vec::new(); - walk_files(&root, &mut |path| { - if let Ok(text) = fs::read_to_string(path) { - for (line, content) in text.lines().enumerate() { - if content.contains(needle) { - matches.push(format!("{}:{}:{}", path.display(), line + 1, content)); - } - } - } - })?; - (matches.join("\n"), false, Some(0)) - } - "TaskCreate" | "TaskGet" | "TaskList" | "TaskUpdate" | "TodoWrite" => { - return replay_original_observation(call, started); - } - other => { - return Err(ReplayError::new( - ReplayErrorKind::UnsupportedVersion, - format!("unsupported Claude replay tool {other}"), - )); - } - }; - observation(call, content, is_error, return_code, started) -} - -fn claude_arguments_are_invalid(call: &ToolCall) -> bool { - let string = |name: &str| { - call.arguments - .get(name) - .and_then(Value::as_str) - .is_some_and(|value| !value.is_empty()) - }; - match call.name.as_str() { - "Agent" => !(string("description") && string("prompt") && string("subagent_type")), - "TaskOutput" => !string("task_id"), - "Bash" => !string("command"), - "Read" | "Write" | "Edit" | "MultiEdit" => !(string("file_path") || string("path")), - "Glob" => !string("pattern"), - "Grep" => !(string("pattern") || string("search")), - _ => false, - } -} - -fn replay_original_observation( - call: &ToolCall, - started: Instant, -) -> Result { - let mut metadata = BTreeMap::new(); - metadata.insert( - "opaque_source_observation".into(), - json!("stale_source_observation"), - ); - metadata.insert( - "degradation_reason".into(), - json!("stale_source_observation"), - ); - metadata.insert("source_call_id".into(), json!(call.call_id)); - Ok(FreshObservation { - call_id: call.call_id.clone(), - content: call.original_observation.clone(), - is_error: call.original_is_error, - return_code: Some(if call.original_is_error { 1 } else { 0 }), - duration_ms: started.elapsed().as_millis(), - truncated: false, - metadata, - }) -} - -fn observation( - call: &ToolCall, - content: String, - is_error: bool, - return_code: Option, - started: Instant, -) -> Result { - observation_with_truncation(call, content, is_error, return_code, started, false) -} - -fn observation_with_truncation( - call: &ToolCall, - content: String, - is_error: bool, - return_code: Option, - started: Instant, - forced_truncated: bool, -) -> Result { - let bytes = content.into_bytes(); - let truncated = forced_truncated || bytes.len() > MAX_TOOL_OUTPUT_BYTES; - let content = if bytes.len() > MAX_TOOL_OUTPUT_BYTES { - format!( - "{}\n[output truncated by pvisor replay]", - String::from_utf8_lossy(&bytes[..MAX_TOOL_OUTPUT_BYTES]) - ) - } else { - String::from_utf8_lossy(&bytes).into_owned() - }; - Ok(FreshObservation { - call_id: call.call_id.clone(), - content: Value::String(content), - is_error, - return_code, - duration_ms: started.elapsed().as_millis(), - truncated, - metadata: BTreeMap::new(), - }) -} - -fn run_bash( - command: &str, - workspace: &Path, - timeout: Duration, - log_path: &Path, -) -> Result<(String, bool, Option, bool), ReplayError> { - let mut process = Command::new("/bin/bash"); - process.args(["-c", command]).current_dir(workspace); - sanitized_environment(&mut process, true); - let output = run_process(ProcessSpec { - command: process, - stdin: None, - idle_timeout: None, - step_finish_limit: None, - stdout_redirect: None, - timeout, - termination_grace: Duration::from_millis(250), - pipe_grace: Duration::from_millis(100), - retained_bytes: MAX_TOOL_OUTPUT_BYTES / 2, - log_path: log_path.to_path_buf(), - })?; - let mut content = String::from_utf8_lossy(&output.stdout_tail).into_owned(); - if !output.stderr_tail.is_empty() { - if !content.is_empty() && !content.ends_with('\n') { - content.push('\n'); - } - content.push_str(&String::from_utf8_lossy(&output.stderr_tail)); - } - if output.stdout_truncated || output.stderr_truncated { - content - .push_str("\n[output truncated by pvisor replay; full output is in the process log]"); - } - if output.background_cleanup && !output.timed_out { - content.push_str("\n[background descendants were terminated after the command exited]"); - } - if output.timed_out { - content = format!( - "Command timed out after {} ms\n{}", - timeout.as_millis(), - content - ) - .trim_end() - .to_owned(); - } - Ok(( - content, - output.timed_out || output.background_cleanup || !output.status.success(), - if output.timed_out { - Some(124) - } else { - output.status.code() - }, - output.stdout_truncated || output.stderr_truncated, - )) -} - -fn edit_tool( - arguments: &Value, - workspace: &Path, -) -> Result<(String, bool, Option), ReplayError> { - let path = tool_path(arguments, workspace, false)?; - let old = arguments - .get("old_string") - .or_else(|| arguments.get("old_str")) - .and_then(Value::as_str) - .unwrap_or_default(); - let new = arguments - .get("new_string") - .or_else(|| arguments.get("new_str")) - .and_then(Value::as_str) - .unwrap_or_default(); - let source = fs::read_to_string(&path).replay_context( - ReplayErrorKind::Executor, - format!("read edit target {}", path.display()), - )?; - let occurrences = source.matches(old).count(); - let replace_all = arguments - .get("replace_all") - .and_then(Value::as_bool) - .unwrap_or(false); - if occurrences == 0 || (occurrences > 1 && !replace_all) { - return Ok(( - format!("Edit rejected: old string occurs {occurrences} times"), - true, - Some(1), - )); - } - let updated = if replace_all { - source.replace(old, new) - } else { - source.replacen(old, new, 1) - }; - fs::write(&path, updated).replay_context( - ReplayErrorKind::Executor, - format!("write edit target {}", path.display()), - )?; - Ok((format!("Updated {}", path.display()), false, Some(0))) -} - -fn tool_path( - arguments: &Value, - workspace: &Path, - allow_missing: bool, -) -> Result { - let raw = arguments - .get("file_path") - .or_else(|| arguments.get("path")) - .and_then(Value::as_str) - .ok_or_else(|| ReplayError::new(ReplayErrorKind::Executor, "tool path is missing"))?; - confined_path(workspace, Path::new(raw), allow_missing) -} - -fn confined_path( - workspace: &Path, - path: &Path, - allow_missing: bool, -) -> Result { - let workspace = canonicalize(workspace, ReplayErrorKind::Workspace, "workspace")?; - let candidate = if path.is_absolute() { - path.to_path_buf() - } else { - workspace.join(path) - }; - let resolved = if allow_missing && fs::symlink_metadata(&candidate).is_err() { - let mut existing = candidate.as_path(); - let mut missing = Vec::new(); - while fs::symlink_metadata(existing).is_err() { - missing.push( - existing - .file_name() - .ok_or_else(|| { - ReplayError::new(ReplayErrorKind::Executor, "tool path has no file name") - })? - .to_os_string(), - ); - existing = existing.parent().ok_or_else(|| { - ReplayError::new( - ReplayErrorKind::Executor, - "tool path has no existing parent", - ) - })?; - } - let mut resolved = canonicalize(existing, ReplayErrorKind::Executor, "tool path parent")?; - for component in missing.iter().rev() { - resolved.push(component); - } - resolved - } else { - canonicalize(&candidate, ReplayErrorKind::Executor, "tool path")? - }; - if !resolved.starts_with(&workspace) { - return Err(ReplayError::new( - ReplayErrorKind::Executor, - format!("tool path escapes workspace: {}", resolved.display()), - )); - } - Ok(resolved) -} - -fn walk_files(root: &Path, visit: &mut impl FnMut(&Path)) -> Result<(), ReplayError> { - if root.is_file() { - visit(root); - return Ok(()); - } - for entry in fs::read_dir(root).replay_context( - ReplayErrorKind::Executor, - format!("scan {}", root.display()), - )? { - let entry = entry.replay_context(ReplayErrorKind::Executor, "read directory entry")?; - let file_type = entry - .file_type() - .replay_context(ReplayErrorKind::Executor, "read directory entry type")?; - if file_type.is_symlink() { - continue; - } - if file_type.is_dir() { - walk_files(&entry.path(), visit)?; - } else if file_type.is_file() { - visit(&entry.path()); - } - } - Ok(()) -} - -fn wildcard_match(pattern: &str, value: &str) -> bool { - fn matches(pattern: &[u8], value: &[u8]) -> bool { - match pattern.split_first() { - None => value.is_empty(), - Some((&b'*', rest)) => { - matches(rest, value) || (!value.is_empty() && matches(pattern, &value[1..])) - } - Some((&b'?', rest)) => !value.is_empty() && matches(rest, &value[1..]), - Some((&character, rest)) => { - value.first() == Some(&character) && matches(rest, &value[1..]) - } - } - } - matches(pattern.as_bytes(), value.as_bytes()) -} - -fn rebuild_claude( - plan: &ReplayPlan, - replacements: &BTreeMap, -) -> Result { - let events = plan.native["events"] - .as_array() - .ok_or_else(|| ReplayError::trajectory("Claude plan lost native events"))?; - let boundary = plan.native["boundary_result_index"] - .as_u64() - .ok_or_else(|| ReplayError::trajectory("Claude plan lost boundary"))? - as usize; - let boundary_uuid = events - .get(boundary) - .ok_or_else(|| ReplayError::trajectory("Claude boundary is outside the native session"))? - .get("uuid") - .and_then(Value::as_str) - .ok_or_else(|| ReplayError::trajectory("Claude boundary event has no UUID"))?; - let active_chain = plan.native["active_chain_uuids"] - .as_array() - .ok_or_else(|| ReplayError::trajectory("Claude plan lost its active chain"))?; - let boundary_chain_position = active_chain - .iter() - .position(|uuid| uuid.as_str() == Some(boundary_uuid)) - .ok_or_else(|| ReplayError::trajectory("Claude boundary is not on the active chain"))?; - let mut allowed_uuids: BTreeSet = active_chain - .iter() - .take(boundary_chain_position + 1) - .filter_map(Value::as_str) - .map(str::to_owned) - .collect(); - let selected: BTreeSet<_> = plan.calls().map(|call| call.call_id.as_str()).collect(); - let owner_by_call: BTreeMap<_, _> = plan - .calls() - .map(|call| { - let owner = call - .native - .get("assistant_uuid") - .and_then(Value::as_str) - .ok_or_else(|| ReplayError::trajectory("Claude tool call lost its owner UUID"))?; - let result_uuid = call - .native - .get("result_uuid") - .and_then(Value::as_str) - .ok_or_else(|| ReplayError::trajectory("Claude tool call lost its result UUID"))?; - let result_index = call - .native - .get("result_index") - .and_then(Value::as_u64) - .ok_or_else(|| ReplayError::trajectory("Claude tool call lost its result index"))? - as usize; - if result_index > boundary { - return Err(ReplayError::trajectory( - "Claude selected result appears after its logical boundary", - )); - } - allowed_uuids.insert(result_uuid.to_owned()); - Ok((call.call_id.as_str(), owner)) - }) - .collect::>()?; - let mut replaced = BTreeSet::new(); - let mut output_events = Vec::new(); - for event in events.iter().take(boundary + 1) { - if !main_event(event) { - continue; - } - let event_uuid = required_str(event, "uuid", "Claude canonical event")?; - if !allowed_uuids.contains(event_uuid) { - continue; - } - let mut updated = event.clone(); - let selected_call_ids = updated - .get("message") - .and_then(|message| message.get("content")) - .and_then(Value::as_array) - .into_iter() - .flatten() - .filter(|block| block.get("type").and_then(Value::as_str) == Some("tool_result")) - .filter_map(|block| block.get("tool_use_id").and_then(Value::as_str)) - .filter(|call_id| selected.contains(call_id)) - .map(str::to_owned) - .collect::>(); - if !selected_call_ids.is_empty() { - let owners: BTreeSet<_> = selected_call_ids - .iter() - .filter_map(|call_id| owner_by_call.get(call_id.as_str()).copied()) - .collect(); - if owners.len() != 1 { - return Err(ReplayError::trajectory( - "Claude tool_result event combines calls from different assistant events", - )); - } - let owner = *owners.iter().next().unwrap(); - updated["parentUuid"] = Value::String(owner.to_owned()); - updated["sourceToolAssistantUUID"] = Value::String(owner.to_owned()); - if let Some(object) = updated.as_object_mut() { - object.remove("toolUseResult"); - } - if let Some(blocks) = updated - .get_mut("message") - .and_then(|message| message.get_mut("content")) - .and_then(Value::as_array_mut) - { - for block in blocks { - let Some(call_id) = block - .get("tool_use_id") - .and_then(Value::as_str) - .filter(|call_id| selected.contains(*call_id)) - .map(str::to_owned) - else { - continue; - }; - let replacement = replacements.get(&call_id).ok_or_else(|| { - ReplayError::trajectory(format!("fresh observation missing for {call_id}")) - })?; - block["content"] = replacement.content.clone(); - if replacement.is_error { - block["is_error"] = Value::Bool(true); - } else if let Some(object) = block.as_object_mut() { - object.remove("is_error"); - } - replaced.insert(call_id); - } - } - } - output_events.push(updated); - } - if replaced.len() != selected.len() { - return Err(ReplayError::trajectory( - "canonical Claude session did not replace every selected observation", - )); - } - if output_events - .last() - .and_then(|event| event.get("uuid")) - .and_then(Value::as_str) - != Some(boundary_uuid) - { - return Err(ReplayError::trajectory( - "canonical Claude session does not end at its logical boundary", - )); - } - let mut output = output_events - .iter() - .map(serde_json::to_string) - .collect::, _>>() - .replay_context(ReplayErrorKind::Internal, "serialize Claude native event")? - .join("\n"); - output.push('\n'); - Ok(output) -} - -fn clean_claude_continuation( - plan: &ReplayPlan, - nonce: &str, - raw: &str, -) -> Result<(String, usize), ReplayError> { - let events: Vec = raw - .lines() - .map(|line| { - serde_json::from_str(line).replay_context( - ReplayErrorKind::Continuation, - "parse resumed Claude native event", - ) - }) - .collect::>()?; - let boundary_uuid = plan.native["events"] - .as_array() - .and_then(|events| events.get(plan.native["boundary_result_index"].as_u64()? as usize)) - .and_then(|event| event.get("uuid")) - .and_then(Value::as_str) - .ok_or_else(|| ReplayError::trajectory("Claude boundary event has no UUID"))? - .to_owned(); - let boundary_indexes: Vec<_> = events - .iter() - .enumerate() - .filter_map(|(index, event)| { - (event.get("uuid").and_then(Value::as_str) == Some(&boundary_uuid)).then_some(index) - }) - .collect(); - if boundary_indexes.len() != 1 { - return Err(ReplayError::continuation( - "resumed Claude session must contain exactly one boundary observation", - )); - } - let boundary_index = boundary_indexes[0]; - let envelope = events - .get(boundary_index + 1..boundary_index + 6) - .ok_or_else(|| ReplayError::continuation("Claude native resume envelope is incomplete"))?; - let [ - enqueue, - dequeue, - continue_event, - no_response_event, - nonce_event, - ] = envelope - else { - return Err(ReplayError::continuation( - "Claude native resume envelope is incomplete", - )); - }; - let session_id = plan.native["session_id"] - .as_str() - .ok_or_else(|| ReplayError::trajectory("Claude plan lost its session ID"))?; - if enqueue.get("type").and_then(Value::as_str) != Some("queue-operation") - || enqueue.get("operation").and_then(Value::as_str) != Some("enqueue") - || enqueue.get("content").and_then(Value::as_str) != Some(nonce) - || enqueue.get("sessionId").and_then(Value::as_str) != Some(session_id) - || dequeue.get("type").and_then(Value::as_str) != Some("queue-operation") - || dequeue.get("operation").and_then(Value::as_str) != Some("dequeue") - || dequeue.get("sessionId").and_then(Value::as_str) != Some(session_id) - { - return Err(ReplayError::continuation( - "Claude native queue resume envelope is malformed", - )); - } - if continue_event.get("type").and_then(Value::as_str) != Some("user") - || continue_event.get("isMeta").and_then(Value::as_bool) != Some(true) - || exact_claude_event_text(continue_event) != Some("Continue from where you left off.") - || no_response_event.get("type").and_then(Value::as_str) != Some("assistant") - || exact_claude_event_text(no_response_event) != Some("No response requested.") - || nonce_event.get("type").and_then(Value::as_str) != Some("user") - || exact_claude_event_text(nonce_event) != Some(nonce) - { - return Err(ReplayError::continuation( - "Claude native resume message envelope is malformed", - )); - } - let continue_uuid = required_event_uuid(continue_event, "continue")?; - let no_response_uuid = required_event_uuid(no_response_event, "no-response")?; - let nonce_uuid = required_event_uuid(nonce_event, "nonce")?; - if continue_event.get("parentUuid").and_then(Value::as_str) != Some(&boundary_uuid) - || no_response_event.get("parentUuid").and_then(Value::as_str) != Some(&continue_uuid) - || nonce_event.get("parentUuid").and_then(Value::as_str) != Some(&no_response_uuid) - { - return Err(ReplayError::continuation( - "Claude resume envelope is not attached directly to the boundary observation", - )); - } - - let mut remove_indexes: BTreeSet = (boundary_index + 1..boundary_index + 6).collect(); - let mut removed_parent_by_uuid = BTreeMap::from([ - (continue_uuid.clone(), boundary_uuid.clone()), - (no_response_uuid.clone(), continue_uuid), - (nonce_uuid.clone(), no_response_uuid), - ]); - let last_prompt_indexes: Vec<_> = events - .iter() - .enumerate() - .filter_map(|(index, event)| { - (event.get("type").and_then(Value::as_str) == Some("last-prompt") - && event.get("lastPrompt").and_then(Value::as_str) == Some(nonce) - && event.get("sessionId").and_then(Value::as_str) == Some(session_id)) - .then_some(index) - }) - .collect(); - if last_prompt_indexes.is_empty() { - return Err(ReplayError::continuation( - "resumed Claude session has no nonce last-prompt metadata", - )); - } - remove_indexes.extend(last_prompt_indexes); - - let mut attachment_parent = nonce_uuid; - let mut previous_attachment_order = -1_i8; - let mut seen_attachment_types = BTreeSet::new(); - loop { - let matching: Vec<_> = events - .iter() - .enumerate() - .filter(|(index, event)| { - !remove_indexes.contains(index) - && event.get("type").and_then(Value::as_str) == Some("attachment") - && event.get("parentUuid").and_then(Value::as_str) - == Some(attachment_parent.as_str()) - }) - .collect(); - if matching.is_empty() { - break; - } - if matching.len() != 1 { - return Err(ReplayError::continuation( - "Claude resume attachment branch is ambiguous", - )); - } - let (index, event) = matching[0]; - let attachment = event.get("attachment").unwrap_or(&Value::Null); - let attachment_type = attachment - .get("type") - .and_then(Value::as_str) - .unwrap_or_default(); - let attachment_order = match attachment_type { - "agent_listing_delta" => 0, - "skill_listing" => 1, - "task_reminder" => 2, - _ => -1, - }; - let event_uuid = required_event_uuid(event, "resume attachment")?; - if !valid_claude_resume_attachment(attachment) - || !seen_attachment_types.insert(attachment_type.to_owned()) - || attachment_order <= previous_attachment_order - { - return Err(ReplayError::continuation( - "unexpected attachment in Claude resume envelope", - )); - } - previous_attachment_order = attachment_order; - remove_indexes.insert(index); - removed_parent_by_uuid.insert(event_uuid.clone(), attachment_parent); - attachment_parent = event_uuid; - } - - let mut cleaned_events = Vec::with_capacity(events.len() - remove_indexes.len()); - let mut first_real_assistant: Option = None; - for (index, event) in events.into_iter().enumerate() { - if remove_indexes.contains(&index) { - continue; - } - let mut updated = event; - if let Some(parent) = updated - .get("parentUuid") - .and_then(Value::as_str) - .map(str::to_owned) - { - let resolved = resolve_claude_parent(parent, &removed_parent_by_uuid)?; - updated["parentUuid"] = Value::String(resolved); - } - if index > boundary_index - && first_real_assistant.is_none() - && updated.get("type").and_then(Value::as_str) == Some("assistant") - && updated.get("isSidechain").and_then(Value::as_bool) != Some(true) - { - first_real_assistant = Some(updated.clone()); - } - cleaned_events.push(updated); - } - let first_real_assistant = first_real_assistant - .ok_or_else(|| ReplayError::continuation("Claude produced no real continuation turn"))?; - if first_real_assistant - .get("parentUuid") - .and_then(Value::as_str) - != Some(boundary_uuid.as_str()) - { - return Err(ReplayError::continuation( - "first real resumed Claude assistant is not a child of the boundary observation", - )); - } - for forbidden in [ - nonce, - "Continue from where you left off.", - "No response requested.", - ] { - if cleaned_events - .iter() - .any(|event| value_contains(event, forbidden)) - { - return Err(ReplayError::continuation( - "Claude resume transport text remains after native-session cleanup", - )); - } - } - let cleaned_boundary_index = cleaned_events - .iter() - .position(|event| event.get("uuid").and_then(Value::as_str) == Some(&boundary_uuid)) - .ok_or_else(|| ReplayError::continuation("cleaned Claude session lost its boundary"))?; - let continued_steps = cleaned_events - .iter() - .skip(cleaned_boundary_index + 1) - .filter(|event| { - main_event(event) - && event.get("type").and_then(Value::as_str) == Some("assistant") - && event - .get("message") - .and_then(|message| message.get("stop_reason")) - .is_some_and(|reason| !reason.is_null()) - }) - .count(); - if continued_steps == 0 { - return Err(ReplayError::continuation( - "cleaned Claude session has no complete continuation turn", - )); - } - let mut output = cleaned_events - .iter() - .map(serde_json::to_string) - .collect::, _>>() - .replay_context( - ReplayErrorKind::Internal, - "serialize cleaned Claude session", - )? - .join("\n"); - output.push('\n'); - Ok((output, continued_steps)) -} - -fn exact_claude_event_text(event: &Value) -> Option<&str> { - let content = event.get("message")?.get("content")?; - if let Some(text) = content.as_str() { - return Some(text); - } - let blocks = content.as_array()?; - if blocks.len() == 1 && blocks[0].get("type").and_then(Value::as_str) == Some("text") { - return blocks[0].get("text").and_then(Value::as_str); - } - None -} - -fn required_event_uuid(event: &Value, context: &str) -> Result { - event - .get("uuid") - .and_then(Value::as_str) - .filter(|uuid| !uuid.is_empty()) - .map(str::to_owned) - .ok_or_else(|| ReplayError::continuation(format!("Claude {context} event lacks a UUID"))) -} - -fn valid_claude_resume_attachment(attachment: &Value) -> bool { - match attachment.get("type").and_then(Value::as_str) { - Some("task_reminder") => { - attachment - .get("content") - .and_then(Value::as_array) - .is_some_and(Vec::is_empty) - && attachment.get("itemCount").and_then(Value::as_u64) == Some(0) - } - Some("agent_listing_delta") => { - let Some(added_lines) = attachment.get("addedLines").and_then(Value::as_array) else { - return false; - }; - let Some(added_types) = attachment.get("addedTypes").and_then(Value::as_array) else { - return false; - }; - attachment.get("isInitial").and_then(Value::as_bool) == Some(true) - && attachment - .get("showConcurrencyNote") - .and_then(Value::as_bool) - .is_some() - && added_lines.iter().all(Value::is_string) - && added_types.iter().all(Value::is_string) - && added_lines.len() == added_types.len() - && attachment - .get("removedTypes") - .and_then(Value::as_array) - .is_some_and(Vec::is_empty) - } - Some("skill_listing") => { - let Some(names) = attachment.get("names").and_then(Value::as_array) else { - return false; - }; - attachment.get("isInitial").and_then(Value::as_bool) == Some(true) - && attachment.get("content").and_then(Value::as_str).is_some() - && names.iter().all(Value::is_string) - && attachment.get("skillCount").and_then(Value::as_u64) == Some(names.len() as u64) - } - _ => false, - } -} - -fn resolve_claude_parent( - mut parent: String, - removed_parent_by_uuid: &BTreeMap, -) -> Result { - let mut seen = BTreeSet::new(); - while let Some(next) = removed_parent_by_uuid.get(&parent) { - if !seen.insert(parent.clone()) { - return Err(ReplayError::continuation( - "cycle in Claude resume transport parent chain", - )); - } - parent = next.clone(); - } - Ok(parent) -} - -fn value_contains(value: &Value, needle: &str) -> bool { - match value { - Value::String(value) => value.contains(needle), - Value::Array(values) => values.iter().any(|value| value_contains(value, needle)), - Value::Object(values) => values.values().any(|value| value_contains(value, needle)), - _ => false, - } -} -fn expected_claude_max_turn_exit(stdout: &[u8], max_turns: Option) -> bool { - let Some(max_turns) = max_turns else { - return false; - }; - for line in String::from_utf8_lossy(stdout).lines().rev() { - let Ok(event) = serde_json::from_str::(line) else { - continue; - }; - return event.get("type").and_then(Value::as_str) == Some("result") - && event.get("subtype").and_then(Value::as_str) == Some("error_max_turns") - && event.get("terminal_reason").and_then(Value::as_str) == Some("max_turns") - && event.get("num_turns").and_then(Value::as_u64) == Some((max_turns + 1) as u64); - } - false -} - -#[cfg(test)] -mod tests { - use std::collections::BTreeMap; - use std::fs; - use std::path::PathBuf; - use std::time::{Duration, Instant}; - - use serde_json::{Value, json}; - - use super::{ - claude_boundary_tool_use_ids, claude_canonical_messages, execute_claude_tool_with_policy, - expected_claude_max_turn_exit, rebuild_claude, run_bash, validate_claude_tool_policy, - wildcard_match, - }; - use crate::adapter::{RunContext, build_plan, run}; - use crate::claude_resume::ResumeTransportManifest; - use crate::journal::Journal; - use crate::model::{ - AdapterPlan, AgentKind, FreshObservation, PlaybackRequest, ReplayMode, ToolCall, - }; - - fn claude_tool_call(name: &str, arguments: Value) -> ToolCall { - ToolCall { - ordinal: 1, - call_id: "call-1".into(), - name: name.into(), - arguments, - original_observation: Value::Null, - original_is_error: false, - native: Value::Null, - } - } - - #[test] - fn claude_max_turn_exit_requires_exact_terminal_result() { - let event = json!({ - "type": "result", - "subtype": "error_max_turns", - "terminal_reason": "max_turns", - "num_turns": 2, - }) - .to_string(); - assert!(expected_claude_max_turn_exit(event.as_bytes(), Some(1))); - assert!(!expected_claude_max_turn_exit(event.as_bytes(), None)); - assert!(!expected_claude_max_turn_exit(event.as_bytes(), Some(2))); - - let wrong_terminal = json!({ - "type": "result", - "subtype": "error_max_turns", - "terminal_reason": "other", - "num_turns": 2, - }) - .to_string(); - assert!(!expected_claude_max_turn_exit( - wrong_terminal.as_bytes(), - Some(1) - )); - - let trailing_json = format!("{event}\n{}", json!({"type": "assistant"})); - assert!(!expected_claude_max_turn_exit( - trailing_json.as_bytes(), - Some(1) - )); - let trailing_noise = format!("{event}\nnot-json"); - assert!(expected_claude_max_turn_exit( - trailing_noise.as_bytes(), - Some(1) - )); - } - - #[test] - fn claude_input_validation_errors_are_replayed_without_execution() { - let workspace = tempfile::tempdir().unwrap(); - let mut call = claude_tool_call("Read", json!({})); - call.original_observation = json!("file_path is missing"); - call.original_is_error = true; - - let observation = - execute_claude_tool_with_policy(&call, workspace.path(), true, None).unwrap(); - assert!(observation.is_error); - assert_eq!(observation.content, call.original_observation); - assert_eq!( - observation.metadata.get("opaque_source_observation"), - Some(&json!("stale_source_observation")) - ); - } - - #[test] - fn claude_read_only_explore_agent_is_marked_opaque() { - let workspace = tempfile::tempdir().unwrap(); - let mut call = claude_tool_call( - "Agent", - json!({ - "description": "Inspect code", - "prompt": "Find the relevant files", - "subagent_type": "Explore", - }), - ); - call.original_observation = json!([{ - "type": "text", - "text": "Async agent launched successfully.", - }]); - - let observation = - execute_claude_tool_with_policy(&call, workspace.path(), true, None).unwrap(); - assert!(!observation.is_error); - assert_eq!(observation.content, call.original_observation); - assert_eq!( - observation.metadata.get("opaque_source_observation"), - Some(&json!("stale_source_observation")) - ); - } - - #[test] - fn claude_read_only_task_output_is_marked_opaque() { - let workspace = tempfile::tempdir().unwrap(); - let mut call = claude_tool_call( - "TaskOutput", - json!({ - "task_id": "a7e5a5bf351a66db5", - "block": true, - "timeout": 120000, - }), - ); - call.original_observation = json!([{ - "type": "text", - "text": "Explore agent result", - }]); - - let observation = - execute_claude_tool_with_policy(&call, workspace.path(), true, None).unwrap(); - assert!(!observation.is_error); - assert_eq!(observation.content, call.original_observation); - assert_eq!( - observation.metadata.get("opaque_source_observation"), - Some(&json!("stale_source_observation")) - ); - } - - #[test] - fn stale_observations_fail_closed_by_default() { - for call in [ - claude_tool_call( - "Agent", - json!({ - "description": "Inspect code", - "prompt": "Find files", - "subagent_type": "Explore", - }), - ), - claude_tool_call("TaskOutput", json!({"task_id": "task-1"})), - claude_tool_call("TaskCreate", json!({"subject": "work"})), - claude_tool_call("TodoWrite", json!({"todos": []})), - ] { - let error = validate_claude_tool_policy(&call, false).unwrap_err(); - assert!(error.to_string().contains("--allow-stale-observations")); - } - - let find = claude_tool_call("Find", json!({"pattern": "*.rs"})); - assert!(validate_claude_tool_policy(&find, true).is_err()); - } - - #[test] - fn stale_observations_are_explicitly_degraded() { - let workspace = tempfile::tempdir().unwrap(); - let mut call = claude_tool_call("TaskOutput", json!({"task_id": "task-1"})); - call.original_observation = json!("source observation"); - - validate_claude_tool_policy(&call, true).unwrap(); - let observation = - execute_claude_tool_with_policy(&call, workspace.path(), true, None).unwrap(); - - assert_eq!(observation.content, call.original_observation); - assert_eq!( - observation.metadata.get("degradation_reason"), - Some(&json!("stale_source_observation")) - ); - assert_eq!( - observation.metadata.get("source_call_id"), - Some(&json!(call.call_id)) - ); - } - - #[test] - fn prepare_only_executes_no_historical_tool() { - let temporary = tempfile::tempdir().unwrap(); - let workspace = temporary.path().join("workspace"); - let state = temporary.path().join("state"); - let output = temporary.path().join("output"); - let trajectory = temporary.path().join("trajectory.jsonl"); - fs::create_dir_all(&workspace).unwrap(); - fs::create_dir_all(output.join("native")).unwrap(); - let marker = workspace.join("must-not-exist"); - let events = [ - json!({ - "type": "assistant", "uuid": "assistant-1", "parentUuid": null, - "sessionId": "session", "version": "2.1.220", - "message": {"id": "message-1", "stop_reason": "tool_use", "content": [{ - "type": "tool_use", "id": "tool-1", "name": "Bash", - "input": {"command": format!("touch {}", marker.display())} - }]} - }), - json!({ - "type": "user", "uuid": "result-1", "parentUuid": "assistant-1", - "sourceToolAssistantUUID": "assistant-1", "sessionId": "session", - "version": "2.1.220", "message": {"content": [{ - "type": "tool_result", "tool_use_id": "tool-1", "content": "old" - }]} - }), - json!({ - "type": "assistant", "uuid": "assistant-2", "parentUuid": "result-1", - "sessionId": "session", "version": "2.1.220", - "message": {"id": "message-2", "stop_reason": "end_turn", "content": [{ - "type": "text", "text": "next" - }]} - }), - ]; - fs::write( - &trajectory, - events - .iter() - .map(|event| serde_json::to_string(event).unwrap()) - .collect::>() - .join("\n") - + "\n", - ) - .unwrap(); - let request = PlaybackRequest { - agent: AgentKind::ClaudeCode, - trajectory, - after_step: 1, - workspace, - state_dir: state.clone(), - output_dir: output.clone(), - agent_entrypoint: None, - agent_runtime: None, - disallowed_tools: Vec::new(), - trajectory_assets: None, - session_id: None, - max_steps: None, - mode: ReplayMode::PrepareOnly, - allow_stale_observations: false, - run_id: Some("test".into()), - disable_thinking: false, - boundary_user_prompt: None, - }; - let plan = build_plan(&request).unwrap(); - let mut journal = Journal::open(&state).unwrap(); - let context = RunContext { - request: &request, - state_dir: &state, - output_dir: &output, - launch: None, - session_id: "session", - nonce: "nonce", - }; - - let outcome = run(&plan, &context, &mut journal).unwrap(); - - assert_eq!(outcome.status, "prepared"); - assert!(outcome.observations.is_empty()); - assert!(!marker.exists()); - } - - #[test] - fn claude_read_directory_and_missing_nested_path_are_tool_errors() { - let workspace = tempfile::tempdir().unwrap(); - fs::create_dir(workspace.path().join("directory")).unwrap(); - - for file_path in ["directory", "missing/nested/file.txt"] { - let observation = execute_claude_tool_with_policy( - &claude_tool_call("Read", json!({"file_path": file_path})), - workspace.path(), - false, - None, - ) - .unwrap(); - assert!(observation.is_error); - assert_eq!(observation.return_code, Some(1)); - assert!( - observation - .content - .as_str() - .unwrap() - .contains("Read failed") - ); - } - } - - #[test] - fn claude_grep_accepts_search_and_files_aliases() { - let workspace = tempfile::tempdir().unwrap(); - let source = workspace.path().join("source.txt"); - fs::write(&source, "first\nneedle here\nlast\n").unwrap(); - let observation = execute_claude_tool_with_policy( - &claude_tool_call("Grep", json!({"search": "needle", "files": source})), - workspace.path(), - false, - None, - ) - .unwrap(); - assert!(!observation.is_error); - let content = observation.content.as_str().unwrap(); - assert!(content.contains("needle here")); - assert!(!content.contains(":1:first")); - assert!(!content.contains(":3:last")); - } - - #[test] - fn claude_grep_rejects_an_empty_pattern() { - let workspace = tempfile::tempdir().unwrap(); - fs::write(workspace.path().join("source.txt"), "content").unwrap(); - let observation = execute_claude_tool_with_policy( - &claude_tool_call("Grep", json!({"pattern": ""})), - workspace.path(), - false, - None, - ) - .unwrap(); - assert!(observation.is_error); - assert_eq!(observation.return_code, Some(1)); - assert!( - observation - .content - .as_str() - .unwrap() - .contains("missing or empty") - ); - } - - #[test] - fn claude_fixture_builds_one_complete_batch() { - let request = PlaybackRequest { - agent: AgentKind::ClaudeCode, - trajectory: PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("tests/fixtures/claude_bash_one_step.jsonl"), - after_step: 1, - workspace: PathBuf::from("/tmp"), - state_dir: PathBuf::from("/tmp/state"), - output_dir: PathBuf::from("/tmp/output"), - agent_entrypoint: None, - agent_runtime: None, - disallowed_tools: Vec::new(), - trajectory_assets: None, - session_id: None, - max_steps: None, - mode: ReplayMode::PrepareOnly, - allow_stale_observations: false, - run_id: Some("test".into()), - disable_thinking: false, - boundary_user_prompt: None, - }; - if !request.trajectory.exists() { - return; - } - let AdapterPlan::ClaudeCode(plan) = build_plan(&request).unwrap() else { - panic!("Claude fixture produced a non-Claude plan"); - }; - assert_eq!(plan.batches.len(), 1); - assert_eq!(plan.batches[0].tool_calls[0].name, "Bash"); - let replacements = BTreeMap::from([( - "tool-1".to_owned(), - FreshObservation { - call_id: "tool-1".into(), - content: json!("fresh observation"), - is_error: false, - return_code: Some(0), - duration_ms: 1, - truncated: false, - metadata: BTreeMap::new(), - }, - )]); - let rebuilt = rebuild_claude(&plan, &replacements).unwrap(); - let canonical_messages = claude_canonical_messages(&rebuilt).unwrap(); - assert_eq!(canonical_messages.len(), 3); - assert_eq!( - canonical_messages[2]["content"][0]["content"], - "fresh observation" - ); - let manifest = ResumeTransportManifest::create( - "session-1", - vec!["tool-1".into()], - canonical_messages, - "__PVISOR_NATIVE_REPLAY_0123456789abcdef__".into(), - ) - .unwrap(); - assert_eq!(manifest.canonical_message_count, 3); - assert_eq!(manifest.boundary_observation_sha256.len(), 1); - } - - #[test] - fn claude_groups_interleaved_stream_fragments_into_one_logical_batch() { - let temp = tempfile::tempdir().unwrap(); - let trajectory = temp.path().join("trajectory.jsonl"); - let events = [ - json!({ - "type":"assistant", "uuid":"assistant-1", "parentUuid":null, - "sessionId":"session", "version":"2.1.220", - "message":{"id":"message-1","stop_reason":null,"content":[ - {"type":"tool_use","id":"tool-1","name":"Bash","input":{"command":"true"}} - ]} - }), - json!({ - "type":"user", "uuid":"result-1", "parentUuid":"assistant-1", - "sourceToolAssistantUUID":"assistant-1", - "sessionId":"session", "version":"2.1.220", - "message":{"content":[ - {"type":"tool_result","tool_use_id":"tool-1","content":"old-1"} - ]} - }), - json!({ - "type":"assistant", "uuid":"assistant-2", "parentUuid":"result-1", - "sessionId":"session", "version":"2.1.220", - "message":{"id":"message-1","stop_reason":"tool_use","content":[ - {"type":"tool_use","id":"tool-2","name":"Bash","input":{"command":"true"}} - ]} - }), - json!({ - "type":"user", "uuid":"result-2", "parentUuid":"assistant-2", - "sourceToolAssistantUUID":"assistant-2", - "sessionId":"session", "version":"2.1.220", - "message":{"content":[ - {"type":"tool_result","tool_use_id":"tool-2","content":"old-2"} - ]} - }), - json!({ - "type":"assistant", "uuid":"assistant-next", "parentUuid":"result-2", - "sessionId":"session", "version":"2.1.220", - "message":{"id":"message-2","stop_reason":"end_turn","content":[ - {"type":"text","text":"next"} - ]} - }), - ]; - fs::write( - &trajectory, - events - .iter() - .map(|event| serde_json::to_string(event).unwrap()) - .collect::>() - .join("\n") - + "\n", - ) - .unwrap(); - let request = PlaybackRequest { - agent: AgentKind::ClaudeCode, - trajectory, - after_step: 1, - workspace: PathBuf::from("/tmp"), - state_dir: PathBuf::from("/tmp/state"), - output_dir: PathBuf::from("/tmp/output"), - agent_entrypoint: None, - agent_runtime: None, - disallowed_tools: Vec::new(), - trajectory_assets: None, - session_id: None, - max_steps: None, - mode: ReplayMode::PrepareOnly, - allow_stale_observations: false, - run_id: Some("test".into()), - disable_thinking: false, - boundary_user_prompt: None, - }; - let AdapterPlan::ClaudeCode(plan) = build_plan(&request).unwrap() else { - panic!("Claude fixture produced a non-Claude plan"); - }; - assert_eq!(plan.batches.len(), 1); - assert_eq!(plan.batches[0].tool_calls.len(), 2); - assert_eq!( - claude_boundary_tool_use_ids(&plan), - vec!["tool-1".to_owned(), "tool-2".to_owned()] - ); - assert_eq!(plan.native["boundary_result_index"], 3); - assert_eq!(plan.original_next_action.as_ref().unwrap()["text"], "next"); - assert_eq!(plan.original_next_action.as_ref().unwrap()["reasoning"], ""); - - let replacements = plan - .calls() - .map(|call| { - ( - call.call_id.clone(), - FreshObservation { - call_id: call.call_id.clone(), - content: Value::String(format!("fresh-{}", call.call_id)), - is_error: false, - return_code: Some(0), - duration_ms: 1, - truncated: false, - metadata: BTreeMap::new(), - }, - ) - }) - .collect(); - let rebuilt = rebuild_claude(&plan, &replacements).unwrap(); - let rebuilt: Vec = rebuilt - .lines() - .map(|line| serde_json::from_str(line).unwrap()) - .collect(); - assert_eq!(rebuilt.last().unwrap()["uuid"], "result-2"); - assert_eq!(rebuilt.len(), 4); - } - - #[test] - fn bash_timeout_kills_the_historical_process_group() { - let workspace = tempfile::tempdir().unwrap(); - let log = workspace.path().join("bash.log"); - let started = Instant::now(); - let (content, is_error, return_code, truncated) = - run_bash("sleep 5", workspace.path(), Duration::from_millis(50), &log).unwrap(); - assert!(started.elapsed() < Duration::from_secs(2)); - assert!(is_error); - assert_eq!(return_code, Some(124)); - assert!(content.contains("timed out")); - assert!(!truncated); - } - - #[test] - fn bash_reports_truncation_and_background_cleanup() { - let workspace = tempfile::tempdir().unwrap(); - let large_log = workspace.path().join("large.log"); - let (_, is_error, _, truncated) = run_bash( - "yes x | head -c 6291456", - workspace.path(), - Duration::from_secs(2), - &large_log, - ) - .unwrap(); - assert!(!is_error); - assert!(truncated); - - let background_log = workspace.path().join("background.log"); - let (content, is_error, _, _) = run_bash( - "sleep 30 &", - workspace.path(), - Duration::from_secs(2), - &background_log, - ) - .unwrap(); - assert!(is_error); - assert!(content.contains("background descendants were terminated")); - } - - #[test] - fn wildcard_supports_recursive_style_patterns() { - assert!(wildcard_match("**/*.rs", "src/lib.rs")); - assert!(!wildcard_match("*.toml", "src/lib.rs")); - } -} diff --git a/crates/persisting-replay/src/adapter/generic.rs b/crates/persisting-replay/src/adapter/generic.rs deleted file mode 100644 index f8452f8b3..000000000 --- a/crates/persisting-replay/src/adapter/generic.rs +++ /dev/null @@ -1,2480 +0,0 @@ -//! Replay support for agents whose native transcript is a JSONL event stream. -//! -//! OpenCode prints `run --format=json` events, while Codex persists -//! `response_item` events in its rollout JSONL. Both formats carry the -//! assistant tool call and its observation in the transcript, so the replay -//! prefix can be rebuilt without an Agent SDK. The live phase is delegated to -//! the native CLI after the reconstructed transcript has been staged. - -use std::fs; -use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; -use std::time::{Duration, Instant}; - -use serde_json::{Value, json}; - -use super::{ - MAX_TOOL_OUTPUT_BYTES, RunContext, agent_command, check_boundary, prepared_outcome, - with_boundary_user_prompt_metadata, -}; -use crate::codex_bridge::{CodexBridgeHandle, PromptMode}; -use crate::error::{ReplayError, ReplayErrorKind, ResultExt}; -use crate::io::{atomic_write, atomic_write_json, canonicalize, read_regular_file, sha256}; -use crate::journal::Journal; -use crate::model::{ - AgentKind, FreshObservation, PlaybackRequest, ReplayMode, ReplayOutcome, ReplayPlan, ToolBatch, - ToolCall, -}; -use crate::opencode_bridge; -use crate::process::{ProcessSpec, run_process}; - -#[derive(Debug, Clone, Copy)] -pub(super) enum NativeJsonlAgent { - Opencode, - Codex, -} - -impl NativeJsonlAgent { - fn kind(self) -> AgentKind { - match self { - Self::Opencode => AgentKind::Opencode, - Self::Codex => AgentKind::Codex, - } - } - - fn label(self) -> &'static str { - self.kind().as_str() - } -} - -#[derive(Debug, Clone)] -struct CallRecord { - call_event: usize, - output_event: usize, - call_id: String, - name: String, - arguments: Value, - observation: Value, - is_error: bool, - complete: bool, -} - -#[derive(Debug, Clone)] -struct TurnRecord { - start_event: usize, - end_event: usize, - text: String, - reasoning: String, - calls: Vec, -} - -type ParsedNative = (Vec, Option, Option); - -pub(super) fn build( - request: &PlaybackRequest, - agent: NativeJsonlAgent, -) -> Result { - let raw = read_regular_file(&request.trajectory)?; - let events = parse_jsonl(&raw, agent.label())?; - let (turns, user_prompt, session_id) = match agent { - NativeJsonlAgent::Opencode => parse_opencode(&events)?, - NativeJsonlAgent::Codex => parse_codex(&events)?, - }; - let complete_turns = turns - .iter() - .filter(|turn| !turn.calls.is_empty() && turn.calls.iter().all(|call| call.complete)) - .collect::>(); - check_boundary(request.after_step, complete_turns.len())?; - let selected = &complete_turns[..request.after_step]; - let boundary_end = selected - .last() - .map(|turn| turn.end_event) - .ok_or_else(|| ReplayError::trajectory("native JSONL replay boundary has no turn"))?; - let prefix_model_turns = turns - .iter() - .filter(|turn| turn.end_event <= boundary_end) - .count(); - let original_next_action = turns - .iter() - .filter(|turn| turn.start_event > boundary_end) - .find(|turn| is_actionable_turn(turn)) - .map(turn_signature); - let batches = selected - .iter() - .enumerate() - .map(|(index, turn)| ToolBatch { - ordinal: index + 1, - native_locator: format!("events:{}-{}", turn.start_event, turn.end_event), - tool_calls: turn - .calls - .iter() - .enumerate() - .map(|(ordinal, call)| ToolCall { - ordinal: ordinal + 1, - call_id: call.call_id.clone(), - name: call.name.clone(), - arguments: call.arguments.clone(), - original_observation: call.observation.clone(), - original_is_error: call.is_error, - native: json!({ - "call_event": call.call_event, - "output_event": call.output_event, - }), - }) - .collect(), - assistant_text: turn.text.clone(), - native: json!({ - "start_event": turn.start_event, - "end_event": turn.end_event, - }), - }) - .collect(); - Ok(ReplayPlan { - agent: request.agent, - source_path: canonicalize( - &request.trajectory, - ReplayErrorKind::Trajectory, - "trajectory", - )?, - source_sha256: sha256(&raw), - after_step: request.after_step, - prefix_model_turns, - batches, - native: json!({ - "format": agent.label(), - "events": events, - "user_prompt": user_prompt, - "session_id": session_id, - }), - original_next_action, - }) -} - -pub(super) fn execute( - plan: &ReplayPlan, - context: &RunContext<'_>, - journal: &mut Journal, - agent: NativeJsonlAgent, -) -> Result { - let events = plan - .native - .get("events") - .and_then(Value::as_array) - .ok_or_else(|| ReplayError::trajectory("native JSONL plan has no events"))?; - let boundary_end = plan - .batches - .last() - .and_then(|batch| batch.native.get("end_event")) - .and_then(Value::as_u64) - .ok_or_else(|| ReplayError::trajectory("native JSONL batch has no end event"))? - as usize; - if boundary_end >= events.len() { - return Err(ReplayError::trajectory( - "native JSONL boundary is out of bounds", - )); - } - let mut reconstructed_events = events[..=boundary_end].to_vec(); - let mut observations = Vec::new(); - for call in plan.calls() { - let fresh = execute_call(call, context)?; - let output_event = call - .native - .get("output_event") - .and_then(Value::as_u64) - .ok_or_else(|| ReplayError::trajectory("native JSONL call has no output event"))? - as usize; - match agent { - NativeJsonlAgent::Opencode => replace_opencode_observation( - reconstructed_events.get_mut(output_event).ok_or_else(|| { - ReplayError::trajectory("OpenCode call event is out of bounds") - })?, - &fresh, - )?, - NativeJsonlAgent::Codex => replace_codex_observation( - reconstructed_events.get_mut(output_event).ok_or_else(|| { - ReplayError::trajectory("Codex output event is out of bounds") - })?, - &fresh, - )?, - } - observations.push(fresh); - } - let prepared = context.output_dir.join("native/prepared-prefix.jsonl"); - write_jsonl(&prepared, &reconstructed_events)?; - journal.append( - "session_rebuilt", - [( - "prepared_only".into(), - json!(context.request.mode == ReplayMode::PrepareOnly), - )], - )?; - if context.request.mode == ReplayMode::PrepareOnly { - return Ok(prepared_outcome(prepared, context.request)); - } - - let replayed = context - .output_dir - .join("native/reconstructed-trajectory.jsonl"); - write_jsonl(&replayed, &reconstructed_events)?; - if context.request.mode == ReplayMode::ReplayOnly { - write_comparison(context, plan, &observations)?; - return Ok(ReplayOutcome { - status: "replayed".into(), - reconstructed_path: Some(replayed), - continued_path: None, - observations, - continued_steps: 0, - metadata: with_boundary_user_prompt_metadata( - json!({"native_cli": agent.label()}), - context.request, - false, - ), - }); - } - - let (continued, continued_steps, step_limited) = continue_native_cli( - plan, - context, - journal, - agent, - &reconstructed_events, - &replayed, - )?; - write_comparison(context, plan, &observations)?; - if continued_steps == 0 { - return Err(ReplayError::continuation(format!( - "{} produced no continuation turns; see logs", - agent.label() - ))); - } - let mut metadata = json!({"native_cli": agent.label()}); - if matches!(agent, NativeJsonlAgent::Opencode) && step_limited { - metadata["opencode_step_budget"] = json!({ - "enforced_by": "pvisor_event_watchdog", - "reason": "opencode ignores agent steps on resumed sessions", - }); - } - if matches!(agent, NativeJsonlAgent::Codex) { - let prompt_mode = if context.request.boundary_user_prompt().is_some() { - PromptMode::ExplicitUserPrompt - } else { - PromptMode::TransportNonce - }; - metadata["codex_resume_transport"] = json!({ - "prompt_mode": prompt_mode.as_str(), - "removed_before_model_request": prompt_mode == PromptMode::TransportNonce, - "removed_from_native_trajectory": prompt_mode == PromptMode::TransportNonce, - }); - } - Ok(ReplayOutcome { - status: if step_limited { - "max_steps".into() - } else { - "completed".into() - }, - reconstructed_path: None, - continued_path: Some(continued), - observations, - continued_steps, - metadata: with_boundary_user_prompt_metadata( - metadata, - context.request, - context.request.boundary_user_prompt().is_some(), - ), - }) -} - -fn parse_jsonl(raw: &[u8], label: &str) -> Result, ReplayError> { - let source = std::str::from_utf8(raw).map_err(|error| { - ReplayError::trajectory(format!("{label} trajectory is not UTF-8: {error}")) - })?; - let physical = source.split('\n').collect::>(); - let mut events = Vec::new(); - for (index, line) in physical.iter().enumerate() { - if line.trim().is_empty() { - continue; - } - match serde_json::from_str::(line) { - Ok(Value::Object(event)) => events.push(Value::Object(event)), - Ok(_) => { - return Err(ReplayError::trajectory(format!( - "{label} JSONL line {} must be an object", - index + 1 - ))); - } - Err(_) if index + 1 == physical.len() && !source.ends_with('\n') => break, - Err(error) => { - return Err(ReplayError::trajectory(format!( - "invalid {label} JSONL line {}: {error}", - index + 1 - ))); - } - } - } - if events.is_empty() { - return Err(ReplayError::trajectory(format!( - "{label} trajectory has no JSON events" - ))); - } - Ok(events) -} - -fn parse_opencode(events: &[Value]) -> Result { - let mut user_prompt = None; - let mut session_id = None; - let mut turns = Vec::new(); - let mut current: Option = None; - for (index, event) in events.iter().enumerate() { - session_id = session_id.or_else(|| { - event - .get("sessionID") - .and_then(Value::as_str) - .map(str::to_owned) - }); - match event.get("type").and_then(Value::as_str) { - Some("user") => { - if user_prompt.is_none() { - user_prompt = opencode_text(event); - } - } - Some("step_start") => { - if let Some(previous) = current.take() { - turns.push(previous); - } - current = Some(TurnRecord { - start_event: index, - end_event: index, - text: String::new(), - reasoning: String::new(), - calls: Vec::new(), - }); - } - Some("text") | Some("reasoning") | Some("tool_use") => { - let Some(turn) = current.as_mut() else { - continue; - }; - turn.end_event = index; - let part = event.get("part").cloned().unwrap_or_else(|| json!({})); - let part_type = part - .get("type") - .and_then(Value::as_str) - .or_else(|| event.get("type").and_then(Value::as_str)); - match part_type { - Some("text") => { - append_text(&mut turn.text, part.get("text").and_then(Value::as_str)) - } - Some("reasoning") => append_text( - &mut turn.reasoning, - part.get("text").and_then(Value::as_str), - ), - Some("tool") | Some("tool_use") => { - let id = part - .get("callID") - .or_else(|| part.get("id")) - .and_then(Value::as_str) - .map(str::to_owned) - .unwrap_or_else(|| format!("opencode-{index}")); - let name = part - .get("tool") - .or_else(|| part.get("name")) - .and_then(Value::as_str) - .unwrap_or_default() - .to_owned(); - let state = part.get("state").cloned().unwrap_or_else(|| json!({})); - let arguments = state.get("input").cloned().unwrap_or_else(|| json!({})); - let is_error = state.get("status").and_then(Value::as_str) == Some("error") - || state.get("error").is_some_and(|value| !value.is_null()); - let observation = state.get("output").cloned().unwrap_or(Value::Null); - let complete = !observation.is_null() - || is_error - || state.get("status").and_then(Value::as_str) == Some("completed"); - // OpenCode can emit more than one `tool_use` event for - // a call while its state transitions from pending to - // completed. Keep one call record and retain the final - // observation/event location. - if let Some(call) = turn.calls.iter_mut().find(|call| call.call_id == id) { - call.output_event = index; - if !name.is_empty() { - call.name = name; - } - if arguments != json!({}) { - call.arguments = arguments; - } - if !observation.is_null() { - call.observation = observation; - } - call.is_error |= is_error; - call.complete |= complete; - } else { - turn.calls.push(CallRecord { - call_event: index, - output_event: index, - call_id: id, - name, - arguments, - observation, - is_error, - complete, - }); - } - } - _ => {} - } - } - Some("step_finish") => { - if let Some(mut turn) = current.take() { - turn.end_event = index; - turns.push(turn); - } - } - _ => {} - } - } - if let Some(turn) = current { - turns.push(turn); - } - if user_prompt.is_none() { - return Err(ReplayError::trajectory( - "OpenCode trajectory has no user prompt", - )); - } - Ok((turns, user_prompt, session_id)) -} - -fn parse_codex(events: &[Value]) -> Result { - let mut user_prompt = None; - let mut session_id = None; - let mut turns = Vec::new(); - let mut current: Option = None; - let mut pending_outputs: Vec<(String, usize, Value, bool)> = Vec::new(); - for (index, event) in events.iter().enumerate() { - let event_type = event - .get("type") - .and_then(Value::as_str) - .unwrap_or_default(); - let Some(payload) = event.get("payload") else { - continue; - }; - if event_type == "session_meta" { - session_id = session_id.or_else(|| { - payload - .get("id") - .or_else(|| payload.get("session_id")) - .or_else(|| event.get("id")) - .and_then(Value::as_str) - .filter(|id| !id.is_empty()) - .map(str::to_owned) - }); - continue; - } - if event_type != "response_item" { - continue; - } - match payload.get("type").and_then(Value::as_str) { - Some("message") => { - let role = payload - .get("role") - .and_then(Value::as_str) - .unwrap_or_default(); - if role == "user" { - if user_prompt.is_none() { - user_prompt = codex_message_text(payload, "input_text"); - } - if let Some(turn) = current.take() { - turns.push(turn); - } - } else if role == "assistant" { - if let Some(turn) = current.take() - && (!turn.calls.is_empty() - || !turn.text.is_empty() - || !turn.reasoning.is_empty()) - { - turns.push(turn); - } - current = Some(TurnRecord { - start_event: index, - end_event: index, - text: codex_message_text(payload, "output_text").unwrap_or_default(), - reasoning: String::new(), - calls: Vec::new(), - }); - } - } - Some("reasoning") => { - let turn = current.get_or_insert_with(|| TurnRecord { - start_event: index, - end_event: index, - text: String::new(), - reasoning: String::new(), - calls: Vec::new(), - }); - turn.end_event = index; - if let Some(summary) = payload.get("summary").and_then(Value::as_array) { - for item in summary { - append_text( - &mut turn.reasoning, - item.get("text").and_then(Value::as_str), - ); - } - } - } - Some("function_call") | Some("custom_tool_call") => { - let turn = current.get_or_insert_with(|| TurnRecord { - start_event: index, - end_event: index, - text: String::new(), - reasoning: String::new(), - calls: Vec::new(), - }); - turn.end_event = index; - let call_id = payload - .get("call_id") - .or_else(|| payload.get("id")) - .and_then(Value::as_str) - .map(str::to_owned) - .ok_or_else(|| { - ReplayError::trajectory(format!( - "Codex function_call at event {index} has no call_id" - )) - })?; - let arguments = match payload.get("arguments").or_else(|| payload.get("input")) { - Some(Value::String(raw)) => { - serde_json::from_str(raw).unwrap_or_else(|_| json!(raw)) - } - Some(value) => value.clone(), - None => json!({}), - }; - let name = payload - .get("name") - .and_then(Value::as_str) - .unwrap_or_default() - .to_owned(); - turn.calls.push(CallRecord { - call_event: index, - output_event: usize::MAX, - call_id, - name, - arguments, - observation: Value::Null, - is_error: false, - complete: false, - }); - } - Some("function_call_output") | Some("custom_tool_call_output") => { - let call_id = payload - .get("call_id") - .and_then(Value::as_str) - .unwrap_or_default(); - if let Some(turn) = current.as_mut() { - turn.end_event = index; - if let Some(call) = turn.calls.iter_mut().find(|call| call.call_id == call_id) { - call.output_event = index; - call.observation = payload.get("output").cloned().unwrap_or(Value::Null); - call.complete = true; - call.is_error = payload - .get("status") - .and_then(Value::as_str) - .is_some_and(|status| status != "completed") - || payload.get("error").is_some_and(|value| !value.is_null()); - } else { - pending_outputs.push(( - call_id.to_owned(), - index, - payload.get("output").cloned().unwrap_or(Value::Null), - false, - )); - } - } - } - _ => {} - } - if !pending_outputs.is_empty() { - for (call_id, output_event, observation, is_error) in pending_outputs.drain(..) { - if let Some(turn) = current.as_mut() - && let Some(call) = turn.calls.iter_mut().find(|call| call.call_id == call_id) - { - call.output_event = output_event; - call.observation = observation; - call.is_error = is_error; - call.complete = true; - } - } - } - } - if let Some(turn) = current { - turns.push(turn); - } - if user_prompt.is_none() { - return Err(ReplayError::trajectory( - "Codex trajectory has no user message", - )); - } - // A response_item session_meta carries the ID in its payload, but older - // rollouts put it directly in the event. Accept both forms. - if session_id.is_none() { - session_id = events.iter().find_map(|event| { - (event.get("type").and_then(Value::as_str) == Some("session_meta")) - .then(|| { - event - .get("payload") - .and_then(|payload| payload.get("id")) - .or_else(|| event.get("id")) - .and_then(Value::as_str) - .map(str::to_owned) - }) - .flatten() - }); - } - Ok((turns, user_prompt, session_id)) -} - -fn turn_signature(turn: &TurnRecord) -> Value { - json!({ - "text": turn.text, - "reasoning": turn.reasoning, - "tools": turn.calls.iter().map(|call| json!({"name": call.name, "arguments": call.arguments})).collect::>(), - }) -} - -fn is_actionable_turn(turn: &TurnRecord) -> bool { - !turn.text.trim().is_empty() || !turn.calls.is_empty() -} - -fn opencode_text(event: &Value) -> Option { - event - .get("parts") - .and_then(Value::as_array) - .or_else(|| event.get("part").and_then(Value::as_array)) - .map(|parts| { - parts - .iter() - .filter_map(|part| part.get("text").and_then(Value::as_str)) - .collect::>() - .join("\n") - }) - .filter(|text| !text.is_empty()) -} - -fn codex_message_text(payload: &Value, wanted_type: &str) -> Option { - payload - .get("content") - .and_then(Value::as_array) - .map(|content| { - content - .iter() - .filter_map(|part| { - (part.get("type").and_then(Value::as_str) == Some(wanted_type)) - .then(|| part.get("text").and_then(Value::as_str)) - .flatten() - }) - .collect::>() - .join("\n") - }) - .filter(|text| !text.is_empty()) -} - -fn append_text(target: &mut String, value: Option<&str>) { - let Some(value) = value.filter(|value| !value.is_empty()) else { - return; - }; - if !target.is_empty() { - target.push('\n'); - } - target.push_str(value); -} - -fn execute_call( - call: &ToolCall, - context: &RunContext<'_>, -) -> Result { - let started = Instant::now(); - let mut is_error = false; - let mut return_code = None; - let content = match execute_tool_value(&call.name, &call.arguments, context, call.ordinal) { - Ok((content, code)) => { - return_code = code; - content - } - Err(error) => { - is_error = true; - Value::String(error.to_string()) - } - }; - if return_code.is_some_and(|code| code != 0) { - is_error = true; - } - Ok(FreshObservation { - call_id: call.call_id.clone(), - content, - is_error, - return_code, - duration_ms: started.elapsed().as_millis(), - truncated: false, - metadata: Default::default(), - }) -} - -fn execute_tool_value( - name: &str, - arguments: &Value, - context: &RunContext<'_>, - ordinal: usize, -) -> Result<(Value, Option), ReplayError> { - let mut arguments = arguments.clone(); - if let Value::String(raw) = &arguments { - arguments = match serde_json::from_str(raw) { - Ok(parsed) => parsed, - Err(_) if normalized_name(name) == "apply_patch" => { - return Err(ReplayError::new( - ReplayErrorKind::UnsupportedVersion, - "native apply_patch calls must provide structured JSON arguments", - )); - } - Err(_) => json!({"command": raw}), - }; - } - let normalized = normalized_name(name); - if matches!( - normalized.as_str(), - "bash" | "shell" | "exec" | "execute" | "terminal" - ) || arguments.get("command").is_some() - || arguments.get("cmd").is_some() - { - let command = arguments - .get("command") - .or_else(|| arguments.get("cmd")) - .or_else(|| arguments.get("script")) - .and_then(Value::as_str) - .ok_or_else(|| { - ReplayError::trajectory(format!( - "{} tool {name:?} has no command", - context.request.agent.as_str() - )) - })?; - let mut process = Command::new("/bin/sh"); - process - .args(["-c", command]) - .current_dir(&context.request.workspace); - let output = run_process(ProcessSpec { - command: process, - stdin: None, - idle_timeout: None, - step_finish_limit: None, - stdout_redirect: None, - timeout: Duration::from_secs(30 * 60), - termination_grace: Duration::from_secs(2), - pipe_grace: Duration::from_millis(250), - retained_bytes: MAX_TOOL_OUTPUT_BYTES, - log_path: context.state_dir.join(format!("native-tool-{ordinal}.log")), - }) - .map_err(|error| ReplayError::new(ReplayErrorKind::Executor, error.message))?; - let mut rendered = String::from_utf8_lossy(&output.stdout_tail).into_owned(); - if !output.stderr_tail.is_empty() { - if !rendered.is_empty() { - rendered.push('\n'); - } - rendered.push_str(&String::from_utf8_lossy(&output.stderr_tail)); - } - return Ok((Value::String(rendered), output.status.code())); - } - match normalized.as_str() { - "read" | "cat" => { - let path = tool_path( - arguments - .get("path") - .or_else(|| arguments.get("filePath")) - .or_else(|| arguments.get("file_path")), - context, - )?; - Ok(( - Value::String(String::from_utf8_lossy(&read_regular_file(&path)?).into_owned()), - Some(0), - )) - } - "write" => { - let path = tool_path( - arguments - .get("path") - .or_else(|| arguments.get("filePath")) - .or_else(|| arguments.get("file_path")), - context, - )?; - let content = arguments - .get("content") - .or_else(|| arguments.get("file_text")) - .and_then(Value::as_str) - .unwrap_or_default(); - if let Some(parent) = path.parent() { - fs::create_dir_all(parent) - .replay_context(ReplayErrorKind::Executor, "create native write parent")?; - } - fs::write(path, content) - .replay_context(ReplayErrorKind::Executor, "write native tool file")?; - Ok((Value::String(String::new()), Some(0))) - } - "edit" => { - let path = tool_path( - arguments - .get("path") - .or_else(|| arguments.get("filePath")) - .or_else(|| arguments.get("file_path")), - context, - )?; - let old = arguments - .get("oldString") - .or_else(|| arguments.get("old_str")) - .and_then(Value::as_str) - .unwrap_or_default(); - let new = arguments - .get("newString") - .or_else(|| arguments.get("new_str")) - .and_then(Value::as_str) - .unwrap_or_default(); - let original = String::from_utf8_lossy(&read_regular_file(&path)?).into_owned(); - if !original.contains(old) { - return Err(ReplayError::trajectory(format!( - "edit target does not contain old text: {}", - path.display() - ))); - } - fs::write(&path, original.replacen(old, new, 1)) - .replay_context(ReplayErrorKind::Executor, "write native edit")?; - Ok((Value::String(String::new()), Some(0))) - } - _ => Err(ReplayError::new( - ReplayErrorKind::UnsupportedVersion, - format!( - "{} replay does not support tool {name:?}; use a command-shaped tool or add an adapter", - context.request.agent.as_str() - ), - )), - } -} - -fn normalized_name(name: &str) -> String { - name.to_ascii_lowercase() -} - -fn tool_path(value: Option<&Value>, context: &RunContext<'_>) -> Result { - let rendered = value - .and_then(Value::as_str) - .ok_or_else(|| ReplayError::trajectory("native file tool has no path"))?; - let path = Path::new(rendered); - let workspace = canonicalize( - &context.request.workspace, - ReplayErrorKind::Workspace, - "workspace", - )?; - let candidate = if path.is_absolute() { - path.to_path_buf() - } else { - workspace.join(path) - }; - // Resolve the nearest existing ancestor when the target is new. This - // catches a symlinked directory that would otherwise let a write escape - // the workspace before the file itself exists. - let check = if fs::symlink_metadata(&candidate).is_err() { - let mut existing = candidate.as_path(); - let mut missing = Vec::new(); - while fs::symlink_metadata(existing).is_err() { - missing.push( - existing - .file_name() - .ok_or_else(|| ReplayError::trajectory("native file tool path has no name"))? - .to_os_string(), - ); - existing = existing.parent().ok_or_else(|| { - ReplayError::trajectory("native file tool path has no existing parent") - })?; - } - let mut resolved = canonicalize( - existing, - ReplayErrorKind::Executor, - "native file tool path parent", - )?; - for component in missing.iter().rev() { - resolved.push(component); - } - resolved - } else { - canonicalize( - &candidate, - ReplayErrorKind::Executor, - "native file tool path", - )? - }; - if !check.starts_with(&workspace) { - return Err(ReplayError::trajectory(format!( - "native file tool path escapes workspace: {rendered:?}" - ))); - } - Ok(candidate) -} - -fn replace_opencode_observation( - event: &mut Value, - fresh: &FreshObservation, -) -> Result<(), ReplayError> { - let part = event - .get_mut("part") - .ok_or_else(|| ReplayError::trajectory("OpenCode tool event has no part"))?; - let state = part - .as_object_mut() - .and_then(|part| part.get_mut("state")) - .and_then(Value::as_object_mut) - .ok_or_else(|| ReplayError::trajectory("OpenCode tool event has no state"))?; - state.insert( - "status".into(), - json!(if fresh.is_error { "error" } else { "completed" }), - ); - state.insert("output".into(), fresh.content.clone()); - if fresh.is_error { - state.insert("error".into(), fresh.content.clone()); - } else { - state.remove("error"); - } - Ok(()) -} - -fn replace_codex_observation( - event: &mut Value, - fresh: &FreshObservation, -) -> Result<(), ReplayError> { - let payload = event - .get_mut("payload") - .and_then(Value::as_object_mut) - .ok_or_else(|| ReplayError::trajectory("Codex output event has no payload"))?; - payload.insert("output".into(), fresh.content.clone()); - if fresh.is_error { - payload.insert("status".into(), json!("failed")); - } - Ok(()) -} - -fn write_jsonl(path: &Path, values: &[Value]) -> Result<(), ReplayError> { - let mut bytes = Vec::new(); - for value in values { - serde_json::to_writer(&mut bytes, value) - .replay_context(ReplayErrorKind::Executor, "serialize native JSONL")?; - bytes.push(b'\n'); - } - atomic_write(path, &bytes) -} - -fn write_comparison( - context: &RunContext<'_>, - plan: &ReplayPlan, - observations: &[FreshObservation], -) -> Result<(), ReplayError> { - let comparisons = plan.calls().zip(observations).map(|(call, fresh)| json!({ - "call_id": call.call_id, - "tool": call.name, - "exact": call.original_observation == fresh.content && call.original_is_error == fresh.is_error, - "original_is_error": call.original_is_error, - "replayed_is_error": fresh.is_error, - })).collect::>(); - atomic_write_json( - &context.output_dir.join("observation-comparison.json"), - &comparisons, - ) -} - -fn continue_native_cli( - plan: &ReplayPlan, - context: &RunContext<'_>, - journal: &mut Journal, - agent: NativeJsonlAgent, - prefix: &[Value], - reconstructed: &Path, -) -> Result<(PathBuf, usize, bool), ReplayError> { - let launch = context - .launch - .ok_or_else(|| ReplayError::continuation("native CLI replay has no launch spec"))?; - let logs = context.output_dir.join("logs"); - fs::create_dir_all(&logs) - .replay_context(ReplayErrorKind::Executor, "create native CLI log directory")?; - let log_path = logs.join(format!("{}.log", agent.label())); - // `PlaybackRequest::session_id` is the pVisor/model-router session. It - // must not be used as a native Codex session identity: SweEval (and other - // callers) deliberately set it to a routing key such as - // `sweeval-`. Codex resume resolves the rollout from the native - // session id stored in the source trajectory. Mixing the two makes - // `codex exec resume` silently start a fresh conversation, which is much - // worse than failing the replay because the resulting patch can still - // pass a verifier while A(N+1) is no longer comparable with A'(N+1). - let session_id = continuation_session_id(agent, plan, context)?; - let mut command = agent_command(&launch.entrypoint, context); - let mut codex_bridge = None; - let mut codex_transport_prompt = None; - let mut codex_prompt_mode = None; - let mut opencode_bridge = None; - let mut opencode_transport_prompt = None; - let mut remaining_steps: Option = None; - command.env("PVISOR_REPLAY_TRAJECTORY", reconstructed); - command.env("PVISOR_REPLAY_AFTER_STEP", plan.after_step.to_string()); - command.env( - "PVISOR_REPLAY_MAX_STEPS", - context - .request - .max_steps - .map(|value| value.to_string()) - .unwrap_or_default(), - ); - command.env("PVISOR_REPLAY_SESSION_ID", &session_id); - match agent { - NativeJsonlAgent::Opencode => { - let session_id = opencode_session_id(&session_id); - // `opencode run --session` refuses to start without a message. - // Pass a unique transport nonce as that message and strip it on - // the wire through the local bridge, so the first live request - // still ends exactly at the replayed boundary observation. - let explicit_prompt = context.request.boundary_user_prompt().map(str::to_owned); - let transport_prompt = explicit_prompt - .clone() - .unwrap_or_else(|| format!("pvisor-opencode-resume-{}", context.nonce)); - let temperature = env_f64("PVISOR_OPENCODE_TEMPERATURE"); - let top_p = env_f64("PVISOR_OPENCODE_TOP_P"); - let bridge = opencode_bridge::OpencodeBridgeHandle::start( - context.session_id, - explicit_prompt.is_none().then(|| transport_prompt.clone()), - temperature, - top_p, - context.request.disable_thinking, - )?; - let opencode_config = context.state_dir.join("opencode-config"); - let opencode_data = context.state_dir.join("opencode-data"); - remaining_steps = context - .request - .max_steps - .map(|max_steps| max_steps.saturating_sub(plan.after_step)) - .filter(|steps| *steps > 0); - write_opencode_provider_config( - &opencode_config, - Some(&bridge.base_url), - temperature, - top_p, - remaining_steps, - )?; - let export_path = context.output_dir.join("native/opencode-session.json"); - atomic_write_json( - &export_path, - &opencode_export(plan, prefix, &session_id, &context.request.workspace), - )?; - let mut import = agent_command(&launch.entrypoint, context); - import.args([ - "import", - export_path.to_str().ok_or_else(|| { - ReplayError::configuration("OpenCode export path is not valid UTF-8") - })?, - ]); - import.env("XDG_CONFIG_HOME", &opencode_config); - import.env("XDG_DATA_HOME", &opencode_data); - import.env("OPENCODE_DISABLE_AUTOUPDATE", "1"); - let import_log = logs.join("opencode-import.log"); - let imported = run_process(ProcessSpec { - command: import, - stdin: None, - idle_timeout: None, - step_finish_limit: None, - stdout_redirect: None, - timeout: Duration::from_secs(5 * 60), - termination_grace: Duration::from_secs(2), - pipe_grace: Duration::from_millis(250), - retained_bytes: MAX_TOOL_OUTPUT_BYTES / 4, - log_path: import_log.clone(), - }) - .map_err(|error| ReplayError::new(ReplayErrorKind::Continuation, error.message))?; - if !imported.status.success() { - return Err(ReplayError::classify_continuation( - format!( - "OpenCode session import exited {}; see {}", - imported.status, - import_log.display() - ), - &String::from_utf8_lossy(&imported.stderr_tail), - )); - } - command.env("XDG_CONFIG_HOME", &opencode_config); - command.env("XDG_DATA_HOME", &opencode_data); - command.env("OPENCODE_DISABLE_AUTOUPDATE", "1"); - for (name, value) in bridge.child_environment() { - command.env(name, value); - } - if let Some(model) = configured_model_from_environment() { - command.args(["--model", &model]); - } - command.args([ - "run", - "--format=json", - // The stderr progress log is the only real-time turn signal; - // stdout JSONL is block-buffered by OpenCode's runtime. - "--print-logs", - "--session", - &session_id, - "--dangerously-skip-permissions", - ]); - if !context.request.disable_thinking { - command.arg("--thinking"); - } - command.arg("--"); - command.arg(&transport_prompt); - // OpenCode awaits stdin EOF whenever it is not a TTY; inheriting - // the controller's stdin would hang the continuation forever. - command.stdin(Stdio::null()); - if explicit_prompt.is_none() { - opencode_transport_prompt = Some(transport_prompt); - } - opencode_bridge = Some(bridge); - } - NativeJsonlAgent::Codex => { - let explicit_prompt = context.request.boundary_user_prompt().map(str::to_owned); - let transport_prompt = explicit_prompt - .clone() - .unwrap_or_else(|| format!("pvisor-codex-resume-{}", context.nonce)); - let bridge = CodexBridgeHandle::start( - context.session_id, - transport_prompt.clone(), - explicit_prompt.as_deref(), - )?; - let codex_home = context.state_dir.join("codex-home"); - let session_path = codex_session_path(&codex_home, &session_id, &plan.native)?; - fs::create_dir_all( - session_path - .parent() - .expect("Codex session path has parent"), - ) - .replay_context(ReplayErrorKind::Executor, "create Codex session directory")?; - let staged = codex_staged_events(prefix, &session_id, &context.request.workspace); - write_jsonl(&session_path, &staged)?; - // Recent Codex releases resolve the Responses endpoint from a - // model-provider profile. Point the isolated profile at the - // local SandboxReplay bridge; the bridge removes the transport - // nonce before forwarding the request upstream. - let encoded = serde_json::to_string(bridge.base_url.as_str()).map_err(|error| { - ReplayError::configuration(format!("cannot encode Codex bridge URL: {error}")) - })?; - let config = format!( - "model_provider = \"pvisor-replay\"\n\n[model_providers.pvisor-replay]\nname = \"pvisor-replay\"\nbase_url = {encoded}\nenv_key = \"OPENAI_API_KEY\"\nwire_api = \"responses\"\n" - ); - atomic_write(&codex_home.join("config.toml"), config.as_bytes())?; - for (name, value) in bridge.child_environment() { - command.env(name, value); - } - command.env("CODEX_HOME", &codex_home); - command.args([ - "exec", - "resume", - &session_id, - "--json", - "--skip-git-repo-check", - "--dangerously-bypass-approvals-and-sandbox", - ]); - // ``exec resume`` otherwise falls back to Codex's default model. - // The native SweEval launch pins the configured model explicitly; - // carry the same value into the continuation so local - // OpenAI-compatible endpoints do not receive an unsupported - // default model (for example ``gpt-5``). - if let Some(model) = configured_model_from_environment() { - command.args(["--model", model.rsplit('/').next().unwrap_or(&model)]); - } - if let Some(reasoning_effort) = std::env::var("PVISOR_REPLAY_REASONING_EFFORT") - .ok() - .filter(|value| !value.trim().is_empty()) - { - command.args(["-c", &format!("model_reasoning_effort={reasoning_effort}")]); - } - if let Some(max_steps) = context.request.max_steps { - command.args(["-c", &format!("agent_max_steps={max_steps}")]); - } - // Older Codex releases require a prompt argument for resume. In - // the default mode this is an opaque nonce removed by the local - // Responses bridge before the request reaches the model. - command.arg(&transport_prompt); - codex_transport_prompt = Some(transport_prompt); - codex_prompt_mode = Some(bridge.prompt_mode()); - codex_bridge = Some(bridge); - } - } - journal.append( - "continuation_started", - [("agent".into(), json!(agent.label()))], - )?; - let output = run_process(ProcessSpec { - command, - stdin: None, - // A live OpenCode continuation can otherwise wait forever when a - // model request or tool subprocess wedges. Keep the overall - // 24-hour ceiling for long tasks, but fail closed after a bounded - // silent interval so the caller can retry instead of hanging. - idle_timeout: matches!(agent, NativeJsonlAgent::Opencode) - .then_some(Duration::from_secs(10 * 60)), - // OpenCode ignores its `agent.steps` budget on resumed sessions, so - // the remaining live-action budget is enforced on the event stream. - step_finish_limit: remaining_steps, - // OpenCode's Bun runtime fully buffers stdout on pipes; events only - // reach the log at exit. Redirect stdout to a file so the stream is - // live and the watchdogs can see it. - stdout_redirect: matches!(agent, NativeJsonlAgent::Opencode) - .then(|| logs.join("opencode-events.jsonl")), - timeout: Duration::from_secs(24 * 60 * 60), - termination_grace: Duration::from_secs(2), - pipe_grace: Duration::from_millis(250), - retained_bytes: MAX_TOOL_OUTPUT_BYTES / 2, - log_path: log_path.clone(), - }) - .map_err(|error| ReplayError::new(ReplayErrorKind::Continuation, error.message))?; - let step_limited = output.step_limited; - let bridge_result = codex_bridge - .take() - .map(|bridge| bridge.finish()) - .or_else(|| opencode_bridge.take().map(|bridge| bridge.finish())); - let bridge_error = bridge_result.and_then(|result| result.err()); - if !output.status.success() && matches!(agent, NativeJsonlAgent::Opencode) { - // OpenCode can wedge silently between tool executions; the idle - // watchdog then terminates it. Keep any complete live turns that - // were already produced instead of discarding them with the sandbox. - let events_path = log_path - .parent() - .unwrap_or_else(|| Path::new(".")) - .join("opencode-events.jsonl"); - let db_path = context.state_dir.join("opencode-data/opencode/opencode.db"); - let mut raw = read_regular_file(&events_path) - .or_else(|_| read_regular_file(&log_path)) - .unwrap_or_default(); - if !String::from_utf8_lossy(&raw).contains("\"step_finish\"") { - let session_id = opencode_session_id(&session_id); - if rebuild_opencode_events_from_db(&db_path, &events_path, &session_id)? { - raw = read_regular_file(&events_path).unwrap_or_default(); - } - } - let rescued = parse_json_lines_from_log(&raw); - let complete = rescued - .iter() - .filter(|event| event.get("type") == Some(&json!("step_finish"))) - .count(); - if complete > 0 { - journal.append( - "continuation_terminated_with_partial_turns", - [ - ("agent".into(), json!("opencode")), - ("exit".into(), json!(output.status.to_string())), - ("rescued_step_finish".into(), json!(complete)), - ], - )?; - } else { - let process_error = ReplayError::classify_continuation( - format!( - "{} replay/continuation exited {}; see {}", - agent.label(), - output.status, - log_path.display() - ), - &String::from_utf8_lossy(&output.stderr_tail), - ); - return Err(process_error); - } - } else if !output.status.success() { - let process_error = ReplayError::classify_continuation( - format!( - "{} replay/continuation exited {}; see {}", - agent.label(), - output.status, - log_path.display() - ), - &String::from_utf8_lossy(&output.stderr_tail), - ); - if let Some(error) = bridge_error { - return Err(ReplayError::continuation(format!( - "{process_error}; Codex bridge validation also failed: {error}" - ))); - } - return Err(process_error); - } - if let Some(error) = bridge_error { - return Err(error); - } - let output_path = context.output_dir.join("native/continued-trajectory.jsonl"); - let (continued_events, continued_steps, step_limited) = match agent { - NativeJsonlAgent::Codex => { - let codex_home = context.state_dir.join("codex-home"); - let staged_path = codex_session_path(&codex_home, &session_id, &plan.native)?; - // `exec resume` may rotate the rollout into a new timestamped - // file instead of appending to the staged path. Prefer the - // newest file from this isolated CODEX_HOME so the continuation - // is not reported as zero-step merely because we read the stale - // prefix file. - let session_path = - latest_codex_session_path(&codex_home, session_id.as_str()).unwrap_or(staged_path); - let raw_events = if session_path.is_file() { - parse_jsonl(&read_regular_file(&session_path)?, agent.label())? - } else { - Vec::new() - }; - validate_codex_continuation(&raw_events, plan, &session_id, &session_path)?; - let events = clean_codex_transport_events( - raw_events, - plan, - &session_id, - codex_prompt_mode.unwrap_or(PromptMode::TransportNonce), - codex_transport_prompt.as_deref(), - &session_path, - )?; - validate_codex_continuation(&events, plan, &session_id, &session_path)?; - let steps = count_codex_turns_after(&events, plan); - (events, steps, step_limited) - } - NativeJsonlAgent::Opencode => { - let events_path = log_path - .parent() - .unwrap_or_else(|| Path::new(".")) - .join("opencode-events.jsonl"); - let db_path = context.state_dir.join("opencode-data/opencode/opencode.db"); - let mut raw = - read_regular_file(&events_path).or_else(|_| read_regular_file(&log_path))?; - if !String::from_utf8_lossy(&raw).contains("\"step_finish\"") { - let live_session_id = opencode_session_id(&session_id); - if rebuild_opencode_events_from_db(&db_path, &events_path, &live_session_id)? { - raw = read_regular_file(&events_path).unwrap_or_default(); - } - } - let events = parse_json_lines_from_log(&raw); - // The transport nonce was only a CLI wake-up signal; drop it if - // the native stream echoed it back as a user or text event. - let nonce = opencode_transport_prompt.as_deref().unwrap_or_default(); - let events: Vec = events - .into_iter() - .filter(|event| !opencode_event_is_nonce(event, nonce)) - .collect(); - let steps = count_opencode_turns(&events); - let mut combined = prefix.to_vec(); - combined.extend(events); - (combined, steps, step_limited) - } - }; - if continued_events.is_empty() { - return Err(ReplayError::continuation( - "native CLI produced no JSONL trajectory", - )); - } - write_jsonl(&output_path, &continued_events)?; - Ok((output_path, continued_steps, step_limited)) -} - -fn configured_model_from_environment() -> Option { - std::env::var("MODEL_NAME") - .ok() - .or_else(|| std::env::var("OPENAI_MODEL").ok()) - .filter(|model| !model.trim().is_empty()) -} - -fn env_f64(name: &str) -> Option { - std::env::var(name) - .ok() - .and_then(|value| value.trim().parse::().ok()) -} - -/// Provider config for the isolated continuation `XDG_CONFIG_HOME`. -/// -/// OpenCode reads the endpoint from `OPENAI_BASE_URL`, but sampling options -/// have no environment channel, so a live continuation would silently fall -/// back to provider defaults and diverge from the recorded sampling. The -/// shape mirrors what a SweEval trial writes for the original run. -/// `effective_base` overrides the environment endpoint (used for the local -/// sampling-injection proxy). -fn opencode_provider_config( - model: &str, - base_url: Option<&str>, - temperature: Option, - top_p: Option, - steps: Option, -) -> Option { - let (provider, model_id) = model.split_once('/')?; - let base_url = base_url.map(str::trim).filter(|value| !value.is_empty()); - if base_url.is_none() && temperature.is_none() && top_p.is_none() && steps.is_none() { - return None; - } - let mut provider_config = serde_json::Map::new(); - if let Some(base_url) = base_url { - provider_config.insert("options".into(), json!({ "baseURL": base_url })); - } - // Always register the model id. A model that is absent from OpenCode's - // fetched catalog cannot be resolved at all without a `models` entry, - // even when only the endpoint or step budget is pinned. - let mut model_entry = serde_json::Map::new(); - if temperature.is_some() || top_p.is_some() { - let mut model_options = serde_json::Map::new(); - if let Some(temperature) = temperature { - model_options.insert("temperature".into(), json!(temperature)); - } - if let Some(top_p) = top_p { - model_options.insert("topP".into(), json!(top_p)); - } - model_entry.insert("options".into(), Value::Object(model_options)); - } - provider_config.insert( - "models".into(), - json!({ model_id: Value::Object(model_entry) }), - ); - let mut root = serde_json::Map::new(); - root.insert( - "provider".into(), - json!({ provider: Value::Object(provider_config) }), - ); - if let Some(steps) = steps { - // OpenCode has no CLI max-step flag. Constrain the live build agent - // to the remaining portion of the pVisor total action budget. - root.insert("agent".into(), json!({"build": {"steps": steps}})); - } - Some(Value::Object(root)) -} - -fn write_opencode_provider_config( - config_root: &Path, - effective_base: Option<&str>, - temperature: Option, - top_p: Option, - steps: Option, -) -> Result<(), ReplayError> { - let Some(model) = configured_model_from_environment() else { - return Ok(()); - }; - let base_url = match effective_base { - Some(base) => Some(base.to_owned()), - None => std::env::var("OPENAI_BASE_URL") - .ok() - .or_else(|| std::env::var("OPENAI_API_BASE").ok()), - }; - let config = opencode_provider_config(&model, base_url.as_deref(), temperature, top_p, steps); - let Some(config) = config else { - return Ok(()); - }; - let directory = config_root.join("opencode"); - fs::create_dir_all(&directory).replay_context( - ReplayErrorKind::Executor, - "create OpenCode config directory", - )?; - atomic_write_json(&directory.join("opencode.json"), &config) -} - -/// Rebuild the live continuation events from OpenCode's session database. -/// -/// OpenCode block-buffers its stdout JSONL, so a watchdog-terminated -/// continuation can discard everything still in the buffer. The sqlite -/// session store is written transactionally in real time; `python3` is part -/// of every task sandbox pVisor replays into, so the rebuild stays -/// dependency-free. -fn rebuild_opencode_events_from_db( - db_path: &Path, - events_path: &Path, - session_id: &str, -) -> Result { - if !db_path.is_file() { - return Ok(false); - } - let script = r#" -import json, sqlite3, sys -db, session_id = sys.argv[1], sys.argv[2] -con = sqlite3.connect("file:" + db + "?mode=ro", uri=True) -messages = [] -for mid, created, data in con.execute( - "SELECT id, time_created, data FROM message ORDER BY time_created" -): - if mid.startswith("msg_pvisor"): - continue - try: - info = json.loads(data) - except Exception: - continue - parts = [] - for (raw,) in con.execute( - "SELECT data FROM part WHERE message_id=? ORDER BY time_created, rowid", - (mid,), - ): - try: - parts.append(json.loads(raw)) - except Exception: - continue - messages.append((created, info, parts)) -events = [] -for _, info, parts in messages: - if info.get("role") != "assistant": - continue - def rank(part): - kind = part.get("type") - if kind == "step-start": - return (0, (part.get("time") or {}).get("start") or 0) - if kind == "step-finish": - return (2, 0) - return (1, (part.get("time") or {}).get("start") or 0) - mapping = { - "step-start": "step_start", - "text": "text", - "reasoning": "reasoning", - "tool": "tool_use", - "step-finish": "step_finish", - } - for part in sorted(parts, key=rank): - event_type = mapping.get(part.get("type")) - if event_type is None: - continue - events.append( - json.dumps( - {"type": event_type, "sessionID": session_id, "part": part}, - separators=(",", ":"), - ) - ) -sys.stdout.write("\n".join(events) + ("\n" if events else "")) -"#; - let output = Command::new("python3") - .arg("-c") - .arg(script) - .arg(db_path) - .arg(session_id) - .output() - .replay_context(ReplayErrorKind::Executor, "rebuild OpenCode events")?; - if !output.status.success() || output.stdout.is_empty() { - return Ok(false); - } - atomic_write(events_path, &output.stdout)?; - Ok(true) -} - -/// True when the native event echoes the transport nonce back as a user or -/// text part; such events are transport noise, not model input. -fn opencode_event_is_nonce(event: &Value, nonce: &str) -> bool { - if nonce.is_empty() { - return false; - } - match event.get("type").and_then(Value::as_str) { - Some("user") => event - .get("parts") - .and_then(Value::as_array) - .map(|parts| { - parts - .iter() - .any(|part| part.get("text") == Some(&json!(nonce))) - }) - .unwrap_or(false), - Some("text") => event.pointer("/part/text") == Some(&json!(nonce)), - _ => false, - } -} - -fn continuation_session_id( - agent: NativeJsonlAgent, - plan: &ReplayPlan, - context: &RunContext<'_>, -) -> Result { - let native = plan - .native - .get("session_id") - .and_then(Value::as_str) - .filter(|value| !value.is_empty()); - match agent { - NativeJsonlAgent::Codex => codex_native_session_id(&plan.native), - NativeJsonlAgent::Opencode => Ok(context - .request - .session_id - .as_deref() - .or(native) - .unwrap_or(context.session_id) - .to_owned()), - } -} - -fn codex_native_session_id(native: &Value) -> Result { - native - .get("session_id") - .and_then(Value::as_str) - .filter(|value| !value.is_empty()) - .map(str::to_owned) - .ok_or_else(|| { - ReplayError::continuation( - "Codex trajectory has no native session_meta id; refusing to use the pVisor/router session_id for resume", - ) - }) -} - -fn latest_codex_session_path(root: &Path, session_id: &str) -> Option { - fn visit( - directory: &Path, - session_id: &str, - newest: &mut Option<(std::time::SystemTime, PathBuf)>, - ) { - let Ok(entries) = fs::read_dir(directory) else { - return; - }; - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - visit(&path, session_id, newest); - continue; - } - if path.extension().and_then(|value| value.to_str()) != Some("jsonl") { - continue; - } - // Never pick an unrelated/stale rollout from the isolated - // CODEX_HOME. The native session id is also checked from the - // event stream below; matching the filename avoids selecting a - // rotated file belonging to another replay attempt. - let Some(file_name) = path.file_name().and_then(|value| value.to_str()) else { - continue; - }; - if !file_name.ends_with(&format!("-{session_id}.jsonl")) { - continue; - } - let Ok(modified) = entry.metadata().and_then(|metadata| metadata.modified()) else { - continue; - }; - if newest - .as_ref() - .is_none_or(|(current, _)| modified > *current) - { - *newest = Some((modified, path)); - } - } - } - let mut newest = None; - visit(root, session_id, &mut newest); - newest.map(|(_, path)| path) -} - -fn validate_codex_continuation( - events: &[Value], - plan: &ReplayPlan, - session_id: &str, - path: &Path, -) -> Result<(), ReplayError> { - if events.is_empty() { - return Err(ReplayError::continuation(format!( - "Codex resume produced no events in {}", - path.display() - ))); - } - let observed_session_id = events.iter().find_map(|event| { - (event.get("type").and_then(Value::as_str) == Some("session_meta")).then(|| { - event - .pointer("/payload/id") - .or_else(|| event.pointer("/payload/session_id")) - .and_then(Value::as_str) - }) - }); - if observed_session_id.flatten() != Some(session_id) { - return Err(ReplayError::continuation(format!( - "Codex resume did not return native session {session_id:?} (observed {:?}); refusing an unverified continuation", - observed_session_id.flatten() - ))); - } - - let assistant_turns = events - .iter() - .filter(|event| { - event.get("type").and_then(Value::as_str) == Some("response_item") - && event.pointer("/payload/type").and_then(Value::as_str) == Some("message") - && event.pointer("/payload/role").and_then(Value::as_str) == Some("assistant") - }) - .count(); - if assistant_turns < plan.batches.len() { - return Err(ReplayError::continuation(format!( - "Codex resume returned only {assistant_turns} assistant turns, but the staged boundary contains {} tool batches; refusing a fresh-session continuation", - plan.batches.len() - ))); - } - - if let Some(expected_prompt) = plan - .native - .get("user_prompt") - .and_then(Value::as_str) - .filter(|value| !value.is_empty()) - { - let has_original_prompt = events.iter().any(|event| { - event.get("type").and_then(Value::as_str) == Some("response_item") - && event.pointer("/payload/type").and_then(Value::as_str) == Some("message") - && event.pointer("/payload/role").and_then(Value::as_str) == Some("user") - && event - .get("payload") - .and_then(|payload| codex_message_text(payload, "input_text")) - .as_deref() - == Some(expected_prompt) - }); - if !has_original_prompt { - return Err(ReplayError::continuation( - "Codex resume did not contain the original user task; refusing a fresh-session continuation", - )); - } - } - - if let Some(last_call_id) = plan - .batches - .last() - .and_then(|batch| batch.tool_calls.last()) - .map(|call| call.call_id.as_str()) - { - let has_boundary_call = events.iter().any(|event| { - event.pointer("/payload/call_id").and_then(Value::as_str) == Some(last_call_id) - }); - if !has_boundary_call { - return Err(ReplayError::continuation(format!( - "Codex resume did not contain the boundary call {last_call_id:?}; refusing an unverified continuation" - ))); - } - } - Ok(()) -} - -fn clean_codex_transport_events( - mut events: Vec, - plan: &ReplayPlan, - _session_id: &str, - prompt_mode: PromptMode, - transport_prompt: Option<&str>, - path: &Path, -) -> Result, ReplayError> { - if prompt_mode == PromptMode::ExplicitUserPrompt { - return Ok(events); - } - let expected = transport_prompt - .filter(|prompt| !prompt.is_empty()) - .ok_or_else(|| ReplayError::continuation("Codex transport nonce is missing"))?; - let boundary_call_id = plan - .batches - .last() - .and_then(|batch| batch.tool_calls.last()) - .map(|call| call.call_id.as_str()) - .ok_or_else(|| ReplayError::trajectory("Codex plan has no boundary call"))?; - let boundary_index = events - .iter() - .rposition(|event| { - event.pointer("/payload/call_id").and_then(Value::as_str) == Some(boundary_call_id) - }) - .ok_or_else(|| { - ReplayError::continuation(format!( - "Codex continuation has no boundary call in {}", - path.display() - )) - })?; - let matches = events - .iter() - .enumerate() - .filter_map(|(index, event)| { - (index > boundary_index - && event.get("type").and_then(Value::as_str) == Some("response_item") - && event.pointer("/payload/type").and_then(Value::as_str) == Some("message") - && event.pointer("/payload/role").and_then(Value::as_str) == Some("user") - && event - .get("payload") - .and_then(|payload| codex_message_text(payload, "input_text")) - .as_deref() - == Some(expected)) - .then_some(index) - }) - .collect::>(); - if matches.len() != 1 { - return Err(ReplayError::continuation(format!( - "expected exactly one Codex transport nonce in the resumed trajectory, found {}", - matches.len() - ))); - } - events.remove(matches[0]); - for event in &mut events { - redact_codex_transport_nonce(event, expected); - } - let legacy_prompt = "Continue from the replay boundary."; - if events.iter().any(|event| { - event.get("type").and_then(Value::as_str) == Some("response_item") - && event.pointer("/payload/type").and_then(Value::as_str) == Some("message") - && event.pointer("/payload/role").and_then(Value::as_str) == Some("user") - && event - .get("payload") - .and_then(|payload| codex_message_text(payload, "input_text")) - .as_deref() - == Some(legacy_prompt) - }) { - return Err(ReplayError::continuation( - "legacy Codex resume prompt remains in the cleaned trajectory", - )); - } - if events - .iter() - .any(|event| event.to_string().contains(expected)) - { - return Err(ReplayError::continuation( - "Codex transport nonce remains in the cleaned trajectory", - )); - } - Ok(events) -} - -fn redact_codex_transport_nonce(value: &mut Value, expected: &str) { - match value { - Value::String(text) => { - if text.contains(expected) { - *text = text.replace(expected, ""); - } - } - Value::Array(values) => { - for value in values { - redact_codex_transport_nonce(value, expected); - } - } - Value::Object(fields) => { - for value in fields.values_mut() { - redact_codex_transport_nonce(value, expected); - } - } - Value::Null | Value::Bool(_) | Value::Number(_) => {} - } -} - -fn codex_session_path( - codex_home: &Path, - session_id: &str, - native: &Value, -) -> Result { - if session_id.is_empty() - || !session_id - .chars() - .all(|character| character.is_ascii_alphanumeric() || matches!(character, '-' | '_')) - { - return Err(ReplayError::configuration( - "Codex session_id must contain only ASCII letters, digits, '-' and '_'", - )); - } - // Codex discovers sessions below CODEX_HOME/sessions by the rollout file - // name. Keep the directory and timestamp shape used by the native CLI - // while placing the staged transcript in the replay state directory. A - // fixed 1970 path is accepted by some versions but is not a native rollout - // identity and can make `exec resume` ignore the staged file. - let timestamp = native - .get("events") - .and_then(Value::as_array) - .and_then(|events| { - events.iter().find_map(|event| { - (event.get("type").and_then(Value::as_str) == Some("session_meta")).then(|| { - event - .pointer("/payload/timestamp") - .or_else(|| event.pointer("/timestamp")) - .and_then(Value::as_str) - }) - }) - }) - .flatten() - .and_then(|value| chrono::DateTime::parse_from_rfc3339(value).ok()); - let (directory, filename_timestamp) = timestamp - .map(|value| { - ( - value.format("%Y/%m/%d").to_string(), - value.format("%Y-%m-%dT%H-%M-%S").to_string(), - ) - }) - .unwrap_or_else(|| ("1970/01/01".into(), "1970-01-01T00-00-00".into())); - Ok(codex_home.join(format!( - "sessions/{directory}/rollout-{filename_timestamp}-{session_id}.jsonl" - ))) -} - -fn opencode_session_id(raw: &str) -> String { - let suffix = raw - .chars() - .filter(|character| character.is_ascii_alphanumeric() || matches!(character, '_' | '-')) - .collect::(); - if raw.starts_with("ses_") && raw == suffix && !suffix.is_empty() { - raw.to_owned() - } else { - format!( - "ses_pvisor_{}", - if suffix.is_empty() { "replay" } else { &suffix } - ) - } -} - -fn opencode_export( - plan: &ReplayPlan, - prefix: &[Value], - session_id: &str, - workspace: &Path, -) -> Value { - let user_id = "msg_pvisor_user"; - let prompt = plan - .native - .get("user_prompt") - .and_then(Value::as_str) - .unwrap_or_default(); - // OpenCode resolves a session's default model from the last user message - // metadata when a request does not pin one. The synthetic placeholder - // must therefore carry the configured model; "pvisor/replay" would poison - // that fallback with a provider that does not exist. - let (placeholder_provider, placeholder_model) = configured_model_from_environment() - .and_then(|model| { - model - .split_once('/') - .map(|(p, m)| (p.to_owned(), m.to_owned())) - }) - .unwrap_or_else(|| ("pvisor".to_owned(), "replay".to_owned())); - let mut messages = vec![json!({ - "info": { - "id": user_id, - "sessionID": session_id, - "role": "user", - "time": {"created": 0}, - "agent": "build", - "model": {"providerID": placeholder_provider, "modelID": placeholder_model}, - }, - "parts": [{ - "id": "prt_pvisor_user", - "sessionID": session_id, - "messageID": user_id, - "type": "text", - "text": prompt, - }], - })]; - for batch in &plan.batches { - let message_id = format!("msg_pvisor_{:04}", batch.ordinal); - let mut parts = vec![json!({ - "id": format!("prt_pvisor_step_start_{:04}", batch.ordinal), - "sessionID": session_id, - "messageID": message_id, - "type": "step-start", - })]; - let start_event = batch - .native - .get("start_event") - .and_then(Value::as_u64) - .unwrap_or_default() as usize; - let end_event = batch - .native - .get("end_event") - .and_then(Value::as_u64) - .unwrap_or(start_event as u64) as usize; - for (reasoning_ordinal, event) in prefix - .get(start_event..=end_event.min(prefix.len().saturating_sub(1))) - .into_iter() - .flatten() - .filter(|event| event.get("type").and_then(Value::as_str) == Some("reasoning")) - .enumerate() - { - let Some(text) = event - .get("part") - .and_then(|part| part.get("text")) - .and_then(Value::as_str) - .filter(|text| !text.is_empty()) - else { - continue; - }; - parts.push(json!({ - "id": format!("prt_pvisor_reasoning_{:04}_{:04}", batch.ordinal, reasoning_ordinal + 1), - "sessionID": session_id, - "messageID": message_id, - "type": "reasoning", - "text": text, - "time": {"start": 0, "end": 0}, - })); - } - if !batch.assistant_text.is_empty() { - parts.push(json!({ - "id": format!("prt_pvisor_text_{:04}", batch.ordinal), - "sessionID": session_id, - "messageID": message_id, - "type": "text", - "text": batch.assistant_text, - })); - } - for (ordinal, call) in batch.tool_calls.iter().enumerate() { - let output_event = call - .native - .get("output_event") - .and_then(Value::as_u64) - .unwrap_or_default() as usize; - let state = prefix - .get(output_event) - .and_then(|event| event.get("part")) - .and_then(|part| part.get("state")); - let fresh_output = state - .and_then(|state| state.get("output")) - .map(render_opencode_output) - .unwrap_or_default(); - let fresh_error = state - .and_then(|state| state.get("status")) - .and_then(Value::as_str) - == Some("error"); - let input = if call.arguments.is_object() { - call.arguments.clone() - } else { - json!({"value": call.arguments}) - }; - let state = if fresh_error { - json!({ - "status": "error", - "input": input, - "error": fresh_output, - "metadata": {}, - "time": {"start": 0, "end": 0}, - }) - } else { - json!({ - "status": "completed", - "input": input, - "output": fresh_output, - "title": call.name, - "metadata": {}, - "time": {"start": 0, "end": 0}, - }) - }; - parts.push(json!({ - "id": format!("prt_pvisor_tool_{:04}_{:04}", batch.ordinal, ordinal + 1), - "sessionID": session_id, - "messageID": message_id, - "type": "tool", - "callID": call.call_id, - "tool": call.name, - "state": state, - })); - } - parts.push(json!({ - "id": format!("prt_pvisor_step_finish_{:04}", batch.ordinal), - "sessionID": session_id, - "messageID": message_id, - "type": "step-finish", - "reason": "tool-calls", - "cost": 0, - "tokens": {"input": 0, "output": 0, "reasoning": 0, "cache": {"read": 0, "write": 0}}, - })); - messages.push(json!({ - "info": { - "id": message_id, - "sessionID": session_id, - "role": "assistant", - "time": {"created": batch.ordinal as u64, "completed": batch.ordinal as u64}, - "parentID": user_id, - "modelID": placeholder_model, - "providerID": placeholder_provider, - "mode": "build", - "agent": "build", - "path": {"cwd": workspace.display().to_string(), "root": workspace.display().to_string()}, - "cost": 0, - "tokens": {"input": 0, "output": 0, "reasoning": 0, "cache": {"read": 0, "write": 0}}, - }, - "parts": parts, - })); - } - json!({ - "info": { - "id": session_id, - "slug": "pvisor-replay", - "projectID": "global", - "directory": workspace.display().to_string(), - "path": "", - "title": if prompt.is_empty() { "pVisor replay" } else { prompt }, - "version": "1", - "time": {"created": 0, "updated": plan.batches.len() as u64}, - }, - "messages": messages, - }) -} - -fn render_opencode_output(value: &Value) -> String { - value - .as_str() - .map(str::to_owned) - .unwrap_or_else(|| value.to_string()) -} - -fn codex_staged_events(prefix: &[Value], session_id: &str, workspace: &Path) -> Vec { - let mut events = prefix.to_vec(); - let mut found_meta = false; - for event in &mut events { - if event.get("type").and_then(Value::as_str) != Some("session_meta") { - continue; - } - found_meta = true; - if let Some(payload) = event.get_mut("payload").and_then(Value::as_object_mut) { - payload.insert("id".into(), json!(session_id)); - if payload.contains_key("session_id") { - payload.insert("session_id".into(), json!(session_id)); - } - payload - .entry("cwd") - .or_insert_with(|| json!(workspace.display().to_string())); - payload - .entry("cli_version") - .or_insert_with(|| json!(AgentKind::Codex.supported_version())); - } - } - if !found_meta { - events.insert( - 0, - json!({ - "timestamp": "1970-01-01T00:00:00Z", - "type": "session_meta", - "payload": { - "id": session_id, - "cwd": workspace.display().to_string(), - "cli_version": AgentKind::Codex.supported_version(), - }, - }), - ); - } - events -} - -fn parse_json_lines_from_log(raw: &[u8]) -> Vec { - String::from_utf8_lossy(raw) - .lines() - .filter_map(|line| serde_json::from_str::(line).ok()) - .filter(|event| { - matches!( - event.get("type").and_then(Value::as_str), - Some("user" | "step_start" | "text" | "reasoning" | "tool_use" | "step_finish") - ) - }) - .collect() -} - -fn count_opencode_turns(events: &[Value]) -> usize { - events - .iter() - .filter(|event| event.get("type").and_then(Value::as_str) == Some("step_finish")) - .count() -} - -fn count_codex_turns_after(events: &[Value], plan: &ReplayPlan) -> usize { - let boundary_call = plan - .batches - .last() - .and_then(|batch| batch.tool_calls.last()) - .map(|call| call.call_id.as_str()); - let boundary_index = boundary_call.and_then(|call_id| { - events.iter().rposition(|event| { - event.pointer("/payload/call_id").and_then(Value::as_str) == Some(call_id) - }) - }); - let continuation_events = boundary_index - .and_then(|index| events.get(index.saturating_add(1)..)) - .unwrap_or(events); - let turns = continuation_events - .iter() - .filter(|event| { - event.get("type").and_then(Value::as_str) == Some("response_item") - && event.pointer("/payload/role").and_then(Value::as_str) == Some("assistant") - }) - .count(); - if turns > 0 { - return turns; - } - // Some Codex releases emit tool calls without an assistant message for a - // short continuation. Such a stream is still a live turn; use the - // number of post-prefix tool calls as a conservative lower bound rather - // than incorrectly classifying a successful run as zero-step. - let continuation_calls = continuation_events - .iter() - .filter(|event| { - event.get("type").and_then(Value::as_str) == Some("response_item") - && matches!( - event.pointer("/payload/type").and_then(Value::as_str), - Some("function_call" | "custom_tool_call") - ) - }) - .count(); - usize::from(continuation_calls > 0) -} - -#[cfg(test)] -mod tests { - use std::path::PathBuf; - - use super::{ - CallRecord, NativeJsonlAgent, RunContext, TurnRecord, codex_native_session_id, - continuation_session_id, is_actionable_turn, opencode_event_is_nonce, - opencode_provider_config, parse_codex, parse_jsonl, parse_opencode, - redact_codex_transport_nonce, validate_codex_continuation, - }; - use crate::model::{AgentKind, PlaybackRequest, ReplayMode, ReplayPlan, ToolBatch, ToolCall}; - use serde_json::{Value, json}; - - #[test] - fn opencode_nonce_events_are_filtered_from_the_continued_stream() { - let nonce = "pvisor-opencode-resume-nonce"; - let events = vec![ - json!({"type": "step_start", "sessionID": "ses"}), - json!({"type": "user", "sessionID": "ses", "parts": [{"type": "text", "text": nonce}]}), - json!({"type": "text", "sessionID": "ses", "part": {"type": "text", "text": nonce}}), - json!({"type": "text", "sessionID": "ses", "part": {"type": "text", "text": "real text"}}), - json!({"type": "step_finish", "sessionID": "ses"}), - ]; - let kept: Vec = events - .iter() - .filter(|event| !opencode_event_is_nonce(event, nonce)) - .cloned() - .collect(); - let kinds: Vec<&str> = kept.iter().map(|e| e["type"].as_str().unwrap()).collect(); - assert_eq!(kinds, vec!["step_start", "text", "step_finish"]); - assert_eq!(kept[1]["part"]["text"], "real text"); - // An empty nonce (explicit boundary prompt mode) filters nothing. - for event in &events { - assert!(!opencode_event_is_nonce(event, "")); - } - } - - #[test] - fn opencode_provider_config_mirrors_recorded_sampling() { - let config = opencode_provider_config( - "openai/model-x", - Some("http://127.0.0.1:8000/v1"), - Some(0.0), - Some(1.0), - None, - ) - .unwrap(); - assert_eq!( - config, - json!({ - "provider": { - "openai": { - "options": {"baseURL": "http://127.0.0.1:8000/v1"}, - "models": {"model-x": {"options": {"temperature": 0.0, "topP": 1.0}}} - } - } - }) - ); - - // Without sampling overrides the endpoint still comes from the - // environment; the model entry stays registered so OpenCode can - // resolve an id that is absent from its fetched catalog. - let base_only = - opencode_provider_config("openai/model-x", Some("http://m:1/v1"), None, None, None) - .unwrap(); - assert_eq!( - base_only, - json!({"provider": {"openai": { - "options": {"baseURL": "http://m:1/v1"}, - "models": {"model-x": {}} - }}}) - ); - - // Nothing to pin: leave OpenCode on its environment-only defaults. - assert!(opencode_provider_config("openai/model-x", None, None, None, None).is_none()); - // A model without a provider namespace cannot be pinned either. - assert!( - opencode_provider_config("model-x", Some("http://m:1/v1"), Some(0.0), None, None) - .is_none() - ); - // Blank endpoints are ignored rather than written. - assert!(opencode_provider_config("openai/model-x", Some(" "), None, None, None).is_none()); - let with_steps = - opencode_provider_config("openai/model-x", Some("http://m:1/v1"), None, None, Some(7)) - .unwrap(); - assert_eq!(with_steps["agent"]["build"]["steps"], 7); - } - - #[test] - fn opencode_events_group_tool_parts_into_complete_turns() { - let source = [ - json!({"type":"user","sessionID":"ses-test","parts":[{"type":"text","text":"fix it"}]}), - json!({"type":"step_start","sessionID":"ses-test"}), - json!({"type":"text","sessionID":"ses-test","part":{"type":"text","text":"Inspecting"}}), - json!({"type":"tool_use","sessionID":"ses-test","part":{"type":"tool","callID":"call-1","tool":"bash","state":{"status":"completed","input":{"command":"pwd"},"output":"/workspace"}}}), - json!({"type":"step_finish","sessionID":"ses-test","part":{"reason":"tool-calls"}}), - json!({"type":"step_start","sessionID":"ses-test"}), - json!({"type":"text","sessionID":"ses-test","part":{"type":"text","text":"Done"}}), - json!({"type":"step_finish","sessionID":"ses-test","part":{"reason":"stop"}}), - ]; - let (turns, prompt, session) = parse_opencode(&source).unwrap(); - assert_eq!(prompt.as_deref(), Some("fix it")); - assert_eq!(session.as_deref(), Some("ses-test")); - assert_eq!(turns.len(), 2); - assert_eq!(turns[0].calls[0].name, "bash"); - assert_eq!(turns[0].calls[0].observation, "/workspace"); - assert_eq!(turns[1].text, "Done"); - } - - #[test] - fn reasoning_only_turn_is_not_an_actionable_next_step() { - let reasoning_only = TurnRecord { - start_event: 1, - end_event: 1, - text: " ".into(), - reasoning: "internal planning".into(), - calls: Vec::new(), - }; - assert!(!is_actionable_turn(&reasoning_only)); - - let visible_text = TurnRecord { - text: "continue".into(), - ..reasoning_only.clone() - }; - assert!(is_actionable_turn(&visible_text)); - - let tool_call = TurnRecord { - calls: vec![CallRecord { - call_event: 2, - output_event: 3, - call_id: "call-1".into(), - name: "exec_command".into(), - arguments: json!({"cmd": "pwd"}), - observation: Value::Null, - is_error: false, - complete: false, - }], - ..reasoning_only - }; - assert!(is_actionable_turn(&tool_call)); - } - - #[test] - fn codex_rollouts_accept_responses_and_custom_tool_calls() { - let source = [ - json!({"type":"session_meta","payload":{"id":"sess-test"}}), - json!({"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"fix it"}]}}), - json!({"type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"Inspecting"}]}}), - json!({"type":"response_item","payload":{"type":"custom_tool_call","call_id":"call-1","name":"exec","input":"{\"command\":\"pwd\"}"}}), - json!({"type":"response_item","payload":{"type":"custom_tool_call_output","call_id":"call-1","output":[{"type":"input_text","text":"/workspace"}]}}), - json!({"type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"Done"}]}}), - ]; - let (turns, prompt, session) = parse_codex(&source).unwrap(); - assert_eq!(prompt.as_deref(), Some("fix it")); - assert_eq!(session.as_deref(), Some("sess-test")); - assert_eq!(turns.len(), 2); - assert_eq!(turns[0].calls[0].name, "exec"); - assert_eq!(turns[0].calls[0].arguments["command"], "pwd"); - assert_eq!(turns[0].calls[0].output_event, 4); - assert_eq!(turns[1].text, "Done"); - } - - #[test] - fn codex_transport_nonce_is_redacted_from_model_echoes() { - let nonce = "pvisor-codex-resume-abc123"; - let mut event = json!({ - "payload": { - "summary": [{"text": format!("I received {nonce}")}], - "content": [{"text": format!("{nonce} should not persist")}] - } - }); - redact_codex_transport_nonce(&mut event, nonce); - assert!(!event.to_string().contains(nonce)); - assert_eq!(event["payload"]["summary"][0]["text"], "I received "); - } - - #[test] - fn codex_session_meta_accepts_legacy_top_level_id() { - let source = [ - json!({"type":"session_meta","id":"legacy-session"}), - json!({"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"fix it"}]}}), - ]; - let (_, _, session) = parse_codex(&source).unwrap(); - assert_eq!(session.as_deref(), Some("legacy-session")); - } - - #[test] - fn parser_tolerates_only_a_truncated_final_jsonl_line() { - let raw = b"{\"type\":\"user\",\"parts\":[{\"type\":\"text\",\"text\":\"hi\"}]}\n{"; - let events = parse_jsonl(raw, NativeJsonlAgent::Opencode.label()).unwrap(); - assert_eq!(events.len(), 1); - } - - #[test] - fn codex_native_identity_is_taken_from_the_trajectory() { - let native = json!({"session_id": "native-session"}); - assert_eq!(codex_native_session_id(&native).unwrap(), "native-session"); - assert!(codex_native_session_id(&json!({})).is_err()); - } - - #[test] - fn codex_native_identity_cannot_be_overridden_by_router_session() { - let plan = ReplayPlan { - agent: AgentKind::Codex, - source_path: PathBuf::from("/trajectory.jsonl"), - source_sha256: "sha".into(), - after_step: 1, - prefix_model_turns: 1, - native: json!({"session_id": "native-session", "user_prompt": "Original task"}), - original_next_action: None, - batches: Vec::new(), - }; - let request = PlaybackRequest { - agent: AgentKind::Codex, - trajectory: PathBuf::from("/trajectory.jsonl"), - after_step: 1, - workspace: PathBuf::from("/workspace"), - state_dir: PathBuf::from("/state"), - output_dir: PathBuf::from("/output"), - agent_entrypoint: None, - agent_runtime: None, - disallowed_tools: Vec::new(), - trajectory_assets: None, - session_id: Some("sweeval-router-key".into()), - max_steps: None, - mode: ReplayMode::ReplayAndContinue, - allow_stale_observations: false, - run_id: None, - disable_thinking: false, - boundary_user_prompt: None, - }; - let context = RunContext { - request: &request, - state_dir: std::path::Path::new("/state"), - output_dir: std::path::Path::new("/output"), - launch: None, - session_id: "sweeval-router-key", - nonce: "nonce", - }; - assert_eq!( - continuation_session_id(NativeJsonlAgent::Codex, &plan, &context).unwrap(), - "native-session" - ); - } - - #[test] - fn codex_continuation_rejects_a_fresh_session_without_the_staged_prefix() { - let plan = ReplayPlan { - agent: AgentKind::Codex, - source_path: PathBuf::from("/trajectory.jsonl"), - source_sha256: "sha".into(), - after_step: 1, - prefix_model_turns: 1, - native: json!({"session_id": "native-session", "user_prompt": "Original task"}), - original_next_action: None, - batches: vec![ToolBatch { - ordinal: 1, - native_locator: "events:0-3".into(), - assistant_text: "before".into(), - native: json!({"start_event": 0, "end_event": 3}), - tool_calls: vec![ToolCall { - ordinal: 1, - call_id: "boundary-call".into(), - name: "exec_command".into(), - arguments: json!({"cmd": "true"}), - original_observation: json!("old"), - original_is_error: false, - native: json!({"call_event": 2, "output_event": 3}), - }], - }], - }; - let fresh = vec![ - json!({"type":"session_meta","payload":{"id":"native-session"}}), - json!({"type":"response_item","payload":{"type":"message","role":"assistant","content":[]}}), - ]; - let error = validate_codex_continuation( - &fresh, - &plan, - "native-session", - std::path::Path::new("/rollout.jsonl"), - ) - .unwrap_err(); - assert!(error.message.contains("user task") || error.message.contains("boundary")); - - let resumed = vec![ - json!({"type":"session_meta","payload":{"id":"native-session"}}), - json!({"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"Original task"}]}}), - json!({"type":"response_item","payload":{"type":"message","role":"assistant","content":[]}}), - json!({"type":"response_item","payload":{"type":"function_call","call_id":"boundary-call"}}), - json!({"type":"response_item","payload":{"type":"message","role":"assistant","content":[]}}), - ]; - validate_codex_continuation( - &resumed, - &plan, - "native-session", - std::path::Path::new("/rollout.jsonl"), - ) - .unwrap(); - } -} diff --git a/crates/persisting-replay/src/adapter/mini_swe_agent.rs b/crates/persisting-replay/src/adapter/mini_swe_agent.rs deleted file mode 100644 index 53256d780..000000000 --- a/crates/persisting-replay/src/adapter/mini_swe_agent.rs +++ /dev/null @@ -1,326 +0,0 @@ -use serde_json::{Value, json}; - -use super::{RunContext, check_boundary, prepared_outcome, run_sdk_bridge}; -use crate::error::{ReplayError, ReplayErrorKind, ResultExt}; -use crate::io::{atomic_write_json, canonicalize, read_regular_file, sha256}; -use crate::journal::Journal; -use crate::model::{ - AdapterPlan, AgentKind, PlaybackRequest, ReplayMode, ReplayOutcome, ReplayPlan, ToolBatch, - ToolCall, -}; - -pub(super) fn build(request: &PlaybackRequest) -> Result { - build_mini_plan(request).map(AdapterPlan::MiniSweAgent) -} - -pub(super) fn execute( - plan: &ReplayPlan, - context: &RunContext<'_>, - journal: &mut Journal, -) -> Result { - run_mini(plan, context, journal) -} - -fn mini_reasoning(message: &Value) -> &str { - message - .get("reasoning_content") - .and_then(Value::as_str) - .or_else(|| { - message - .pointer("/extra/response/choices/0/message/reasoning_content") - .and_then(Value::as_str) - }) - .unwrap_or_default() -} - -fn mini_batch_signature(batch: &ToolBatch, message: &Value) -> Value { - json!({ - "text": batch.assistant_text.as_str(), - "reasoning": mini_reasoning(message), - "tools": batch.tool_calls.iter().map(|call| json!({ - "name": call.name.as_str(), - "arguments": &call.arguments, - })).collect::>(), - }) -} - -fn build_mini_plan(request: &PlaybackRequest) -> Result { - let raw = read_regular_file(&request.trajectory)?; - let value: Value = serde_json::from_slice(&raw).replay_context( - ReplayErrorKind::Trajectory, - "invalid mini-swe-agent trajectory JSON", - )?; - if value.get("trajectory_format").and_then(Value::as_str) != Some("mini-swe-agent-1.1") { - return Err(ReplayError::trajectory( - "mini-swe-agent trajectory_format must be mini-swe-agent-1.1", - )); - } - if value - .get("info") - .and_then(|info| info.get("mini_version")) - .and_then(Value::as_str) - != Some("2.4.6") - { - return Err(ReplayError::new( - ReplayErrorKind::UnsupportedVersion, - "mini-swe-agent trajectory requires exact version 2.4.6", - )); - } - let messages = value - .get("messages") - .and_then(Value::as_array) - .ok_or_else(|| ReplayError::trajectory("mini-swe-agent messages must be an array"))?; - let mut batches = Vec::new(); - for (message_index, message) in messages.iter().enumerate() { - let native_calls = mini_calls(message, message_index)?; - if native_calls.is_empty() { - continue; - } - let mut observations = Vec::new(); - for candidate in messages.iter().skip(message_index + 1) { - if !mini_calls(candidate, message_index + 1 + observations.len())?.is_empty() { - break; - } - if matches!( - candidate.get("role").and_then(Value::as_str), - Some("tool" | "user") - ) || candidate.get("type").and_then(Value::as_str) == Some("function_call_output") - { - observations.push(candidate); - if observations.len() == native_calls.len() { - break; - } - } - } - if observations.len() != native_calls.len() { - break; - } - let batch_is_in_prefix = batches.len() < request.after_step; - let calls = native_calls - .into_iter() - .zip(observations) - .enumerate() - .map(|(index, (native, observation))| { - let command = native["arguments"]["command"].as_str().unwrap_or_default(); - if mini_submission_in_prefix(batch_is_in_prefix, command) { - return Err(ReplayError::new( - ReplayErrorKind::UnsupportedVersion, - "mini-swe-agent submission cannot appear inside a replay prefix", - )); - } - let return_code = observation - .get("extra") - .and_then(|extra| extra.get("returncode")) - .and_then(Value::as_i64); - Ok(ToolCall { - ordinal: index + 1, - call_id: native["id"].as_str().unwrap().to_owned(), - name: "bash".into(), - arguments: native["arguments"].clone(), - original_observation: mini_observation(observation), - original_is_error: return_code.is_some_and(|code| code != 0), - native, - }) - }) - .collect::, _>>()?; - batches.push(ToolBatch { - ordinal: batches.len() + 1, - native_locator: format!("messages:{message_index}"), - tool_calls: calls, - assistant_text: message - .get("content") - .and_then(Value::as_str) - .unwrap_or_default() - .to_owned(), - native: json!({"message_index": message_index}), - }); - } - check_boundary(request.after_step, batches.len())?; - let original_next_action = if let Some(batch) = batches.get(request.after_step) { - let message_index = batch.native["message_index"].as_u64().ok_or_else(|| { - ReplayError::trajectory("mini-swe-agent next action lost message_index") - })? as usize; - let message = messages.get(message_index).ok_or_else(|| { - ReplayError::trajectory(format!( - "mini-swe-agent next action message index {message_index} is out of bounds" - )) - })?; - Some(mini_batch_signature(batch, message)) - } else { - None - }; - batches.truncate(request.after_step); - let boundary_message_index = batches.last().unwrap().native["message_index"] - .as_u64() - .ok_or_else(|| ReplayError::trajectory("mini-swe-agent batch lost message_index"))? - as usize; - let prefix_model_turns = value["messages"] - .as_array() - .ok_or_else(|| ReplayError::trajectory("mini-swe-agent messages must be an array"))? - .iter() - .take(boundary_message_index + 1) - .filter(|message| { - message - .get("extra") - .and_then(|extra| extra.get("response")) - .is_some_and(Value::is_object) - }) - .count(); - Ok(ReplayPlan { - agent: request.agent, - source_path: canonicalize( - &request.trajectory, - ReplayErrorKind::Trajectory, - "trajectory", - )?, - source_sha256: sha256(&raw), - after_step: request.after_step, - prefix_model_turns, - batches, - native: value, - original_next_action, - }) -} - -fn mini_submission_in_prefix(batch_is_in_prefix: bool, command: &str) -> bool { - batch_is_in_prefix - && command - .trim_start() - .starts_with("echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT") -} - -fn mini_calls(message: &Value, message_index: usize) -> Result, ReplayError> { - if let Some(actions) = message - .get("extra") - .and_then(|extra| extra.get("actions")) - .and_then(Value::as_array) - { - let native_calls = message - .get("tool_calls") - .and_then(Value::as_array) - .cloned() - .unwrap_or_default(); - return actions - .iter() - .enumerate() - .map(|(index, action)| { - let command = action - .get("command") - .and_then(Value::as_str) - .ok_or_else(|| { - ReplayError::trajectory(format!( - "mini-swe-agent message[{message_index}] has an invalid native action" - )) - })?; - let call_id = action - .get("tool_call_id") - .and_then(Value::as_str) - .or_else(|| { - native_calls - .get(index) - .and_then(|call| call.get("id")) - .and_then(Value::as_str) - }) - .map(str::to_owned) - .unwrap_or_else(|| format!("mini-{message_index}-{}", index + 1)); - Ok(json!({ - "id": call_id, - "arguments": {"command": command}, - "native": action, - })) - }) - .collect(); - } - let mut result = Vec::new(); - for (index, call) in message - .get("tool_calls") - .and_then(Value::as_array) - .into_iter() - .flatten() - .enumerate() - { - let function = call - .get("function") - .and_then(Value::as_object) - .ok_or_else(|| { - ReplayError::trajectory(format!( - "mini-swe-agent message[{message_index}] has an invalid tool call" - )) - })?; - if function.get("name").and_then(Value::as_str) != Some("bash") { - return Err(ReplayError::new( - ReplayErrorKind::UnsupportedVersion, - "mini-swe-agent playback supports only native bash actions", - )); - } - let arguments = match function.get("arguments") { - Some(Value::String(raw)) => serde_json::from_str(raw).replay_context( - ReplayErrorKind::Trajectory, - "invalid mini-swe-agent tool arguments", - )?, - Some(value) => value.clone(), - None => json!({}), - }; - if arguments.get("command").and_then(Value::as_str).is_none() { - return Err(ReplayError::trajectory( - "mini-swe-agent bash action has no command", - )); - } - result.push(json!({ - "id": call.get("id").and_then(Value::as_str) - .map(str::to_owned).unwrap_or_else(|| format!("mini-{message_index}-{}", index + 1)), - "arguments": arguments, - "native": call, - })); - } - Ok(result) -} - -fn mini_observation(message: &Value) -> Value { - message - .get("extra") - .and_then(|extra| extra.get("raw_output")) - .cloned() - .or_else(|| message.get("output").cloned()) - .or_else(|| message.get("content").cloned()) - .unwrap_or(Value::String(String::new())) -} - -fn run_mini( - plan: &ReplayPlan, - context: &RunContext<'_>, - journal: &mut Journal, -) -> Result { - let boundary = plan.batches.last().unwrap().native["message_index"] - .as_u64() - .unwrap() as usize; - let mut prepared = plan.native.clone(); - prepared["messages"] = - Value::Array(plan.native["messages"].as_array().unwrap()[..=boundary].to_vec()); - let path = context.output_dir.join("native/prepared-prefix.json"); - atomic_write_json(&path, &prepared)?; - journal.append( - "session_rebuilt", - [( - "prepared_only".into(), - json!(context.request.mode == ReplayMode::PrepareOnly), - )], - )?; - if context.request.mode == ReplayMode::PrepareOnly { - return Ok(prepared_outcome(path, context.request)); - } - run_sdk_bridge(plan, context, journal, AgentKind::MiniSweAgent) -} - -#[cfg(test)] -mod tests { - use super::mini_submission_in_prefix; - - #[test] - fn mini_submit_is_rejected_only_inside_the_selected_prefix() { - let command = " echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT"; - assert!(mini_submission_in_prefix(true, command)); - assert!(!mini_submission_in_prefix(false, command)); - assert!(!mini_submission_in_prefix(true, "echo still-working")); - } -} diff --git a/crates/persisting-replay/src/adapter/mod.rs b/crates/persisting-replay/src/adapter/mod.rs deleted file mode 100644 index 0e41d7dfd..000000000 --- a/crates/persisting-replay/src/adapter/mod.rs +++ /dev/null @@ -1,637 +0,0 @@ -use std::collections::BTreeMap; -use std::fs; -use std::path::{Path, PathBuf}; -use std::process::Command; -use std::time::Duration; - -mod claude_code; -mod generic; -mod mini_swe_agent; -mod openhands; -mod pi_agent; -mod runtime; -mod swe_agent; - -use serde_json::{Value, json}; - -use crate::error::{ReplayError, ReplayErrorKind, ResultExt}; -use crate::io::{atomic_write, atomic_write_json, read_regular_file, sha256}; -use crate::journal::Journal; -use crate::model::{ - AdapterPlan, AgentKind, FreshObservation, PlaybackRequest, ReplayMode, ReplayOutcome, - ReplayPlan, -}; -use crate::process::{ProcessSpec, run_process}; -pub(crate) use runtime::{LaunchSpec, resolve_launch_spec}; -use runtime::{ - configure_mini_python_environment, mini_python_library_path, mini_python_runtime, - pi_node_runtime, -}; - -const MAX_TOOL_OUTPUT_BYTES: usize = 4 * 1024 * 1024; - -pub struct RunContext<'a> { - pub request: &'a PlaybackRequest, - pub state_dir: &'a Path, - pub output_dir: &'a Path, - pub launch: Option<&'a LaunchSpec>, - pub session_id: &'a str, - pub nonce: &'a str, -} - -pub fn build_plan(request: &PlaybackRequest) -> Result { - match request.agent { - AgentKind::ClaudeCode => claude_code::build(request), - AgentKind::Codex => { - generic::build(request, generic::NativeJsonlAgent::Codex).map(AdapterPlan::Codex) - } - AgentKind::MiniSweAgent => mini_swe_agent::build(request), - AgentKind::Openhands => openhands::build(request), - AgentKind::Opencode => { - generic::build(request, generic::NativeJsonlAgent::Opencode).map(AdapterPlan::Opencode) - } - AgentKind::PiAgent => pi_agent::build(request), - AgentKind::SweAgent => swe_agent::build(request), - } -} - -pub fn run( - plan: &AdapterPlan, - context: &RunContext<'_>, - journal: &mut Journal, -) -> Result { - match plan { - AdapterPlan::ClaudeCode(plan) => claude_code::execute(plan, context, journal), - AdapterPlan::Codex(plan) => { - generic::execute(plan, context, journal, generic::NativeJsonlAgent::Codex) - } - AdapterPlan::MiniSweAgent(plan) => mini_swe_agent::execute(plan, context, journal), - AdapterPlan::Openhands(plan) => openhands::execute(plan, context, journal), - AdapterPlan::Opencode(plan) => { - generic::execute(plan, context, journal, generic::NativeJsonlAgent::Opencode) - } - AdapterPlan::PiAgent(plan) => pi_agent::execute(plan, context, journal), - AdapterPlan::SweAgent(plan) => swe_agent::execute(plan, context, journal), - } -} - -fn check_boundary(after_step: usize, complete: usize) -> Result<(), ReplayError> { - if after_step == 0 || after_step > complete { - return Err(ReplayError::trajectory(format!( - "requested after-step {after_step}, trajectory has {complete} complete batches" - ))); - } - Ok(()) -} - -fn prepared_outcome(path: PathBuf, request: &PlaybackRequest) -> ReplayOutcome { - ReplayOutcome { - status: "prepared".into(), - reconstructed_path: Some(path), - continued_path: None, - observations: Vec::new(), - continued_steps: 0, - metadata: with_boundary_user_prompt_metadata( - json!({"replay_only_execution": false}), - request, - false, - ), - } -} - -pub(super) fn with_boundary_user_prompt_metadata( - mut metadata: Value, - request: &PlaybackRequest, - injected: bool, -) -> Value { - let prompt = request.boundary_user_prompt(); - let mut detail = json!({ - "requested": prompt.is_some(), - "injected": injected, - "injection_count": usize::from(injected), - }); - if let Some(prompt) = prompt { - detail["sha256"] = json!(sha256(prompt.as_bytes())); - detail["length"] = json!(prompt.chars().count()); - if injected { - detail["position"] = json!("after_boundary_observation"); - } else { - detail["reason"] = json!(match request.mode { - ReplayMode::PrepareOnly => "prepare_only", - ReplayMode::ReplayOnly => "replay_only", - ReplayMode::ReplayAndContinue => "not_injected", - }); - } - } - metadata - .as_object_mut() - .expect("replay outcome metadata must be an object") - .insert("boundary_user_prompt".into(), detail); - metadata -} - -fn run_sdk_bridge( - plan: &ReplayPlan, - context: &RunContext<'_>, - journal: &mut Journal, - agent: AgentKind, -) -> Result { - let launch = context - .launch - .ok_or_else(|| ReplayError::continuation("SDK continuation has no launch spec"))?; - let native_dir = context.output_dir.join("native"); - let logs_dir = context.output_dir.join("logs"); - fs::create_dir_all(&native_dir) - .replay_context(ReplayErrorKind::Executor, "create native output directory")?; - fs::create_dir_all(&logs_dir) - .replay_context(ReplayErrorKind::Executor, "create Agent log directory")?; - - let runner_result = context - .state_dir - .join(format!("{}-runner-result.json", agent.as_str())); - let mode = match context.request.mode { - ReplayMode::ReplayOnly => "replay_only", - ReplayMode::ReplayAndContinue => "replay_and_continue", - ReplayMode::PrepareOnly => { - return Err(ReplayError::new( - ReplayErrorKind::Internal, - "prepare-only unexpectedly started an SDK runner", - )); - } - }; - let ( - program, - bridge_source, - bridge_name, - request_value, - reconstructed, - continued, - observations_path, - ) = match agent { - AgentKind::MiniSweAgent => { - let source = context.state_dir.join("mini-source.json"); - let reconstructed = native_dir.join("reconstructed-trajectory.json"); - let continued = native_dir.join("continued-trajectory.json"); - let observations = context.state_dir.join("mini-fresh-observations.json"); - atomic_write_json(&source, &plan.native)?; - let runtime = mini_python_runtime(&launch.entrypoint)?; - let program = runtime - .loader - .clone() - .unwrap_or_else(|| runtime.python.clone()); - ( - program, - include_str!("../../assets/mini_swe_agent_runner.py"), - "mini-swe-agent-runner.py", - json!({ - "source": source, - "reconstructed": reconstructed, - "continued": continued, - "observations": observations, - "result": runner_result, - "mode": mode, - "workspace": context.request.workspace, - "after_step": plan.after_step, - "max_steps": context.request.max_steps, - "session_id": context.session_id, - "boundary_user_prompt": context.request.boundary_user_prompt(), - }), - reconstructed, - continued, - Some(observations), - ) - } - AgentKind::SweAgent => { - let source = native_dir.join("continuation-source.traj"); - let run_output = native_dir.join("swe-agent-run"); - let reconstructed = native_dir.join("reconstructed-trajectory.traj"); - let continued = native_dir.join("continued-trajectory.traj"); - atomic_write_json(&source, &plan.native)?; - ( - launch.entrypoint.clone(), - include_str!("../../assets/swe_agent_runner.py"), - "swe-agent-runner.py", - json!({ - "trajectory": source, - "reconstructed": reconstructed, - "continued": continued, - "trajectory_assets": context.request.trajectory_assets, - "after_step": plan.after_step, - "max_steps": context.request.max_steps, - "mode": mode, - "result": runner_result, - "workspace": context.request.workspace, - "output_dir": run_output, - "boundary_user_prompt": context.request.boundary_user_prompt(), - }), - reconstructed, - continued, - None, - ) - } - AgentKind::PiAgent => { - let source = context.state_dir.join("pi-source.json"); - let reconstructed = native_dir.join("reconstructed-events.jsonl"); - let continued = native_dir.join("continued-events.jsonl"); - let observations = context.state_dir.join("pi-fresh-observations.json"); - atomic_write_json(&source, &plan.native)?; - let runtime = pi_node_runtime(&launch.entrypoint)?; - ( - runtime.node, - include_str!("../../assets/pi_agent_runner.mjs"), - "pi-agent-runner.mjs", - json!({ - "source": source, - "reconstructed": reconstructed, - "continued": continued, - "observations": observations, - "result": runner_result, - "mode": mode, - "workspace": context.request.workspace, - "after_step": plan.after_step, - "prefix_model_turns": plan.prefix_model_turns, - "max_steps": context.request.max_steps, - "session_id": context.session_id, - "boundary_user_prompt": context.request.boundary_user_prompt(), - "disable_thinking": context.request.disable_thinking, - "package_json": runtime.package_json, - "config_dir": context.state_dir.join("pi-config"), - "session_dir": context.state_dir.join("pi-sessions"), - }), - reconstructed, - continued, - Some(observations), - ) - } - _ => { - return Err(ReplayError::new( - ReplayErrorKind::Internal, - "SDK bridge selected for a non-SDK agent", - )); - } - }; - - let bridge = context.state_dir.join(bridge_name); - let request_path = context - .state_dir - .join(format!("{}-request.json", agent.as_str())); - atomic_write(&bridge, bridge_source.as_bytes())?; - atomic_write_json(&request_path, &request_value)?; - let mut command = agent_command(&program, context); - if agent == AgentKind::MiniSweAgent { - let runtime = mini_python_runtime(&launch.entrypoint)?; - if runtime.loader.is_some() { - let library_path = mini_python_library_path(&runtime)?.ok_or_else(|| { - ReplayError::continuation("bundled mini-swe-agent Python has no library path") - })?; - let argv0 = runtime - .virtual_env - .as_deref() - .map(|venv| venv.join("bin/python")) - .unwrap_or_else(|| runtime.python.clone()); - command - .arg("--argv0") - .arg(argv0) - .arg("--library-path") - .arg(library_path) - .arg(&runtime.python); - } - configure_mini_python_environment(&mut command, &runtime)?; - command.env("MSWEA_CONFIGURED", "true"); - command.env("MSWEA_COST_TRACKING", "ignore_errors"); - command.env("SWE_EVAL_MINI_RUNTIME", "1"); - } - command.arg(&bridge).arg(&request_path); - journal.append("continuation_started", std::iter::empty())?; - let log = logs_dir.join(format!("{}.log", agent.as_str())); - let output = run_process(ProcessSpec { - command, - stdin: None, - idle_timeout: None, - step_finish_limit: None, - stdout_redirect: None, - timeout: Duration::from_secs(24 * 60 * 60), - termination_grace: Duration::from_secs(2), - pipe_grace: Duration::from_millis(250), - retained_bytes: MAX_TOOL_OUTPUT_BYTES / 2, - log_path: log.clone(), - }) - .map_err(|error| ReplayError::new(ReplayErrorKind::Continuation, error.message))?; - if !output.status.success() { - let mut rendered = String::from_utf8_lossy(&output.stdout_tail).into_owned(); - if !output.stderr_tail.is_empty() { - rendered.push('\n'); - rendered.push_str(&String::from_utf8_lossy(&output.stderr_tail)); - } - return Err(ReplayError::classify_continuation( - format!( - "{} replay/continuation exited {}; see {}", - agent.as_str(), - output.status, - log.display() - ), - &rendered, - )); - } - - let runner: Value = serde_json::from_slice(&read_regular_file(&runner_result)?) - .replay_context( - ReplayErrorKind::Continuation, - "parse SDK replay runner result", - )?; - let expected_phase = if context.request.mode == ReplayMode::ReplayOnly { - "replayed" - } else { - "continued" - }; - if runner.get("phase").and_then(Value::as_str) != Some(expected_phase) - || runner.get("replayed_steps").and_then(Value::as_u64) != Some(plan.after_step as u64) - { - return Err(ReplayError::continuation(format!( - "{} runner returned an invalid replay boundary", - agent.as_str() - ))); - } - let runner_continued_steps = runner - .get("continued_steps") - .and_then(Value::as_u64) - .ok_or_else(|| ReplayError::continuation("SDK runner omitted continued_steps"))? - as usize; - let runner_agent_status = runner - .get("agent_status") - .and_then(Value::as_str) - .ok_or_else(|| ReplayError::continuation("SDK runner omitted agent_status"))?; - let prompt_injected = runner - .get("boundary_user_prompt_injected") - .and_then(Value::as_bool) - .ok_or_else(|| { - ReplayError::continuation("SDK runner omitted boundary_user_prompt_injected") - })?; - let expected_prompt_injected = context.request.mode == ReplayMode::ReplayAndContinue - && context.request.boundary_user_prompt().is_some(); - if prompt_injected != expected_prompt_injected { - return Err(ReplayError::continuation(format!( - "{} runner reported an invalid boundary user prompt injection state", - agent.as_str() - ))); - } - let status_is_valid = match context.request.mode { - ReplayMode::ReplayOnly => { - runner_agent_status == "not_started" && runner_continued_steps == 0 - } - ReplayMode::ReplayAndContinue => { - matches!(runner_agent_status, "completed" | "max_steps") - && context.request.max_steps.is_none_or(|max_steps| { - plan.prefix_model_turns + runner_continued_steps <= max_steps - }) - } - ReplayMode::PrepareOnly => false, - }; - if !status_is_valid { - return Err(ReplayError::continuation(format!( - "{} runner returned an invalid terminal status or step count", - agent.as_str() - ))); - } - let runner_trajectory = runner - .get("trajectory") - .and_then(Value::as_str) - .map(PathBuf::from) - .ok_or_else(|| ReplayError::continuation("SDK runner omitted trajectory"))?; - let expected_trajectory = if context.request.mode == ReplayMode::ReplayOnly { - &reconstructed - } else { - &continued - }; - if runner_trajectory != *expected_trajectory || !runner_trajectory.is_file() { - return Err(ReplayError::continuation(format!( - "{} runner produced an unexpected trajectory path", - agent.as_str() - ))); - } - - let (observations, continued_steps) = - if matches!(agent, AgentKind::MiniSweAgent | AgentKind::PiAgent) { - let raw_observations: Vec = serde_json::from_slice(&read_regular_file( - observations_path.as_ref().expect("SDK observations path"), - )?) - .replay_context( - ReplayErrorKind::Trajectory, - format!("parse {} fresh observations", agent.as_str()), - )?; - if raw_observations.len() != plan.calls().count() { - return Err(ReplayError::trajectory(format!( - "{} output lost replayed observations", - agent.as_str() - ))); - } - let observations = plan - .calls() - .zip(raw_observations) - .map(|(call, value)| FreshObservation { - call_id: call.call_id.clone(), - content: value.get("content").cloned().unwrap_or(Value::Null), - is_error: value - .get("is_error") - .and_then(Value::as_bool) - .unwrap_or(false), - return_code: value - .get("return_code") - .and_then(Value::as_i64) - .map(|code| code as i32), - duration_ms: value - .get("duration_ms") - .and_then(Value::as_u64) - .unwrap_or_default() as u128, - truncated: false, - metadata: BTreeMap::new(), - }) - .collect::>(); - let measured = if agent == AgentKind::MiniSweAgent { - let continued_value: Value = - serde_json::from_slice(&read_regular_file(&runner_trajectory)?) - .replay_context( - ReplayErrorKind::Trajectory, - "parse continued mini-swe-agent trajectory", - )?; - continued_value["messages"] - .as_array() - .map(|messages| { - messages - .iter() - .filter(|message| { - message - .get("extra") - .and_then(|extra| extra.get("actions")) - .and_then(Value::as_array) - .is_some_and(|actions| !actions.is_empty()) - }) - .count() - }) - .unwrap_or_default() - .saturating_sub(plan.after_step) - } else { - let raw = read_regular_file(&runner_trajectory)?; - let turns = String::from_utf8_lossy(&raw) - .lines() - .filter_map(|line| serde_json::from_str::(line).ok()) - .filter(|event| event.get("type").and_then(Value::as_str) == Some("turn_end")) - .count(); - turns.saturating_sub(plan.prefix_model_turns) - }; - if measured != runner_continued_steps { - return Err(ReplayError::trajectory(format!( - "{} runner result disagrees with its trajectory", - agent.as_str() - ))); - } - (observations, measured) - } else { - let replayed: Value = serde_json::from_slice(&read_regular_file(&runner_trajectory)?) - .replay_context( - ReplayErrorKind::Trajectory, - "parse SWE-agent replay runner trajectory", - )?; - let steps = replayed["trajectory"].as_array().ok_or_else(|| { - ReplayError::trajectory("continued SWE-agent trajectory is invalid") - })?; - if steps.len() < plan.after_step { - return Err(ReplayError::trajectory( - "SWE-agent output lost replayed steps", - )); - } - let observations = plan - .calls() - .zip(steps.iter()) - .map(|(call, step)| FreshObservation { - call_id: call.call_id.clone(), - content: step.get("observation").cloned().unwrap_or(Value::Null), - is_error: false, - return_code: None, - duration_ms: 0, - truncated: false, - metadata: BTreeMap::new(), - }) - .collect::>(); - let continued_steps = steps[plan.after_step..] - .iter() - .filter(|step| { - step.get("action") - .and_then(Value::as_str) - .is_some_and(|action| !action.trim().is_empty()) - }) - .count(); - if continued_steps != runner_continued_steps { - return Err(ReplayError::trajectory( - "SWE-agent runner result disagrees with its trajectory", - )); - } - (observations, continued_steps) - }; - if context.request.mode == ReplayMode::ReplayAndContinue && continued_steps == 0 { - return Err(ReplayError::continuation(format!( - "{} produced no actionable continuation step; see {}", - agent.as_str(), - log.display() - ))); - } - let comparisons: Vec<_> = plan - .calls() - .zip(&observations) - .map(|(call, fresh)| { - json!({ - "call_id": call.call_id, - "tool": call.name, - "exact": call.original_observation == fresh.content - && call.original_is_error == fresh.is_error, - "original_is_error": call.original_is_error, - "replayed_is_error": fresh.is_error, - }) - }) - .collect(); - atomic_write_json( - &context.output_dir.join("observation-comparison.json"), - &comparisons, - )?; - journal.append( - "continuation_finished", - [ - ("return_code".into(), json!(output.status.code())), - ("continued_steps".into(), json!(continued_steps)), - ], - )?; - Ok(ReplayOutcome { - status: if context.request.mode == ReplayMode::ReplayOnly { - "replayed".into() - } else { - runner_agent_status.into() - }, - reconstructed_path: Some(reconstructed), - continued_path: (context.request.mode == ReplayMode::ReplayAndContinue) - .then_some(continued), - observations, - continued_steps, - metadata: with_boundary_user_prompt_metadata( - json!({"sdk_bridge": bridge_name}), - context.request, - prompt_injected, - ), - }) -} - -fn agent_command(entrypoint: &Path, context: &RunContext<'_>) -> Command { - let mut command = Command::new(entrypoint); - command.current_dir(&context.request.workspace); - sanitized_environment(&mut command, context.request.agent == AgentKind::ClaudeCode); - if context.request.agent != AgentKind::ClaudeCode { - command.env("X_LITELLM_SESSION_ID", context.session_id); - command.env( - "LITELLM_EXTRA_HEADERS", - json!({"X-LiteLLM-Session-ID": context.session_id}).to_string(), - ); - } - command -} - -fn sanitized_environment(command: &mut Command, strip_credentials: bool) { - command.env_clear(); - for (name, value) in std::env::vars_os() { - let rendered = name.to_string_lossy().to_ascii_uppercase(); - if !environment_name_allowed(&rendered, strip_credentials) { - continue; - } - command.env(name, value); - } -} - -fn environment_name_allowed(rendered: &str, strip_credentials: bool) -> bool { - let credential = ["API_KEY", "TOKEN", "SECRET", "AUTHORIZATION", "PASSWORD"] - .iter() - .any(|fragment| rendered.contains(fragment)); - let claude_provider_override = strip_credentials - && matches!( - rendered, - "CLAUDE_CODE_USE_BEDROCK" | "CLAUDE_CODE_USE_VERTEX" | "CLAUDE_CODE_USE_FOUNDRY" - ); - !(claude_provider_override - || matches!(rendered, "PYTHONHOME" | "PYTHONPATH" | "VIRTUAL_ENV") - || strip_credentials && credential) -} - -#[cfg(test)] -mod tests { - use super::environment_name_allowed; - - #[test] - fn direct_agents_keep_model_credentials_but_claude_tools_do_not() { - assert!(environment_name_allowed("OPENAI_API_KEY", false)); - assert!(environment_name_allowed("LLM_API_KEY", false)); - assert!(environment_name_allowed("OPENAI_BASE_URL", false)); - assert!(!environment_name_allowed("OPENAI_API_KEY", true)); - assert!(!environment_name_allowed("ANTHROPIC_AUTH_TOKEN", true)); - assert!(!environment_name_allowed("CLAUDE_CODE_USE_BEDROCK", true)); - assert!(!environment_name_allowed("CLAUDE_CODE_USE_VERTEX", true)); - assert!(!environment_name_allowed("CLAUDE_CODE_USE_FOUNDRY", true)); - assert!(!environment_name_allowed("PYTHONPATH", false)); - } -} diff --git a/crates/persisting-replay/src/adapter/openhands.rs b/crates/persisting-replay/src/adapter/openhands.rs deleted file mode 100644 index be5837407..000000000 --- a/crates/persisting-replay/src/adapter/openhands.rs +++ /dev/null @@ -1,856 +0,0 @@ -use std::collections::{BTreeMap, BTreeSet}; -use std::fs; -use std::path::Path; -use std::process::Command; -use std::time::Duration; - -use serde_json::{Value, json}; - -use super::{ - LaunchSpec, MAX_TOOL_OUTPUT_BYTES, RunContext, agent_command, check_boundary, prepared_outcome, - with_boundary_user_prompt_metadata, -}; -use crate::error::{ReplayError, ReplayErrorKind, ResultExt}; -use crate::io::{atomic_write_json, canonicalize, read_regular_file, sha256}; -use crate::journal::Journal; -use crate::model::{ - AdapterPlan, FreshObservation, PlaybackRequest, ReplayMode, ReplayOutcome, ReplayPlan, - ToolBatch, ToolCall, -}; -use crate::process::{ProcessSpec, run_process}; - -pub(super) fn build(request: &PlaybackRequest) -> Result { - build_openhands_plan(request).map(AdapterPlan::Openhands) -} - -pub(super) fn execute( - plan: &ReplayPlan, - context: &RunContext<'_>, - journal: &mut Journal, -) -> Result { - run_openhands(plan, context, journal) -} - -fn build_openhands_plan(request: &PlaybackRequest) -> Result { - let raw = read_regular_file(&request.trajectory)?; - let events: Vec = serde_json::from_slice(&raw).replay_context( - ReplayErrorKind::Trajectory, - "invalid OpenHands trajectory JSON", - )?; - if events.is_empty() { - return Err(ReplayError::trajectory( - "OpenHands trajectory must be a non-empty event array", - )); - } - let mut ids = BTreeSet::new(); - for event in &events { - let id = event_id(event)?; - if !ids.insert(id) { - return Err(ReplayError::trajectory(format!( - "duplicate OpenHands event id {id}" - ))); - } - } - let observations: BTreeMap = events - .iter() - .filter_map(|event| { - (event.get("observation").is_some() && !event["observation"].is_null()) - .then(|| { - event - .get("cause") - .and_then(Value::as_i64) - .map(|cause| (cause, event)) - }) - .flatten() - }) - .collect(); - let supported = ["run", "read", "edit", "run_ipython", "think"]; - let mut batches = Vec::new(); - for action in &events { - let action_name = action.get("action").and_then(Value::as_str); - if action.get("source").and_then(Value::as_str) != Some("agent") - || matches!(action_name, None | Some("system" | "finish" | "message")) - { - continue; - } - let action_name = action_name.unwrap(); - if !supported.contains(&action_name) { - return Err(ReplayError::new( - ReplayErrorKind::UnsupportedVersion, - format!("unsupported OpenHands action {action_name:?}"), - )); - } - let id = event_id(action)?; - let Some(observation) = observations.get(&id) else { - break; - }; - batches.push(ToolBatch { - ordinal: batches.len() + 1, - native_locator: format!("event:{id}"), - tool_calls: vec![ToolCall { - ordinal: batches.len() + 1, - call_id: id.to_string(), - name: action_name.to_owned(), - arguments: action.get("args").cloned().unwrap_or_else(|| json!({})), - original_observation: json!({ - "observation": observation.get("observation"), - "message": observation.get("message"), - "args": observation.get("args"), - }), - original_is_error: observation.get("observation").and_then(Value::as_str) - == Some("error"), - native: action.clone(), - }], - assistant_text: action - .get("args") - .and_then(|args| args.get("thought")) - .and_then(Value::as_str) - .unwrap_or_default() - .to_owned(), - native: json!({"observation_id": observation.get("id")}), - }); - } - check_boundary(request.after_step, batches.len())?; - batches.truncate(request.after_step); - let boundary_id = batches.last().unwrap().tool_calls[0] - .call_id - .parse::() - .unwrap(); - let initial_user_event = events - .iter() - .find(|event| { - event.get("source").and_then(Value::as_str) == Some("user") - && event.get("action").and_then(Value::as_str) == Some("message") - && event.get("id").and_then(Value::as_i64).unwrap_or(i64::MAX) <= boundary_id - }) - .cloned() - .ok_or_else(|| { - ReplayError::trajectory("OpenHands replay has no user message through the boundary") - })?; - let original_next_action = events.iter().find_map(|event| { - let id = event.get("id").and_then(Value::as_i64)?; - let action = event.get("action").and_then(Value::as_str)?; - if id <= boundary_id - || event.get("source").and_then(Value::as_str) != Some("agent") - || action == "finish" - { - return None; - } - Some(openhands_action_signature(event)) - }); - Ok(ReplayPlan { - agent: request.agent, - source_path: canonicalize( - &request.trajectory, - ReplayErrorKind::Trajectory, - "trajectory", - )?, - source_sha256: sha256(&raw), - after_step: request.after_step, - prefix_model_turns: request.after_step, - batches, - native: json!({"events": events, "initial_user_event": initial_user_event}), - original_next_action, - }) -} - -fn openhands_action_signature(event: &Value) -> Value { - let response_message = event - .get("tool_call_metadata") - .and_then(|metadata| metadata.get("model_response")) - .and_then(|response| response.get("choices")) - .and_then(Value::as_array) - .and_then(|choices| choices.first()) - .and_then(|choice| choice.get("message")); - let text = response_message - .and_then(|message| message.get("content")) - .and_then(Value::as_str) - .unwrap_or_default(); - let reasoning = response_message - .and_then(|message| message.get("reasoning_content")) - .and_then(Value::as_str) - .or_else(|| { - response_message.is_none().then(|| { - event - .get("args") - .and_then(|args| args.get("thought")) - .and_then(Value::as_str) - .unwrap_or_default() - }) - }) - .unwrap_or_default(); - json!({ - "text": text, - "reasoning": reasoning, - "tools": [{ - "name": event.get("action").and_then(Value::as_str).unwrap_or_default(), - "arguments": openhands_reconstructed_tool_arguments(event), - }], - }) -} - -fn openhands_reconstructed_tool_metadata(event: &Value) -> Result { - let event_id = event_id(event)?; - let action = event - .get("action") - .and_then(Value::as_str) - .ok_or_else(|| ReplayError::trajectory("OpenHands replay action has no action"))?; - let tool_name = match action { - "run" => "execute_bash", - "read" | "edit" => "str_replace_editor", - "run_ipython" => "execute_ipython_cell", - "think" => "think", - _ => { - return Err(ReplayError::new( - ReplayErrorKind::UnsupportedVersion, - format!("unsupported OpenHands action {action:?}"), - )); - } - }; - let tool_call_id = format!("sandbox-playback-replay-{event_id}"); - let arguments = openhands_reconstructed_tool_arguments(event); - let serialized_arguments = serde_json::to_string(&arguments).replay_context( - ReplayErrorKind::Internal, - "serialize reconstructed OpenHands tool arguments", - )?; - let thought = event - .get("args") - .and_then(|args| args.get("thought")) - .and_then(Value::as_str) - .filter(|thought| !thought.is_empty()) - .map(str::to_owned); - Ok(json!({ - "function_name": tool_name, - "tool_call_id": tool_call_id.clone(), - "total_calls_in_response": 1, - "model_response": { - "id": format!("sandbox-playback-response-{event_id}"), - "created": 0, - "model": "sandbox-playback/reconstructed", - "object": "chat.completion", - "choices": [{ - "index": 0, - "finish_reason": "tool_calls", - "message": { - "role": "assistant", - "content": thought, - "tool_calls": [{ - "id": tool_call_id, - "type": "function", - "function": { - "name": tool_name, - "arguments": serialized_arguments, - }, - }], - }, - }], - }, - })) -} - -fn openhands_reconstructed_tool_arguments(event: &Value) -> Value { - let action = event - .get("action") - .and_then(Value::as_str) - .unwrap_or_default(); - let source = event.get("args").cloned().unwrap_or_else(|| json!({})); - match action { - "run" => { - let mut arguments = serde_json::Map::from_iter([( - "command".to_owned(), - source.get("command").cloned().unwrap_or_else(|| json!("")), - )]); - if let Some(value) = source.get("is_input") { - arguments.insert( - "is_input".to_owned(), - if let Some(value) = value.as_bool() { - Value::String(value.to_string()) - } else { - value.clone() - }, - ); - } - if let Some(value) = source.get("timeout").filter(|value| !value.is_null()) { - arguments.insert("timeout".to_owned(), value.clone()); - } - Value::Object(arguments) - } - "run_ipython" => json!({ - "code": source.get("code").cloned().unwrap_or_else(|| json!("")), - }), - "read" => { - let mut arguments = serde_json::Map::from_iter([ - ("command".to_owned(), json!("view")), - ( - "path".to_owned(), - source.get("path").cloned().unwrap_or_else(|| json!("")), - ), - ]); - if let Some(value) = source.get("view_range").filter(|value| !value.is_null()) { - arguments.insert("view_range".to_owned(), value.clone()); - } - Value::Object(arguments) - } - "edit" => { - let mut arguments = serde_json::Map::new(); - for key in [ - "command", - "path", - "file_text", - "old_str", - "new_str", - "insert_line", - "view_range", - ] { - if let Some(value) = source.get(key) { - arguments.insert(key.to_owned(), value.clone()); - } - } - arguments - .entry("command".to_owned()) - .or_insert(json!("str_replace")); - Value::Object(arguments) - } - "think" => json!({ - "thought": source.get("thought").cloned().unwrap_or_else(|| json!("")), - }), - _ => source, - } -} - -fn event_id(event: &Value) -> Result { - event - .get("id") - .and_then(Value::as_i64) - .ok_or_else(|| ReplayError::trajectory("OpenHands event has no integer id")) -} - -fn run_openhands( - plan: &ReplayPlan, - context: &RunContext<'_>, - journal: &mut Journal, -) -> Result { - let events = plan.native["events"].as_array().unwrap(); - let initial = plan.native["initial_user_event"].clone(); - let boundary_id = plan.batches.last().unwrap().tool_calls[0] - .call_id - .parse::() - .unwrap(); - let mut prepared_events = vec![initial.clone()]; - for event in events { - if event_id(event)? > boundary_id { - break; - } - if event == &initial || event.get("action").and_then(Value::as_str) == Some("system") { - continue; - } - if event.get("action").is_some() && !event["action"].is_null() { - let mut reconstructed = event.clone(); - if reconstructed.get("source").and_then(Value::as_str) == Some("agent") - && matches!( - reconstructed.get("action").and_then(Value::as_str), - Some("run" | "read" | "edit" | "run_ipython" | "think") - ) - && reconstructed - .get("tool_call_metadata") - .is_none_or(Value::is_null) - { - reconstructed["tool_call_metadata"] = - openhands_reconstructed_tool_metadata(&reconstructed)?; - } - prepared_events.push(reconstructed); - } - } - let prompt_injected = if context.request.mode == ReplayMode::ReplayAndContinue { - if let Some(prompt) = context.request.boundary_user_prompt() { - prepared_events.push(openhands_boundary_user_prompt_event( - &initial, events, prompt, - )?); - true - } else { - false - } - } else { - false - }; - let prepared = context - .output_dir - .join("native/prepared-replay-events.json"); - atomic_write_json(&prepared, &prepared_events)?; - journal.append( - "session_rebuilt", - [ - ( - "prepared_only".into(), - json!(context.request.mode == ReplayMode::PrepareOnly), - ), - ( - "boundary_user_prompt_injected".into(), - json!(prompt_injected), - ), - ], - )?; - if context.request.mode == ReplayMode::PrepareOnly { - return Ok(prepared_outcome(prepared, context.request)); - } - let launch = context - .launch - .ok_or_else(|| ReplayError::continuation("OpenHands replay has no launch spec"))?; - let replayed_trajectory = match context.request.mode { - ReplayMode::ReplayOnly => context - .output_dir - .join("native/reconstructed-trajectory.json"), - ReplayMode::ReplayAndContinue => { - context.output_dir.join("native/continued-trajectory.json") - } - ReplayMode::PrepareOnly => unreachable!("prepare-only returned before OpenHands launch"), - }; - let mut command = agent_command(&launch.entrypoint, context); - command.args(["-m", "openhands.core.main"]); - command.env("REPLAY_TRAJECTORY_PATH", &prepared); - command.env("SAVE_TRAJECTORY_PATH", &replayed_trajectory); - command.env("FILE_STORE", "local"); - command.env( - "FILE_STORE_PATH", - context.state_dir.join("openhands-file-store"), - ); - command.env("RUNTIME", "local"); - command.env("SU_TO_USER", "false"); - command.env("RUN_AS_OPENHANDS", "false"); - command.env("SKIP_DEPENDENCY_CHECK", "1"); - command.env("INIT_PLUGIN_TIMEOUT", "240"); - command.env("AGENT_ENABLE_PROMPT_EXTENSIONS", "false"); - command.env("AGENT_ENABLE_BROWSING", "false"); - command.env("ENABLE_BROWSER", "false"); - command.env("SANDBOX_ENABLE_AUTO_LINT", "true"); - command.env( - "SANDBOX_VOLUMES", - format!("{}:/workspace:rw", context.request.workspace.display()), - ); - prepend_openhands_runtime_tools(&mut command, launch)?; - command.env( - "OPENAI_CUSTOM_HEADERS", - format!("X-LiteLLM-Session-ID: {}", context.session_id), - ); - let iteration_limit = match context.request.mode { - ReplayMode::ReplayOnly => Some(plan.prefix_model_turns), - ReplayMode::ReplayAndContinue => context.request.max_steps, - ReplayMode::PrepareOnly => None, - }; - if let Some(max) = iteration_limit { - command.env("MAX_ITERATIONS", max.to_string()); - } - journal.append("continuation_started", std::iter::empty())?; - let log = context.output_dir.join("logs/openhands.log"); - fs::create_dir_all(log.parent().expect("OpenHands log has a parent")) - .replay_context(ReplayErrorKind::Executor, "create OpenHands log directory")?; - let output = run_process(ProcessSpec { - command, - stdin: Some(b"\n".to_vec()), - idle_timeout: None, - step_finish_limit: None, - stdout_redirect: None, - timeout: Duration::from_secs(24 * 60 * 60), - termination_grace: Duration::from_secs(2), - pipe_grace: Duration::from_millis(250), - retained_bytes: MAX_TOOL_OUTPUT_BYTES / 2, - log_path: log.clone(), - }) - .map_err(|error| ReplayError::new(ReplayErrorKind::Continuation, error.message))?; - let mut rendered = String::from_utf8_lossy(&output.stdout_tail).into_owned(); - if !output.stderr_tail.is_empty() { - rendered.push('\n'); - rendered.push_str(&String::from_utf8_lossy(&output.stderr_tail)); - } - let fatal_marker = openhands_fatal_controller_marker(&rendered); - if output.timed_out || !output.status.success() || !replayed_trajectory.is_file() { - let detail = fatal_marker - .map(|marker| format!("; OpenHands controller reported {marker:?}")) - .unwrap_or_default(); - return Err(ReplayError::classify_continuation( - format!( - "OpenHands replay/continuation exited {}{detail}; see {}", - output.status, - log.display() - ), - &rendered, - )); - } - if let Some(marker) = fatal_marker { - return Err(ReplayError::classify_continuation( - format!( - "OpenHands controller reported {marker:?} despite exiting successfully; partial trajectory retained at {}; see {}", - replayed_trajectory.display(), - log.display() - ), - &rendered, - )); - } - let continued_events: Vec = - serde_json::from_slice(&read_regular_file(&replayed_trajectory)?).replay_context( - ReplayErrorKind::Trajectory, - "parse replayed OpenHands trajectory", - )?; - let complete = openhands_complete_batches(&continued_events)?; - if complete.len() < plan.after_step { - return Err(ReplayError::trajectory( - "OpenHands output lost replayed action/observation batches", - )); - } - if context.request.mode == ReplayMode::ReplayOnly && complete.len() != plan.after_step { - return Err(ReplayError::continuation(format!( - "OpenHands replay-only crossed the selected boundary: expected {} actions, observed {}", - plan.after_step, - complete.len() - ))); - } - if context - .request - .max_steps - .is_some_and(|max_steps| complete.len() > max_steps) - { - return Err(ReplayError::continuation(format!( - "OpenHands exceeded the total max_steps budget: allowed {}, observed {} actions", - context.request.max_steps.unwrap(), - complete.len() - ))); - } - let replayed = &complete[..plan.after_step]; - let observations = plan - .calls() - .zip(replayed.iter()) - .map(|(call, (_, observation))| FreshObservation { - call_id: call.call_id.clone(), - content: openhands_observation_content(observation), - is_error: observation.get("observation").and_then(Value::as_str) == Some("error"), - return_code: None, - duration_ms: 0, - truncated: false, - metadata: BTreeMap::new(), - }) - .collect::>(); - let comparisons = plan - .calls() - .zip(&observations) - .map(|(call, fresh)| { - json!({ - "call_id": call.call_id, - "tool": call.name, - "exact": call.original_observation == fresh.content - && call.original_is_error == fresh.is_error, - "original_is_error": call.original_is_error, - "replayed_is_error": fresh.is_error, - }) - }) - .collect::>(); - atomic_write_json( - &context.output_dir.join("observation-comparison.json"), - &comparisons, - )?; - let continued_steps = complete.len() - plan.after_step; - journal.append( - "continuation_finished", - [ - ("continued_steps".into(), json!(continued_steps)), - ("agent_error".into(), Value::Null), - ], - )?; - let reached_max_steps = context.request.mode == ReplayMode::ReplayAndContinue - && context - .request - .max_steps - .is_some_and(|max_steps| complete.len() == max_steps) - && rendered.contains("Agent reached maximum iteration"); - Ok(ReplayOutcome { - status: if context.request.mode == ReplayMode::ReplayOnly { - "replayed".into() - } else if reached_max_steps { - "max_steps".into() - } else { - "completed".into() - }, - reconstructed_path: (context.request.mode == ReplayMode::ReplayOnly) - .then_some(replayed_trajectory.clone()), - continued_path: (context.request.mode == ReplayMode::ReplayAndContinue) - .then_some(replayed_trajectory), - observations, - continued_steps, - metadata: with_boundary_user_prompt_metadata(json!({}), context.request, prompt_injected), - }) -} - -fn openhands_boundary_user_prompt_event( - initial: &Value, - events: &[Value], - prompt: &str, -) -> Result { - let next_id = events - .iter() - .map(event_id) - .collect::, _>>()? - .into_iter() - .max() - .unwrap_or_default() - .checked_add(1) - .ok_or_else(|| ReplayError::trajectory("OpenHands event IDs are exhausted"))?; - let mut event = initial.clone(); - let object = event - .as_object_mut() - .ok_or_else(|| ReplayError::trajectory("OpenHands initial user event is not an object"))?; - object.insert("id".into(), json!(next_id)); - object.insert("source".into(), json!("user")); - object.insert("action".into(), json!("message")); - object.insert("args".into(), json!({"content": prompt})); - object.remove("cause"); - object.remove("observation"); - Ok(event) -} - -fn openhands_fatal_controller_marker(output: &str) -> Option<&'static str> { - if output.contains("Agent reached maximum iteration") { - return None; - } - [ - "AgentState.ERROR", - "Error while running the agent", - "There was an unexpected error while running the agent", - ] - .into_iter() - .find(|marker| output.contains(marker)) -} - -fn openhands_observation_content(observation: &Value) -> Value { - json!({ - "observation": observation.get("observation"), - "message": observation.get("message"), - "args": observation.get("args"), - }) -} - -fn openhands_complete_batches(events: &[Value]) -> Result, ReplayError> { - let mut observations = BTreeMap::new(); - for event in events { - let Some(cause) = event - .get("observation") - .filter(|value| !value.is_null()) - .and_then(|_| event.get("cause")) - .and_then(Value::as_i64) - else { - continue; - }; - if observations.insert(cause, event).is_some() { - return Err(ReplayError::trajectory(format!( - "multiple OpenHands observations for action {cause}" - ))); - } - } - - let supported = ["run", "read", "edit", "run_ipython", "think"]; - let mut batches = Vec::new(); - for event in events { - let action = event.get("action").and_then(Value::as_str); - if event.get("source").and_then(Value::as_str) != Some("agent") - || matches!(action, None | Some("system" | "finish" | "message")) - { - continue; - } - let action = action.unwrap(); - if !supported.contains(&action) { - return Err(ReplayError::new( - ReplayErrorKind::UnsupportedVersion, - format!("unsupported OpenHands action {action:?}"), - )); - } - let id = event_id(event)?; - let Some(observation) = observations.get(&id) else { - break; - }; - batches.push((event, *observation)); - } - Ok(batches) -} - -fn prepend_openhands_runtime_tools( - command: &mut Command, - launch: &LaunchSpec, -) -> Result<(), ReplayError> { - let inferred_root = launch - .entrypoint - .parent() - .and_then(Path::parent) - .unwrap_or_else(|| Path::new("/")); - let tools = launch - .runtime_root - .as_deref() - .unwrap_or(inferred_root) - .join("tools"); - if !tools.is_dir() { - return Ok(()); - } - let current = std::env::var_os("PATH").unwrap_or_else(|| "/usr/bin:/bin".into()); - let paths = std::iter::once(tools.clone()).chain(std::env::split_paths(¤t)); - let path = std::env::join_paths(paths).map_err(|error| { - ReplayError::configuration(format!( - "cannot prepend OpenHands runtime tools {} to PATH: {error}", - tools.display() - )) - })?; - command.env("PATH", path); - Ok(()) -} - -#[cfg(test)] -mod tests { - use std::fs; - use std::process::Command; - - use serde_json::{Value, json}; - - use super::{ - LaunchSpec, openhands_action_signature, openhands_boundary_user_prompt_event, - openhands_complete_batches, openhands_fatal_controller_marker, - openhands_observation_content, openhands_reconstructed_tool_metadata, - prepend_openhands_runtime_tools, - }; - - #[test] - fn openhands_boundary_prompt_is_a_unique_native_user_message() { - let initial = json!({ - "id": 0, - "source": "user", - "action": "message", - "args": {"content": "original task"}, - "timestamp": "source timestamp" - }); - let events = vec![ - initial.clone(), - json!({"id": 7, "source": "agent", "action": "run", "args": {"command": "pwd"}}), - json!({"id": 8, "source": "environment", "observation": "run", "cause": 7}), - ]; - - let prompt = - openhands_boundary_user_prompt_event(&initial, &events, "review O-prime N").unwrap(); - - assert_eq!(prompt["id"], 9); - assert_eq!(prompt["source"], "user"); - assert_eq!(prompt["action"], "message"); - assert_eq!(prompt["args"]["content"], "review O-prime N"); - assert_eq!(prompt["timestamp"], "source timestamp"); - } - - #[test] - fn openhands_reconstructs_legacy_native_tool_metadata() { - let event = json!({ - "id": 7, - "source": "agent", - "action": "read", - "args": {"path": "/workspace/file", "view_range": [1, 2], "thought": "inspect"}, - }); - let metadata = openhands_reconstructed_tool_metadata(&event).unwrap(); - assert_eq!(metadata["function_name"], "str_replace_editor"); - assert_eq!(metadata["tool_call_id"], "sandbox-playback-replay-7"); - let arguments = metadata["model_response"]["choices"][0]["message"]["tool_calls"][0] - ["function"]["arguments"] - .as_str() - .unwrap(); - let arguments: Value = serde_json::from_str(arguments).unwrap(); - assert_eq!(arguments["command"], "view"); - assert_eq!(arguments["path"], "/workspace/file"); - } - - #[test] - fn openhands_signature_separates_visible_text_reasoning_and_tool_arguments() { - let event = json!({ - "id": 7, - "source": "agent", - "action": "run", - "args": {"command": "pwd", "thought": "legacy thought"}, - "tool_call_metadata": { - "model_response": { - "choices": [{ - "message": { - "content": "visible preamble", - "reasoning_content": "hidden reasoning" - } - }] - } - } - }); - - let signature = openhands_action_signature(&event); - - assert_eq!(signature["text"], "visible preamble"); - assert_eq!(signature["reasoning"], "hidden reasoning"); - assert_eq!( - signature["tools"][0]["arguments"], - json!({"command": "pwd"}) - ); - } - - #[test] - fn openhands_complete_batches_preserve_fresh_observations() { - let events = vec![ - json!({ - "id": 5, - "source": "agent", - "action": "run", - "args": {"command": "pwd"}, - }), - json!({ - "id": 6, - "source": "environment", - "observation": "run", - "cause": 5, - "message": "ok", - "args": {"command": "pwd", "metadata": {"exit_code": 0}}, - }), - ]; - let batches = openhands_complete_batches(&events).unwrap(); - assert_eq!(batches.len(), 1); - assert_eq!(batches[0].0["id"], 5); - assert_eq!( - openhands_observation_content(batches[0].1), - json!({ - "observation": "run", - "message": "ok", - "args": {"command": "pwd", "metadata": {"exit_code": 0}}, - }) - ); - } - - #[test] - fn openhands_runtime_tools_are_prepended_to_path() { - let runtime = tempfile::tempdir().unwrap(); - let bin = runtime.path().join("bin/openhands-python"); - fs::create_dir_all(bin.parent().unwrap()).unwrap(); - fs::create_dir(runtime.path().join("tools")).unwrap(); - let launch = LaunchSpec { - entrypoint: bin, - version: "0.53.0".into(), - source: "explicit_entrypoint".into(), - runtime_root: None, - }; - let mut command = Command::new(&launch.entrypoint); - prepend_openhands_runtime_tools(&mut command, &launch).unwrap(); - let path = command - .get_envs() - .find_map(|(name, value)| { - (name == "PATH").then(|| value.expect("PATH value").to_os_string()) - }) - .expect("PATH override"); - let first = std::env::split_paths(&path).next().unwrap(); - assert_eq!(first, runtime.path().join("tools")); - } - - #[test] - fn openhands_zero_exit_controller_errors_are_detected_for_partial_results() { - assert_eq!( - openhands_fatal_controller_marker("Error while running the agent"), - Some("Error while running the agent") - ); - assert_eq!( - openhands_fatal_controller_marker("Agent reached maximum iteration AgentState.ERROR"), - None - ); - } -} diff --git a/crates/persisting-replay/src/adapter/pi_agent.rs b/crates/persisting-replay/src/adapter/pi_agent.rs deleted file mode 100644 index ac337673d..000000000 --- a/crates/persisting-replay/src/adapter/pi_agent.rs +++ /dev/null @@ -1,350 +0,0 @@ -use std::collections::BTreeSet; - -use serde_json::{Value, json}; - -use super::{RunContext, check_boundary, prepared_outcome, run_sdk_bridge}; -use crate::error::{ReplayError, ReplayErrorKind, ResultExt}; -use crate::io::{atomic_write, canonicalize, read_regular_file, sha256}; -use crate::journal::Journal; -use crate::model::{ - AdapterPlan, AgentKind, PlaybackRequest, ReplayMode, ReplayOutcome, ReplayPlan, ToolBatch, - ToolCall, -}; - -const SUPPORTED_TOOLS: &[&str] = &["read", "bash", "edit", "write"]; - -pub(super) fn build(request: &PlaybackRequest) -> Result { - build_pi_plan(request).map(AdapterPlan::PiAgent) -} - -pub(super) fn execute( - plan: &ReplayPlan, - context: &RunContext<'_>, - journal: &mut Journal, -) -> Result { - let boundary = plan.batches.last().unwrap().native["event_index"] - .as_u64() - .expect("validated Pi batch event index") as usize; - let events = plan - .native - .as_array() - .expect("Pi replay plan stores an event array"); - let path = context.output_dir.join("native/prepared-prefix.jsonl"); - write_jsonl(&path, &events[..=boundary])?; - journal.append( - "session_rebuilt", - [( - "prepared_only".into(), - json!(context.request.mode == ReplayMode::PrepareOnly), - )], - )?; - if context.request.mode == ReplayMode::PrepareOnly { - return Ok(prepared_outcome(path, context.request)); - } - run_sdk_bridge(plan, context, journal, AgentKind::PiAgent) -} - -fn build_pi_plan(request: &PlaybackRequest) -> Result { - let raw = read_regular_file(&request.trajectory)?; - let events = parse_jsonl_events(&raw)?; - let last_turn_end = events - .iter() - .rposition(|event| event.get("type").and_then(Value::as_str) == Some("turn_end")); - let mut batches = Vec::new(); - for (event_index, event) in events.iter().enumerate() { - if event.get("type").and_then(Value::as_str) != Some("turn_end") { - continue; - } - let Some(message) = event.get("message") else { - return Err(ReplayError::trajectory(format!( - "Pi turn_end event[{event_index}] has no message" - ))); - }; - if message.get("role").and_then(Value::as_str) != Some("assistant") { - return Err(ReplayError::trajectory(format!( - "Pi turn_end event[{event_index}] message is not an assistant message" - ))); - } - let native_calls = pi_calls(message, event_index)?; - if native_calls.is_empty() { - continue; - } - let results = event - .get("toolResults") - .and_then(Value::as_array) - .ok_or_else(|| { - ReplayError::trajectory(format!( - "Pi turn_end event[{event_index}] has no toolResults array" - )) - })?; - if results.len() != native_calls.len() { - // A killed source run can end with an incomplete final batch. It is - // not selectable, but earlier complete batches remain replayable. - if Some(event_index) == last_turn_end { - break; - } - return Err(ReplayError::trajectory(format!( - "Pi turn_end event[{event_index}] has {} tool calls but {} results", - native_calls.len(), - results.len() - ))); - } - let mut result_ids = BTreeSet::new(); - for result in results { - let result_id = result - .get("toolCallId") - .and_then(Value::as_str) - .ok_or_else(|| { - ReplayError::trajectory(format!( - "Pi turn_end event[{event_index}] has a result without toolCallId" - )) - })?; - if !result_ids.insert(result_id) { - return Err(ReplayError::trajectory(format!( - "Pi turn_end event[{event_index}] has duplicate result id {result_id:?}" - ))); - } - } - let mut calls = Vec::with_capacity(native_calls.len()); - for (ordinal, native) in native_calls.into_iter().enumerate() { - let call_id = native["id"] - .as_str() - .expect("validated Pi tool call id") - .to_owned(); - let result = results - .iter() - .find(|result| { - result.get("toolCallId").and_then(Value::as_str) == Some(call_id.as_str()) - }) - .ok_or_else(|| { - ReplayError::trajectory(format!( - "Pi turn_end event[{event_index}] has no result for tool call {call_id:?}" - )) - })?; - if result.get("toolName").and_then(Value::as_str) - != native.get("name").and_then(Value::as_str) - { - return Err(ReplayError::trajectory(format!( - "Pi result for {call_id:?} does not match its tool name" - ))); - } - calls.push(ToolCall { - ordinal: ordinal + 1, - call_id, - name: native["name"] - .as_str() - .expect("validated Pi tool name") - .to_owned(), - arguments: native["arguments"].clone(), - original_observation: result.get("content").cloned().unwrap_or(Value::Null), - original_is_error: result - .get("isError") - .and_then(Value::as_bool) - .unwrap_or(false), - native, - }); - } - batches.push(ToolBatch { - ordinal: batches.len() + 1, - native_locator: format!("events:{event_index}"), - tool_calls: calls, - assistant_text: pi_text(message), - native: json!({"event_index": event_index}), - }); - } - check_boundary(request.after_step, batches.len())?; - let boundary_event_index = batches[request.after_step - 1].native["event_index"] - .as_u64() - .expect("validated Pi batch event index") as usize; - let prefix_model_turns = events[..=boundary_event_index] - .iter() - .filter(|event| event.get("type").and_then(Value::as_str) == Some("turn_end")) - .count(); - let original_next_action = events[boundary_event_index + 1..] - .iter() - .find(|event| event.get("type").and_then(Value::as_str) == Some("turn_end")) - .and_then(|event| event.get("message")) - .map(pi_action_signature); - batches.truncate(request.after_step); - Ok(ReplayPlan { - agent: request.agent, - source_path: canonicalize( - &request.trajectory, - ReplayErrorKind::Trajectory, - "trajectory", - )?, - source_sha256: sha256(&raw), - after_step: request.after_step, - batches, - prefix_model_turns, - native: Value::Array(events), - original_next_action, - }) -} - -fn parse_jsonl_events(raw: &[u8]) -> Result, ReplayError> { - let source = std::str::from_utf8(raw).map_err(|error| { - ReplayError::trajectory(format!("Pi event JSONL is not UTF-8: {error}")) - })?; - let physical = source.split('\n').collect::>(); - let mut events = Vec::new(); - for (index, line) in physical.iter().enumerate() { - if line.trim().is_empty() { - continue; - } - match serde_json::from_str::(line) { - Ok(Value::Object(event)) => events.push(Value::Object(event)), - Ok(_) => { - return Err(ReplayError::trajectory(format!( - "Pi event JSONL line {} must contain an object", - index + 1 - ))); - } - Err(_) if index + 1 == physical.len() && !source.ends_with('\n') => { - // The source process may be killed while writing its final line. - } - Err(error) => { - return Err(ReplayError::trajectory(format!( - "invalid Pi event JSONL line {}: {error}", - index + 1 - ))); - } - } - } - if !events.iter().any(|event| { - event.get("type").and_then(Value::as_str) == Some("message_end") - && event.pointer("/message/role").and_then(Value::as_str) == Some("user") - }) { - return Err(ReplayError::trajectory( - "Pi event trajectory has no native user message", - )); - } - Ok(events) -} - -fn pi_calls(message: &Value, event_index: usize) -> Result, ReplayError> { - let content = message - .get("content") - .and_then(Value::as_array) - .ok_or_else(|| { - ReplayError::trajectory(format!( - "Pi turn_end event[{event_index}] assistant content must be an array" - )) - })?; - let mut calls = Vec::new(); - let mut call_ids = BTreeSet::new(); - for (part_index, part) in content.iter().enumerate() { - if part.get("type").and_then(Value::as_str) != Some("toolCall") { - continue; - } - let id = part.get("id").and_then(Value::as_str).ok_or_else(|| { - ReplayError::trajectory(format!( - "Pi turn_end event[{event_index}] toolCall[{part_index}] has no id" - )) - })?; - let name = part.get("name").and_then(Value::as_str).ok_or_else(|| { - ReplayError::trajectory(format!( - "Pi turn_end event[{event_index}] toolCall[{part_index}] has no name" - )) - })?; - if !call_ids.insert(id) { - return Err(ReplayError::trajectory(format!( - "Pi turn_end event[{event_index}] has duplicate tool call id {id:?}" - ))); - } - if !SUPPORTED_TOOLS.contains(&name) { - return Err(ReplayError::new( - ReplayErrorKind::UnsupportedVersion, - format!("Pi Replay profile does not support tool {name:?}"), - )); - } - let arguments = part.get("arguments").cloned().unwrap_or_else(|| json!({})); - if !arguments.is_object() { - return Err(ReplayError::trajectory(format!( - "Pi tool call {id:?} arguments must be an object" - ))); - } - calls.push(json!({"id": id, "name": name, "arguments": arguments})); - } - Ok(calls) -} - -fn pi_text(message: &Value) -> String { - message - .get("content") - .and_then(Value::as_array) - .into_iter() - .flatten() - .filter(|part| part.get("type").and_then(Value::as_str) == Some("text")) - .filter_map(|part| part.get("text").and_then(Value::as_str)) - .collect::>() - .join("\n") -} - -fn pi_action_signature(message: &Value) -> Value { - let mut reasoning = Vec::new(); - let mut tools = Vec::new(); - for part in message - .get("content") - .and_then(Value::as_array) - .into_iter() - .flatten() - { - match part.get("type").and_then(Value::as_str) { - Some("thinking") => { - if let Some(value) = part.get("thinking").and_then(Value::as_str) { - reasoning.push(value); - } - } - Some("toolCall") => tools.push(json!({ - "name": part.get("name").and_then(Value::as_str).unwrap_or_default(), - "arguments": part.get("arguments").cloned().unwrap_or_else(|| json!({})), - })), - _ => {} - } - } - json!({ - "text": pi_text(message), - "reasoning": reasoning.join("\n\n"), - "tools": tools, - }) -} - -fn write_jsonl(path: &std::path::Path, values: &[Value]) -> Result<(), ReplayError> { - let mut rendered = Vec::new(); - for value in values { - serde_json::to_writer(&mut rendered, value) - .replay_context(ReplayErrorKind::Executor, "serialize Pi event JSONL")?; - rendered.push(b'\n'); - } - atomic_write(path, &rendered) -} - -#[cfg(test)] -mod tests { - use super::{parse_jsonl_events, pi_action_signature}; - use serde_json::json; - - #[test] - fn pi_signature_separates_text_reasoning_and_tools() { - let signature = pi_action_signature(&json!({ - "role": "assistant", - "content": [ - {"type": "thinking", "thinking": "inspect"}, - {"type": "text", "text": "I will inspect."}, - {"type": "toolCall", "id": "call-1", "name": "read", "arguments": {"path": "a"}} - ] - })); - assert_eq!(signature["text"], "I will inspect."); - assert_eq!(signature["reasoning"], "inspect"); - assert_eq!(signature["tools"][0]["name"], "read"); - } - - #[test] - fn pi_jsonl_tolerates_only_a_truncated_final_line() { - let raw = b"{\"type\":\"message_end\",\"message\":{\"role\":\"user\"}}\n{\"type\":"; - assert_eq!(parse_jsonl_events(raw).unwrap().len(), 1); - let invalid = b"{\"type\":\"message_end\",\"message\":{\"role\":\"user\"}}\nnot-json\n"; - assert!(parse_jsonl_events(invalid).is_err()); - } -} diff --git a/crates/persisting-replay/src/adapter/runtime.rs b/crates/persisting-replay/src/adapter/runtime.rs deleted file mode 100644 index e216dedb9..000000000 --- a/crates/persisting-replay/src/adapter/runtime.rs +++ /dev/null @@ -1,595 +0,0 @@ -use std::collections::BTreeMap; -use std::ffi::OsString; -use std::fs; -use std::path::{Component, Path, PathBuf}; -use std::process::Command; - -use serde::Deserialize; - -use crate::error::{ReplayError, ReplayErrorKind, ResultExt}; -use crate::io::{canonicalize, read_regular_file}; -use crate::model::{AgentKind, PlaybackRequest, ReplayMode}; - -#[derive(Debug, Clone)] -pub(crate) struct LaunchSpec { - pub entrypoint: PathBuf, - pub version: String, - pub source: String, - pub runtime_root: Option, -} - -pub(crate) fn resolve_launch_spec( - request: &PlaybackRequest, -) -> Result, ReplayError> { - if request.agent_entrypoint.is_some() && request.agent_runtime.is_some() { - return Err(ReplayError::configuration( - "agent entrypoint and agent runtime are mutually exclusive", - )); - } - if request.mode == ReplayMode::PrepareOnly { - return Ok(None); - } - let (entrypoint, source, runtime_root, declared_version) = - if let Some(runtime_root) = &request.agent_runtime { - let root = canonicalize( - runtime_root, - ReplayErrorKind::Configuration, - "agent runtime", - )?; - let manifest_path = root.join("sandbox-playback-agent.json"); - let manifest: RuntimeManifest = - serde_json::from_slice(&read_regular_file(&manifest_path)?).replay_context( - ReplayErrorKind::Configuration, - format!("parse agent runtime manifest {}", manifest_path.display()), - )?; - if manifest.schema_version != "sandbox-playback.agent-runtime/v1" { - return Err(ReplayError::configuration( - "agent runtime schema_version must be sandbox-playback.agent-runtime/v1", - )); - } - if manifest.agent != request.agent.as_str() { - return Err(ReplayError::new( - ReplayErrorKind::UnsupportedAgent, - format!( - "agent runtime declares {:?}, requested {:?}", - manifest.agent, - request.agent.as_str() - ), - )); - } - if manifest.version != request.agent.supported_version() { - return Err(ReplayError::new( - ReplayErrorKind::UnsupportedVersion, - format!( - "agent runtime declares {:?}; profile requires {}", - manifest.version, - request.agent.supported_version() - ), - )); - } - let relative = safe_relative(&manifest.entrypoint)?; - ( - root.join(relative), - "runtime_manifest".to_owned(), - Some(root), - Some(manifest.version), - ) - } else { - let entrypoint = request.agent_entrypoint.clone().ok_or_else(|| { - ReplayError::configuration( - "replay and continuation modes require --agent-entrypoint or --agent-runtime", - ) - })?; - (entrypoint, "explicit_entrypoint".to_owned(), None, None) - }; - if !entrypoint.is_absolute() { - return Err(ReplayError::configuration( - "agent entrypoint must be an absolute path", - )); - } - let entrypoint = canonicalize( - &entrypoint, - ReplayErrorKind::Configuration, - "agent entrypoint", - )?; - if !entrypoint.is_file() { - return Err(ReplayError::configuration(format!( - "agent entrypoint is not a regular file: {}", - entrypoint.display() - ))); - } - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - if entrypoint - .metadata() - .map(|metadata| metadata.permissions().mode() & 0o111 == 0) - .unwrap_or(true) - { - return Err(ReplayError::configuration(format!( - "agent entrypoint is not executable: {}", - entrypoint.display() - ))); - } - } - let version = probe_version(request.agent, &entrypoint)?; - if declared_version - .as_deref() - .is_some_and(|declared| declared != version) - { - return Err(ReplayError::new( - ReplayErrorKind::UnsupportedVersion, - "agent runtime manifest and executable versions differ", - )); - } - Ok(Some(LaunchSpec { - entrypoint, - version, - source, - runtime_root, - })) -} - -#[derive(Deserialize)] -#[serde(deny_unknown_fields)] -struct RuntimeManifest { - schema_version: String, - agent: String, - version: String, - entrypoint: PathBuf, - #[serde(default, rename = "paths")] - _paths: BTreeMap, -} - -pub(super) fn safe_relative(path: &Path) -> Result { - if path.as_os_str().is_empty() - || path.is_absolute() - || path.components().any(|component| { - matches!( - component, - Component::ParentDir | Component::RootDir | Component::Prefix(_) - ) - }) - { - return Err(ReplayError::configuration( - "agent runtime entrypoint must be a non-empty relative path without '..'", - )); - } - Ok(path.to_path_buf()) -} - -fn probe_version(agent: AgentKind, entrypoint: &Path) -> Result { - let expected = agent.supported_version(); - let mut command = Command::new(entrypoint); - match agent { - AgentKind::ClaudeCode - | AgentKind::Codex - | AgentKind::MiniSweAgent - | AgentKind::Opencode - | AgentKind::PiAgent => { - command.arg("--version"); - } - AgentKind::Openhands => { - command.args([ - "-c", - "import importlib.metadata;print(importlib.metadata.version('openhands-ai'))", - ]); - } - AgentKind::SweAgent => { - command.args([ - "-c", - "import importlib.metadata;print(importlib.metadata.version('sweagent'))", - ]); - } - } - command.env_remove("PYTHONHOME"); - command.env_remove("PYTHONPATH"); - command.env_remove("VIRTUAL_ENV"); - if agent == AgentKind::MiniSweAgent { - let runtime = mini_python_runtime(entrypoint)?; - configure_mini_python_environment(&mut command, &runtime)?; - } - let output = command.output().replay_context( - ReplayErrorKind::UnsupportedVersion, - format!( - "probe {} version from {}", - agent.as_str(), - entrypoint.display() - ), - )?; - let rendered = String::from_utf8_lossy(if output.stdout.is_empty() { - &output.stderr - } else { - &output.stdout - }); - let detected = parse_version(agent, &rendered); - let status_is_acceptable = - output.status.success() || (agent == AgentKind::MiniSweAgent && detected == Some(expected)); - if !status_is_acceptable || detected != Some(expected) { - return Err(ReplayError::new( - ReplayErrorKind::UnsupportedVersion, - format!( - "{} profile requires {}, got {:?} from {}", - agent.as_str(), - expected, - rendered.trim(), - entrypoint.display() - ), - )); - } - Ok(expected.to_owned()) -} - -fn parse_version(agent: AgentKind, rendered: &str) -> Option<&'static str> { - let expected = agent.supported_version(); - match agent { - AgentKind::ClaudeCode => { - let mut lines = rendered.trim().lines(); - let first = lines.next()?.trim(); - let token = first.split_whitespace().next()?; - (lines.next().is_none() && token == expected).then_some(expected) - } - AgentKind::MiniSweAgent => { - const PREFIX: &str = "This is mini-swe-agent version "; - let mut versions = rendered.lines().filter_map(|line| { - line.trim() - .strip_prefix(PREFIX)? - .split_whitespace() - .next() - .map(|version| version.trim_end_matches('.')) - }); - let version = versions.next()?; - (versions.next().is_none() && version == expected).then_some(expected) - } - AgentKind::Openhands | AgentKind::PiAgent | AgentKind::SweAgent => { - (rendered.trim() == expected).then_some(expected) - } - AgentKind::Codex => rendered - .split_whitespace() - .filter_map(|token| token.strip_prefix('v').or(Some(token))) - .find(|version| *version == expected) - .map(|_| expected), - AgentKind::Opencode => rendered - .trim() - .strip_prefix('v') - .unwrap_or_else(|| rendered.trim()) - .strip_suffix("-baseline") - .unwrap_or_else(|| { - rendered - .trim() - .strip_prefix('v') - .unwrap_or_else(|| rendered.trim()) - }) - .trim() - .eq(expected) - .then_some(expected), - } -} - -#[derive(Debug)] -pub(super) struct PiNodeRuntime { - pub node: PathBuf, - pub package_json: PathBuf, -} - -pub(super) fn pi_node_runtime(entrypoint: &Path) -> Result { - let runtime_root = entrypoint - .parent() - .and_then(Path::parent) - .ok_or_else(|| ReplayError::continuation("Pi entrypoint has no runtime root"))?; - let node = runtime_root.join("node/bin/node"); - let package_json = runtime_root - .join("npm-global/lib/node_modules/@earendil-works/pi-coding-agent/package.json"); - if !node.is_file() { - return Err(ReplayError::continuation(format!( - "Pi runtime Node executable does not exist: {}", - node.display() - ))); - } - if !package_json.is_file() { - return Err(ReplayError::continuation(format!( - "Pi runtime package manifest does not exist: {}", - package_json.display() - ))); - } - Ok(PiNodeRuntime { node, package_json }) -} - -#[derive(Debug)] -pub(super) struct MiniPythonRuntime { - pub python: PathBuf, - pub loader: Option, - pub python_home: Option, - pub virtual_env: Option, - library_paths: Vec, -} - -pub(super) fn mini_python_runtime(entrypoint: &Path) -> Result { - if let Some(local_root) = entrypoint.parent().and_then(Path::parent) { - let uv_root = local_root.join("share/uv"); - let virtual_env = uv_root.join("tools/mini-swe-agent"); - let python = virtual_env.join("bin/python"); - if python.is_file() { - let python = fs::canonicalize(&python).replay_context( - ReplayErrorKind::Continuation, - format!( - "resolve bundled mini-swe-agent Python from {}", - python.display() - ), - )?; - let python_home = python - .parent() - .and_then(Path::parent) - .ok_or_else(|| ReplayError::continuation("bundled Python has no prefix"))? - .to_path_buf(); - if !python_home.join("lib/python3.12/encodings").is_dir() { - return Err(ReplayError::continuation(format!( - "bundled mini-swe-agent Python has no standard library below {}", - python_home.display() - ))); - } - let loader = uv_root.join("sweeval-system-libs/ld-linux-x86-64.so.2"); - if !loader.is_file() { - return Err(ReplayError::continuation(format!( - "bundled mini-swe-agent Python loader does not exist: {}", - loader.display() - ))); - } - return Ok(MiniPythonRuntime { - python, - loader: Some(loader), - python_home: Some(python_home.clone()), - virtual_env: Some(virtual_env), - library_paths: vec![uv_root.join("sweeval-system-libs"), python_home.join("lib")], - }); - } - } - - let prefix = read_regular_file(entrypoint)?; - if let Some(first) = prefix.split(|byte| *byte == b'\n').next() - && let Some(shebang) = first.strip_prefix(b"#!") - { - let rendered = String::from_utf8_lossy(shebang); - let words: Vec<_> = rendered.split_whitespace().collect(); - if words.first() == Some(&"/usr/bin/env") - && let Some(program) = words.get(1) - && program.contains("python") - { - return Ok(MiniPythonRuntime { - python: PathBuf::from(program), - loader: None, - python_home: None, - virtual_env: None, - library_paths: Vec::new(), - }); - } else if let Some(program) = words.first() - && program.contains("python") - { - return Ok(MiniPythonRuntime { - python: PathBuf::from(program), - loader: None, - python_home: None, - virtual_env: None, - library_paths: Vec::new(), - }); - } - } - for name in ["python3", "python"] { - let candidate = entrypoint.parent().unwrap_or(Path::new("/")).join(name); - if candidate.is_file() { - return Ok(MiniPythonRuntime { - python: candidate, - loader: None, - python_home: None, - virtual_env: None, - library_paths: Vec::new(), - }); - } - } - Err(ReplayError::continuation( - "mini-swe-agent entrypoint does not expose its Python interpreter", - )) -} - -pub(super) fn mini_python_library_path( - runtime: &MiniPythonRuntime, -) -> Result, ReplayError> { - let paths = runtime - .library_paths - .iter() - .filter(|path| path.is_dir()) - .collect::>(); - if paths.is_empty() { - return Ok(None); - } - std::env::join_paths(paths).map(Some).map_err(|error| { - ReplayError::configuration(format!( - "cannot construct mini-swe-agent Python library path: {error}" - )) - }) -} - -pub(super) fn configure_mini_python_environment( - command: &mut Command, - runtime: &MiniPythonRuntime, -) -> Result<(), ReplayError> { - if let Some(python_home) = &runtime.python_home { - command.env("PYTHONHOME", python_home); - } - if let Some(virtual_env) = &runtime.virtual_env { - command.env("VIRTUAL_ENV", virtual_env); - command.env( - "PYTHONPATH", - virtual_env.join("lib/python3.12/site-packages"), - ); - let current = std::env::var_os("PATH").unwrap_or_else(|| "/usr/bin:/bin".into()); - let paths = std::iter::once(virtual_env.join("bin")).chain(std::env::split_paths(¤t)); - let path = std::env::join_paths(paths).map_err(|error| { - ReplayError::configuration(format!( - "cannot prepend mini-swe-agent virtual environment to PATH: {error}" - )) - })?; - command.env("PATH", path); - } - if let Some(library_path) = mini_python_library_path(runtime)? { - command.env("LD_LIBRARY_PATH", library_path); - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use std::{fs, process::Command}; - - use super::{ - configure_mini_python_environment, mini_python_runtime, parse_version, resolve_launch_spec, - }; - use crate::model::{AgentKind, PlaybackRequest, ReplayMode}; - - #[test] - fn mini_version_probe_accepts_exact_banner_before_config_noise() { - let output = "This is mini-swe-agent version 2.4.6.\n\ -Check the v2 migration guide at https://example.invalid\n\ -Loading global config from '/root/.config/mini-swe-agent/.env'"; - assert_eq!( - parse_version(AgentKind::MiniSweAgent, output), - Some("2.4.6") - ); - } - - #[cfg(unix)] - #[test] - fn mini_python_runtime_finds_the_portable_uv_bundle() { - use std::os::unix::fs::symlink; - - let root = tempfile::tempdir().unwrap(); - let local = root.path().join(".local"); - let entrypoint = local.join("bin/mini-swe-agent"); - let virtual_env = local.join("share/uv/tools/mini-swe-agent"); - let python_home = local.join("share/uv/python/cpython-3.12.11"); - let python = python_home.join("bin/python3.12"); - fs::create_dir_all(entrypoint.parent().unwrap()).unwrap(); - fs::create_dir_all(virtual_env.join("bin")).unwrap(); - fs::create_dir_all(virtual_env.join("lib/python3.12/site-packages")).unwrap(); - fs::create_dir_all(python_home.join("lib/python3.12/encodings")).unwrap(); - fs::create_dir_all(python.parent().unwrap()).unwrap(); - let loader = local.join("share/uv/sweeval-system-libs/ld-linux-x86-64.so.2"); - fs::create_dir_all(loader.parent().unwrap()).unwrap(); - fs::write(&loader, "loader").unwrap(); - fs::write(&entrypoint, "#!/bin/sh\nexit 0\n").unwrap(); - fs::write(&python, "python").unwrap(); - symlink(&python, virtual_env.join("bin/python")).unwrap(); - - let runtime = mini_python_runtime(&entrypoint).unwrap(); - let canonical_python_home = fs::canonicalize(&python_home).unwrap(); - assert_eq!(runtime.python, fs::canonicalize(&python).unwrap()); - assert_eq!( - runtime.python_home.as_deref(), - Some(canonical_python_home.as_path()) - ); - assert_eq!(runtime.loader.as_deref(), Some(loader.as_path())); - assert_eq!(runtime.virtual_env.as_deref(), Some(virtual_env.as_path())); - let mut command = Command::new(&runtime.python); - configure_mini_python_environment(&mut command, &runtime).unwrap(); - assert!(command.get_envs().any(|(name, value)| { - name == "PYTHONHOME" && value == Some(canonical_python_home.as_os_str()) - })); - let path = command - .get_envs() - .find_map(|(name, value)| { - (name == "PATH").then(|| value.expect("PATH value").to_os_string()) - }) - .expect("PATH override"); - assert_eq!( - std::env::split_paths(&path).next().unwrap(), - virtual_env.join("bin") - ); - } - - #[cfg(unix)] - use std::os::unix::fs::PermissionsExt; - - #[test] - fn version_probes_require_exact_banners() { - assert_eq!( - parse_version(AgentKind::ClaudeCode, "2.1.220 (Claude Code)"), - Some("2.1.220") - ); - assert_eq!( - parse_version(AgentKind::ClaudeCode, "12.1.220 (Claude Code)"), - None - ); - assert_eq!( - parse_version(AgentKind::Openhands, "0.53.0\n"), - Some("0.53.0") - ); - assert_eq!( - parse_version( - AgentKind::Openhands, - "warning about 0.53.0; actual runtime 0.54.0" - ), - None - ); - assert_eq!( - parse_version( - AgentKind::MiniSweAgent, - "This is mini-swe-agent version 2.4.6.\n" - ), - Some("2.4.6") - ); - assert_eq!(parse_version(AgentKind::SweAgent, "1.1.0"), Some("1.1.0")); - assert_eq!(parse_version(AgentKind::SweAgent, "swe-agent 1.1.0"), None); - assert_eq!( - parse_version(AgentKind::Codex, "codex-cli 0.149.0"), - Some("0.149.0") - ); - assert_eq!(parse_version(AgentKind::Codex, "codex-cli 0.148.0"), None); - assert_eq!(parse_version(AgentKind::Opencode, "1.17.7"), Some("1.17.7")); - assert_eq!( - parse_version(AgentKind::Opencode, "v1.17.7"), - Some("1.17.7") - ); - assert_eq!(parse_version(AgentKind::Opencode, "1.17.6"), None); - } - - #[cfg(unix)] - #[test] - fn prepare_only_never_starts_a_supplied_runtime() { - let temporary = tempfile::tempdir().unwrap(); - let marker = temporary.path().join("started"); - let entrypoint = temporary.path().join("claude"); - fs::write( - &entrypoint, - format!( - "#!/bin/sh\ntouch '{}'\nprintf '2.1.220 (Claude Code)\\n'\n", - marker.display() - ), - ) - .unwrap(); - let mut permissions = fs::metadata(&entrypoint).unwrap().permissions(); - permissions.set_mode(0o700); - fs::set_permissions(&entrypoint, permissions).unwrap(); - let request = PlaybackRequest { - agent: AgentKind::ClaudeCode, - trajectory: temporary.path().join("trajectory"), - after_step: 1, - workspace: temporary.path().to_path_buf(), - state_dir: temporary.path().join("state"), - output_dir: temporary.path().join("output"), - agent_entrypoint: Some(entrypoint), - agent_runtime: None, - disallowed_tools: Vec::new(), - trajectory_assets: None, - session_id: None, - max_steps: None, - mode: ReplayMode::PrepareOnly, - allow_stale_observations: false, - run_id: None, - disable_thinking: false, - boundary_user_prompt: None, - }; - - assert!(resolve_launch_spec(&request).unwrap().is_none()); - assert!(!marker.exists()); - } -} diff --git a/crates/persisting-replay/src/adapter/swe_agent.rs b/crates/persisting-replay/src/adapter/swe_agent.rs deleted file mode 100644 index b903edf94..000000000 --- a/crates/persisting-replay/src/adapter/swe_agent.rs +++ /dev/null @@ -1,215 +0,0 @@ -use std::path::Path; - -use serde_json::{Value, json}; - -use super::runtime::safe_relative; -use super::{RunContext, check_boundary, prepared_outcome, run_sdk_bridge}; -use crate::error::{ReplayError, ReplayErrorKind, ResultExt}; -use crate::io::{atomic_write_json, canonicalize, read_regular_file, sha256}; -use crate::journal::Journal; -use crate::model::{ - AdapterPlan, AgentKind, PlaybackRequest, ReplayMode, ReplayOutcome, ReplayPlan, ToolBatch, - ToolCall, -}; - -pub(super) fn build(request: &PlaybackRequest) -> Result { - build_swe_plan(request).map(AdapterPlan::SweAgent) -} - -pub(super) fn execute( - plan: &ReplayPlan, - context: &RunContext<'_>, - journal: &mut Journal, -) -> Result { - run_swe(plan, context, journal) -} - -fn build_swe_plan(request: &PlaybackRequest) -> Result { - let raw = read_regular_file(&request.trajectory)?; - let mut value: Value = serde_json::from_slice(&raw).replay_context( - ReplayErrorKind::Trajectory, - "invalid SWE-agent trajectory JSON", - )?; - for field in ["trajectory", "history", "replay_config"] { - if value.get(field).is_none() { - return Err(ReplayError::trajectory(format!( - "SWE-agent trajectory is missing {field}" - ))); - } - } - resolve_swe_problem_asset(&mut value, request.trajectory_assets.as_deref())?; - let trajectory = value["trajectory"] - .as_array() - .ok_or_else(|| ReplayError::trajectory("SWE-agent trajectory must be an array"))?; - let history: Vec<_> = value["history"] - .as_array() - .ok_or_else(|| ReplayError::trajectory("SWE-agent history must be an array"))? - .iter() - .filter(|item| item.get("role").and_then(Value::as_str) == Some("assistant")) - .collect(); - check_boundary(request.after_step, trajectory.len().min(history.len()))?; - let original_next_action = trajectory.get(request.after_step).map(|step| { - json!({ - "text": "", - "reasoning": step.get("thought").and_then(Value::as_str).unwrap_or_default(), - "tools": [{ - "name": "swe_agent_action", - "arguments": {"raw_action": step.get("action").cloned().unwrap_or(Value::Null)}, - }], - }) - }); - let mut batches = Vec::new(); - for index in 0..request.after_step { - let step = &trajectory[index]; - let assistant = history[index]; - let action = step - .get("action") - .and_then(Value::as_str) - .ok_or_else(|| ReplayError::trajectory("SWE-agent step has no action"))?; - if action.trim() == "submit" || action.trim_start().starts_with("submit\n") { - return Err(ReplayError::new( - ReplayErrorKind::UnsupportedVersion, - "SWE-agent submit cannot appear inside a replay prefix", - )); - } - let observation = step - .get("observation") - .and_then(Value::as_str) - .ok_or_else(|| ReplayError::trajectory("SWE-agent step has no observation"))?; - let calls = assistant - .get("tool_calls") - .and_then(Value::as_array) - .cloned() - .unwrap_or_default(); - let call_id = if calls.len() == 1 { - calls[0] - .get("id") - .and_then(Value::as_str) - .map(str::to_owned) - .unwrap_or_else(|| format!("swe-agent-step-{}", index + 1)) - } else { - format!("swe-agent-step-{}", index + 1) - }; - batches.push(ToolBatch { - ordinal: index + 1, - native_locator: format!("trajectory:{index}"), - tool_calls: vec![ToolCall { - ordinal: index + 1, - call_id, - name: "swe_agent_action".into(), - arguments: json!({"raw_action": action}), - original_observation: Value::String(observation.to_owned()), - original_is_error: false, - native: json!({"assistant": assistant}), - }], - assistant_text: step - .get("thought") - .and_then(Value::as_str) - .or_else(|| assistant.get("content").and_then(Value::as_str)) - .unwrap_or_default() - .to_owned(), - native: json!({"state": step.get("state")}), - }); - } - Ok(ReplayPlan { - agent: request.agent, - source_path: canonicalize( - &request.trajectory, - ReplayErrorKind::Trajectory, - "trajectory", - )?, - source_sha256: sha256(&raw), - after_step: request.after_step, - prefix_model_turns: request.after_step, - batches, - native: value, - original_next_action, - }) -} - -fn resolve_swe_problem_asset(value: &mut Value, assets: Option<&Path>) -> Result<(), ReplayError> { - let replay_config = value - .get_mut("replay_config") - .ok_or_else(|| ReplayError::trajectory("SWE-agent replay_config is required"))?; - if replay_config.is_string() { - let encoded = replay_config.as_str().unwrap(); - *replay_config = serde_json::from_str(encoded).replay_context( - ReplayErrorKind::Trajectory, - "invalid encoded SWE-agent replay_config", - )?; - } - let Some(problem) = replay_config.get_mut("problem_statement") else { - return Ok(()); - }; - if !matches!( - problem.get("type").and_then(Value::as_str), - Some("file" | "path") - ) { - return Ok(()); - } - let root = assets.ok_or_else(|| { - ReplayError::trajectory("SWE-agent file problem_statement requires trajectory_assets") - })?; - let relative = problem - .get("path") - .or_else(|| problem.get("file")) - .and_then(Value::as_str) - .ok_or_else(|| ReplayError::trajectory("SWE-agent problem asset path is invalid"))?; - let relative = safe_relative(Path::new(relative))?; - let root = canonicalize(root, ReplayErrorKind::Trajectory, "trajectory assets")?; - let path = canonicalize( - &root.join(relative), - ReplayErrorKind::Trajectory, - "trajectory asset", - )?; - if !path.starts_with(&root) { - return Err(ReplayError::trajectory( - "SWE-agent trajectory asset escapes its root", - )); - } - let text = String::from_utf8(read_regular_file(&path)?).replay_context( - ReplayErrorKind::Trajectory, - "SWE-agent problem asset is not UTF-8", - )?; - let id = problem - .get("id") - .cloned() - .unwrap_or_else(|| json!("replay")); - *problem = json!({"type": "text", "text": text, "id": id}); - Ok(()) -} - -fn run_swe( - plan: &ReplayPlan, - context: &RunContext<'_>, - journal: &mut Journal, -) -> Result { - let mut prepared = plan.native.clone(); - prepared["trajectory"] = - Value::Array(plan.native["trajectory"].as_array().unwrap()[..plan.after_step].to_vec()); - let mut assistant = 0; - let mut history = Vec::new(); - for item in plan.native["history"].as_array().unwrap() { - history.push(item.clone()); - if item.get("role").and_then(Value::as_str) == Some("assistant") { - assistant += 1; - if assistant == plan.after_step { - break; - } - } - } - prepared["history"] = Value::Array(history); - let path = context.output_dir.join("native/prepared-prefix.traj"); - atomic_write_json(&path, &prepared)?; - journal.append( - "session_rebuilt", - [( - "prepared_only".into(), - json!(context.request.mode == ReplayMode::PrepareOnly), - )], - )?; - if context.request.mode == ReplayMode::PrepareOnly { - return Ok(prepared_outcome(path, context.request)); - } - run_sdk_bridge(plan, context, journal, AgentKind::SweAgent) -} diff --git a/crates/persisting-replay/src/claude_bridge.rs b/crates/persisting-replay/src/claude_bridge.rs deleted file mode 100644 index 79dd116a9..000000000 --- a/crates/persisting-replay/src/claude_bridge.rs +++ /dev/null @@ -1,1927 +0,0 @@ -use std::collections::BTreeMap; -use std::net::TcpListener; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex, mpsc}; -use std::thread::{self, JoinHandle}; -use std::time::Duration; - -use axum::Router; -use axum::body::{Body, Bytes}; -use axum::extract::{DefaultBodyLimit, State}; -use axum::http::header::{AUTHORIZATION, CACHE_CONTROL, CONTENT_TYPE}; -use axum::http::{HeaderMap, HeaderValue, StatusCode}; -use axum::response::Response; -use axum::routing::{get, post}; -use serde_json::{Map, Value, json}; -use tokio::sync::{Notify, oneshot}; - -use crate::claude_resume::{ResumeTransportManifest, clean_resume_transport_envelope}; -use crate::error::{ReplayError, ReplayErrorKind, ResultExt}; - -const BRIDGE_VERSION: &str = "sandbox-replay-anthropic-openai-bridge/1"; -const DEFAULT_MAX_OUTPUT_TOKENS: usize = 8192; -const DEFAULT_MODEL_CONTEXT_TOKENS: usize = 200_000; -const DEFAULT_CONTEXT_SAFETY_TOKENS: usize = 1024; -const DEFAULT_UPSTREAM_TIMEOUT_SECONDS: f64 = 300.0; -const DEFAULT_RETRY_DELAYS_SECONDS: &[f64] = &[15.0, 30.0, 60.0]; -const RETRYABLE_UPSTREAM_STATUSES: &[u16] = &[408, 429, 500, 502, 503, 504]; -const BRIDGE_START_TIMEOUT: Duration = Duration::from_secs(10); -const BRIDGE_STOP_TIMEOUT: Duration = Duration::from_secs(5); - -pub struct ClaudeBridgeHandle { - pub base_url: String, - pub api_key: String, - shared: Arc, - shutdown: Option>, - worker_done: Option>>, - worker: Option>, -} - -struct BridgeShared { - resume: Mutex, - client: reqwest::Client, - upstream_url: String, - upstream_api_key: String, - model_name: String, - routing_session_id: String, - bridge_api_key: String, - disable_thinking: bool, - boundary_user_prompt: Option, - max_output_tokens: usize, - model_context_tokens: usize, - context_safety_tokens: usize, - upstream_timeout: Duration, - retry_delays: Vec, - cancelled: AtomicBool, - cancel_notify: Notify, -} - -struct ResumeState { - manifest: ResumeTransportManifest, - request_sequence: usize, - forwarded_requests: usize, - pending_forward_sequence: Option, - failed: bool, - failure: Option, -} - -impl ResumeState { - fn clean(&mut self, payload: &Value) -> anyhow::Result<(Value, usize)> { - if self.failed { - anyhow::bail!( - "Resume transport state is FAILED after an earlier rejected request: {}", - self.failure - .as_deref() - .unwrap_or("unknown protocol failure") - ); - } - if self.pending_forward_sequence.is_some() { - self.fail("another request arrived before the previous request was forwarded"); - anyhow::bail!( - "Resume transport received another request before the previous validated request was forwarded" - ); - } - self.request_sequence += 1; - let sequence = self.request_sequence; - match clean_resume_transport_envelope(payload, &self.manifest, sequence) { - Ok(cleaned) => { - self.pending_forward_sequence = Some(sequence); - Ok((cleaned.payload, sequence)) - } - Err(error) => { - self.fail(error.to_string()); - Err(error) - } - } - } - - fn mark_forwarded(&mut self, sequence: usize) -> anyhow::Result<()> { - if self.failed || self.pending_forward_sequence != Some(sequence) { - self.fail("forwarded sequence did not match the pending validated request"); - anyhow::bail!( - "Resume transport forwarded sequence does not match the pending validated request" - ); - } - self.pending_forward_sequence = None; - self.forwarded_requests += 1; - Ok(()) - } - - fn fail(&mut self, message: impl Into) { - self.failed = true; - if self.failure.is_none() { - self.failure = Some(message.into()); - } - } -} - -impl ClaudeBridgeHandle { - pub fn start( - manifest: ResumeTransportManifest, - routing_session_id: &str, - disable_thinking: bool, - boundary_user_prompt: Option<&str>, - ) -> Result { - let upstream_base = first_nonempty_env(&[ - "OPENAI_BASE_URL", - "OPENAI_API_BASE", - "LLM_BASE_URL", - ]) - .map(|(_, value)| value) - .ok_or_else(|| { - ReplayError::configuration( - "Claude SandboxReplay bridge requires OPENAI_BASE_URL, OPENAI_API_BASE, or LLM_BASE_URL", - ) - })?; - let upstream_api_key = first_nonempty_env(&["OPENAI_API_KEY", "LLM_API_KEY"]) - .map(|(_, value)| value) - .ok_or_else(|| { - ReplayError::configuration( - "Claude SandboxReplay bridge requires OPENAI_API_KEY or LLM_API_KEY", - ) - })?; - let model_name = first_nonempty_env(&["MODEL_NAME", "LLM_MODEL"]) - .map(|(_, value)| value) - .ok_or_else(|| { - ReplayError::configuration( - "Claude SandboxReplay bridge requires MODEL_NAME or LLM_MODEL", - ) - })?; - let upstream_url = chat_completions_url(&upstream_base)?; - let bridge_api_key = format!("pvisor-sandbox-replay-{}", uuid::Uuid::new_v4().simple()); - let listener = TcpListener::bind(("127.0.0.1", 0)).replay_context( - ReplayErrorKind::Continuation, - "allocate Claude SandboxReplay bridge port", - )?; - let address = listener.local_addr().replay_context( - ReplayErrorKind::Continuation, - "read Claude SandboxReplay bridge address", - )?; - listener.set_nonblocking(true).replay_context( - ReplayErrorKind::Continuation, - "configure Claude SandboxReplay bridge listener", - )?; - - let client = reqwest::Client::builder() - .no_proxy() - .build() - .replay_context( - ReplayErrorKind::Continuation, - "build Claude SandboxReplay bridge client", - )?; - let shared = Arc::new(BridgeShared { - resume: Mutex::new(ResumeState { - manifest, - request_sequence: 0, - forwarded_requests: 0, - pending_forward_sequence: None, - failed: false, - failure: None, - }), - client, - upstream_url, - upstream_api_key, - model_name, - routing_session_id: routing_session_id.to_owned(), - bridge_api_key: bridge_api_key.clone(), - disable_thinking, - boundary_user_prompt: boundary_user_prompt.map(str::to_owned), - max_output_tokens: integer_environment( - "SANDBOX_PLAYBACK_BRIDGE_MAX_OUTPUT_TOKENS", - DEFAULT_MAX_OUTPUT_TOKENS, - 1, - )?, - model_context_tokens: integer_environment( - "SANDBOX_PLAYBACK_BRIDGE_MODEL_CONTEXT_TOKENS", - DEFAULT_MODEL_CONTEXT_TOKENS, - 1, - )?, - context_safety_tokens: integer_environment( - "SANDBOX_PLAYBACK_BRIDGE_CONTEXT_SAFETY_TOKENS", - DEFAULT_CONTEXT_SAFETY_TOKENS, - 0, - )?, - upstream_timeout: Duration::from_secs_f64(float_environment( - "SANDBOX_PLAYBACK_BRIDGE_UPSTREAM_TIMEOUT_SECONDS", - DEFAULT_UPSTREAM_TIMEOUT_SECONDS, - 0.001, - )?), - retry_delays: retry_delays_environment()?, - cancelled: AtomicBool::new(false), - cancel_notify: Notify::new(), - }); - let router = router(Arc::clone(&shared)); - let (shutdown_tx, shutdown_rx) = oneshot::channel(); - let (ready_tx, ready_rx) = mpsc::sync_channel(1); - let (done_tx, done_rx) = mpsc::sync_channel(1); - let worker = thread::Builder::new() - .name("pvisor-claude-replay-bridge".into()) - .spawn(move || { - let result = run_bridge_worker(listener, router, shutdown_rx, ready_tx); - let _ = done_tx.send(result); - }) - .replay_context( - ReplayErrorKind::Continuation, - "start Claude SandboxReplay bridge thread", - )?; - let mut handle = Self { - base_url: format!("http://{address}"), - api_key: bridge_api_key, - shared, - shutdown: Some(shutdown_tx), - worker_done: Some(done_rx), - worker: Some(worker), - }; - let startup_error = match ready_rx.recv_timeout(BRIDGE_START_TIMEOUT) { - Ok(Ok(())) => None, - Ok(Err(message)) => Some(message), - Err(mpsc::RecvTimeoutError::Timeout) => Some(format!( - "Claude SandboxReplay bridge did not become ready within {} seconds", - BRIDGE_START_TIMEOUT.as_secs() - )), - Err(mpsc::RecvTimeoutError::Disconnected) => { - Some("Claude SandboxReplay bridge worker exited before reporting readiness".into()) - } - }; - if let Some(mut message) = startup_error { - if let Err(error) = handle.stop_worker() { - message.push_str(&format!("; cleanup also failed: {error}")); - } - return Err(ReplayError::continuation(message)); - } - Ok(handle) - } - - pub fn child_environment(&self) -> BTreeMap { - let no_proxy = merged_no_proxy_environment(); - let mut environment = BTreeMap::from([ - ("ANTHROPIC_BASE_URL".into(), self.base_url.clone()), - ("ANTHROPIC_API_KEY".into(), self.api_key.clone()), - ("ANTHROPIC_AUTH_TOKEN".into(), self.api_key.clone()), - ("ANTHROPIC_MODEL".into(), self.shared.model_name.clone()), - ( - "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC".into(), - "1".into(), - ), - ("IS_SANDBOX".into(), "1".into()), - ]); - environment.insert("NO_PROXY".into(), no_proxy.clone()); - environment.insert("no_proxy".into(), no_proxy); - environment - } - - pub fn finish(mut self) -> Result { - self.stop_worker()?; - let state = self - .shared - .resume - .lock() - .map_err(|_| ReplayError::continuation("Claude bridge state lock poisoned"))?; - if state.failed { - return Err(ReplayError::continuation(format!( - "Claude Resume Transport bridge failed closed: {}", - state - .failure - .as_deref() - .unwrap_or("unknown protocol failure") - ))); - } - if let Some(sequence) = state.pending_forward_sequence { - return Err(ReplayError::continuation(format!( - "Claude Resume Transport request {sequence} was validated but not forwarded" - ))); - } - if state.forwarded_requests == 0 { - return Err(ReplayError::continuation( - "Claude continuation made no validated model request through the SandboxReplay bridge", - )); - } - Ok(state.forwarded_requests) - } - - fn stop_worker(&mut self) -> Result<(), ReplayError> { - self.shared.cancelled.store(true, Ordering::Release); - self.shared.cancel_notify.notify_waiters(); - if let Some(shutdown) = self.shutdown.take() { - let _ = shutdown.send(()); - } - - let worker_result = match self.worker_done.take() { - Some(done) => match done.recv_timeout(BRIDGE_STOP_TIMEOUT) { - Ok(result) => Some(result), - Err(mpsc::RecvTimeoutError::Disconnected) => None, - Err(mpsc::RecvTimeoutError::Timeout) => { - self.worker.take(); - return Err(ReplayError::continuation(format!( - "Claude SandboxReplay bridge did not stop within {} seconds; detached worker", - BRIDGE_STOP_TIMEOUT.as_secs() - ))); - } - }, - None => None, - }; - - if let Some(worker) = self.worker.take() - && worker.join().is_err() - { - return Err(ReplayError::continuation( - "Claude SandboxReplay bridge thread panicked", - )); - } - if let Some(result) = worker_result { - result.replay_context( - ReplayErrorKind::Continuation, - "stop Claude SandboxReplay bridge", - )?; - } - Ok(()) - } -} - -fn run_bridge_worker( - listener: TcpListener, - router: Router, - shutdown_rx: oneshot::Receiver<()>, - ready_tx: mpsc::SyncSender>, -) -> anyhow::Result<()> { - let runtime = match tokio::runtime::Builder::new_multi_thread() - .worker_threads(2) - .enable_all() - .build() - { - Ok(runtime) => runtime, - Err(error) => { - let _ = ready_tx.send(Err(format!("build bridge runtime: {error}"))); - return Err(error.into()); - } - }; - runtime.block_on(async move { - let listener = match tokio::net::TcpListener::from_std(listener) { - Ok(listener) => listener, - Err(error) => { - let _ = ready_tx.send(Err(format!("adopt bridge listener: {error}"))); - return Err(error.into()); - } - }; - ready_tx - .send(Ok(())) - .map_err(|_| anyhow::anyhow!("bridge startup receiver was dropped"))?; - axum::serve(listener, router) - .with_graceful_shutdown(async move { - let _ = shutdown_rx.await; - }) - .await?; - anyhow::Ok(()) - }) -} - -impl Drop for ClaudeBridgeHandle { - fn drop(&mut self) { - let _ = self.stop_worker(); - } -} - -fn router(shared: Arc) -> Router { - Router::new() - .route("/health", get(health)) - .route("/messages", post(messages)) - .route("/v1/messages", post(messages)) - .route("/messages/count_tokens", post(count_tokens)) - .route("/v1/messages/count_tokens", post(count_tokens)) - .fallback(not_found) - .layer(DefaultBodyLimit::max(64 * 1024 * 1024)) - .with_state(shared) -} - -async fn health(State(shared): State>) -> Response { - let (failed, request_sequence) = shared - .resume - .lock() - .map(|state| (state.failed, state.request_sequence)) - .unwrap_or((true, 0)); - json_response( - StatusCode::OK, - json!({ - "status": "healthy", - "bridge_version": BRIDGE_VERSION, - "resume_mode": true, - "resume_failed": failed, - "request_sequence": request_sequence, - }), - ) -} - -async fn count_tokens( - State(shared): State>, - headers: HeaderMap, - body: Bytes, -) -> Response { - if !authorized(&shared, &headers) { - return authentication_error(); - } - let payload = match json_object(&body) { - Ok(payload) => payload, - Err(error) => return invalid_request(StatusCode::BAD_REQUEST, error), - }; - json_response( - StatusCode::OK, - json!({"input_tokens": estimate_input_tokens(&payload)}), - ) -} - -async fn messages( - State(shared): State>, - headers: HeaderMap, - body: Bytes, -) -> Response { - if !authorized(&shared, &headers) { - return authentication_error(); - } - let payload = match json_object(&body) { - Ok(payload) => payload, - Err(error) => return invalid_request(StatusCode::BAD_REQUEST, error), - }; - let (mut cleaned, sequence) = { - let mut resume = match shared.resume.lock() { - Ok(resume) => resume, - Err(_) => { - return invalid_request( - StatusCode::UNPROCESSABLE_ENTITY, - "Resume transport state lock poisoned".into(), - ); - } - }; - match resume.clean(&payload) { - Ok(cleaned) => cleaned, - Err(error) => { - return invalid_request(StatusCode::UNPROCESSABLE_ENTITY, error.to_string()); - } - } - }; - if let Err(error) = inject_boundary_user_prompt( - &mut cleaned, - shared.boundary_user_prompt.as_deref(), - sequence, - ) { - fail_resume( - &shared, - format!("boundary user prompt injection failed: {error}"), - ); - return invalid_request(StatusCode::UNPROCESSABLE_ENTITY, error.to_string()); - } - let request = match openai_request( - &cleaned, - &shared.model_name, - shared.max_output_tokens, - shared.model_context_tokens, - shared.context_safety_tokens, - shared.disable_thinking, - ) { - Ok(request) => request, - Err(error) => { - fail_resume(&shared, format!("request conversion failed: {error}")); - return invalid_request(StatusCode::UNPROCESSABLE_ENTITY, error.to_string()); - } - }; - { - let mut resume = match shared.resume.lock() { - Ok(resume) => resume, - Err(_) => { - return invalid_request( - StatusCode::UNPROCESSABLE_ENTITY, - "Resume transport state lock poisoned".into(), - ); - } - }; - if let Err(error) = resume.mark_forwarded(sequence) { - return invalid_request(StatusCode::UNPROCESSABLE_ENTITY, error.to_string()); - } - } - - let serialized = match serde_json::to_vec(&request) { - Ok(serialized) => serialized, - Err(error) => { - return upstream_error( - StatusCode::BAD_GATEWAY, - format!("serialize upstream request: {error}"), - ); - } - }; - let (status, upstream_body) = match forward_openai_request(&shared, serialized).await { - Ok(result) => result, - Err(error) => return upstream_error(StatusCode::BAD_GATEWAY, error.to_string()), - }; - if status != StatusCode::OK.as_u16() { - let status = StatusCode::from_u16(status).unwrap_or(StatusCode::BAD_GATEWAY); - return upstream_error( - status, - String::from_utf8_lossy(&upstream_body[..upstream_body.len().min(1000)]).into_owned(), - ); - } - let upstream: Value = match serde_json::from_slice(&upstream_body) { - Ok(Value::Object(value)) => Value::Object(value), - Ok(_) => { - return upstream_error( - StatusCode::BAD_GATEWAY, - "Upstream response must be a JSON object".into(), - ); - } - Err(error) => { - return upstream_error( - StatusCode::BAD_GATEWAY, - format!("parse upstream response: {error}"), - ); - } - }; - let message = match anthropic_response(&cleaned, &upstream, &shared.model_name) { - Ok(message) => message, - Err(error) => return upstream_error(StatusCode::BAD_GATEWAY, error.to_string()), - }; - if cleaned.get("stream").and_then(Value::as_bool) == Some(true) { - sse_response(&message) - } else { - json_response(StatusCode::OK, message) - } -} - -fn inject_boundary_user_prompt( - payload: &mut Value, - prompt: Option<&str>, - request_sequence: usize, -) -> anyhow::Result { - let Some(prompt) = prompt.filter(|prompt| !prompt.is_empty()) else { - return Ok(false); - }; - if request_sequence != 1 { - return Ok(false); - } - let messages = payload - .get_mut("messages") - .and_then(Value::as_array_mut) - .ok_or_else(|| anyhow::anyhow!("cleaned payload.messages must be an array"))?; - messages.push(json!({"role": "user", "content": prompt})); - Ok(true) -} - -async fn not_found() -> Response { - json_response( - StatusCode::NOT_FOUND, - json!({"error": {"message": "not found"}}), - ) -} - -fn authorized(shared: &BridgeShared, headers: &HeaderMap) -> bool { - let supplied = headers - .get("x-api-key") - .and_then(|value| value.to_str().ok()) - .or_else(|| { - headers - .get(AUTHORIZATION) - .and_then(|value| value.to_str().ok()) - .and_then(|value| value.strip_prefix("Bearer ")) - }); - supplied == Some(shared.bridge_api_key.as_str()) -} - -fn authentication_error() -> Response { - json_response( - StatusCode::UNAUTHORIZED, - json!({ - "type": "error", - "error": {"type": "authentication_error", "message": "invalid API key"}, - }), - ) -} - -fn invalid_request(status: StatusCode, message: String) -> Response { - json_response( - status, - json!({ - "type": "error", - "error": { - "type": "invalid_request_error", - "message": message, - }, - }), - ) -} - -fn upstream_error(status: StatusCode, message: String) -> Response { - json_response( - status, - json!({ - "type": "error", - "error": {"type": "api_error", "message": message}, - }), - ) -} - -fn json_response(status: StatusCode, payload: Value) -> Response { - let mut response = Response::new(Body::from(payload.to_string())); - *response.status_mut() = status; - response.headers_mut().insert( - CONTENT_TYPE, - HeaderValue::from_static("application/json; charset=utf-8"), - ); - response -} - -fn sse_response(message: &Value) -> Response { - let mut rendered = String::new(); - for (event, payload) in sse_events(message) { - rendered.push_str("event: "); - rendered.push_str(event); - rendered.push_str("\ndata: "); - rendered.push_str(&payload.to_string()); - rendered.push_str("\n\n"); - } - let mut response = Response::new(Body::from(rendered)); - *response.status_mut() = StatusCode::OK; - response - .headers_mut() - .insert(CONTENT_TYPE, HeaderValue::from_static("text/event-stream")); - response - .headers_mut() - .insert(CACHE_CONTROL, HeaderValue::from_static("no-cache")); - response -} - -fn json_object(body: &[u8]) -> Result { - match serde_json::from_slice(body) { - Ok(Value::Object(value)) => Ok(Value::Object(value)), - Ok(_) => Err("payload must be a JSON object".into()), - Err(error) => Err(format!("invalid JSON: {error}")), - } -} - -fn fail_resume(shared: &BridgeShared, message: String) { - if let Ok(mut resume) = shared.resume.lock() { - resume.fail(message); - } -} - -async fn forward_openai_request( - shared: &BridgeShared, - body: Vec, -) -> anyhow::Result<(u16, Vec)> { - let attempts = shared.retry_delays.len() + 1; - for attempt in 0..attempts { - if shared.cancelled.load(Ordering::Acquire) { - anyhow::bail!("Claude SandboxReplay bridge request was cancelled during shutdown"); - } - let send = shared - .client - .post(&shared.upstream_url) - .timeout(shared.upstream_timeout) - .header( - AUTHORIZATION.as_str(), - format!("Bearer {}", shared.upstream_api_key), - ) - .header(CONTENT_TYPE.as_str(), "application/json") - .header("X-LiteLLM-Session-ID", &shared.routing_session_id) - .body(body.clone()) - .send(); - let result = tokio::select! { - biased; - _ = wait_for_cancellation(shared) => { - anyhow::bail!("Claude SandboxReplay bridge request was cancelled during shutdown"); - } - result = send => result, - }; - match result { - Ok(response) => { - let status = response.status().as_u16(); - let body_result = tokio::select! { - biased; - _ = wait_for_cancellation(shared) => { - anyhow::bail!("Claude SandboxReplay bridge request was cancelled during shutdown"); - } - result = response.bytes() => result, - }; - match body_result { - Ok(response_body) => { - let response_body = response_body.to_vec(); - if !RETRYABLE_UPSTREAM_STATUSES.contains(&status) || attempt + 1 == attempts - { - return Ok((status, response_body)); - } - } - Err(error) if attempt + 1 == attempts => return Err(error.into()), - Err(_) => {} - } - } - Err(error) if attempt + 1 == attempts => return Err(error.into()), - Err(_) => {} - } - tokio::select! { - biased; - _ = wait_for_cancellation(shared) => { - anyhow::bail!("Claude SandboxReplay bridge request was cancelled during shutdown"); - } - _ = tokio::time::sleep(shared.retry_delays[attempt]) => {} - } - } - unreachable!("upstream retry loop always returns") -} - -async fn wait_for_cancellation(shared: &BridgeShared) { - loop { - let notified = shared.cancel_notify.notified(); - tokio::pin!(notified); - notified.as_mut().enable(); - if shared.cancelled.load(Ordering::Acquire) { - return; - } - notified.await; - if shared.cancelled.load(Ordering::Acquire) { - return; - } - } -} - -fn openai_request( - payload: &Value, - model_name: &str, - max_output_tokens: usize, - model_context_tokens: usize, - context_safety_tokens: usize, - disable_thinking: bool, -) -> anyhow::Result { - let input_tokens = estimate_input_tokens(payload); - let available_output_tokens = model_context_tokens - .checked_sub(context_safety_tokens) - .and_then(|remaining| remaining.checked_sub(input_tokens)) - .filter(|remaining| *remaining > 0) - .ok_or_else(|| { - anyhow::anyhow!( - "input token count exceeds the maximum number of tokens: estimated_input={input_tokens}, context={model_context_tokens}, reserved={context_safety_tokens}" - ) - })?; - let requested_output_tokens = - parse_requested_output_tokens(payload.get("max_tokens"), max_output_tokens)?; - let temperature = parse_temperature(payload.get("temperature"))?; - let mut request = Map::new(); - request.insert("model".into(), Value::String(model_name.to_owned())); - request.insert("messages".into(), Value::Array(openai_messages(payload)?)); - request.insert( - "max_tokens".into(), - json!( - requested_output_tokens - .min(max_output_tokens) - .min(available_output_tokens) - ), - ); - request.insert("temperature".into(), json!(temperature)); - let tools = openai_tools(payload)?; - if !tools.is_empty() { - request.insert("tools".into(), Value::Array(tools)); - request.insert( - "tool_choice".into(), - tool_choice(payload.get("tool_choice"))?.unwrap_or_else(|| json!("auto")), - ); - } - if disable_thinking { - request.insert( - "chat_template_kwargs".into(), - json!({"enable_thinking": false}), - ); - request.insert("reasoning_effort".into(), json!("none")); - } - Ok(Value::Object(request)) -} - -fn parse_requested_output_tokens(value: Option<&Value>, default: usize) -> anyhow::Result { - let Some(value) = value.filter(|value| python_truthy(value)) else { - return Ok(default); - }; - let parsed = match value { - Value::Bool(true) => 1_i128, - Value::Number(number) => { - if let Some(value) = number.as_i64() { - i128::from(value) - } else if let Some(value) = number.as_u64() { - i128::from(value) - } else if let Some(value) = number.as_f64() { - let truncated = value.trunc(); - if !value.is_finite() - || truncated < i128::MIN as f64 - || truncated > i128::MAX as f64 - { - anyhow::bail!("payload.max_tokens must be an integer"); - } - truncated as i128 - } else { - anyhow::bail!("payload.max_tokens must be an integer"); - } - } - Value::String(value) => value - .trim() - .parse::() - .map_err(|_| anyhow::anyhow!("payload.max_tokens must be an integer"))?, - _ => anyhow::bail!("payload.max_tokens must be an integer"), - }; - if parsed < 1 { - anyhow::bail!("payload.max_tokens must be positive"); - } - usize::try_from(parsed).map_err(|_| anyhow::anyhow!("payload.max_tokens must be an integer")) -} - -fn parse_temperature(value: Option<&Value>) -> anyhow::Result { - let Some(value) = value.filter(|value| python_truthy(value)) else { - return Ok(0.0); - }; - let parsed = match value { - Value::Bool(true) => 1.0, - Value::Number(value) => value - .as_f64() - .ok_or_else(|| anyhow::anyhow!("payload.temperature must be a number"))?, - Value::String(value) => value - .trim() - .parse::() - .map_err(|_| anyhow::anyhow!("payload.temperature must be a number"))?, - _ => anyhow::bail!("payload.temperature must be a number"), - }; - if !parsed.is_finite() { - anyhow::bail!("payload.temperature must be a finite number"); - } - Ok(parsed) -} - -fn python_truthy(value: &Value) -> bool { - match value { - Value::Null => false, - Value::Bool(value) => *value, - Value::Number(value) => value.as_f64() != Some(0.0), - Value::String(value) => !value.is_empty(), - Value::Array(value) => !value.is_empty(), - Value::Object(value) => !value.is_empty(), - } -} - -fn openai_messages(payload: &Value) -> anyhow::Result> { - let messages = payload - .get("messages") - .and_then(Value::as_array) - .ok_or_else(|| anyhow::anyhow!("payload.messages must be an array of objects"))?; - if messages.iter().any(|message| !message.is_object()) { - anyhow::bail!("payload.messages must be an array of objects"); - } - let mut output = Vec::new(); - let mut system_parts = Vec::new(); - if let Some(system) = payload.get("system") { - let rendered = content_text(system); - if !rendered.is_empty() { - system_parts.push(rendered); - } - } - for (message_index, message) in messages.iter().enumerate() { - let role = required_string( - message.get("role"), - &format!("messages[{message_index}].role"), - )?; - let content = message.get("content").unwrap_or(&Value::Null); - if matches!(role, "system" | "developer") { - let rendered = content_text(content); - if !rendered.is_empty() { - system_parts.push(rendered); - } - continue; - } - if let Some(text) = content.as_str() { - output.push(json!({"role": role, "content": text})); - continue; - } - let blocks = content.as_array().ok_or_else(|| { - anyhow::anyhow!("messages[{message_index}].content must be text or a block array") - })?; - if blocks.iter().any(|block| !block.is_object()) { - anyhow::bail!("messages[{message_index}].content must be text or a block array"); - } - if role == "assistant" { - let text = content_text(content); - let mut tool_calls = Vec::new(); - for (block_index, block) in blocks.iter().enumerate() { - if block.get("type").and_then(Value::as_str) != Some("tool_use") { - continue; - } - let id = required_string( - block.get("id"), - &format!("messages[{message_index}].content[{block_index}].tool_use.id"), - )?; - let name = required_string(block.get("name"), &format!("tool_use {id:?} name"))?; - let input = block - .get("input") - .filter(|input| input.is_object()) - .ok_or_else(|| anyhow::anyhow!("tool_use {id:?} input must be an object"))?; - tool_calls.push(json!({ - "id": id, - "type": "function", - "function": { - "name": name, - "arguments": serde_json::to_string(input)?, - }, - })); - } - let mut item = json!({ - "role": "assistant", - "content": if text.is_empty() { Value::Null } else { Value::String(text) }, - }); - if !tool_calls.is_empty() { - item["tool_calls"] = Value::Array(tool_calls); - } - output.push(item); - continue; - } - if role != "user" { - anyhow::bail!("Unsupported Anthropic message role {role:?}"); - } - let mut pending_text = Vec::new(); - for (block_index, block) in blocks.iter().enumerate() { - match block.get("type").and_then(Value::as_str) { - Some("text") => pending_text.push( - block - .get("text") - .and_then(Value::as_str) - .unwrap_or_default() - .to_owned(), - ), - Some("tool_result") => { - if !pending_text.is_empty() { - output.push(json!({"role": "user", "content": pending_text.join("\n")})); - pending_text.clear(); - } - let call_id = - required_string(block.get("tool_use_id"), "tool_result.tool_use_id")?; - let result_content = block.get("content").unwrap_or(&Value::Null); - let rendered = content_text(result_content); - output.push(json!({ - "role": "tool", - "tool_call_id": call_id, - "content": if rendered.is_empty() { - scalar_text(result_content) - } else { - rendered - }, - })); - } - other => anyhow::bail!( - "Unsupported user content block {other:?} at messages[{message_index}].content[{block_index}]" - ), - } - } - if !pending_text.is_empty() { - output.push(json!({"role": "user", "content": pending_text.join("\n")})); - } - } - if !system_parts.is_empty() { - output.insert( - 0, - json!({"role": "system", "content": system_parts.join("\n\n")}), - ); - } - Ok(output) -} - -fn openai_tools(payload: &Value) -> anyhow::Result> { - let Some(raw_tools) = payload.get("tools") else { - return Ok(Vec::new()); - }; - if raw_tools.is_null() { - return Ok(Vec::new()); - } - let raw_tools = raw_tools - .as_array() - .ok_or_else(|| anyhow::anyhow!("payload.tools must be an array of objects"))?; - let mut tools = Vec::new(); - for (index, tool) in raw_tools.iter().enumerate() { - let name = required_string(tool.get("name"), &format!("tools[{index}].name"))?; - let schema = tool - .get("input_schema") - .filter(|schema| !schema.is_null()) - .cloned() - .unwrap_or_else(|| json!({"type": "object", "properties": {}})); - if !schema.is_object() { - anyhow::bail!("tools[{index}].input_schema must be an object"); - } - tools.push(json!({ - "type": "function", - "function": { - "name": name, - "description": tool.get("description").and_then(Value::as_str).unwrap_or_default(), - "parameters": schema, - }, - })); - } - Ok(tools) -} - -fn tool_choice(value: Option<&Value>) -> anyhow::Result> { - let Some(value) = value.and_then(Value::as_object) else { - return Ok(None); - }; - match value.get("type").and_then(Value::as_str) { - Some("auto") => Ok(Some(json!("auto"))), - Some("any") => Ok(Some(json!("required"))), - Some("tool") => Ok(Some(json!({ - "type": "function", - "function": {"name": required_string(value.get("name"), "tool_choice.name")?}, - }))), - other => anyhow::bail!("Unsupported tool_choice type {other:?}"), - } -} - -fn anthropic_response( - payload: &Value, - upstream: &Value, - model_name: &str, -) -> anyhow::Result { - let choice = upstream - .get("choices") - .and_then(Value::as_array) - .and_then(|choices| choices.first()) - .filter(|choice| choice.is_object()) - .ok_or_else(|| anyhow::anyhow!("OpenAI response has no first choice"))?; - let message = choice - .get("message") - .filter(|message| message.is_object()) - .ok_or_else(|| anyhow::anyhow!("OpenAI response choice has no message"))?; - let mut content = Vec::new(); - if let Some(text) = message.get("content").and_then(Value::as_str) - && !text.is_empty() - { - content.push(json!({"type": "text", "text": text})); - } - let raw_tool_calls = match message.get("tool_calls") { - None | Some(Value::Null) => Vec::new(), - Some(value) => value - .as_array() - .ok_or_else(|| anyhow::anyhow!("OpenAI response tool_calls must be objects"))? - .clone(), - }; - if raw_tool_calls.iter().any(|call| !call.is_object()) { - anyhow::bail!("OpenAI response tool_calls must be objects"); - } - for (index, call) in raw_tool_calls.iter().enumerate() { - let function = call - .get("function") - .filter(|function| function.is_object()) - .ok_or_else(|| anyhow::anyhow!("OpenAI tool_calls[{index}] has no function"))?; - let arguments_text = match function.get("arguments") { - None | Some(Value::Null) => "{}", - Some(Value::String(value)) if value.is_empty() => "{}", - Some(Value::String(value)) => value.as_str(), - Some(_) => { - anyhow::bail!("OpenAI tool_calls[{index}] arguments must be JSON text") - } - }; - let arguments: Value = serde_json::from_str(arguments_text).map_err(|_| { - anyhow::anyhow!("OpenAI tool_calls[{index}] arguments are invalid JSON") - })?; - if !arguments.is_object() { - anyhow::bail!("OpenAI tool_calls[{index}] arguments must decode to an object"); - } - content.push(json!({ - "type": "tool_use", - "id": required_string(call.get("id"), &format!("OpenAI tool_calls[{index}].id"))?, - "name": required_string(function.get("name"), &format!("OpenAI tool_calls[{index}].function.name"))?, - "input": arguments, - })); - } - let stop_reason = if !raw_tool_calls.is_empty() { - "tool_use" - } else if choice.get("finish_reason").and_then(Value::as_str) == Some("length") { - "max_tokens" - } else { - "end_turn" - }; - let usage = upstream.get("usage").and_then(Value::as_object); - Ok(json!({ - "id": upstream.get("id").and_then(Value::as_str).map(str::to_owned) - .unwrap_or_else(|| format!("msg_{}", uuid::Uuid::new_v4().simple())), - "type": "message", - "role": "assistant", - "model": payload.get("model").and_then(Value::as_str).unwrap_or(model_name), - "content": content, - "stop_reason": stop_reason, - "stop_sequence": Value::Null, - "usage": { - "input_tokens": usage.and_then(|value| value.get("prompt_tokens")).and_then(Value::as_u64).unwrap_or(0), - "output_tokens": usage.and_then(|value| value.get("completion_tokens")).and_then(Value::as_u64).unwrap_or(0), - "cache_creation_input_tokens": 0, - "cache_read_input_tokens": 0, - }, - })) -} - -fn sse_events(message: &Value) -> Vec<(&'static str, Value)> { - let usage = message.get("usage").cloned().unwrap_or_else(|| json!({})); - let mut start = message.clone(); - start["content"] = json!([]); - start["stop_reason"] = Value::Null; - start["usage"] = usage.clone(); - start["usage"]["output_tokens"] = json!(0); - let mut events = vec![( - "message_start", - json!({"type": "message_start", "message": start}), - )]; - for (index, block) in message - .get("content") - .and_then(Value::as_array) - .into_iter() - .flatten() - .enumerate() - { - if block.get("type").and_then(Value::as_str) == Some("text") { - events.push(( - "content_block_start", - json!({ - "type": "content_block_start", - "index": index, - "content_block": {"type": "text", "text": ""}, - }), - )); - events.push(( - "content_block_delta", - json!({ - "type": "content_block_delta", - "index": index, - "delta": {"type": "text_delta", "text": block.get("text").cloned().unwrap_or(json!(""))}, - }), - )); - } else { - events.push(( - "content_block_start", - json!({ - "type": "content_block_start", - "index": index, - "content_block": { - "type": "tool_use", - "id": block.get("id"), - "name": block.get("name"), - "input": {}, - }, - }), - )); - events.push(( - "content_block_delta", - json!({ - "type": "content_block_delta", - "index": index, - "delta": { - "type": "input_json_delta", - "partial_json": serde_json::to_string(block.get("input").unwrap_or(&json!({}))).unwrap_or_else(|_| "{}".into()), - }, - }), - )); - } - events.push(( - "content_block_stop", - json!({"type": "content_block_stop", "index": index}), - )); - } - events.push(( - "message_delta", - json!({ - "type": "message_delta", - "delta": { - "stop_reason": message.get("stop_reason"), - "stop_sequence": Value::Null, - }, - "usage": {"output_tokens": usage.get("output_tokens").cloned().unwrap_or(json!(0))}, - }), - )); - events.push(("message_stop", json!({"type": "message_stop"}))); - events -} - -fn content_text(value: &Value) -> String { - if let Some(text) = value.as_str() { - return text.to_owned(); - } - let Some(values) = value.as_array() else { - return String::new(); - }; - values - .iter() - .filter_map(|item| { - if let Some(text) = item.as_str() { - return Some(text.to_owned()); - } - match item.get("type").and_then(Value::as_str) { - Some("text") => Some( - item.get("text") - .and_then(Value::as_str) - .unwrap_or_default() - .to_owned(), - ), - Some("tool_result") => { - Some(content_text(item.get("content").unwrap_or(&Value::Null))) - } - _ => None, - } - }) - .filter(|text| !text.is_empty()) - .collect::>() - .join("\n") -} - -fn scalar_text(value: &Value) -> String { - match value { - Value::Null | Value::Bool(false) => String::new(), - Value::String(value) => value.clone(), - Value::Bool(true) => "True".into(), - Value::Number(value) if value.as_f64() == Some(0.0) => String::new(), - Value::Number(value) => value.to_string(), - Value::Array(value) if value.is_empty() => String::new(), - Value::Object(value) if value.is_empty() => String::new(), - other => other.to_string(), - } -} - -fn required_string<'a>(value: Option<&'a Value>, context: &str) -> anyhow::Result<&'a str> { - value - .and_then(Value::as_str) - .filter(|value| !value.is_empty()) - .ok_or_else(|| anyhow::anyhow!("{context} must be a non-empty string")) -} - -fn estimate_input_tokens(payload: &Value) -> usize { - let rendered = serde_json::to_vec(payload).unwrap_or_default(); - rendered.len().div_ceil(3).max(1) -} - -fn chat_completions_url(base: &str) -> Result { - let mut url = reqwest::Url::parse(base).map_err(|error| { - ReplayError::configuration(format!("invalid OpenAI base URL {base:?}: {error}")) - })?; - let path = url.path().trim_end_matches('/'); - let path = if path.ends_with("/chat/completions") { - path.to_owned() - } else if path.is_empty() { - "/v1/chat/completions".into() - } else { - format!("{path}/chat/completions") - }; - url.set_path(&path); - url.set_query(None); - url.set_fragment(None); - Ok(url.to_string()) -} - -fn merged_no_proxy_environment() -> String { - let configured = ["NO_PROXY", "no_proxy"] - .iter() - .filter_map(|name| std::env::var(name).ok()) - .collect::>(); - merge_no_proxy_values(configured.iter().map(String::as_str)) -} - -fn merge_no_proxy_values<'a>(values: impl IntoIterator) -> String { - let mut entries = Vec::::new(); - for value in values { - for entry in value - .split(',') - .map(str::trim) - .filter(|entry| !entry.is_empty()) - { - if !entries.iter().any(|existing| existing == entry) { - entries.push(entry.to_owned()); - } - } - } - for required in ["127.0.0.1", "localhost", "::1"] { - if !entries.iter().any(|existing| existing == required) { - entries.push(required.into()); - } - } - entries.join(",") -} - -fn first_nonempty_env(names: &[&'static str]) -> Option<(&'static str, String)> { - names.iter().find_map(|name| { - std::env::var(name) - .ok() - .filter(|value| !value.trim().is_empty()) - .map(|value| (*name, value)) - }) -} - -fn integer_environment(name: &str, default: usize, minimum: usize) -> Result { - let Some(raw) = std::env::var(name) - .ok() - .filter(|value| !value.trim().is_empty()) - else { - return Ok(default); - }; - let value = raw.parse::().map_err(|error| { - ReplayError::configuration(format!("{name} must be an integer: {error}")) - })?; - if value < minimum { - return Err(ReplayError::configuration(format!( - "{name} must be >= {minimum}" - ))); - } - Ok(value) -} - -fn float_environment(name: &str, default: f64, minimum: f64) -> Result { - let Some(raw) = std::env::var(name) - .ok() - .filter(|value| !value.trim().is_empty()) - else { - return Ok(default); - }; - let value = raw - .parse::() - .map_err(|error| ReplayError::configuration(format!("{name} must be a number: {error}")))?; - if !value.is_finite() || value < minimum { - return Err(ReplayError::configuration(format!( - "{name} must be a finite number >= {minimum}" - ))); - } - Ok(value) -} - -fn retry_delays_environment() -> Result, ReplayError> { - let Some(raw) = std::env::var("SANDBOX_PLAYBACK_BRIDGE_RETRY_DELAYS_SECONDS") - .ok() - .filter(|value| !value.trim().is_empty()) - else { - return Ok(DEFAULT_RETRY_DELAYS_SECONDS - .iter() - .copied() - .map(Duration::from_secs_f64) - .collect()); - }; - raw.split(',') - .map(|part| { - let value = part.trim().parse::().map_err(|error| { - ReplayError::configuration(format!( - "SANDBOX_PLAYBACK_BRIDGE_RETRY_DELAYS_SECONDS must contain numbers: {error}" - )) - })?; - if !value.is_finite() || value < 0.0 { - return Err(ReplayError::configuration( - "SANDBOX_PLAYBACK_BRIDGE_RETRY_DELAYS_SECONDS values must be finite and non-negative", - )); - } - Ok(Duration::from_secs_f64(value)) - }) - .collect() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn boundary_user_prompt_is_injected_once_after_the_clean_prefix() { - let mut first = json!({ - "messages": [ - {"role": "user", "content": "task"}, - {"role": "tool", "tool_call_id": "call-1", "content": "O-prime N"} - ] - }); - assert!(inject_boundary_user_prompt(&mut first, Some("review it"), 1).unwrap()); - assert_eq!(first["messages"].as_array().unwrap().len(), 3); - assert_eq!( - first["messages"][2], - json!({"role": "user", "content": "review it"}) - ); - - let mut later = first.clone(); - assert!(!inject_boundary_user_prompt(&mut later, Some("review it"), 2).unwrap()); - assert_eq!(later, first); - - let mut disabled = json!({"messages": []}); - assert!(!inject_boundary_user_prompt(&mut disabled, None, 1).unwrap()); - assert!(disabled["messages"].as_array().unwrap().is_empty()); - } - - #[test] - fn openai_projection_matches_python_bridge_contract() { - let payload = json!({ - "model": "claude", - "max_tokens": 64, - "system": [{"type":"text","text":"system"}], - "messages": [ - {"role":"user","content":"task"}, - {"role":"assistant","content":[ - {"type":"thinking","thinking":"hidden"}, - {"type":"text","text":"run"}, - {"type":"tool_use","id":"call-1","name":"Bash","input":{"command":"pwd"}} - ]}, - {"role":"user","content":[ - {"type":"tool_result","tool_use_id":"call-1","content":"/workspace"} - ]} - ], - "tools": [{"name":"Bash","description":"shell","input_schema":{"type":"object"}}] - }); - let request = openai_request(&payload, "qwen", 8192, 200_000, 1024, true).unwrap(); - assert_eq!(request["model"], "qwen"); - assert_eq!(request["messages"][0]["role"], "system"); - assert_eq!(request["messages"][2]["tool_calls"][0]["id"], "call-1"); - assert_eq!(request["messages"][3]["role"], "tool"); - assert_eq!(request["chat_template_kwargs"]["enable_thinking"], false); - assert_eq!(request["reasoning_effort"], "none"); - assert!(request.get("stream").is_none()); - } - - #[test] - fn openai_response_is_synthesized_as_anthropic_sse() { - let payload = json!({"model":"claude","stream":true}); - let upstream = json!({ - "id":"chat-1", - "choices":[{"finish_reason":"tool_calls","message":{ - "content":"working", - "tool_calls":[{"id":"call-1","function":{"name":"Read","arguments":"{\"file_path\":\"/a\"}"}}] - }}], - "usage":{"prompt_tokens":12,"completion_tokens":5} - }); - let message = anthropic_response(&payload, &upstream, "qwen").unwrap(); - assert_eq!(message["stop_reason"], "tool_use"); - assert_eq!(message["content"][1]["input"]["file_path"], "/a"); - let events = sse_events(&message); - assert_eq!(events.first().unwrap().0, "message_start"); - assert_eq!(events.last().unwrap().0, "message_stop"); - } - - #[test] - fn base_url_accepts_root_v1_and_complete_endpoint() { - assert_eq!( - chat_completions_url("http://127.0.0.1:8000").unwrap(), - "http://127.0.0.1:8000/v1/chat/completions" - ); - assert_eq!( - chat_completions_url("http://127.0.0.1:8000/v1").unwrap(), - "http://127.0.0.1:8000/v1/chat/completions" - ); - assert_eq!( - chat_completions_url("http://127.0.0.1:8000/v1/chat/completions").unwrap(), - "http://127.0.0.1:8000/v1/chat/completions" - ); - } - #[test] - fn bridge_cleans_resume_transport_before_upstream_io() { - let runtime = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap(); - runtime.block_on(async { - type Captured = Arc>>; - - async fn capture( - State(captured): State, - headers: HeaderMap, - axum::Json(payload): axum::Json, - ) -> axum::Json { - *captured.lock().unwrap() = Some((headers, payload)); - axum::Json(json!({ - "id": "chat-test", - "choices": [{ - "finish_reason": "stop", - "message": {"role": "assistant", "content": "continued"} - }], - "usage": {"prompt_tokens": 31, "completion_tokens": 2} - })) - } - - let captured: Captured = Arc::new(Mutex::new(None)); - let upstream_listener = tokio::net::TcpListener::bind(("127.0.0.1", 0)) - .await - .unwrap(); - let upstream_address = upstream_listener.local_addr().unwrap(); - let upstream_router = Router::new() - .route("/v1/chat/completions", post(capture)) - .with_state(Arc::clone(&captured)); - let upstream = tokio::spawn(async move { - axum::serve(upstream_listener, upstream_router) - .await - .unwrap(); - }); - - let canonical = vec![ - json!({"role": "user", "content": "task"}), - json!({ - "role": "assistant", - "content": [{ - "type": "tool_use", - "id": "tool-1", - "name": "Bash", - "input": {"command": "pwd"} - }] - }), - json!({ - "role": "user", - "content": [{ - "type": "tool_result", - "tool_use_id": "tool-1", - "content": "fresh observation" - }] - }), - ]; - let nonce = "__PVISOR_NATIVE_REPLAY_0123456789abcdef__".to_owned(); - let manifest = ResumeTransportManifest::create( - "session-1", - vec!["tool-1".into()], - canonical.clone(), - nonce.clone(), - ) - .unwrap(); - let payload = json!({ - "model": "claude", - "stream": true, - "max_tokens": 64, - "messages": [ - canonical[0].clone(), - canonical[1].clone(), - {"role": "user", "content": [ - { - "type": "tool_result", - "tool_use_id": "tool-1", - "content": "fresh observation" - }, - {"type": "text", "text": "Continue from where you left off."} - ]}, - {"role": "assistant", "content": "No response requested."}, - {"role": "user", "content": nonce}, - {"role": "system", "content": "temporary suffix must be dropped"} - ] - }); - let shared = Arc::new(BridgeShared { - resume: Mutex::new(ResumeState { - manifest, - request_sequence: 0, - forwarded_requests: 0, - pending_forward_sequence: None, - failed: false, - failure: None, - }), - client: reqwest::Client::builder().no_proxy().build().unwrap(), - upstream_url: format!("http://{upstream_address}/v1/chat/completions"), - upstream_api_key: "upstream-secret".into(), - model_name: "qwen".into(), - routing_session_id: "trial-session".into(), - bridge_api_key: "local-key".into(), - disable_thinking: true, - boundary_user_prompt: None, - max_output_tokens: 8192, - model_context_tokens: 200_000, - context_safety_tokens: 1024, - upstream_timeout: Duration::from_secs(5), - retry_delays: Vec::new(), - cancelled: AtomicBool::new(false), - cancel_notify: Notify::new(), - }); - let mut headers = HeaderMap::new(); - headers.insert("x-api-key", HeaderValue::from_static("local-key")); - let response = messages( - State(Arc::clone(&shared)), - headers, - Bytes::from(payload.to_string()), - ) - .await; - assert_eq!(response.status(), StatusCode::OK); - assert_eq!( - response.headers().get(CONTENT_TYPE).unwrap(), - "text/event-stream" - ); - let body = axum::body::to_bytes(response.into_body(), usize::MAX) - .await - .unwrap(); - let rendered = String::from_utf8(body.to_vec()).unwrap(); - assert!(rendered.contains("event: message_start")); - assert!(rendered.contains("continued")); - - let (upstream_headers, upstream_payload) = captured.lock().unwrap().clone().unwrap(); - assert_eq!( - upstream_headers - .get("X-LiteLLM-Session-ID") - .unwrap() - .to_str() - .unwrap(), - "trial-session" - ); - assert_eq!(upstream_payload["model"], "qwen"); - assert_eq!( - upstream_payload["chat_template_kwargs"]["enable_thinking"], - false - ); - assert_eq!(upstream_payload["reasoning_effort"], "none"); - assert!(upstream_payload.get("stream").is_none()); - let messages = upstream_payload["messages"].as_array().unwrap(); - assert_eq!(messages.len(), 3); - assert_eq!(messages.last().unwrap()["role"], "tool"); - assert_eq!(messages.last().unwrap()["content"], "fresh observation"); - assert!( - !upstream_payload - .to_string() - .contains("PVISOR_NATIVE_REPLAY") - ); - assert!( - !upstream_payload - .to_string() - .contains("temporary suffix must be dropped") - ); - - let resume = shared.resume.lock().unwrap(); - assert!(!resume.failed); - assert_eq!(resume.request_sequence, 1); - assert_eq!(resume.forwarded_requests, 1); - assert_eq!(resume.pending_forward_sequence, None); - drop(resume); - upstream.abort(); - }); - } - - #[test] - fn bridge_resume_state_stays_failed_after_one_rejection() { - let canonical = vec![ - json!({"role": "user", "content": "task"}), - json!({"role": "assistant", "content": [{ - "type": "tool_use", "id": "tool-1", "name": "Bash", "input": {} - }]}), - json!({"role": "user", "content": [{ - "type": "tool_result", "tool_use_id": "tool-1", "content": "fresh" - }]}), - ]; - let manifest = ResumeTransportManifest::create( - "session", - vec!["tool-1".into()], - canonical, - "__PVISOR_NATIVE_REPLAY_0123456789abcdef__".into(), - ) - .unwrap(); - let mut state = ResumeState { - manifest, - request_sequence: 0, - forwarded_requests: 0, - pending_forward_sequence: None, - failed: false, - failure: None, - }; - assert!(state.clean(&json!({"messages": []})).is_err()); - assert!(state.failed); - assert!( - state - .clean(&json!({"messages": []})) - .unwrap_err() - .to_string() - .contains("FAILED") - ); - assert_eq!(state.forwarded_requests, 0); - } - #[test] - fn empty_openai_tool_arguments_mean_an_empty_object_but_non_text_is_rejected() { - let payload = json!({"model": "claude"}); - let empty = json!({ - "choices": [{"message": {"tool_calls": [{ - "id": "call-1", - "function": {"name": "TaskList", "arguments": ""} - }]}}] - }); - let converted = anthropic_response(&payload, &empty, "qwen").unwrap(); - assert_eq!(converted["content"][0]["input"], json!({})); - - let non_text = json!({ - "choices": [{"message": {"tool_calls": [{ - "id": "call-1", - "function": {"name": "TaskList", "arguments": {}} - }]}}] - }); - assert!( - anthropic_response(&payload, &non_text, "qwen") - .unwrap_err() - .to_string() - .contains("must be JSON text") - ); - } - fn test_manifest() -> ResumeTransportManifest { - let canonical = vec![ - json!({"role": "assistant", "content": [{ - "type": "tool_use", "id": "tool-1", "name": "Bash", "input": {} - }]}), - json!({"role": "user", "content": [{ - "type": "tool_result", "tool_use_id": "tool-1", "content": "fresh" - }]}), - ]; - ResumeTransportManifest::create( - "session", - vec!["tool-1".into()], - canonical, - "__PVISOR_NATIVE_REPLAY_0123456789abcdef__".into(), - ) - .unwrap() - } - - fn test_shared(upstream_url: String, retry_delays: Vec) -> Arc { - Arc::new(BridgeShared { - resume: Mutex::new(ResumeState { - manifest: test_manifest(), - request_sequence: 0, - forwarded_requests: 0, - pending_forward_sequence: None, - failed: false, - failure: None, - }), - client: reqwest::Client::builder().no_proxy().build().unwrap(), - upstream_url, - upstream_api_key: "upstream-secret".into(), - model_name: "qwen".into(), - routing_session_id: "trial-session".into(), - bridge_api_key: "local-key".into(), - disable_thinking: false, - boundary_user_prompt: None, - max_output_tokens: 8192, - model_context_tokens: 200_000, - context_safety_tokens: 1024, - upstream_timeout: Duration::from_secs(60), - retry_delays, - cancelled: AtomicBool::new(false), - cancel_notify: Notify::new(), - }) - } - - #[test] - fn bridge_version_matches_python_contract_exactly() { - assert_eq!(BRIDGE_VERSION, "sandbox-replay-anthropic-openai-bridge/1"); - } - - #[test] - fn resume_state_preserves_the_first_failure_for_pending_concurrency() { - let mut state = ResumeState { - manifest: test_manifest(), - request_sequence: 1, - forwarded_requests: 0, - pending_forward_sequence: Some(1), - failed: false, - failure: None, - }; - let error = state.clean(&json!({"messages": []})).unwrap_err(); - assert!(error.to_string().contains("previous validated request")); - assert!(state.failed); - assert_eq!( - state.failure.as_deref(), - Some("another request arrived before the previous request was forwarded") - ); - state.fail("later failure must not hide the root cause"); - assert_eq!( - state.failure.as_deref(), - Some("another request arrived before the previous request was forwarded") - ); - } - - #[test] - fn invalid_present_sampling_values_are_rejected() { - let mut payload = json!({"messages": []}); - payload["max_tokens"] = json!("not-an-integer"); - assert!( - openai_request(&payload, "qwen", 8192, 200_000, 1024, false) - .unwrap_err() - .to_string() - .contains("max_tokens must be an integer") - ); - - payload = json!({"messages": [], "max_tokens": -1}); - assert!( - openai_request(&payload, "qwen", 8192, 200_000, 1024, false) - .unwrap_err() - .to_string() - .contains("max_tokens must be positive") - ); - - payload = json!({"messages": [], "temperature": {"bad": true}}); - assert!( - openai_request(&payload, "qwen", 8192, 200_000, 1024, false) - .unwrap_err() - .to_string() - .contains("temperature must be a number") - ); - - payload = json!({"messages": [], "max_tokens": "32", "temperature": "0.25"}); - let converted = openai_request(&payload, "qwen", 8192, 200_000, 1024, false).unwrap(); - assert_eq!(converted["max_tokens"], 32); - assert_eq!(converted["temperature"], 0.25); - } - - #[test] - fn scalar_tool_result_text_matches_python_truthiness() { - assert_eq!(scalar_text(&Value::Null), ""); - assert_eq!(scalar_text(&json!(false)), ""); - assert_eq!(scalar_text(&json!(0)), ""); - assert_eq!(scalar_text(&json!([])), ""); - assert_eq!(scalar_text(&json!({})), ""); - assert_eq!(scalar_text(&json!(true)), "True"); - assert_eq!(scalar_text(&json!(["value"])), "[\"value\"]"); - } - - #[test] - fn no_proxy_merge_preserves_values_and_guarantees_loopback() { - let merged = - merge_no_proxy_values(["example.internal, localhost", "10.0.0.0/8,example.internal"]); - assert_eq!( - merged, - "example.internal,localhost,10.0.0.0/8,127.0.0.1,::1" - ); - } - - #[test] - fn active_upstream_send_is_cancelled_promptly() { - let runtime = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap(); - runtime.block_on(async { - async fn hang(State(started): State>) -> Response { - started.notify_one(); - std::future::pending::().await - } - - let started = Arc::new(Notify::new()); - let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0)) - .await - .unwrap(); - let address = listener.local_addr().unwrap(); - let server_started = Arc::clone(&started); - let server = tokio::spawn(async move { - axum::serve( - listener, - Router::new() - .route("/v1/chat/completions", post(hang)) - .with_state(server_started), - ) - .await - .unwrap(); - }); - let shared = test_shared(format!("http://{address}/v1/chat/completions"), Vec::new()); - let request_shared = Arc::clone(&shared); - let request = tokio::spawn(async move { - forward_openai_request(&request_shared, b"{}".to_vec()).await - }); - tokio::time::timeout(Duration::from_secs(2), started.notified()) - .await - .unwrap(); - shared.cancelled.store(true, Ordering::Release); - shared.cancel_notify.notify_waiters(); - let error = tokio::time::timeout(Duration::from_secs(2), request) - .await - .expect("cancelled request should return promptly") - .unwrap() - .unwrap_err(); - assert!(error.to_string().contains("cancelled during shutdown")); - server.abort(); - }); - } - - #[test] - fn retry_sleep_is_cancelled_promptly() { - let runtime = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap(); - runtime.block_on(async { - async fn unavailable(State(started): State>) -> Response { - started.notify_one(); - upstream_error(StatusCode::SERVICE_UNAVAILABLE, "retry".into()) - } - - let started = Arc::new(Notify::new()); - let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0)) - .await - .unwrap(); - let address = listener.local_addr().unwrap(); - let server_started = Arc::clone(&started); - let server = tokio::spawn(async move { - axum::serve( - listener, - Router::new() - .route("/v1/chat/completions", post(unavailable)) - .with_state(server_started), - ) - .await - .unwrap(); - }); - let shared = test_shared( - format!("http://{address}/v1/chat/completions"), - vec![Duration::from_secs(60)], - ); - let request_shared = Arc::clone(&shared); - let request = tokio::spawn(async move { - forward_openai_request(&request_shared, b"{}".to_vec()).await - }); - tokio::time::timeout(Duration::from_secs(2), started.notified()) - .await - .unwrap(); - tokio::time::sleep(Duration::from_millis(20)).await; - shared.cancelled.store(true, Ordering::Release); - shared.cancel_notify.notify_waiters(); - let error = tokio::time::timeout(Duration::from_secs(2), request) - .await - .expect("retry sleep should stop promptly") - .unwrap() - .unwrap_err(); - assert!(error.to_string().contains("cancelled during shutdown")); - server.abort(); - }); - } -} diff --git a/crates/persisting-replay/src/claude_resume.rs b/crates/persisting-replay/src/claude_resume.rs deleted file mode 100644 index 5f6f69f72..000000000 --- a/crates/persisting-replay/src/claude_resume.rs +++ /dev/null @@ -1,949 +0,0 @@ -//! Versioned, fail-closed Claude Code resume-transport cleanup. -//! -//! Claude Code needs a local wake-up input when `--resume --print` starts from -//! a native session ending in `tool_result`. Claude Code 2.1.220 wraps that -//! input in a deterministic three-message envelope. This module validates the -//! complete envelope and the reconstructed native prefix before removing the -//! transport-only messages. It deliberately performs no network I/O and writes -//! no audit or trajectory files. - -use std::collections::BTreeSet; - -use anyhow::{Context, Result, ensure}; -use serde::{Deserialize, Serialize}; -use serde_json::{Map, Value, json}; -use sha2::{Digest, Sha256}; - -pub const TRANSPORT_SCHEMA_VERSION: &str = "sandbox-playback.claude-resume-transport/v1"; -pub const PROFILE_ID: &str = "claude-code/2.1.220/native-resume-v1"; -pub const CLAUDE_CODE_VERSION: &str = "2.1.220"; -pub const CONTINUE_TEXT: &str = "Continue from where you left off."; -pub const NO_RESPONSE_TEXT: &str = "No response requested."; - -const SKILLS_SYSTEM_PREFIX: &str = - "The following skills are available for use with the Skill tool:\n\n- "; -const AGENT_TYPES_SYSTEM_PREFIX: &str = "Available agent types for the Agent tool:\n"; -const TASK_TOOL_REMINDER: &str = "The task tools haven't been used recently. If you're working on tasks that would benefit from tracking progress, consider using TaskCreate to add new tasks and TaskUpdate to update task status (set to in_progress when starting, completed when done). Also consider cleaning up the task list if it has become stale. Only use these if relevant to the current work. This is just a gentle reminder - ignore if not applicable."; -const TASKS_SUFFIX_PREFIX: &str = "\n\n\nHere are the existing tasks:\n\n"; -const CURRENT_DATE_PREFIX: &str = "\nAs you answer the user's questions, you can use the following context:\n# currentDate\nToday's date is "; -const CURRENT_DATE_SUFFIX: &str = ".\n\n IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task.\n"; - -/// Run-scoped integrity contract shared by session reconstruction and the -/// local Claude bridge. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ResumeTransportManifest { - pub schema_version: String, - pub profile_id: String, - pub claude_code_version: String, - pub nonce: String, - pub session_id: String, - pub boundary_tool_use_ids: Vec, - pub boundary_observation_sha256: Vec, - pub canonical_message_count: usize, - pub canonical_prefix_sha256: String, - #[serde(default)] - pub canonical_message_sha256: Vec, -} - -impl ResumeTransportManifest { - pub fn create( - session_id: &str, - boundary_tool_use_ids: Vec, - canonical_messages: Vec, - nonce: String, - ) -> Result { - ensure!( - canonical_messages.len() >= 2, - "canonical messages must end in assistant tool_use and user tool_result" - ); - ensure!( - canonical_messages.iter().all(Value::is_object), - "canonical messages must be JSON objects" - ); - let assistant_ids = tool_use_ids( - &canonical_messages[canonical_messages.len() - 2], - "canonical boundary assistant", - )?; - let boundary_message = canonical_messages.last().expect("length checked"); - ensure!( - boundary_message.get("role").and_then(Value::as_str) == Some("user"), - "canonical boundary result must have role=user" - ); - let content = boundary_message - .get("content") - .and_then(Value::as_array) - .context("canonical boundary result must contain only tool_result blocks")?; - ensure!( - !content.is_empty() - && content.iter().all(|block| { - block.is_object() - && block.get("type").and_then(Value::as_str) == Some("tool_result") - }), - "canonical boundary result must contain only tool_result blocks" - ); - let result_ids = content - .iter() - .map(|block| required_string(block.get("tool_use_id"), "boundary tool_use_id")) - .collect::>>()?; - ensure!( - assistant_ids == boundary_tool_use_ids && result_ids == boundary_tool_use_ids, - "canonical boundary tool IDs do not match boundary_tool_use_ids" - ); - - let manifest = Self { - schema_version: TRANSPORT_SCHEMA_VERSION.to_owned(), - profile_id: PROFILE_ID.to_owned(), - claude_code_version: CLAUDE_CODE_VERSION.to_owned(), - nonce, - session_id: session_id.to_owned(), - boundary_tool_use_ids, - boundary_observation_sha256: content - .iter() - .map(canonical_observation_sha256) - .collect::>>()?, - canonical_message_count: canonical_messages.len(), - canonical_prefix_sha256: canonical_messages_sha256(&canonical_messages)?, - canonical_message_sha256: canonical_messages - .iter() - .map(|message| canonical_messages_sha256(std::slice::from_ref(message))) - .collect::>>()?, - }; - manifest.validate()?; - Ok(manifest) - } - - pub fn validate(&self) -> Result<()> { - ensure!( - self.schema_version == TRANSPORT_SCHEMA_VERSION, - "unsupported transport schema {:?}", - self.schema_version - ); - ensure!( - self.profile_id == PROFILE_ID, - "unknown Claude resume transport profile {:?}", - self.profile_id - ); - ensure!( - self.claude_code_version == CLAUDE_CODE_VERSION, - "profile {:?} requires Claude Code {}, got {}", - self.profile_id, - CLAUDE_CODE_VERSION, - self.claude_code_version - ); - ensure!(!self.session_id.is_empty(), "session_id must not be empty"); - ensure!( - self.nonce.chars().count() >= 16, - "nonce must contain at least 16 characters" - ); - ensure!( - !self.boundary_tool_use_ids.is_empty(), - "boundary_tool_use_ids must not be empty" - ); - ensure!( - self.boundary_tool_use_ids - .iter() - .all(|call_id| !call_id.is_empty()), - "boundary_tool_use_ids must contain non-empty strings" - ); - ensure!( - all_unique(&self.boundary_tool_use_ids), - "boundary_tool_use_ids must be unique" - ); - ensure!( - self.boundary_observation_sha256.len() == self.boundary_tool_use_ids.len() - && self - .boundary_observation_sha256 - .iter() - .all(|digest| valid_sha256(digest)), - "boundary_observation_sha256 must align with boundary_tool_use_ids" - ); - ensure!( - self.canonical_message_count >= 2, - "canonical_message_count must be at least two" - ); - ensure!( - valid_sha256(&self.canonical_prefix_sha256), - "canonical_prefix_sha256 must be a lowercase SHA-256 hex digest" - ); - ensure!( - self.canonical_message_sha256.is_empty() - || (self.canonical_message_sha256.len() == self.canonical_message_count - && self - .canonical_message_sha256 - .iter() - .all(|digest| valid_sha256(digest))), - "canonical_message_sha256 must align with canonical_message_count" - ); - Ok(()) - } -} - -/// Non-secret validation facts returned to the in-memory bridge state machine. -/// The caller may use these for fail-closed checks; this module never persists -/// them. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ResumeTransportValidation { - pub request_sequence: usize, - pub profile_id: String, - pub nonce_message_index: usize, - pub sentinel_removed: usize, - pub closure_messages_removed: usize, - pub boundary_tool_use_ids: Vec, - pub boundary_observation_sha256: Vec, - pub clean_message_count: usize, - pub clean_prefix_sha256: String, - pub first_request_boundary_ok: bool, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct CleanedResumeRequest { - pub payload: Value, - pub validation: ResumeTransportValidation, -} - -pub fn canonical_messages_sha256(messages: &[Value]) -> Result { - ensure!( - messages.iter().all(Value::is_object), - "messages must be a list of JSON objects" - ); - Ok(sha256_hex( - canonical_json(&Value::Array(messages.to_vec())).as_bytes(), - )) -} - -pub fn canonical_observation_sha256(tool_result_block: &Value) -> Result { - ensure!( - tool_result_block.get("type").and_then(Value::as_str) == Some("tool_result"), - "observation hash input must be a tool_result block" - ); - let value = json!({ - "content": tool_result_block.get("content").cloned().unwrap_or(Value::Null), - "is_error": tool_result_block - .get("is_error") - .and_then(Value::as_bool) - .unwrap_or(false), - }); - Ok(sha256_hex(canonical_json(&value).as_bytes())) -} - -/// Validate and remove exactly one Claude Code resume envelope. -/// -/// The first model request discards every message after the nonce because all -/// such messages are resume-time additions. Later requests retain their suffix, -/// which contains the newly generated continuation. -pub fn clean_resume_transport_envelope( - payload: &Value, - manifest: &ResumeTransportManifest, - request_sequence: usize, -) -> Result { - ensure!( - request_sequence >= 1, - "request_sequence must be a positive integer" - ); - manifest.validate()?; - let payload_object = payload - .as_object() - .context("resume payload must be a JSON object")?; - let raw_messages = payload_object - .get("messages") - .and_then(Value::as_array) - .context("payload.messages must be a list of JSON objects")?; - ensure!( - raw_messages.iter().all(Value::is_object), - "payload.messages must be a list of JSON objects" - ); - let messages = raw_messages.clone(); - - let mut nonce_indexes = Vec::new(); - for (index, message) in messages.iter().enumerate() { - if message.get("role").and_then(Value::as_str) == Some("user") - && sole_text(message, &format!("messages[{index}]"))? == Some(manifest.nonce.as_str()) - { - nonce_indexes.push(index); - } - } - ensure!( - nonce_indexes.len() == 1, - "expected exactly one sole-content resume nonce message, found {}", - nonce_indexes.len() - ); - let nonce_index = nonce_indexes[0]; - ensure!( - nonce_index >= 3, - "resume nonce has no complete preceding envelope" - ); - - let assistant_closure_index = nonce_index - 1; - let boundary_result_index = nonce_index - 2; - let boundary_assistant_index = nonce_index - 3; - let assistant_closure = &messages[assistant_closure_index]; - ensure!( - assistant_closure.get("role").and_then(Value::as_str) == Some("assistant") - && sole_text( - assistant_closure, - &format!("messages[{assistant_closure_index}]") - )? == Some(NO_RESPONSE_TEXT), - "resume nonce is not preceded by the exact assistant closure" - ); - - let (result_ids, clean_result_blocks) = - boundary_result_and_closure(&messages[boundary_result_index])?; - let assistant = &messages[boundary_assistant_index]; - ensure!( - assistant.get("role").and_then(Value::as_str) == Some("assistant"), - "boundary result envelope is not preceded by an assistant message" - ); - let assistant_ids = tool_use_ids(assistant, &format!("messages[{boundary_assistant_index}]"))?; - ensure!( - assistant_ids == manifest.boundary_tool_use_ids, - "boundary assistant tool_use IDs do not match manifest" - ); - ensure!( - result_ids == manifest.boundary_tool_use_ids, - "boundary tool_result IDs do not match manifest" - ); - let observation_hashes = clean_result_blocks - .iter() - .map(canonical_observation_sha256) - .collect::>>()?; - ensure!( - observation_hashes == manifest.boundary_observation_sha256, - "boundary O' observation hash does not match resume manifest" - ); - - let continuation_messages = if request_sequence == 1 { - Vec::new() - } else { - messages[nonce_index + 1..].to_vec() - }; - let mut cleaned_boundary = messages[boundary_result_index].clone(); - cleaned_boundary["content"] = Value::Array(clean_result_blocks); - let mut cleaned_messages = messages[..boundary_result_index].to_vec(); - cleaned_messages.push(cleaned_boundary); - cleaned_messages.extend(continuation_messages); - - ensure!( - !contains_exact_transport_text(&cleaned_messages), - "resume transport text remains after structured cleanup" - ); - let canonical_messages = canonical_cli_message_projection(&cleaned_messages); - let count = manifest.canonical_message_count; - ensure!( - canonical_messages.len() >= count, - "clean request has {} canonical messages, fewer than canonical prefix length {}", - canonical_messages.len(), - count - ); - let clean_prefix = &canonical_messages[..count]; - let prefix_digest = canonical_messages_sha256(clean_prefix)?; - ensure!( - prefix_digest == manifest.canonical_prefix_sha256, - "clean request canonical prefix hash does not match resume manifest" - ); - let first_request_ok = canonical_messages.len() == count; - if request_sequence == 1 { - ensure!( - first_request_ok, - "first resumed model request contains messages after the canonical boundary" - ); - } - - let mut cleaned_payload = payload.clone(); - cleaned_payload["messages"] = Value::Array(cleaned_messages); - ensure!( - !contains_nonce(&cleaned_payload, &manifest.nonce), - "resume nonce remains anywhere in the cleaned model payload" - ); - - Ok(CleanedResumeRequest { - payload: cleaned_payload, - validation: ResumeTransportValidation { - request_sequence, - profile_id: manifest.profile_id.clone(), - nonce_message_index: nonce_index, - sentinel_removed: 1, - closure_messages_removed: 2, - boundary_tool_use_ids: manifest.boundary_tool_use_ids.clone(), - boundary_observation_sha256: observation_hashes, - clean_message_count: canonical_messages.len(), - clean_prefix_sha256: prefix_digest, - first_request_boundary_ok: first_request_ok, - }, - }) -} - -fn required_string(value: Option<&Value>, context: &str) -> Result { - value - .and_then(Value::as_str) - .filter(|value| !value.is_empty()) - .map(str::to_owned) - .with_context(|| format!("{context} must be a non-empty string")) -} - -fn sole_text<'a>(message: &'a Value, context: &str) -> Result> { - let content = message - .get("content") - .with_context(|| format!("{context} has no content"))?; - if let Some(text) = content.as_str() { - return Ok(Some(text)); - } - let Some(blocks) = content.as_array() else { - return Ok(None); - }; - if blocks.len() == 1 - && blocks[0].get("type").and_then(Value::as_str) == Some("text") - && blocks[0].get("text").and_then(Value::as_str).is_some() - { - return Ok(blocks[0].get("text").and_then(Value::as_str)); - } - Ok(None) -} - -fn tool_use_ids(message: &Value, context: &str) -> Result> { - let content = message - .get("content") - .and_then(Value::as_array) - .with_context(|| format!("{context} content must be a block list"))?; - ensure!( - content.iter().all(Value::is_object), - "{context} contains a non-object content block" - ); - let ids = content - .iter() - .filter(|block| block.get("type").and_then(Value::as_str) == Some("tool_use")) - .map(|block| required_string(block.get("id"), &format!("{context} tool_use ID"))) - .collect::>>()?; - ensure!( - all_unique(&ids), - "{context} contains duplicate tool_use IDs" - ); - Ok(ids) -} - -fn boundary_result_and_closure(message: &Value) -> Result<(Vec, Vec)> { - ensure!( - message.get("role").and_then(Value::as_str) == Some("user"), - "boundary result envelope message must have role=user" - ); - let content = message - .get("content") - .and_then(Value::as_array) - .context("boundary result envelope must contain tool_result block(s) and closure text")?; - ensure!( - content.len() >= 2, - "boundary result envelope must contain tool_result block(s) and closure text" - ); - ensure!( - content.iter().all(Value::is_object), - "boundary result envelope contains a non-object block" - ); - let closure = content.last().expect("length checked"); - ensure!( - closure.get("type").and_then(Value::as_str) == Some("text") - && closure.get("text").and_then(Value::as_str) == Some(CONTINUE_TEXT), - "boundary result envelope does not end with the exact continue closure" - ); - let result_blocks = content[..content.len() - 1].to_vec(); - ensure!( - result_blocks - .iter() - .all(|block| { block.get("type").and_then(Value::as_str) == Some("tool_result") }), - "boundary result envelope may contain only tool_result block(s) before closure" - ); - let ids = result_blocks - .iter() - .map(|block| required_string(block.get("tool_use_id"), "boundary tool_result ID")) - .collect::>>()?; - ensure!( - all_unique(&ids), - "boundary envelope has duplicate tool_result IDs" - ); - Ok((ids, result_blocks)) -} - -fn contains_exact_transport_text(messages: &[Value]) -> bool { - messages.iter().any(|message| { - let Some(content) = message.get("content") else { - return false; - }; - if let Some(text) = content.as_str() { - return is_transport_text(text); - } - content.as_array().is_some_and(|blocks| { - blocks.iter().any(|block| { - block.get("type").and_then(Value::as_str) == Some("text") - && block - .get("text") - .and_then(Value::as_str) - .is_some_and(is_transport_text) - }) - }) - }) -} - -fn is_transport_text(text: &str) -> bool { - text == CONTINUE_TEXT || text == NO_RESPONSE_TEXT -} - -/// Scan every string in the model-bound payload, including object keys. This -/// keeps newly introduced extension fields from becoming nonce-leak bypasses. -fn contains_nonce(value: &Value, nonce: &str) -> bool { - match value { - Value::String(text) => text.contains(nonce), - Value::Array(values) => values.iter().any(|value| contains_nonce(value, nonce)), - Value::Object(values) => values - .iter() - .any(|(key, value)| key.contains(nonce) || contains_nonce(value, nonce)), - _ => false, - } -} - -fn canonical_cli_message_projection(messages: &[Value]) -> Vec { - let mut projected = messages - .iter() - .filter(|message| !runtime_only_system_message(message)) - .cloned() - .collect::>(); - let Some(first) = projected.first_mut() else { - return projected; - }; - if first.get("role").and_then(Value::as_str) != Some("user") { - return projected; - } - let Some(content) = first.get("content").and_then(Value::as_array) else { - return projected; - }; - if !content.iter().all(Value::is_object) { - return projected; - } - let kept = content - .iter() - .filter(|block| { - !(block.get("type").and_then(Value::as_str) == Some("text") - && block - .get("text") - .and_then(Value::as_str) - .is_some_and(is_current_date_reminder)) - }) - .cloned() - .collect::>(); - if kept.len() == 1 - && kept[0].get("type").and_then(Value::as_str) == Some("text") - && kept[0].get("text").and_then(Value::as_str).is_some() - { - first["content"] = kept[0]["text"].clone(); - } else { - first["content"] = Value::Array(kept); - } - projected -} - -fn runtime_only_system_message(message: &Value) -> bool { - if message.get("role").and_then(Value::as_str) != Some("system") { - return false; - } - let Some(content) = message.get("content").and_then(Value::as_str) else { - return false; - }; - content.starts_with(SKILLS_SYSTEM_PREFIX) - || content.starts_with(AGENT_TYPES_SYSTEM_PREFIX) - || content.trim_end_matches('\n') == TASK_TOOL_REMINDER - || is_task_reminder_with_tasks(content) -} - -fn is_task_reminder_with_tasks(content: &str) -> bool { - let Some(tasks) = content.strip_prefix(TASK_TOOL_REMINDER) else { - return false; - }; - let Some(tasks) = tasks.strip_prefix(TASKS_SUFFIX_PREFIX) else { - return false; - }; - !tasks.is_empty() && tasks.split('\n').all(valid_task_summary_line) -} - -fn valid_task_summary_line(line: &str) -> bool { - let Some(after_hash) = line.strip_prefix('#') else { - return false; - }; - let digit_count = after_hash.bytes().take_while(u8::is_ascii_digit).count(); - if digit_count == 0 { - return false; - } - let Some(summary) = after_hash[digit_count..].strip_prefix(". [") else { - return false; - }; - let Some((status, title)) = summary.split_once("] ") else { - return false; - }; - !status.is_empty() && !status.contains(']') && !title.is_empty() -} - -fn is_current_date_reminder(text: &str) -> bool { - let trimmed = text.trim(); - let Some(rest) = trimmed.strip_prefix(CURRENT_DATE_PREFIX) else { - return false; - }; - let Some(date) = rest.strip_suffix(CURRENT_DATE_SUFFIX) else { - return false; - }; - let bytes = date.as_bytes(); - bytes.len() == 10 - && bytes[4] == b'-' - && bytes[7] == b'-' - && bytes - .iter() - .enumerate() - .all(|(index, byte)| matches!(index, 4 | 7) || byte.is_ascii_digit()) -} - -fn all_unique(values: &[String]) -> bool { - values.iter().collect::>().len() == values.len() -} - -fn valid_sha256(value: &str) -> bool { - value.len() == 64 - && value - .bytes() - .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) -} - -fn sha256_hex(value: &[u8]) -> String { - Sha256::digest(value) - .iter() - .map(|byte| format!("{byte:02x}")) - .collect() -} - -/// Render JSON with recursively sorted object keys, compact separators and -/// UTF-8 text, matching Python's `json.dumps(sort_keys=True,separators=(",",":"))`. -fn canonical_json(value: &Value) -> String { - match value { - Value::Null => "null".to_owned(), - Value::Bool(value) => value.to_string(), - Value::Number(value) => value.to_string(), - Value::String(value) => serde_json::to_string(value).expect("JSON string serialization"), - Value::Array(values) => format!( - "[{}]", - values - .iter() - .map(canonical_json) - .collect::>() - .join(",") - ), - Value::Object(values) => canonical_object(values), - } -} - -fn canonical_object(values: &Map) -> String { - let mut keys = values.keys().collect::>(); - keys.sort_unstable(); - let entries = keys - .into_iter() - .map(|key| { - format!( - "{}:{}", - serde_json::to_string(key).expect("JSON key serialization"), - canonical_json(&values[key]) - ) - }) - .collect::>(); - format!("{{{}}}", entries.join(",")) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn canonical_messages() -> Vec { - vec![ - json!({"role": "user", "content": "fix it"}), - json!({ - "role": "assistant", - "content": [ - {"type": "text", "text": "inspect"}, - { - "type": "tool_use", - "id": "call-1", - "name": "Read", - "input": {"file_path": "/app/a.py"}, - }, - { - "type": "tool_use", - "id": "call-2", - "name": "Grep", - "input": {"pattern": "bug"}, - }, - ], - }), - json!({ - "role": "user", - "content": [ - {"type": "tool_result", "tool_use_id": "call-1", "content": "new read"}, - {"type": "tool_result", "tool_use_id": "call-2", "content": "new grep"}, - ], - }), - ] - } - - fn manifest_and_payload() -> (ResumeTransportManifest, Value) { - let canonical = canonical_messages(); - let manifest = ResumeTransportManifest::create( - "native-session", - vec!["call-1".into(), "call-2".into()], - canonical.clone(), - "__SANDBOX_PLAYBACK_TEST_NONCE_0123456789__".into(), - ) - .unwrap(); - let mut messages = canonical; - messages.last_mut().unwrap()["content"] - .as_array_mut() - .unwrap() - .push(json!({"type": "text", "text": CONTINUE_TEXT})); - messages.extend([ - json!({"role": "assistant", "content": NO_RESPONSE_TEXT}), - json!({"role": "user", "content": manifest.nonce}), - ]); - ( - manifest, - json!({"model": "test", "messages": messages, "stream": true}), - ) - } - - #[test] - fn manifest_and_first_request_cleanup_match_python_contract() { - let (manifest, payload) = manifest_and_payload(); - let original = payload.clone(); - assert_eq!( - manifest.canonical_prefix_sha256, - "34fbd2a13b5788fbe6f11abaf47bf664316f75f28ae383a4637f5c42f9792837" - ); - assert_eq!( - manifest.boundary_observation_sha256[0], - "581c12494899d692e1b8456047397b13c4140bfbed9e522b6fdb02ef325fa954" - ); - let cleaned = clean_resume_transport_envelope(&payload, &manifest, 1).unwrap(); - assert_eq!(payload, original); - assert_eq!( - cleaned.payload["messages"], - Value::Array(canonical_messages()) - ); - assert_eq!(cleaned.validation.sentinel_removed, 1); - assert_eq!(cleaned.validation.closure_messages_removed, 2); - assert_eq!( - cleaned.validation.boundary_tool_use_ids, - ["call-1", "call-2"] - ); - assert_eq!( - cleaned.validation.boundary_observation_sha256, - manifest.boundary_observation_sha256 - ); - assert!(cleaned.validation.first_request_boundary_ok); - } - - #[test] - fn later_request_keeps_real_continuation_suffix() { - let (manifest, mut payload) = manifest_and_payload(); - payload["messages"].as_array_mut().unwrap().extend([ - json!({ - "role": "assistant", - "content": [{"type": "tool_use", "id": "call-3", "name": "Edit", "input": {}}], - }), - json!({ - "role": "user", - "content": [{"type": "tool_result", "tool_use_id": "call-3", "content": "done"}], - }), - ]); - let cleaned = clean_resume_transport_envelope(&payload, &manifest, 2).unwrap(); - assert_eq!( - &cleaned.payload["messages"].as_array().unwrap()[..3], - canonical_messages().as_slice() - ); - assert_eq!(cleaned.payload["messages"].as_array().unwrap().len(), 5); - assert!(!cleaned.validation.first_request_boundary_ok); - } - - #[test] - fn first_request_drops_every_suffix_after_nonce() { - let (manifest, mut payload) = manifest_and_payload(); - payload["messages"] - .as_array_mut() - .unwrap() - .push(json!({"role": "user", "content": "unexpected suffix"})); - let cleaned = clean_resume_transport_envelope(&payload, &manifest, 1).unwrap(); - assert_eq!( - cleaned.payload["messages"], - Value::Array(canonical_messages()) - ); - } - - #[test] - fn audited_cli_reminders_are_projected_but_preserved_for_forwarding() { - let (manifest, mut payload) = manifest_and_payload(); - let current_date = format!("{CURRENT_DATE_PREFIX}2026-07-31{CURRENT_DATE_SUFFIX}"); - payload["messages"][0]["content"] = json!([ - {"type": "text", "text": current_date}, - {"type": "text", "text": "fix it"}, - ]); - payload["messages"].as_array_mut().unwrap().insert( - 1, - json!({ - "role": "system", - "content": format!("{SKILLS_SYSTEM_PREFIX}example: test skill"), - }), - ); - payload["messages"].as_array_mut().unwrap().insert( - 2, - json!({ - "role": "system", - "content": format!("{AGENT_TYPES_SYSTEM_PREFIX}- Explore: read-only"), - }), - ); - let envelope_start = payload["messages"].as_array().unwrap().len() - 4; - payload["messages"].as_array_mut().unwrap().insert( - envelope_start, - json!({"role": "system", "content": TASK_TOOL_REMINDER}), - ); - let cleaned = clean_resume_transport_envelope(&payload, &manifest, 1).unwrap(); - assert_eq!( - cleaned.validation.clean_message_count, - canonical_messages().len() - ); - assert_eq!( - cleaned.validation.clean_prefix_sha256, - manifest.canonical_prefix_sha256 - ); - assert!( - cleaned.payload["messages"] - .as_array() - .unwrap() - .iter() - .any(|message| message["role"] == "system") - ); - } - - #[test] - fn reminder_with_existing_tasks_is_projected() { - let (manifest, mut payload) = manifest_and_payload(); - let envelope_start = payload["messages"].as_array().unwrap().len() - 4; - payload["messages"].as_array_mut().unwrap().insert( - envelope_start, - json!({ - "role": "system", - "content": format!( - "{TASK_TOOL_REMINDER}{TASKS_SUFFIX_PREFIX}#1. [in_progress] Inspect code\n#2. [pending] Fix it" - ), - }), - ); - let cleaned = clean_resume_transport_envelope(&payload, &manifest, 1).unwrap(); - assert_eq!( - cleaned.validation.clean_message_count, - canonical_messages().len() - ); - } - - #[test] - fn unknown_system_message_fails_prefix_integrity() { - let (manifest, mut payload) = manifest_and_payload(); - payload["messages"] - .as_array_mut() - .unwrap() - .insert(1, json!({"role": "system", "content": "unknown injection"})); - let error = clean_resume_transport_envelope(&payload, &manifest, 1).unwrap_err(); - assert!(error.to_string().contains("canonical prefix hash")); - } - - #[test] - fn nonce_in_any_payload_value_or_key_is_rejected() { - let (manifest, mut payload) = manifest_and_payload(); - payload["system"] = json!([{ - "type": "text", - "text": format!("echo {}", manifest.nonce), - }]); - assert!( - clean_resume_transport_envelope(&payload, &manifest, 1) - .unwrap_err() - .to_string() - .contains("nonce remains anywhere") - ); - - let (manifest, mut payload) = manifest_and_payload(); - payload - .as_object_mut() - .unwrap() - .insert(format!("extension-{}", manifest.nonce), Value::Bool(true)); - assert!( - clean_resume_transport_envelope(&payload, &manifest, 1) - .unwrap_err() - .to_string() - .contains("nonce remains anywhere") - ); - } - - #[test] - fn stale_observation_and_malformed_envelope_fail_closed() { - let (manifest, mut stale) = manifest_and_payload(); - let last = stale["messages"].as_array().unwrap().len(); - stale["messages"][last - 3]["content"][0]["content"] = json!("stale"); - assert!( - clean_resume_transport_envelope(&stale, &manifest, 1) - .unwrap_err() - .to_string() - .contains("observation hash") - ); - - let (manifest, mut closure) = manifest_and_payload(); - let last = closure["messages"].as_array().unwrap().len(); - closure["messages"][last - 2]["content"] = json!("changed"); - assert!( - clean_resume_transport_envelope(&closure, &manifest, 1) - .unwrap_err() - .to_string() - .contains("assistant closure") - ); - - let (manifest, mut duplicate) = manifest_and_payload(); - duplicate["messages"] - .as_array_mut() - .unwrap() - .push(json!({"role": "user", "content": manifest.nonce})); - assert!( - clean_resume_transport_envelope(&duplicate, &manifest, 1) - .unwrap_err() - .to_string() - .contains("exactly one") - ); - } - - #[test] - fn canonical_hash_sorts_nested_object_keys() { - let left = vec![json!({"role":"user", "content":{"b":2,"a":{"d":4,"c":3}}})]; - let right = vec![json!({"content":{"a":{"c":3,"d":4},"b":2}, "role":"user"})]; - assert_eq!( - canonical_messages_sha256(&left).unwrap(), - canonical_messages_sha256(&right).unwrap() - ); - } - - #[test] - fn manifest_rejects_profile_or_version_drift_after_deserialization() { - let (manifest, _) = manifest_and_payload(); - let mut wrong_profile = manifest.clone(); - wrong_profile.profile_id = "claude-code/unknown/native-resume-v1".into(); - assert!( - wrong_profile - .validate() - .unwrap_err() - .to_string() - .contains("unknown") - ); - let mut wrong_version = manifest; - wrong_version.claude_code_version = "2.1.71".into(); - assert!( - wrong_version - .validate() - .unwrap_err() - .to_string() - .contains("requires") - ); - } -} diff --git a/crates/persisting-replay/src/codex_bridge.rs b/crates/persisting-replay/src/codex_bridge.rs deleted file mode 100644 index 4a171347b..000000000 --- a/crates/persisting-replay/src/codex_bridge.rs +++ /dev/null @@ -1,761 +0,0 @@ -//! Codex Responses API resume-transport bridge. -//! -//! Older Codex releases require a prompt argument for `exec resume`. The -//! prompt is useful as a CLI wake-up signal, but it must not become part of -//! the model input for an unmodified replay. This bridge removes the unique -//! nonce from every request before forwarding it upstream; Codex may resend -//! the full conversation history on subsequent requests. - -use std::collections::BTreeMap; -use std::net::TcpListener; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex, mpsc}; -use std::thread::{self, JoinHandle}; -use std::time::Duration; - -use anyhow::Context; -use axum::Router; -use axum::body::{Body, Bytes}; -use axum::extract::{DefaultBodyLimit, State}; -use axum::http::header::{AUTHORIZATION, CACHE_CONTROL, CONTENT_TYPE}; -use axum::http::{HeaderMap, HeaderValue, StatusCode}; -use axum::response::Response; -use axum::routing::{get, post}; -use serde_json::{Value, json}; -use tokio::sync::{Notify, oneshot}; - -use crate::error::{ReplayError, ReplayErrorKind, ResultExt}; - -const BRIDGE_VERSION: &str = "sandbox-replay-codex-responses-bridge/1"; -const START_TIMEOUT: Duration = Duration::from_secs(10); -const STOP_TIMEOUT: Duration = Duration::from_secs(5); -const MAX_BODY_BYTES: usize = 64 * 1024 * 1024; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PromptMode { - TransportNonce, - ExplicitUserPrompt, -} - -impl PromptMode { - pub fn as_str(self) -> &'static str { - match self { - Self::TransportNonce => "transport_nonce", - Self::ExplicitUserPrompt => "explicit_user_prompt", - } - } -} - -pub struct CodexBridgeHandle { - pub base_url: String, - api_key: String, - shared: Arc, - shutdown: Option>, - worker_done: Option>>, - worker: Option>, -} - -struct BridgeShared { - state: Mutex, - client: reqwest::Client, - upstream_url: String, - upstream_api_key: String, - routing_session_id: String, - bridge_api_key: String, - prompt_mode: PromptMode, - transport_prompt: String, - explicit_prompt: Option, - cancelled: AtomicBool, - cancel_notify: Notify, -} - -struct BridgeState { - request_sequence: usize, - forwarded_requests: usize, - removed_transport_prompt: bool, - pending_forward_sequence: Option, - failed: bool, - failure: Option, -} - -impl BridgeState { - fn fail(&mut self, message: impl Into) { - self.failed = true; - if self.failure.is_none() { - self.failure = Some(message.into()); - } - } -} - -impl CodexBridgeHandle { - pub fn start( - routing_session_id: &str, - transport_prompt: String, - explicit_prompt: Option<&str>, - ) -> Result { - let upstream_base = first_nonempty_env(&[ - "OPENAI_BASE_URL", - "OPENAI_API_BASE", - "LLM_BASE_URL", - ]) - .ok_or_else(|| { - ReplayError::configuration( - "Codex SandboxReplay bridge requires OPENAI_BASE_URL, OPENAI_API_BASE, or LLM_BASE_URL", - ) - })?; - let upstream_api_key = - first_nonempty_env(&["OPENAI_API_KEY", "LLM_API_KEY"]).ok_or_else(|| { - ReplayError::configuration( - "Codex SandboxReplay bridge requires OPENAI_API_KEY or LLM_API_KEY", - ) - })?; - let upstream_url = responses_url(&upstream_base)?; - let prompt_mode = if explicit_prompt.is_some() { - PromptMode::ExplicitUserPrompt - } else { - PromptMode::TransportNonce - }; - let bridge_api_key = format!("pvisor-sandbox-replay-{}", uuid::Uuid::new_v4().simple()); - let listener = TcpListener::bind(("127.0.0.1", 0)).replay_context( - ReplayErrorKind::Continuation, - "allocate Codex SandboxReplay bridge port", - )?; - let address = listener.local_addr().replay_context( - ReplayErrorKind::Continuation, - "read Codex SandboxReplay bridge address", - )?; - listener.set_nonblocking(true).replay_context( - ReplayErrorKind::Continuation, - "configure Codex SandboxReplay bridge listener", - )?; - let client = reqwest::Client::builder() - .no_proxy() - .build() - .replay_context( - ReplayErrorKind::Continuation, - "build Codex SandboxReplay bridge client", - )?; - let shared = Arc::new(BridgeShared { - state: Mutex::new(BridgeState { - request_sequence: 0, - forwarded_requests: 0, - removed_transport_prompt: false, - pending_forward_sequence: None, - failed: false, - failure: None, - }), - client, - upstream_url, - upstream_api_key, - routing_session_id: routing_session_id.to_owned(), - bridge_api_key: bridge_api_key.clone(), - prompt_mode, - transport_prompt, - explicit_prompt: explicit_prompt.map(str::to_owned), - cancelled: AtomicBool::new(false), - cancel_notify: Notify::new(), - }); - let router = router(Arc::clone(&shared)); - let (shutdown_tx, shutdown_rx) = oneshot::channel(); - let (ready_tx, ready_rx) = mpsc::sync_channel(1); - let (done_tx, done_rx) = mpsc::sync_channel(1); - let worker = thread::Builder::new() - .name("pvisor-codex-replay-bridge".into()) - .spawn(move || { - let result = run_worker(listener, router, shutdown_rx, ready_tx); - let _ = done_tx.send(result); - }) - .replay_context( - ReplayErrorKind::Continuation, - "start Codex SandboxReplay bridge thread", - )?; - let mut handle = Self { - base_url: format!("http://{address}/v1"), - api_key: bridge_api_key, - shared, - shutdown: Some(shutdown_tx), - worker_done: Some(done_rx), - worker: Some(worker), - }; - let startup_error = match ready_rx.recv_timeout(START_TIMEOUT) { - Ok(Ok(())) => None, - Ok(Err(message)) => Some(message), - Err(mpsc::RecvTimeoutError::Timeout) => Some(format!( - "Codex SandboxReplay bridge did not become ready within {} seconds", - START_TIMEOUT.as_secs() - )), - Err(mpsc::RecvTimeoutError::Disconnected) => { - Some("Codex SandboxReplay bridge exited before reporting readiness".into()) - } - }; - if let Some(message) = startup_error { - let _ = handle.stop_worker(); - return Err(ReplayError::continuation(message)); - } - Ok(handle) - } - - pub fn child_environment(&self) -> BTreeMap { - let no_proxy = merged_no_proxy_environment(); - BTreeMap::from([ - ("OPENAI_BASE_URL".into(), self.base_url.clone()), - ("OPENAI_API_BASE".into(), self.base_url.clone()), - ("OPENAI_API_KEY".into(), self.api_key.clone()), - ("NO_PROXY".into(), no_proxy.clone()), - ("no_proxy".into(), no_proxy), - ]) - } - - pub fn prompt_mode(&self) -> PromptMode { - self.shared.prompt_mode - } - - pub fn finish(mut self) -> Result { - self.stop_worker()?; - let state = self - .shared - .state - .lock() - .map_err(|_| ReplayError::continuation("Codex bridge state lock poisoned"))?; - if state.failed { - return Err(ReplayError::continuation(format!( - "Codex resume transport bridge failed closed: {}", - state - .failure - .as_deref() - .unwrap_or("unknown protocol failure") - ))); - } - if state.pending_forward_sequence.is_some() { - return Err(ReplayError::continuation( - "Codex bridge has a validated request that was not forwarded", - )); - } - if state.forwarded_requests == 0 { - return Err(ReplayError::continuation( - "Codex continuation made no validated model request through the SandboxReplay bridge", - )); - } - if self.shared.prompt_mode == PromptMode::TransportNonce && !state.removed_transport_prompt - { - return Err(ReplayError::continuation( - "Codex transport nonce was not removed from the first model request", - )); - } - Ok(state.forwarded_requests) - } - - fn stop_worker(&mut self) -> Result<(), ReplayError> { - self.shared.cancelled.store(true, Ordering::Release); - self.shared.cancel_notify.notify_waiters(); - if let Some(shutdown) = self.shutdown.take() { - let _ = shutdown.send(()); - } - let worker_result = match self.worker_done.take() { - Some(done) => match done.recv_timeout(STOP_TIMEOUT) { - Ok(result) => Some(result), - Err(mpsc::RecvTimeoutError::Disconnected) => None, - Err(mpsc::RecvTimeoutError::Timeout) => { - self.worker.take(); - return Err(ReplayError::continuation(format!( - "Codex SandboxReplay bridge did not stop within {} seconds", - STOP_TIMEOUT.as_secs() - ))); - } - }, - None => None, - }; - if let Some(worker) = self.worker.take() - && worker.join().is_err() - { - return Err(ReplayError::continuation( - "Codex SandboxReplay bridge thread panicked", - )); - } - if let Some(result) = worker_result { - result.replay_context(ReplayErrorKind::Executor, "stop Codex SandboxReplay bridge")?; - } - Ok(()) - } -} - -impl Drop for CodexBridgeHandle { - fn drop(&mut self) { - let _ = self.stop_worker(); - } -} - -fn run_worker( - listener: TcpListener, - router: Router, - shutdown_rx: oneshot::Receiver<()>, - ready_tx: mpsc::SyncSender>, -) -> anyhow::Result<()> { - let runtime = tokio::runtime::Builder::new_multi_thread() - .worker_threads(2) - .enable_all() - .build() - .map_err(|error| { - let _ = ready_tx.send(Err(format!("build Codex bridge runtime: {error}"))); - error - })?; - runtime.block_on(async move { - let listener = tokio::net::TcpListener::from_std(listener).map_err(|error| { - let _ = ready_tx.send(Err(format!("adopt Codex bridge listener: {error}"))); - error - })?; - ready_tx - .send(Ok(())) - .map_err(|_| anyhow::anyhow!("Codex bridge startup receiver was dropped"))?; - axum::serve(listener, router) - .with_graceful_shutdown(async move { - let _ = shutdown_rx.await; - }) - .await?; - anyhow::Ok(()) - }) -} - -fn router(shared: Arc) -> Router { - Router::new() - .route("/health", get(health)) - .route("/responses", post(responses)) - .route("/v1/responses", post(responses)) - .fallback(not_found) - .layer(DefaultBodyLimit::max(MAX_BODY_BYTES)) - .with_state(shared) -} - -async fn health(State(shared): State>) -> Response { - let (failed, request_sequence) = shared - .state - .lock() - .map(|state| (state.failed, state.request_sequence)) - .unwrap_or((true, 0)); - response( - StatusCode::OK.as_u16(), - "application/json", - json!({ - "status": "healthy", - "bridge_version": BRIDGE_VERSION, - "resume_mode": true, - "resume_failed": failed, - "request_sequence": request_sequence, - }) - .to_string() - .into_bytes(), - ) -} - -async fn responses( - State(shared): State>, - headers: HeaderMap, - body: Bytes, -) -> Response { - if !authorized(&shared, &headers) { - return error_response(StatusCode::UNAUTHORIZED, "invalid bridge API key"); - } - let payload: Value = match serde_json::from_slice(&body) { - Ok(Value::Object(payload)) => Value::Object(payload), - Ok(_) => return error_response(StatusCode::BAD_REQUEST, "request must be a JSON object"), - Err(error) => { - return error_response(StatusCode::BAD_REQUEST, &format!("invalid JSON: {error}")); - } - }; - let (cleaned, sequence) = match clean_request(&shared, payload) { - Ok(result) => result, - Err(error) => { - fail(&shared, error.to_string()); - return error_response(StatusCode::UNPROCESSABLE_ENTITY, &error.to_string()); - } - }; - let serialized = match serde_json::to_vec(&cleaned) { - Ok(serialized) => serialized, - Err(error) => { - fail(&shared, format!("serialize cleaned Codex request: {error}")); - return error_response(StatusCode::UNPROCESSABLE_ENTITY, &error.to_string()); - } - }; - let upstream = match forward(&shared, serialized).await { - Ok(response) => response, - Err(error) => { - fail(&shared, error.to_string()); - return error_response(StatusCode::BAD_GATEWAY, &error.to_string()); - } - }; - { - let mut state = match shared.state.lock() { - Ok(state) => state, - Err(_) => { - return error_response( - StatusCode::INTERNAL_SERVER_ERROR, - "bridge state lock poisoned", - ); - } - }; - if state.pending_forward_sequence != Some(sequence) { - state.fail("forwarded Codex request sequence did not match the validated request"); - return error_response( - StatusCode::UNPROCESSABLE_ENTITY, - "bridge request sequence mismatch", - ); - } - state.pending_forward_sequence = None; - state.forwarded_requests += 1; - } - response_with_headers(upstream.status, upstream.headers, upstream.body) -} - -struct UpstreamResponse { - status: u16, - headers: HeaderMap, - body: Vec, -} - -async fn forward(shared: &BridgeShared, body: Vec) -> anyhow::Result { - if shared.cancelled.load(Ordering::Acquire) { - anyhow::bail!("Codex SandboxReplay bridge was cancelled"); - } - let request = shared - .client - .post(&shared.upstream_url) - .header( - AUTHORIZATION.as_str(), - format!("Bearer {}", shared.upstream_api_key), - ) - .header(CONTENT_TYPE.as_str(), "application/json") - .header("X-LiteLLM-Session-ID", &shared.routing_session_id) - .body(body) - .send() - .await?; - let status = request.status().as_u16(); - let headers: HeaderMap = request - .headers() - .iter() - .filter(|(name, _)| !is_hop_by_hop_header(name.as_str())) - .map(|(name, value)| (name.clone(), value.clone())) - .collect(); - Ok(UpstreamResponse { - status, - headers, - body: request.bytes().await?.to_vec(), - }) -} - -fn clean_request(shared: &Arc, mut payload: Value) -> anyhow::Result<(Value, usize)> { - let mut state = shared - .state - .lock() - .map_err(|_| anyhow::anyhow!("Codex bridge state lock poisoned"))?; - if state.failed { - anyhow::bail!( - "Codex bridge is failed closed: {}", - state.failure.as_deref().unwrap_or("unknown failure") - ); - } - if state.pending_forward_sequence.is_some() { - state.fail("another Codex request arrived before the previous request was forwarded"); - anyhow::bail!("another request arrived before the previous request was forwarded"); - } - state.request_sequence += 1; - let sequence = state.request_sequence; - let input = payload - .get_mut("input") - .context("Codex Responses request has no input")?; - match shared.prompt_mode { - PromptMode::TransportNonce => { - let removed = remove_exact_user_input(input, &shared.transport_prompt)?; - if sequence == 1 && removed != 1 { - state.fail(format!( - "expected exactly one Codex transport nonce in the first request, found {removed}" - )); - anyhow::bail!( - "expected exactly one Codex transport nonce in the first request, found {removed}" - ); - } - if sequence == 1 { - state.removed_transport_prompt = true; - } - } - PromptMode::ExplicitUserPrompt => { - if sequence == 1 { - let prompt = shared - .explicit_prompt - .as_deref() - .context("explicit Codex prompt mode has no prompt")?; - let count = count_exact_user_input(input, prompt)?; - if count != 1 { - state.fail(format!( - "expected exactly one explicit Codex boundary prompt in the first request, found {count}" - )); - anyhow::bail!( - "expected exactly one explicit Codex boundary prompt in the first request, found {count}" - ); - } - } - } - } - state.pending_forward_sequence = Some(sequence); - Ok((payload, sequence)) -} - -fn remove_exact_user_input(input: &mut Value, expected: &str) -> anyhow::Result { - let items = input - .as_array_mut() - .context("Codex Responses input must be an array for resume transport cleanup")?; - let mut matches = Vec::new(); - for (index, item) in items.iter().enumerate() { - if item.get("role").and_then(Value::as_str) == Some("user") - && exact_message_text(item) == Some(expected) - { - matches.push(index); - } - } - if matches.len() > 1 { - anyhow::bail!("Codex transport nonce occurred more than once in input"); - } - if let Some(index) = matches.first().copied() { - items.remove(index); - Ok(1) - } else { - Ok(0) - } -} - -fn count_exact_user_input(input: &Value, expected: &str) -> anyhow::Result { - let items = input - .as_array() - .context("Codex Responses input must be an array for resume transport validation")?; - Ok(items - .iter() - .filter(|item| { - item.get("role").and_then(Value::as_str) == Some("user") - && exact_message_text(item) == Some(expected) - }) - .count()) -} - -fn exact_message_text(message: &Value) -> Option<&str> { - let content = message.get("content")?; - if let Some(text) = content.as_str() { - return Some(text); - } - let blocks = content.as_array()?; - if blocks.len() != 1 { - return None; - } - let block = &blocks[0]; - if block.get("type").and_then(Value::as_str) != Some("input_text") { - return None; - } - block.get("text").and_then(Value::as_str) -} - -fn authorized(shared: &BridgeShared, headers: &HeaderMap) -> bool { - let supplied = headers - .get("x-api-key") - .and_then(|value| value.to_str().ok()) - .or_else(|| { - headers - .get(AUTHORIZATION) - .and_then(|value| value.to_str().ok()) - .and_then(|value| value.strip_prefix("Bearer ")) - }); - supplied == Some(shared.bridge_api_key.as_str()) -} - -fn fail(shared: &BridgeShared, message: String) { - if let Ok(mut state) = shared.state.lock() { - state.fail(message); - } -} - -fn response(status: u16, content_type: &str, body: Vec) -> Response { - let mut response = Response::new(Body::from(body)); - *response.status_mut() = StatusCode::from_u16(status).unwrap_or(StatusCode::BAD_GATEWAY); - if let Ok(value) = HeaderValue::from_str(content_type) { - response.headers_mut().insert(CONTENT_TYPE, value); - } - response - .headers_mut() - .insert(CACHE_CONTROL, HeaderValue::from_static("no-cache")); - response -} - -fn response_with_headers(status: u16, headers: HeaderMap, body: Vec) -> Response { - let mut response = Response::new(Body::from(body)); - *response.status_mut() = StatusCode::from_u16(status).unwrap_or(StatusCode::BAD_GATEWAY); - *response.headers_mut() = headers; - response -} - -fn is_hop_by_hop_header(name: &str) -> bool { - matches!( - name.to_ascii_lowercase().as_str(), - "connection" - | "keep-alive" - | "proxy-authenticate" - | "proxy-authorization" - | "te" - | "trailer" - | "transfer-encoding" - | "upgrade" - | "content-length" - ) -} - -fn error_response(status: StatusCode, message: &str) -> Response { - response( - status.as_u16(), - "application/json", - json!({"error": {"message": message}}) - .to_string() - .into_bytes(), - ) -} - -async fn not_found() -> Response { - error_response(StatusCode::NOT_FOUND, "not found") -} - -fn responses_url(base: &str) -> Result { - let mut url = reqwest::Url::parse(base).map_err(|error| { - ReplayError::configuration(format!("invalid OpenAI base URL {base:?}: {error}")) - })?; - let path = url.path().trim_end_matches('/'); - let path = if path.ends_with("/responses") { - path.to_owned() - } else if path.ends_with("/v1") { - format!("{path}/responses") - } else if path.is_empty() { - "/v1/responses".into() - } else { - format!("{path}/v1/responses") - }; - url.set_path(&path); - url.set_query(None); - url.set_fragment(None); - Ok(url.to_string()) -} - -fn merged_no_proxy_environment() -> String { - let configured = ["NO_PROXY", "no_proxy"] - .iter() - .filter_map(|name| std::env::var(name).ok()) - .collect::>(); - let mut entries = Vec::new(); - for value in configured { - for entry in value - .split(',') - .map(str::trim) - .filter(|entry| !entry.is_empty()) - { - if !entries.iter().any(|existing| existing == entry) { - entries.push(entry.to_owned()); - } - } - } - for required in ["127.0.0.1", "localhost", "::1"] { - if !entries.iter().any(|existing| existing == required) { - entries.push(required.into()); - } - } - entries.join(",") -} - -fn first_nonempty_env(names: &[&str]) -> Option { - names.iter().find_map(|name| { - std::env::var(name) - .ok() - .filter(|value| !value.trim().is_empty()) - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn responses_url_appends_responses_endpoint() { - assert_eq!( - responses_url("http://model/v1").unwrap(), - "http://model/v1/responses" - ); - assert_eq!( - responses_url("http://model/v1/").unwrap(), - "http://model/v1/responses" - ); - assert_eq!( - responses_url("http://model").unwrap(), - "http://model/v1/responses" - ); - } - - #[test] - fn hop_by_hop_response_headers_are_not_forwarded() { - assert!(is_hop_by_hop_header("connection")); - assert!(is_hop_by_hop_header("Transfer-Encoding")); - assert!(is_hop_by_hop_header("content-length")); - assert!(!is_hop_by_hop_header("content-type")); - assert!(!is_hop_by_hop_header("date")); - } - - #[test] - fn removes_exact_nonce_from_responses_input() { - let mut input = json!([ - {"role":"user","content":[{"type":"input_text","text":"task"}]}, - {"role":"user","content":[{"type":"input_text","text":"nonce-1234567890"}]}, - {"role":"assistant","content":[]} - ]); - assert_eq!( - remove_exact_user_input(&mut input, "nonce-1234567890").unwrap(), - 1 - ); - assert_eq!(input.as_array().unwrap().len(), 2); - assert_eq!( - count_exact_user_input(&input, "nonce-1234567890").unwrap(), - 0 - ); - } - - #[test] - fn nonce_must_be_unique() { - let mut input = json!([ - {"role":"user","content":"nonce"}, - {"role":"user","content":"nonce"} - ]); - assert!(remove_exact_user_input(&mut input, "nonce").is_err()); - } - - #[test] - fn historical_nonce_is_removed_from_later_requests() { - let nonce = "pvisor-codex-resume-test"; - let mut first = json!({ - "input": [ - {"role": "user", "content": [{"type": "input_text", "text": nonce}]}, - {"role": "assistant", "content": [{"type": "output_text", "text": "ok"}]} - ] - }); - let mut second = first.clone(); - let removed_first = - remove_exact_user_input(first.get_mut("input").unwrap(), nonce).unwrap(); - let removed_second = - remove_exact_user_input(second.get_mut("input").unwrap(), nonce).unwrap(); - assert_eq!(removed_first, 1); - assert_eq!(removed_second, 1); - assert!(!first.to_string().contains(nonce)); - assert!(!second.to_string().contains(nonce)); - } - - #[test] - fn explicit_prompt_is_not_removed() { - let input = json!([ - {"role":"user","content":"task"}, - {"role":"user","content":"review O-prime N"} - ]); - assert_eq!( - count_exact_user_input(&input, "review O-prime N").unwrap(), - 1 - ); - } -} diff --git a/crates/persisting-replay/src/comparison.rs b/crates/persisting-replay/src/comparison.rs deleted file mode 100644 index 1b487f773..000000000 --- a/crates/persisting-replay/src/comparison.rs +++ /dev/null @@ -1,305 +0,0 @@ -use std::path::Path; - -use serde_json::{Value, json}; - -use crate::error::ReplayError; -use crate::io::atomic_write_json; - -pub fn write_next_action( - path: &Path, - original: &Value, - replayed: &Value, - boundary_user_prompt_injected: bool, -) -> Result<(), ReplayError> { - atomic_write_json( - path, - &json!({ - "schema_version": "sandbox-playback.next-action-comparison/v2", - "original": original, - "replayed": replayed, - "metrics": metrics(original, replayed), - "gating": false, - "input_condition": if boundary_user_prompt_injected { - "boundary_user_prompt_appended" - } else { - "replayed_boundary_only" - }, - "boundary_user_prompt_injected": boundary_user_prompt_injected, - }), - ) -} - -fn metrics(original: &Value, replayed: &Value) -> Value { - let original_text = text(original); - let replayed_text = text(replayed); - let original_reasoning = reasoning(original); - let replayed_reasoning = reasoning(replayed); - let original_tools = tools(original); - let replayed_tools = tools(replayed); - let text_metrics = content_metrics(original_text, replayed_text); - let reasoning_metrics = content_metrics(original_reasoning, replayed_reasoning); - json!({ - "original_text_present": text_metrics.original_present, - "replayed_text_present": text_metrics.replayed_present, - "text_comparison_status": text_metrics.status, - "text_exact": text_metrics.exact, - "text_similarity": text_metrics.similarity, - "original_reasoning_present": reasoning_metrics.original_present, - "replayed_reasoning_present": reasoning_metrics.replayed_present, - "reasoning_comparison_status": reasoning_metrics.status, - "reasoning_exact": reasoning_metrics.exact, - "reasoning_similarity": reasoning_metrics.similarity, - "tool_count_equal": original_tools.len() == replayed_tools.len(), - "ordered_tool_names_equal": original_tools.iter().map(|tool| tool.get("name")) - .eq(replayed_tools.iter().map(|tool| tool.get("name"))), - "tool_arguments_equal": original_tools.iter().map(|tool| tool.get("arguments")) - .eq(replayed_tools.iter().map(|tool| tool.get("arguments"))), - }) -} - -#[derive(Debug)] -struct ContentMetrics { - original_present: bool, - replayed_present: bool, - status: &'static str, - exact: Option, - similarity: Option, -} - -fn content_metrics(original: &str, replayed: &str) -> ContentMetrics { - let normalized_original = normalize(original); - let normalized_replayed = normalize(replayed); - let original_present = !normalized_original.is_empty(); - let replayed_present = !normalized_replayed.is_empty(); - match (original_present, replayed_present) { - (false, false) => ContentMetrics { - original_present, - replayed_present, - status: "not_applicable_both_empty", - exact: None, - similarity: None, - }, - (false, true) => ContentMetrics { - original_present, - replayed_present, - status: "original_empty", - exact: Some(false), - similarity: Some(0.0), - }, - (true, false) => ContentMetrics { - original_present, - replayed_present, - status: "replayed_empty", - exact: Some(false), - similarity: Some(0.0), - }, - (true, true) => ContentMetrics { - original_present, - replayed_present, - status: "comparable", - exact: Some(original == replayed), - similarity: Some(sequence_matcher_ratio( - &normalized_original, - &normalized_replayed, - )), - }, - } -} - -fn text(action: &Value) -> &str { - action - .get("text") - .and_then(Value::as_str) - .unwrap_or_default() -} - -fn reasoning(action: &Value) -> &str { - action - .get("reasoning") - .and_then(Value::as_str) - .unwrap_or_default() -} - -fn tools(action: &Value) -> &[Value] { - action - .get("tools") - .and_then(Value::as_array) - .map(Vec::as_slice) - .unwrap_or_default() -} - -fn normalize(value: &str) -> String { - value.split_whitespace().collect::>().join(" ") -} - -// Port of difflib.SequenceMatcher's ratio for strings with isjunk=None. Keeping -// this local avoids a runtime Python dependency while preserving the metric -// emitted by the original SandboxReplay implementation. -fn sequence_matcher_ratio(left: &str, right: &str) -> f64 { - let a: Vec = left.chars().collect(); - let b: Vec = right.chars().collect(); - let total = a.len() + b.len(); - if total == 0 { - return 1.0; - } - let matches: usize = matching_blocks(&a, &b).iter().map(|block| block.2).sum(); - 2.0 * matches as f64 / total as f64 -} - -fn matching_blocks(a: &[char], b: &[char]) -> Vec<(usize, usize, usize)> { - let mut queue = vec![(0, a.len(), 0, b.len())]; - let mut matches = Vec::new(); - while let Some((alo, ahi, blo, bhi)) = queue.pop() { - let (i, j, size) = longest_match(a, b, alo, ahi, blo, bhi); - if size == 0 { - continue; - } - matches.push((i, j, size)); - if alo < i && blo < j { - queue.push((alo, i, blo, j)); - } - if i + size < ahi && j + size < bhi { - queue.push((i + size, ahi, j + size, bhi)); - } - } - matches.sort_unstable(); - let mut collapsed: Vec<(usize, usize, usize)> = Vec::new(); - for (i, j, size) in matches { - if let Some(last) = collapsed.last_mut() - && last.0 + last.2 == i - && last.1 + last.2 == j - { - last.2 += size; - continue; - } - collapsed.push((i, j, size)); - } - collapsed -} - -fn longest_match( - a: &[char], - b: &[char], - alo: usize, - ahi: usize, - blo: usize, - bhi: usize, -) -> (usize, usize, usize) { - use std::collections::{HashMap, HashSet}; - - let mut positions: HashMap> = HashMap::new(); - for (index, character) in b.iter().copied().enumerate() { - positions.entry(character).or_default().push(index); - } - let popular: HashSet = if b.len() >= 200 { - let threshold = b.len() / 100 + 1; - positions - .iter() - .filter_map(|(character, indexes)| (indexes.len() > threshold).then_some(*character)) - .collect() - } else { - HashSet::new() - }; - - let (mut best_i, mut best_j, mut best_size) = (alo, blo, 0); - let mut previous: HashMap = HashMap::new(); - for (i, character) in a.iter().copied().enumerate().take(ahi).skip(alo) { - let mut current = HashMap::new(); - if !popular.contains(&character) - && let Some(indexes) = positions.get(&character) - { - for &j in indexes { - if j < blo { - continue; - } - if j >= bhi { - break; - } - let size = previous - .get(&j.checked_sub(1).unwrap_or(usize::MAX)) - .copied() - .unwrap_or(0) - + 1; - current.insert(j, size); - if size > best_size { - (best_i, best_j, best_size) = (i + 1 - size, j + 1 - size, size); - } - } - } - previous = current; - } - while best_i > alo && best_j > blo && a[best_i - 1] == b[best_j - 1] { - best_i -= 1; - best_j -= 1; - best_size += 1; - } - while best_i + best_size < ahi - && best_j + best_size < bhi - && a[best_i + best_size] == b[best_j + best_size] - { - best_size += 1; - } - (best_i, best_j, best_size) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn sequence_matcher_matches_difflib_examples() { - assert_eq!(sequence_matcher_ratio("", ""), 1.0); - assert_eq!(sequence_matcher_ratio("abcd", "abxd"), 0.75); - assert_eq!(sequence_matcher_ratio("abc", "abc"), 1.0); - } - - #[test] - fn next_action_metrics_preserve_nonempty_text_contract() { - let original = json!({ - "text": "inspect\r\n file", - "tools": [{"name": "read", "arguments": {"path": "a"}}], - }); - let replayed = json!({ - "text": "inspect file", - "tools": [{"name": "read", "arguments": {"path": "a"}}], - }); - let value = metrics(&original, &replayed); - assert_eq!(value["text_exact"], false); - assert_eq!(value["text_similarity"], 1.0); - assert_eq!(value["text_comparison_status"], "comparable"); - assert_eq!(value["original_text_present"], true); - assert_eq!(value["replayed_text_present"], true); - assert_eq!(value["reasoning_exact"], Value::Null); - assert_eq!(value["reasoning_similarity"], Value::Null); - assert_eq!( - value["reasoning_comparison_status"], - "not_applicable_both_empty" - ); - assert_eq!(value["tool_count_equal"], true); - assert_eq!(value["ordered_tool_names_equal"], true); - assert_eq!(value["tool_arguments_equal"], true); - } - - #[test] - fn empty_text_is_not_reported_as_a_perfect_match() { - let original = json!({"text": "\n\n", "tools": []}); - let replayed = json!({"text": " \t", "tools": []}); - let value = metrics(&original, &replayed); - assert_eq!(value["original_text_present"], false); - assert_eq!(value["replayed_text_present"], false); - assert_eq!(value["text_comparison_status"], "not_applicable_both_empty"); - assert_eq!(value["text_exact"], Value::Null); - assert_eq!(value["text_similarity"], Value::Null); - } - - #[test] - fn reasoning_is_compared_separately_from_visible_text() { - let original = json!({"text": "", "reasoning": "inspect files", "tools": []}); - let replayed = json!({"text": "", "reasoning": "inspect code", "tools": []}); - let value = metrics(&original, &replayed); - assert_eq!(value["text_similarity"], Value::Null); - assert_eq!(value["reasoning_comparison_status"], "comparable"); - assert_eq!(value["reasoning_exact"], false); - assert!(value["reasoning_similarity"].as_f64().unwrap() > 0.0); - } -} diff --git a/crates/persisting-replay/src/config.rs b/crates/persisting-replay/src/config.rs deleted file mode 100644 index 78352e6a5..000000000 --- a/crates/persisting-replay/src/config.rs +++ /dev/null @@ -1,472 +0,0 @@ -use std::fs; -use std::path::{Path, PathBuf}; -use std::str::FromStr; - -use serde::Deserialize; - -use crate::error::{ReplayError, ReplayErrorKind, ResultExt}; -use crate::model::{AgentKind, PlaybackRequest, REQUEST_SCHEMA_VERSION, ReplayMode}; - -#[derive(Debug, Clone, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ReplayToml { - pub replay: ReplayConfig, - #[serde(default)] - pub run: RunConfig, - #[serde(default)] - pub overlayfs: OverlayFsConfig, - #[serde(default)] - pub overlaynet: OverlayNetConfig, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ReplayConfig { - pub agent: String, - pub trajectory: PathBuf, - pub after_step: usize, - pub agent_entrypoint: Option, - pub agent_runtime: Option, - #[serde(default)] - pub disallowed_tools: Vec, - pub trajectory_assets: Option, - pub max_steps: Option, - pub session_id: Option, - #[serde(default)] - pub replay_only: bool, - #[serde(default)] - pub prepare_only: bool, - #[serde(default)] - pub allow_stale_observations: bool, - #[serde(default)] - pub disable_thinking: bool, - pub boundary_user_prompt: Option, - pub run_id: Option, - pub workspace: Option, - pub state_dir: Option, - pub output_dir: Option, -} - -#[derive(Debug, Clone, Default, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct RunConfig { - #[serde(default)] - pub safe: bool, - pub executor: Option, - pub timeout_ms: Option, - pub policy: Option, - #[serde(default)] - pub inherit_env: bool, - #[serde(default)] - pub pass_env: Vec, -} - -#[derive(Debug, Clone, Default, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct OverlayFsConfig { - /// Absolute path visible to the replay Agent. - pub path: Option, - /// Host layers composed in declaration order. - #[serde(default)] - pub compose: Vec, - pub backend: Option, - pub commit: Option, -} - -#[derive(Debug, Clone, Default, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct OverlayNetConfig { - pub mode: Option, - pub policy: Option, -} - -impl ReplayToml { - pub fn from_file(path: &Path) -> Result { - let source = fs::read_to_string(path).replay_context( - ReplayErrorKind::Configuration, - format!("read replay config {}", path.display()), - )?; - toml::from_str(&source).replay_context( - ReplayErrorKind::Configuration, - format!("parse replay config {}", path.display()), - ) - } - - pub fn into_request(self, cwd: &Path) -> Result { - let replay = self.replay; - let mode = replay_mode(replay.prepare_only, replay.replay_only)?; - Ok(PlaybackRequest { - agent: AgentKind::from_str(&replay.agent) - .map_err(|message| ReplayError::new(ReplayErrorKind::UnsupportedAgent, message))?, - trajectory: replay.trajectory, - after_step: replay.after_step, - workspace: replay.workspace.unwrap_or_else(|| cwd.to_path_buf()), - state_dir: replay - .state_dir - .unwrap_or_else(|| PathBuf::from("/tmp/pvisor-sandbox-replay/state")), - output_dir: replay - .output_dir - .unwrap_or_else(|| PathBuf::from("/tmp/pvisor-sandbox-replay/output")), - agent_entrypoint: replay.agent_entrypoint, - agent_runtime: replay.agent_runtime, - disallowed_tools: replay.disallowed_tools, - trajectory_assets: replay.trajectory_assets, - session_id: replay.session_id, - max_steps: replay.max_steps, - mode, - allow_stale_observations: replay.allow_stale_observations, - run_id: replay.run_id, - disable_thinking: replay.disable_thinking, - boundary_user_prompt: replay.boundary_user_prompt, - }) - } -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct JsonRequest { - schema_version: String, - agent: JsonAgent, - trajectory: PathBuf, - trajectory_assets: Option, - after_step: usize, - workspace: PathBuf, - state_dir: PathBuf, - output_dir: PathBuf, - max_steps: Option, - session_id: Option, - #[serde(default)] - replay_only: bool, - #[serde(default)] - prepare_only: bool, - #[serde(default)] - allow_stale_observations: bool, - #[serde(default)] - disable_thinking: bool, - boundary_user_prompt: Option, - run_id: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct JsonAgent { - #[serde(rename = "type")] - kind: String, - entrypoint: Option, - runtime: Option, - #[serde(default)] - disallowed_tools: Vec, -} - -pub fn request_from_json(path: &Path) -> Result { - let bytes = fs::read(path).replay_context( - ReplayErrorKind::Configuration, - format!("read request {}", path.display()), - )?; - let request: JsonRequest = serde_json::from_slice(&bytes).replay_context( - ReplayErrorKind::Configuration, - "parse sandbox-playback request", - )?; - if request.schema_version != REQUEST_SCHEMA_VERSION { - return Err(ReplayError::configuration(format!( - "request schema_version must be {REQUEST_SCHEMA_VERSION:?}" - ))); - } - let mode = replay_mode(request.prepare_only, request.replay_only)?; - Ok(PlaybackRequest { - agent: AgentKind::from_str(&request.agent.kind) - .map_err(|message| ReplayError::new(ReplayErrorKind::UnsupportedAgent, message))?, - trajectory: request.trajectory, - after_step: request.after_step, - workspace: request.workspace, - state_dir: request.state_dir, - output_dir: request.output_dir, - agent_entrypoint: request.agent.entrypoint, - agent_runtime: request.agent.runtime, - disallowed_tools: request.agent.disallowed_tools, - trajectory_assets: request.trajectory_assets, - session_id: request.session_id, - max_steps: request.max_steps, - mode, - allow_stale_observations: request.allow_stale_observations, - run_id: request.run_id, - disable_thinking: request.disable_thinking, - boundary_user_prompt: request.boundary_user_prompt, - }) -} - -fn replay_mode(prepare_only: bool, replay_only: bool) -> Result { - match (prepare_only, replay_only) { - (true, true) => Err(ReplayError::configuration( - "prepare_only and replay_only are mutually exclusive", - )), - (true, false) => Ok(ReplayMode::PrepareOnly), - (false, true) => Ok(ReplayMode::ReplayOnly), - (false, false) => Ok(ReplayMode::ReplayAndContinue), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::model::ReplayMode; - - #[test] - fn toml_maps_prepare_and_replay_modes_and_rejects_both() { - let prepare: ReplayToml = toml::from_str( - r#" -[replay] -agent = "claude-code" -trajectory = "/input/session.jsonl" -after_step = 1 -prepare_only = true -allow_stale_observations = true -"#, - ) - .unwrap(); - let prepare = prepare.into_request(Path::new("/workspace")).unwrap(); - assert_eq!(prepare.mode, ReplayMode::PrepareOnly); - assert!(prepare.allow_stale_observations); - - let replay: ReplayToml = toml::from_str( - r#" -[replay] -agent = "claude-code" -trajectory = "/input/session.jsonl" -after_step = 1 -replay_only = true -"#, - ) - .unwrap(); - assert_eq!( - replay.into_request(Path::new("/workspace")).unwrap().mode, - ReplayMode::ReplayOnly - ); - - let both: ReplayToml = toml::from_str( - r#" -[replay] -agent = "claude-code" -trajectory = "/input/session.jsonl" -after_step = 1 -prepare_only = true -replay_only = true -"#, - ) - .unwrap(); - assert!(both.into_request(Path::new("/workspace")).is_err()); - } - - #[test] - fn minimal_toml_defaults_to_prepared_sandbox() { - let config: ReplayToml = toml::from_str( - r#" -[replay] -agent = "claude-code" -trajectory = "/input/session.jsonl" -after_step = 30 -agent_entrypoint = "/usr/bin/claude" -"#, - ) - .unwrap(); - let request = config.into_request(Path::new("/workspace")).unwrap(); - assert_eq!(request.workspace, Path::new("/workspace")); - assert_eq!( - request.state_dir, - Path::new("/tmp/pvisor-sandbox-replay/state") - ); - assert_eq!( - request.output_dir, - Path::new("/tmp/pvisor-sandbox-replay/output") - ); - assert!(!request.disable_thinking); - } - - #[test] - fn toml_can_disable_thinking_for_live_continuation() { - let config: ReplayToml = toml::from_str( - r#" -[replay] -agent = "claude-code" -trajectory = "/input/session.jsonl" -after_step = 30 -agent_entrypoint = "/usr/bin/claude" -disable_thinking = true -"#, - ) - .unwrap(); - - let request = config.into_request(Path::new("/workspace")).unwrap(); - - assert!(request.disable_thinking); - } - - #[test] - fn toml_accepts_a_boundary_user_prompt() { - let config: ReplayToml = toml::from_str( - r#" -[replay] -agent = "claude-code" -trajectory = "/input/session.jsonl" -after_step = 30 -boundary_user_prompt = "Review the fresh observation before continuing." -"#, - ) - .unwrap(); - - let request = config.into_request(Path::new("/workspace")).unwrap(); - - assert_eq!( - request.boundary_user_prompt(), - Some("Review the fresh observation before continuing.") - ); - } - - #[test] - fn toml_rejects_removed_gateway_section() { - let config = toml::from_str::( - r#" -[replay] -agent = "claude-code" -trajectory = "/input/session.jsonl" -after_step = 1 -agent_entrypoint = "/usr/bin/claude" -[gateway] -mode = "off" -"#, - ); - assert!(config.is_err()); - } - - #[test] - fn toml_rejects_removed_chronicle_section() { - let config = toml::from_str::( - r#" -[replay] -agent = "claude-code" -trajectory = "/input/session.jsonl" -after_step = 1 -agent_entrypoint = "/usr/bin/claude" -[chronicle] -mode = "off" -"#, - ); - assert!(config.is_err()); - } - - #[test] - fn json_request_accepts_the_sweeval_contract() { - let temporary = tempfile::tempdir().unwrap(); - let path = temporary.path().join("request.json"); - fs::write( - &path, - serde_json::to_vec(&serde_json::json!({ - "schema_version": "sandbox-playback.request/v1", - "agent": { - "type": "mini-swe-agent", - "entrypoint": "/root/.local/bin/mini-swe-agent" - }, - "trajectory": "/tmp/sweeval-sandbox-replay/input/trajectory.json", - "after_step": 49, - "workspace": "/app", - "state_dir": "/tmp/sweeval-sandbox-replay/state", - "output_dir": "/tmp/sweeval-sandbox-replay/output", - "max_steps": 200, - "session_id": "task-291-attempt-1", - "run_id": "sweeval" - })) - .unwrap(), - ) - .unwrap(); - - let request = request_from_json(&path).unwrap(); - - assert_eq!(request.agent, AgentKind::MiniSweAgent); - assert_eq!(request.after_step, 49); - assert_eq!(request.workspace, Path::new("/app")); - assert_eq!(request.max_steps, Some(200)); - assert_eq!(request.session_id.as_deref(), Some("task-291-attempt-1")); - assert_eq!(request.run_id.as_deref(), Some("sweeval")); - assert_eq!(request.mode, ReplayMode::ReplayAndContinue); - assert!(!request.disable_thinking); - } - - #[test] - fn json_maps_prepare_mode_and_rejects_conflicting_modes() { - let temporary = tempfile::tempdir().unwrap(); - let path = temporary.path().join("request.json"); - let mut value = serde_json::json!({ - "schema_version": "sandbox-playback.request/v1", - "agent": { "type": "claude-code" }, - "trajectory": "/input/session.jsonl", - "after_step": 1, - "workspace": "/workspace", - "state_dir": "/state", - "output_dir": "/output", - "prepare_only": true, - "allow_stale_observations": true - }); - fs::write(&path, serde_json::to_vec(&value).unwrap()).unwrap(); - - let request = request_from_json(&path).unwrap(); - assert_eq!(request.mode, ReplayMode::PrepareOnly); - assert!(request.allow_stale_observations); - - value["replay_only"] = serde_json::Value::Bool(true); - fs::write(&path, serde_json::to_vec(&value).unwrap()).unwrap(); - assert!(request_from_json(&path).is_err()); - } - - #[test] - fn json_request_can_disable_thinking() { - let temporary = tempfile::tempdir().unwrap(); - let path = temporary.path().join("request.json"); - fs::write( - &path, - serde_json::to_vec(&serde_json::json!({ - "schema_version": "sandbox-playback.request/v1", - "agent": { - "type": "mini-swe-agent", - "entrypoint": "/root/.local/bin/mini-swe-agent" - }, - "trajectory": "/tmp/sweeval-sandbox-replay/input/trajectory.json", - "after_step": 49, - "workspace": "/app", - "state_dir": "/tmp/sweeval-sandbox-replay/state", - "output_dir": "/tmp/sweeval-sandbox-replay/output", - "disable_thinking": true - })) - .unwrap(), - ) - .unwrap(); - - let request = request_from_json(&path).unwrap(); - - assert!(request.disable_thinking); - } - - #[test] - fn json_request_accepts_a_boundary_user_prompt() { - let temporary = tempfile::tempdir().unwrap(); - let path = temporary.path().join("request.json"); - fs::write( - &path, - serde_json::to_vec(&serde_json::json!({ - "schema_version": "sandbox-playback.request/v1", - "agent": {"type": "mini-swe-agent"}, - "trajectory": "/input/trajectory.json", - "after_step": 1, - "workspace": "/workspace", - "state_dir": "/state", - "output_dir": "/output", - "boundary_user_prompt": "Inspect O-prime N." - })) - .unwrap(), - ) - .unwrap(); - - let request = request_from_json(&path).unwrap(); - - assert_eq!(request.boundary_user_prompt(), Some("Inspect O-prime N.")); - } -} diff --git a/crates/persisting-replay/src/engine.rs b/crates/persisting-replay/src/engine.rs deleted file mode 100644 index 52759d341..000000000 --- a/crates/persisting-replay/src/engine.rs +++ /dev/null @@ -1,870 +0,0 @@ -use std::collections::BTreeSet; -use std::fs; -use std::path::Path; - -use serde_json::{Value, json}; - -use crate::adapter::{LaunchSpec, RunContext, build_plan, resolve_launch_spec, run}; -use crate::comparison::write_next_action; -use crate::error::{ReplayError, ReplayErrorKind, ResultExt}; -use crate::io::{atomic_write_json, canonicalize, sha256}; -use crate::journal::Journal; -use crate::model::{ - AdapterPlan, AgentKind, AgentResult, AgentStatus, Artifact, ExecutionReport, PlaybackRequest, - RESULT_SCHEMA_VERSION, ReplayFailure, ReplayMode, ReplayOutcome, ReplayPhase, ReplayQuality, - ReplayResult, -}; - -pub fn execute(request: PlaybackRequest) -> Result { - let run_id = request - .run_id - .clone() - .unwrap_or_else(|| format!("replay-{}", uuid::Uuid::new_v4().simple())); - let state_dir = location_hint(&request.state_dir, &run_id); - let output_dir = location_hint(&request.output_dir, &run_id); - execute_with_run_id(request, run_id.clone()) - .map_err(|error| error.with_default_locations(run_id, state_dir, output_dir)) -} - -fn execute_with_run_id( - mut request: PlaybackRequest, - run_id: String, -) -> Result { - validate(&request)?; - request.workspace = canonicalize(&request.workspace, ReplayErrorKind::Workspace, "workspace")?; - request.trajectory = canonicalize( - &request.trajectory, - ReplayErrorKind::Configuration, - "trajectory", - )?; - let state_root = absolute_or_current(&request.state_dir)?; - let output_root = absolute_or_current(&request.output_dir)?; - let state_dir = state_root.join(&run_id); - let output_dir = output_root.join(&run_id); - let plan = build_plan(&request)?; - validate_step_budget(request.mode, request.max_steps, plan.prefix_model_turns())?; - let launch = resolve_launch_spec(&request)?; - if let Some(launch) = &launch - && launch.version != plan.agent().supported_version() - { - return Err(ReplayError::new( - ReplayErrorKind::UnsupportedVersion, - format!( - "trajectory version {:?} does not match agent version {:?}", - plan.agent().supported_version(), - launch.version - ), - )); - } - - let mut journal = Journal::open(&state_dir)?; - fs::create_dir_all(&output_root).replay_context( - ReplayErrorKind::Executor, - format!("create output root {}", output_root.display()), - )?; - fs::create_dir(&output_dir).replay_context( - ReplayErrorKind::Executor, - format!("create unique replay output {}", output_dir.display()), - )?; - - match execute_allocated( - &request, - &run_id, - &state_dir, - &output_dir, - &plan, - launch.as_ref(), - &mut journal, - ) { - Ok(result) => Ok(ExecutionReport { - result, - exit_code: 0, - }), - Err(error) => finalize_failure( - &request, - &run_id, - &state_dir, - &output_dir, - &plan, - launch.as_ref(), - &mut journal, - error, - ), - } -} - -fn execute_allocated( - request: &PlaybackRequest, - run_id: &str, - state_dir: &Path, - output_dir: &Path, - plan: &AdapterPlan, - launch: Option<&LaunchSpec>, - journal: &mut Journal, -) -> Result { - journal.append( - "run_started", - [ - ("run_id".into(), json!(run_id)), - ("agent".into(), json!(request.agent.as_str())), - ("source_sha256".into(), json!(plan.source_sha256())), - ("after_step".into(), json!(plan.after_step())), - ( - "boundary_user_prompt_requested".into(), - json!(request.boundary_user_prompt().is_some()), - ), - ( - "boundary_user_prompt_sha256".into(), - json!( - request - .boundary_user_prompt() - .map(|prompt| sha256(prompt.as_bytes())) - ), - ), - ], - )?; - journal.append( - "plan_validated", - [ - ("profile".into(), json!(plan.agent().profile())), - ("tool_calls".into(), json!(plan.calls().count())), - ], - )?; - atomic_write_json(&output_dir.join("manifest.json"), &plan.public_value())?; - - let session_id = request - .session_id - .clone() - .unwrap_or_else(|| run_id.to_owned()); - let nonce = format!("__PVISOR_NATIVE_REPLAY_{}__", uuid::Uuid::new_v4().simple()); - let context = RunContext { - request, - state_dir, - output_dir, - launch, - session_id: &session_id, - nonce: &nonce, - }; - let outcome = run(plan, &context, journal)?; - write_next_action_comparison(request, plan, &outcome, output_dir)?; - journal.append("run_finished", [("status".into(), json!(outcome.status))])?; - fs::copy(&journal.path, output_dir.join("replay-events.jsonl")) - .replay_context(ReplayErrorKind::Executor, "copy replay journal to output")?; - - let comparison = read_comparison(&output_dir.join("observation-comparison.json")); - let exact = comparison - .iter() - .filter(|item| item.get("exact").and_then(Value::as_bool) == Some(true)) - .count(); - let different = comparison - .iter() - .filter(|item| item.get("exact").and_then(Value::as_bool) == Some(false)) - .count(); - atomic_write_json( - &output_dir.join("replay-summary.json"), - &json!({ - "schema_version": "sandbox-playback.summary/v1", - "agent": request.agent.as_str(), - "after_step": plan.after_step(), - "replayed_tool_calls": outcome.observations.len(), - "exact_observations": exact, - "different_observations": different, - "comparison_is_gating": false, - "continuation_status": outcome.status, - "continued_steps": outcome.continued_steps, - "boundary_user_prompt": outcome.metadata.get("boundary_user_prompt"), - }), - )?; - - let artifacts = artifacts(request, &outcome, output_dir); - let result = ReplayResult { - schema_version: RESULT_SCHEMA_VERSION, - phase: phase_for_mode(request.mode), - quality: quality_for_outcome(&outcome), - agent_status: agent_status_for_outcome(request.mode, &outcome), - run_id: run_id.to_owned(), - agent: agent_result(request, launch), - after_step: plan.after_step(), - replayed_tool_calls: outcome.observations.len(), - prefix_model_turns: plan.prefix_model_turns(), - continued_steps: outcome.continued_steps, - state_dir: state_dir.to_path_buf(), - output_dir: output_dir.to_path_buf(), - artifacts, - failure: None, - retryable: false, - metadata: outcome.metadata, - }; - atomic_write_json(&output_dir.join("result.json"), &result)?; - Ok(result) -} - -#[allow(clippy::too_many_arguments)] -fn finalize_failure( - request: &PlaybackRequest, - run_id: &str, - state_dir: &Path, - output_dir: &Path, - plan: &AdapterPlan, - launch: Option<&LaunchSpec>, - journal: &mut Journal, - error: ReplayError, -) -> Result { - let _ = journal.append( - "run_failed", - [ - ("category".into(), json!(error.kind.category())), - ("message".into(), json!(error.message)), - ], - ); - let _ = fs::copy(&journal.path, output_dir.join("replay-events.jsonl")); - let result = failure_result( - request, - launch, - plan, - run_id.to_owned(), - state_dir.to_path_buf(), - output_dir.to_path_buf(), - &error, - ); - atomic_write_json(&output_dir.join("result.json"), &result)?; - Ok(ExecutionReport { - exit_code: error.exit_code(), - result, - }) -} - -fn phase_for_mode(mode: ReplayMode) -> ReplayPhase { - match mode { - ReplayMode::PrepareOnly => ReplayPhase::Prepared, - ReplayMode::ReplayOnly => ReplayPhase::Replayed, - ReplayMode::ReplayAndContinue => ReplayPhase::Continued, - } -} - -fn quality_for_outcome(outcome: &ReplayOutcome) -> ReplayQuality { - if outcome.observations.iter().any(|observation| { - observation - .metadata - .contains_key("opaque_source_observation") - || observation.metadata.contains_key("degradation_reason") - }) { - ReplayQuality::Degraded - } else { - ReplayQuality::Verified - } -} - -fn agent_status_for_outcome(mode: ReplayMode, outcome: &ReplayOutcome) -> AgentStatus { - if mode != ReplayMode::ReplayAndContinue { - AgentStatus::NotStarted - } else if outcome.status == "max_steps" { - AgentStatus::MaxSteps - } else { - AgentStatus::Completed - } -} - -fn failure_result( - request: &PlaybackRequest, - launch: Option<&LaunchSpec>, - plan: &AdapterPlan, - run_id: String, - state_dir: std::path::PathBuf, - output_dir: std::path::PathBuf, - error: &ReplayError, -) -> ReplayResult { - let artifacts = existing_artifacts(request, &output_dir); - let replay_completed = artifacts.iter().any(|artifact| { - matches!( - artifact.role.as_str(), - "reconstructed_native_trajectory" | "continued_native_trajectory" - ) - }); - ReplayResult { - schema_version: RESULT_SCHEMA_VERSION, - phase: if replay_completed { - ReplayPhase::Replayed - } else { - ReplayPhase::Prepared - }, - quality: ReplayQuality::Verified, - agent_status: if request.mode == ReplayMode::ReplayAndContinue && replay_completed { - AgentStatus::Failed - } else { - AgentStatus::NotStarted - }, - run_id, - agent: agent_result(request, launch), - after_step: plan.after_step(), - replayed_tool_calls: 0, - prefix_model_turns: plan.prefix_model_turns(), - continued_steps: 0, - state_dir, - output_dir: output_dir.clone(), - artifacts, - failure: Some(ReplayFailure { - category: error.kind.category().into(), - message: error.to_string(), - }), - retryable: error.kind.retryable(), - metadata: Value::Null, - } -} - -fn write_next_action_comparison( - request: &PlaybackRequest, - plan: &AdapterPlan, - outcome: &ReplayOutcome, - output_dir: &Path, -) -> Result<(), ReplayError> { - let (Some(original), Some(continued_path)) = - (plan.original_next_action(), &outcome.continued_path) - else { - return Ok(()); - }; - let mut continued_request = request.clone(); - continued_request.trajectory = continued_path.clone(); - let continued_plan = build_plan(&continued_request)?; - let Some(replayed) = continued_plan.original_next_action() else { - return Ok(()); - }; - write_next_action( - &output_dir.join("next-action-comparison.json"), - original, - replayed, - outcome - .metadata - .pointer("/boundary_user_prompt/injected") - .and_then(Value::as_bool) - .unwrap_or(false), - ) -} - -fn validate(request: &PlaybackRequest) -> Result<(), ReplayError> { - if request.after_step == 0 { - return Err(ReplayError::configuration( - "after_step must be a positive complete batch ordinal", - )); - } - if request.max_steps == Some(0) { - return Err(ReplayError::configuration("max_steps must be positive")); - } - if request - .boundary_user_prompt - .as_deref() - .is_some_and(|prompt| !prompt.is_empty() && prompt.trim().is_empty()) - { - return Err(ReplayError::configuration( - "boundary_user_prompt must contain a non-whitespace character", - )); - } - if !request.workspace.is_dir() { - return Err(ReplayError::new( - ReplayErrorKind::Workspace, - format!( - "workspace is not a directory: {}", - request.workspace.display() - ), - )); - } - if !request.trajectory.is_file() { - return Err(ReplayError::configuration(format!( - "trajectory does not exist: {}", - request.trajectory.display() - ))); - } - if let Some(run_id) = &request.run_id - && (run_id.is_empty() - || !run_id.chars().all(|character| { - character.is_ascii_alphanumeric() || matches!(character, '-' | '_') - })) - { - return Err(ReplayError::configuration( - "run_id may contain only letters, digits, '-' and '_'", - )); - } - if let Some(session_id) = &request.session_id - && (session_id.is_empty() - || session_id - .chars() - .any(|character| matches!(character, '\0' | '\r' | '\n'))) - { - return Err(ReplayError::configuration( - "session_id must be non-empty and contain no NUL, CR, or LF", - )); - } - if request.agent != AgentKind::ClaudeCode && !request.disallowed_tools.is_empty() { - return Err(ReplayError::configuration( - "disallowed_tools is supported only for claude-code", - )); - } - let mut seen = BTreeSet::new(); - for tool in &request.disallowed_tools { - if tool.is_empty() - || !tool.chars().all(|character| { - character.is_ascii_alphanumeric() || matches!(character, '_' | '-') - }) - { - return Err(ReplayError::configuration(format!( - "invalid disallowed tool name {tool:?}" - ))); - } - if !seen.insert(tool) { - return Err(ReplayError::configuration(format!( - "duplicate disallowed tool {tool:?}" - ))); - } - } - Ok(()) -} - -fn validate_step_budget( - mode: ReplayMode, - max_steps: Option, - prefix_steps: usize, -) -> Result<(), ReplayError> { - let Some(max_steps) = max_steps else { - return Ok(()); - }; - match mode { - ReplayMode::PrepareOnly => Ok(()), - ReplayMode::ReplayOnly if max_steps < prefix_steps => { - Err(ReplayError::configuration(format!( - "max_steps {max_steps} is smaller than the selected replay prefix of {prefix_steps} steps" - ))) - } - ReplayMode::ReplayAndContinue if max_steps <= prefix_steps => { - Err(ReplayError::configuration(format!( - "max_steps {max_steps} leaves no live step after the selected replay prefix of {prefix_steps} steps" - ))) - } - _ => Ok(()), - } -} - -fn absolute_or_current(path: &Path) -> Result { - if path.is_absolute() { - Ok(path.to_path_buf()) - } else { - Ok(std::env::current_dir() - .replay_context(ReplayErrorKind::Configuration, "read current directory")? - .join(path)) - } -} - -fn location_hint(path: &Path, run_id: &str) -> std::path::PathBuf { - if path.is_absolute() { - path.join(run_id) - } else { - std::env::current_dir() - .map(|cwd| cwd.join(path).join(run_id)) - .unwrap_or_else(|_| path.join(run_id)) - } -} - -fn read_comparison(path: &Path) -> Vec { - fs::read(path) - .ok() - .and_then(|bytes| serde_json::from_slice(&bytes).ok()) - .unwrap_or_default() -} - -fn agent_result(request: &PlaybackRequest, launch: Option<&LaunchSpec>) -> AgentResult { - AgentResult { - kind: request.agent.as_str().into(), - version: launch - .map(|launch| launch.version.clone()) - .unwrap_or_else(|| request.agent.supported_version().into()), - entrypoint: launch.map(|launch| launch.entrypoint.clone()), - launch_source: launch - .map(|launch| launch.source.clone()) - .unwrap_or_else(|| "prepare_only".into()), - disallowed_tools: request.disallowed_tools.clone(), - } -} - -fn artifacts( - request: &PlaybackRequest, - outcome: &ReplayOutcome, - output_dir: &Path, -) -> Vec { - let mut artifacts = vec![ - artifact( - "playback_plan", - "sandbox-playback/plan-v1", - output_dir.join("manifest.json"), - ), - artifact( - "replay_events", - "sandbox-playback/events-v1", - output_dir.join("replay-events.jsonl"), - ), - artifact( - "replay_summary", - "sandbox-playback/summary-v1", - output_dir.join("replay-summary.json"), - ), - ]; - let native_format = match request.agent { - AgentKind::ClaudeCode => "claude-code/native-jsonl-2.1.220", - AgentKind::Codex => "codex/native-jsonl-0.149.0", - AgentKind::MiniSweAgent => "mini-swe-agent/native-json-2.4.6", - AgentKind::Openhands => "openhands/native-json-0.53.0", - AgentKind::Opencode => "opencode/native-events-jsonl-1.17.7", - AgentKind::PiAgent => "pi-agent/native-events-jsonl-0.83.0", - AgentKind::SweAgent => "swe-agent/native-traj-1.1.0", - }; - let prepared_path = prepared_native_path(request.agent, output_dir); - if prepared_path.is_file() { - artifacts.push(artifact( - "prepared_native_prefix", - native_format, - prepared_path.clone(), - )); - } - if request.agent == AgentKind::Opencode { - let path = output_dir.join("native/opencode-session.json"); - if path.is_file() { - artifacts.push(artifact( - "native_session_export", - "opencode/session-export-v1", - path, - )); - } - } - if let Some(path) = &outcome.reconstructed_path - && path != &prepared_path - { - artifacts.push(artifact( - "reconstructed_native_trajectory", - native_format, - path.clone(), - )); - } - if let Some(path) = &outcome.continued_path { - artifacts.push(artifact( - "continued_native_trajectory", - native_format, - path.clone(), - )); - } - for (role, format, name) in [ - ( - "observation_comparison", - "sandbox-playback/comparison-v1", - "observation-comparison.json", - ), - ( - "next_action_comparison", - "sandbox-playback/next-action-v2", - "next-action-comparison.json", - ), - ] { - let path = output_dir.join(name); - if path.is_file() { - artifacts.push(artifact(role, format, path)); - } - } - if output_dir.join("logs").is_dir() { - artifacts.push(artifact( - "agent_logs", - "sandbox-playback/log-directory-v1", - output_dir.join("logs"), - )); - } - artifacts -} - -fn existing_artifacts(request: &PlaybackRequest, output_dir: &Path) -> Vec { - let mut artifacts = Vec::new(); - for (role, format, path) in [ - ( - "playback_plan", - "sandbox-playback/plan-v1", - output_dir.join("manifest.json"), - ), - ( - "replay_events", - "sandbox-playback/events-v1", - output_dir.join("replay-events.jsonl"), - ), - ( - "replay_summary", - "sandbox-playback/summary-v1", - output_dir.join("replay-summary.json"), - ), - ( - "observation_comparison", - "sandbox-playback/comparison-v1", - output_dir.join("observation-comparison.json"), - ), - ( - "next_action_comparison", - "sandbox-playback/next-action-v2", - output_dir.join("next-action-comparison.json"), - ), - ] { - if path.is_file() { - artifacts.push(artifact(role, format, path)); - } - } - - let native_format = match request.agent { - AgentKind::ClaudeCode => "claude-code/native-jsonl-2.1.220", - AgentKind::Codex => "codex/native-jsonl-0.149.0", - AgentKind::MiniSweAgent => "mini-swe-agent/native-json-2.4.6", - AgentKind::Openhands => "openhands/native-json-0.53.0", - AgentKind::Opencode => "opencode/native-events-jsonl-1.17.7", - AgentKind::PiAgent => "pi-agent/native-events-jsonl-0.83.0", - AgentKind::SweAgent => "swe-agent/native-traj-1.1.0", - }; - let native_paths: &[(&str, &str)] = match request.agent { - AgentKind::ClaudeCode => &[ - ("prepared_native_prefix", "native/prepared-prefix.jsonl"), - ( - "reconstructed_native_trajectory", - "native/reconstructed-prefix.jsonl", - ), - ( - "continued_native_trajectory", - "native/continued-session.jsonl", - ), - ], - AgentKind::Codex | AgentKind::Opencode => &[ - ("prepared_native_prefix", "native/prepared-prefix.jsonl"), - ( - "reconstructed_native_trajectory", - "native/reconstructed-trajectory.jsonl", - ), - ( - "continued_native_trajectory", - "native/continued-trajectory.jsonl", - ), - ], - AgentKind::MiniSweAgent => &[ - ("prepared_native_prefix", "native/prepared-prefix.json"), - ( - "reconstructed_native_trajectory", - "native/reconstructed-trajectory.json", - ), - ( - "continued_native_trajectory", - "native/continued-trajectory.json", - ), - ], - AgentKind::Openhands => &[ - ( - "prepared_native_prefix", - "native/prepared-replay-events.json", - ), - ( - "reconstructed_native_trajectory", - "native/reconstructed-trajectory.json", - ), - ( - "continued_native_trajectory", - "native/continued-trajectory.json", - ), - ], - AgentKind::PiAgent => &[ - ("prepared_native_prefix", "native/prepared-prefix.jsonl"), - ( - "reconstructed_native_trajectory", - "native/reconstructed-events.jsonl", - ), - ( - "continued_native_trajectory", - "native/continued-events.jsonl", - ), - ], - AgentKind::SweAgent => &[ - ("prepared_native_prefix", "native/prepared-prefix.traj"), - ( - "reconstructed_native_trajectory", - "native/reconstructed-trajectory.traj", - ), - ( - "continued_native_trajectory", - "native/continued-trajectory.traj", - ), - ], - }; - for (role, relative) in native_paths { - let path = output_dir.join(relative); - if path.is_file() { - artifacts.push(artifact(role, native_format, path)); - } - } - if request.agent == AgentKind::Opencode { - let path = output_dir.join("native/opencode-session.json"); - if path.is_file() { - artifacts.push(artifact( - "native_session_export", - "opencode/session-export-v1", - path, - )); - } - } - if output_dir.join("logs").is_dir() { - artifacts.push(artifact( - "agent_logs", - "sandbox-playback/log-directory-v1", - output_dir.join("logs"), - )); - } - artifacts -} - -fn prepared_native_path(agent: AgentKind, output_dir: &Path) -> std::path::PathBuf { - output_dir.join(match agent { - AgentKind::ClaudeCode => "native/prepared-prefix.jsonl", - AgentKind::Codex | AgentKind::Opencode => "native/prepared-prefix.jsonl", - AgentKind::MiniSweAgent => "native/prepared-prefix.json", - AgentKind::Openhands => "native/prepared-replay-events.json", - AgentKind::PiAgent => "native/prepared-prefix.jsonl", - AgentKind::SweAgent => "native/prepared-prefix.traj", - }) -} - -fn artifact(role: &str, format: &str, path: std::path::PathBuf) -> Artifact { - Artifact { - role: role.into(), - format: format.into(), - path, - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::collections::BTreeMap; - - #[test] - fn validation_errors_keep_resolved_run_locations() { - let request = PlaybackRequest { - agent: AgentKind::ClaudeCode, - trajectory: std::path::PathBuf::from("/missing/trajectory.jsonl"), - after_step: 1, - workspace: std::path::PathBuf::from("/missing/workspace"), - state_dir: std::path::PathBuf::from("/state"), - output_dir: std::path::PathBuf::from("/output"), - agent_entrypoint: None, - agent_runtime: None, - disallowed_tools: Vec::new(), - trajectory_assets: None, - session_id: None, - max_steps: None, - mode: ReplayMode::PrepareOnly, - allow_stale_observations: false, - run_id: Some("replay-1".into()), - disable_thinking: false, - boundary_user_prompt: None, - }; - - let error = execute(request).unwrap_err(); - let (run_id, state_dir, output_dir) = error.locations().unwrap(); - assert_eq!(run_id, "replay-1"); - assert_eq!(state_dir, Path::new("/state/replay-1")); - assert_eq!(output_dir, Path::new("/output/replay-1")); - } - - #[test] - fn validation_rejects_a_whitespace_only_boundary_prompt() { - let request = PlaybackRequest { - agent: AgentKind::ClaudeCode, - trajectory: std::path::PathBuf::from("/missing/trajectory.jsonl"), - after_step: 1, - workspace: std::path::PathBuf::from("/missing/workspace"), - state_dir: std::path::PathBuf::from("/state"), - output_dir: std::path::PathBuf::from("/output"), - agent_entrypoint: None, - agent_runtime: None, - disallowed_tools: Vec::new(), - trajectory_assets: None, - session_id: None, - max_steps: None, - mode: ReplayMode::ReplayAndContinue, - allow_stale_observations: false, - run_id: Some("prompt-validation".into()), - disable_thinking: false, - boundary_user_prompt: Some(" \n\t".into()), - }; - - let error = execute(request).unwrap_err(); - - assert!( - error - .to_string() - .contains("boundary_user_prompt must contain a non-whitespace character") - ); - } - - #[test] - fn validation_does_not_consume_output_run_id() { - let temporary = tempfile::tempdir().unwrap(); - let workspace = temporary.path().join("workspace"); - fs::create_dir(&workspace).unwrap(); - let trajectory = temporary.path().join("invalid-openhands.json"); - fs::write(&trajectory, "{}").unwrap(); - let output_root = temporary.path().join("output"); - let request = PlaybackRequest { - agent: AgentKind::Openhands, - trajectory, - after_step: 1, - workspace, - state_dir: temporary.path().join("state"), - output_dir: output_root.clone(), - agent_entrypoint: None, - agent_runtime: None, - disallowed_tools: Vec::new(), - trajectory_assets: None, - session_id: None, - max_steps: None, - mode: ReplayMode::PrepareOnly, - allow_stale_observations: false, - run_id: Some("reserved-run".into()), - disable_thinking: false, - boundary_user_prompt: None, - }; - - let error = execute(request).unwrap_err(); - - assert_eq!(error.kind, ReplayErrorKind::Trajectory); - assert!(!output_root.join("reserved-run").exists()); - } - - #[test] - fn stale_source_observations_degrade_result_quality() { - let outcome = ReplayOutcome { - status: "replayed".into(), - reconstructed_path: None, - continued_path: None, - observations: vec![crate::model::FreshObservation { - call_id: "call-1".into(), - content: Value::Null, - is_error: false, - return_code: Some(0), - duration_ms: 0, - truncated: false, - metadata: BTreeMap::from([( - "degradation_reason".into(), - json!("stale_source_observation"), - )]), - }], - continued_steps: 0, - metadata: Value::Null, - }; - - assert_eq!(quality_for_outcome(&outcome), ReplayQuality::Degraded); - } - - #[test] - fn total_step_budget_is_checked_before_replay() { - assert!(validate_step_budget(ReplayMode::ReplayOnly, Some(3), 3).is_ok()); - assert!(validate_step_budget(ReplayMode::ReplayOnly, Some(2), 3).is_err()); - assert!(validate_step_budget(ReplayMode::ReplayAndContinue, Some(3), 3).is_err()); - assert!(validate_step_budget(ReplayMode::ReplayAndContinue, Some(4), 3).is_ok()); - assert!(validate_step_budget(ReplayMode::PrepareOnly, Some(1), 3).is_ok()); - } -} diff --git a/crates/persisting-replay/src/error.rs b/crates/persisting-replay/src/error.rs deleted file mode 100644 index ce2ed7072..000000000 --- a/crates/persisting-replay/src/error.rs +++ /dev/null @@ -1,203 +0,0 @@ -use std::fmt; -use std::path::{Path, PathBuf}; - -/// Stable error categories retained from SandboxReplay's public protocol. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ReplayErrorKind { - Configuration, - Trajectory, - UnsupportedAgent, - UnsupportedVersion, - Workspace, - Executor, - AmbiguousExecution, - Continuation, - ModelRateLimit, - ModelInternal, - ModelConnection, - Internal, -} - -impl ReplayErrorKind { - pub fn category(self) -> &'static str { - match self { - Self::Configuration => "configuration_error", - Self::Trajectory => "trajectory_error", - Self::UnsupportedAgent => "unsupported_agent", - Self::UnsupportedVersion => "unsupported_version", - Self::Workspace => "workspace_error", - Self::Executor => "executor_error", - Self::AmbiguousExecution => "ambiguous_execution", - Self::Continuation => "continuation_error", - Self::ModelRateLimit => "model_rate_limit", - Self::ModelInternal => "model_internal_error", - Self::ModelConnection => "model_connection_error", - Self::Internal => "internal_error", - } - } - - pub fn exit_code(self) -> i32 { - match self { - Self::Configuration => 2, - Self::Trajectory => 10, - Self::UnsupportedAgent | Self::UnsupportedVersion => 11, - Self::Workspace => 20, - Self::Executor => 30, - Self::AmbiguousExecution => 31, - Self::Continuation - | Self::ModelRateLimit - | Self::ModelInternal - | Self::ModelConnection => 40, - Self::Internal => 50, - } - } - - pub fn retryable(self) -> bool { - matches!( - self, - Self::ModelRateLimit | Self::ModelInternal | Self::ModelConnection - ) - } -} - -#[derive(Debug)] -pub struct ReplayError { - pub kind: ReplayErrorKind, - pub message: String, - pub locations: Option, -} - -#[derive(Debug, Clone)] -pub struct ReplayLocations { - pub run_id: String, - pub state_dir: PathBuf, - pub output_dir: PathBuf, -} - -impl ReplayError { - pub fn new(kind: ReplayErrorKind, message: impl Into) -> Self { - Self { - kind, - message: message.into(), - locations: None, - } - } - - pub fn configuration(message: impl Into) -> Self { - Self::new(ReplayErrorKind::Configuration, message) - } - - pub fn trajectory(message: impl Into) -> Self { - Self::new(ReplayErrorKind::Trajectory, message) - } - - pub fn continuation(message: impl Into) -> Self { - Self::new(ReplayErrorKind::Continuation, message) - } - - pub fn classify_continuation(message: impl Into, output: &str) -> Self { - let message = message.into(); - let lowered = output.to_ascii_lowercase(); - let kind = if ["rate limit", "too many requests", "status 429"] - .iter() - .any(|needle| lowered.contains(needle)) - { - ReplayErrorKind::ModelRateLimit - } else if [ - "internal server error", - "status 500", - "status 502", - "status 503", - ] - .iter() - .any(|needle| lowered.contains(needle)) - { - ReplayErrorKind::ModelInternal - } else if [ - "connection refused", - "connection reset", - "connection timed out", - "could not resolve host", - "connecterror", - ] - .iter() - .any(|needle| lowered.contains(needle)) - { - ReplayErrorKind::ModelConnection - } else { - ReplayErrorKind::Continuation - }; - Self::new(kind, message) - } - - pub fn exit_code(&self) -> i32 { - self.kind.exit_code() - } - - pub fn with_locations( - mut self, - run_id: impl Into, - state_dir: PathBuf, - output_dir: PathBuf, - ) -> Self { - self.locations = Some(ReplayLocations { - run_id: run_id.into(), - state_dir, - output_dir, - }); - self - } - - pub fn with_default_locations( - mut self, - run_id: impl Into, - state_dir: PathBuf, - output_dir: PathBuf, - ) -> Self { - if self.locations.is_none() { - self.locations = Some(ReplayLocations { - run_id: run_id.into(), - state_dir, - output_dir, - }); - } - self - } - - pub fn locations(&self) -> Option<(&str, &Path, &Path)> { - self.locations.as_ref().map(|locations| { - ( - locations.run_id.as_str(), - locations.state_dir.as_path(), - locations.output_dir.as_path(), - ) - }) - } -} - -impl fmt::Display for ReplayError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str(&self.message) - } -} - -impl std::error::Error for ReplayError {} - -pub(crate) trait ResultExt { - fn replay_context( - self, - kind: ReplayErrorKind, - message: impl Into, - ) -> Result; -} - -impl ResultExt for Result { - fn replay_context( - self, - kind: ReplayErrorKind, - message: impl Into, - ) -> Result { - let message = message.into(); - self.map_err(|error| ReplayError::new(kind, format!("{message}: {error}"))) - } -} diff --git a/crates/persisting-replay/src/io.rs b/crates/persisting-replay/src/io.rs deleted file mode 100644 index f1915632e..000000000 --- a/crates/persisting-replay/src/io.rs +++ /dev/null @@ -1,107 +0,0 @@ -use std::fs::{self, File, OpenOptions}; -use std::io::{Read, Write}; -use std::path::{Path, PathBuf}; - -use sha2::{Digest, Sha256}; - -use crate::error::{ReplayError, ReplayErrorKind, ResultExt}; - -pub fn read_regular_file(path: &Path) -> Result, ReplayError> { - let metadata = fs::symlink_metadata(path).replay_context( - ReplayErrorKind::Configuration, - format!("inspect {}", path.display()), - )?; - if !metadata.file_type().is_file() || metadata.file_type().is_symlink() { - return Err(ReplayError::configuration(format!( - "input must be a regular file: {}", - path.display() - ))); - } - const MAX_BYTES: u64 = 256 * 1024 * 1024; - if metadata.len() > MAX_BYTES { - return Err(ReplayError::configuration(format!( - "input exceeds {MAX_BYTES} bytes: {}", - path.display() - ))); - } - let mut file = File::open(path).replay_context( - ReplayErrorKind::Configuration, - format!("open {}", path.display()), - )?; - let mut bytes = Vec::with_capacity(metadata.len() as usize); - file.read_to_end(&mut bytes).replay_context( - ReplayErrorKind::Configuration, - format!("read {}", path.display()), - )?; - if bytes.len() as u64 != metadata.len() { - return Err(ReplayError::configuration(format!( - "input changed while being read: {}", - path.display() - ))); - } - Ok(bytes) -} - -pub fn sha256(bytes: &[u8]) -> String { - Sha256::digest(bytes) - .iter() - .map(|byte| format!("{byte:02x}")) - .collect() -} - -pub fn atomic_write(path: &Path, bytes: &[u8]) -> Result<(), ReplayError> { - let parent = path - .parent() - .ok_or_else(|| ReplayError::configuration("output path has no parent"))?; - fs::create_dir_all(parent).replay_context( - ReplayErrorKind::Executor, - format!("create {}", parent.display()), - )?; - let temporary = parent.join(format!( - ".{}.{}.tmp", - path.file_name() - .and_then(|name| name.to_str()) - .unwrap_or("output"), - uuid::Uuid::new_v4().simple() - )); - let mut options = OpenOptions::new(); - options.create_new(true).write(true); - #[cfg(unix)] - { - use std::os::unix::fs::OpenOptionsExt; - options.mode(0o600); - } - let mut file = options.open(&temporary).replay_context( - ReplayErrorKind::Executor, - format!("create {}", temporary.display()), - )?; - file.write_all(bytes) - .and_then(|_| file.sync_all()) - .replay_context( - ReplayErrorKind::Executor, - format!("write {}", temporary.display()), - )?; - fs::rename(&temporary, path).replay_context( - ReplayErrorKind::Executor, - format!("replace {}", path.display()), - )?; - if let Ok(directory) = File::open(parent) { - let _ = directory.sync_all(); - } - Ok(()) -} - -pub fn atomic_write_json(path: &Path, value: &impl serde::Serialize) -> Result<(), ReplayError> { - let mut bytes = serde_json::to_vec_pretty(value) - .replay_context(ReplayErrorKind::Internal, "serialize replay artifact")?; - bytes.push(b'\n'); - atomic_write(path, &bytes) -} - -pub fn canonicalize( - path: &Path, - kind: ReplayErrorKind, - label: &str, -) -> Result { - fs::canonicalize(path).replay_context(kind, format!("resolve {label} {}", path.display())) -} diff --git a/crates/persisting-replay/src/journal.rs b/crates/persisting-replay/src/journal.rs deleted file mode 100644 index 3316aff4b..000000000 --- a/crates/persisting-replay/src/journal.rs +++ /dev/null @@ -1,225 +0,0 @@ -use std::collections::BTreeSet; -use std::fs::{self, File, OpenOptions}; -use std::io::{BufRead, BufReader, Write}; -use std::path::{Path, PathBuf}; - -use fs2::FileExt; -use serde_json::{Map, Value}; - -use crate::error::{ReplayError, ReplayErrorKind, ResultExt}; - -pub struct Journal { - lock: File, - file: File, - pub path: PathBuf, -} - -impl Journal { - pub fn open(state_dir: &Path) -> Result { - fs::create_dir_all(state_dir).replay_context( - ReplayErrorKind::Executor, - format!("create replay state {}", state_dir.display()), - )?; - let lock_path = state_dir.join("run.lock"); - let lock = OpenOptions::new() - .create(true) - .truncate(false) - .read(true) - .write(true) - .open(&lock_path) - .replay_context( - ReplayErrorKind::Executor, - format!("open {}", lock_path.display()), - )?; - lock.try_lock_exclusive().map_err(|error| { - ReplayError::new( - ReplayErrorKind::AmbiguousExecution, - format!( - "another replay process owns {}: {error}", - lock_path.display() - ), - ) - })?; - let path = state_dir.join("replay-events.jsonl"); - if let Some(call_id) = Self::find_ambiguous(&path)? { - return Err(ReplayError::new( - ReplayErrorKind::AmbiguousExecution, - format!( - "state contains an uncertain started tool call {call_id:?}; use a new sandbox and run-id to replay from T1" - ), - )); - } - let file = OpenOptions::new() - .create(true) - .append(true) - .open(&path) - .replay_context( - ReplayErrorKind::Executor, - format!("open {}", path.display()), - )?; - Ok(Self { lock, file, path }) - } - - pub fn append( - &mut self, - event: &str, - fields: impl IntoIterator, - ) -> Result<(), ReplayError> { - let mut object = Map::new(); - object.insert("event".into(), Value::String(event.into())); - object.insert( - "timestamp_ns".into(), - Value::Number( - chrono::Utc::now() - .timestamp_nanos_opt() - .unwrap_or_default() - .into(), - ), - ); - object.extend(fields); - serde_json::to_writer(&mut self.file, &Value::Object(object)) - .replay_context(ReplayErrorKind::Executor, "append replay journal")?; - self.file - .write_all(b"\n") - .and_then(|_| self.file.sync_data()) - .replay_context(ReplayErrorKind::Executor, "flush replay journal") - } - - pub fn find_ambiguous(path: &Path) -> Result, ReplayError> { - if !path.exists() { - return Ok(None); - } - let file = File::open(path).replay_context( - ReplayErrorKind::AmbiguousExecution, - format!("read {}", path.display()), - )?; - let mut started_since_terminal = BTreeSet::new(); - for line in BufReader::new(file).lines() { - let line = - line.replay_context(ReplayErrorKind::AmbiguousExecution, "read replay journal")?; - let event: Value = serde_json::from_str(&line) - .replay_context(ReplayErrorKind::AmbiguousExecution, "parse replay journal")?; - match event.get("event").and_then(Value::as_str) { - Some("tool_started") => { - if let Some(call_id) = event.get("call_id").and_then(Value::as_str) { - started_since_terminal.insert(call_id.to_owned()); - } - } - Some("run_finished" | "run_failed") => started_since_terminal.clear(), - _ => {} - } - } - Ok(started_since_terminal.into_iter().next()) - } -} - -impl Drop for Journal { - fn drop(&mut self) { - let _ = self.file.sync_data(); - let _ = FileExt::unlock(&self.lock); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn write_events(path: &Path, events: &[Value]) { - let contents = events - .iter() - .map(Value::to_string) - .collect::>() - .join("\n"); - fs::write(path, format!("{contents}\n")).unwrap(); - } - - #[test] - fn finished_tool_without_terminal_run_is_ambiguous() { - let temporary = tempfile::tempdir().unwrap(); - let path = temporary.path().join("replay-events.jsonl"); - write_events( - &path, - &[ - serde_json::json!({"event": "run_started"}), - serde_json::json!({"event": "tool_started", "call_id": "same-call"}), - serde_json::json!({"event": "tool_finished", "call_id": "same-call"}), - ], - ); - - assert_eq!( - Journal::find_ambiguous(&path).unwrap().as_deref(), - Some("same-call") - ); - } - - #[test] - fn repeated_call_id_after_a_terminal_run_remains_ambiguous() { - let temporary = tempfile::tempdir().unwrap(); - let path = temporary.path().join("replay-events.jsonl"); - write_events( - &path, - &[ - serde_json::json!({"event": "run_started"}), - serde_json::json!({"event": "tool_started", "call_id": "same-call"}), - serde_json::json!({"event": "tool_finished", "call_id": "same-call"}), - serde_json::json!({"event": "run_finished"}), - serde_json::json!({"event": "run_started"}), - serde_json::json!({"event": "tool_started", "call_id": "same-call"}), - ], - ); - - assert_eq!( - Journal::find_ambiguous(&path).unwrap().as_deref(), - Some("same-call") - ); - } - - #[test] - fn interruption_before_any_tool_starts_is_retryable() { - let temporary = tempfile::tempdir().unwrap(); - let path = temporary.path().join("replay-events.jsonl"); - write_events( - &path, - &[ - serde_json::json!({"event": "run_started"}), - serde_json::json!({"event": "plan_validated"}), - ], - ); - - assert_eq!(Journal::find_ambiguous(&path).unwrap(), None); - } - - #[test] - fn failed_run_is_terminal() { - let temporary = tempfile::tempdir().unwrap(); - let path = temporary.path().join("replay-events.jsonl"); - write_events( - &path, - &[ - serde_json::json!({"event": "run_started"}), - serde_json::json!({"event": "tool_started", "call_id": "call-1"}), - serde_json::json!({"event": "run_failed"}), - ], - ); - - assert_eq!(Journal::find_ambiguous(&path).unwrap(), None); - } - - #[test] - fn opening_a_journal_rejects_ambiguous_state_while_holding_the_lock() { - let temporary = tempfile::tempdir().unwrap(); - let path = temporary.path().join("replay-events.jsonl"); - write_events( - &path, - &[ - serde_json::json!({"event": "run_started"}), - serde_json::json!({"event": "tool_started", "call_id": "call-1"}), - ], - ); - - let error = Journal::open(temporary.path()).err().unwrap(); - - assert_eq!(error.kind, ReplayErrorKind::AmbiguousExecution); - assert!(error.message.contains("call-1")); - } -} diff --git a/crates/persisting-replay/src/lib.rs b/crates/persisting-replay/src/lib.rs deleted file mode 100644 index 664e2497c..000000000 --- a/crates/persisting-replay/src/lib.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! Rust implementation of agent-native sandbox replay. -//! -//! The default execution model assumes pVisor is already running inside a -//! fresh sandbox. Replay therefore touches only the selected workspace and -//! connects live Agents directly to their configured model endpoint. -//! Claude Code alone uses a replay-local protocol bridge to remove Resume Transport messages. - -mod adapter; -mod claude_bridge; -mod claude_resume; -mod codex_bridge; -mod comparison; -mod config; -mod engine; -mod error; -mod io; -mod journal; -mod model; -pub(crate) mod opencode_bridge; -mod process; - -pub use config::{ - OverlayFsConfig, OverlayNetConfig, ReplayConfig, ReplayToml, RunConfig, request_from_json, -}; -pub use engine::execute; -pub use error::{ReplayError, ReplayErrorKind}; -pub use model::{ - AgentKind, AgentStatus, ExecutionReport, PlaybackRequest, RESULT_SCHEMA_VERSION, ReplayFailure, - ReplayMode, ReplayPhase, ReplayQuality, ReplayResult, -}; diff --git a/crates/persisting-replay/src/model.rs b/crates/persisting-replay/src/model.rs deleted file mode 100644 index b06f2668a..000000000 --- a/crates/persisting-replay/src/model.rs +++ /dev/null @@ -1,411 +0,0 @@ -use std::collections::BTreeMap; -use std::path::PathBuf; -use std::str::FromStr; - -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -pub const PLAN_SCHEMA_VERSION: &str = "sandbox-replay.plan/v1"; -pub const REQUEST_SCHEMA_VERSION: &str = "sandbox-playback.request/v1"; -pub const RESULT_SCHEMA_VERSION: &str = "sandbox-playback.result/v3"; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum AgentKind { - ClaudeCode, - Codex, - MiniSweAgent, - Openhands, - Opencode, - PiAgent, - SweAgent, -} - -impl AgentKind { - pub fn as_str(self) -> &'static str { - match self { - Self::ClaudeCode => "claude-code", - Self::Codex => "codex", - Self::MiniSweAgent => "mini-swe-agent", - Self::Openhands => "openhands", - Self::Opencode => "opencode", - Self::PiAgent => "pi-agent", - Self::SweAgent => "swe-agent", - } - } - - pub fn supported_version(self) -> &'static str { - match self { - Self::ClaudeCode => "2.1.220", - Self::Codex => "0.149.0", - Self::MiniSweAgent => "2.4.6", - Self::Openhands => "0.53.0", - Self::Opencode => "1.17.7", - Self::PiAgent => "0.83.0", - Self::SweAgent => "1.1.0", - } - } - - pub fn profile(self) -> &'static str { - match self { - Self::ClaudeCode => "claude-code/2.1.220/native-resume-v1", - Self::Codex => "codex/0.149.0/native-responses-jsonl-v1", - Self::MiniSweAgent => "mini-swe-agent/2.4.6/native-messages-v1", - Self::Openhands => "openhands/0.53.0/native-replay-v1", - Self::Opencode => "opencode/1.17.7/native-events-jsonl-v1", - Self::PiAgent => "pi-agent/0.83.0/native-rpc-events-v1", - Self::SweAgent => "swe-agent/1.1.0/replay-then-live-v1", - } - } -} - -impl FromStr for AgentKind { - type Err = String; - - fn from_str(value: &str) -> Result { - match value { - "claude-code" => Ok(Self::ClaudeCode), - "codex" => Ok(Self::Codex), - "mini-swe-agent" => Ok(Self::MiniSweAgent), - "openhands" => Ok(Self::Openhands), - "opencode" => Ok(Self::Opencode), - "pi-agent" => Ok(Self::PiAgent), - "swe-agent" => Ok(Self::SweAgent), - other => Err(format!( - "unsupported agent {other:?}; expected claude-code, codex, mini-swe-agent, openhands, opencode, pi-agent, or swe-agent" - )), - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum ReplayMode { - PrepareOnly, - ReplayOnly, - ReplayAndContinue, -} - -#[derive(Debug, Clone)] -pub struct PlaybackRequest { - pub agent: AgentKind, - pub trajectory: PathBuf, - pub after_step: usize, - pub workspace: PathBuf, - pub state_dir: PathBuf, - pub output_dir: PathBuf, - pub agent_entrypoint: Option, - pub agent_runtime: Option, - pub disallowed_tools: Vec, - pub trajectory_assets: Option, - pub session_id: Option, - pub max_steps: Option, - pub mode: ReplayMode, - pub allow_stale_observations: bool, - pub run_id: Option, - pub disable_thinking: bool, - pub boundary_user_prompt: Option, -} - -impl PlaybackRequest { - pub fn boundary_user_prompt(&self) -> Option<&str> { - self.boundary_user_prompt - .as_deref() - .filter(|prompt| !prompt.is_empty()) - } -} - -#[derive(Debug, Clone, Serialize)] -pub struct ToolCall { - pub ordinal: usize, - pub call_id: String, - pub name: String, - pub arguments: Value, - #[serde(skip)] - pub original_observation: Value, - #[serde(skip)] - pub original_is_error: bool, - #[serde(skip)] - pub native: Value, -} - -#[derive(Debug, Clone, Serialize)] -pub struct ToolBatch { - pub ordinal: usize, - pub native_locator: String, - pub tool_calls: Vec, - #[serde(skip)] - pub assistant_text: String, - #[serde(skip)] - pub native: Value, -} - -#[derive(Debug, Clone)] -pub(crate) struct ReplayPlan { - pub(crate) agent: AgentKind, - pub(crate) source_path: PathBuf, - pub(crate) source_sha256: String, - pub(crate) after_step: usize, - pub(crate) batches: Vec, - pub(crate) prefix_model_turns: usize, - pub(crate) native: Value, - pub(crate) original_next_action: Option, -} - -impl ReplayPlan { - pub fn calls(&self) -> impl Iterator { - self.batches - .iter() - .flat_map(|batch| batch.tool_calls.iter()) - } - - pub fn public_value(&self) -> Value { - serde_json::json!({ - "schema_version": PLAN_SCHEMA_VERSION, - "agent": { - "name": self.agent.as_str(), - "version": self.agent.supported_version(), - "profile": self.agent.profile(), - }, - "source": { - "path": self.source_path, - "sha256": self.source_sha256, - }, - "boundary": { - "after_step": self.after_step, - "complete_tool_batch": true, - "prefix_model_turns": self.prefix_model_turns, - "tool_calls": self.calls().count(), - }, - "batches": self.batches, - }) - } -} - -#[derive(Debug, Clone)] -pub(crate) enum AdapterPlan { - ClaudeCode(ReplayPlan), - Codex(ReplayPlan), - MiniSweAgent(ReplayPlan), - Openhands(ReplayPlan), - Opencode(ReplayPlan), - PiAgent(ReplayPlan), - SweAgent(ReplayPlan), -} - -impl AdapterPlan { - pub(crate) fn agent(&self) -> AgentKind { - self.plan().agent - } - - pub(crate) fn after_step(&self) -> usize { - self.plan().after_step - } - - pub(crate) fn prefix_model_turns(&self) -> usize { - self.plan().prefix_model_turns - } - - pub(crate) fn source_sha256(&self) -> &str { - &self.plan().source_sha256 - } - - pub(crate) fn calls(&self) -> impl Iterator { - self.plan().calls() - } - - pub(crate) fn public_value(&self) -> Value { - self.plan().public_value() - } - - pub(crate) fn original_next_action(&self) -> Option<&Value> { - self.plan().original_next_action.as_ref() - } - - fn plan(&self) -> &ReplayPlan { - match self { - Self::ClaudeCode(plan) - | Self::Codex(plan) - | Self::MiniSweAgent(plan) - | Self::Openhands(plan) - | Self::Opencode(plan) - | Self::PiAgent(plan) - | Self::SweAgent(plan) => plan, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FreshObservation { - pub call_id: String, - pub content: Value, - pub is_error: bool, - pub return_code: Option, - pub duration_ms: u128, - pub truncated: bool, - #[serde(default)] - pub metadata: BTreeMap, -} - -#[derive(Debug)] -pub struct ReplayOutcome { - pub status: String, - pub reconstructed_path: Option, - pub continued_path: Option, - pub observations: Vec, - pub continued_steps: usize, - pub metadata: Value, -} - -#[derive(Debug, Clone, Serialize)] -pub struct Artifact { - pub role: String, - pub format: String, - pub path: PathBuf, -} - -#[derive(Debug, Clone, Serialize)] -pub struct AgentResult { - #[serde(rename = "type")] - pub kind: String, - pub version: String, - pub entrypoint: Option, - pub launch_source: String, - pub disallowed_tools: Vec, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum ReplayPhase { - Prepared, - Replayed, - Continued, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum ReplayQuality { - Verified, - Degraded, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum AgentStatus { - Completed, - MaxSteps, - Failed, - NotStarted, -} - -#[derive(Debug, Clone, Serialize)] -pub struct ReplayFailure { - pub category: String, - pub message: String, -} - -#[derive(Debug, Clone, Serialize)] -pub struct ReplayResult { - pub schema_version: &'static str, - pub phase: ReplayPhase, - pub quality: ReplayQuality, - pub agent_status: AgentStatus, - pub run_id: String, - pub agent: AgentResult, - pub after_step: usize, - pub replayed_tool_calls: usize, - pub prefix_model_turns: usize, - pub continued_steps: usize, - pub state_dir: PathBuf, - pub output_dir: PathBuf, - pub artifacts: Vec, - pub failure: Option, - pub retryable: bool, - pub metadata: Value, -} - -#[derive(Debug)] -pub struct ExecutionReport { - pub result: ReplayResult, - pub exit_code: i32, -} - -#[cfg(test)] -mod tests { - use super::*; - - fn replay_plan(agent: AgentKind, marker: &str) -> ReplayPlan { - ReplayPlan { - agent, - source_path: PathBuf::from(format!("/{marker}")), - source_sha256: marker.into(), - after_step: 1, - batches: vec![ToolBatch { - ordinal: 1, - native_locator: marker.into(), - tool_calls: Vec::new(), - assistant_text: String::new(), - native: serde_json::json!({"private": marker}), - }], - prefix_model_turns: 1, - native: serde_json::json!({"private": marker}), - original_next_action: None, - } - } - - #[test] - fn adapter_plan_exposes_only_common_dispatch_fields() { - let plans = [ - AdapterPlan::ClaudeCode(replay_plan(AgentKind::ClaudeCode, "claude")), - AdapterPlan::Codex(replay_plan(AgentKind::Codex, "codex")), - AdapterPlan::MiniSweAgent(replay_plan(AgentKind::MiniSweAgent, "mini")), - AdapterPlan::Openhands(replay_plan(AgentKind::Openhands, "openhands")), - AdapterPlan::Opencode(replay_plan(AgentKind::Opencode, "opencode")), - AdapterPlan::PiAgent(replay_plan(AgentKind::PiAgent, "pi")), - AdapterPlan::SweAgent(replay_plan(AgentKind::SweAgent, "swe")), - ]; - - for plan in plans { - assert_eq!(plan.after_step(), 1); - assert_eq!(plan.prefix_model_turns(), 1); - assert_eq!(plan.calls().count(), 0); - assert_eq!(plan.public_value()["agent"]["name"], plan.agent().as_str()); - assert!(!plan.source_sha256().is_empty()); - } - } - - #[test] - fn v3_result_serializes_typed_execution_state() { - let result = ReplayResult { - schema_version: RESULT_SCHEMA_VERSION, - phase: ReplayPhase::Replayed, - quality: ReplayQuality::Degraded, - agent_status: AgentStatus::NotStarted, - run_id: "replay-1".into(), - agent: AgentResult { - kind: "claude-code".into(), - version: "2.1.220".into(), - entrypoint: None, - launch_source: "runtime_manifest".into(), - disallowed_tools: Vec::new(), - }, - after_step: 1, - replayed_tool_calls: 1, - prefix_model_turns: 1, - continued_steps: 0, - state_dir: PathBuf::from("/state/replay-1"), - output_dir: PathBuf::from("/output/replay-1"), - artifacts: Vec::new(), - failure: None, - retryable: false, - metadata: Value::Null, - }; - - let value = serde_json::to_value(result).unwrap(); - assert_eq!(value["schema_version"], "sandbox-playback.result/v3"); - assert_eq!(value["phase"], "replayed"); - assert_eq!(value["quality"], "degraded"); - assert_eq!(value["agent_status"], "not_started"); - assert_eq!(value["failure"], Value::Null); - } -} diff --git a/crates/persisting-replay/src/opencode_bridge.rs b/crates/persisting-replay/src/opencode_bridge.rs deleted file mode 100644 index 3dd1dc937..000000000 --- a/crates/persisting-replay/src/opencode_bridge.rs +++ /dev/null @@ -1,804 +0,0 @@ -//! OpenCode Responses API resume-transport bridge. -//! -//! `opencode run --session ` refuses to start without a message. The -//! SandboxReplay continuation therefore passes a unique transport nonce as -//! that message, and this bridge removes the nonce from every request before -//! forwarding it upstream, so the first live model request still ends exactly -//! at the replayed boundary observation. OpenCode may resend the full -//! conversation history (including the persisted nonce) on every request, so -//! the cleanup is exact-match and repeated. The bridge also pins sampling: -//! OpenCode never forwards `temperature`/`top_p` to the Responses API, so the -//! continuation would otherwise drift away from the recorded sampling. -//! -//! Responses bodies are streamed through unchanged apart from the JSON -//! rewrite: OpenCode treats a stalled stream as a dead connection and retries. - -use std::net::TcpListener; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex, mpsc}; -use std::thread::{self, JoinHandle}; -use std::time::Duration; - -use anyhow::Context; -use axum::Router; -use axum::body::{Body, Bytes}; -use axum::extract::{DefaultBodyLimit, State}; -use axum::http::HeaderMap; -use axum::http::{HeaderValue, StatusCode}; -use axum::response::Response; -use serde_json::Value; -use tokio::sync::oneshot; - -use crate::error::{ReplayError, ReplayErrorKind, ResultExt}; - -const BRIDGE_VERSION: &str = "sandbox-replay-opencode-responses-bridge/1"; -const START_TIMEOUT: Duration = Duration::from_secs(10); -const STOP_TIMEOUT: Duration = Duration::from_secs(5); -const MAX_BODY_BYTES: usize = 64 * 1024 * 1024; - -pub struct OpencodeBridgeHandle { - pub base_url: String, - api_key: String, - shared: Arc, - shutdown: Option>, - worker_done: Option>>, - worker: Option>, -} - -struct BridgeShared { - state: Mutex, - client: reqwest::Client, - upstream_origin: String, - upstream_api_key: String, - routing_session_id: String, - bridge_api_key: String, - strip_prompt: Option, - temperature: Option, - top_p: Option, - disable_thinking: bool, - cancelled: AtomicBool, -} - -#[derive(Default)] -struct BridgeState { - forwarded_requests: usize, - removed_transport_prompt: bool, - failed: bool, - failure: Option, -} - -impl BridgeState { - fn fail(&mut self, message: impl Into) { - self.failed = true; - if self.failure.is_none() { - self.failure = Some(message.into()); - } - } -} - -impl OpencodeBridgeHandle { - /// Start the bridge. `strip_prompt` is the transport nonce to remove - /// from every request; `None` keeps an explicit `boundary_user_prompt` - /// in the model input on purpose. - pub fn start( - routing_session_id: &str, - strip_prompt: Option, - temperature: Option, - top_p: Option, - disable_thinking: bool, - ) -> Result { - let upstream_base = first_nonempty_env(&["OPENAI_BASE_URL", "OPENAI_API_BASE"]) - .ok_or_else(|| { - ReplayError::configuration( - "OpenCode SandboxReplay bridge requires OPENAI_BASE_URL or OPENAI_API_BASE", - ) - })?; - let upstream_api_key = - first_nonempty_env(&["OPENAI_API_KEY", "LLM_API_KEY"]).ok_or_else(|| { - ReplayError::configuration( - "OpenCode SandboxReplay bridge requires OPENAI_API_KEY or LLM_API_KEY", - ) - })?; - let upstream_origin = url_origin(&upstream_base)?; - // Mirror the original API path prefix (for example "/v1"): OpenCode - // appends "/responses" to this base URL and the bridge forwards the - // resulting path verbatim, so dropping the prefix would 404 upstream. - let upstream_prefix = url_path_prefix(&upstream_base)?; - let bridge_api_key = format!("pvisor-sandbox-replay-{}", uuid::Uuid::new_v4().simple()); - let listener = TcpListener::bind(("127.0.0.1", 0)).replay_context( - ReplayErrorKind::Continuation, - "allocate OpenCode SandboxReplay bridge port", - )?; - let address = listener.local_addr().replay_context( - ReplayErrorKind::Continuation, - "read OpenCode SandboxReplay bridge address", - )?; - listener.set_nonblocking(true).replay_context( - ReplayErrorKind::Continuation, - "configure OpenCode SandboxReplay bridge listener", - )?; - let client = reqwest::Client::builder() - .no_proxy() - .build() - .replay_context( - ReplayErrorKind::Continuation, - "build OpenCode SandboxReplay bridge client", - )?; - let shared = Arc::new(BridgeShared { - state: Mutex::new(BridgeState::default()), - client, - upstream_origin, - upstream_api_key, - routing_session_id: routing_session_id.to_owned(), - bridge_api_key: bridge_api_key.clone(), - strip_prompt, - temperature, - top_p, - disable_thinking, - cancelled: AtomicBool::new(false), - }); - let router = router(Arc::clone(&shared)); - let (shutdown_tx, shutdown_rx) = oneshot::channel(); - let (ready_tx, ready_rx) = mpsc::sync_channel(1); - let (done_tx, done_rx) = mpsc::sync_channel(1); - let worker = thread::Builder::new() - .name("pvisor-opencode-replay-bridge".into()) - .spawn(move || { - let result = run_worker(listener, router, shutdown_rx, ready_tx); - let _ = done_tx.send(result); - }) - .replay_context( - ReplayErrorKind::Continuation, - "start OpenCode SandboxReplay bridge thread", - )?; - let mut handle = Self { - base_url: format!("http://{address}{upstream_prefix}"), - api_key: bridge_api_key, - shared, - shutdown: Some(shutdown_tx), - worker_done: Some(done_rx), - worker: Some(worker), - }; - let startup_error = match ready_rx.recv_timeout(START_TIMEOUT) { - Ok(Ok(())) => None, - Ok(Err(message)) => Some(message), - Err(mpsc::RecvTimeoutError::Timeout) => Some(format!( - "OpenCode SandboxReplay bridge did not become ready within {} seconds", - START_TIMEOUT.as_secs() - )), - Err(mpsc::RecvTimeoutError::Disconnected) => { - Some("OpenCode SandboxReplay bridge exited before reporting readiness".into()) - } - }; - if let Some(message) = startup_error { - let _ = handle.stop_worker(); - return Err(ReplayError::continuation(message)); - } - Ok(handle) - } - - /// Environment for the OpenCode child so it talks only to this bridge. - pub fn child_environment(&self) -> Vec<(String, String)> { - let no_proxy = merged_no_proxy_environment(); - vec![ - ("OPENAI_BASE_URL".to_owned(), self.base_url.clone()), - ("OPENAI_API_BASE".to_owned(), self.base_url.clone()), - ("OPENAI_API_KEY".to_owned(), self.api_key.clone()), - ("NO_PROXY".to_owned(), no_proxy.clone()), - ("no_proxy".to_owned(), no_proxy), - ] - } - - pub fn finish(mut self) -> Result { - self.stop_worker()?; - let state = self - .shared - .state - .lock() - .map_err(|_| ReplayError::continuation("OpenCode bridge state lock poisoned"))?; - if state.failed { - return Err(ReplayError::continuation(format!( - "OpenCode resume transport bridge failed closed: {}", - state - .failure - .as_deref() - .unwrap_or("unknown protocol failure") - ))); - } - if state.forwarded_requests == 0 { - return Err(ReplayError::continuation( - "OpenCode continuation made no model request through the SandboxReplay bridge", - )); - } - if self.shared.strip_prompt.is_some() && !state.removed_transport_prompt { - return Err(ReplayError::continuation( - "OpenCode transport nonce was not removed from any model request", - )); - } - Ok(state.forwarded_requests) - } - - fn stop_worker(&mut self) -> Result<(), ReplayError> { - self.shared.cancelled.store(true, Ordering::Release); - if let Some(shutdown) = self.shutdown.take() { - let _ = shutdown.send(()); - } - let worker_result = match self.worker_done.take() { - Some(done) => match done.recv_timeout(STOP_TIMEOUT) { - Ok(result) => Some(result), - Err(mpsc::RecvTimeoutError::Disconnected) => None, - Err(mpsc::RecvTimeoutError::Timeout) => { - self.worker.take(); - return Err(ReplayError::continuation(format!( - "OpenCode SandboxReplay bridge did not stop within {} seconds", - STOP_TIMEOUT.as_secs() - ))); - } - }, - None => None, - }; - if let Some(worker) = self.worker.take() - && worker.join().is_err() - { - return Err(ReplayError::continuation( - "OpenCode SandboxReplay bridge thread panicked", - )); - } - if let Some(result) = worker_result { - result.replay_context( - ReplayErrorKind::Executor, - "stop OpenCode SandboxReplay bridge", - )?; - } - Ok(()) - } -} - -impl Drop for OpencodeBridgeHandle { - fn drop(&mut self) { - let _ = self.stop_worker(); - } -} - -fn run_worker( - listener: TcpListener, - router: Router, - shutdown_rx: oneshot::Receiver<()>, - ready_tx: mpsc::SyncSender>, -) -> anyhow::Result<()> { - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .map_err(|error| { - let _ = ready_tx.send(Err(format!("build OpenCode bridge runtime: {error}"))); - error - })?; - runtime.block_on(async move { - let listener = tokio::net::TcpListener::from_std(listener).map_err(|error| { - let _ = ready_tx.send(Err(format!("adopt OpenCode bridge listener: {error}"))); - error - })?; - ready_tx - .send(Ok(())) - .map_err(|_| anyhow::anyhow!("OpenCode bridge startup receiver was dropped"))?; - axum::serve(listener, router) - .with_graceful_shutdown(async move { - let _ = shutdown_rx.await; - }) - .await?; - anyhow::Ok(()) - }) -} - -fn router(shared: Arc) -> Router { - Router::new() - .route("/health", axum::routing::get(health)) - .fallback(forward_handler) - .layer(DefaultBodyLimit::max(MAX_BODY_BYTES)) - .with_state(shared) -} - -async fn health(State(shared): State>) -> Response { - let (failed, forwarded) = shared - .state - .lock() - .map(|state| (state.failed, state.forwarded_requests)) - .unwrap_or((true, 0)); - json_response(StatusCode::OK, json_health(failed, forwarded)) -} - -fn json_health(failed: bool, forwarded: usize) -> Bytes { - serde_json::to_vec(&serde_json::json!({ - "status": "healthy", - "bridge_version": BRIDGE_VERSION, - "failed": failed, - "forwarded_requests": forwarded, - })) - .unwrap_or_default() - .into() -} - -/// Forward any request upstream, rewriting JSON bodies: strip the transport -/// nonce and pin sampling. Non-JSON requests (catalog fetches and probes) -/// pass through untouched. -async fn forward_handler( - State(shared): State>, - request: axum::extract::Request, -) -> Response { - if !authorized(&shared, request.headers()) { - return error_response(StatusCode::UNAUTHORIZED, "invalid bridge API key"); - } - let method = request.method().clone(); - let path = request - .uri() - .path_and_query() - .map(|v| v.as_str().to_owned()); - let headers = request.headers().clone(); - let body = match axum::body::to_bytes(request.into_body(), MAX_BODY_BYTES).await { - Ok(bytes) => bytes, - Err(error) => { - fail( - &shared, - format!("read OpenCode bridge request body: {error}"), - ); - return error_response(StatusCode::BAD_REQUEST, "invalid request body"); - } - }; - let Some(path) = path else { - return error_response(StatusCode::BAD_REQUEST, "request has no path"); - }; - let debug_level = std::env::var("PVISOR_OPENCODE_BRIDGE_DEBUG") - .ok() - .and_then(|value| value.trim().parse::().ok()) - .unwrap_or(0); - if debug_level > 0 { - use std::io::Write; - if let Ok(mut log) = std::fs::OpenOptions::new() - .create(true) - .append(true) - .open("/tmp/pvisor-opencode-bridge-debug.log") - { - let _ = writeln!( - log, - "[{}] {} {} body={}B head={}", - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_secs()) - .unwrap_or(0), - method, - path, - body.len(), - String::from_utf8_lossy(&body[..body.len().min(300)]).replace('\n', " ") - ); - } - if debug_level >= 2 - && let Ok(mut bodies) = std::fs::OpenOptions::new() - .create(true) - .append(true) - .open("/tmp/pvisor-opencode-bridge-bodies.log") - { - use std::io::Write as _; - let _ = bodies.write_all(&body); - let _ = bodies.write_all(b"\n===REQUEST-END===\n"); - } - } - let content_type = headers - .get(axum::http::header::CONTENT_TYPE) - .and_then(|value| value.to_str().ok()) - .unwrap_or_default() - .to_ascii_lowercase(); - let is_json = method == axum::http::Method::POST && content_type.contains("application/json"); - let body: Bytes = if is_json && !body.is_empty() { - match rewrite_request(&shared, &body) { - Ok(rewritten) => rewritten, - Err(error) => { - fail(&shared, error.to_string()); - return error_response(StatusCode::UNPROCESSABLE_ENTITY, &error.to_string()); - } - } - } else { - body - }; - if debug_level >= 2 { - use std::io::Write; - if let Ok(mut bodies) = std::fs::OpenOptions::new() - .create(true) - .append(true) - .open("/tmp/pvisor-opencode-bridge-upstream.log") - { - let _ = bodies.write_all(&body); - let _ = bodies.write_all(b"\n===UPSTREAM-END===\n"); - } - } - let upstream_url = format!("{}{}", shared.upstream_origin, path); - let mut upstream = shared.client.request(method, &upstream_url); - for (name, value) in headers.iter() { - let name = name.as_str(); - if is_hop_by_hop_header(name) || name == "host" { - continue; - } - if name == "authorization" || name == "x-api-key" { - continue; - } - // The JSON rewrite changes the body length (nonce removal, sampling - // injection), so the incoming framing headers must never be trusted; - // reqwest re-frames the full Bytes body itself. - if name == "content-length" || name == "transfer-encoding" { - continue; - } - upstream = upstream.header(name, value.clone()); - } - upstream = upstream - .header( - axum::http::header::AUTHORIZATION.as_str(), - format!("Bearer {}", shared.upstream_api_key), - ) - .header("X-LiteLLM-Session-ID", &shared.routing_session_id); - if !body.is_empty() { - upstream = upstream.body(reqwest::Body::from(body)); - } - let response = match upstream.send().await { - Ok(response) => response, - Err(error) => { - fail(&shared, format!("forward OpenCode request: {error}")); - return error_response(StatusCode::BAD_GATEWAY, &error.to_string()); - } - }; - let status = response.status(); - if std::env::var("PVISOR_OPENCODE_BRIDGE_DEBUG").is_ok() { - use std::io::Write; - if let Ok(mut log) = std::fs::OpenOptions::new() - .create(true) - .append(true) - .open("/tmp/pvisor-opencode-bridge-debug.log") - { - let _ = writeln!(log, "[upstream] status={}", status.as_u16()); - } - } - let mut response_headers = HeaderMap::new(); - for (name, value) in response.headers().iter() { - if is_hop_by_hop_header(name.as_str()) { - continue; - } - if let Ok(header_value) = HeaderValue::from_bytes(value.as_bytes()) { - response_headers.insert(name.clone(), header_value); - } - } - // Buffer the whole upstream body before replying, mirroring the Codex - // bridge. A pass-through stream can die mid-flight; OpenCode's Bun-based - // fetch then hangs on the half-open response without retrying, which - // stalls the continuation forever. - let body = match response.bytes().await { - Ok(bytes) => bytes, - Err(error) => { - fail(&shared, format!("read upstream OpenCode response: {error}")); - return error_response(StatusCode::BAD_GATEWAY, &error.to_string()); - } - }; - let mut builder = Response::builder() - .status(StatusCode::from_u16(status.as_u16()).unwrap_or(StatusCode::BAD_GATEWAY)); - for (name, value) in response_headers.iter() { - builder = builder.header(name.clone(), value.clone()); - } - match builder.body(Body::from(body)) { - Ok(response) => { - if let Ok(mut state) = shared.state.lock() { - state.forwarded_requests += 1; - } - response - } - Err(error) => { - fail(&shared, format!("build OpenCode bridge response: {error}")); - error_response(StatusCode::BAD_GATEWAY, "bridge response failure") - } - } -} - -fn rewrite_request(shared: &BridgeShared, body: &[u8]) -> anyhow::Result { - let mut payload: Value = - serde_json::from_slice(body).context("OpenCode bridge request is not valid JSON")?; - if !payload.is_object() { - anyhow::bail!("OpenCode bridge request must be a JSON object"); - } - if let Some(nonce) = &shared.strip_prompt { - let removed = remove_exact_user_input(&mut payload, nonce)?; - if removed > 0 - && let Ok(mut state) = shared.state.lock() - { - state.removed_transport_prompt = true; - } - } - let mut changed = false; - if let Some(temperature) = shared.temperature { - payload["temperature"] = serde_json::json!(temperature); - changed = true; - } - if let Some(top_p) = shared.top_p { - payload["top_p"] = serde_json::json!(top_p); - changed = true; - } - if shared.disable_thinking { - // Greedy reasoning models can loop until the output cap and emit an - // empty turn; the endpoint only disables thinking through the chat - // template, which OpenCode cannot express. - payload["chat_template_kwargs"] = serde_json::json!({ "enable_thinking": false }); - changed = true; - } - if changed || shared.strip_prompt.is_some() { - Ok(serde_json::to_vec(&payload)?.into()) - } else { - Ok(Bytes::copy_from_slice(body)) - } -} - -/// Remove the exact nonce user message from a request. Handles both the -/// Responses `input` array (items typed `user` or roled `user` with -/// `input_text` content) and the Chat Completions `messages` array. -fn remove_exact_user_input(payload: &mut Value, expected: &str) -> anyhow::Result { - let mut removed = 0; - for key in ["input", "messages"] { - let Some(items) = payload.get_mut(key).and_then(Value::as_array_mut) else { - continue; - }; - let mut index = 0; - while index < items.len() { - let is_user = items[index].get("role").and_then(Value::as_str) == Some("user") - || items[index].get("type").and_then(Value::as_str) == Some("user"); - if is_user && exact_message_text(&items[index]) == Some(expected) { - items.remove(index); - removed += 1; - } else { - index += 1; - } - } - } - Ok(removed) -} - -fn exact_message_text(message: &Value) -> Option<&str> { - let content = message.get("content")?; - if let Some(text) = content.as_str() { - return Some(text); - } - let blocks = content.as_array()?; - if blocks.len() != 1 { - return None; - } - let block = &blocks[0]; - let kind = block.get("type").and_then(Value::as_str)?; - if kind != "input_text" && kind != "text" { - return None; - } - block.get("text").and_then(Value::as_str) -} - -fn authorized(shared: &BridgeShared, headers: &HeaderMap) -> bool { - let supplied = headers - .get("x-api-key") - .and_then(|value| value.to_str().ok()) - .or_else(|| { - headers - .get(axum::http::header::AUTHORIZATION) - .and_then(|value| value.to_str().ok()) - .and_then(|value| value.strip_prefix("Bearer ")) - }); - supplied == Some(shared.bridge_api_key.as_str()) -} - -fn fail(shared: &BridgeShared, message: String) { - if let Ok(mut state) = shared.state.lock() { - state.fail(message); - } -} - -fn error_response(status: StatusCode, message: &str) -> Response { - json_response( - status, - serde_json::to_vec(&serde_json::json!({"error": {"message": message}})) - .unwrap_or_default() - .into(), - ) -} - -fn json_response(status: StatusCode, body: Bytes) -> Response { - Response::builder() - .status(status) - .header(axum::http::header::CONTENT_TYPE, "application/json") - .body(Body::from(body)) - .unwrap_or_else(|_| Response::new(Body::empty())) -} - -fn is_hop_by_hop_header(name: &str) -> bool { - matches!( - name, - "connection" - | "keep-alive" - | "proxy-authenticate" - | "proxy-authorization" - | "te" - | "trailer" - | "transfer-encoding" - | "upgrade" - ) -} - -fn url_path_prefix(base: &str) -> Result { - let parsed = reqwest::Url::parse(base).replay_context( - ReplayErrorKind::Configuration, - "parse OpenCode upstream URL", - )?; - let path = parsed.path().trim_end_matches('/'); - Ok(path.to_owned()) -} - -fn url_origin(base: &str) -> Result { - let parsed = reqwest::Url::parse(base).replay_context( - ReplayErrorKind::Configuration, - "parse OpenCode upstream URL", - )?; - let origin = match parsed.port() { - Some(port) => format!( - "{}://{}:{}", - parsed.scheme(), - parsed.host_str().unwrap_or_default(), - port - ), - None => format!( - "{}://{}", - parsed.scheme(), - parsed.host_str().unwrap_or_default() - ), - }; - if parsed.scheme() != "http" && parsed.scheme() != "https" { - return Err(ReplayError::configuration( - "OpenCode upstream URL must be HTTP(S)", - )); - } - Ok(origin) -} - -fn first_nonempty_env(names: &[&str]) -> Option { - names - .iter() - .filter_map(|name| std::env::var(name).ok()) - .find(|value| !value.trim().is_empty()) -} - -fn merged_no_proxy_environment() -> String { - let mut entries: Vec = ["127.0.0.1", "localhost", "::1"] - .iter() - .map(|entry| (*entry).to_owned()) - .collect(); - for name in ["NO_PROXY", "no_proxy"] { - if let Ok(value) = std::env::var(name) - && !value.trim().is_empty() - { - entries.extend(value.split(',').map(|entry| entry.trim().to_owned())); - } - } - entries.join(",") -} - -#[cfg(test)] -mod tests { - use super::{BridgeShared, remove_exact_user_input, rewrite_request}; - use serde_json::json; - use std::sync::Mutex; - - fn shared(strip: Option<&str>, temperature: Option, top_p: Option) -> BridgeShared { - shared_full(strip, temperature, top_p, false) - } - - fn shared_full( - strip: Option<&str>, - temperature: Option, - top_p: Option, - disable_thinking: bool, - ) -> BridgeShared { - BridgeShared { - state: Mutex::new(Default::default()), - client: reqwest::Client::new(), - upstream_origin: "http://127.0.0.1:9".into(), - upstream_api_key: "upstream".into(), - routing_session_id: "ses".into(), - bridge_api_key: "bridge".into(), - strip_prompt: strip.map(str::to_owned), - temperature, - top_p, - disable_thinking, - cancelled: Default::default(), - } - } - - #[test] - fn disables_thinking_through_the_chat_template() { - let bridge = shared_full(None, Some(0.0), Some(1.0), true); - let request = json!({"model": "m", "input": [{"role": "user", "content": "task"}]}); - let rewritten: serde_json::Value = serde_json::from_slice( - &rewrite_request(&bridge, serde_json::to_vec(&request).unwrap().as_slice()).unwrap(), - ) - .unwrap(); - assert_eq!( - rewritten["chat_template_kwargs"], - serde_json::json!({"enable_thinking": false}) - ); - } - - #[test] - fn strips_nonce_and_pins_sampling_in_responses_shape() { - let bridge = shared(Some("pvisor-opencode-resume-nonce"), Some(0.0), Some(1.0)); - let request = json!({ - "model": "m", - "input": [ - {"type": "system"}, - {"role": "user", "content": "the task"}, - {"role": "assistant", "content": [{"type": "output_text", "text": "working"}]}, - {"type": "function_call", "call_id": "c1"}, - {"type": "function_call_output", "call_id": "c1"}, - {"type": "user", "content": [{"type": "input_text", "text": "pvisor-opencode-resume-nonce"}]} - ] - }); - let rewritten: serde_json::Value = serde_json::from_slice( - &rewrite_request(&bridge, serde_json::to_vec(&request).unwrap().as_slice()).unwrap(), - ) - .unwrap(); - let kinds: Vec<&str> = rewritten["input"] - .as_array() - .unwrap() - .iter() - .map(|item| { - item.get("type") - .and_then(|v| v.as_str()) - .or_else(|| item.get("role").and_then(|v| v.as_str())) - .unwrap_or("?") - }) - .collect(); - assert_eq!( - kinds, - vec![ - "system", - "user", - "assistant", - "function_call", - "function_call_output" - ] - ); - assert_eq!(rewritten["temperature"], 0.0); - assert_eq!(rewritten["top_p"], 1.0); - assert!(bridge.state.lock().unwrap().removed_transport_prompt); - } - - #[test] - fn strips_nonce_from_chat_completions_shape() { - let mut request = json!({ - "model": "m", - "messages": [ - {"role": "user", "content": "task"}, - {"role": "user", "content": "nonce-value"}, - {"role": "assistant", "content": "ok"} - ] - }); - assert_eq!( - remove_exact_user_input(&mut request, "nonce-value").unwrap(), - 1 - ); - assert_eq!(request["messages"].as_array().unwrap().len(), 2); - } - - #[test] - fn keeps_explicit_boundary_prompt() { - let bridge = shared(None, None, None); - let request = json!({ - "model": "m", - "input": [ - {"role": "user", "content": [{"type": "input_text", "text": "continue from here"}]} - ] - }); - let rewritten: serde_json::Value = serde_json::from_slice( - &rewrite_request(&bridge, serde_json::to_vec(&request).unwrap().as_slice()).unwrap(), - ) - .unwrap(); - assert_eq!(rewritten["input"].as_array().unwrap().len(), 1); - assert!(!bridge.state.lock().unwrap().removed_transport_prompt); - } -} diff --git a/crates/persisting-replay/src/process.rs b/crates/persisting-replay/src/process.rs deleted file mode 100644 index 3f92b24de..000000000 --- a/crates/persisting-replay/src/process.rs +++ /dev/null @@ -1,680 +0,0 @@ -use std::fs::{File, OpenOptions}; -use std::io::{self, Read, Write}; -#[cfg(unix)] -use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; -#[cfg(unix)] -use std::os::unix::process::CommandExt; -use std::path::PathBuf; -use std::process::{Command, ExitStatus, Stdio}; -use std::sync::{Arc, Mutex}; -use std::thread; -use std::time::{Duration, Instant}; - -use crate::error::{ReplayError, ReplayErrorKind, ResultExt}; - -#[allow(dead_code)] -pub(crate) struct ProcessSpec { - pub command: Command, - pub stdin: Option>, - /// Terminate the process when neither stdout, stderr, nor a redirected - /// stdout file produced new bytes for this long. Long silent stretches - /// are the signature of an agent CLI that wedged internally instead of - /// working. - pub idle_timeout: Option, - /// Terminate the process once stdout has carried this many - /// `"type":"step_finish"` JSONL events. OpenCode ignores its - /// `agent.steps` budget on resumed sessions, so pVisor enforces the - /// remaining live-action budget itself. - pub step_finish_limit: Option, - /// Redirect the child's stdout straight to this file instead of a pipe. - /// OpenCode's Bun runtime fully buffers stdout on pipes (events only - /// appear at exit) but streams into regular files, so event-driven - /// watchdogs must watch the file. - pub stdout_redirect: Option, - pub timeout: Duration, - pub termination_grace: Duration, - pub pipe_grace: Duration, - pub retained_bytes: usize, - pub log_path: PathBuf, -} - -#[allow(dead_code)] -pub(crate) struct ProcessOutput { - pub status: ExitStatus, - pub stdout_tail: Vec, - pub stderr_tail: Vec, - pub stdout_bytes: u64, - pub stderr_bytes: u64, - pub stdout_truncated: bool, - pub stderr_truncated: bool, - pub timed_out: bool, - pub step_limited: bool, - pub background_cleanup: bool, -} - -struct StreamCapture { - tail: Vec, - total: u64, - log_error: Option, -} - -#[allow(dead_code)] -pub(crate) fn run_process(mut spec: ProcessSpec) -> Result { - let log = owner_only_log(&spec.log_path)?; - let log = Arc::new(Mutex::new(log)); - let stdout_redirect = spec.stdout_redirect.take(); - if let Some(target) = &stdout_redirect { - if let Some(parent) = target.parent() { - std::fs::create_dir_all(parent) - .replay_context(ReplayErrorKind::Executor, "create stdout redirect parent")?; - } - let file = std::fs::OpenOptions::new() - .create(true) - .truncate(true) - .write(true) - .open(target) - .replay_context(ReplayErrorKind::Executor, "open stdout redirect file")?; - spec.command.stdout(Stdio::from(file)); - } else { - spec.command.stdout(Stdio::piped()); - } - spec.command.stderr(Stdio::piped()); - if spec.stdin.is_some() { - spec.command.stdin(Stdio::piped()); - } - #[cfg(unix)] - unsafe { - spec.command.pre_exec(|| { - if libc::setpgid(0, 0) == 0 { - Ok(()) - } else { - Err(io::Error::last_os_error()) - } - }); - } - let mut child = spec - .command - .spawn() - .replay_context(ReplayErrorKind::Executor, "spawn supervised replay process")?; - let process_group = child.id() as i32; - let stdout = child.stdout.take(); - let stderr = child - .stderr - .take() - .ok_or_else(|| ReplayError::new(ReplayErrorKind::Internal, "stderr pipe missing"))?; - let last_activity = Arc::new(std::sync::atomic::AtomicU64::new( - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|value| value.as_millis() as u64) - .unwrap_or(0), - )); - let step_finish_seen = Arc::new(std::sync::atomic::AtomicUsize::new(0)); - // With a redirected stdout there is no pipe to drain; the wait loop - // below polls the redirect file for growth so idle_timeout still sees - // live event writes. Step budget keeps watching `--print-logs` stderr. - let mut redirect_seen_bytes = 0_u64; - let stdout_reader = stdout.map(|pipe| { - spawn_reader( - pipe, - Arc::clone(&log), - spec.retained_bytes, - Some(Arc::clone(&last_activity)), - None, - ) - }); - let stderr_reader = spawn_reader( - stderr, - Arc::clone(&log), - spec.retained_bytes, - Some(Arc::clone(&last_activity)), - Some(( - Arc::clone(&step_finish_seen), - spec.step_finish_limit.is_some(), - )), - ); - if let Some(input) = spec.stdin.take() { - let write_result = child - .stdin - .take() - .ok_or_else(|| io::Error::other("stdin pipe missing")) - .and_then(|mut stdin| stdin.write_all(&input)); - if let Err(error) = write_result { - #[cfg(unix)] - let _ = signal_group(process_group, libc::SIGKILL); - #[cfg(not(unix))] - let _ = child.kill(); - let _ = child.wait(); - if let Some(reader) = stdout_reader { - let _ = reader.join(); - } - let _ = stderr_reader.join(); - return Err(ReplayError::new( - ReplayErrorKind::Executor, - format!("write supervised process stdin: {error}"), - )); - } - } - - let started = Instant::now(); - let mut timed_out = false; - let mut step_limited = false; - let mut background_cleanup = false; - let status = loop { - if let Some(status) = child - .try_wait() - .replay_context(ReplayErrorKind::Executor, "poll supervised replay process")? - { - break status; - } - if started.elapsed() >= spec.timeout { - timed_out = true; - background_cleanup = true; - break terminate_running_group(&mut child, process_group, spec.termination_grace)?; - } - if let Some(limit) = spec.step_finish_limit - && step_finish_seen.load(std::sync::atomic::Ordering::Acquire) >= limit - { - step_limited = true; - background_cleanup = true; - // SIGINT lets OpenCode exit gracefully and flush its buffered - // stdout events; SIGTERM would discard them. - break terminate_running_group_with( - &mut child, - process_group, - Duration::from_secs(20).max(spec.termination_grace), - libc::SIGINT, - )?; - } - if let Some(path) = &stdout_redirect - && let Ok(meta) = std::fs::metadata(path) - { - let len = meta.len(); - if len > redirect_seen_bytes { - redirect_seen_bytes = len; - let now_ms = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|value| value.as_millis() as u64) - .unwrap_or(0); - last_activity.store(now_ms, std::sync::atomic::Ordering::Release); - } - } - if let Some(idle_timeout) = spec.idle_timeout { - let now_ms = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|value| value.as_millis() as u64) - .unwrap_or(0); - let last_ms = last_activity.load(std::sync::atomic::Ordering::Acquire); - if now_ms.saturating_sub(last_ms) >= idle_timeout.as_millis() as u64 { - background_cleanup = true; - break terminate_running_group(&mut child, process_group, spec.termination_grace)?; - } - } - thread::sleep(Duration::from_millis(10)); - }; - - #[cfg(unix)] - if process_group_exists(process_group)? { - background_cleanup = true; - terminate_remaining_group(process_group, spec.termination_grace)?; - } - - let pipe_deadline = Instant::now() + spec.pipe_grace + spec.termination_grace; - let stdout_pending = stdout_reader - .as_ref() - .is_some_and(|reader| !reader.is_finished()); - while (stdout_pending || !stderr_reader.is_finished()) && Instant::now() < pipe_deadline { - thread::sleep(Duration::from_millis(5)); - } - #[cfg(unix)] - let stdout_still_pending = stdout_reader - .as_ref() - .is_some_and(|reader| !reader.is_finished()); - if stdout_still_pending || !stderr_reader.is_finished() { - background_cleanup = true; - let _ = signal_group(process_group, libc::SIGKILL); - } - - let stdout = match stdout_reader { - Some(reader) => reader - .join() - .map_err(|_| ReplayError::new(ReplayErrorKind::Internal, "stdout reader panicked"))? - .replay_context(ReplayErrorKind::Executor, "drain supervised stdout")?, - None => { - // Redirected stdout: summarize the redirect file itself so the - // caller keeps its usual byte accounting. - let bytes = stdout_redirect - .as_deref() - .and_then(|path| std::fs::read(path).ok()) - .unwrap_or_default(); - let tail_len = bytes.len().min(spec_retained(spec.retained_bytes)); - StreamCapture { - tail: bytes[bytes.len() - tail_len..].to_vec(), - total: bytes.len() as u64, - log_error: None, - } - } - }; - let stderr = stderr_reader - .join() - .map_err(|_| ReplayError::new(ReplayErrorKind::Internal, "stderr reader panicked"))? - .replay_context(ReplayErrorKind::Executor, "drain supervised stderr")?; - if let Some(error) = stdout.log_error.or(stderr.log_error) { - return Err(ReplayError::new( - ReplayErrorKind::Executor, - format!("write supervised process log: {error}"), - )); - } - - Ok(ProcessOutput { - status, - stdout_truncated: stdout.total > stdout.tail.len() as u64, - stderr_truncated: stderr.total > stderr.tail.len() as u64, - stdout_tail: stdout.tail, - stderr_tail: stderr.tail, - stdout_bytes: stdout.total, - stderr_bytes: stderr.total, - timed_out, - step_limited, - background_cleanup, - }) -} - -fn owner_only_log(path: &std::path::Path) -> Result { - let mut options = OpenOptions::new(); - options.create(true).truncate(true).write(true); - #[cfg(unix)] - options.mode(0o600); - let file = options.open(path).replay_context( - ReplayErrorKind::Executor, - format!("create process log {}", path.display()), - )?; - #[cfg(unix)] - file.set_permissions(std::fs::Permissions::from_mode(0o600)) - .replay_context( - ReplayErrorKind::Executor, - format!("restrict process log {}", path.display()), - )?; - Ok(file) -} - -fn spawn_reader( - mut reader: R, - log: Arc>, - retained_bytes: usize, - activity: Option>, - step_counter: Option<(Arc, bool)>, -) -> thread::JoinHandle> -where - R: Read + Send + 'static, -{ - thread::spawn(move || { - let mut tail = Vec::with_capacity(retained_bytes.min(64 * 1024)); - let mut total = 0_u64; - let mut log_error = None; - let mut chunk = [0_u8; 16 * 1024]; - loop { - let count = reader.read(&mut chunk)?; - if count == 0 { - break; - } - total = total.saturating_add(count as u64); - if let Some((counter, _)) = &step_counter { - let hits = count_step_finish(&chunk[..count]); - if hits > 0 { - counter.fetch_add(hits, std::sync::atomic::Ordering::AcqRel); - } - } - if let Some(activity) = &activity { - let now_ms = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|value| value.as_millis() as u64) - .unwrap_or(0); - activity.store(now_ms, std::sync::atomic::Ordering::Release); - } - if log_error.is_none() { - let write_result = log - .lock() - .map_err(|_| io::Error::other("process log lock poisoned"))? - .write_all(&chunk[..count]); - if let Err(error) = write_result { - log_error = Some(error); - } - } - retain_tail(&mut tail, &chunk[..count], retained_bytes); - } - Ok(StreamCapture { - tail, - total, - log_error, - }) - }) -} - -fn spec_retained(retained_bytes: usize) -> usize { - retained_bytes.max(1) -} - -fn count_step_finish(chunk: &[u8]) -> usize { - // OpenCode's stdout JSONL is block-buffered (events only flush in ~8KB - // batches or at exit), but `--print-logs` stderr carries one - // `message=loop ... step=N` line per live turn in real time. - const NEEDLE: &[u8] = b"message=loop"; - let mut hits = 0; - let mut offset = 0; - while offset + NEEDLE.len() <= chunk.len() { - if &chunk[offset..offset + NEEDLE.len()] == NEEDLE { - hits += 1; - offset += NEEDLE.len(); - } else { - offset += 1; - } - } - hits -} - -fn retain_tail(tail: &mut Vec, chunk: &[u8], limit: usize) { - if limit == 0 { - tail.clear(); - } else if chunk.len() >= limit { - tail.clear(); - tail.extend_from_slice(&chunk[chunk.len() - limit..]); - } else { - let overflow = tail.len().saturating_add(chunk.len()).saturating_sub(limit); - if overflow != 0 { - tail.drain(..overflow); - } - tail.extend_from_slice(chunk); - } -} - -#[cfg(unix)] -fn terminate_running_group( - child: &mut std::process::Child, - process_group: i32, - grace: Duration, -) -> Result { - terminate_running_group_with(child, process_group, grace, libc::SIGTERM) -} - -#[cfg(unix)] -fn terminate_running_group_with( - child: &mut std::process::Child, - process_group: i32, - grace: Duration, - first_signal: libc::c_int, -) -> Result { - let _ = signal_group(process_group, first_signal)?; - let deadline = Instant::now() + grace; - loop { - if let Some(status) = child - .try_wait() - .replay_context(ReplayErrorKind::Executor, "poll terminated process leader")? - { - if process_group_exists(process_group)? { - let _ = signal_group(process_group, libc::SIGKILL)?; - } - return Ok(status); - } - if Instant::now() >= deadline { - let _ = signal_group(process_group, libc::SIGKILL)?; - return child - .wait() - .replay_context(ReplayErrorKind::Executor, "reap killed process leader"); - } - thread::sleep(Duration::from_millis(5)); - } -} - -#[cfg(not(unix))] -fn terminate_running_group( - child: &mut std::process::Child, - _process_group: i32, - _grace: Duration, -) -> Result { - child - .kill() - .replay_context(ReplayErrorKind::Executor, "kill timed out process")?; - child - .wait() - .replay_context(ReplayErrorKind::Executor, "reap killed process") -} - -#[cfg(unix)] -fn terminate_remaining_group(process_group: i32, grace: Duration) -> Result<(), ReplayError> { - let _ = signal_group(process_group, libc::SIGTERM)?; - let deadline = Instant::now() + grace; - while process_group_exists(process_group)? && Instant::now() < deadline { - thread::sleep(Duration::from_millis(5)); - } - if process_group_exists(process_group)? { - let _ = signal_group(process_group, libc::SIGKILL)?; - } - Ok(()) -} - -#[cfg(unix)] -fn process_group_exists(process_group: i32) -> Result { - let result = unsafe { libc::kill(-process_group, 0) }; - classify_process_group_kill( - process_group, - "inspect replay process group", - result, - io::Error::last_os_error(), - ) -} - -/// POSIX `kill(-pgid, sig)`: 0 means members exist, ESRCH means the group is -/// gone, and EPERM means members exist that this process cannot signal. -#[cfg(unix)] -fn classify_process_group_kill( - process_group: i32, - operation: &str, - result: i32, - error: io::Error, -) -> Result { - if result == 0 { - return Ok(true); - } - match error.raw_os_error() { - Some(libc::ESRCH) => Ok(false), - Some(libc::EPERM) => Ok(true), - _ => Err(ReplayError::new( - ReplayErrorKind::Executor, - format!("{operation} {process_group}: {error}"), - )), - } -} - -#[cfg(unix)] -fn signal_group(process_group: i32, signal: i32) -> Result { - let result = unsafe { libc::kill(-process_group, signal) }; - classify_process_group_kill( - process_group, - "signal replay process group", - result, - io::Error::last_os_error(), - ) -} - -#[cfg(all(test, unix))] -mod tests { - use super::*; - use std::path::Path; - use std::process::Command; - use std::time::{Duration, Instant}; - - fn shell_spec(script: &str, log_path: &Path) -> ProcessSpec { - let mut command = Command::new("/bin/sh"); - command.args(["-c", script]); - ProcessSpec { - command, - stdin: None, - idle_timeout: None, - step_finish_limit: None, - stdout_redirect: None, - timeout: Duration::from_secs(5), - termination_grace: Duration::from_millis(100), - pipe_grace: Duration::from_millis(100), - retained_bytes: 64 * 1024, - log_path: log_path.to_path_buf(), - } - } - - #[test] - fn writes_configured_stdin_before_waiting() { - let temporary = tempfile::tempdir().unwrap(); - let log_path = temporary.path().join("stdin.log"); - let mut spec = shell_spec("cat", &log_path); - spec.stdin = Some(b"resume nonce".to_vec()); - - let output = run_process(spec).unwrap(); - - assert!(output.status.success()); - assert_eq!(output.stdout_tail, b"resume nonce"); - } - - #[test] - fn drains_large_output_to_log_with_a_bounded_tail() { - let temporary = tempfile::tempdir().unwrap(); - let log_path = temporary.path().join("large.log"); - let output = run_process(shell_spec("yes x | head -c 8388608", &log_path)).unwrap(); - - assert!(output.status.success()); - assert_eq!(output.stdout_bytes, 8 * 1024 * 1024); - assert!(output.stdout_truncated); - assert_eq!(output.stdout_tail.len(), 64 * 1024); - assert_eq!(std::fs::metadata(log_path).unwrap().len(), 8 * 1024 * 1024); - assert_eq!( - std::fs::metadata(temporary.path().join("large.log")) - .unwrap() - .permissions() - .mode() - & 0o777, - 0o600 - ); - } - - #[test] - fn cleans_background_descendants_after_the_leader_exits() { - let temporary = tempfile::tempdir().unwrap(); - let log_path = temporary.path().join("background.log"); - let started = Instant::now(); - let output = run_process(shell_spec("sleep 30 & echo $!", &log_path)).unwrap(); - - assert!(started.elapsed() < Duration::from_secs(3)); - assert!(output.status.success()); - assert!(output.background_cleanup); - let pid: i32 = String::from_utf8(output.stdout_tail) - .unwrap() - .trim() - .parse() - .unwrap(); - let deadline = Instant::now() + Duration::from_secs(1); - while unsafe { libc::kill(pid, 0) } == 0 && Instant::now() < deadline { - std::thread::sleep(Duration::from_millis(10)); - } - assert_ne!(unsafe { libc::kill(pid, 0) }, 0); - } - - #[test] - fn eperm_means_the_process_group_still_has_members() { - let error = io::Error::from_raw_os_error(libc::EPERM); - assert!( - classify_process_group_kill(4242, "inspect replay process group", -1, error).unwrap() - ); - } - - #[test] - fn esrch_means_the_process_group_is_gone() { - let error = io::Error::from_raw_os_error(libc::ESRCH); - assert!( - !classify_process_group_kill(4242, "inspect replay process group", -1, error).unwrap() - ); - } - - #[test] - fn counts_stderr_loop_progress_lines() { - use super::count_step_finish; - assert_eq!(count_step_finish(b"message=loop step=1"), 1); - assert_eq!( - count_step_finish(b"message=loop step=1\nmessage=loop step=2"), - 2 - ); - assert_eq!(count_step_finish(b"message=tracking"), 0); - assert_eq!(count_step_finish(b"message=exiting loop"), 0); - } - - #[test] - fn step_finish_limit_terminates_the_process_early() { - let temporary = tempfile::tempdir().unwrap(); - let log_path = temporary.path().join("steps.log"); - let script = - "for i in 1 2 3 4 5; do echo 'level=INFO message=loop step='$i >&2; sleep 10; done"; - let mut spec = shell_spec(script, &log_path); - spec.step_finish_limit = Some(2); - spec.timeout = Duration::from_secs(120); - let output = run_process(spec).unwrap(); - assert!(output.step_limited); - assert!(!output.timed_out); - // The third emission never happens: the loop is killed during the - // second sleep. - let log = std::fs::read_to_string(log_path).unwrap(); - assert_eq!(log.matches("message=loop").count(), 2); - } - - #[test] - fn redirected_stdout_growth_refreshes_idle_watchdog() { - let temporary = tempfile::tempdir().unwrap(); - let log_path = temporary.path().join("redirect-idle.log"); - let events_path = temporary.path().join("events.jsonl"); - // stderr stays silent; only the redirect file grows. Without polling - // the redirect, a 300ms idle watchdog would kill this mid-loop. - let script = "for i in 1 2 3 4 5 6; do echo event-$i; sleep 0.2; done"; - let mut spec = shell_spec(script, &log_path); - spec.stdout_redirect = Some(events_path.clone()); - spec.idle_timeout = Some(Duration::from_millis(300)); - spec.timeout = Duration::from_secs(10); - - let output = run_process(spec).unwrap(); - - assert!(output.status.success()); - assert!(!output.timed_out); - assert!(!output.step_limited); - let events = std::fs::read_to_string(&events_path).unwrap(); - assert_eq!(events.lines().count(), 6); - } - - #[test] - fn idle_timeout_still_fires_when_redirect_stalls() { - let temporary = tempfile::tempdir().unwrap(); - let log_path = temporary.path().join("redirect-stall.log"); - let events_path = temporary.path().join("events.jsonl"); - let mut spec = shell_spec("echo once; sleep 5", &log_path); - spec.stdout_redirect = Some(events_path); - spec.idle_timeout = Some(Duration::from_millis(200)); - spec.timeout = Duration::from_secs(10); - let started = Instant::now(); - - let output = run_process(spec).unwrap(); - - assert!(started.elapsed() < Duration::from_secs(2)); - assert!(!output.status.success()); - assert!(!output.timed_out); - assert!(output.background_cleanup); - } - - #[test] - fn times_out_and_reaps_the_foreground_process_group() { - let temporary = tempfile::tempdir().unwrap(); - let log_path = temporary.path().join("timeout.log"); - let mut spec = shell_spec("sleep 30", &log_path); - spec.timeout = Duration::from_millis(100); - let started = Instant::now(); - - let output = run_process(spec).unwrap(); - - assert!(started.elapsed() < Duration::from_secs(3)); - assert!(output.timed_out); - assert!(output.background_cleanup); - } -} diff --git a/crates/persisting-replay/tests/fixtures/claude_bash_one_step.jsonl b/crates/persisting-replay/tests/fixtures/claude_bash_one_step.jsonl deleted file mode 100644 index b7578d557..000000000 --- a/crates/persisting-replay/tests/fixtures/claude_bash_one_step.jsonl +++ /dev/null @@ -1,4 +0,0 @@ -{"type":"user","uuid":"user-1","parentUuid":null,"isSidechain":false,"sessionId":"session-1","version":"2.1.220","message":{"role":"user","content":"Create the marker."}} -{"type":"assistant","uuid":"assistant-1","parentUuid":"user-1","isSidechain":false,"sessionId":"session-1","version":"2.1.220","message":{"id":"message-1","role":"assistant","content":[{"type":"tool_use","id":"tool-1","name":"Bash","input":{"command":"printf fresh > marker.txt"}}]}} -{"type":"user","uuid":"result-1","parentUuid":"assistant-1","sourceToolAssistantUUID":"assistant-1","isSidechain":false,"sessionId":"session-1","version":"2.1.220","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"tool-1","content":"old"}]}} -{"type":"assistant","uuid":"assistant-2","parentUuid":"result-1","isSidechain":false,"sessionId":"session-1","version":"2.1.220","message":{"id":"message-2","role":"assistant","content":[{"type":"text","text":"done"}]}} diff --git a/crates/persisting-replay/tests/fixtures/fake_agent_runtime.py b/crates/persisting-replay/tests/fixtures/fake_agent_runtime.py deleted file mode 100644 index 306fb5133..000000000 --- a/crates/persisting-replay/tests/fixtures/fake_agent_runtime.py +++ /dev/null @@ -1,241 +0,0 @@ -"""Inject minimal pinned-SDK fakes, then execute one replay runner.""" - -from __future__ import annotations - -import json -import os -import runpy -import sys -import types -from pathlib import Path -from types import SimpleNamespace -from typing import Any - - -def _module(name: str) -> types.ModuleType: - module = types.ModuleType(name) - sys.modules[name] = module - return module - - -def _touch(path: str | None, text: str = "1\n") -> None: - if path: - target = Path(path) - target.parent.mkdir(parents=True, exist_ok=True) - with target.open("a", encoding="utf-8") as stream: - stream.write(text) - - -def _install_mini() -> None: - _module("minisweagent") - agents = _module("minisweagent.agents") - environments = _module("minisweagent.environments") - models = _module("minisweagent.models") - exceptions = _module("minisweagent.exceptions") - - class FormatError(Exception): - pass - - class InterruptAgentFlow(Exception): - pass - - exceptions.FormatError = FormatError - exceptions.InterruptAgentFlow = InterruptAgentFlow - - class FakeEnvironment: - def execute(self, action: dict[str, Any]) -> dict[str, Any]: - _touch(action.get("marker")) - return {"output": "fresh observation", "returncode": 0} - - class FakeModel: - def format_observation_messages( - self, - assistant: dict[str, Any], - outputs: list[dict[str, Any]], - template_vars: dict[str, Any], - ) -> list[dict[str, Any]]: - del assistant, template_vars - return [ - { - "role": "tool", - "content": output["output"], - "extra": {"returncode": output["returncode"]}, - } - for output in outputs - ] - - class FakeAgent: - def __init__(self, model: Any, environment: Any, config: dict[str, Any]) -> None: - self.model = model - self.environment = environment - self.messages: list[dict[str, Any]] = [] - self.n_calls = 0 - self.cost = 0.0 - self.n_consecutive_format_errors = 0 - self.config = SimpleNamespace( - max_consecutive_format_errors=3, - step_limit=config.get("step_limit"), - ) - - def add_messages(self, *messages: dict[str, Any]) -> None: - self.messages.extend(messages) - - def get_template_vars(self) -> dict[str, Any]: - return {} - - def save(self, path: Path) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text( - json.dumps({"messages": self.messages, "info": {}}, indent=2) + "\n", - encoding="utf-8", - ) - - def step(self) -> None: - if self.config.step_limit is not None and self.n_calls >= self.config.step_limit: - self.add_messages( - { - "role": "exit", - "content": "LimitsExceeded", - "extra": {"exit_status": "LimitsExceeded", "submission": ""}, - } - ) - return - _touch(os.environ.get("FAKE_LIVE_MARKER")) - self.n_calls += 1 - if os.environ.get("FAKE_NEVER_COMPLETE") == "1": - self.add_messages({"role": "assistant", "content": "continue", "extra": {}}) - else: - self.add_messages( - { - "role": "exit", - "content": "Completed", - "extra": {"exit_status": "Completed", "submission": "done"}, - } - ) - - def handle_uncaught_exception(self, exc: Exception) -> None: - raise exc - - agents.get_agent = lambda model, environment, config, default_type: FakeAgent( - model, environment, config - ) - environments.get_environment = lambda config, default_type: FakeEnvironment() - models.get_model = lambda config: FakeModel() - - -def _install_swe() -> None: - for name in [ - "sweagent", - "sweagent.agent", - "sweagent.environment", - "sweagent.run", - "swerex", - "swerex.deployment", - ]: - _module(name) - agents = _module("sweagent.agent.agents") - environment_module = _module("sweagent.environment.swe_env") - run_single = _module("sweagent.run.run_single") - deployment = _module("swerex.deployment.config") - - class FakeProblem: - id = "fake-problem" - - def get_problem_statement(self) -> str: - return "fake problem" - - class RunSingleConfig: - @classmethod - def model_validate(cls, value: dict[str, Any]) -> Any: - agent_value = value.get("agent") or {} - return SimpleNamespace( - agent=SimpleNamespace(type=agent_value.get("type", "default")), - env=SimpleNamespace( - deployment=(value.get("env") or {}).get("deployment"), - repo=(value.get("env") or {}).get("repo"), - ), - problem_statement=FakeProblem(), - model_dump=lambda: value, - ) - - class FakeLiveModel: - stats = SimpleNamespace() - config = SimpleNamespace() - - def query(self, history: list[dict[str, Any]], **kwargs: Any) -> dict[str, Any]: - del history, kwargs - _touch(os.environ.get("FAKE_LIVE_MARKER")) - return {"message": "live action"} - - class DefaultAgent: - @classmethod - def from_config(cls, config: Any) -> "DefaultAgent": - del config - instance = cls() - instance.model = FakeLiveModel() - instance.trajectory: list[dict[str, Any]] = [] - instance.history: list[dict[str, Any]] = [] - instance.info: dict[str, Any] = {} - instance.replay_config = None - instance.traj_path: Path | None = None - instance._env = None - return instance - - def setup(self, env: Any, problem_statement: Any, output_dir: Path) -> None: - self._env = env - output_dir.mkdir(parents=True, exist_ok=True) - self.traj_path = output_dir / f"{problem_statement.id}.traj" - - def step(self) -> Any: - response = self.model.query(self.history) - action = str(response.get("message") or "") - self.history.append({"role": "assistant", "content": action}) - self.trajectory.append( - {"action": action, "observation": f"fresh-{len(self.trajectory) + 1}"} - ) - return SimpleNamespace(done=False) - - def get_trajectory_data(self) -> dict[str, Any]: - return { - "trajectory": self.trajectory, - "history": self.history, - "info": self.info, - "replay_config": None, - "environment": "fake", - } - - def save_trajectory(self) -> None: - assert self.traj_path is not None - self.traj_path.write_text( - json.dumps(self.get_trajectory_data(), indent=2) + "\n", - encoding="utf-8", - ) - - class SWEEnv: - def __init__(self, deployment: Any, repo: Any, post_startup_commands: list[str]) -> None: - del deployment, post_startup_commands - self.repo = SimpleNamespace(repo_name=str(repo)) - self.name = "fake" - - agents.DefaultAgent = DefaultAgent - environment_module.SWEEnv = SWEEnv - run_single.RunSingleConfig = RunSingleConfig - deployment.get_deployment = lambda config: config - - -def main() -> None: - if len(sys.argv) != 4: - raise SystemExit("usage: fake_agent_runtime.py KIND RUNNER REQUEST") - kind, runner, request = sys.argv[1:] - if kind == "mini": - _install_mini() - elif kind == "swe": - _install_swe() - else: - raise ValueError(f"unknown fake kind: {kind}") - sys.argv = [runner, request] - runpy.run_path(runner, run_name="__main__") - - -if __name__ == "__main__": - main() diff --git a/crates/persisting-replay/tests/fixtures/replay-managed-smoke.toml b/crates/persisting-replay/tests/fixtures/replay-managed-smoke.toml deleted file mode 100644 index c711c8010..000000000 --- a/crates/persisting-replay/tests/fixtures/replay-managed-smoke.toml +++ /dev/null @@ -1,15 +0,0 @@ -[replay] -agent = "claude-code" -trajectory = "trajectory.jsonl" -after_step = 1 -workspace = "." -state_dir = "/tmp/pvisor-replay-managed-state" -output_dir = "/tmp/pvisor-replay-managed-output" -prepare_only = true -disable_thinking = false - -[run] -executor = "host" -timeout_ms = 60000 -policy = "observe" -inherit_env = false diff --git a/crates/persisting-replay/tests/fixtures/replay-smoke.toml b/crates/persisting-replay/tests/fixtures/replay-smoke.toml deleted file mode 100644 index f1325bf0f..000000000 --- a/crates/persisting-replay/tests/fixtures/replay-smoke.toml +++ /dev/null @@ -1,9 +0,0 @@ -[replay] -agent = "claude-code" -trajectory = "crates/persisting-replay/tests/fixtures/claude_bash_one_step.jsonl" -after_step = 1 -workspace = "/tmp/pvisor-replay-toml-workspace" -state_dir = "/tmp/pvisor-replay-toml-state" -output_dir = "/tmp/pvisor-replay-toml-output" -prepare_only = true -disable_thinking = false diff --git a/crates/persisting-replay/tests/replay_contract.rs b/crates/persisting-replay/tests/replay_contract.rs deleted file mode 100644 index 54e904310..000000000 --- a/crates/persisting-replay/tests/replay_contract.rs +++ /dev/null @@ -1,597 +0,0 @@ -use std::fs; -use std::path::{Path, PathBuf}; -use std::process::Command; - -#[cfg(unix)] -use std::os::unix::fs::PermissionsExt; - -use persisting_replay::{ - AgentKind, AgentStatus, PlaybackRequest, ReplayMode, ReplayPhase, execute, -}; -use serde_json::{Value, json}; - -fn crate_path(relative: &str) -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")).join(relative) -} - -fn run_fake(kind: &str, runner: &str, request: &Path, live_marker: &Path) { - let output = Command::new("python3") - .arg(crate_path("tests/fixtures/fake_agent_runtime.py")) - .arg(kind) - .arg(crate_path(runner)) - .arg(request) - .env("FAKE_LIVE_MARKER", live_marker) - .env("OPENAI_API_KEY", "fake") - .env("OPENAI_BASE_URL", "http://127.0.0.1.invalid") - .env("MODEL_NAME", "fake-model") - .output() - .unwrap(); - assert!( - output.status.success(), - "fake runner failed: {}", - String::from_utf8_lossy(&output.stderr) - ); -} - -#[cfg(unix)] -fn write_fake_openhands_entrypoint(path: &Path) { - fs::write( - path, - r#"#!/usr/bin/env python3 -import json -import os -import pathlib -import sys - -if len(sys.argv) > 1 and sys.argv[1] == "-c": - print("0.53.0") - raise SystemExit(0) - -prepared = pathlib.Path(os.environ["REPLAY_TRAJECTORY_PATH"]) -continued = pathlib.Path(os.environ["SAVE_TRAJECTORY_PATH"]) -events = json.loads(prepared.read_text(encoding="utf-8")) -actions = [event for event in events if event.get("source") == "agent" and event.get("action") == "run"] -next_id = max(event["id"] for event in events) + 1 -for action in actions: - if not any(event.get("cause") == action["id"] for event in events): - events.append({ - "id": next_id, - "source": "environment", - "observation": "run", - "cause": action["id"], - "message": "fresh replay observation", - "args": {"command": action.get("args", {}).get("command", ""), "metadata": {"exit_code": 0}}, - }) - next_id += 1 - -limit = int(os.environ["MAX_ITERATIONS"]) -while len(actions) < limit: - pathlib.Path("live-marker").write_text("live\n", encoding="utf-8") - action_id = next_id - next_id += 1 - action = {"id": action_id, "source": "agent", "action": "run", "args": {"command": "echo live"}} - observation = { - "id": next_id, - "source": "environment", - "observation": "run", - "cause": action_id, - "message": "fresh live observation", - "args": {"command": "echo live", "metadata": {"exit_code": 0}}, - } - next_id += 1 - events.extend([action, observation]) - actions.append(action) - -continued.parent.mkdir(parents=True, exist_ok=True) -continued.write_text(json.dumps(events), encoding="utf-8") -if pathlib.Path("fatal-mode").exists(): - print("Error while running the agent", file=sys.stderr) -"#, - ) - .unwrap(); - let mut permissions = fs::metadata(path).unwrap().permissions(); - permissions.set_mode(0o755); - fs::set_permissions(path, permissions).unwrap(); -} - -#[cfg(unix)] -fn openhands_request(root: &Path, mode: ReplayMode, max_steps: usize) -> PlaybackRequest { - let workspace = root.join("workspace"); - fs::create_dir_all(&workspace).unwrap(); - let trajectory = root.join("openhands-trajectory.json"); - fs::write( - &trajectory, - serde_json::to_vec(&json!([ - {"id": 0, "source": "user", "action": "message", "args": {"content": "fix it"}}, - {"id": 1, "source": "agent", "action": "run", "args": {"command": "pwd"}}, - { - "id": 2, - "source": "environment", - "observation": "run", - "cause": 1, - "message": "old observation", - "args": {"command": "pwd", "metadata": {"exit_code": 0}} - } - ])) - .unwrap(), - ) - .unwrap(); - let entrypoint = root.join("fake-openhands"); - write_fake_openhands_entrypoint(&entrypoint); - PlaybackRequest { - agent: AgentKind::Openhands, - trajectory, - after_step: 1, - workspace, - state_dir: root.join("state"), - output_dir: root.join("output"), - agent_entrypoint: Some(entrypoint), - agent_runtime: None, - disallowed_tools: Vec::new(), - trajectory_assets: None, - session_id: None, - max_steps: Some(max_steps), - mode, - allow_stale_observations: false, - run_id: Some("contract".into()), - disable_thinking: false, - boundary_user_prompt: None, - } -} - -#[test] -fn mini_replay_only_executes_prefix_without_live_model() { - let temporary = tempfile::tempdir().unwrap(); - let historical_marker = temporary.path().join("historical-marker"); - let live_marker = temporary.path().join("live-marker"); - let source = temporary.path().join("source.json"); - fs::write( - &source, - serde_json::to_vec(&json!({ - "info": { - "config": { - "model": {}, "environment": {}, "agent": {} - } - }, - "messages": [ - {"role": "system", "content": "system", "extra": {}}, - { - "role": "assistant", - "content": "historical action", - "extra": { - "response": {}, - "actions": [{ - "tool_call_id": "call-1", - "marker": historical_marker, - }] - } - }, - {"role": "tool", "content": "old observation", "extra": {}} - ] - })) - .unwrap(), - ) - .unwrap(); - let request_path = temporary.path().join("request.json"); - let result_path = temporary.path().join("result.json"); - let observations = temporary.path().join("observations.json"); - let reconstructed = temporary.path().join("reconstructed.json"); - let continued = temporary.path().join("continued.json"); - fs::write( - &request_path, - serde_json::to_vec(&json!({ - "source": source, - "reconstructed": reconstructed, - "continued": continued, - "observations": observations, - "result": result_path, - "workspace": temporary.path(), - "after_step": 1, - "max_steps": 1, - "session_id": "session", - "mode": "replay_only", - "boundary_user_prompt": "must not be injected" - })) - .unwrap(), - ) - .unwrap(); - - run_fake( - "mini", - "assets/mini_swe_agent_runner.py", - &request_path, - &live_marker, - ); - - let result: Value = serde_json::from_slice(&fs::read(result_path).unwrap()).unwrap(); - assert!(historical_marker.is_file()); - assert!(!live_marker.exists()); - assert_eq!(result["phase"], "replayed"); - assert_eq!(result["replayed_steps"], 1); - assert_eq!(result["continued_steps"], 0); - assert_eq!(result["boundary_user_prompt_injected"], false); - assert!(reconstructed.is_file()); - assert!(!continued.exists()); -} - -#[test] -fn mini_boundary_prompt_is_persisted_after_the_fresh_observation() { - let temporary = tempfile::tempdir().unwrap(); - let historical_marker = temporary.path().join("historical-marker"); - let live_marker = temporary.path().join("live-marker"); - let source = temporary.path().join("source.json"); - fs::write( - &source, - serde_json::to_vec(&json!({ - "info": {"config": {"model": {}, "environment": {}, "agent": {}}}, - "messages": [ - {"role": "system", "content": "system", "extra": {}}, - { - "role": "assistant", - "content": "historical action", - "extra": { - "response": {}, - "actions": [{"tool_call_id": "call-1", "marker": historical_marker}] - } - }, - {"role": "tool", "content": "old observation", "extra": {}} - ] - })) - .unwrap(), - ) - .unwrap(); - let request_path = temporary.path().join("request.json"); - let result_path = temporary.path().join("result.json"); - let continued = temporary.path().join("continued.json"); - fs::write( - &request_path, - serde_json::to_vec(&json!({ - "source": source, - "reconstructed": temporary.path().join("reconstructed.json"), - "continued": continued, - "observations": temporary.path().join("observations.json"), - "result": result_path, - "workspace": temporary.path(), - "after_step": 1, - "max_steps": 2, - "session_id": "session", - "mode": "replay_and_continue", - "boundary_user_prompt": "review O-prime N" - })) - .unwrap(), - ) - .unwrap(); - - run_fake( - "mini", - "assets/mini_swe_agent_runner.py", - &request_path, - &live_marker, - ); - - let result: Value = serde_json::from_slice(&fs::read(result_path).unwrap()).unwrap(); - let trajectory: Value = serde_json::from_slice(&fs::read(continued).unwrap()).unwrap(); - let messages = trajectory["messages"].as_array().unwrap(); - let prompt_index = messages - .iter() - .position(|message| message["content"] == "review O-prime N") - .unwrap(); - assert_eq!(messages[prompt_index]["role"], "user"); - assert_eq!(messages[prompt_index - 1]["content"], "fresh observation"); - assert_eq!(result["boundary_user_prompt_injected"], true); -} - -#[test] -fn swe_max_steps_caps_total_actions() { - let temporary = tempfile::tempdir().unwrap(); - let live_marker = temporary.path().join("live-marker"); - let source = temporary.path().join("source.traj"); - fs::write( - &source, - serde_json::to_vec(&json!({ - "replay_config": { - "agent": {"type": "default", "model": {}}, - "env": {}, - "problem_statement": {"type": "text", "text": "problem", "id": "fake"} - }, - "history": [ - {"role": "assistant", "content": "historical action"}, - {"role": "user", "content": "old observation"} - ], - "trajectory": [ - {"action": "historical action", "observation": "old observation"} - ] - })) - .unwrap(), - ) - .unwrap(); - let request_path = temporary.path().join("request.json"); - let result_path = temporary.path().join("result.json"); - let reconstructed = temporary.path().join("reconstructed.traj"); - let continued = temporary.path().join("continued.traj"); - fs::write( - &request_path, - serde_json::to_vec(&json!({ - "trajectory": source, - "reconstructed": reconstructed, - "continued": continued, - "result": result_path, - "workspace": temporary.path(), - "output_dir": temporary.path().join("agent-output"), - "after_step": 1, - "max_steps": 3, - "mode": "replay_and_continue", - "boundary_user_prompt": "review O-prime N" - })) - .unwrap(), - ) - .unwrap(); - - run_fake( - "swe", - "assets/swe_agent_runner.py", - &request_path, - &live_marker, - ); - - let result: Value = serde_json::from_slice(&fs::read(result_path).unwrap()).unwrap(); - assert_eq!(result["phase"], "continued"); - assert_eq!(result["agent_status"], "max_steps"); - assert_eq!(result["replayed_steps"], 1); - assert_eq!(result["continued_steps"], 2); - assert_eq!(result["boundary_user_prompt_injected"], true); - assert_eq!(fs::read_to_string(live_marker).unwrap().lines().count(), 2); - assert!(reconstructed.is_file()); - assert!(continued.is_file()); - let trajectory: Value = serde_json::from_slice(&fs::read(continued).unwrap()).unwrap(); - assert!( - trajectory["history"] - .as_array() - .unwrap() - .iter() - .any(|message| { - message["role"] == "user" && message["content"] == "review O-prime N" - }) - ); -} - -#[cfg(unix)] -#[test] -fn openhands_replay_only_stops_at_boundary() { - let temporary = tempfile::tempdir().unwrap(); - let report = execute(openhands_request( - temporary.path(), - ReplayMode::ReplayOnly, - 1, - )) - .unwrap(); - - assert_eq!(report.exit_code, 0); - assert_eq!(report.result.phase, ReplayPhase::Replayed); - assert_eq!(report.result.agent_status, AgentStatus::NotStarted); - assert_eq!(report.result.replayed_tool_calls, 1); - assert_eq!(report.result.continued_steps, 0); - assert!(!temporary.path().join("workspace/live-marker").exists()); - assert!(report.result.artifacts.iter().any(|artifact| { - artifact.role == "reconstructed_native_trajectory" - && artifact - .path - .ends_with("native/reconstructed-trajectory.json") - })); -} - -#[cfg(unix)] -#[test] -fn openhands_boundary_prompt_is_queued_after_the_replay_prefix() { - let temporary = tempfile::tempdir().unwrap(); - let mut request = openhands_request(temporary.path(), ReplayMode::ReplayAndContinue, 2); - request.boundary_user_prompt = Some("review O-prime N".into()); - - let report = execute(request).unwrap(); - - assert_eq!(report.exit_code, 0); - let prepared: Value = serde_json::from_slice( - &fs::read( - report - .result - .output_dir - .join("native/prepared-replay-events.json"), - ) - .unwrap(), - ) - .unwrap(); - let last = prepared.as_array().unwrap().last().unwrap(); - assert_eq!(last["source"], "user"); - assert_eq!(last["action"], "message"); - assert_eq!(last["args"]["content"], "review O-prime N"); - assert_eq!( - report.result.metadata["boundary_user_prompt"]["injected"], - true - ); - assert!( - !report - .result - .metadata - .to_string() - .contains("review O-prime N") - ); -} - -#[cfg(unix)] -#[test] -fn openhands_zero_exit_fatal_status_is_a_failed_result_with_trajectory() { - let temporary = tempfile::tempdir().unwrap(); - let workspace = temporary.path().join("workspace"); - fs::create_dir_all(&workspace).unwrap(); - fs::write(workspace.join("fatal-mode"), "1\n").unwrap(); - let report = execute(openhands_request( - temporary.path(), - ReplayMode::ReplayAndContinue, - 2, - )) - .unwrap(); - - assert_ne!(report.exit_code, 0); - assert_eq!(report.result.agent_status, AgentStatus::Failed); - assert!( - report - .result - .failure - .as_ref() - .is_some_and(|failure| { failure.message.contains("Error while running the agent") }) - ); - assert_eq!( - report.result.output_dir, - temporary.path().join("output/contract") - ); - assert_eq!( - report.result.state_dir, - temporary.path().join("state/contract") - ); - assert!(report.result.output_dir.join("result.json").is_file()); - let journal = fs::read_to_string(report.result.output_dir.join("replay-events.jsonl")).unwrap(); - let terminal: Value = serde_json::from_str(journal.lines().last().unwrap()).unwrap(); - assert_eq!(terminal["event"], "run_failed"); - assert!(report.result.artifacts.iter().any(|artifact| { - artifact.role == "continued_native_trajectory" && artifact.path.is_file() - })); -} - -#[cfg(unix)] -fn write_fake_pi_runtime(root: &Path) -> PathBuf { - use std::os::unix::fs::symlink; - - let bin = root.join("bin"); - let node_bin = root.join("node/bin"); - let package = root.join("npm-global/lib/node_modules/@earendil-works/pi-coding-agent"); - fs::create_dir_all(&bin).unwrap(); - fs::create_dir_all(&node_bin).unwrap(); - fs::create_dir_all(&package).unwrap(); - let entrypoint = bin.join("pi"); - fs::write(&entrypoint, "#!/bin/sh\nprintf '0.83.0\\n'\n").unwrap(); - let mut permissions = fs::metadata(&entrypoint).unwrap().permissions(); - permissions.set_mode(0o755); - fs::set_permissions(&entrypoint, permissions).unwrap(); - let node = Command::new("node") - .args(["-p", "process.execPath"]) - .output() - .unwrap(); - assert!(node.status.success()); - let node = String::from_utf8(node.stdout).unwrap(); - symlink(node.trim(), node_bin.join("node")).unwrap(); - fs::write( - package.join("package.json"), - r#"{"name":"@earendil-works/pi-coding-agent","type":"module","exports":{".":{"import":"./dist/index.mjs"}}}"#, - ) - .unwrap(); - fs::create_dir(package.join("dist")).unwrap(); - fs::write( - package.join("dist/index.mjs"), - r#" -import fs from "node:fs"; -export function createCodingTools() { - return [{ - name: "bash", - async execute(_id, args) { - fs.writeFileSync(args.marker, "fresh\n"); - return {content: [{type: "text", text: "fresh observation"}], details: {exitCode: 0}}; - }, - }]; -} -export const SessionManager = { - create() { return {appendMessage() {}}; }, -}; -"#, - ) - .unwrap(); - entrypoint -} - -#[cfg(unix)] -#[test] -fn pi_replay_only_executes_native_tool_prefix_without_a_model_request() { - let temporary = tempfile::tempdir().unwrap(); - let workspace = temporary.path().join("workspace"); - fs::create_dir(&workspace).unwrap(); - let marker = workspace.join("pi-replayed"); - let trajectory = temporary.path().join("pi-agent.events.jsonl"); - let events = [ - json!({ - "type": "message_end", - "message": {"role": "user", "content": [{"type": "text", "text": "fix it"}], "timestamp": 1} - }), - json!({ - "type": "turn_end", - "message": { - "role": "assistant", - "provider": "sweeval", - "model": "fake-model", - "api": "openai-completions", - "content": [{ - "type": "toolCall", "id": "call-1", "name": "bash", - "arguments": {"marker": marker} - }], - "stopReason": "toolUse", - "usage": {"input": 1, "output": 1}, - "timestamp": 2 - }, - "toolResults": [{ - "role": "toolResult", "toolCallId": "call-1", "toolName": "bash", - "content": [{"type": "text", "text": "old observation"}], - "isError": false, "timestamp": 3 - }] - }), - json!({ - "type": "turn_end", - "message": { - "role": "assistant", "provider": "sweeval", "model": "fake-model", - "api": "openai-completions", "content": [{"type": "text", "text": "next"}], - "stopReason": "stop", "usage": {"input": 2, "output": 1}, "timestamp": 4 - }, - "toolResults": [] - }), - ]; - fs::write( - &trajectory, - events - .iter() - .map(Value::to_string) - .collect::>() - .join("\n") - + "\n", - ) - .unwrap(); - let entrypoint = write_fake_pi_runtime(&temporary.path().join("pi-runtime")); - let request = PlaybackRequest { - agent: AgentKind::PiAgent, - trajectory, - after_step: 1, - workspace, - state_dir: temporary.path().join("state"), - output_dir: temporary.path().join("output"), - agent_entrypoint: Some(entrypoint), - agent_runtime: None, - disallowed_tools: Vec::new(), - trajectory_assets: None, - session_id: Some("pi-contract".into()), - max_steps: Some(1), - mode: ReplayMode::ReplayOnly, - allow_stale_observations: false, - run_id: Some("pi-contract".into()), - disable_thinking: false, - boundary_user_prompt: None, - }; - - let report = execute(request).unwrap(); - - assert_eq!(report.exit_code, 0, "{:#?}", report.result); - assert_eq!(report.result.phase, ReplayPhase::Replayed); - assert_eq!(report.result.agent_status, AgentStatus::NotStarted); - assert_eq!(report.result.replayed_tool_calls, 1); - assert_eq!(fs::read_to_string(marker).unwrap(), "fresh\n"); - assert!(report.result.artifacts.iter().any(|artifact| { - artifact.role == "reconstructed_native_trajectory" - && artifact.format == "pi-agent/native-events-jsonl-0.83.0" - && artifact.path.ends_with("native/reconstructed-events.jsonl") - })); -} diff --git a/docs/archive/legacy/en/ppilot/design/orchestration.md b/docs/archive/legacy/en/ppilot/design/orchestration.md deleted file mode 100644 index 9a689e193..000000000 --- a/docs/archive/legacy/en/ppilot/design/orchestration.md +++ /dev/null @@ -1,65 +0,0 @@ -# pPilot architecture - -pPilot is the durable Run orchestrator. Its product surface is deliberately -limited to `ppilot run` and `ppilot produce`. - -This page owns the many-Run control algorithm. The user workflow belongs to -[Orchestrate many Agent Runs](../guides/orchestrate.md), and the cross-product -commit boundary belongs to [System Design](../../system-design/architecture.md). - -```text -planner / plan() - │ stable task identity + backpressure - ▼ -pPilot ── RunSpec/file + process ──► pVisor ── RunResult ──► durable result journal - │ │ - │ lease / CAS │ EventRecord + Attempt state - └──────────────┬─────────────────┘ - ▼ - pChronicle control sidecar - │ - ▼ - durable history store -``` - -pPilot owns planning, bounded concurrency, leases, infrastructure retries, -resume/reconciliation, and result collection. pVisor owns each Run/Attempt and -its runtime drivers. pChronicle owns trajectory Dataset catalog, SQL, analysis, -find, import/export, and serving. - -pPilot does not link the pVisor implementation crate. It launches one -foreground `pvisor` binary per Run, submits a versioned `RunSpec`, and reads an -atomic `RunResult`. The job Supervisor's registration, heartbeat, quota, and -cancel messages are shared agentctl contracts. Process exit remains the -lifecycle boundary; a Supervisor connection supplies live control without a -resident pVisor daemon. - -The default pVisor build has no embedded Chronicle storage adapter and does not -link Lance or DataFusion. When durable publication is configured, pVisor starts -the Control component of `pchronicle serve` and publishes Attempt state plus -lifecycle/Gateway events through the shared `persisting-events` control contract. pPilot uses the same -contract for lease/CAS and result-journal coordination; each command owns the -sidecar process it starts. The executable is selected by the pVisor installation -or Run configuration; recording is selected with `--record-format` and -`--record-destination`. - -The local control protocol is versioned, request-correlated, authenticated with -a per-process token, and bound to loopback. It is a process boundary rather -than a second storage implementation: pChronicle alone selects the physical -backend and sends the durable acknowledgement. See -[RFC-0007](../../rfcs/0007-events-contract-pchronicle-sidecar.md). - -`run` executes a map-style `plan()` / `execute(item)` workload. `produce` -streams complete Run descriptions from a planner and creates one independent -pVisor workspace per item. Both start an in-process job Supervisor and publish -stable lineage (`parent_run_id`, `task_id`, and job metadata). - -The durable path uses monotonically increasing lease epochs and terminal CAS -to reject stale workers. On restart, pPilot reconciles its journal, Attempt -records, and Run control records before deciding whether to defer, recover, or -redispatch work. External effects still require application-level idempotency; -the system does not promise exactly-once execution. - -See the [`ppilot` command reference](../reference/cli.md) for the public -interface and [Run, Attempt, and Effect](../../pvisor/concepts/run-model.md) for the -identity and retry model used here. diff --git a/docs/archive/legacy/en/ppilot/get-started.md b/docs/archive/legacy/en/ppilot/get-started.md deleted file mode 100644 index 258e0598a..000000000 --- a/docs/archive/legacy/en/ppilot/get-started.md +++ /dev/null @@ -1,54 +0,0 @@ -# Get Started with pPilot - -This page runs the shortest verified pPilot loop: a streaming Python plan -executed by multiple workers, with terminal results written to a durable sink. - -## Install - -`ppilot` ships in the same Python wheel as `pvisor` and `pchronicle`: - -```bash -pip install persisting -ppilot --version -``` - -From a source checkout, `just install-cli` installs the same component set. - -## Define the work - -Create `plan.py`: - -```python -def plan(): - for value in range(6): - yield {"id": f"square-{value}", "value": value} - - -def execute(item): - return {"square": item["value"] ** 2} -``` - -`plan()` yields work items with stable `id`s; `execute(item)` processes one -item. Stable identity lets an interrupted job resume without repeating -completed work. - -## Run it - -```bash -ppilot run plan.py --workers 2 --per-worker 2 --sink ./results --results ndjson -``` - -## Verify the durable result - -```bash -cat ./results/ready.ndjson -``` - -Expected: six result records, one per task, with squares 0, 1, 4, 9, 16, 25 -(sum 55). A scripted version of this loop lives in -[`examples/ppilot/01-run/`](https://github.com/DeepLink-org/Persisting/tree/main/examples/ppilot/01-run). - -## Where to go next - -- [Orchestrate many Agent Runs](guides/orchestrate.md) — resume, retries, and production sinks -- [pPilot CLI reference](reference/cli.md) — `run` and `produce` flags diff --git a/docs/archive/legacy/en/ppilot/guides/orchestrate.md b/docs/archive/legacy/en/ppilot/guides/orchestrate.md deleted file mode 100644 index bfc1554b4..000000000 --- a/docs/archive/legacy/en/ppilot/guides/orchestrate.md +++ /dev/null @@ -1,59 +0,0 @@ -# Orchestrate many Agent Runs - -`pPilot` extends the Run model from one execution to a bounded collection of -tasks. It owns planning, concurrency, leases, retries for infrastructure -failures, durable result publication, and recovery. - -It does not redefine the Agent runtime. Each task remains an independent Run. - -## Define the work - -Create `plan.py`: - -```python -def plan(): - for value in range(6): - yield {"id": f"square-{value}", "value": value} - - -def execute(item): - value = item["value"] - return {"square": value * value} -``` - -Stable IDs are important: retries and reconciliation use them to identify the -same logical task. - -## Run with bounded concurrency - -```bash -ppilot run plan.py --workers 2 --per-worker 2 --sink ./results -``` - -`--workers` and `--per-worker` bound active work. `--sink` enables the durable -result journal and lease fencing. pPilot invokes the standalone `pvisor` -binary for every task; use `--pvisor-binary PATH` or -`PERSISTING_PVISOR_BIN` to select it explicitly. - -## Inspect durable results - -```bash -cat ./results/ready.ndjson -``` - -Infrastructure failures may be retried. Business errors are reported rather -than silently retried. The reconciler repairs the supported crash windows -around result publication. - -## Treat external effects explicitly - -Lease fencing protects result ownership; it cannot make an arbitrary external -API exactly-once. Use stable task IDs as idempotency keys, or make the external -operation transactional or compensatable. - -## Continue to history - -The result sink is not trajectory history. Capture Agent events during each Run -and use [pChronicle](../../pchronicle/get-started.md) to inspect them across Runs. - -For every runnable orchestration example, see [Reproducible examples](../../project/examples.md). diff --git a/docs/archive/legacy/en/ppilot/index.md b/docs/archive/legacy/en/ppilot/index.md deleted file mode 100644 index 42ae39254..000000000 --- a/docs/archive/legacy/en/ppilot/index.md +++ /dev/null @@ -1,24 +0,0 @@ -# pPilot - -**Durable Run production at scale.** - -pPilot extends the Run model from one execution to a bounded collection of -tasks. It owns planning, bounded concurrency, leases and fencing decisions, -infrastructure retry and recovery, reconciliation, durable result publication, -and task-to-Run mapping. - -It does not redefine the Agent runtime: each task remains an independent -[pVisor Run](../pvisor/concepts/run-model.md), executed by the standalone -`pvisor` binary. - -| Command | Owns | -| --- | --- | -| `ppilot run` | execute a `plan()` / `execute(item)` workload with durable recovery | -| `ppilot produce` | create independent pVisor Runs from a streaming planner | - -## Where to start - -- [Get Started](get-started.md) — run your first parallel plan in five minutes -- [Orchestrate many Agent Runs](guides/orchestrate.md) — planning, workers, resume, and sinks -- [Orchestration design](design/orchestration.md) — leases, fencing, and recovery guarantees -- [pPilot CLI reference](reference/cli.md) — exact flags and exit behavior diff --git a/docs/archive/legacy/en/ppilot/reference/cli.md b/docs/archive/legacy/en/ppilot/reference/cli.md deleted file mode 100644 index 423985a89..000000000 --- a/docs/archive/legacy/en/ppilot/reference/cli.md +++ /dev/null @@ -1,80 +0,0 @@ -# `ppilot` command reference - -`ppilot` is the scalable Run-production CLI. It exposes exactly two commands: - -```text -ppilot -├── run execute plan() / execute(item) with durable recovery -└── produce create independent pVisor Runs from a streaming planner -``` - -Dataset discovery, SQL, built-in analysis, find, import/export, and serving are -owned by [`pchronicle`](../../pchronicle/reference/cli.md). - -## `run` - -```bash -ppilot run plan.py --workers 8 --per-worker 2 --sink ./results -ppilot run plan.py --workers 8 --sink ./results --resume -ppilot run plan.py --check -ppilot run plan.py --pvisor-binary ./target/release/pvisor -``` - -The script defines `plan()` and `execute(item)`. pPilot applies bounded -concurrency and backpressure, writes terminal results to the durable sink, and -uses stable task identity for resume and retry. `--check` validates the plan -and a sample execution without running the full workload. - -`--results` selects the result stream on stdout: `ndjson` (one JSON object per -completed task), `summary` (a JSON summary after the job; failed tasks also -print NDJSON on stderr), or `quiet` (no result stream on stdout). The default -is `ndjson`. With `--observe`, the default becomes `quiet` so progress lines -are not drowned out; pass `--results ndjson` to keep both. - -## `produce` - -```bash -ppilot produce production.py --output ./runs --parallelism 8 -ppilot produce production.py --output ./runs --parallelism 8 \ - --cluster-network-limit 10mbps -- --dataset train -``` - -The planner's `plan()` may be a synchronous or asynchronous iterator. Each -item describes one Run: - -```python -def plan(): - for index in range(100): - yield { - "id": f"task-{index:04d}", - "agent": "codex", - "command": ["codex", "exec", f"Solve task {index}"], - "cwd": "/work/eval", - } -``` - -Each emitted item gets its own pVisor workspace below `--output`. The planner -is streamed under the concurrency window, so large batches are not fully held -in memory. The command writes `production-report.json`; any failed Run makes -the command exit unsuccessfully after the report is durable. - -`--cluster-network-limit` divides a conservative aggregate proxy rate across -the requested parallelism. It requires Gateway capture and does not cover -direct sockets that bypass the explicit proxy. - -## Runtime ownership - -Both commands start an in-process, job-scoped Supervisor. pPilot owns planning, -leases, retries, reconciliation, and collection. pVisor owns Run execution and -the embedded Gateway. pPilot invokes one foreground `pvisor` process per Run; -the two components share Run and Supervisor contracts through agentctl rather -than linking pVisor into pPilot. `--pvisor-binary` and -`PERSISTING_PVISOR_BIN` select an explicit executable. pChronicle owns -trajectory Dataset operations. - -The executable's `--help` is authoritative for flags and defaults. - -Use [Orchestrate many Agent Runs](../guides/orchestrate.md) for the complete -workflow, [pPilot architecture](../design/orchestration.md) for leases and -reconciliation, and [Run, Attempt, and Effect](../../pvisor/concepts/run-model.md) for the -retry identity model. diff --git a/docs/archive/legacy/zh/ppilot/design/orchestration.md b/docs/archive/legacy/zh/ppilot/design/orchestration.md deleted file mode 100644 index 81f81aeea..000000000 --- a/docs/archive/legacy/zh/ppilot/design/orchestration.md +++ /dev/null @@ -1,55 +0,0 @@ -# pPilot 架构 - -pPilot 是 durable Run 编排器。产品面刻意只暴露 `ppilot run` 和 `ppilot produce`。 - -本页负责多 Run 控制算法。用户工作流属于 -[编排多个 Agent Run](../guides/orchestrate.md),跨产品提交边界属于 -[系统架构](../../system-design/architecture.md)。 - -```text -planner / plan() - │ stable task identity + backpressure - ▼ -pPilot ── RunSpec/file + process ──► pVisor ── RunResult ──► durable result journal - │ │ - │ lease / CAS │ EventRecord + Attempt state - └──────────────┬─────────────────┘ - ▼ - pChronicle control sidecar - │ - ▼ - durable history store -``` - -pPilot 负责 planning、有界并发、lease、基础设施重试、resume/reconciliation -和结果收集。pVisor 负责每个 Run/Attempt 及其 runtime driver。pChronicle 负责 -轨迹 Dataset catalog、SQL、分析、find、导入导出与 serving。 - -pPilot 不链接 pVisor 实现 crate。它为每个 Run 启动一个前台 `pvisor` 二进制, -提交带版本的 `RunSpec`,并读取原子 `RunResult`。作业 Supervisor 的注册、 -heartbeat、quota 和 cancel 消息是共享的 agentctl 契约。进程退出仍是生命周期 -边界;Supervisor 连接提供实时控制,而不需要常驻 pVisor daemon。 - -默认 pVisor 构建没有内嵌 Chronicle 存储适配器,也不链接 Lance 或 DataFusion。 -配置了 durable publication 时,pVisor 启动 `pchronicle serve` 的 Control -组件,并通过共享的 `persisting-events` control 契约发布 Attempt 状态以及 -lifecycle/Gateway 事件。pPilot 用同一契约做 lease/CAS 和结果日志协调;每条 -命令各自拥有它启动的 sidecar 进程。可执行文件由 pVisor 安装或 Run 配置选择; -录制由 `--record-format` 与 `--record-destination` 选择。 - -本地 control 协议带版本、按请求关联、用进程级 token 鉴权,并绑定 loopback。 -它是进程边界,而不是第二套存储实现:只有 pChronicle 选择物理 backend,并 -发送 durable acknowledgement。见 -[RFC-0007](../../rfcs/0007-events-contract-pchronicle-sidecar.md)。 - -`run` 执行 map 风格的 `plan()` / `execute(item)` 工作负载。`produce` 从 -planner 流式接收完整 Run 描述,并为每项创建独立的 pVisor workspace。两者都 -启动进程内 job Supervisor,并发布稳定谱系(`parent_run_id`、`task_id` 和 -作业元数据)。 - -durable 路径使用单调递增的 lease epoch 和终端 CAS,拒绝过期 worker。重启时, -pPilot 先核对 journal、Attempt 记录和 Run 控制记录,再决定 defer、recover -还是 redispatch。外部 Effect 仍需要应用层幂等;系统不承诺 exactly-once 执行。 - -公开接口见 [`ppilot` 命令参考](../reference/cli.md),此处使用的身份与重试 -模型见 [Run、Attempt 与 Effect](../../pvisor/concepts/run-model.md)。 diff --git a/docs/archive/legacy/zh/ppilot/get-started.md b/docs/archive/legacy/zh/ppilot/get-started.md deleted file mode 100644 index 9f622a498..000000000 --- a/docs/archive/legacy/zh/ppilot/get-started.md +++ /dev/null @@ -1,53 +0,0 @@ -# 开始使用 pPilot - -本页走最短的已验证 pPilot 闭环:一份流式 Python plan,由多个 worker 执行, -终端结果写入 durable sink。 - -## 安装 - -`ppilot` 与 `pvisor`、`pchronicle` 装在同一个 Python wheel 里: - -```bash -pip install persisting -ppilot --version -``` - -从源码 checkout 时,`just install-cli` 会安装同一组组件。 - -## 定义工作 - -创建 `plan.py`: - -```python -def plan(): - for value in range(6): - yield {"id": f"square-{value}", "value": value} - - -def execute(item): - return {"square": item["value"] ** 2} -``` - -`plan()` 产出带稳定 `id` 的工作项;`execute(item)` 处理其中一项。稳定身份让 -被中断的作业可以 resume,而不重复已完成的工作。 - -## 运行 - -```bash -ppilot run plan.py --workers 2 --per-worker 2 --sink ./results --results ndjson -``` - -## 核对持久结果 - -```bash -cat ./results/ready.ndjson -``` - -预期:六条结果记录,每条任务一条,平方值为 0、1、4、9、16、25(合计 55)。 -脚本化版本见 -[`examples/ppilot/01-run/`](https://github.com/DeepLink-org/Persisting/tree/main/examples/ppilot/01-run)。 - -## 接下来去哪 - -- [编排多个 Agent Run](guides/orchestrate.md) — resume、重试与生产 sink -- [pPilot CLI 参考](reference/cli.md) — `run` 与 `produce` 标志 diff --git a/docs/archive/legacy/zh/ppilot/guides/orchestrate.md b/docs/archive/legacy/zh/ppilot/guides/orchestrate.md deleted file mode 100644 index 4b624360a..000000000 --- a/docs/archive/legacy/zh/ppilot/guides/orchestrate.md +++ /dev/null @@ -1,53 +0,0 @@ -# 编排多个 Agent Run - -`pPilot` 把 Run 模型从一次执行扩展到一组有界任务。它负责 planning、并发、lease、 -基础设施故障重试、持久结果发布和恢复。 - -它不会重新定义 Agent runtime;每个任务仍然是独立 Run。 - -## 定义任务 - -创建 `plan.py`: - -```python -def plan(): - for value in range(6): - yield {"id": f"square-{value}", "value": value} - - -def execute(item): - value = item["value"] - return {"square": value * value} -``` - -稳定 ID 很重要:retry 与 reconciliation 依靠它识别同一个逻辑任务。 - -## 使用有界并发运行 - -```bash -ppilot run plan.py --workers 2 --per-worker 2 --sink ./results -``` - -`--workers` 与 `--per-worker` 限制 active work;`--sink` 启用 durable result journal -和 lease fencing。 - -## 检查持久结果 - -```bash -cat ./results/ready.ndjson -``` - -基础设施故障可以重试;业务错误会被报告,而不是静默重试。Reconciler 修复结果发布 -附近已经支持的崩溃窗口。 - -## 显式处理外部 Effect - -Lease fencing 保护结果所有权,但不能让任意外部 API 自动拥有 exactly-once 语义。请把 -稳定 task ID 用作幂等键,或者让外部操作支持 transaction 或 compensation。 - -## 继续进入历史层 - -Result sink 不是轨迹历史。应在每个 Run 中捕获 Agent event,再使用 -[pChronicle](../../pchronicle/get-started.md)跨 Run 检查。 - -完整可运行编排案例见[可复现示例](../../project/examples.md)。 diff --git a/docs/archive/legacy/zh/ppilot/index.md b/docs/archive/legacy/zh/ppilot/index.md deleted file mode 100644 index 86c023d01..000000000 --- a/docs/archive/legacy/zh/ppilot/index.md +++ /dev/null @@ -1,22 +0,0 @@ -# pPilot - -**大规模、可恢复的 Run 生产。** - -pPilot 把 Run 模型从一次执行扩展到一组有界任务。它负责 planning、有界并发、 -lease 与 fencing 决策、基础设施重试与恢复、reconciliation、持久结果发布,以及 -task 到 Run 的映射。 - -它不会重新定义 Agent runtime:每个任务仍然是独立的 -[pVisor Run](../pvisor/concepts/run-model.md),由独立的 `pvisor` 二进制执行。 - -| 命令 | 负责 | -| --- | --- | -| `ppilot run` | 以可恢复的方式执行 `plan()` / `execute(item)` 工作负载 | -| `ppilot produce` | 从流式 planner 创建彼此独立的 pVisor Run | - -## 从这里开始 - -- [快速开始](get-started.md) — 五分钟跑完第一个并行 plan -- [编排多个 Agent Run](guides/orchestrate.md) — planning、worker、resume 与 sink -- [编排架构](design/orchestration.md) — lease、fencing 与恢复保证 -- [pPilot CLI 参考](reference/cli.md) — 精确的标志与退出行为 diff --git a/docs/archive/legacy/zh/ppilot/reference/cli.md b/docs/archive/legacy/zh/ppilot/reference/cli.md deleted file mode 100644 index c0dfcd9e9..000000000 --- a/docs/archive/legacy/zh/ppilot/reference/cli.md +++ /dev/null @@ -1,73 +0,0 @@ -# `ppilot` 命令参考 - -`ppilot` 是可扩展的 Run 生产 CLI。它只暴露两条命令: - -```text -ppilot -├── run execute plan() / execute(item) with durable recovery -└── produce create independent pVisor Runs from a streaming planner -``` - -Dataset 发现、SQL、内置分析、find、导入导出与 serving 由 -[`pchronicle`](../../pchronicle/reference/cli.md) 负责。 - -## `run` - -```bash -ppilot run plan.py --workers 8 --per-worker 2 --sink ./results -ppilot run plan.py --workers 8 --sink ./results --resume -ppilot run plan.py --check -ppilot run plan.py --pvisor-binary ./target/release/pvisor -``` - -脚本定义 `plan()` 和 `execute(item)`。pPilot 施加有界并发与 backpressure, -把终端结果写入 durable sink,并用稳定 task 身份做 resume 与 retry。`--check` -校验 plan 和一次样例执行,而不跑完整工作负载。 - -`--results` 选择 stdout 上的结果流:`ndjson`(每个完成的 task 一行 JSON)、 -`summary`(作业结束后输出 JSON 汇总;失败 task 还会在 stderr 打印 NDJSON)、 -或 `quiet`(stdout 不输出结果流)。默认是 `ndjson`。配合 `--observe` 时默认变为 -`quiet`,以免进度行被淹没;需要两者并存时传 `--results ndjson`。 - -## `produce` - -```bash -ppilot produce production.py --output ./runs --parallelism 8 -ppilot produce production.py --output ./runs --parallelism 8 \ - --cluster-network-limit 10mbps -- --dataset train -``` - -planner 的 `plan()` 可以是同步或异步 iterator。每一项描述一次 Run: - -```python -def plan(): - for index in range(100): - yield { - "id": f"task-{index:04d}", - "agent": "codex", - "command": ["codex", "exec", f"Solve task {index}"], - "cwd": "/work/eval", - } -``` - -每个产出项在 `--output` 下拥有自己的 pVisor workspace。planner 在并发窗口内 -流式消费,因此大批次不会完整驻留内存。命令写入 `production-report.json`; -任一 Run 失败会在报告落盘后让命令以失败退出。 - -`--cluster-network-limit` 把保守的聚合代理速率按请求的 parallelism 均分。 -它要求 Gateway capture,且不覆盖绕过显式代理的直接 socket。 - -## 运行时所有权 - -两条命令都启动进程内、作业范围的 Supervisor。pPilot 负责 planning、lease、 -重试、reconciliation 和收集。pVisor 负责 Run 执行和内嵌 Gateway。pPilot 为 -每个 Run 调用一个前台 `pvisor` 进程;两个组件通过 agentctl 共享 Run 与 -Supervisor 契约,而不是把 pVisor 链进 pPilot。`--pvisor-binary` 和 -`PERSISTING_PVISOR_BIN` 选择显式可执行文件。pChronicle 负责轨迹 Dataset -操作。 - -可执行文件的 `--help` 是标志与默认值的权威来源。 - -完整工作流见 [编排多个 Agent Run](../guides/orchestrate.md),lease 与 -reconciliation 见 [pPilot 架构](../design/orchestration.md),重试身份模型见 -[Run、Attempt 与 Effect](../../pvisor/concepts/run-model.md)。 diff --git a/docs/overrides/home.html b/docs/overrides/home.html index 9b20d9674..ede12c6ae 100644 --- a/docs/overrides/home.html +++ b/docs/overrides/home.html @@ -8,13 +8,13 @@
- -
{% if zh %}快速开始{% else %}Quick start{% endif %}
$ pip install persisting$ pvisor run --stage ./runs/task-001 -- codex$ pchronicle onboard
+

Persisting

{% if zh %}Agent 时代的持久化基础设施{% else %}Persistent Infrastructure for the Agent Era{% endif %}

{% if zh %}捕获、导入并查询 Agent 轨迹,把真实发生的事情保存为持久历史。{% else %}Capture, import, and query Agent trajectories as durable history.{% endif %}

{% if zh %}从事件和外部轨迹到 Dataset,捕获、分析与留存沿着同一条可追溯路径连接起来。{% else %}From captured events to Datasets, capture, analysis, and durable history follow one traceable path.{% endif %}

+
{% if zh %}快速开始{% else %}Quick start{% endif %}
$ pip install persisting$ pchronicle onboard

{% if zh %}Agent 历史 = Dataset + Query{% else %}Agent history = Dataset + query{% endif %}

{% if zh %}让 Agent 更容易理解和改进{% else %}Make agents easier to understand and improve.{% endif %}

{% if zh %}Harness 让 Agent 继续工作,Chronicle 把运行变成持久、可查询的历史,让下一次决策能看到真实发生过什么。{% else %}A harness keeps an agent working. Chronicle keeps the run as durable, queryable history so the next decision can see what actually happened.{% endif %}

{% if zh %}Datasets{% else %}Datasets{% endif %}

{% if zh %}挂载捕获或导入的 Source,在深入查看前先看到运行数量和范围。{% else %}Mount captured or imported Sources and see run counts before you drill in.{% endif %}

Trajectory

{% if zh %}从一条事件流重建完整运行:提示、工具调用、结果和上下文注入。{% else %}Reconstruct a complete run from one event stream: prompts, tool calls, results, and context injection.{% endif %}

Analysis

{% if zh %}针对同一个 Snapshot 提问,或执行有边界的只读 SQL。{% else %}Ask a question or run bounded SQL against the same Snapshot the warehouse is serving.{% endif %}

-

{% if zh %}设计方法{% else %}Design approach{% endif %}

{% if zh %}每次运行都是 Dataset,每次查询都有范围{% else %}Every run is a Dataset. Every query is scoped.{% endif %}

{% if zh %}执行先于解释{% else %}Execution before interpretation{% endif %}

{% if zh %}先在 staged workspace 中运行并审查,再决定哪些 Effect 进入项目。{% else %}Run and review inside a staged workspace, then decide which Effects reach the project.{% endif %}

{% if zh %}历史保持可追溯{% else %}Every run is traceable{% endif %}

{% if zh %}按 Source 检查记录,查询、恢复和回放都基于同一条事件流。{% else %}Inspect records by Source; query, resume, and replay operate on the same event stream.{% endif %}

{% if zh %}Dataset 概览{% else %}Dataset overview{% endif %}{% if zh %}运行轨迹{% else %}Run trajectory{% endif %}
-

{% if zh %}文档入口{% else %}Documentation surfaces{% endif %}

{% if zh %}从你当前的问题开始{% else %}Start with the question you have.{% endif %}

+

{% if zh %}设计方法{% else %}Design approach{% endif %}

{% if zh %}每次运行都是 Dataset,每次查询都有范围{% else %}Every run is a Dataset. Every query is scoped.{% endif %}

{% if zh %}事实先于解释{% else %}Facts before interpretation{% endif %}

{% if zh %}保留原始事件和来源,再构建用于查询的投影。{% else %}Preserve source events before building queryable projections.{% endif %}

{% if zh %}历史保持可追溯{% else %}Every run is traceable{% endif %}

{% if zh %}按 Source 检查记录,查询、恢复和回放都基于同一条事件流。{% else %}Inspect records by Source; query, resume, and replay operate on the same event stream.{% endif %}

{% if zh %}Dataset 概览{% else %}Dataset overview{% endif %}{% if zh %}运行轨迹{% else %}Run trajectory{% endif %}
+

{% if zh %}文档入口{% else %}Documentation surfaces{% endif %}

{% if zh %}从你当前的问题开始{% else %}Start with the question you have.{% endif %}

{% if zh %}开源 · Apache-2.0 · Persisting{% else %}Open source · Apache-2.0 · Persisting{% endif %}

GitHub

{% endblock %} diff --git a/docs/src/en/guides/using-persisting.md b/docs/src/en/guides/using-persisting.md index 0712258cf..08519774c 100644 --- a/docs/src/en/guides/using-persisting.md +++ b/docs/src/en/guides/using-persisting.md @@ -3,24 +3,17 @@ Choose the smallest workflow that answers the question in front of you. You do not need to adopt every component at once. -## I need an Agent to change a project safely - -Start with [pVisor](../pvisor/get-started.md): run one Agent in a staged -workspace, review the Run Bundle, and apply only a trusted path. Add network or -provider controls when the next Run needs them. - ## I already have trajectory data Start with [pChronicle](../pchronicle/get-started.md): open a Dataset, inspect a summary, ask one bounded SQL question, and locate the evidence behind the answer. Use exchange or serving guides only after the read-only path works. -## I need execution and history together +## I need to capture new model traffic -Use [pVisor capture](../pvisor/guides/capture.md) when lifecycle events from a -Run should become a pChronicle Source. The private Run Bundle remains a local -execution record; capture is an explicit handoff, not an implicit copy of every -artifact. +Use the [Gateway](../pchronicle/guides/serve-gateway.md) to forward requests +and persist canonical events. External execution components can submit events +through the pChronicle Control service. ## A reliable operating habit diff --git a/docs/src/en/index.md b/docs/src/en/index.md index e0a50e30d..62ee42a18 100644 --- a/docs/src/en/index.md +++ b/docs/src/en/index.md @@ -1,7 +1,7 @@ --- template: home.html title: Persistent Infrastructure for the Agent Era -description: Run Agents inside a reviewable execution boundary and preserve queryable history. +description: Capture, import, and query durable Agent trajectory history. hide: - navigation - toc diff --git a/docs/src/en/installation.md b/docs/src/en/installation.md index e8d2c38c3..35f64a39e 100644 --- a/docs/src/en/installation.md +++ b/docs/src/en/installation.md @@ -1,108 +1,28 @@ # Installation -Persisting gives you two public command-line paths: +This repository distributes the `pchronicle` command and embedded Web UI. +pVisor and pPilot are maintained in external repositories. -- `pvisor` runs an Agent inside a reviewable execution boundary. -- `pchronicle` opens, queries, exchanges, and serves trajectory Datasets. - -The default install includes the complete public CLI and Dataset workflow. - -## 1. Install the tools +## Install ```bash pip install persisting +pchronicle --version +pchronicle onboard ``` -Verify that the commands are available: - -```bash -pvisor --version -pchronicle --help -``` - -The wheel installs matching versions of the Python package and the public CLI -entry points into the active Python environment. Use a virtual environment when -the project has other Python dependencies: - -```bash -python3 -m venv .venv -source .venv/bin/activate -python -m pip install --upgrade pip -pip install persisting -``` - -!!! tip "You can start with either product" - - You do not need a pVisor Run to explore pChronicle. If you want to run an Agent - first, continue with [Run your first Agent](pvisor/get-started.md). If you - already have trajectory data, continue with [Explore your first Dataset](pchronicle/get-started.md). - -## 2. Check platform requirements - -The CLI supports macOS and Linux with Python 3.10 or newer. A normal host Run -works without a filesystem extension. On macOS, install macFUSE before using a -host-process staged Run (`pvisor run --stage …`): - -```bash -brew install --cask macfuse -``` - -Approve the macFUSE system extension when macOS asks. Without `--stage`, the -Agent may write the real project tree. With `--stage`, if the required mount -capability is unavailable, the Run fails closed rather than silently writing -the workspace without COW. The libkrun VM executor does not require macFUSE. - -## 3. Install from source when needed +Platform wheels support Linux x86_64 and Apple Silicon macOS with Python 3.10+. +They install the Python package and `pchronicle` into the active environment. -Use the nightly wheel when you need the latest build published from `main`: +## Nightly or source builds ```bash curl -fsSL https://raw.githubusercontent.com/DeepLink-org/Persisting/main/scripts/install-nightly.sh | bash ``` -For local development, install the Python package from a checkout: - -```bash -git clone https://github.com/DeepLink-org/Persisting.git -cd Persisting -pip install -e . -``` - -A source build of the CLI components is also available: - -```bash -just install-cli -``` - -Use `PERSISTING_PVISOR_BIN` only when you deliberately need to test a specific -pVisor binary. Keep the Python package and CLI from the same revision when -debugging provider behavior. - -## 4. Enable VM or OCI execution when needed - -The default local workflow does not require Docker or Podman. To run an OCI -image through the VM executor, provide an image explicitly: - -```bash -pvisor run --image ubuntu:latest -- COMMAND -``` - -`ubuntu:latest` is also the default VM image. `--image-store DIR` changes the -local content-addressed cache, `--overlayfs-target` selects the guest workspace, -and `--vm-rootfs DIR` points to a prepared Linux rootfs. Linux hosts use KVM; -Apple Silicon macOS hosts use HVF. Building the VM support from source on macOS -also requires Zig: - -```bash -brew install zig -``` - -Treat these options as a separate platform step. First complete the staged host -workflow so that you have a baseline Run Bundle to compare against. - -## 5. Choose the next step +From a checkout, `pip install -e .` builds pChronicle and its Web assets. +`just install-cli` installs the Rust CLI; building embedded Web assets requires +Dioxus CLI. See [Engineering notes](project/engineering.md). -- [Run your first Agent](pvisor/get-started.md) — stage, review, and selectively apply changes. -- [Explore your first Dataset](pchronicle/get-started.md) — query temporary data before preparing a source. -- [Choose a workflow](overview.md) — decide which product matches the work in front of you. -- [Execution environments](pvisor/guides/execution.md) — compare host, OCI, and VM boundaries. +Continue with [Explore your first Dataset](pchronicle/get-started.md) or +[Gateway capture](pchronicle/guides/serve-gateway.md). diff --git a/docs/src/en/overview.md b/docs/src/en/overview.md index 2149c1563..9805744e3 100644 --- a/docs/src/en/overview.md +++ b/docs/src/en/overview.md @@ -5,46 +5,20 @@ hide: # Get started -Follow one path from an installed CLI to an Agent run you can review and query. +Capture, import, and query durable Agent history with pChronicle. ## 1. Installation -Install the command-line tools and confirm both product entry points: +Install the command-line tools and confirm the pChronicle entry point: ```bash pip install persisting -pvisor --help pchronicle --help ``` -On macOS, install macFUSE before using a staged host workspace: - -```bash -brew install --cask macfuse -``` - [Read the installation guide →](installation.md) -## 2. Running an Agent with pVisor - -Run an Agent in a staged workspace, inspect what actually happened, and apply -only the changes you trust: - -```bash -pvisor run --stage ./runs/task-001 -- codex -pvisor review last -pvisor apply last --path src -``` - -The base project stays unchanged while the Agent works. The Run Bundle records -filesystem Effects, effective controls, network evidence, and warnings. Continue -with [Run your first Agent](pvisor/get-started.md) for the complete walkthrough, -then learn [selective apply](pvisor/guides/review-apply.md). - -**At the end of this section:** you have a reviewed project change and a clear -record of what remains staged. - -## 3. Recording and Analyzing Agent Trajectories +## 2. Recording and Analyzing Agent Trajectories Once an Agent has run, use pChronicle to turn its trajectory into a Dataset you can inspect and query. Start with a temporary example so the workflow is safe: @@ -66,5 +40,5 @@ Continue with [Explore your first Dataset](pchronicle/get-started.md) to learn Dataset health, evidence location, formats, exchange, and the read-only Web/API. **At the end of this section:** you can connect an answer to the Dataset and -Source that produced it. If you need the two products together, continue with -[pVisor capture](pvisor/guides/capture.md). +Source that produced it. Use [Gateway capture](pchronicle/guides/serve-gateway.md) +to record new model traffic. diff --git a/docs/src/en/pchronicle/design/trajectory-storage.md b/docs/src/en/pchronicle/design/trajectory-storage.md index c365b7252..bc1252f53 100644 --- a/docs/src/en/pchronicle/design/trajectory-storage.md +++ b/docs/src/en/pchronicle/design/trajectory-storage.md @@ -228,6 +228,6 @@ formats are handled by `pchronicle import/export`. - [Discover and query](../guides/discover-and-query.md) - [Snapshot](catalog.md) - [AgenticMD format](../reference/agenticmd.md) -- [Gateway architecture](../../pvisor/design/gateway.md) -- [pVisor CLI](../../pvisor/reference/cli.md) +- Gateway architecture (external repository) +- pVisor CLI (external repository) - [`pchronicle` Dataset commands](../reference/cli.md) diff --git a/docs/src/en/pchronicle/get-started.md b/docs/src/en/pchronicle/get-started.md index 44834baf3..022cf2bd9 100644 --- a/docs/src/en/pchronicle/get-started.md +++ b/docs/src/en/pchronicle/get-started.md @@ -134,7 +134,6 @@ use `--errors strict` in automation when partial results should fail the job. - [Open the local Web UI](guides/ui.md) - [Import or export Runs](guides/exchange.md) - [Serve a Dataset locally](guides/serve.md) -- [Capture a new Run with pVisor](../pvisor/guides/capture.md) - [Learn the Dataset and Source model](concepts/index.md) The walkthrough Dataset is temporary. Use your own path before moving to diff --git a/docs/src/en/pchronicle/guides/index.md b/docs/src/en/pchronicle/guides/index.md index 67abb2ec6..9dd6ddb66 100644 --- a/docs/src/en/pchronicle/guides/index.md +++ b/docs/src/en/pchronicle/guides/index.md @@ -8,7 +8,6 @@ task-oriented workflow. 3. [Serve Datasets locally](serve.md). 4. [Browse, drill down, and analyze in the local Web UI](ui.md). 5. [Forward, rewrite, and capture through the `serve` Gateway](serve-gateway.md). -6. [Capture a new Run with pVisor](../../pvisor/guides/capture.md). The guides explain decisions and complete workflows. Use the [`pchronicle` reference](../reference/cli.md) for exact flags, and diff --git a/docs/src/en/pchronicle/guides/serve-gateway.md b/docs/src/en/pchronicle/guides/serve-gateway.md index 0bc295e1d..4714a274a 100644 --- a/docs/src/en/pchronicle/guides/serve-gateway.md +++ b/docs/src/en/pchronicle/guides/serve-gateway.md @@ -68,7 +68,7 @@ remain read-only. Use this mode when an Agent or SDK already knows how to call an OpenAI-, Anthropic-, or Gemini-compatible base URL and you want to capture that traffic -without starting a pVisor Run. Use [pVisor capture](../../pvisor/guides/capture.md) +without starting a pVisor Run. Use pVisor capture (external repository) instead when the Gateway must share the lifecycle and isolation boundary of an Agent execution. @@ -183,7 +183,7 @@ closes the corresponding capture call. The shared Gateway schema also accepts an `[overlay]` table, but `pchronicle serve` does not create or apply a filesystem overlay. Overlay -lifecycle belongs to [pVisor](../../pvisor/guides/execution.md). +lifecycle belongs to pVisor (external repository). ### Capture levels diff --git a/docs/src/en/pchronicle/index.md b/docs/src/en/pchronicle/index.md index 04e4d9587..21bc6aae4 100644 --- a/docs/src/en/pchronicle/index.md +++ b/docs/src/en/pchronicle/index.md @@ -72,7 +72,7 @@ When you already have a question, follow the matching path: pChronicle reads and organizes run history. It does not execute or schedule Agents. To run an Agent in a controlled workspace, start with -[pVisor](../pvisor/index.md). +pVisor (external repository). ## A useful reading order diff --git a/docs/src/en/pchronicle/reference/agenticmd.md b/docs/src/en/pchronicle/reference/agenticmd.md index 0978fbeb1..39d87e75f 100644 --- a/docs/src/en/pchronicle/reference/agenticmd.md +++ b/docs/src/en/pchronicle/reference/agenticmd.md @@ -92,7 +92,6 @@ exchange uses the formats supported by ## 6. Examples and implementation -- Gateway end-to-end quantitative example: `examples/pvisor/04-gateway-llm-control/` - Lance/ATIF storage and analysis examples: `examples/pchronicle/` - Format and view implementation: `crates/persisting-pchronicle/src/formats/`, `src/projection/` - [pChronicle run storage](../design/trajectory-storage.md) diff --git a/docs/src/en/ppilot/design/orchestration.md b/docs/src/en/ppilot/design/orchestration.md deleted file mode 100644 index 9a689e193..000000000 --- a/docs/src/en/ppilot/design/orchestration.md +++ /dev/null @@ -1,65 +0,0 @@ -# pPilot architecture - -pPilot is the durable Run orchestrator. Its product surface is deliberately -limited to `ppilot run` and `ppilot produce`. - -This page owns the many-Run control algorithm. The user workflow belongs to -[Orchestrate many Agent Runs](../guides/orchestrate.md), and the cross-product -commit boundary belongs to [System Design](../../system-design/architecture.md). - -```text -planner / plan() - │ stable task identity + backpressure - ▼ -pPilot ── RunSpec/file + process ──► pVisor ── RunResult ──► durable result journal - │ │ - │ lease / CAS │ EventRecord + Attempt state - └──────────────┬─────────────────┘ - ▼ - pChronicle control sidecar - │ - ▼ - durable history store -``` - -pPilot owns planning, bounded concurrency, leases, infrastructure retries, -resume/reconciliation, and result collection. pVisor owns each Run/Attempt and -its runtime drivers. pChronicle owns trajectory Dataset catalog, SQL, analysis, -find, import/export, and serving. - -pPilot does not link the pVisor implementation crate. It launches one -foreground `pvisor` binary per Run, submits a versioned `RunSpec`, and reads an -atomic `RunResult`. The job Supervisor's registration, heartbeat, quota, and -cancel messages are shared agentctl contracts. Process exit remains the -lifecycle boundary; a Supervisor connection supplies live control without a -resident pVisor daemon. - -The default pVisor build has no embedded Chronicle storage adapter and does not -link Lance or DataFusion. When durable publication is configured, pVisor starts -the Control component of `pchronicle serve` and publishes Attempt state plus -lifecycle/Gateway events through the shared `persisting-events` control contract. pPilot uses the same -contract for lease/CAS and result-journal coordination; each command owns the -sidecar process it starts. The executable is selected by the pVisor installation -or Run configuration; recording is selected with `--record-format` and -`--record-destination`. - -The local control protocol is versioned, request-correlated, authenticated with -a per-process token, and bound to loopback. It is a process boundary rather -than a second storage implementation: pChronicle alone selects the physical -backend and sends the durable acknowledgement. See -[RFC-0007](../../rfcs/0007-events-contract-pchronicle-sidecar.md). - -`run` executes a map-style `plan()` / `execute(item)` workload. `produce` -streams complete Run descriptions from a planner and creates one independent -pVisor workspace per item. Both start an in-process job Supervisor and publish -stable lineage (`parent_run_id`, `task_id`, and job metadata). - -The durable path uses monotonically increasing lease epochs and terminal CAS -to reject stale workers. On restart, pPilot reconciles its journal, Attempt -records, and Run control records before deciding whether to defer, recover, or -redispatch work. External effects still require application-level idempotency; -the system does not promise exactly-once execution. - -See the [`ppilot` command reference](../reference/cli.md) for the public -interface and [Run, Attempt, and Effect](../../pvisor/concepts/run-model.md) for the -identity and retry model used here. diff --git a/docs/src/en/ppilot/get-started.md b/docs/src/en/ppilot/get-started.md deleted file mode 100644 index 258e0598a..000000000 --- a/docs/src/en/ppilot/get-started.md +++ /dev/null @@ -1,54 +0,0 @@ -# Get Started with pPilot - -This page runs the shortest verified pPilot loop: a streaming Python plan -executed by multiple workers, with terminal results written to a durable sink. - -## Install - -`ppilot` ships in the same Python wheel as `pvisor` and `pchronicle`: - -```bash -pip install persisting -ppilot --version -``` - -From a source checkout, `just install-cli` installs the same component set. - -## Define the work - -Create `plan.py`: - -```python -def plan(): - for value in range(6): - yield {"id": f"square-{value}", "value": value} - - -def execute(item): - return {"square": item["value"] ** 2} -``` - -`plan()` yields work items with stable `id`s; `execute(item)` processes one -item. Stable identity lets an interrupted job resume without repeating -completed work. - -## Run it - -```bash -ppilot run plan.py --workers 2 --per-worker 2 --sink ./results --results ndjson -``` - -## Verify the durable result - -```bash -cat ./results/ready.ndjson -``` - -Expected: six result records, one per task, with squares 0, 1, 4, 9, 16, 25 -(sum 55). A scripted version of this loop lives in -[`examples/ppilot/01-run/`](https://github.com/DeepLink-org/Persisting/tree/main/examples/ppilot/01-run). - -## Where to go next - -- [Orchestrate many Agent Runs](guides/orchestrate.md) — resume, retries, and production sinks -- [pPilot CLI reference](reference/cli.md) — `run` and `produce` flags diff --git a/docs/src/en/ppilot/guides/orchestrate.md b/docs/src/en/ppilot/guides/orchestrate.md deleted file mode 100644 index bfc1554b4..000000000 --- a/docs/src/en/ppilot/guides/orchestrate.md +++ /dev/null @@ -1,59 +0,0 @@ -# Orchestrate many Agent Runs - -`pPilot` extends the Run model from one execution to a bounded collection of -tasks. It owns planning, concurrency, leases, retries for infrastructure -failures, durable result publication, and recovery. - -It does not redefine the Agent runtime. Each task remains an independent Run. - -## Define the work - -Create `plan.py`: - -```python -def plan(): - for value in range(6): - yield {"id": f"square-{value}", "value": value} - - -def execute(item): - value = item["value"] - return {"square": value * value} -``` - -Stable IDs are important: retries and reconciliation use them to identify the -same logical task. - -## Run with bounded concurrency - -```bash -ppilot run plan.py --workers 2 --per-worker 2 --sink ./results -``` - -`--workers` and `--per-worker` bound active work. `--sink` enables the durable -result journal and lease fencing. pPilot invokes the standalone `pvisor` -binary for every task; use `--pvisor-binary PATH` or -`PERSISTING_PVISOR_BIN` to select it explicitly. - -## Inspect durable results - -```bash -cat ./results/ready.ndjson -``` - -Infrastructure failures may be retried. Business errors are reported rather -than silently retried. The reconciler repairs the supported crash windows -around result publication. - -## Treat external effects explicitly - -Lease fencing protects result ownership; it cannot make an arbitrary external -API exactly-once. Use stable task IDs as idempotency keys, or make the external -operation transactional or compensatable. - -## Continue to history - -The result sink is not trajectory history. Capture Agent events during each Run -and use [pChronicle](../../pchronicle/get-started.md) to inspect them across Runs. - -For every runnable orchestration example, see [Reproducible examples](../../project/examples.md). diff --git a/docs/src/en/ppilot/index.md b/docs/src/en/ppilot/index.md deleted file mode 100644 index 42ae39254..000000000 --- a/docs/src/en/ppilot/index.md +++ /dev/null @@ -1,24 +0,0 @@ -# pPilot - -**Durable Run production at scale.** - -pPilot extends the Run model from one execution to a bounded collection of -tasks. It owns planning, bounded concurrency, leases and fencing decisions, -infrastructure retry and recovery, reconciliation, durable result publication, -and task-to-Run mapping. - -It does not redefine the Agent runtime: each task remains an independent -[pVisor Run](../pvisor/concepts/run-model.md), executed by the standalone -`pvisor` binary. - -| Command | Owns | -| --- | --- | -| `ppilot run` | execute a `plan()` / `execute(item)` workload with durable recovery | -| `ppilot produce` | create independent pVisor Runs from a streaming planner | - -## Where to start - -- [Get Started](get-started.md) — run your first parallel plan in five minutes -- [Orchestrate many Agent Runs](guides/orchestrate.md) — planning, workers, resume, and sinks -- [Orchestration design](design/orchestration.md) — leases, fencing, and recovery guarantees -- [pPilot CLI reference](reference/cli.md) — exact flags and exit behavior diff --git a/docs/src/en/ppilot/reference/cli.md b/docs/src/en/ppilot/reference/cli.md deleted file mode 100644 index 423985a89..000000000 --- a/docs/src/en/ppilot/reference/cli.md +++ /dev/null @@ -1,80 +0,0 @@ -# `ppilot` command reference - -`ppilot` is the scalable Run-production CLI. It exposes exactly two commands: - -```text -ppilot -├── run execute plan() / execute(item) with durable recovery -└── produce create independent pVisor Runs from a streaming planner -``` - -Dataset discovery, SQL, built-in analysis, find, import/export, and serving are -owned by [`pchronicle`](../../pchronicle/reference/cli.md). - -## `run` - -```bash -ppilot run plan.py --workers 8 --per-worker 2 --sink ./results -ppilot run plan.py --workers 8 --sink ./results --resume -ppilot run plan.py --check -ppilot run plan.py --pvisor-binary ./target/release/pvisor -``` - -The script defines `plan()` and `execute(item)`. pPilot applies bounded -concurrency and backpressure, writes terminal results to the durable sink, and -uses stable task identity for resume and retry. `--check` validates the plan -and a sample execution without running the full workload. - -`--results` selects the result stream on stdout: `ndjson` (one JSON object per -completed task), `summary` (a JSON summary after the job; failed tasks also -print NDJSON on stderr), or `quiet` (no result stream on stdout). The default -is `ndjson`. With `--observe`, the default becomes `quiet` so progress lines -are not drowned out; pass `--results ndjson` to keep both. - -## `produce` - -```bash -ppilot produce production.py --output ./runs --parallelism 8 -ppilot produce production.py --output ./runs --parallelism 8 \ - --cluster-network-limit 10mbps -- --dataset train -``` - -The planner's `plan()` may be a synchronous or asynchronous iterator. Each -item describes one Run: - -```python -def plan(): - for index in range(100): - yield { - "id": f"task-{index:04d}", - "agent": "codex", - "command": ["codex", "exec", f"Solve task {index}"], - "cwd": "/work/eval", - } -``` - -Each emitted item gets its own pVisor workspace below `--output`. The planner -is streamed under the concurrency window, so large batches are not fully held -in memory. The command writes `production-report.json`; any failed Run makes -the command exit unsuccessfully after the report is durable. - -`--cluster-network-limit` divides a conservative aggregate proxy rate across -the requested parallelism. It requires Gateway capture and does not cover -direct sockets that bypass the explicit proxy. - -## Runtime ownership - -Both commands start an in-process, job-scoped Supervisor. pPilot owns planning, -leases, retries, reconciliation, and collection. pVisor owns Run execution and -the embedded Gateway. pPilot invokes one foreground `pvisor` process per Run; -the two components share Run and Supervisor contracts through agentctl rather -than linking pVisor into pPilot. `--pvisor-binary` and -`PERSISTING_PVISOR_BIN` select an explicit executable. pChronicle owns -trajectory Dataset operations. - -The executable's `--help` is authoritative for flags and defaults. - -Use [Orchestrate many Agent Runs](../guides/orchestrate.md) for the complete -workflow, [pPilot architecture](../design/orchestration.md) for leases and -reconciliation, and [Run, Attempt, and Effect](../../pvisor/concepts/run-model.md) for the -retry identity model. diff --git a/docs/src/en/project/engineering.md b/docs/src/en/project/engineering.md index 464dd4d7c..0f97f1b5b 100644 --- a/docs/src/en/project/engineering.md +++ b/docs/src/en/project/engineering.md @@ -11,17 +11,17 @@ Run these from the repository root. `just --list` shows the full recipe set. | Command | What it does | |---|---| | `just test` | Workspace Rust tests through `cargo nextest`, then the Python suite | -| `just test ` | One crate or Cargo package (for example `pvisor` or `persisting-pvisor`) | +| `just test ` | One crate or Cargo package (for example `pchronicle` or `persisting-pchronicle`) | | `just docs-sync` | Install the locked documentation environment | | `just docs-serve` | Local Zensical preview with automatic reload when files change | | `just docs-serve-dirty` | Local Zensical preview when automatic reload stalls | | `just docs-build` | Build the static documentation site | -| `just examples` | pVisor and pChronicle product example suites | +| `just examples` | pChronicle product example suite | | `just gate` | Format, lint, and the full Rust test workspace | | `just dev` | Scoped runtime-crate check; not the full workspace matrix | `just test` uses the debug nextest profile for faster iteration. Pass a Cargo -package name or a short crate alias (`pvisor`, `pchronicle`, +package name or a short crate alias (`pchronicle`, `pchronicle-cli`, `agentctl`, `capture`). `just test pchronicle` runs both `persisting-pchronicle` and `persisting-pchronicle-cli` (same as the CI pchronicle shard); use `just test pchronicle-cli` for the CLI crate alone. @@ -46,7 +46,7 @@ execution; install version `0.9.137` with repository CI setup action. Local and ordinary CI builds use the platform's default linker. Linux wheels -use the manylinux_2_28 image (glibc 2.28) so rustc libstd and libkrun can +use the manylinux_2_28 image (glibc 2.28) so rustc libstd can link `statx` / `copy_file_range`. `just dev` is intentionally scoped to runtime crates and a no-default-feature @@ -61,7 +61,7 @@ Cargo runner when needed, for example `cargo test --doc -p `. The repository keeps two expensive/nightly diagnostics out of the normal edit loop: -- `just build-analysis persisting-pvisor` enables Cargo's `-Z build-analysis` +- `just build-analysis persisting-pchronicle` enables Cargo's `-Z build-analysis` for one package and writes per-session JSONL metrics under `$CARGO_HOME/log`. Inspect them with `just build-analysis-report` (or pass `report=timings` or `report=rebuilds`). The dedicated target directory prevents diagnostic @@ -75,8 +75,6 @@ Sanitizer builds are deliberately not part of `just dev`/CI's default path: they rebuild the standard library and are intended for focused debugging sessions. -For supported behavior, start with [pVisor Guides](../pvisor/guides/index.md), -批量 Run 工作流,或 -[pChronicle Guides](../pchronicle/guides/index.md), and consult +For supported behavior, start with [pChronicle Guides](../pchronicle/guides/index.md), and consult [System Design](../system-design/index.md) for the rationale behind an implementation. diff --git a/docs/src/en/project/examples.md b/docs/src/en/project/examples.md index a984f97b3..084d0408a 100644 --- a/docs/src/en/project/examples.md +++ b/docs/src/en/project/examples.md @@ -1,51 +1,12 @@ -# Reproduce the Run lifecycle +# Reproducible examples -The [`examples/`](https://github.com/DeepLink-org/Persisting/tree/main/examples) -directory is organized by product CLI. Each `run.sh` manages its own `.work/` -directory and reports durable outputs or query results. Together they follow -the documented sequence: execute, govern effects, and inspect history. +Run `just examples-pchronicle` (or `just examples`) from the repository root. +The deterministic CLI examples cover Dataset lifecycle, analysis, cross-Dataset +SQL, storage performance, format roundtrips, and direct OpenAI/ACTF queries. -```bash -just examples -just examples-pvisor -just examples-pchronicle -``` +Each example manages its own `.work/` directory and prints its output for +inspection. The suite builds release executables and requires Cargo, Python 3, +`jq`, and standard POSIX tools. -## pVisor - -| Example | What it demonstrates | -|---|---| -| `01-filesystem-isolation` | Transactional workspace isolation | -| `02-changeset-management` | Review, apply, and drop | -| `03-network-isolation` | Explicit proxy policy and its boundary | -| `04-gateway-llm-control` | Embedded Gateway routing and capture | - -## pChronicle - -| Example | What it demonstrates | -|---|---| -| `01-dataset-lifecycle` | Import, inspect, query, locate, and strictly export a Dataset | -| `02-built-in-analysis` | Summarize Sources, Agents, Models, and tools, then locate a Step | -| `03-cross-dataset-sql` | Run cross-Dataset SQL over three named Dataset mounts | -| `04-storage-query-performance` | Compare JSON/Lance size, compression, query ratios, and lifecycle latency | -| `05-format-roundtrip` | Strict ATIF roundtrip and canonical byte comparison | -| `06-query-openai-actf-directly` | Direct SQL over OpenAI Messages and ACTF Datasets | - -The pChronicle examples use the deterministic fixtures in `examples/data`. -Their default output is a compact report; complete command stdout/stderr remains -under each scenario's `.work/run.*`, or can be expanded with -`PCHRONICLE_EXAMPLE_VERBOSE=1`. -Requirements are macOS or Linux, Cargo, Python 3, and common POSIX tools such -as `jq`. The pVisor filesystem examples additionally require macFUSE or FUSE3. -`just examples-pvisor-filesystem` runs the FUSE-backed 01/02 scenarios; -`just examples-pvisor-portable` runs 03/04 without FUSE. - -Start with `pvisor/01-filesystem-isolation`, continue to changeset management, -then run the orchestration layer examples when you want many-Run production, and the -pChronicle examples when you are ready to inspect history. - -Use [pVisor Guides](../pvisor/guides/index.md) for task explanations, -orchestration layer orchestration for many-Run workflows, -and [pChronicle Guides](../pchronicle/guides/index.md) for Dataset workflows. -The examples verify a product workflow; exact command syntax remains in each -product's Reference section. +See [pChronicle guides](../pchronicle/guides/index.md) and the repository's +`examples/pchronicle/` and `examples/data/` directories. diff --git a/docs/src/en/project/index.md b/docs/src/en/project/index.md index f89363a3c..9ba1ea4fb 100644 --- a/docs/src/en/project/index.md +++ b/docs/src/en/project/index.md @@ -1,6 +1,6 @@ # Project -Persisting's public product path is pVisor and pChronicle. This +Persisting's public product is pChronicle. This section records delivery state, durable decisions, contributor workflows, and systems outside that current path. diff --git a/docs/src/en/project/releasing.md b/docs/src/en/project/releasing.md index 2dfa8bc2a..ff45321cf 100644 --- a/docs/src/en/project/releasing.md +++ b/docs/src/en/project/releasing.md @@ -7,9 +7,8 @@ does not contain a PyO3 extension and does not use Maturin. Each platform wheel is tagged `py3-none-` and contains: - the Python `persisting` package; -- native command-line scripts; +- the native `pchronicle` command; - the bundled pChronicle Web assets; -- the platform libkrun firmware payload required by pVisor. The release set currently contains Linux x86_64 and Apple Silicon macOS wheels. Source distributions are not part of the published artifact set. @@ -53,18 +52,13 @@ project during the first successful upload but does not reserve the name. ## Build and verification path The PEP 517 backend is setuptools with the repository-owned -`scripts/packaging/build_backend.py`. Before wheel assembly it builds the three -Rust CLIs, stages firmware, and ensures the Dioxus bundle exists. `setup.py` +`scripts/packaging/build_backend.py`. Before wheel assembly it builds the +pChronicle CLI and ensures the Dioxus bundle exists. `setup.py` marks the wheel platform-specific while keeping the Python and ABI tags `py3-none`. -The packaging script fetches the pinned libkrun firmware archive for both -Linux x86_64 and Apple Silicon macOS unless `PERSISTING_LIBKRUNFW_PATH` points -at an existing payload. Local wheel builds must use one of those supported -paths; a missing payload is a build error rather than an incomplete wheel. - Linux wheels use the manylinux_2_28 / glibc 2.28 tag. Current rustc libstd -and libkrun's virtiofs passthrough need `statx` and `copy_file_range`, which +needs `statx` and `copy_file_range`, which are not available on manylinux2014. Every wheel is checked for its component set and install-time CLI smoke tests. diff --git a/docs/src/en/pvisor/concepts/agentvisor.md b/docs/src/en/pvisor/concepts/agentvisor.md deleted file mode 100644 index d9586de84..000000000 --- a/docs/src/en/pvisor/concepts/agentvisor.md +++ /dev/null @@ -1,348 +0,0 @@ -# What is an AgentVisor? - -!!! note "How to read this page" - - This page defines the **AgentVisor category**. It is not a pVisor feature list or - delivery commitment. What you can complete today is described in - [Get Started](../get-started.md) and the [guides](../guides/index.md). - -**An AgentVisor is the hypervisor for Agent execution.** - -It organizes compute, filesystems, networks, models, tools, credentials, and -durable state on a personal computer, workstation, or cluster into a shared -resource pool. It then gives every Agent Run an isolated **Agent virtual -execution environment**. Multiple Agents can reuse the same underlying -environment resources without sharing identity, workspace, authority, state, -external effects, or failure domains. - -![The AgentVisor category in the Agent infrastructure stack](../../../assets/diagrams/agentvisor/agentvisor-stack.svg) - -## Definition - -> An **AgentVisor** is the virtualization layer for Agents. It maps shared -> environment resources into isolated, governable, suspendable, and portable -> Agent virtual execution environments, and manages resource multiplexing, -> isolation, lifecycle, authority, state, and external effects across Agents. - -Traditional hypervisors present virtual machines to operating systems. An -AgentVisor presents virtual execution environments to Agents. This environment -is not a new machine-image format. It is the complete execution boundary seen -by an Agent, including: - -- schedulable compute, memory, and accelerators; -- isolated workspace, process, network, and tool spaces; -- delegated and bounded access to models, data, secrets, and external services; -- execution state that can pause, recover, fork, and migrate; -- external effects that can be contained, reviewed, committed, or compensated; -- identity, lineage, and evidence that remain attached to the Run. - -An AgentVisor can map that virtual environment onto a local process, operating- -system sandbox, container, microVM, confidential environment, or remote fleet. -The kernel, node, and scheduler can change while the Agent continues to see a -stable Run identity, capabilities, checkpoints, effect semantics, and -accountability boundary. - -## Why the category is needed - -Traditional software normally crosses a human-controlled boundary before it -acts: a user clicks, an operator deploys, or an API caller supplies a narrow -request. An autonomous Agent can instead form a chain of decisions over -minutes, hours, or days. During that time it may: - -- read private context and acquire temporary credentials; -- write code, documents, infrastructure, or business records; -- call models and tools chosen at runtime; -- spawn subprocesses or delegate to other Agents; -- communicate with people or external services; -- pause, recover, branch, and continue from accumulated state; -- leave effects that survive every process involved in the Run. - -What is missing is a unified virtualization layer for Agents. A sandbox can isolate a process but does -not decide which results should become real. A workflow engine can sequence -steps but does not prove that direct network or filesystem paths were confined. -A model gateway can mediate inference but does not own subprocesses, tools, or -workspace state. An observability platform can record events after the fact -but does not govern them before they occur. - -The AgentVisor composes these separate capabilities into an Agent virtual -execution environment that can be created, scheduled, suspended, migrated, -and reclaimed, while allowing multiple Agents to share the underlying pool. - -## Where an AgentVisor sits - -An AgentVisor is a narrow, compositional infrastructure layer. It works with -the systems around it rather than replacing them. - -| Neighboring category | What it primarily owns | What the AgentVisor adds | -| --- | --- | --- | -| Agent framework | Reasoning loop, prompts, tool adapters, application logic | Provider-independent Run identity, authority, effects, continuity, and evidence | -| Model gateway | Model routing, authentication, quotas, inference telemetry | One policy context spanning models, tools, files, processes, and network access | -| Workflow engine | Dependency graph, retries, scheduled steps | Autonomous Run semantics, effect boundaries, checkpoints, and causal lineage | -| Sandbox / container / VM runtime | Process and kernel isolation | Agent-aware capability admission, result promotion, and cross-substrate identity | -| Policy engine | Decision evaluation | Binding policy decisions to a concrete Run, enforcement mechanism, and observed outcome | -| Observability platform | Logs, traces, metrics, analytics | Durable action/effect identity and evidence about what was enforced, not only what was observed | -| Secrets manager | Credential storage and issuance | Run-scoped delegation, delivery, expiry, and evidence of use | - -An Agent operating system may describe an entire developer or enterprise -platform. AgentVisor is the more precise category inside that larger vision: -the layer that supervises autonomous execution and its consequences. - -## The six responsibilities - -Every credible AgentVisor must address six connected responsibilities. - -### 1. Identity and lifecycle - -The durable unit is an **Agent Run**, not a process or container. One Run may -have multiple physical Attempts because of retry, recovery, migration, or -placement changes. The Run retains one identity and a causal relationship to -its parents, children, and checkpoints. - -Lifecycle includes admission, start, observation, quiescence, cancellation, -recovery, terminal publication, and retention. A successful process exit is -not sufficient if effects, evidence, or durable state remain ambiguous. - -### 2. Delegated authority - -An Agent should receive explicit, bounded authority rather than ambient user -power. Authority can cover models, tools, filesystem regions, network -destinations, secrets, subprocesses, financial limits, communication channels, -or compute budgets. - -The AgentVisor binds that authority to the Run and its active Attempt. It -decides whether a request is admissible, selects mechanisms capable of -enforcing it, and prevents silent fallback to a weaker boundary. - -### 3. Effect governance - -Execution permission and effect promotion are separate decisions. An Agent may -be allowed to explore, generate, and mutate inside a contained Run without -receiving automatic authority to change the real environment. - -The AgentVisor observes and classifies effects, stages them when possible, and -applies policy to promotion, rejection, or compensation. Filesystem changes -are one example. Messages, payments, deployments, tickets, database writes, -and tool mutations belong to the same conceptual plane even when their -reversibility differs. - -### 4. Continuity and branching - -Agents accumulate more than memory pages. They accumulate conversation state, -workspace changes, tool state, unresolved effects, credentials, artifacts, and -causal history. An AgentVisor defines a **semantic checkpoint** that records -which of those elements are present, absent, open, or externally committed. - -That checkpoint can support pause/resume, recovery, fork, replay, evaluation, -or migration without pretending that every external system can be rewound. - -### 5. Evidence and accountability - -Requested policy is not evidence of enforcement. An AgentVisor records the -actual mechanism and outcome for each relevant dimension. It preserves enough -information to answer: - -- Which Agent, Run, Attempt, and authority generation acted? -- What code, model, tool, artifact, and environment were involved? -- Which access was requested, allowed, denied, or bypassable? -- Which effects were observed, staged, promoted, rejected, or compensated? -- Where did execution occur, and what boundary was actually installed? -- Why was the Run considered complete, failed, or cancelled? - -### 6. Placement portability - -Execution providers should be replaceable without rewriting Agent semantics. -Local processes, operating-system sandboxes, containers, microVMs, confidential -environments, and remote fleets can offer different isolation and performance -profiles while consuming the same logical Run and authority model. - -Portability does not mean every provider is equivalent. It means differences -are explicit, admission is capability-aware, and evidence follows the Run. - -## The AgentVisor object model - -A shared vocabulary is necessary before implementations can interoperate. - -| Object | Industry-level meaning | -| --- | --- | -| **Agent Run** | One durable, user-meaningful execution with stable identity and intent | -| **Attempt** | One physical realization of a Run on a particular provider and ownership generation | -| **Capability Grant** | Delegated authority scoped by resource, action, conditions, limits, and lifetime | -| **Effect** | An externally meaningful observation or mutation with stable identity and lifecycle | -| **Checkpoint** | A declared consistency frontier across Agent state, workspace, effects, and artifacts | -| **Lineage** | Causal relationships among Runs, checkpoints, delegations, artifacts, and derived outcomes | -| **Evidence Bundle** | Durable facts describing execution, enforcement, effects, and terminal outcome | -| **Execution Provider** | A substrate that realizes an Attempt while reporting its capabilities and evidence | - -This model is intentionally independent of a specific wire protocol, database, -container format, cloud, or Agent framework. - -## Governing the effect loop - -![The AgentVisor effect governance loop](../../../assets/diagrams/agentvisor/effect-governance.svg) - -The effect loop begins before execution and ends after the consequence is -known. A useful effect lifecycle includes: - -1. **Intent** — the Agent or its tool describes a requested action. -2. **Admission** — policy evaluates the Run, capability, resource, context, - and budget. -3. **Execution** — an enforcement point allows, denies, or transforms the - action. -4. **Observation** — the actual result is captured with stable identity. -5. **Containment** — the result remains isolated or pending when the medium - permits it. -6. **Promotion** — policy or a person accepts the effect into the real system. -7. **Compensation** — a committed effect is counteracted when true rollback is - unavailable. -8. **Evidence** — the complete decision and outcome become part of the Run's - accountable history. - -Effects differ by reversibility: - -| Effect class | Examples | Appropriate control | -| --- | --- | --- | -| Reversible | Workspace file, generated artifact, isolated branch | Stage, review, promote, discard | -| Transactional | Database transaction, deployment plan, API with prepare/commit | Reserve, validate, commit atomically | -| Compensatable | Ticket creation, cloud resource, reversible business operation | Commit with durable compensation plan | -| Irreversible | External message, published secret, physical action, settled payment | Strong admission, explicit authority, minimal scope, complete evidence | - -Calling every action “sandboxed” obscures these differences. AgentVisor makes -them part of the product model. - -## Authority is multidimensional - -Security cannot be reduced to one label such as *sandboxed*, *containerized*, -or *running in a VM*. A single Run may have different guarantees for -filesystem reads, filesystem writes, network egress, secret access, -subprocesses, devices, tools, models, and resource budgets. - -The industry needs to distinguish four evidence levels: - -| Level | Meaning | -| --- | --- | -| **Declared** | Policy intent exists, but no mediation or enforcement is demonstrated | -| **Mediated** | The normal integration path passes through a control point, but bypass may exist | -| **Enforced** | The scoped actor cannot bypass the mechanism within the stated threat model | -| **Attested** | Enforcement evidence is cryptographically bound to the exact Run, provider, software identity, and authority generation | - -An AgentVisor should report evidence independently per dimension and refuse a -Run when the requested guarantee cannot be met. Silent downgrade destroys the -meaning of delegated authority. - -## From personal device to fleet - -![The AgentVisor execution continuum](../../../assets/diagrams/agentvisor/execution-continuum.svg) - -The same category matters on both a personal computer and a multi-tenant -cluster. - -On a personal device, an AgentVisor can remove constant approval prompts by -giving the Agent broad freedom inside a contained workspace while retaining -control over promotion into the user's real environment. - -In a team or fleet, the same Run identity and effect semantics can be combined -with scheduling, leases, node attestation, tenant isolation, organization -policy, shared artifacts, and durable reconciliation. - -The portable unit is not necessarily a live VM. It is the combination of: - -- Run identity and intent; -- delegated authority; -- semantic checkpoint and lineage; -- effect frontier; -- content-addressed artifacts; -- enforcement and outcome evidence. - -This makes local-to-fleet migration a semantic problem first and a machine -transport problem second. - -## Design principles of the category - -1. **Autonomy inside, control at the boundary.** High-frequency Agent decisions - should not require high-frequency human approvals. -2. **Authority follows the Run.** Permissions must not depend on an accidental - process, shell, node, or container identity. -3. **Effects are first-class.** External consequences need identity, state, - policy, and evidence—not only logs. -4. **Evidence beats labels.** A mechanism and threat model are more meaningful - than a generic “secure” or “sandboxed” badge. -5. **No silent weakening.** Placement or recovery must never reinterpret an - existing Run under a weaker boundary. -6. **Checkpoint is semantic.** It declares consistency across Agent state and - effects, not merely a memory snapshot. -7. **Providers remain replaceable.** The category sits above kernels, - containers, VMs, clouds, and schedulers. -8. **Terminal means accountable.** A Run is not complete while its effects or - terminal evidence remain ambiguous. - -## A maturity model - -AgentVisor products can be evaluated by capability rather than marketing -language. - -| Level | Name | Minimum characteristics | -| ---: | --- | --- | -| 0 | Observed Agent | Stable Run identity and correlated logs, but no governed authority or effects | -| 1 | Supervised Agent | Isolated virtual execution environment, lifecycle control, cancellation, bounded resources, and explicit execution placement | -| 2 | Governed Agent | Multidimensional capability enforcement and first-class effect lifecycle | -| 3 | Portable Agent | Semantic checkpoints, lineage, provider-independent Attempts, and local-to-fleet continuity | -| 4 | Accountable Agent | Attested enforcement, durable effect reconciliation, multi-tenant isolation, and verifiable terminal evidence | - -Level 0 is useful infrastructure, but it is not sufficient to claim the full -AgentVisor category. The category becomes distinctive at Level 2, where the -system controls both delegated authority and external effects. - -## What qualifies as an AgentVisor? - -A product belongs to this category when it can answer all of the following: - -- Does the Agent have a stable Run identity independent of process placement? -- Can multiple Agents share underlying environment resources while preserving - isolation of identity, state, authority, and failure domains? -- Is delegated authority explicit, bounded, and tied to that Run? -- Are enforcement claims separated by capability dimension and backed by - concrete evidence? -- Are externally meaningful effects represented before and after commitment? -- Can the Run pause, recover, or fork at a declared semantic frontier? -- Do lineage and evidence survive movement across execution providers? -- Can terminal state be reconciled after failures without silently duplicating - irreversible work? - -A sandbox alone is not an AgentVisor. Neither is a model proxy, workflow -engine, tracing product, permission prompt, or container scheduler. Any of them -can become an essential provider within an AgentVisor architecture. - -## Where industry standardization can emerge - -The category does not require one implementation, but it benefits from open -interfaces around: - -- a portable Agent Run envelope and identity model; -- a vocabulary for capability dimensions and constraints; -- provider capability discovery and per-dimension enforcement evidence; -- effect identity, lifecycle, promotion, and compensation records; -- semantic checkpoint manifests and effect frontiers; -- causal lineage across parent Runs, delegated Agents, artifacts, and tools; -- evidence bundles that can be verified outside the producing platform; -- conformance profiles for personal, enterprise, and multi-tenant operation. - -Standardization at this layer would let Agent frameworks remain creative, -execution runtimes remain specialized, and organizations choose infrastructure -without giving up authority, continuity, or accountability. - -## Category definition - -A hypervisor lets multiple operating systems share a machine safely. An -AgentVisor lets multiple Agents share execution environments safely. Downward, -it unifies heterogeneous compute and isolation substrates. Upward, it exposes -a stable Agent virtual execution environment and places authority, state, -effects, and evidence inside the same virtualization boundary. - -**AgentVisor is the virtualization infrastructure for Agent execution.** - -## Continue from the category to the product - -- [Run, Attempt, and Effect](run-model.md) defines the portable execution object. -- [Capabilities and evidence](capabilities-and-evidence.md) defines authority and enforcement reporting. -- [pVisor Overview](../index.md) explains Persisting's implementation of the category. -- [Local to fleet](../../system-design/local-to-fleet.md) explains which contracts survive placement changes. diff --git a/docs/src/en/pvisor/concepts/capabilities-and-evidence.md b/docs/src/en/pvisor/concepts/capabilities-and-evidence.md deleted file mode 100644 index 1dfb667e9..000000000 --- a/docs/src/en/pvisor/concepts/capabilities-and-evidence.md +++ /dev/null @@ -1,34 +0,0 @@ -# Capabilities and evidence - -A capability is bounded authority over one resource and action. pVisor reasons -about capabilities by dimension because no single `safe` or `sandboxed` label -can describe an Agent environment accurately. - -| Dimension | Example request | Evidence to inspect | -| --- | --- | --- | -| Filesystem read | read selected project and toolchain paths | visible roots and installed read controls | -| Filesystem write | write only to a staged workspace | write boundary and promotion decisions | -| Network | reach declared destinations | interception path and bypass resistance | -| Process | start bounded descendants | namespace/profile and inherited handles | -| Credentials | use one short-lived identity | delivery, expiry, and observed use | -| Tools and models | invoke declared endpoints | policy decision and routed calls | - -Requested authority and installed enforcement are different facts. Admission -must reject a required capability dimension when the selected provider cannot -satisfy it. Optional controls may degrade only when the Run record reports that -degradation explicitly. - -Evidence answers four progressively stronger questions: - -1. **Declared** — what policy was requested? -2. **Mediated** — which actions passed through a control point? -3. **Enforced** — which bypass paths were blocked for the stated threat model? -4. **Attested** — is that enforcement bound to this exact Run and provider? - -The Run Bundle is the place to inspect the answer for a concrete execution. -Return to [pVisor concepts](index.md), use the -[network guide](../guides/network.md) to configure one capability dimension, -or read [pVisor isolation design](../design/isolation.md) for platform -mechanisms. For the end-to-end trust chain across execution, orchestration, -and history, read -[Security and evidence](../../system-design/security-evidence.md). diff --git a/docs/src/en/pvisor/concepts/index.md b/docs/src/en/pvisor/concepts/index.md deleted file mode 100644 index e3954f971..000000000 --- a/docs/src/en/pvisor/concepts/index.md +++ /dev/null @@ -1,28 +0,0 @@ -# pVisor concepts - -pVisor is built around an **Agent Run**, not a process, container, or virtual -machine. Read these concepts before comparing providers or interpreting a Run -Bundle. - -!!! note "When this section helps" - - Come here after the first Run when a command succeeded but you need to know what - was actually isolated, which changes are still staged, or why two execution - providers make different guarantees. - -Follow these articles in order: - -1. [Run, Attempt, and Effect](run-model.md) defines one execution, its attempts, - and the changes it produces. -2. [Capabilities and evidence](capabilities-and-evidence.md) explains how a - request becomes an installed mechanism and a claim in the Run Bundle. -3. [What is an AgentVisor?](agentvisor.md) explains the product category and - the boundary between an Agent and its runtime. - -AgentVisor is a category definition, not a pVisor feature list. The Run and capability pages -define pVisor's stable user model. Platform mechanisms and current gaps belong -to [pVisor Design](../design/index.md). - -After this section you should be able to read a Run Bundle without confusing a -requested capability with an enforced one. Continue to [practical guides](../guides/index.md) -when you are ready to make a provider or policy decision. diff --git a/docs/src/en/pvisor/concepts/run-model.md b/docs/src/en/pvisor/concepts/run-model.md deleted file mode 100644 index c6dcf6fb2..000000000 --- a/docs/src/en/pvisor/concepts/run-model.md +++ /dev/null @@ -1,54 +0,0 @@ -# Run, Attempt, and Effect - -pVisor manages an **Agent Run**. A Run is not the process that happens to -execute it, and it is not the container or virtual machine selected for one -attempt. - -## Run - -A Run is the stable, user-meaningful identity of one Agent task. Its identity, -requested capabilities, parent/child lineage, accepted effects, artifacts, and -terminal result survive executor changes and process exits. - -## Attempt - -An Attempt is one physical realization of a Run on a provider. Infrastructure -failure may create another Attempt without changing the Run. A semantic retry -is different: it represents a new decision and therefore creates a derived -Run. - -```text -Run -├── Attempt 1 → infrastructure failure -└── Attempt 2 → terminal result -``` - -This distinction allows infrastructure retries while keeping the history -understandable. - -## Effect - -An Effect is a consequence that matters outside the Agent's reasoning loop. -Filesystem changes, network requests, tool calls, credential use, and external -API mutations are separate effect dimensions. Capturing an effect is not the -same as preventing it, and staging one dimension does not isolate another. - -For a staged workspace, the lifecycle is: - -```text -execute → inspect stage → apply selected paths zero or more times → drop stage -``` - -`apply` promotes selected filesystem changes. It does not imply that network or -remote-service effects were rolled back. - -## Checkpoint and terminal result - -A Checkpoint records a declared consistency frontier for the state a provider -can preserve. The terminal Run result records the final status and references -to evidence and artifacts. Neither should be inferred from a process exit code -alone. - -Return to [pVisor concepts](index.md), or continue with -[Capabilities and evidence](capabilities-and-evidence.md), then use the -[execution guide](../guides/execution.md) to choose a provider. diff --git a/docs/src/en/pvisor/design/cli.md b/docs/src/en/pvisor/design/cli.md deleted file mode 100644 index 7f0321f48..000000000 --- a/docs/src/en/pvisor/design/cli.md +++ /dev/null @@ -1,101 +0,0 @@ -# pVisor command model - -The `pvisor` command is the public entry point for one governed Agent Run. Its -interface is organized around four responsibilities: start a Run, inspect its -record, decide what happens to staged changes, and manage reusable environments. -The command line and `RunConfig` describe the same model; a configuration file -is an explicit input, never an implicit project policy. - -## Start with `run` - -The short form is intentionally equivalent to the explicit form: - -```bash -pvisor -- codex -pvisor run --stage ./runs/task-001 -- codex -``` - -Use `--stage` when filesystem changes must remain available for review. Without -a stage, pVisor still records a Run, but there is no durable workspace change set -to apply. The selected host, container, or VM provider records its effective -capabilities and limitations in the Run Bundle. - -Common controls are grouped by purpose: - -| Purpose | Options | Result | -| --- | --- | --- | -| Workspace | `--stage`, `--overlayfs-path`, `--overlayfs-compose` | create a copy-on-write view and retain a changeset | -| Runtime | `--executor host\|container\|vm`, `--rootfs`, `--container-image` | select the execution provider and root filesystem | -| Network | `--overlaynet-deny-all`, `--overlaynet-allow`, `--overlaynet-limit` | request deny, allowlist, or rate-limit policy | -| Gateway | `--gateway-mode`, `--gateway-route`, `--gateway-level` | route and optionally capture model traffic | -| Limits | `--timeout`, `--memory`, `--max-processes`, `--max-open-files` | constrain the Attempt where the provider supports it | -| Configuration | `--spec`, `--name`, `--pass-env` | provide a prepared RunSpec, identity, and explicit environment | - -Provider selection does not change the Run contract. It changes the mechanism -used to enforce each capability dimension, and the resulting evidence is -reported separately. - -## Inspect and decide - -A completed Run remains a record until its staged effects are explicitly -accepted or discarded: - -```bash -pvisor review last -pvisor inspect last -- git status --short -pvisor apply last --path src -pvisor apply last --include 'tests/**' --exclude 'tests/generated/**' -pvisor apply last --all -# or: pvisor drop last -``` - -`review` explains the Run Bundle and staged changes. `inspect` executes a -read-only command against the Run view. `apply` commits a selected path set and -keeps the remainder staged; `drop` discards the stage. Neither operation -rewrites a live Run. A reset creates a new stage generation so stale metadata -cannot replace a newer decision. - -## Checkpoints and forks - -Checkpoints are stopped-consistent filesystem and AgentCtl safe points. They do -not claim to capture process memory: - -```bash -pvisor checkpoint last --name before-experiment -pvisor fork last --checkpoint before-experiment -- codex -``` - -Use checkpoints to preserve a known workspace state before a new attempt. The -checkpoint protocol waits for participating AgentCtl sessions to quiesce, -records the upper layer and lineage, then resumes the Run. - -## Reusable environments - -`env` gives a named stage a stable lifecycle across commands: - -```bash -pvisor env create dev --target ./project -pvisor env exec dev -- make test -pvisor env shell dev -pvisor env inspect dev -- git status --short -pvisor env apply dev --path src -pvisor env drop dev -pvisor env delete dev --force -``` - -An environment is a persistent stage, not a resident VM. `start` and `stop` -control whether new sessions are accepted. `apply` and `drop` advance the stage -generation after a decision. - -## Configuration precedence - -`--spec` accepts TOML `RunConfig` or prepared JSON `RunSpec`. Explicit scalar -options override file values. Repeated list options replace the complete list, -and the command after `--` replaces `run.command`. `--container-image` and -`--rootfs` may infer the matching executor; an explicit `--executor` remains -clearer in automation. - -Keep the public workflow small: start a Run, inspect its evidence, then make an -explicit decision about staged effects. Detailed provider behavior belongs to -[execution environments](../guides/execution.md), while the complete option -surface belongs to the [CLI reference](../reference/cli.md). diff --git a/docs/src/en/pvisor/design/gateway.md b/docs/src/en/pvisor/design/gateway.md deleted file mode 100644 index ef822842c..000000000 --- a/docs/src/en/pvisor/design/gateway.md +++ /dev/null @@ -1,661 +0,0 @@ -# pVisor Gateway — architecture and design - -This page covers model routing, protocol adaptation, non-blocking capture, -and event emission. How to capture a Run belongs to the -[Capture guide](../guides/capture.md). Ownership of facts and projections -after capture belongs to -[pChronicle run storage](../../pchronicle/design/trajectory-storage.md). - -> **Audience**: platform engineers, architects, and integrators who need -> **observable, replayable, auditable** trajectories between Agents and -> LLMs. -> **Version**: 1.1 (external)  |  **Last updated**: 2026-07-30 - -This document describes the product role, core concepts, and architectural -trade-offs of **Persisting Gateway**. Implementation details (block-format -field tables, CLI flags, directory layout) are in Further reading at the -end. The text avoids binding to specific source-code paths. - ---- - -## Contents - -1. Summary -2. Problem and value -3. Design principles -4. Core concepts -5. System overview -6. Data flow: from HTTP to trajectory (including §6.4 multimodal) -7. Storage and consistency -8. Gateway and protocols -9. Multi-agent and sessions -10. Reliability and runtime shapes -11. Evolution -12. Further reading - ---- - -## 1. Summary - -**Persisting Gateway is the trajectory observation layer for coding -agents.** Run **Claude Code** or **OpenAI Codex** through a local explicit -proxy from `persisting-overlaynet` and you get a replayable event stream, -which pChronicle then persists in structured form. - -Main path: - -```text -HTTP ──► events stream - ├─ record (append → events.lance, SoT) - └─ trigger (subscribe / handler) - └─ format conversion (via storyline hub) + persist - (agenticmd / atif / openai_msg / …) -``` - -It is an embeddable **event observer and state machine** on top of the -overlaynet proxy. On supported clients, `pvisor run` injects the proxy or -sets the model API address so that, **without changing application -code**, you can: - -- transparently forward dialogue traffic to the upstream model; -- write every HTTP exchange into the **events stream** (durable and - replayable); -- let subscriptions on events trigger materialize and export (Markdown, - ATIF, openai_msg, and so on) instead of hard-coding formats on the - proxy path. - -Gateway is not a substitute for a general enterprise API gateway, and it -does not own the network data-plane implementation. As an OverlayNet -sink, it interprets proxy exchanges, forwards protocols, and produces -events around the **Agent trajectory**. - ---- - -## 2. Problem and value - -### 2.1 Typical pain points - -| Pain point | Gateway response | -|------|----------------| -| Agent dialogue is scattered across vendor APIs and hard to analyze uniformly | Normalize to a shared event record, then materialize a dialogue view | -| Want logs without changing code | Reverse proxy + environment injection (`pvisor run`) | -| Need a human-reviewable session transcript | TLV Markdown: readable body, metadata in comments | -| Want streaming output visible as it is generated | Live Markdown upsert (draft block → final block) | -| Subagents and multiple sessions mix together | One file per storyline + spawn links; do not inline full subagent text | -| Capture must not delay the LLM first token | **Observation does not block**: capture is async; failures go to dead letter | - -### 2.2 Client support (live capture) - -| Client | `pvisor run` live capture | Notes | -|--------|:----------------------:|------| -| **Claude Code** | ✅ | Primary target: Anthropic Messages, subagent tracks, history-replay dedup | -| **OpenAI Codex** | ✅ | Responses API path; inject the gateway with `-c openai_base_url=…` and similar | -| **Cursor** | ❌ | **Not supported in this version** (no official injection or traffic adapter) | -| **Custom / generic OpenAI SDK** | ⚠️ | May work if the client uses `HTTP_PROXY` or `OPENAI_BASE_URL` / `ANTHROPIC_BASE_URL`; no dedicated guarantee | - -Post-hoc **import** from local IDE JSONL follows the CLI docs. Cursor -local-log import is also planned and is independent of the live-capture -table above. - -### 2.3 Capability bounds - -**Strengths** - -- Embedded Gateway in `pvisor run` capturing **Claude Code / Codex** - dialogue; -- History-replay dedup and subagent tracks for Claude Code; -- Responses ↔ Completions bridging and context-injection filtering for - Codex; -- Dual storage: full Lance events plus a Markdown materialized view; -- Lightweight model routing and protocol bridging (Messages / - Completions / Responses, and so on). - -**Does not replace** - -- Multi-tenant billing, complex RBAC, MCP/A2A federation, and similar - enterprise gateways (see projects such as - [agentgateway](https://github.com/agentgateway/agentgateway)); -- A one-stop SDK for 100+ vendors (see LiteLLM-style projects); -- Token compression of terminal command output (complements tools such - as [RTK](https://github.com/rtk-ai/rtk)). - -### 2.4 Place in the Persisting ecosystem - -```text -Agent client - │ HTTP - ▼ -┌─────────────────────────────────────┐ -│ Persisting Gateway │ -│ HTTP → events stream │ -│ · record → events.lance (SoT) │ -│ · trigger → storyline → persist │ -└──────────────┬──────────────────────┘ - │ events / derived artifacts - ▼ -┌─────────────────────────────────────┐ -│ pChronicle / analysis / retrieval │ -└─────────────────────────────────────┘ -``` - ---- - -## 3. Design principles - -| Principle | Meaning | -|------|------| -| **Observation does not block** | User-request latency and success come first. Capture failures write dead letter and do **not** interrupt the HTTP response because a disk write failed. | -| **HTTP → events** | The proxy's primary product is the **events stream** (HTTP-first wire), not a direct Markdown / ATIF write. | -| **Record and trigger are separate** | The same event can **append** to storage and **fan-out** to downstream handlers; the two are decoupled. | -| **Convert through the hub** | Materialize / export goes through **storyline** (ATIF-aligned) and then to each format. Pairwise conversion among peripheral formats is forbidden. | -| **Lance is the source of truth** | Canonical storage is only `events.lance`. Markdown / ATIF and similar are **derived persistence** and may be lossy. | -| **Single write gate** | Lance appends go through one engine path, avoiding dual-write races. | - ---- - -## 4. Core concepts - -### 4.1 Main path - -```text -Agent HTTP - │ - ▼ -overlaynet proxy (CONNECT / forward / network policy) - │ - ▼ -Gateway Sink (LLM protocol adapt + emit trajectory observations) - │ - ▼ -events stream ─────────────────────────────────────────┐ - │ │ - ├─ record append ──► events.lance (SoT / replay) │ - │ │ - └─ trigger handler ──► interpret / fold │ - │ │ - ▼ │ - storyline (hub) │ - │ │ - ┌────────────┼────────────┐ │ - ▼ ▼ ▼ │ - agenticmd atif openai_msg … │ - │ │ │ │ - └──────── persist / materialize ─────┘ │ - │ -(optional) replay from Lance ──────────────────────┘ -``` - -Key points: - -1. **overlaynet owns the proxy mechanism**: request classification, - CONNECT, absolute-URI forwarding, egress policy, and connection - counts. -2. **Gateway Sink owns business semantics**: LLM routing/protocol - conversion, session association, and capture events. It does not - implement a second proxy. -3. **The events stream is the bus**: it can be recorded and subscribed. - The same record can persist and trigger at once. -4. **Format conversion and persistence are downstream**: they go through - the storyline hub and emit agenticmd / atif / openai_msg and so on. - -Auxiliary coordinates (session bounds, not SoT): - -| Concept | Meaning | -|------|------| -| **Run** | One `pvisor run` / root workspace | -| **session** | One Agent conversation line (≈ ATIF `session_id` / storyline `session`) | -| **call_id** | Ties request/response of one HTTP round-trip (events envelope field) | - -### 4.2 Layers - -```text -┌─────────────────────────────────────────────────────────────┐ -│ Protocol layer: HTTP, SSE, OpenAI / Anthropic / Responses │ -│ Role: forward, translate; emit HTTP-first observations │ -└───────────────────────────┬─────────────────────────────────┘ - │ emit - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ events stream │ -│ Role: ordered events; append records; fan-out triggers │ -└───────────────┬─────────────────────────┬───────────────────┘ - │ record │ trigger - ▼ ▼ - events.lance handlers (interpret) - │ - ▼ - storyline → persist formats -``` - -**Ingress**: protocol layer → events (keep the wire when possible; -summary fields are optional). -**Egress**: events replay / subscribe → storyline → derived-format -persist; decoupled from the capture hot path. - -![Gateway event write and derived data flow](../../../assets/diagrams/persisting/gateway-dataflow.svg) - -### 4.3 Write path and derived path - -| | Write path (record) | Derived path (trigger) | -|---|--------|--------| -| **Input** | Event emitted by proxy / import | Event already in the stream (live or replay) | -| **Output** | `events.lance` append | storyline and agenticmd / atif / … | -| **Failure** | dead letter; do not block HTTP | Independent retry; does not affect SoT | -| **Fidelity** | HTTP-first, target is replay | Lossy fold is allowed | - -Live Markdown, turn indexes, and similar are **handlers triggered by -events**, not a second source of truth beside events. - -## 5. System overview - -### 5.1 Logical components - -```text - ┌───────────────┐ - │ Agent process │ - └───────┬───────┘ - │ HTTP(S) - ▼ - ┌────────────────────────┐ - │ Capture Proxy │ - │ · routing / auth │ - │ · protocol bridge │ - │ · stream forward │ - │ · emit → events │ - └───────────┬────────────┘ - │ - ┌───────────────┼───────────────┐ - ▼ ▼ ▼ - ┌────────────┐ ┌────────────┐ ┌────────────┐ - │ events │ │ upstream │ │ session │ - │ engine │ │ LLM │ │ index │ - │ · record │ │ │ │ │ - │ · trigger │ └────────────┘ └────────────┘ - └──────┬─────┘ - │ - ┌─────┴──────────────────┐ - ▼ ▼ - events.lance handlers → storyline - (SoT) → persist agenticmd / atif / … -``` - -| Component | Role | -|------|------| -| **Proxy** | Sole HTTP entry; forwards up and down stream; **emits** observations **into the events stream** (does not write multiple formats directly). | -| **events engine** | Maintains the ordered stream: **record** (append Lance) and **trigger** (fan-out handlers). | -| **Record path** | WAL → per-session ordered apply → `events.lance`. | -| **Trigger path** | Subscribe events → interpret → storyline → persist formats / Live Markdown. | -| **Session index** | Lightweight `sessions.json`: listing, tokens, cost estimates. | -| **Reconcile and dead letter** | Consistency of SoT vs derived persist; failed events can be replayed. | - -### 5.2 Integration (conceptual) - -- **Library embed**: a Rust project can mount OverlayNet and the Gateway - sink and supply its own trajectory event sink. -- **CLI**: `pvisor run` wraps the child process and manages the - Run-scoped Gateway lifecycle. -- **Config**: TOML declares listen address, model routes, capture level, - and storage root; Agent source code does not change. - -The public API is published by **module boundary** (proxy, engine, -record, trajectory, session) rather than a flat export of hundreds of -symbols. The story read model is visible mainly through snapshots and -reconcile artifacts. - -### 5.3 Relationship to agentgateway - -Gateway borrows a subset of agentgateway **config semantics and routing -model**, and can use its fixtures for protocol regression. The two -runtimes are **independent**. Positioning: agentgateway is a cluster- -scale multi-protocol gateway; Persisting Gateway is an **embedded, -single-node trajectory source of truth**. - ---- - -## 6. Data flow: from HTTP to trajectory - -Main path: **HTTP → events stream → (record | trigger) → persist**. - -### 6.1 One dialogue request (conceptual sequence) - -![Gateway capture sequence for one dialogue request](../../../assets/diagrams/persisting/gateway-request.svg) - -Key points: - -1. The **Proxy does not wait** for derived persist to finish before - responding; it emits first, then continues forwarding. -2. **Drafts trigger handlers only by default** (for example Live - Markdown). Only a complete response is **recorded** into Lance, so - partials do not pollute SoT. -3. Derived formats (agenticmd / atif / …) always go through - **storyline**. They can trigger live or be replayed from Lance later. - -### 6.2 Capture events and record types - -The write path drives all persistence from a small set of **event -kinds**: - -| Event | Typical effect (Dialogue level) | -|------|---------------------------| -| Request arrived | Lance: request record; Markdown: user block | -| Streaming draft | Markdown only: assistant draft (in-place overwrite) | -| Response complete | Lance: stream/full response record; Markdown: final assistant | -| Call canceled | Lance only: cancel record | -| Spawn link | Lance + Markdown: association metadata (not skippable noise) | - -**Capture level** (Summary / Dialogue / Full) controls record grain. -Production default is **Dialogue**: - -| Level | Lance / Markdown summary fields | `payload.body` | -|------|---------------------------|----------------| -| `summary` | model, path, byte counts only | ❌ | -| `dialogue` (default) | visible dialogue text in `user_content` / `assistant_content` | ❌ | -| `full` | same plus the fully parsed request/response JSON | ✅ | - -Rules that omit unrelated probe traffic (for example `count_tokens`, -history replay) are independent of capture level. Materialize filtering -handles them uniformly. See §6.4 on this page. - -Storage record types (`http.request` / `llm.request`, -`llm.response.stream`, `session.*`, and so on) belong to the **events -vocabulary** and are emitted by the Proxy. Handlers then fold them into -storyline; they need not map one HTTP frame to one dialogue turn. - -#### 6.2.1 Timestamps and order - -Every `EventRecord` that enters durable capture carries two consistent -observation times: `timestamp` (RFC3339 UTC) and `timestamp_unix_ms` -(Unix milliseconds). Request events use the time the request was -accepted; response events use the time the response was captured. The -Gateway sink is the last common write boundary and fills both values for -older producer records that lack them. Runtime lifecycle events from -pVisor also write both forms, and the two must agree at millisecond -precision. - -Event order is still defined by `source + seq`. Timestamps are only for -wall-clock correlation, latency display, and cross-component alignment. -They do not replace sequence ordering. Different sources may have -independent `seq` spaces. - -### 6.3 Streaming and the human-readable view - -```text -Assistant: "H" → "He" → "Hello, I can help…" -Markdown: [draft] → [overwrite draft] → [final] -Lance: — — one final response event -``` - -- Draft blocks are explicitly marked. On finalize, the same block is - overwritten by **call + role**, avoiding duplicate paragraphs. -- The block-header schema carries a version (`v: 1`) so the line format - can evolve without changing the file suffix. - -See [AgenticMD run format](../../pchronicle/reference/agenticmd.md). - -### 6.4 Visible-dialogue extraction (including multimodal) - -At the **Dialogue** level, Gateway extracts human-visible body text from -the client's original HTTP body (not the upstream-transformed form), -writes `payload.user_content` / `payload.assistant_content`, and drives -Markdown block bodies, frontmatter `turns`, and derived stats. - -**Single entry**: the `dialogue_extract` module, branched by wire -protocol: - -| Client / API | Typical path | User input | Assistant output | -|--------------|----------|----------|----------| -| Claude Code | `/v1/messages` | `content[]`: `text` / `image` / `tool_result` | SSE / JSON: `text` / `tool_use` | -| Codex | `/v1/responses` | `input[]`: `input_text` / `input_image` / tool round-trips | SSE / JSON: `output_text` / `function_call` / `image_generation_call` | -| OpenAI SDK | `/v1/chat/completions` | `messages[]`: `text` / `image_url` | `choices[].message` / streaming delta | - -**Multimodal Phase 0 (current)**: images are **not written as blobs**. -Placeholders stay in the dialogue string so `turns` counts stay correct -and review still "knows there was an image": - -| Direction | Placeholder example | -|------|------------| -| User input (URL) | `[image: url:https://…]` | -| User input (base64 / data URL) | `[image: base64:128KB image/png hash=abc…]` | -| Assistant image (Codex Responses) | `[image_generated: ig_xxx, png, 1024x1024, ~1MB]` + optional `prompt: …` | - -An image-only user turn with no text still **counts as 1 turn** (fixes -"image but no text → stats 0 turns"). -When `capture_level = full`, the complete JSON remains in -`payload.body`, but Markdown materialize **still shows only -placeholders** and does not embed pixel data. - -**Later (planned)**: a sidecar asset directory -`{run}/assets/{call_id}/…` plus payload references. An internal -materializer can emit Markdown images that point at `assets/…`. The -current public `pchronicle` CLI does not reserve a command for this -plan. See §11 Evolution on this page. - -Protocol regression: -`crates/persisting-gateway/tests/ag_fixture_tests.rs` + -`tests/support/ag_capture_cases.rs` (agentgateway fixture matrix). - ---- - -## 7. Storage and consistency - -> Dual storage, directory conventions, and materialize/import paths are -> in [Run storage](../../pchronicle/design/trajectory-storage.md). - -### 7.1 Dual storage - -| | Lance (source of truth) | Markdown (materialized view) | -|---|----------------|----------------------| -| **Reader** | Programs, retrieval, replay | Humans, git, review | -| **Completeness** | Lossless (within the capture level) | Lossy: filters internals and repeated history | -| **Write** | append to `events.lance` | live upsert or batch append / full materialize | -| **Relation** | row count ≥ block count (materialize only shrinks) | Rebuild from Lance can repair drift | - -### 7.2 Materialize filtering (one policy) - -Whether the write is live or a later materialize, **the same rules** -decide whether an event appears in Markdown, for example: - -- Internal `count_tokens` and shadow-model warmup; -- Claude Code-style **history replay** (resend that does not increase - the user-message count); -- Empty records with no visible body; -- Pure lifecycle and cancel-only records (kept in Lance). - -Events that still matter to humans, such as spawn links, are **not** -dropped by mistake. - -### 7.3 Session summary (frontmatter) - -Each Markdown session file may carry a YAML summary: `turns`, tokens, -estimated cost, subagent list, client info, and so on. -**Turn count follows the story read model.** The in-block `turn` field -is a display heuristic only, not the authoritative count. - -### 7.4 Three-track reconcile - -When a Run ends normally, each session is compared: - -| Track | Meaning | -|------|------| -| **Markdown** | Call set in materialized blocks | -| **Lance** | Call set that should appear as dialogue in the event log | -| **Story** | Call set obtained by replaying events | - -Only when all three agree and structure checks pass is the -human-readable view considered aligned with the source of truth. On -mismatch, apply materialize or inspect dead letter rather than trusting -Markdown directly. - -### 7.5 Auxiliary artifacts - -| Artifact | Role | -|------|------| -| Event WAL | Replay unconfirmed capture events after a process crash | -| dead letter | Retain and replay apply failures or Lance flush failures | -| Story snapshot | On exit, freeze each Story's turn read model for summary and recovery | - ---- - -## 8. Gateway and protocols - -Persisting Gateway is a **lightweight LLM protocol gateway**. It serves -"local or team-fixed upstream + capture" and does not replace a cloud -vendor console. - -| Capability | Notes | -|------|------| -| **Model routing** | Match model names in config order; prefix/wildcard and single-hop forward. | -| **Protocol bridge** | For example Anthropic Messages ↔ OpenAI Completions. Responses API falls back when the upstream is not OpenAI. | -| **Stream translation** | Unified SSE shape; TTFT observation and cached replay of reasoning fields. | -| **Auth** | Inject API keys from config, environment, or client headers; header names follow the provider. | - -Gateway logic stays strictly at the **protocol layer**. It does not -enter the story-layer state machine, so routing rules stay decoupled -from turn semantics. - ---- - -## 9. Multi-agent and sessions - -### 9.1 Routing and storage keys - -Each HTTP request binds a **capture route**: logical session, on-disk -storage key (which decides the `.md` filename and the Lance event-log -path), and an optional subagent id. -Under a Capture run, subagents usually write `agent-{id}.md`; the main -session writes `run-{id}.md` or a flat session name. - -### 9.2 File-isolation invariants - -- Subagent body text appears only in **agent-*** files; -- The main Agent's spawn references and links appear in **run-*** files - and do **not inline** the full subagent text; -- Block-header JSON carries machine-readable links; body footnotes are - human-only (parse roundtrip strips footnote lines). - -### 9.3 Spawn linking - -The spawn hint in a main-Agent assistant message and the subagent's -first-packet registration may be **time-skewed**. A Run-level registry -does delayed match and backfill so the main session can still see, after -the fact, which subagent was called and where its trajectory file is. - -### 9.4 One run dataset, several `session_id`s (Claude run bucket) - -A `pvisor run --record-format lance --record-destination WAREHOUSE` -pChronicle sidecar usually writes one `events.lance/` dataset under the -run directory, but in-row `session_id` **may mix several values**. -pVisor does not open Lance itself: - -| Typical source | `session_id` value | -|----------|-------------------| -| pVisor lifecycle / Run header | `run-{uuid}` (matches the directory name) | -| Claude Code dialogue HTTP | UUID injected via header (different from the run id) | - -So when internal stats expand a run bucket -(`session_id == root_session_id`), they first read distinct -`session_id`s from Lance, then **stat each partition**, avoiding "the -second session shows 0 turns". Implementation: -`persisting-pchronicle::expand_story_locations`. The current public CLI -exposes stats through `analysis` and `query`. See the run-bucket -partition notes in -[Run storage](../../pchronicle/design/trajectory-storage.md). - ---- - -## 10. Reliability and runtime shapes - -### 10.1 Reliability model - -```text -Request thread ──► emit event (non-blocking WAL enqueue + apply enqueue) ──► continue forward - │ - └──► background: ordered apply ──► pChronicle sidecar / Markdown - │ - ├─ success → confirm WAL - └─ failure → dead letter + keep WAL (replay on restart; HTTP unaffected) -``` - -| Mechanism | Purpose | -|------|------| -| **Async apply** | Capture does not occupy the upstream connection thread | -| **Blocking-sink isolation** | sidecar durable ACK wait runs on a blocking pool, not Gateway Tokio HTTP workers | -| **Per-story ordered queue** | Event order is reproducible inside one storyline | -| **Event WAL** | The request thread only does a bounded `try_send`; the background waits at most 2 ms to batch and `sync_data`. Persisted events can replay after a crash | -| **ACK WAL** | Async best-effort batching. A lost ACK only causes safe replay. The flush/shutdown barrier persists already-received ACKs first | -| **Barrier flush** | Drain queues and actor mailboxes before graceful exit | -| **Dead letter** | Operable replay instead of silent drop | - -Known limits (implementation still tightening): WAL sequence and -duplicate-delivery policy under extreme crash, and the I/O cost of -full-file Markdown upsert on very long sessions — see §11 Evolution. - -### 10.2 Runtime shapes - -| Shape | When to use | -|------|----------| -| **`pvisor run`** | Wrap one Agent command (for example `claude`, `codex`); inject proxy environment variables and manage the embedded Gateway | -| **pChronicle sidecar / extra Markdown** | `--record-format lance` persists to `events.lance/` via sidecar; enable `--gateway-stream-markdown` as well when live md is needed | -| **Dead letter** | Retained in Run storage for pChronicle API diagnosis | - -Config excerpt: - -```toml -listen = "127.0.0.1:19080" -admin_listen = "127.0.0.1:9876" -agent_id = "my-team" -capture_level = "dialogue" - -[[models]] -name = "deepseek-chat" -upstream = "https://api.deepseek.com/v1" -api_key_env = "DEEPSEEK_API_KEY" -``` - -The admin port serves health and session-list queries (usage, model, -active request count) for sidecar monitoring. - ---- - -## 11. Evolution - -!!! note "Target architecture" - The items below are product-level directions. They are not current - capabilities and they do not commit a schedule. - -| Direction | Motive | -|------|------| -| **Multimodal sidecar (Phase 1)** | Persist base64 / generated images under `{run}/assets/`; Lance stores references only. Supports materialize embeds and controlled replay | -| **Cursor live capture and import** | Injection and JSONL import on par with Claude Code | -| Lance dataset split and compaction | Split strategy when `events.lance/` grows too large on a long run | -| Stronger WAL and sequence recovery | Lower risk of duplicate apply and seq conflict after a crash | -| Markdown append log + periodic compact | I/O and git-diff friendliness of live upsert on long sessions | -| External price table | Configurable cost estimates in the summary | -| Story read-model enrich | Close the loop on parent/child Stories, call metadata, and spawn | -| Lance column-layout optimization | Better columnar retrieval instead of large blobs | -| Narrower protocol surface | Shrink the conversion matrix as industry APIs stabilize | - -Block format is explicitly versioned with `v: 1`. See -[AgenticMD run format](../../pchronicle/reference/agenticmd.md). - ---- - -## 12. Further reading - -| Document | Contents | -|------|------| -| [Capture quick start](../guides/capture.md) | **Getting started**: build the CLI, `pvisor run`, view trajectories, troubleshoot | -| [Run storage](../../pchronicle/design/trajectory-storage.md) | Lance ↔ Markdown data flow, materialize, import | -| [AgenticMD run format](../../pchronicle/reference/agenticmd.md) | Block structure, field spec, subagent footnotes, golden examples | -| [pVisor commands](../reference/cli.md) | Single-Run execution, status, and filesystem operations | -| [pChronicle CLI](../../pchronicle/reference/cli.md) | Dataset query, analysis, exchange, and read-only serve | - -**Runnable examples**: - -- [Gateway capture and LLM control](https://github.com/DeepLink-org/Persisting/tree/main/examples/pvisor/04-gateway-llm-control) - ---- - -*This page tracks Persisting Gateway releases. If behavior and the -document disagree, the tests and golden fixtures in the repository -win.* diff --git a/docs/src/en/pvisor/design/index.md b/docs/src/en/pvisor/design/index.md deleted file mode 100644 index 5cd690905..000000000 --- a/docs/src/en/pvisor/design/index.md +++ /dev/null @@ -1,15 +0,0 @@ -# pVisor design - -These pages explain how pVisor realizes one Agent virtual execution environment. - -| Area | Document | -| --- | --- | -| Provider portfolio and security properties | [Isolation architecture](isolation.md) | -| Transparent VM and planned host interception | [OverlayNet](overlaynet.md) | -| Model routing, capture, and event emission | [Gateway](gateway.md) | -| Product command model and lifecycle semantics | [CLI design](cli.md) | - -Cross-product Run and history ownership is defined in -[System Design](../../system-design/index.md). User-facing behavior is described -by the [pVisor guides](../guides/index.md), not by roadmap sections in design -documents. diff --git a/docs/src/en/pvisor/design/isolation.md b/docs/src/en/pvisor/design/isolation.md deleted file mode 100644 index 91beb2555..000000000 --- a/docs/src/en/pvisor/design/isolation.md +++ /dev/null @@ -1,833 +0,0 @@ -# pVisor isolation architecture - -This document compares provider mechanisms and their actual security -properties. Choose a provider with the [execution guide](../guides/execution.md) -and interpret guarantees with -[Capabilities and evidence](../concepts/capabilities-and-evidence.md). - -!!! note "Target architecture" - This document combines current implementation with explicitly identified - target architecture. Linux `pvisor --` implements the FUSE + - synthetic root + rootless user/mount namespace + Landlock path described - in section 2. macOS host execution uses Seatbelt when available and staged writes - deny-all socket confinement; filesystem reads remain ambient and are - reported separately. Docker and libkrun/KVM transports also exist. - The Virtualization.framework backend in section 2.5, LiteBox VFS in - section 3, the Docker production profile in section 4.2, and the - Firecracker architecture in section 5 are targets, not implemented - backends. Seccomp and complete resource enforcement remain acceptance - criteria and roadmap work. - -pVisor needs more than one isolation backend. A local coding Agent values fast -startup and an exact view of the developer's workspace; an untrusted tenant -requires a boundary that remains useful after the guest runtime is compromised. -The design therefore separates the **transactional workspace** from the -**enforcement boundary** instead of trying to make one mechanism serve both -roles. - -The multiple backends are an implementation portfolio, **not a configuration -surface imposed on the user**. The normal product experience remains: - -```bash -pvisor -- [args...] -``` - -pVisor probes the host, workload, and available placement, selects a backend, -constructs the workspace, and applies the policy. Users do not configure -Landlock rights, mount propagation, UID maps, 9P transports, seccomp JSON, -container capabilities, TAP devices, or microVM images. Expert backend flags -may exist for development and diagnosis, but must not be required for the -normal path. - -Easy to use does not mean an invisible security downgrade. If the requested -guarantee cannot be provided, pVisor either chooses another available backend -or returns one actionable error. It never reports a `cwd`-only Run as safely -sandboxed. - -## 1. Common model - -```text -RunSpec / capability policy - | - v - pVisor supervisor (trusted) - | - +-- WorkspaceOverlay - | lower + compose + writable upper - | review / checkpoint / apply / drop - | - +-- IsolationBackend - workspace-landlock | workspace-seatbelt - litebox | container | microvm - | - v - Agent process tree (untrusted) -``` - -`WorkspaceOverlay` is the data plane for file changes. It provides an isolated -Run view, copy-on-write, whiteouts, and an auditable changeset. It does **not** -by itself prevent a process from opening a path outside that view. - -`IsolationBackend` is the security plane. It determines which kernel, syscall -surface, namespace, host paths, file descriptors, and network paths the Agent -can reach. Every backend consumes the same logical workspace and must return a -changeset with the same review/apply/drop semantics. - -The following invariants apply to backends that claim complete capability -enforcement. A partial native backend may enforce a smaller dimension only -when the Run Bundle identifies that dimension explicitly and records the -remaining ambient access: - -1. Deny by default; every host file, socket, credential, device, and endpoint is - an explicit capability. -2. The Agent never receives host control-plane credentials or the Docker socket. -3. Only `stdin`, `stdout`, `stderr`, the Run-scoped AgentCtl transport, and - explicitly granted resource handles cross the execution boundary. -4. The writable workspace is separate from the read-only base. A successful - process exit never implies permission to apply its changes. -5. Requested and effective enforcement are recorded separately. An enforce - request fails closed when its backend is unavailable; it never silently - falls back to the current `cwd`-only behavior. -6. pVisor records enough evidence to audit the boundary: backend/version, - workspace digest, effective UID/capabilities, kernel feature probes, - network mode, resource limits, image/rootfs digest, and downgrade reasons. - -## 2. Native host paths - -### 2.1 Linux: FUSE + Workspace + Landlock - -This is the preferred lightweight Linux host path. It keeps today's embedded -FUSE OverlayFS and adds a kernel-enforced, unprivileged filesystem policy to -the Agent process tree. - -Landlock is entirely internal: no system policy file, root helper, daemon, or -per-project rule configuration is exposed to the user. pVisor derives the -rules from the workspace, executable/runtime closure, explicit inputs, and -Run-scoped scratch directory. - -#### Current implementation - -The native Linux safe path is operational for ordinary local executables: - -- pVisor self-executes a hidden launcher before Agent code starts; -- the launcher creates one-ID user plus private mount and child PID namespaces without - `/etc/subuid`, `newuidmap`, a setuid binary, or a daemon; -- a private tmpfs root bind-projects only the runtime, staged workspace, exact - device nodes, Run-scoped AgentCtl socket, and explicit capabilities before - the launcher enters it with `chroot`; arbitrary host pathname Unix sockets - are therefore absent rather than left to Landlock; -- inherited descriptors above stderr are closed, Landlock ABI v3 handles all - filesystem rights through `TRUNCATE`, `no_new_privs` is set, namespace and - ambient capabilities are cleared, and a trusted namespace PID 1 supervises - and reaps the Agent tree; -- successful main-process exit, cancellation, and forced termination all end - at the PID namespace boundary, so `setsid` and double-fork descendants cannot - survive the Run; -- the writable FUSE merged workspace and explicit read/write capabilities are - admitted, while the executable and a broad host runtime are read-only; -- `NetworkCapability::Deny` also creates a private network namespace. Public - and allowlist proxy policies remain cooperative and are not reported as - non-bypassable; -- any namespace or Landlock setup error terminates before Agent execution with - a reserved infrastructure result and a Run Bundle downgrade warning. - -The broad immutable runtime currently includes existing `/bin`, `/sbin`, -`/usr`, `/lib*`, `/etc`, and the process-local procfs views. This favors -compatibility with shell, Python, Node, and dynamically linked local tools. A -measured runtime-closure builder may narrow it later; the current policy never -makes those hierarchies writable. - -The default pVisor dependency graph does not include a pChronicle storage -backend, Lance, or DataFusion. Durable Attempt and trajectory publication uses -the lightweight `persisting-events` control feature to start and communicate -with the Control component of `pchronicle serve`; storage-engine and cloud SDK dependencies -remain in that process. `jujutsu-overlay` adds the Jujutsu OverlayFS upper. -See [RFC-0007](../../rfcs/0007-events-contract-pchronicle-sidecar.md) for the -dependency boundary. - -```text -pVisor process - +-- embedded FUSE server - | base/compose (read-only) + upper (writable) - | | - | v - | merged workspace - | - +-- small sandbox launcher - close unrelated FDs - synthetic bind-projected root + chroot - PR_SET_NO_NEW_PRIVS - Landlock ruleset - | - v - Agent process tree -``` - -The pVisor supervisor and FUSE request loop remain outside the chroot and -Landlock domain. The child receives read/write access to the merged workspace, -read/execute access to a runtime, and read-only access to explicit inputs. An -unprojected path is absent from the synthetic root; Landlock independently -enforces access rights over projected hierarchies. Absolute paths are resolved -inside that root rather than redirected into the workspace. - -### 2.2 Minimum policy - -| Hierarchy | Effective access | -|---|---| -| merged workspace | read, write, create, remove, rename, link as required | -| Agent executable and loader | read, execute | -| required shared libraries and runtime data | read-only | -| explicit input datasets | read-only | -| Run scratch directory | read/write; preferably a size-limited tmpfs | -| pVisor state, pChronicle, source credentials, home directory | denied | -| `/proc`, `/sys`, host sockets | not admitted to Agent access unless explicitly projected or separately virtualized | -| minimal devices | exact null, zero/full, random/urandom, and tty nodes only | - -Landlock is additive to normal DAC/ACL/LSM checks; it does not grant an access -the process did not already have. The launcher must negotiate the running -kernel's Landlock ABI and handle all security-relevant rights supported by that -ABI. Older ABIs may lack controls such as cross-directory refer or truncate, -so pVisor must publish the effective guarantee rather than a boolean -"Landlock enabled" flag. - -Files opened before `landlock_restrict_self` are not retroactively constrained. -FD hygiene is consequently part of the boundary: prepare directory handles, -close everything not granted, install `no_new_privs` and the ruleset, then -`exec`. This setup belongs in a small auditable launcher, not in a complex -post-fork closure of the multithreaded supervisor. - -### 2.3 Security and operational properties - -**Strengths** - -- No host root or persistent privileged daemon is required. -- Startup and steady-state overhead are small; file contents still use the - existing FUSE/OverlayFS path. -- Workspace fidelity remains the best of the native host paths, including - current review, checkpoint, apply, and drop behavior. -- A child can no longer escape merely by using `..` or an absolute host path. - -**Limits** - -- The Agent still uses the host kernel and its native syscall ABI. -- Landlock is a filesystem access-control layer, not a root filesystem, - network namespace, resource controller, or complete process sandbox. -- Runtime allowlists are difficult for dynamic language stacks unless pVisor - builds a minimal runtime bundle. -- FUSE context switches remain on the hot path for workspace I/O. -- Linux only. The macOS sibling path has a different, explicitly narrower - Seatbelt boundary. - -The implementation already combines Landlock with an empty capability set, -`no_new_privs`, rootless user/mount/PID namespaces, and a network namespace for -deny-all Runs. Seccomp, complete aggregate resource limits, and transparent -enforcement for selective egress remain necessary hardening without changing -the workspace contract. - -### 2.4 macOS: FUSE + Seatbelt - -The native macOS safe path is operational for ordinary local executables. It -keeps the same staged macFUSE workspace while adding a kernel-enforced Seatbelt -policy around the complete Agent descendant process tree: - -- pVisor invokes only the fixed system `/usr/bin/sandbox-exec`, never a PATH - lookup or a project-supplied wrapper; -- the generated SBPL uses `-D` parameters for every writable path, so a - workspace name cannot inject policy text; -- path-parameterized `file-write*` rules admit only the mounted staged - workspace, explicit read-write filesystem capabilities, exact - terminal/device handles, a Run-owned temporary directory, and a one-time - setup attestation; -- the hidden launcher writes and unlinks that attestation before `exec` of the - Agent. A profile compile/apply failure therefore cannot be mistaken for an - Agent exit and terminates the Run as an infrastructure failure; -- `NetworkCapability::Deny` starts from a deny-by-default profile, blocks IP - sockets and outbound ambient host Unix sockets, and retains only the exact - Run-scoped AgentCtl plus Unix IPC rooted in Run-owned directories; -- public and selective proxy modes remain cooperative because the first - implementation does not yet constrain direct sockets to only the in-process - proxy endpoint. - -The compatibility profile deliberately leaves filesystem reads ambient. This -avoids hard-coding a brittle closure of Homebrew, Xcode, Python, Node, Rustup, -SDK, framework, and user-installed runtime paths. Consequently the Run Bundle -sets `filesystem_write_non_bypassable=true` but keeps -`filesystem_read_non_bypassable=false` and the aggregate -`filesystem_non_bypassable=false`. A future measured runtime-closure mode may -make reads deny-by-default without changing the workspace contract. - -Seatbelt improves the local macOS boundary materially, but it is not a VM or a -complete process sandbox: the host kernel, PID namespace, syscall surface, and -resource accounting remain shared. The `sandbox-exec` interface is deprecated -by Apple even though it remains shipped, so pVisor probes the fixed binary and -fails closed instead of promising indefinite platform availability. macFUSE is -still required for transactional staging until an FSKit backend is available. - -### 2.5 macOS: Virtualization.framework + host-root overlay - -This is the proposed macOS kernel-isolation path for native Mach-O workloads. -It is the macOS analogue of the implemented Linux libkrun full-root executor, -but it cannot use libkrun: a macOS guest must be booted by Apple's -Virtualization.framework on Apple Silicon. The objective is that an executable -sees the invoking host's root filesystem, with all writes captured by the -pVisor upper layer, while executing behind a separate macOS kernel boundary. - -The guest's boot disk is not the Agent's logical root. It contains only a -compatible macOS installation and a privileged pVisor guest supervisor. The -host constructs `host / + Run upper` through OverlayFS/macFUSE, exports the -merged view with VirtioFS, and asks the guest supervisor to enter that view -before executing the target: - -```text -host pVisor (Rust) - +-- host / (lower, access still limited by the invoking host identity) - +-- per-Run upper - +-- merged pVisor root (macFUSE / future FSKit) - +-- MacVmExecutor - | - | private Unix socket / framed control protocol - v - pvisor-vz-helper (Swift, one helper process per active VM) - Virtualization.framework - +-- compatible macOS boot disk - +-- stable VirtioFS share tag -> merged pVisor root - +-- VZVirtioSocket control and AgentCtl transports - | - v - pvisor-guestd (root LaunchDaemon) - mount VirtioFS at a private path - chroot into the pVisor root - setgroups / setgid / setuid - set cwd, environment, limits, and stdio - execve host Mach-O -``` - -The Swift helper is deliberately outside the Rust supervisor. It owns only the -Objective-C/Swift Virtualization.framework lifecycle and converts it into a -small versioned protocol. The existing `RunExecutor` contract remains the -product boundary, so selection, cancellation, evidence, review, checkpoint, -apply, and drop keep the same semantics as other pVisor executors. - -#### GhostVM research - -[GhostVM](https://github.com/groundwater/GhostVM) is the closest examined -reference implementation. At commit -[`fe88d586`](https://github.com/groundwater/GhostVM/tree/fe88d5862f74ddb05ce79e04028b84c7f70482f6) -it demonstrates the required control-plane primitives: - -- `VZMacOSBootLoader`, Mac platform identity, a macOS disk, headless display - configuration, VirtioFS, and a virtio socket are assembled in one - [configuration builder](https://github.com/groundwater/GhostVM/blob/fe88d5862f74ddb05ce79e04028b84c7f70482f6/macOS/GhostVMKit/Configuration/VMConfigurationBuilder.swift); -- one helper process owns an active VM and exposes a host Unix-socket API; -- host requests cross `VZVirtioSocket` to a guest agent, which can execute - native macOS programs; -- a running VirtioFS device can receive a rebuilt directory share through - [FolderShareService](https://github.com/groundwater/GhostVM/blob/fe88d5862f74ddb05ce79e04028b84c7f70482f6/macOS/GhostVM/Services/FolderShareService.swift); -- VM suspend/resume uses `saveMachineStateTo` and `restoreMachineStateFrom`; - APFS `clonefile()` creates copy-on-write VM clones in - [VMController](https://github.com/groundwater/GhostVM/blob/fe88d5862f74ddb05ce79e04028b84c7f70482f6/macOS/GhostVMKit/Operations/VMController.swift#L519). - -These are architectural references, not a filesystem-execution solution. -GhostVM boots and executes against its private `disk.img`; VirtioFS directories -remain shares mounted under the normal guest root. Its guest `exec` endpoint is -a user LaunchAgent calling Swift `Process.run()` with buffered stdout and -stderr. It does not chroot, reproduce credentials, stream stdio, forward -signals, control a process group, or expose a pVisor changeset. The pVisor guest -supervisor must therefore be an independently implemented root LaunchDaemon. - -The following split is intentional: - -| GhostVM mechanism | pVisor decision | -|---|---| -| VM configuration builder | reproduce the minimal headless subset in `pvisor-vz-helper` | -| one helper process per VM | retain for VMM crash and lifecycle isolation | -| host Unix socket plus vsock | retain the topology; use a bounded, versioned, streaming protocol | -| runtime VirtioFS replacement | adapt to one stable pVisor-root tag | -| VM suspend/resume | use to amortize boot, with strict template compatibility | -| APFS VM clone | optionally use for creation of a clean boot template | -| GhostTools command execution | replace with privileged `pvisor-guestd` | -| NAT, bridge, clipboard, audio, GUI automation | omit from the default process-isolation VM | -| private guest root as workload root | reject; the exported pVisor merged root is the workload root | - -GhostVM's README currently says its source-code license has not been -determined. pVisor may study the public behavior and architecture but must not -copy its implementation unless a compatible license is published. The helper -and guest supervisor are clean independent implementations against Apple's -public API. - -#### Filesystem and identity semantics - -"Use the host UID and permissions" means preservation of ordinary POSIX file -semantics, not inheritance of every macOS security identity. The host pVisor -opens and serves lower files under the invoking host identity; the guest -supervisor then installs matching numeric UID, GID, and supplementary groups -before `execve`. The implementation must prove how VirtioFS represents owner, -mode, ACL, symlink, hard-link, xattr, device, and rename semantics rather than -assuming numeric identity is sufficient. - -The following host facilities do not become transparent merely because the -numeric UID matches: - -- TCC decisions, Keychain access groups, code-signing identity and entitlements; -- the host login/GUI bootstrap session, launchd services, Mach ports, Apple - Events, and host Unix sockets; -- host kernel state, devices, mounted volumes not visible through the exported - root, and credentials held only by host processes. - -Modern macOS also presents `/` through a sealed system volume, a writable data -volume, and firmlinks. pVisor must verify that exporting the host root presents -one coherent namespace and that whiteout/copy-up behavior remains correct -across `/System/Volumes/Data`. Access to privacy-protected host files may -require Full Disk Access for the trusted host component; pVisor must report -that requirement rather than silently returning a partial root. - -Host and guest should initially require the same architecture and exact macOS -build. A host executable can depend on the matching dyld shared cache, -framework ABI, code-signing policy, and kernel behavior. Cross-build execution -is unsupported until a compatibility matrix proves otherwise. - -#### Lifecycle and security profile - -Cold-installing macOS per Run is infeasible. The intended lifecycle is: - -1. provision and attest one minimal, matching macOS boot template; -2. boot it once, install `pvisor-guestd`, and save a clean suspended state; -3. restore a warm VM or acquire one from a small version-matched pool; -4. attach only the Run's VirtioFS root and per-Run vsock endpoints; -5. rotate Run identity, authentication material, entropy, IPC, and network - state before guest execution; -6. execute exactly one untrusted process tree, export the upper through the - normal pVisor review path, then destroy or return a scrubbed VM to the pool. - -The default VM has no NAT or bridged network device. Network access crosses an -explicit vsock relay owned by OverlayNet. Clipboard, host audio, GUI devices, -arbitrary shared folders, port forwarding, and ambient host sockets are absent. -The host VMM helper receives access only to the prepared merged root, VM -template, its private control socket, and required Virtualization.framework -resources; it must not inherit pChronicle, source credentials, or unrelated -descriptors. - -VirtioFS is the largest feasibility risk. GhostVM has an open report of -[empty mounts and unreadable files](https://github.com/groundwater/GhostVM/issues/255) -under macOS guests. pVisor's design puts dyld, frameworks, SDKs, package -managers, and metadata-heavy toolchains on that path, which is more demanding -than sharing a project directory. VM startup success is therefore not evidence -that the backend is usable or safe. - -#### Feasibility gate - -This backend remains experimental until one focused prototype passes all of -the following on a supported host/guest build pair: - -1. export a pVisor merged root with a stable VirtioFS tag and mount it without - Finder or login-session automation; -2. run `/usr/bin/true`, `/bin/zsh`, and representative `xcrun`/compiler tools - after `chroot`, with correct cwd, environment, UID, GID, groups, exit status, - streaming stdio, signals, cancellation, and descendant cleanup; -3. prove lower files do not change and all creates, modifications, renames, - deletions, whiteouts, xattrs, ACLs, symlinks, and hard links enter the Run - upper and survive review/checkpoint/apply/drop; -4. exercise dyld/framework loading, code signatures, the sealed-system/data - firmlink layout, large output, large files, many small files, concurrent - mutation, crash recovery, and warm-restore attachment changes; -5. demonstrate that no network, clipboard, arbitrary share, stale vsock token, - prior-Run upper, or unrelated host descriptor is reachable; -6. publish cold/warm latency and RSS and compare them with Seatbelt and Linux - libkrun Runs. - -Passing this gate establishes transparent CLI and development-tool execution. -GUI applications and host-session services require separate evidence and are -not implied by success of the process-level backend. - -## 3. Path B: LiteBox + OverlayFS semantics in the VFS - -### 3.1 Positioning - -This is the high-density libOS path. LiteBox handles the guest Linux ABI and -path resolution in userspace. pVisor should implement its overlay semantics as -a LiteBox filesystem backend or composer, rather than FUSE-mounting a host path -and forwarding guest path strings to host `openat`. - -```text -pVisor supervisor - +-- build content-addressed root/workspace bundle - +-- pass sealed bundle FD + policy + AgentCtl FD - | - `-- LiteBox runner process - LiteBox Linux shim - | - v - LiteBox VFS resolver - +-- read-only root/runtime - +-- read-only workspace layers - `-- writable in-memory/delta upper - | - v - exported changeset - | - v - pVisor review / apply / drop -``` - -The adapter preserves pVisor's logical operations: - -- ordered read-only base and compose layers; -- copy-up on first write; -- whiteout and opaque-directory semantics; -- deterministic directory merge; -- metadata policy for modes, timestamps, symlinks, hard links, and xattrs; -- a bounded writable upper that can be exported without traversing unrelated - host paths. - -The initial implementation can reuse LiteBox's read-only tar and in-memory -filesystems, but production adoption requires a filesystem semantic matrix. -Unsupported metadata must fail explicitly or be normalized by a documented -policy; silent loss would break pVisor's changeset contract. - -### 3.2 Security and operational properties - -**Strengths** - -- Guest paths terminate in the LiteBox VFS; the normal path contains no host - pathname lookup. -- A smaller host interface than a native Linux process or general OCI - container makes syscall-level policy and deterministic I/O practical. -- Read-only content-addressed bundles can be cached and shared across Runs; - writable state remains per-Run. -- No kernel FUSE round trip is needed for VFS operations handled entirely in - the runner, which may benefit metadata-heavy workloads. - -**Limits** - -- Linux syscall and filesystem compatibility is narrower than Docker or a VM. -- LiteBox and its pVisor adapter are evolving code and expand pVisor's trusted - computing base. -- A userspace libOS is not automatically a hardware or kernel security - boundary. Bugs in the runner, loader, syscall interception, or shared - address space must be assumed possible. -- Packaging native libraries, dynamic runtimes, JITs, and unusual filesystem - behavior requires explicit compatibility testing. - -The LiteBox runner must therefore execute in a separate unprivileged process -with Landlock, seccomp, `no_new_privs`, empty capabilities, closed FDs, and -resource limits. The outer kernel policy is the containment boundary if the -guest escapes the LiteBox abstraction. Embedding an untrusted LiteBox guest in -the pVisor supervisor process is forbidden. - -### 3.3 Workspace transfer - -Avoid a long-lived, arbitrary pathname broker. Prefer immutable and bounded -objects: - -1. pVisor snapshots the logical lower layers and computes a digest. -2. It supplies a sealed `memfd` or read-only file descriptor to the runner. -3. LiteBox reads the root and workspace through its VFS. -4. Writes enter a per-Run upper with byte/inode quotas. -5. The runner exports a canonical, bounded changeset. -6. pVisor validates paths, entry types, metadata, sizes, and digest before - exposing the changeset to review/apply. - -## 4. Path C: Docker / OCI container - -### 4.1 Positioning - -This is the compatibility and ecosystem path. It supports existing Agent -images and conventional Linux runtimes with stronger placement isolation than -the host executor, while sharing the host kernel. - -The current pVisor Docker/Podman executor already injects a matching static -pVisor into the image and delegates the same `RunSpec`. It mounts the final -workspace and returns a typed `RunResult`. It does not yet translate every -pVisor capability into an OCI restriction, and the injected pVisor currently -bootstraps as container root; those are implementation gaps, not properties of -the target design. - -```text -host pVisor - +-- WorkspaceOverlay / merged view - +-- Docker or Podman transport - | - v - OCI container - read-only image rootfs - /workspace -> pVisor Run view - tmpfs /tmp - injected pVisor -> Agent -``` - -Docker's image-layer OverlayFS and pVisor's WorkspaceOverlay have distinct -roles. The former assembles an OCI root filesystem; the latter owns Agent -changes and review/apply/drop. Container teardown must not commit the OCI -writable layer as the Run result. - -### 4.2 Production profile - -The target profile is: - -- rootless Docker/Podman when supported, or user namespace remapping; -- non-root Agent UID after the injected pVisor bootstrap issue is removed; -- all capabilities dropped, `no-new-privileges`, default or tighter seccomp; -- read-only container rootfs and a private, bounded `/tmp`; -- PID, memory, CPU, file-size, and process-count limits; -- no network by default, otherwise a dedicated namespace connected to a - pVisor-owned broker; -- no host PID/IPC namespace, privileged mode, device passthrough, arbitrary - writable mounts, or Docker socket; -- image digest pinning and an auditable mount/capability manifest. - -### 4.3 Security and operational properties - -**Strengths** - -- Highest workload compatibility short of a VM. -- Mature image construction, distribution, caching, observability, and - operational tooling. -- Namespaces, cgroups, capabilities, seccomp, and host LSMs compose into a - practical production boundary. -- Natural deployment path for Kubernetes and existing CI infrastructure. - -**Limits** - -- Containers share the host kernel; a kernel or container-runtime escape is - outside pVisor's own enforcement. -- Cold-start cost, image storage, daemon/runtime dependencies, and mount - plumbing are higher than the local and LiteBox paths. -- Rootful daemon deployments create a larger privileged control plane. -- Host networking, broad bind mounts, `--privileged`, or the Docker socket can - erase most of the isolation value. -- The current Gateway loopback integration requires host networking in some - configurations; production enforcement needs a guest-visible broker before - that restriction can be removed. - -Docker is the recommended compatibility fallback, not the definition of -pVisor's capability model. - -## 5. Path D: Firecracker microVM - -### 5.1 Positioning - -This is the strongest multi-tenant path. Each Run or warm Run slot receives a -separate guest kernel under KVM. Firecracker intentionally exposes a small -device model and provides a jailer that adds host-side namespace/cgroup -isolation and drops VMM privileges. - -The existing pVisor `vm` executor statically links libkrun and can either use -an explicit Linux rootfs or pull a public OCI image without a container daemon. -Verified image layers form an immutable cached lower rootfs, guest system writes -stay in a reviewable upper layer. Host paths are not implicitly exposed. An -explicit `--overlayfs-compose` plus `--overlayfs-path` mounts a staged view at -the selected guest path. A vendored libkrun serves both root and workspace -copy-on-write unions directly over virtio-fs on Linux and macOS, without a -host FUSE mount, materialization, or reconciliation. It uses KVM on Linux -and HVF on Apple Silicon macOS. Linux -additionally confines the VMM with user/mount/network namespaces and Landlock. -The macOS VMM is not yet wrapped in an equivalent host filesystem sandbox, so -libkrun's virtio-fs proxy remains in the invoking user's security context. -OverlayNet and AgentCtl guest relays are not implemented in this phase. This -is not the hostile multi-tenant Firecracker design below: - -```text -host pVisor / microVM manager - +-- immutable kernel + rootfs image - +-- read-only workspace/base block image - +-- per-Run writable delta block image - +-- vsock control and AgentCtl transport - +-- TAP/network broker under policy - | - v - Firecracker + jailer - | - v - guest kernel + injected pVisor + Agent -``` - -The rootfs and workspace are attached as file-backed block devices. At Run -completion, the guest quiesces the filesystem and returns a manifest over -vsock; the host validates and converts the delta into the normal pVisor -changeset. Firecracker snapshots can amortize boot cost, but VM state, guest -memory, block devices, network devices, and vsock endpoints have separate -lifecycle and compatibility requirements. Snapshot reuse must rotate Run -identity, entropy, credentials, and network state. - -### 5.2 Security and operational properties - -**Strengths** - -- A separate guest kernel provides the clearest boundary for mutually - untrusted tenants and hostile native code. -- Minimal device emulation reduces VMM attack surface relative to a general - machine emulator. -- Resource accounting and network topology are explicit at the VM boundary. -- Warm pools and snapshots can make repeated Run startup practical. - -**Limits** - -- Requires Linux, KVM, kernel/rootfs image production, a jailer, TAP/network - setup, and a microVM lifecycle service. -- Baseline memory and operational complexity are higher than process/container - paths even when the VMM is lightweight. -- Workspace block-image creation and delta extraction are less interactive - than a directly mounted FUSE workspace. -- Kernel, rootfs, snapshot, and VMM versions form a larger compatibility and - patch-management surface. -- Direct host directory sharing would weaken the clean boundary and should not - become the production workspace design. - -Production Firecracker execution must use the jailer or an equivalent stronger -host policy, a dedicated unprivileged VMM identity, cgroups, seccomp, isolated -networking, trusted immutable inputs, and no ambient access to host paths. - -## 6. Comparison and selection - -The table describes the intended production shape, not just the code currently -present in the repository. Performance is deliberately relative until a common -benchmark has measured cold/warm startup, RSS, syscall-heavy and data-heavy -workloads, and teardown. - -| Dimension | FUSE + Landlock | FUSE + Seatbelt | macOS VM + VirtioFS | LiteBox VFS | Docker/OCI | Firecracker | -|---|---|---|---|---|---|---| -| Primary goal | fastest Linux least privilege | zero-config macOS write confinement | transparent Mach-O execution with a guest-kernel boundary | dense libOS isolation | compatibility and deployment | hostile multi-tenant isolation | -| Security boundary | synthetic root + host LSM/namespace policy | Seatbelt write/socket policy on host process | macOS guest kernel + Virtualization.framework VMM | libOS plus outer host policy | namespaces/cgroups/LSM, shared kernel | guest kernel + KVM + jailed VMM | -| Host root required | no | no | exported as a pVisor merged root | no | no in rootless mode | host provisioning normally required | -| Guest compatibility | native Linux ABI | native macOS ABI; ambient reads | native Mach-O, initially exact host/guest build only | constrained Linux ABI | broad Linux userspace | full guest Linux | -| Workspace fidelity | highest | highest with macFUSE | target is full-root fidelity; unproven over VirtioFS | requires semantic adapter | high through mount/volume | explicit block/delta conversion | -| Startup cost | lowest | lowest | high cold; warm restore/pool target | low target | medium, image dependent | highest cold; warm snapshot target | -| Per-Run memory | lowest | lowest | high | low target | medium | highest | -| Kernel escape blast radius | host | host | guest first, then VMM boundary | host, after outer escape | host | guest first, then VMM/KVM boundary | -| Portability | Linux | macOS; deprecated launcher dependency | Apple Silicon Mac with supported macOS virtualization | platform/ABI dependent | broad OCI hosts | Linux + KVM | -| Current pVisor status | implemented; seccomp/limits pending | write confinement and deny-all socket policy implemented | researched design; feasibility prototype required | planned | implemented with hardening gaps | libkrun full-root mode exists; Firecracker planned | - -### Recommended portfolio - -The selection belongs to pVisor and the placement control plane: - -1. A normal Linux `pvisor --` uses FUSE + Workspace + synthetic root + - rootless namespaces + Landlock today. Required controls are installed - fail-closed; an unavailable user namespace, mount, chroot, or Landlock ABI - never falls back silently. -2. pVisor may choose LiteBox automatically for a compatible packaged workload - when it provides a smaller, measured host interface; the user still invokes - the same command. -3. Supplying an OCI image naturally selects Docker/Podman. Otherwise pVisor - may use an already available rootless runtime as a compatibility fallback; - it does not ask users to construct capability or mount flags. -4. A fleet configured for hostile multi-tenant execution places the Run on a - Firecracker worker. Kernel images, snapshots, networking, and jailer setup - are operator-owned fleet infrastructure, not per-user configuration. -5. macOS keeps the same command and uses Seatbelt write confinement for the - implemented low-latency local path. After the feasibility gate passes, - policy requiring a guest-kernel boundary may select the - Virtualization.framework backend automatically. Until then, the Bundle - reports ambient reads and cooperative selective networking separately and a - stricter request routes to another capable placement or fails with one - remediation. - -These paths are a portfolio, not a mandatory migration ladder. A customer -states workload intent and, where necessary, a minimum security requirement; -placement chooses only a backend whose measured capabilities satisfy it. The -customer does not select kernel mechanisms. - -## 7. One backend contract - -All implementations should compile one request into one evidence-bearing -result: - -```text -IsolationRequest { - minimum_boundary, - filesystem_capabilities, - network_capabilities, - compute_limits, - credential_refs, - require_enforcement, -} - -IsolationEvidence { - requested_class, - effective_backend, - backend_version, - effective_controls, - unsupported_controls, - workspace_digest, - runtime_or_image_digest, - identity_and_capabilities, - kernel_features, -} -``` - -`RuntimeCapabilities.filesystem = true` is valid only when tests demonstrate -that the complete Agent process tree cannot reach a non-granted hierarchy. A -mounted workspace or successful setup call alone is not evidence. - -This contract is internal between admission, placement, and runtime drivers. -It is not a requirement for users to understand or configure backend-specific -mechanisms. - -## 8. Zero-configuration acceptance criteria - -The default local path is complete only when all of the following hold: - -- one pVisor installation and one `pvisor --` command are sufficient; -- no root shell, setuid pVisor daemon, manual group membership, hand-written - policy, mount command, or container security flags are required; -- pVisor discovers the executable and its minimal runtime dependencies; -- workspace setup, isolation, cleanup, and changeset recovery are automatic; -- unsupported hosts produce one stable error with a concrete remediation or - an automatically available placement, rather than a cascade of kernel - details; -- `pvisor status` and the Run Bundle explain the effective boundary for audit - without making that explanation a prerequisite for use; -- upgrades preserve the high-level command and Run contract while allowing the - selected backend to change. - -This criterion rules out 9P as a user-facing Docker setup step. pVisor may use -a filesystem protocol internally when a remote backend requires it, but users -must never provision a 9P server, mount it, or grant a container mount -capability for a normal Run. - -## 9. Validation and benchmarks - -Every backend must run the same adversarial suite: - -- absolute paths, `..`, symlink chains, hard links, rename races, magic links, - `/proc/self/fd`, inherited directory FDs, UNIX sockets, device nodes, and - descriptor passing; -- fork/clone/exec descendants, raw syscalls, static binaries, JIT-generated - code, signals, ptrace attempts, and namespace operations where applicable; -- direct sockets, DNS rebinding, literal IPs, UDP/QUIC, loopback, link-local, - and metadata-service addresses; -- byte/inode/process/CPU/memory/network exhaustion and cancellation cleanup; -- power loss or supervisor crash during workspace export, review, and apply; -- semantic comparison of the same changeset across all four backends. - -The shared benchmark reports distributions rather than a single demo number: - -- cold and warm start P50/P95/P99; -- idle and peak RSS; -- sequential and random workspace throughput; -- small-file metadata operations per second; -- syscall-heavy and Python/Node/native Agent workloads; -- checkpoint/export/apply latency and produced bytes; -- host CPU cost, context switches, page faults, and FUSE/VMM/broker overhead. - -The implemented Linux suite currently proves staged writes plus denial of -absolute-path reads/writes, symlink escapes, `/proc/self/root` escapes, -ungranted pathname Unix sockets, preservation of the exact AgentCtl socket, -and host-loopback access in deny-all mode. It also proves setup failures are -reported before Agent execution. This is a useful regression floor, not yet -the complete adversarial/kernel matrix listed above. No backend becomes a -production default from architectural expectations alone; it must publish -repeatable measurements and pass that matrix on every supported host/kernel. - -## References - -- [Apple: Running macOS in a virtual machine on Apple silicon](https://developer.apple.com/documentation/virtualization/running-macos-in-a-virtual-machine-on-apple-silicon) -- [Apple: VZVirtioFileSystemDeviceConfiguration](https://developer.apple.com/documentation/virtualization/vzvirtiofilesystemdeviceconfiguration) -- [GhostVM](https://github.com/groundwater/GhostVM) -- [GhostVM VirtioFS instability report](https://github.com/groundwater/GhostVM/issues/255) -- [Linux Landlock userspace API](https://docs.kernel.org/userspace-api/landlock.html) -- [Docker rootless mode](https://docs.docker.com/engine/security/rootless/) -- [Docker default seccomp profile](https://docs.docker.com/engine/security/seccomp/) -- [Firecracker](https://github.com/firecracker-microvm/firecracker) -- [Firecracker jailer](https://github.com/firecracker-microvm/firecracker/blob/main/docs/jailer.md) -- [Firecracker snapshot support](https://github.com/firecracker-microvm/firecracker/blob/main/docs/snapshotting/snapshot-support.md) diff --git a/docs/src/en/pvisor/design/overlaynet.md b/docs/src/en/pvisor/design/overlaynet.md deleted file mode 100644 index 7a421c92c..000000000 --- a/docs/src/en/pvisor/design/overlaynet.md +++ /dev/null @@ -1,262 +0,0 @@ -# OverlayNet transparent interception - -This document owns interception mechanisms, enforcement gaps, and acceptance -gates. User policy procedures belong to the -[network guide](../guides/network.md); the capability model belongs to -[Capabilities and evidence](../concepts/capabilities-and-evidence.md). - -!!! note "Target architecture" - The libkrun VM driver described first is implemented. Design A, Design B, - and delivery-plan items 1–5 describe target host/container interception - and acceptance gates; they are not current public capabilities. - -## Implemented VM driver - -libkrun VM Attempts now use `vm-smoltcp` when `[overlaynet].mode = "auto"`. -The guest virtio-net device connects to pVisor over libkrun's length-prefixed -UnixStream Ethernet transport. pVisor serves DHCP (`192.0.2.1` router, -`192.0.2.2` guest), synthetic DNS (`198.18.0.0/15`, stable per Attempt), and -IPv4 TCP. A SYN remains paused in smoltcp until hostname/IP, resolved address -or scoped host connector alias, port, and injected Control policy all authorize -and the host connection succeeds. TSI stays disabled, so there is no guest -path around this data plane. - -Some host DNS/TUN connectors return an opaque `198.18.0.0/15` fake IP for an -authorized hostname. The VM connector accepts that result only after the -logical hostname and port pass policy and Control authorization; guest IP -literals in the same range remain blocked. Because the connector hides the -real final address, IP/CIDR policy cannot inspect the endpoint behind that -alias. Deployments that require final-address policy should use a resolver -that exposes concrete addresses. - -The MVP intentionally fails closed for general UDP, IPv6, ICMP, QUIC, inbound -connections, virtual/link-local/multicast/broadcast destinations, and exhausted -flow/DNS capacity. Explicit Gateway capture is an internal virtual-router route; -all ordinary egress shares the same policy and bandwidth registry. Host and -container transparent interception described below remains future work. - -> Status: the libkrun VM driver is implemented on Linux and Apple Silicon -> macOS. The host-process transparent drivers described later in this document -> remain an accepted design. Host/container selective policy still uses the -> explicit proxy; host deny-all keeps its existing platform sandbox behavior. - -## Problem - -OverlayNet's host/container data plane is an explicit HTTP/HTTPS proxy. pVisor -injects proxy environment variables and, for known Agent CLIs, proxy -configuration arguments. Coverage is therefore opt-in: any child process that -ignores proxy environment variables — a static Go binary, a raw socket, a -subprocess that scrubs its environment — talks to the network directly. This -is why the host `ProcessExecutor` cannot claim enforcement and refuses -`PolicyMode::Enforce` for network capabilities. - -The goal of this remaining host-driver design is **complete interception with -a lightweight footprint**: every byte the Agent process tree sends must pass -through a pVisor-owned choke point, regardless of language runtime, linkage, -or syscall discipline — without a VM, a root daemon, or persistent elevated -privileges. - -The key move is to relocate the interception point from *convention* -(environment variables the child may ignore) to *a layer the child cannot -choose to bypass*. - -## Design A (primary): unprivileged network namespace + in-process userspace network stack - -This is the default driver on capable Linux hosts. It mirrors the design of -pVisor's filesystem path: - -```text -filesystem: pVisor embeds a FUSE server and IS the child's filesystem -network: pVisor embeds a userspace TCP/IP stack and IS the child's network -``` - -### Mechanism - -1. The Attempt child is spawned with `CLONE_NEWUSER | CLONE_NEWNET`. Creating - a network namespace inside a fresh user namespace requires **no - privileges**; the namespace owner holds `CAP_NET_ADMIN` within it. -2. Inside the namespace, setup code creates a `tun` device, assigns a - link-local subnet, and installs a default route pointing at it. Loopback is - brought up so Run-local services keep working. -3. The `tun` file descriptor is passed back to the pVisor parent over a - `socketpair` before `exec`. From that point pVisor owns the only egress - path of the entire process tree. -4. pVisor runs a `smoltcp`-based userspace stack on the `tun` fd. Inbound - TCP flows terminate in the stack and are re-originated on the host side - after passing the `persisting-agentctl` policy gate. The existing OverlayNet - proxy / Gateway sink remains the LLM capture path, unchanged. -5. DNS: the stack answers a virtual resolver address advertised via the - namespace's `resolv.conf`. Queries are resolved host-side, giving a - domain-level policy point *before* any connection exists. - -### Properties - -- **Topologically complete.** libc interposition, static binaries, raw - syscalls, and forked grandchildren are all inside the namespace; there is no - second path out. No cooperation from the child is needed or assumed. -- **Zero privilege at runtime.** No root, no setuid helper, no daemon. The - only host prerequisite is unprivileged user namespaces - (`kernel.unprivileged_userns_clone` / distro equivalent). -- **In-process.** Consistent with the embedded FUSE decision: pVisor does not - spawn `passt`/`slirp4netns`-style helpers. - -### Policy evaluation points - -| Layer | Signal | Notes | -|---|---|---| -| DNS | queried name | virtual resolver; cheapest allowlist point | -| L4 | destination IP:port | last resort for literal-IP traffic | -| TLS | SNI from ClientHello | parsed passively, no MITM, no injected CA | -| QUIC | SNI from Initial packet, or blocked | default: refuse UDP/443 to force TCP fallback | - -### Failure and probing - -Driver availability is probed at Attempt prepare time. If user namespaces are -unavailable, behavior depends on the requested policy mode: - -- `PolicyMode::Observe`: fall back to the explicit proxy driver and record the - downgrade in the implant plan notes. -- `PolicyMode::Enforce`: fail the Run preparation. A downgrade under Enforce - must never be silent. - -## Design B (restricted fallback): seccomp user-notify + socket broker - -For hosts where unprivileged user namespaces are disabled (hardened distros, -some container runtimes), a second driver can enforce a deliberately smaller -socket surface without a namespace. It is not considered equivalent to the -netns driver unless unsupported channels are denied. - -### Mechanism - -1. The Attempt child installs a seccomp filter routing `socket`, `connect`, - `sendto`, and `sendmsg` to `SECCOMP_RET_USER_NOTIF`. `io_uring_setup`, raw - packet sockets, namespace changes, and unmediated descriptor passing are - denied while this driver claims enforcement. -2. `socket` is brokered: pVisor creates the socket, retains a duplicate of the - same open file description, and injects the child descriptor with - `SECCOMP_IOCTL_NOTIF_ADDFD`. -3. On `connect`, pVisor copies the socket address once, revalidates the - notification cookie, evaluates policy, and performs `connect` through its - retained descriptor. It then returns the real result without allowing the - child's original pointer-bearing syscall to continue. This avoids a - check-then-`CONTINUE` TOCTOU window. -4. An initial seccomp driver is TCP-only. Unconnected UDP is denied until - OverlayNet can safely copy and broker each datagram. DNS must use a - pVisor-provided resolver path; otherwise a domain allowlist cannot be - reconstructed from the destination IP observed by `connect`. - -### Properties and caveats - -- Covers static binaries and raw syscalls for the explicitly brokered socket - families. `AF_UNIX` gets a separate path policy; it is not blanket-allowed. -- No namespace, no tun, no userspace stack — but descriptor provenance, - `SCM_RIGHTS`, UDP, DNS, and `io_uring` must all be closed or mediated before - the driver is described as non-bypassable. -- Seccomp sees an IP address at `connect`, not the hostname the application - originally resolved. Domain allowlists require mediated DNS, explicit proxy - traffic, or SNI correlation; IP/CIDR policy can be enforced directly. -- Chosen per Attempt; Design A remains preferred when both are available. - -## Enforcement vs capture are separate layers - -Transparent interception provides **enforcement** (deny / allowlist) and flow -accounting. It deliberately does not decrypt: - -- Enforcement needs no MITM CA: mediated DNS names and authorized destination - addresses are sufficient for the VM MVP; a future host netns driver may add - passive SNI parsing. -- **Capture** of LLM payloads stays on the existing explicit-proxy path: - Gateway injects proxy configuration into known Agent CLIs and sees - plaintext. Under a non-bypassable driver, non-cooperating traffic cannot - leave the allowlist but is not decrypted. - -Known erosion: Encrypted ClientHello will eventually hide SNI. When that -matters, deployments choose between an opt-in MITM CA for capture-grade -visibility or falling back to DNS/IP-level enforcement. This is an industry -constraint, not specific to either driver. - -## Capability reporting - -Per-Attempt selection determines whether a non-bypassable driver is attached; -the runtime capability catalog separately advertises VM network support. -Every Run records an -`InterceptionProfile` describing driver, strength, and protocol coverage: - -- `enforce` when VM smoltcp, netns, or seccomp is active; -- `observe` when only the explicit proxy is available. - -The explicit proxy foundation already emits this profile as `cooperative` and -publishes intercepted/allowed/denied/CONNECT/HTTP/sink/failure counters. These -counters prove what reached OverlayNet; they do not estimate bypassed traffic. - -The honesty invariant is preserved: the host `ProcessExecutor` still never -claims network enforcement by itself; the claim is made by the active -OverlayNet driver, and `PolicyMode::Enforce` is satisfiable only while such a -driver is attached. - -## Configuration - -The implemented public mode selector has three values: - -```toml -[overlaynet] -mode = "auto" # auto | off | proxy -policy = "allowlist" - -[[overlaynet.rules]] -host = "api.openai.com" -ports = [443] -transports = ["tcp_tunnel"] -``` - -For a libkrun VM, `auto` selects `vm-smoltcp`; `off` leaves the VM offline and -`proxy` is rejected because it is a host/container-only cooperative driver. -For host/container runs, explicit network flags select `proxy`; the accepted -`netns` and `seccomp` host drivers remain future internal candidates rather -than exposed configuration values. `run.json` records the driver actually -attached so `pvisor status` reports the real enforcement level. - -## Non-goals - -- macOS transparent *selective* interception (Network Extension, pf-based UID - routing). Selective policy remains observe-grade; deny-all is a separate - Seatbelt-enforced boundary and is reported as such. -- An eBPF (`cgroup/connect4`) driver. Elegant, but requires CAP_BPF/root and a - setup-host deployment model; out of scope for now. -- TLS decryption by default. MITM stays an explicit opt-in, if ever. - -## Delivery plan and acceptance gates - -The VM milestone described at the start of this document is complete. The -remaining plan below applies to transparent host/container interception. - -0. **Explicit proxy foundation (implemented):** honest cooperative profile, - interception counters, strict CONNECT parsing, connect-before-200, - streaming forwarding, dynamic hop-header stripping, redirect revalidation, - no implicit loopback or Gateway-upstream egress trust, structured - host/IP/CIDR + port + transport rules, post-DNS address authorization, and - pinned authorized destinations to close policy/connector DNS races. -1. **Driver probe and selection:** extend the implemented public - `off | proxy | auto` selector with internal netns/seccomp probes; record the - selected profile before child exec. `Enforce` fails closed if no - non-bypassable profile is available. -2. **Netns TCP + DNS minimum:** spawn plumbing, tun handoff, TCP relay, - mediated resolver, DNS/IP allowlist, process-tree and namespace escape - tests. Do not claim enforcement until raw syscalls and environment-scrubbed - grandchildren are demonstrably contained. -3. **Protocol closure:** SNI policy, literal-IP behavior, UDP policy, blocked - QUIC fallback, `AF_UNIX`, raw/netlink sockets, `SCM_RIGHTS`, namespace - changes, and `io_uring` conformance cases. -4. **Restricted seccomp fallback:** broker TCP sockets first; deny uncovered - channels. Add UDP/DNS only after descriptor and datagram semantics have - dedicated tests. -5. **Operations:** persist final counters and downgrade reasons in `run.json`, - expose them through `pvisor status`, and benchmark proxy/netns/seccomp modes - separately on Python, Node, Rust, static Go, and forked grandchildren. - -## Related documents - -- [Network guide](../guides/network.md): configure and inspect policy for a Run. -- [Isolation architecture](isolation.md): compare the complete provider boundary. -- [Gateway architecture](gateway.md): model routing and capture above the network layer. -- [Security and evidence](../../system-design/security-evidence.md): interpret enforcement claims across products. diff --git a/docs/src/en/pvisor/get-started.md b/docs/src/en/pvisor/get-started.md deleted file mode 100644 index 878d532a0..000000000 --- a/docs/src/en/pvisor/get-started.md +++ /dev/null @@ -1,96 +0,0 @@ -# Run your first Agent - -This path takes you from an empty project to a reviewed change. Each step leaves -you with a useful checkpoint, so you can stop before adding more power. - -!!! tip "The pVisor loop" - - **Run → review → choose → continue.** The Agent works in a staged view; - your project changes only when you apply an Effect. - -## Before you start - -You need macOS or Linux, a project directory, and an Agent command such as -`codex`. Install the CLI and confirm both product entry points: - -```bash -pip install persisting -pvisor --help -pchronicle --help -``` - -On macOS, install macFUSE before using a staged host workspace: - -```bash -brew install --cask macfuse -``` - -See [Installation](../installation.md) for source builds, VM support, and -platform requirements. - -## 1. Run one Agent in a stage - -From the project directory, start with one explicit stage: - -```bash -pvisor run --stage ./runs/task-001 -- codex -``` - -Replace `codex` with your Agent command. The Agent edits the staged view while -the base project stays unchanged. When the command finishes, you have a Run -Bundle to inspect. - -!!! success "Checkpoint: the base is still safe" - - Check `git status` in the base project. The Agent's edits should not appear - there until you apply them. - -## 2. Review what actually happened - -Start with the summary, then inspect the staged view: - -```bash -pvisor review last -pvisor inspect last -- git status --short -``` - -Review file Effects, effective controls, network evidence, and warnings before -deciding what crosses the boundary. A successful command does not mean every -requested capability was available; the Run Bundle records the mechanisms that -actually applied. - -## 3. Apply one small, trusted change - -Apply a path first. Everything else remains staged: - -```bash -pvisor apply last --path src -pvisor review last -``` - -You can apply another dependency-closed selection later: - -```bash -pvisor apply last --include 'tests/**' --exclude 'tests/generated/**' -``` - -Finish with `pvisor apply last --all`, or discard the remaining Effects with -`pvisor drop last`. - -!!! success "Checkpoint: you control the boundary" - - The accepted batch is in the real project. The remaining batch is still - reviewable and can be applied, inspected, or dropped independently. - -## 4. Choose the next layer - -Only add the control you need for the next Run: - -- [Apply changes repeatedly and keep checkpoints](guides/review-apply.md) -- [Choose a host, OCI, or VM execution environment](guides/execution.md) -- [Control network access](guides/network.md) -- [Capture the Run as pChronicle history](guides/capture.md) -- [Replay or compare a sandbox](guides/sandbox-replay.md) - -For the complementary history workflow, continue with [Explore your first -Dataset](../pchronicle/get-started.md). diff --git a/docs/src/en/pvisor/guides/capture.md b/docs/src/en/pvisor/guides/capture.md deleted file mode 100644 index 76203660d..000000000 --- a/docs/src/en/pvisor/guides/capture.md +++ /dev/null @@ -1,53 +0,0 @@ -# Capture Agent Trajectories - -Gateway capture is a pVisor Run driver. It is started and stopped with the Run; -there is no standalone Gateway command or daemon. The -[capability and evidence model](../concepts/capabilities-and-evidence.md) -explains what capture proves and what it does not enforce. - -Install `pvisor` using the [installation guide](../../installation.md), then run a real Agent: - -```bash -export DEEPSEEK_API_KEY=sk-... - -pvisor run \ - --name deepseek \ - --gateway-mode capture \ - --gateway-route 'name="deepseek", upstream="https://api.deepseek.com/v1", api_key_env="DEEPSEEK_API_KEY"' \ - --gateway-route 'name="*", forward="deepseek"' \ - --gateway-stream-markdown \ - -- claude -``` - -pVisor starts the embedded Gateway, injects proxy/base-URL values into the -child, waits for the child, flushes capture, and stops the Gateway. Each Run -writes all metadata, trajectory, and optional filesystem state into the Run -record directory (or the explicit `--stage` directory). - -Use `--gateway-stream-markdown` for a live human-readable projection. Select -`--record-format json --record-destination ./capture` for lightweight local -JSONL, or `--record-format lance --record-destination WAREHOUSE` for the full -pChronicle sidecar path. Dataset catalog, query, -analysis, import/export, and the read-only Web UI are provided by -[`pchronicle`](../../pchronicle/get-started.md). - -### Event timestamps - -Every newly persisted `EventRecord` carries both wall-clock fields: - -- `timestamp`: an RFC3339 UTC timestamp; -- `timestamp_unix_ms`: the same observation time as Unix milliseconds. - -Gateway timestamps request events when the request is accepted and response -events when the response is captured. The final Gateway capture sink also -backfills both fields for records from older producers, while pVisor runtime -events generate the pair together. The two values must agree within one -millisecond. Use `source + seq` as the ordering key; timestamps are for -wall-clock correlation and display, not ordering. - -Clients must use an injected proxy or base URL to be observed. Direct sockets -can bypass the explicit proxy unless the selected executor provides an enforced -network boundary; inspect the Run Bundle for the effective isolation level. - -Next: [explore the captured history](../../pchronicle/get-started.md) or read the -[Gateway implementation](../design/gateway.md). diff --git a/docs/src/en/pvisor/guides/execution.md b/docs/src/en/pvisor/guides/execution.md deleted file mode 100644 index 5e4fc689d..000000000 --- a/docs/src/en/pvisor/guides/execution.md +++ /dev/null @@ -1,200 +0,0 @@ -# Run workloads with pVisor - -pVisor is an [AgentVisor](../concepts/agentvisor.md): it governs one Agent Run's -lifecycle, capabilities, effects, checkpoints, and evidence independently of -the selected execution provider. - -If this is your first Run, complete [Run your first Agent](../get-started.md) -before choosing a lower-level execution layout here. - -This guide covers the supported host and VM execution layouts. The command-line -surface deliberately keeps three independent decisions separate: - -1. `--executor` chooses where the process runs: the host kernel or a libkrun VM. -2. `--rootfs host`, `--rootfs `, or `--rootfs image=` chooses the VM's Linux root - filesystem. -3. `--overlayfs-path` chooses the absolute path visible to the Agent, while - repeated `--overlayfs-compose` layers host directories in bottom-to-top order. - -There are no workspace aliases. The canonical options are shown below. - -## Option model - -| Option | Meaning | -| --- | --- | -| `--executor host` | Run the command with the host kernel. This is the default. | -| `--executor vm` | Boot a Linux guest kernel with libkrun. | -| `--rootfs host` | Linux only: export the host `/` as the VM rootfs lower layer. Implies `vm` when `--executor` is omitted. | -| `--rootfs image=` | Use a daemonless OCI image as the VM rootfs. The default VM image is `ubuntu:latest`; pin an explicit tag or digest for reproducibility. | -| `--rootfs DIR` | Use an already prepared Linux rootfs directory. | -| `--overlayfs-path PATH` | Absolute path visible to the Agent after the overlay view is mounted. | -| `--overlayfs-compose DIR` | Host read-only layer; repeat in bottom-to-top order. The current workspace is the implicit bottom layer. | -| `--stage DIR` | Durable writable stage containing workspace changes. Use a separate stage for each concurrent run or mode. | -| `--overlayfs-commit manual` | Keep changes staged for review. `apply` writes them to the base; `drop` discards them. | -| `--overlayfs-commit apply` | Apply changes automatically after a successful run. | -| `--overlayfs-commit drop` | Discard changes automatically after the run. | - -`--rootfs host`, `--rootfs image=`, and `--rootfs ` are mutually exclusive rootfs -sources. `--rootfs host` is a semantic selection, not an alias for -`--rootfs /` or any OverlayFS option. - -Without `--overlayfs-path`, the current directory is the default workspace view; -pVisor uses a managed per-Run merged mountpoint so the lower directory is never -mounted over itself. - -## Supported layouts - -| Host platform | Executor | VM rootfs | Workspace inside the command | -| --- | --- | --- | --- | -| macOS | `host` | not applicable | `--overlayfs-path` in the staged host view | -| macOS | `vm` | OCI image or prepared Linux rootfs | `--overlayfs-path` in the guest | -| Linux | `host` | not applicable | `--overlayfs-path` in the staged host view | -| Linux | `vm --rootfs host` | Linux host `/` through virtio-fs | `--overlayfs-path` in the guest | -| Linux | `vm` | OCI image or prepared Linux rootfs | `--overlayfs-path` in the guest | - -### macOS host executor - -```bash -./target/release/pvisor run --executor host \ - --overlayfs-path /workspace \ - --overlayfs-compose /Users/reiase/workspace \ - --stage ./tmp/macos-host \ - --overlayfs-commit manual \ - -- /bin/bash -``` - -The command uses the macOS kernel and host binaries. It sees a copy-on-write -view of the base as its working directory. This mode does not provide a Linux -kernel. Host isolation is safe-best-effort by default; unsupported controls are -reported and downgraded where the platform cannot provide them. - -### macOS VM with an OCI rootfs - -```bash -./target/release/pvisor run --executor vm \ - --rootfs image=ubuntu:24.04 \ - --overlayfs-path /home/workspace \ - --overlayfs-compose /Users/reiase/workspace \ - --stage ./tmp/macos-vm \ - --overlayfs-commit manual \ - -- /bin/bash -``` - -The command path is resolved in the Linux guest, not on macOS. The OCI rootfs -is an immutable lower layer with a temporary root upper. Workspace changes are -kept in the requested durable stage. A macOS rootfs cannot be reused here: -Mach-O programs and the macOS userland do not run under the Linux guest kernel. - -### Linux host executor - -```bash -./target/release/pvisor run --executor host \ - --overlayfs-path /workspace \ - --overlayfs-compose /home/reiase/workspace \ - --stage ./tmp/linux-host \ - --overlayfs-commit manual \ - -- /bin/bash -``` - -This is the Linux equivalent of the macOS host layout. The command uses the -host kernel and host userland. - -### Linux VM with the host rootfs - -```bash -./target/release/pvisor run --executor vm \ - --rootfs host \ - --overlayfs-path /home/workspace \ - --overlayfs-compose /home/reiase/workspace \ - --stage ./tmp/linux-host-rootfs \ - --overlayfs-commit manual \ - -- /bin/bash -``` - -This is the transparent-rootfs layout: the guest uses a different Linux kernel -but reads the host's `/` as its rootfs lower layer through virtio-fs. Rootfs -writes go to a temporary VM upper and are discarded at VM exit. The separately -mounted workspace has the durable stage and can be reviewed or applied. - -This mode exposes host-rootfs contents to the guest for reading. It is intended -for same-owner local isolation, not for untrusted multi-tenant workloads. Keep -`--overlayfs-path` in this layout. Without it, the durable OverlayFS stage -would describe changes to the whole host `/`, and a later apply could target -the host rootfs. - -### Linux VM with an OCI rootfs - -```bash -./target/release/pvisor run --executor vm \ - --rootfs image=ubuntu:24.04 \ - --overlayfs-path /home/workspace \ - --overlayfs-compose /home/reiase/workspace \ - --stage ./tmp/linux-vm \ - --overlayfs-commit manual \ - -- /bin/bash -``` - -This provides a guest kernel and image-defined userland. It is more -reproducible and exposes less host data than `--rootfs host`, at the cost of -maintaining or downloading an image. - -### VM with a prepared rootfs directory - -On either supported VM platform, an unpacked Linux rootfs can replace the OCI -image: - -```bash -./target/release/pvisor run --executor vm \ - --rootfs /opt/pvisor/rootfs \ - --overlayfs-path /home/workspace \ - --overlayfs-compose /path/to/project \ - --stage ./tmp/prepared-rootfs \ - --overlayfs-commit manual \ - -- /bin/bash -``` - -The directory must contain a userland for the host CPU architecture and the -requested command. - -## Review and commit a manual stage - -After a manual run, use the emitted Run id or `last`: - -```bash -./target/release/pvisor review last -./target/release/pvisor inspect last -- git status --short -./target/release/pvisor apply last --path src -./target/release/pvisor apply last --include 'tests/**' --exclude 'tests/generated/**' -./target/release/pvisor apply last --all -# Or discard it instead: -./target/release/pvisor drop last -``` - -`apply` targets the implicit workspace unless `--target` is supplied to the apply -command. A filtered apply consumes only the selected dependency-closed batch; -the remaining changes stay staged and can be applied again or dropped. Opaque -directories and hard-link groups cannot be split unsafely. Successful batches -are recorded in `apply-ledger.json`. A stage must not contain its base or -compose layers. A stage nested inside a base is hidden from the merged view, -but keeping stages in a separate `tmp` directory is easier to operate and audit. - -## Requirements and common errors - -- Build the macOS source binary with `just pvisor`. The recipe builds release, - applies `macos-hypervisor.entitlements`, signs ad hoc, and verifies the - signature. An unsigned binary fails at `krun_start_enter`. -- Linux VM execution requires accessible `/dev/kvm`; macOS VM execution - requires Apple Silicon, HVF, and the Hypervisor entitlement. -- `--overlayfs-path` must be an absolute Agent-visible path and cannot contain - `..`; `--overlayfs-compose` entries must be readable host directories. -- Do not use a macOS path such as `/Users/...` for a Linux host. Use the actual - Linux path, such as `/home/reiase/workspace`. -- VM networking defaults to OverlayNet `auto`: pVisor supplies DHCP, synthetic - DNS, and policy-controlled IPv4 TCP through smoltcp. Use `mode = "off"` for - an offline guest. UDP, IPv6, ICMP, QUIC, and inbound connections are not part - of the current VM network surface. Gateway capture uses the virtual guest - router and does not expose host loopback directly. - -Next, use [Review and apply Agent changes](review-apply.md) for the complete -selective-apply workflow, or [Capture Agent trajectories](capture.md) to add -runtime evidence. diff --git a/docs/src/en/pvisor/guides/index.md b/docs/src/en/pvisor/guides/index.md deleted file mode 100644 index e12ab7d1c..000000000 --- a/docs/src/en/pvisor/guides/index.md +++ /dev/null @@ -1,20 +0,0 @@ -# pVisor guides - -Follow the lifecycle of one Run, then connect execution to durable history when -needed. - -!!! note "Start with the outcome" - - Each guide below is task-shaped. Begin with the first outcome you need and - keep the Run Bundle open while you work; it is the authoritative record of - which controls were actually installed. - -1. [Choose an execution environment](execution.md). -2. [Review and selectively apply filesystem effects](review-apply.md). -3. [Control network access](network.md). -4. [Capture model traffic and trajectory evidence](capture.md). -5. [Replay and continue an Agent trajectory in a fresh sandbox](sandbox-replay.md). - -Filesystem, network, capture, and execution-provider guarantees are separate. -Always inspect the Run Bundle for the mechanisms installed on the current -platform. diff --git a/docs/src/en/pvisor/guides/network.md b/docs/src/en/pvisor/guides/network.md deleted file mode 100644 index cbf23bcaf..000000000 --- a/docs/src/en/pvisor/guides/network.md +++ /dev/null @@ -1,218 +0,0 @@ -# Control network access with OverlayNet - -OverlayNet lets pVisor apply allow, deny, and bandwidth rules to network -egress. Host and container runs use its in-process HTTP proxy; libkrun VM runs -use an in-process smoltcp data plane for IPv4 TCP and DNS. Interpret these -controls with the [capability and evidence model](../concepts/capabilities-and-evidence.md). - -!!! warning "Security boundary depends on the driver" - The host/container explicit proxy is cooperative: a program can bypass it - by removing proxy variables or opening a direct socket. VM `auto` is - non-bypassable for the guest process tree because virtio-net terminates in - pVisor. The VM MVP supports IPv4 TCP plus DNS; UDP, IPv6, ICMP, QUIC, and - inbound forwarding fail closed. - -## Allow only declared destinations - -Pass one or more `--overlaynet-allow` options before the Agent command: - -```bash -pvisor run \ - --overlaynet-allow api.openai.com:443 \ - --overlaynet-allow pypi.org:443 \ - -- agent-command -``` - -The presence of an allow rule enables OverlayNet and changes the default -action to deny. In this example, intercepted traffic may reach the two listed -HTTPS destinations; other intercepted destinations are rejected. - -## Choose a driver mode - -Use `--overlaynet off|auto|proxy` as the primary OverlayNet switch: - -| Mode | Executor | Boundary | -|---|---|---| -| `off` | Any | Disable OverlayNet | -| `proxy` | Host/container | Cooperative host proxy | -| `auto` | VM (recommended) | Non-bypassable smoltcp data plane | - -If omitted, policy flags and Gateway capture infer the executor-appropriate mode. - -## Choose a policy - -The policy options configure the selected driver (or infer one when no explicit -mode is supplied): - -| Goal | Option | Behavior for other intercepted destinations | -|---|---|---| -| Allow only selected targets | `--overlaynet-allow TARGET` | Denied | -| Block selected targets | `--overlaynet-deny TARGET` | Allowed | -| Block all intercepted egress | `--overlaynet-deny-all` | Denied | -| Limit bandwidth | `--overlaynet-limit [TARGET=]RATE` | Unchanged | - -Allow, deny, and limit options are repeatable. Explicit deny rules take -precedence over allow rules. `--overlaynet-deny-all` is a standalone policy and -cannot be combined with the other policy flags. - -Targets accept an exact hostname, wildcard suffix, IP address, or CIDR, with -an optional port: - -```bash -pvisor run \ - --overlaynet-allow '*.example.com:443' \ - --overlaynet-allow 203.0.113.10:443 \ - --overlaynet-deny 169.254.0.0/16 \ - -- agent-command -``` - -### Deny all intercepted traffic - -```bash -pvisor run --overlaynet-deny-all -- agent-command -``` - -For host/container runs, this denies HTTP and HTTPS requests that reach the -injected proxy; it does not disable direct sockets or local Gateway routes. In -VM `auto` mode, the same policy denies ordinary guest TCP egress while the -internal Gateway route remains available when capture is enabled. - -`--overlaynet-deny-all` does not support allow exceptions. If the intended -policy is “deny by default and allow only a few destinations,” do not start -with deny-all; declare the allowed targets directly: - -```bash -pvisor run \ - --overlaynet-allow api.openai.com:443 \ - --overlaynet-allow pypi.org:443 \ - -- agent-command -``` - -The presence of `--overlaynet-allow` automatically selects the allowlist -policy: matching destinations are allowed and all other intercepted -destinations are denied by default. - -### Limit bandwidth - -Apply a global limit and a stricter target-specific limit: - -```bash -pvisor run \ - --overlaynet-limit 10mbps \ - --overlaynet-limit api.openai.com:443=2mbps \ - -- agent-command -``` - -Matching limits stack, and the strictest effective rate applies. Rates ending -in `kbps`, `mbps`, or `gbps` are bits per second; `kb/s`, `mb/s`, and `gb/s` -are bytes per second. A limit constrains traffic but does not grant access. - -## Use structured rules - -Use a TOML configuration when a rule needs multiple ports, transport matching, -or intentional access to a private address resolved from a hostname: - -```toml -[run] -command = ["agent-command"] - -[overlaynet] -mode = "auto" # VM: smoltcp; use "proxy" for host/container -policy = "allowlist" - -[[overlaynet.rules]] -host = "api.example.com" -ports = [443] -transports = ["tcp_tunnel"] -allow_private_ips = false - -[[overlaynet.deny]] -host = "169.254.0.0/16" - -[[overlaynet.limits]] -host = "api.example.com" -port = 443 -bytes_per_second = 250000 -``` - -Run it with: - -```bash -pvisor run --spec run.toml -``` - -Supported transport values are `http`, `https`, and `tcp_tunnel`. Empty -`ports` or `transports` mean unrestricted for that dimension. - -Hostname rules reject private and loopback DNS results by default. For an -intentional private service, prefer an explicit IP/CIDR rule; alternatively, -set `allow_private_ips = true` on a narrowly scoped hostname rule. Link-local -and other special-purpose ranges still require an explicit IP or CIDR rule. - -If the host uses a DNS/TUN fake-IP connector, VM egress accepts a `198.18/15` -result as an opaque connector alias only after the logical hostname and port -are authorized. A guest cannot connect to that range as an IP literal. The -connector does not expose the real final address, so use a concrete-address -resolver when IP/CIDR policy for hostname results is required. - -## Understand which clients are controlled - -For host and container runs, pVisor injects `HTTP_PROXY`, `HTTPS_PROXY`, their -lowercase forms, and `ALL_PROXY` into the Agent process. HTTP clients that -honor these settings are routed through OverlayNet. The proxy handles ordinary -HTTP forwarding and HTTPS `CONNECT` tunnels. - -The following paths are outside that cooperative host/container boundary: - -- a client that ignores or removes the proxy environment; -- a destination added to `NO_PROXY`; -- a program that opens a direct socket; -- DNS and UDP traffic that does not pass through the HTTP proxy. - -Consequently, a host/container cooperative-proxy Run reports -`safety.network_non_bypassable = false`. When direct network access must be -blocked, use `pvisor -- --overlaynet-deny-all`: Linux adds a private -network namespace; macOS blocks non-loopback IP and ambient host Unix sockets with Seatbelt, -while retaining loopback proxy access and the exact AgentCtl and Run-local IPC. Container Runs can instead -use `--container-network none`. Selective allow/deny rules remain cooperative -on both native host paths. The VM executor defaults to `[overlaynet] mode = -"auto"`, which supplies DHCP, synthetic DNS, and policy-controlled IPv4 TCP; -`mode = "off"` leaves it offline. Gateway capture uses the guest virtual -router. The container executor still requires `--container-network host` for -the in-process proxy. - -## Review the result - -The current directory is the default reusable workspace. Each invocation keeps -an independent Run under pVisor's default records root: - -```bash -pvisor run \ - --overlaynet-deny 169.254.0.0/16 \ - -- agent-command - -pvisor review --json last | jq '{policy: .network.policy, - interception: .network.interception, - counters: .network.intercepted, - non_bypassable: .safety.network_non_bypassable}' -``` - -The counters describe traffic handled by the active OverlayNet driver. They -cannot count traffic that bypassed the cooperative host/container proxy; the -VM smoltcp profile has no guest network path around its supported TCP/DNS data -plane. - -## Troubleshooting - -| Symptom | Check | -|---|---| -| An allowed hostname resolves to loopback or a private address | Use an explicit IP/CIDR rule, or a narrowly scoped structured rule with `allow_private_ips = true` | -| A request succeeds under `--overlaynet-deny-all` | Confirm the client honors the injected proxy and does not use `NO_PROXY` or a direct socket | -| pVisor cannot bind the proxy | Select a free non-zero address with `--overlaynet-listen 127.0.0.1:19082` | -| A container cannot reach the proxy | Use `--container-network host` | -| VM `proxy` mode is rejected | Use `auto` for the smoltcp driver, or `off` for an offline guest | - -For an offline runnable walkthrough, use -[`examples/pvisor/03-network-isolation`](https://github.com/DeepLink-org/Persisting/tree/main/examples/pvisor/03-network-isolation). -For LLM request capture and model routing, continue with the -[Capture guide](capture.md). diff --git a/docs/src/en/pvisor/guides/review-apply.md b/docs/src/en/pvisor/guides/review-apply.md deleted file mode 100644 index 77ee94a58..000000000 --- a/docs/src/en/pvisor/guides/review-apply.md +++ /dev/null @@ -1,102 +0,0 @@ -# Review and apply Agent changes - -An Agent can work freely inside a staged workspace without receiving automatic -permission to modify the base project. After the Run, you decide which effects -cross that boundary. This workflow applies the filesystem -[Effect model](../concepts/run-model.md). - -!!! note "Before you start" - - You need a project directory and a command that can run your Agent. If you are - starting from scratch, complete [Run your first Agent](../get-started.md) first. - The workflow below assumes you want manual control over when staged changes - enter the base project. - -## Run with a manual stage - -The short form is: - -```bash -pvisor -- codex -``` - -For explicit paths and commit behavior: - -```bash -pvisor run \ - --overlayfs-compose "$PWD" \ - --stage /tmp/my-agent-stage \ - --overlayfs-commit manual \ - -- codex -``` - -Use a separate stage for every concurrent Run. - -## Review before accepting - -```bash -pvisor review last -pvisor inspect last -- git status --short -``` - -`review` summarizes the Run Bundle, file effects, network evidence, and safety -warnings. `inspect` runs a read-only inspection command against the staged view. - -## Apply only selected files - -Selections can be path-based or pattern-based: - -```bash -pvisor apply last --path src -pvisor apply last --include 'tests/**' --exclude 'tests/generated/**' -``` - -A filtered apply consumes only the selected dependency-closed batch. Opaque -directories and hard-link groups are not split when doing so would produce an -invalid result. - -## Apply more than once - -Applying a subset does not close the stage. Review what remains, then apply -another subset: - -```bash -pvisor review last -pvisor apply last --path docs -pvisor review last -pvisor apply last --all -``` - -Successful batches are recorded in `apply-ledger.json`. You can stop at any -point and discard the remaining changes: - -```bash -pvisor drop last -``` - -!!! tip "Verify the result" - - After each batch, compare the staged view with the base project. A successful - `apply` means that the selected dependency-closed batch crossed the boundary; - it does not mean that every remaining Effect was applied. - -## Continue from an accepted point - -Use a checkpoint before starting another line of work: - -```bash -pvisor checkpoint last --name accepted-base -pvisor fork last --checkpoint accepted-base -- codex -``` - -The new Run receives its own identity and stage while preserving lineage to the -checkpoint. - -## What this boundary does not cover - -Selective apply governs staged filesystem effects. It does not undo a message, -payment, deployment, direct database write, or external API mutation. Those -effects require admission before execution, provider-specific containment when -available, and durable evidence afterward. - -Next: [control network access](network.md) or [capture the Run](capture.md). diff --git a/docs/src/en/pvisor/guides/sandbox-replay.md b/docs/src/en/pvisor/guides/sandbox-replay.md deleted file mode 100644 index a34ad716d..000000000 --- a/docs/src/en/pvisor/guides/sandbox-replay.md +++ /dev/null @@ -1,204 +0,0 @@ -# SandboxReplay - -SandboxReplay is pVisor's Agent-trajectory replay capability. It normally runs -inside a fresh sandbox created by the caller, re-executes the complete tool -batches before a selected boundary, rebuilds the Agent-native context with the -fresh observations, and then continues the Agent directly after that boundary. -It creates a derived Run in the -[Run and Attempt model](../concepts/run-model.md). - -## Replay boundary - -A trajectory can be written as: - -```text -task -> A1 -> O1 -> ... -> AN -> ON -> A(N+1) -> ... -``` - -SandboxReplay executes `A1...AN` in the fresh sandbox to produce -`O'1...O'N`, retains the Agent-native system prompt, tools, task, and actions, -and issues the first continued model request immediately after `O'N`. No -"continue" message is appended. This preserves the replay boundary but does -not guarantee that `A'(N+1)` is byte-identical to `A(N+1)`. - -An explicit boundary user prompt can instead make the first live request: - -```text -system + tools + task + A1 -> O'1 -> ... -> AN -> O'N - + boundary_user_prompt -> A'(N+1) -``` - -Use `--boundary-user-prompt TEXT` only when this changed input is intentional. -SandboxReplay injects it once, after `O'N` and before the first live model -inference. It never replaces the original task and is not injected in -prepare-only or replay-only mode. Without the option, the existing exact -boundary behavior is unchanged. - -## Supported Agent profiles - -Replay profiles are version-pinned and fail closed when the installed runtime -does not match. The Pi profile supports `@earendil-works/pi-coding-agent` -`0.83.0` and consumes the native RPC event JSONL produced by Pi. A replay step -is one complete `turn_end` tool batch. The profile reconstructs a fresh Pi v3 -session with new observations, then continues through Pi's SDK. Its initial -tool surface is intentionally limited to Pi's `read`, `bash`, `edit`, and -`write` tools; trajectories containing another tool fail validation. - -OpenCode `1.17.7` and Codex CLI `0.149.0` are also supported. OpenCode consumes -the native `opencode run --format=json` event stream and groups -`step_start`/`tool_use`/`step_finish` events into complete replay steps. Codex -consumes rollout JSONL (`session_meta` plus `response_item` messages, reasoning, -function calls/custom tool calls, and outputs). Both adapters re-execute the -selected command/file tools in the fresh workspace, replace native observations, -write a native JSONL prefix, and then invoke the native continuation command. -OpenCode uses `run --format=json --session`; Codex stages the prefix below an -isolated `CODEX_HOME` and uses `exec resume --json`. Unknown tool shapes fail -explicitly instead of being silently skipped. - -## Run replay - -```bash -pvisor replay \ - --agent claude-code \ - --trajectory /input/session.jsonl \ - --after-step 30 \ - --agent-entrypoint /usr/bin/claude \ - --boundary-user-prompt 'Review the fresh observation before continuing.' -``` - -The equivalent TOML is: - -```toml -[replay] -agent = "claude-code" -trajectory = "/input/session.jsonl" -after_step = 30 -agent_entrypoint = "/usr/bin/claude" -max_steps = 200 -session_id = "task-291-attempt-1" -replay_only = false -disable_thinking = true -boundary_user_prompt = "Review the fresh observation before continuing." -``` - -When the Pi runtime is installed at `/opt/pi-agent`, the CLI form is: - -```bash -pvisor replay \ - --agent pi-agent \ - --trajectory /input/pi-agent.events.jsonl \ - --after-step 30 \ - --agent-entrypoint /opt/pi-agent/bin/pi -``` - -OpenCode consumes its native event stream: - -```bash -pvisor replay \ - --agent opencode \ - --trajectory /input/opencode.jsonl \ - --after-step 30 \ - --agent-entrypoint /usr/bin/opencode -``` - -Codex consumes its native rollout JSONL. SandboxReplay derives the Codex native -session ID from the trajectory's `session_meta`; the request `session_id` remains -a model-router/run key and cannot override the native Codex identity. If the -trajectory has no native session ID, continuation fails closed instead of -starting a fresh conversation: - -```bash -pvisor replay \ - --agent codex \ - --trajectory /input/rollout.jsonl \ - --after-step 30 \ - --agent-entrypoint /usr/bin/codex -``` - -For Codex CLI versions that require a prompt on `exec resume`, SandboxReplay -generates a per-run transport nonce and starts a loopback-only Responses bridge. -The CLI receives the nonce, but the bridge removes it before every upstream -request (Codex may resend the full history on later requests) and fails closed -on malformed or ambiguous requests. The continued native JSONL is also scrubbed -of the nonce and the legacy `Continue from the replay boundary.` message. The -default metadata records `prompt_mode = "transport_nonce"` and -`input_condition = "replayed_boundary_only"`. With an explicit -`boundary_user_prompt`, the user message is retained and metadata records -`prompt_mode = "explicit_user_prompt"` and -`input_condition = "boundary_user_prompt_appended"`. - -The same TOML surface is used for both; change `[replay].agent`, -`trajectory`, and `agent_entrypoint` to the selected runtime. - -### Execution modes and results - -- The default mode executes the selected prefix and continues with the live Agent. -- `--replay-only` executes the prefix and stops before the next model request. -- `--prepare-only` only validates and constructs the prefix. It executes no tools, - starts no Agent, and does not require an Agent runtime. - -`--max-steps` counts all Agent actions, including the selected prefix. For -example, `--after-step 30 --max-steps 50` leaves at most 20 live actions. A -replay-only budget must cover the prefix; a continuation budget must leave at -least one live action. - -Results use `sandbox-playback.result/v3`. The `phase` is `prepared`, `replayed`, -or `continued`; `quality` is `verified` or `degraded`; and `agent_status` -distinguishes `not_started`, `completed`, `max_steps`, and `failed`. Failures -retain available logs and native trajectories. OpenHands controller fatal states -are failures even when its process exits with status zero. - -Successful result metadata records whether the boundary prompt was requested -and injected, plus its character length and SHA-256 digest; replay journals do -not store the prompt text. Agent-native prepared or continued trajectories may -contain the user message. For Claude Code, the in-memory bridge adds the message -only to the first cleaned upstream request and leaves the reconstructed native -session unchanged. When it is injected, -`next-action-comparison.json` uses the input condition -`boundary_user_prompt_appended`. Its similarity and tool metrics remain -descriptive and must not be interpreted as same-input replay fidelity. - -Migration: older non-Claude configurations sometimes used `replay_only = true` -to construct a prefix without executing it. Use `prepare_only = true` for that -behavior. In v3, replay-only always executes the selected prefix and therefore -requires an exact-version runtime. Claude observations that cannot be reproduced -fresh fail by default; `--allow-stale-observations` explicitly permits a -`degraded` result. - -Runtime isolation is opt-in. Replay only creates an outer managed `pvisor run` -when the caller supplies runtime options such as `--executor`, `--stage`, or -`--overlayfs-path`/`--overlayfs-compose`, or their TOML equivalents. See the -[`pvisor replay` reference](../reference/cli.md#replay-an-agent-trajectory) for -the complete surface. - -## Qwen3.6 evaluation - -The evaluation used Qwen3.6-35B-A3B with thinking disabled. Each original run -and continuation used a newly created sandbox with 2 CPUs, 7 GiB memory, and -70 GiB storage. Step counts are native tool batches recognized by the Rust -parser. Text similarity compares normalized visible text and excludes -reasoning. Exact tools require equal counts, order, names, and JSON arguments. - -| Agent | Task | N | Original steps | Continued steps | Original Reward | Continued Reward | Exact A'(N+1) tools | Text similarity | -|---|---|---:|---:|---:|---:|---:|---|---:| -| Claude Code | NodeBB (291) | 1 | 69 | 101 | 1 | 1 | yes | 0.85 | -| Claude Code | Vuls (666) | 28 | 76 | 200 | 0 | 0 | no | 0.45 | -| Claude Code | qutebrowser (667) | 36 | 46 | 46 | 1 | 1 | no | 0.48 | -| OpenHands | NodeBB (291) | 28 | 62 | 68 | 1 | 0 | yes | 1.00 | -| OpenHands | Vuls (666) | 25 | 43 | 43 | 1 | 1 | yes | 1.00 | -| OpenHands | qutebrowser (667) | 17 | 36 | 87 | 1 | 1 | no | 0.13 | -| mini-swe-agent | NodeBB (291) | 48 | 104 | 115 | 1 | 1 | no | 1.00 | -| mini-swe-agent | Vuls (666) | 39 | 94 | 91 | 1 | 1 | yes | 1.00 | -| mini-swe-agent | qutebrowser (667) | 31 | 115 | 65 | 1 | 1 | no | 0.77 | - -## Boundary-response examples - -The Chinese localization of this page contains all nine paired `A(N+1)` and -`A'(N+1)` responses, including visible text, tool names, and arguments. Use the -language switcher to open that detailed report. Reasoning is removed, and long -file replacements are reduced to their distinguishing targets. - -For exact options, use the -[`pvisor replay` reference](../reference/cli.md#replay-an-agent-trajectory); -for execution boundaries, continue to the -[execution guide](execution.md). diff --git a/docs/src/en/pvisor/guides/troubleshooting.md b/docs/src/en/pvisor/guides/troubleshooting.md deleted file mode 100644 index 39d0e499c..000000000 --- a/docs/src/en/pvisor/guides/troubleshooting.md +++ /dev/null @@ -1,81 +0,0 @@ -# Troubleshoot a Run - -When a Run does not behave as expected, start with the Run Bundle instead of -repeating the command with stronger flags. The Bundle records the boundary that -was requested, the mechanisms that were installed, and the warnings that limit -what the Run can claim. - -## Start with three read-only checks - -Run these from the project that started the Agent: - -```bash -pvisor status last -pvisor inspect last -- git status --short -pvisor review last -``` - -`status` tells you whether the Run is still active or stopped. `inspect` runs a -read-only command in the Run view, so it helps distinguish a staged change from -a change already present in the base project. `review` shows the durable Run -Bundle and its Evidence before any apply or drop decision. - -## The Agent changed the project directly - -Check whether the command used a stage: - -```bash -pvisor run --stage ./runs/task-001 -- AGENT_COMMAND -``` - -Without a stage, the host executor may still provide safe-best-effort controls, -but there is no staged filesystem Effect to review and selectively apply. If a -staged Run was expected, inspect the recorded stage path and the executor -warnings before rerunning it. - -## A requested capability was not enforced - -Treat a requested option as intent, not evidence. Open the Run Bundle and look -for the effective capability record and its mechanism. Provider support varies -by operating system and executor; a cooperative network proxy, for example, -does not make every ambient connection impossible. - -Continue with [Capabilities and evidence](../concepts/capabilities-and-evidence.md) -and [Execution environments](execution.md) before changing the command. - -## The stage is empty or the wrong files appear - -Check the command's working directory and stage path first: - -```bash -pvisor inspect last -- pwd -pvisor inspect last -- git status --short -``` - -The Agent edits the Run-owned view. A command that writes outside that view may -be reported as an external Effect or may be unavailable to the executor. Keep -the stage path inside the intended project boundary and avoid comparing a -generated Run directory with the project root by filename alone. - -## Capture or Dataset output is missing - -Execution review and trajectory capture are separate decisions. First confirm -that the Run completed and its Bundle is readable. Then check the capture -configuration and the destination Dataset with pChronicle: - -```bash -pchronicle ls ./trajectory-data -pchronicle analysis overview ./trajectory-data -``` - -If the Dataset is not present, read [Capture Agent trajectories](capture.md) -and verify the configured destination before starting another Run. A local Run -Bundle is not automatically a pChronicle Dataset. - -## Before opening an issue - -Include the pVisor version, operating system, executor, the exact command, and -the relevant `status` and `review` output. Remove credentials and private -workspace contents. The most useful report explains which capability was -requested, which mechanism the Bundle records, and where the observed result -differs. diff --git a/docs/src/en/pvisor/index.md b/docs/src/en/pvisor/index.md deleted file mode 100644 index f3190fbb1..000000000 --- a/docs/src/en/pvisor/index.md +++ /dev/null @@ -1,78 +0,0 @@ -# pVisor - -pVisor logo - -**pVisor runs an existing Agent command inside a controlled execution -environment.** It gives each Run its own workspace boundary, records the -controls that were actually installed, and lets you review filesystem changes -before they reach the project. - -In Persisting, pVisor runs one Agent and reviews its changes. You can use it -without pChronicle. - -!!! tip "What you will complete" - - By the end of the first walkthrough: the Agent has stopped; its changes remain - in a Run-owned staging directory; you deliberately write them into the project - or discard them. Your first `pvisor review` shows the record of controls that - were actually installed. - -pVisor does not replace the Agent's reasoning loop. You can keep using Agent -CLIs, scripts, and frameworks you already have. - -## Run, review, decide - -From a project directory: - -```bash -pvisor run --stage ./runs/task-001 -- codex -pvisor review last -pvisor apply last --path src -``` - -With `--stage ./runs/task-001`, the Agent writes to a staged view of the project. After the Run, -you can apply all changes, accept selected paths in several batches, or discard -the stage: - -```bash -pvisor apply last --all -# or -pvisor drop last -``` - -The exact filesystem and network boundary depends on the platform and chosen -executor. pVisor records the effective controls so that a Run is not described -as more isolated than it was. - -## Choose your next step - -Start with [Run your first Agent](get-started.md) if this is your first -session. It ends with a reviewed stage and gives you the vocabulary used by -the rest of the documentation. - -When you already know what you need, follow the matching path: - -- **Keep or discard changes:** [Review and apply](guides/review-apply.md) -- **Compare host, container, and VM:** [Execution layouts](guides/execution.md) -- **Constrain network access:** [Network policy](guides/network.md) -- **Publish trajectory events:** [Capture trajectories](guides/capture.md) -- **Look up exact flags:** [CLI reference](reference/cli.md) - -pVisor's local run-review-apply loop works on its own. pChronicle is optional: -use it when you want to retain and query trajectory Datasets after a Run. - -## A useful reading order - -1. [Run your first Agent](get-started.md) to see the complete success loop. -2. [Review and apply](guides/review-apply.md) when you need finer control over changes. -3. [Execution layouts](guides/execution.md) when provider boundaries affect your decision. -4. [Capabilities and evidence](concepts/capabilities-and-evidence.md) when you need to interpret a Run Bundle. -5. [CLI reference](reference/cli.md) only when you need an exact flag or output field. - -## Keep reading - -- [Run your first Agent](get-started.md) -- [Learn the pVisor concepts](concepts/index.md) -- [Follow practical guides](guides/index.md) -- [Inspect runtime and isolation design](design/index.md) -- [Explore trajectory history with pChronicle](../pchronicle/index.md) diff --git a/docs/src/en/pvisor/reference/cases.md b/docs/src/en/pvisor/reference/cases.md deleted file mode 100644 index 4fd5e30bd..000000000 --- a/docs/src/en/pvisor/reference/cases.md +++ /dev/null @@ -1,53 +0,0 @@ -# pVisor case catalog - -The case catalog is the executable index for `pvisor run`. It starts with a -minimal host Run and progresses through staged changes, provider selection, -network policy, Gateway capture, and prepared RunSpecs. - -Use the catalog when you need a reproducible command or a regression target. -Each case documents its prerequisites, command, expected result, and machine -check. The checks are folded into the page so the same examples can be read by -people and executed by `scripts/run-pvisor-cases.py`. - -## Choose a case group - -| Goal | Cases | -| --- | --- | -| First command and Run identity | A01–A03 | -| Limits and runtime controls | A04, B01–B04 | -| Review, apply, and drop changes | C01–C05 | -| Host and VM execution | D01–E06 | -| Native OCI containers | F01–F04 | -| Network and Gateway behavior | G01–H03 | -| RunConfig and RunSpec inputs | I01–I03 | -| Complete combinations | J01–J03 | - -## Run the catalog - -Prepare a workspace and list the available cases: - -```bash -mkdir -p /tmp/pvisor-cases/workspace -cd /tmp/pvisor-cases/workspace -python3 scripts/run-pvisor-cases.py --list -``` - -Run selected cases or produce a report: - -```bash -python3 scripts/run-pvisor-cases.py \ - --pvisor target/release/pvisor \ - --case A01,C01 \ - --keep -python3 scripts/run-pvisor-cases.py \ - --pvisor target/release/pvisor \ - --report target/pvisor-case-report.md -``` - -The runner creates an isolated workspace per case. Replace placeholder paths -and images with local resources when a case requires a Linux rootfs, OCI runtime, -VM image, or Agent executable. Missing prerequisites are reported as failures -so that an environment problem cannot look like a passing case. - -The complete command-by-command catalog is maintained in the localized version -of this page; the executable assertions below each example are language-neutral. diff --git a/docs/src/en/pvisor/reference/cli.md b/docs/src/en/pvisor/reference/cli.md deleted file mode 100644 index 96369464e..000000000 --- a/docs/src/en/pvisor/reference/cli.md +++ /dev/null @@ -1,453 +0,0 @@ -# `pvisor` command reference - -`pvisor` is the product command for a single Run and for durable -environments. -Full command examples for Host, OCI VM, and transparent host-rootfs VM -are in -[Run workloads with pVisor](../guides/execution.md). - -## Find the command you need - -Use the smallest surface that matches your next decision: - -- **Run an Agent:** start with [`pvisor run`](../get-started.md), then use - `review`, `inspect`, and `apply` to decide what reaches the project. -- **Understand a boundary:** use `status` and `inspect`, then read the - [execution guide](../guides/execution.md) before changing providers. -- **Keep a workspace:** use `env create` and `env exec` for a reusable staged - environment; use `env apply` or `env drop` to close the loop. -- **Continue a trajectory:** use `replay` only when you already have a - supported trajectory and want a fresh sandbox; begin with the - [replay guide](../guides/sandbox-replay.md). - -If this is your first command, do not start with the full option list below: - -```bash -pvisor run --stage ./runs/task-001 -- codex -pvisor review last -pvisor apply last --path src -``` - -The reference that follows is organized by lifecycle. Options that affect the -same Run are intentionally described together so that a copied command has a -clear verification step. - -```text -pvisor -├── run execute one Agent Run -├── replay replay and continue an Agent-native trajectory -├── env manage durable reusable environments -├── status aggregate Run, filesystem, and network state -├── inspect open a read-only Run view -├── review review the durable Run Bundle -├── checkpoint snapshot a stopped transactional upper -├── fork start a child Run from a logical checkpoint -├── apply commit a stopped Run's filesystem stage -└── drop discard a stopped Run's filesystem stage -``` - -## Safe first run - -```bash -pvisor -- codex -pvisor review last -``` - -Host execution uses safe-best-effort isolation by default. `--stage ` opts -into an OverlayFS stage for the current workspace, creates an independent Run -and writable stage at the supplied path (or in the generated Run record -directory when no explicit stage is supplied), -retains changes for manual review, and writes `run-bundle.json` with mode `0600`. - -`--strict` fails closed before Agent start unless every requested capability -dimension has non-bypassable enforcement evidence. Today host, container, and -VM executors all request Network and Subprocess enforcement, and none claim -Subprocess — so `--strict` currently exits with `UnsupportedPolicy` on those -paths. Use it to verify fail-closed behavior, not as a “stronger sandbox is -ready” switch. -On Linux, the default host executor self-executes through pVisor's rootless -launcher before the async runtime reaches the Agent. User/mount/PID namespaces, -an in-namespace PID 1 descendant reaper, -minimal bind-projected root plus `chroot`, a kernel-negotiated Landlock ABI v1-v3 policy, closed -inherited descriptors, `no_new_privs`, and an empty capability set make -workspace containment non-bypassable for the Agent process tree. -`--overlaynet-deny-all` adds a private network namespace; the -public/allowlist proxy modes remain cooperative. On macOS the default safe -host executor installs a generated Seatbelt policy that makes staged writes -non-bypassable. For deny-all Runs it blocks IP and ambient host Unix sockets, -while retaining the exact AgentCtl and Run-local IPC. Reads and selective -network policy remain ambient/cooperative and are labeled separately in the -Bundle. Docker and KVM transports retain the same outer Run, OverlayFS, -AgentCtl state observation, and pChronicle control plane. - -After completion: - -```bash -pvisor review last -pvisor checkpoint last --name before-experiment -pvisor fork last --checkpoint before-experiment -- codex -pvisor apply last --all # or: pvisor drop last -``` - -The CLI checkpoint is stopped-consistent. Embedded hosts can call -`RunHandle::checkpoint`: pVisor publishes an AgentCtl quiesce directive, -requires every Session frozen into the checkpoint to report the matching -quiesced state, snapshots the raw upper, then publishes `continue`. Logical -checkpoints preserve filesystem and cooperative client safe-point boundaries, -not process memory. - -A durable environment has a stable name and a reusable OverlayFS upper: - -```bash -pvisor env create dev --target ./project -pvisor env exec dev -- make test -pvisor env shell dev -pvisor env inspect dev -- git status --short -pvisor env stop dev -pvisor env start dev -pvisor env apply dev --path src # commit the selection; the rest stays staged -pvisor env apply dev --all # commit remaining changes and reset to an empty stage -pvisor env drop dev # discard changes and reset to an empty stage -pvisor env delete dev --force -``` - -Default metadata lives in `~/.persisting/envs` and can be overridden -with `--root` or `PERSISTING_ENV_HOME`. `start` / `stop` control whether -new sessions are accepted; they do not mean a resident VM. Each -`exec` / `shell` mounts the same writable upper, so changes persist -across commands. `inspect` uses a kernel-enforced read-only view. -`apply --all` or `drop` do not flip a terminal Overlay back to `staged` -in place; they create a monotonically increasing Overlay generation. -After a command takes the environment lease it re-reads the generation -so metadata from before the reset cannot overwrite the new stage. - -## Replay an Agent trajectory - -`pvisor replay` assumes the caller has normally created a fresh sandbox. It -replays complete tool batches through `after_step`, rebuilds the selected -Agent native context with fresh observations, and then starts the live Agent: - -```bash -pvisor replay \ - --agent claude-code \ - --trajectory /input/session.jsonl \ - --after-step 30 \ - --agent-entrypoint /usr/bin/claude \ - --boundary-user-prompt 'Review the fresh observation before continuing.' -``` - -OpenHands, mini-swe-agent, Pi agent, OpenCode, Codex, and SWE-agent use the model endpoint and -credentials already present in their environment. Pi requires its exact -`0.83.0` runtime and accepts native RPC event JSONL containing the core -`read`, `bash`, `edit`, and `write` tools. Claude Code uses a temporary bridge owned -by SandboxReplay because its native resume transport inserts wake-up messages. -The bridge validates and removes that exact Resume Transport envelope before -forwarding the model request. It does not enable pVisor Gateway, capture model -traffic, or persist a bridge audit. - -OpenCode requires the exact `1.17.7` runtime and its native -`opencode run --format=json` event JSONL. Codex requires the exact `0.149.0` -runtime and Codex rollout `response_item` JSONL. Both rebuild the native prefix -in the fresh sandbox and invoke their native resume command for continuation. -Codex derives its native session ID from the trajectory's `session_meta`; the -request `session_id` is only a model-router/Run key and cannot override it. -Continuation fails closed when the native session is missing. - -The equivalent strict replay TOML is: - -```toml -[replay] -agent = "claude-code" -trajectory = "/input/session.jsonl" -after_step = 30 -agent_entrypoint = "/usr/bin/claude" -max_steps = 200 -session_id = "task-291-attempt-1" -replay_only = false -disable_thinking = true -boundary_user_prompt = "Review the fresh observation before continuing." -``` - -Pi uses the same CLI/TOML surface. When the runtime is installed at -`/opt/pi-agent`, for example: - -```bash -pvisor replay --agent pi-agent \ - --trajectory /input/pi-agent.events.jsonl \ - --after-step 30 \ - --agent-entrypoint /opt/pi-agent/bin/pi -``` - -Replay has three modes. The default replays the prefix and continues; -`--replay-only` executes the prefix and stops before a model request; and -`--prepare-only` constructs the prefix without executing tools or requiring a -runtime. `--max-steps` is the total action budget, including replayed actions. -`--allow-stale-observations` is an explicit Claude-only escape hatch that marks -the v3 result `degraded`. - -`--boundary-user-prompt TEXT` appends one user message after the final fresh -observation and before the first live model inference. The TOML spelling is -`replay.boundary_user_prompt`. It is ignored for inference in prepare-only and -replay-only modes, and an omitted option preserves the unmodified replay -boundary. Structured results and replay journals store only injection state, -length, and a digest; Agent-native prepared or continued trajectories may -contain the user message. - -The result schema is `sandbox-playback.result/v3`, with typed `phase`, `quality`, -and `agent_status` fields plus state/output locations, artifacts, and an optional -structured failure. Existing non-Claude callers that used `replay_only = true` -only to construct a prefix must migrate to `prepare_only = true`. - -`disable_thinking` belongs to `[replay]` and is also exposed as -`--disable-thinking`. Claude Code's protocol bridge applies it to the upstream -request; OpenCode omits its `--thinking` flag when it is set. It does not turn -on Gateway capture. Optional `[run]`, `[overlayfs]`, and `[overlaynet]` sections -create an outer managed `pvisor run`; they do not change the inner replay model -path. - -By default, replay's internal state, WAL, manifest, fresh-observation -comparisons, and native working files remain under -`/tmp/pvisor-sandbox-replay` and disappear with the sandbox. Replay does not -enable pVisor Gateway, pChronicle, a model-traffic capture store, or a Claude -Resume Transport audit. A caller that explicitly selects `--state-dir` or -`--output-dir` owns those files. Use `--replay-only` to execute the prefix and -stop before live inference, or `--prepare-only` to construct it without execution. - -## One configuration model - -`pvisor run` has one canonical `RunConfig`. TOML and command-line options are -two representations of the same fields. `--spec` is optional and explicit; a -file beginning with a JSON object is treated as a prepared RunSpec, otherwise -it is read as TOML RunConfig. pVisor does not discover a hidden project file. - -```bash -pvisor run \ - --name codex \ - --overlayfs-path /workspace \ - --overlayfs-compose /path/to/project \ - --overlayfs-backend directory \ - --overlayfs-commit manual \ - --overlaynet-allow api.openai.com:443 \ - --overlaynet-deny 169.254.0.0/16 \ - --overlaynet-limit 10mbps \ - --gateway-mode capture \ - --gateway-level dialogue \ - --gateway-route \ - 'name="openai", provider="openai", upstream="https://api.openai.com/v1", api_key_env="OPENAI_API_KEY"' \ - --record-format lance \ - --record-destination ./warehouse \ - -- codex -``` - -`--record-format lance` starts `pchronicle serve --control 127.0.0.1:0 DATASET`; -pVisor sends shared `EventRecord` values and waits for durable -acknowledgements. Use `--record-format json` for local JSONL or a JSON warehouse -archive. - -All newly persisted records contain both `timestamp` (RFC3339 UTC) and -`timestamp_unix_ms` (Unix milliseconds). They describe the same observation -time and must agree within one millisecond. Record ordering remains defined by -`source + seq`; timestamps are correlation metadata rather than the ordering -source of truth. - -The equivalent TOML is: - -```toml -[run] -agent = "codex" -executor = "container" -command = ["codex"] - -[container] -runtime = "docker" -image = "example/codex-agent:latest" -network = "host" - -[overlayfs] -path = "/workspace" -compose = ["/path/to/project"] -backend = "directory" -commit = "manual" - -[overlaynet] -mode = "proxy" -policy = "allowlist" - -[[overlaynet.rules]] -host = "api.openai.com" -ports = [443] - -[[overlaynet.deny]] -host = "169.254.0.0/16" - -[[overlaynet.limits]] -bytes_per_second = 1250000 - -[gateway] -mode = "capture" -level = "dialogue" - -[[gateway.routes]] -name = "openai" -provider = "openai" -upstream = "https://api.openai.com/v1" -api_key_env = "OPENAI_API_KEY" - -[chronicle] -mode = "lance" -``` - -Run it with `pvisor run --spec run.toml`. Explicit CLI scalars replace TOML -scalars. Supplying any repeated CLI field (`--overlayfs-compose`, -`--overlaynet-allow`, `--overlaynet-deny`, `--overlaynet-limit`, or -`--gateway-route`) replaces that complete TOML list. -The command after `--` replaces `run.command`. - -`--container-image IMAGE` selects the OCI container executor automatically; -`--executor container` makes the choice explicit. The transport resolves a -matching static `linux-amd64`/`linux-arm64` pVisor, mounts it into the image, -overrides the entrypoint, and invokes the normal -`pvisor run --executor host --spec ...` path. The Agent command is carried -inside the RunSpec rather than exposed in OCI runner argv. The injected -pVisor creates its own AgentCtl and returns a typed RunResult. The final -OverlayFS cwd and session Gateway configuration are mounted at stable paths. -User mounts are repeatable TOML inline tables, for example: - -```bash -pvisor run \ - --container-image example/codex-agent:latest \ - --container-pvisor-binary ./dist/pvisor-linux-amd64 \ - --container-platform linux/amd64 \ - --container-network none \ - --container-mount \ - 'source="/host/cache", target="/cache", read_only=false' \ - -- codex -``` - -The in-process Gateway and explicit OverlayNet proxy currently require -`container.network = "host"`, because their injected addresses are host -loopback endpoints. Bridge and no-network modes are valid when these drivers -are off. The executor records container isolation but does not claim full -capability enforcement. - -`--executor vm` uses statically linked libkrun and its embedded init to boot a -minimal Linux guest. `--rootfs image=IMAGE` selects this executor and pulls an -OCI/Docker image directly, without invoking Docker, Podman, or Buildah. When no -explicit rootfs is supplied, the default is `ubuntu:latest`. Manifests -and layer digests are verified, the host architecture selects `linux/arm64` or -`linux/amd64`, and the unpacked rootfs becomes the immutable lower layer of a -pVisor OverlayFS. `--image-store` overrides the platform cache directory. -OCI cache targets are marked immutable, and this protection survives logical -checkpoint/fork, so `pvisor apply` cannot mutate a rootfs shared by other Runs. - -On Linux, `--rootfs host` selects the host `/` as the VM rootfs lower and -selects the VM executor when `--executor` is omitted. `--rootfs ` selects -a prepared directory and `--rootfs image=` selects an OCI image or image -path. These forms are mutually exclusive, and host rootfs is rejected on macOS. -The OverlayFS view is selected with `--overlayfs-path`, the absolute path the -Agent sees. Repeat `--overlayfs-compose` to layer host directories in -bottom-to-top order; the current workspace remains the implicit bottom layer. -When `--overlayfs-path` is omitted, the current workspace is used as the view -source and pVisor places the merged mount in a managed per-Run path to avoid -mounting over its own lower directory. -With a guest workspace path, writes outside that workspace use a temporary -root upper and are discarded when the VM exits; workspace changes use the -durable OverlayFS stage. - -The merged rootfs is guest `/`, and `/workspace` becomes the guest cwd. On both -Linux and macOS, a vendored libkrun serves pVisor's rootfs and workspace -copy-on-write unions directly over virtio-fs. The VMM never re-exports a host -FUSE mount and does not materialize or reconcile either tree. Linux uses -KVM and Apple Silicon macOS uses HVF through the same executor. libkrunfw is -installed beside pVisor in wheels. Source builds otherwise download the pinned -official release into a SHA-256-verified platform cache; on macOS `/usr/bin/cc` -turns its prebuilt kernel bundle into the required dylib. A system directory can -still be selected with `--vm-library-dir`. OverlayNet `auto` uses the -non-bypassable VM smoltcp IPv4 TCP/DNS driver, while Gateway capture uses an -internal route through the guest virtual router. Linux additionally confines -the VMM with namespaces and Landlock. The macOS VMM still has the invoking -user's host permissions, so the first OCI-image version must not be treated as -a hostile multi-tenant boundary despite the guest-kernel isolation. - -On host/container execution, the four visible OverlayNet policy flags and -Gateway capture automatically enable the proxy driver. Any `--overlayfs-path`, -`--overlayfs-compose`, -`--stage`, `--overlayfs-backend`, or `--overlayfs-commit` option -automatically enables OverlayFS; no separate mode switch exists. The workspace -is the implicit base; explicit compose layers are applied in the order given. -An explicit `--stage` is the unified record -directory for metadata, trajectory, and filesystem state. When a stage is nested inside a base or compose layer, pVisor hides -that subtree from the merged view and rejects guest attempts to recreate it. -libkrun Runs create no live host mountpoint, preventing host indexers from -recursively entering `/merged`. The reverse topology, where a -stage contains a lower layer, is rejected. Both -`commit=apply` and the later `pvisor apply` command are rejected -for composed Runs until pVisor can materialize a complete merged-vs-base diff -safely. -On host/container execution, OverlayNet policy applies to traffic routed -through the explicit proxy and does not claim non-bypassable host network -isolation. On a libkrun VM, `auto` attaches non-bypassable smoltcp IPv4 -TCP/DNS; `off` leaves the guest offline. `--overlaynet-deny-all` supplies the -same default-deny policy to the active driver. Host/container direct sockets -remain ambient, while a VM Gateway route remains available through the guest's -virtual router for configured model traffic. - -## Run project discovery - -The current directory is the default project association. When OverlayFS is -enabled, `--overlayfs-compose` identifies reusable host layers and -`--overlayfs-path` identifies the Agent-visible view. Each Run receives an -independent directory under pVisor's default records root. If that root would be inside -the selected OverlayFS base or a compose layer, pVisor instead uses the system -temporary Run root to keep the writable stage disjoint: - -```text -project/ # reusable workspace / default base - -~/.persisting/runs/ -└── run-/ # one generated Run and default stage - ├── run.json - ├── run-bundle.json # mode 0600; outcome + safety + changes + effects - ├── overlay.json # when OverlayFS is enabled - ├── upper/ # or a Run-named Jujutsu workspace upper - ├── merged/ - ├── checkpoints/ - ├── lease.lock - ├── control.sock # while a live OverlayFS Run is available - ├── .capture/ # when OverlayNet/Gateway is enabled - └── chronicle/ # default pChronicle location -``` - -Lifecycle commands accept a Run id, Run directory, project workspace, -`run.json`, upper, or merged path. A project workspace selects its latest Run: - -```bash -pvisor status /path/to/project -pvisor inspect /path/to/project -- rg TODO . -pvisor apply /path/to/project --all -pvisor apply /path/to/project --path src --path tests/unit -pvisor apply /path/to/project --include 'docs/**' --exclude 'docs/generated/**' -pvisor apply /path/to/project --target /path/to/another-target --all -pvisor drop /path/to/project -``` - -`inspect` creates a separate kernel-read-only view. `apply` and `drop` refuse -to mutate a live Run. A filtered apply is dependency-closed and repeatable: -unselected paths remain staged, while opaque directories and hard-link groups -remain atomic. Each successful batch is persisted in `apply-ledger.json`. -The overlay records a durable first-touch fingerprint for every mutated target -path. `apply` fails closed if a selected target path changed after staging; -prepared batches recover forward, and individual non-directory replacements -commit with a same-directory atomic rename. The host filesystem still provides -no single atomic commit point for an arbitrary multi-file batch. -Applying all remaining changes or dropping the stage is terminal; `drop` cannot -undo already applied batches, and `apply` cannot recover discarded changes. -Terminal cleanup removes `upper`, `work`, and other disposable staging data but -retains compact Run/Overlay metadata, the apply ledger, and pChronicle history. - -## Related workflows - -- [Run your first Agent](../get-started.md) for the shortest complete loop. -- [Execution environments](../guides/execution.md) for choosing a provider. -- [Review and apply changes](../guides/review-apply.md) for filtered, repeatable apply. -- [Network control](../guides/network.md) and [capture](../guides/capture.md) for other Effect dimensions. diff --git a/docs/src/en/pvisor/reference/index.md b/docs/src/en/pvisor/reference/index.md deleted file mode 100644 index 83aaed9a2..000000000 --- a/docs/src/en/pvisor/reference/index.md +++ /dev/null @@ -1,8 +0,0 @@ -# pVisor reference - -- [`pvisor` command reference](cli.md) -- [Case catalog](cases.md) — runnable scenarios for checking a provider boundary - -The command references describe the current public surface. For concepts, use -[pVisor Concepts](../concepts/index.md); for mechanisms and known gaps, use -[pVisor Design](../design/index.md). diff --git a/docs/src/en/rfcs/0002-events-format.md b/docs/src/en/rfcs/0002-events-format.md index 203d045fc..3a712793c 100644 --- a/docs/src/en/rfcs/0002-events-format.md +++ b/docs/src/en/rfcs/0002-events-format.md @@ -7,7 +7,7 @@ | **Date** | 2026-07-30 | | **Component** | `persisting-events` + Gateway + pChronicle | | **Implements** | `persisting-events::EventRecord` · `persisting-pchronicle` `formats/events.rs` / `EventRow` | -| **Related** | [RFC-0001 Storyline](0001-storyline-format.md) · [RFC-0007 Events/Sidecar 边界](0007-events-contract-pchronicle-sidecar.md) · [Capture 管线](../pvisor/design/gateway.md) · [轨迹存储](../pchronicle/design/trajectory-storage.md) | +| **Related** | [RFC-0001 Storyline](0001-storyline-format.md) · [RFC-0007 Events/Sidecar 边界](0007-events-contract-pchronicle-sidecar.md) · Capture 管线 (external repository) · [轨迹存储](../pchronicle/design/trajectory-storage.md) | --- @@ -553,7 +553,7 @@ key = events 字段,value = 在 Storyline 上求值的 JSONPath。 | 文档 | 关系 | |---|---| | [轨迹存储](../pchronicle/design/trajectory-storage.md) | Lance SoT;本 RFC 强调 SoT 内容应是 HTTP wire | -| [Gateway 管线](../pvisor/design/gateway.md) | 生产 events;Story 边界在 **之后** 解释 | +| Gateway 管线 (external repository) | 生产 events;Story 边界在 **之后** 解释 | | [RFC-0001 Storyline](0001-storyline-format.md) | 有损 Normal 视图 / hub | | [轨迹 Markdown 格式](../pchronicle/reference/agenticmd.md) | 人读投影,不是 SoT | diff --git a/docs/src/en/roadmap.md b/docs/src/en/roadmap.md index bb6566c63..2c176e094 100644 --- a/docs/src/en/roadmap.md +++ b/docs/src/en/roadmap.md @@ -6,24 +6,19 @@ source of truth for delivered behavior. ## Now: make the local loop dependable -- Make pVisor Run → review → apply predictable on macOS and Linux. -- Keep effective controls, Effects, and warnings visible in every Run Bundle. - Make pChronicle onboarding, Dataset discovery, bounded SQL, and evidence lookup useful without a service or account. - Keep English and Chinese documentation paths aligned and examples runnable. ## Next: connect execution to durable history -- Make configured pVisor capture produce stable pChronicle Sources. - Preserve Run identity and lineage across capture, normalization, and query. - Improve comparison workflows for Runs, Sessions, and revisions. -- Document provider-specific boundaries instead of presenting one universal - isolation claim. ## Later: move from one workstation to a fleet - Share Dataset catalogs and policies across teams without hiding provenance. -- Support repeatable execution profiles for hosts, OCI containers, and VMs. + - Add operational guidance for retention, access control, and cost-aware storage. diff --git a/docs/src/en/system-design/architecture.md b/docs/src/en/system-design/architecture.md index 85c99452d..4512727b8 100644 --- a/docs/src/en/system-design/architecture.md +++ b/docs/src/en/system-design/architecture.md @@ -1,5 +1,8 @@ # End-to-end architecture +> pVisor and pPilot are maintained in external repositories. This repository +> contains pChronicle and the libraries needed for capture and durable history. + This document defines the contracts between Persisting products. Provider mechanisms belong to pVisor Design; storage layouts belong to pChronicle Design; commands belong to each product's Reference. @@ -33,8 +36,8 @@ boundary follows the selected platform: The provider reports requested versus effective capability dimensions in the Run Bundle. A successful process exit does not imply that the requested boundary was installed, and a workspace stage remains reviewable independently of the -provider that produced it. See [pVisor isolation design](../pvisor/design/isolation.md) -and the [execution guide](../pvisor/guides/execution.md) for provider-specific +provider that produced it. See pVisor isolation design (external repository) +and the execution guide (external repository) for provider-specific behavior and prerequisites. ## Independent ingress paths @@ -145,7 +148,7 @@ Chronicle publication starts a pChronicle sidecar over authenticated loopback IPC and treats only a successful sidecar acknowledgement as durable. The legacy mode name `lance` is an alias for `spawn`; pVisor no longer writes Lance itself. Sidecar flags and mode names belong to the -[pVisor CLI reference](../pvisor/reference/cli.md) and +pVisor CLI reference (external repository) and [RFC-0007](../rfcs/0007-events-contract-pchronicle-sidecar.md). ## Failure and recovery @@ -195,11 +198,11 @@ See [Security and evidence](security-evidence.md) for evidence levels and | Boundary | Contract owner | Detailed document | | --- | --- | --- | | logical runtime event and local Chronicle control protocol | `persisting-events` | [RFC-0007](../rfcs/0007-events-contract-pchronicle-sidecar.md) | -| Agent execution and Effect review | pVisor | [pVisor concepts](../pvisor/concepts/index.md) and [guides](../pvisor/guides/index.md) | -| provider and runtime mechanisms | pVisor | [pVisor design](../pvisor/design/index.md) | +| Agent execution and Effect review | pVisor | pVisor concepts (external repository) and guides (external repository) | +| provider and runtime mechanisms | pVisor | pVisor design (external repository) | | Dataset, facts, and projections | pChronicle | [pChronicle concepts](../pchronicle/concepts/index.md) | | storage and Snapshot implementation | pChronicle | [pChronicle design](../pchronicle/design/index.md) | -| stable command syntax and formats | each product | [pVisor reference](../pvisor/reference/index.md) and [pChronicle reference](../pchronicle/reference/index.md) | +| stable command syntax and formats | each product | pVisor reference (external repository) and [pChronicle reference](../pchronicle/reference/index.md) | | normative ownership decisions | Project RFCs | [RFC index](../rfcs/index.md) | This document changes only when a cross-product contract changes. Product diff --git a/docs/src/en/system-design/design-principles.md b/docs/src/en/system-design/design-principles.md index b9c56d856..39bf613dd 100644 --- a/docs/src/en/system-design/design-principles.md +++ b/docs/src/en/system-design/design-principles.md @@ -1,5 +1,8 @@ # Design principles +> pVisor and pPilot are maintained in external repositories. This repository +> contains pChronicle and the libraries needed for capture and durable history. + These principles explain why Persisting has separate products and why the documentation emphasizes reviewable steps. diff --git a/docs/src/en/system-design/index.md b/docs/src/en/system-design/index.md index 4565123f2..76e8a2a21 100644 --- a/docs/src/en/system-design/index.md +++ b/docs/src/en/system-design/index.md @@ -1,14 +1,17 @@ # System Design -Persisting provides durable infrastructure for Agent execution and trajectory -history. This section focuses on the -current public product path: +> pVisor and pPilot are maintained in external repositories. This repository +> contains pChronicle and the libraries needed for capture and durable history. -- [pVisor](../pvisor/index.md) virtualizes and governs one Agent Run; +Persisting provides durable Agent trajectory history. This section describes +how pChronicle integrates with external execution components: + +- pVisor (external repository) virtualizes and governs one Agent Run; - [pChronicle](../pchronicle/index.md) organizes durable trajectory Sources into queryable Datasets. -Gateway, OverlayFS, and OverlayNet are pVisor runtime mechanisms. Where +Gateway and OverlayNet support trajectory capture here; OverlayFS belongs to +the external execution components. Where available, stable Run identity connects the domains, but each also has a standalone entry path. @@ -53,7 +56,7 @@ need to answer crosses both domains. - [Complete architecture and target model](architecture.md) - [Local-to-fleet continuity](local-to-fleet.md) - [Security and evidence model](security-evidence.md) -- [pVisor implementation boundaries](../pvisor/design/index.md) +- pVisor implementation boundaries (external repository) - [pChronicle implementation boundaries](../pchronicle/design/index.md) Delivery state is reported in the product Design pages and diff --git a/docs/src/en/system-design/local-to-fleet.md b/docs/src/en/system-design/local-to-fleet.md index 407c13b7b..a24a5def0 100644 --- a/docs/src/en/system-design/local-to-fleet.md +++ b/docs/src/en/system-design/local-to-fleet.md @@ -1,5 +1,8 @@ # Local to fleet +> pVisor and pPilot are maintained in external repositories. This repository +> contains pChronicle and the libraries needed for capture and durable history. + The portable unit is a logical Run, not a live virtual machine. ![The AgentVisor execution continuum](../../assets/diagrams/agentvisor/execution-continuum.svg) @@ -22,6 +25,6 @@ effects. In a fleet, the same model adds placement, tenant isolation, leases, attestation, recovery, and reconciliation without redefining the Run. The stable identity model is defined in -[Run, Attempt, and Effect](../pvisor/concepts/run-model.md). Provider admission -belongs to [pVisor isolation](../pvisor/design/isolation.md). Fleet coordination—placement, leases, and reconciliation—belongs to the +Run, Attempt, and Effect (external repository). Provider admission +belongs to pVisor isolation (external repository). Fleet coordination—placement, leases, and reconciliation—belongs to the deployment control plane and does not change the logical Run contract. diff --git a/docs/src/en/system-design/security-evidence.md b/docs/src/en/system-design/security-evidence.md index 31079f6f0..48db3c4c8 100644 --- a/docs/src/en/system-design/security-evidence.md +++ b/docs/src/en/system-design/security-evidence.md @@ -1,5 +1,8 @@ # Security and evidence model +> pVisor and pPilot are maintained in external repositories. This repository +> contains pChronicle and the libraries needed for capture and durable history. + Persisting does not compress security into one `safe` or `sandboxed` label. Every Run reports guarantees by capability dimension. pVisor owns admission and runtime enforcement. Placement and recovery mechanisms do not upgrade a @@ -44,8 +47,8 @@ This final event path is narrower than the Run Bundle: it does not currently publish the complete Artifact, lineage, filesystem Effect, AgentCtl/network/resource Evidence, output, or metrics inventory. -Read [Capabilities and evidence](../pvisor/concepts/capabilities-and-evidence.md) -for the user model, [pVisor isolation design](../pvisor/design/isolation.md) and -[OverlayNet](../pvisor/design/overlaynet.md) for mechanisms, and +Read Capabilities and evidence (external repository) +for the user model, pVisor isolation design (external repository) and +OverlayNet (external repository) for mechanisms, and [Facts and projections](../pchronicle/concepts/facts-and-projections.md) for the history boundary. diff --git a/docs/src/en/why-persisting.md b/docs/src/en/why-persisting.md index 46eeac995..262996210 100644 --- a/docs/src/en/why-persisting.md +++ b/docs/src/en/why-persisting.md @@ -10,11 +10,8 @@ long-running session. A terminal transcript is too shallow to review safely; an isolated sandbox without a durable record is hard to learn from; a raw event log is difficult to query consistently. -Persisting treats execution and history as two related but independent jobs: +Persisting focuses on durable Agent history: -- **pVisor governs the Run.** It gives an Agent a staged workspace, records the - controls that were actually active, and lets a person review Effects before - applying them. - **pChronicle preserves the trajectory.** It normalizes supported Sources into queryable Datasets so teams can inspect, compare, and improve Runs later. @@ -32,11 +29,8 @@ actually available. ## When Persisting fits -Use Persisting when an Agent can change a real project, when a Run needs human -review before merge, or when trajectory history should remain useful after the -terminal session ends. Start with pVisor for controlled execution, pChronicle -for existing history, or connect both when the question crosses the execution -and history boundaries. +Use pChronicle when trajectory history should remain useful after a terminal +session ends. pVisor and pPilot are maintained in external repositories. If you only need a one-off script with no review or history requirement, Persisting may be more infrastructure than the task needs. diff --git a/docs/src/zh/guides/using-persisting.md b/docs/src/zh/guides/using-persisting.md index 151ba4ca4..7692cf830 100644 --- a/docs/src/zh/guides/using-persisting.md +++ b/docs/src/zh/guides/using-persisting.md @@ -2,20 +2,15 @@ 选择能回答当前问题的最小工作流,不需要一次采用所有组件。 -## 我需要 Agent 安全地修改项目 - -从[pVisor 入门](../pvisor/get-started.md)开始:在 staged workspace 中运行 Agent, -审查 Run Bundle,再只应用信任的路径。下一次 Run 确实需要时,再增加网络或 provider 控制。 - ## 我已经有轨迹数据 从[pChronicle 入门](../pchronicle/get-started.md)开始:打开 Dataset、查看汇总、提出一个 有界 SQL 问题,再定位答案背后的 Evidence。只读流程稳定后,再使用导入导出或服务指南。 -## 我需要把执行和历史放在一起 +## 我需要捕获新的模型请求 -当 Run 的生命周期事件需要成为 pChronicle Source 时,使用[pVisor capture](../pvisor/guides/capture.md)。 -私有 Run Bundle 仍是本地执行记录;capture 是显式交接,不会隐式复制所有 Artifact。 +使用 [Gateway](../pchronicle/guides/serve-gateway.md) 转发请求并持久化 canonical events。 +外部执行组件也可以通过 pChronicle Control 服务提交事件。 ## 一套可靠的使用习惯 diff --git a/docs/src/zh/index.md b/docs/src/zh/index.md index 158092a11..fd73649bb 100644 --- a/docs/src/zh/index.md +++ b/docs/src/zh/index.md @@ -1,7 +1,7 @@ --- template: home.html title: Agent 时代的持久化基础设施 -description: 在可审查的执行边界中运行 Agent,把真实发生的事情保存为可查询的历史。 +description: 捕获、导入并查询持久 Agent 轨迹历史。 hide: - navigation - toc diff --git a/docs/src/zh/installation.md b/docs/src/zh/installation.md index 052a09e70..f8ed47778 100644 --- a/docs/src/zh/installation.md +++ b/docs/src/zh/installation.md @@ -1,100 +1,27 @@ -# 安装指南 +# 安装 -Persisting 提供两条公开命令行路径: +本仓库发布 `pchronicle` 命令和内嵌 Web UI。pVisor 与 pPilot 已拆分到外部仓库。 -- `pvisor` 在可审查的执行边界中运行 Agent。 -- `pchronicle` 打开、查询、交换和服务轨迹 Dataset。 - -默认安装包含公开 CLI 和 Dataset 工作流所需的全部能力。 - -## 1. 安装工具 +## 安装 ```bash pip install persisting +pchronicle --version +pchronicle onboard ``` -确认命令可用: - -```bash -pvisor --version -pchronicle --help -``` - -wheel 会把匹配版本的 Python 包和公开 CLI 入口安装到当前 Python 环境。项目有其他 -Python 依赖时,建议使用虚拟环境: - -```bash -python3 -m venv .venv -source .venv/bin/activate -python -m pip install --upgrade pip -pip install persisting -``` - -!!! tip "可以从任意一个产品开始" - - 探索 pChronicle 不要求先运行 pVisor。如果你想先运行 Agent,继续阅读[运行第一个 Agent](pvisor/get-started.md); - 如果已经有轨迹数据,继续阅读[探索第一个 Dataset](pchronicle/get-started.md)。 - -## 2. 检查平台要求 - -CLI 支持 macOS 和 Linux,要求 Python 3.10 或更新版本。普通 host Run 不需要文件系统扩展。 -在 macOS 上使用 host-process 的 staged Run(`pvisor run --stage …`)前,先安装 macFUSE: - -```bash -brew install --cask macfuse -``` - -macOS 提示时允许 macFUSE system extension。不带 `--stage` 时 Agent 可能直写项目目录; -带 `--stage` 且挂载能力不可用时,Run 会 fail closed,而不会静默退化为无 COW 直写。 -libkrun VM executor 不需要 macFUSE。 - -## 3. 需要时从源码安装 +平台 wheel 支持 Linux x86_64 和 Apple Silicon macOS,需要 Python 3.10+。 +Python 包与 `pchronicle` 会安装到当前 Python 环境中。 -需要 `main` 最新构建时,可以使用 nightly wheel: +## Nightly 与源码构建 ```bash curl -fsSL https://raw.githubusercontent.com/DeepLink-org/Persisting/main/scripts/install-nightly.sh | bash ``` -本地开发时,从 checkout 安装 Python 包: - -```bash -git clone https://github.com/DeepLink-org/Persisting.git -cd Persisting -pip install -e . -``` - -也可以从源码构建 CLI 组件: - -```bash -just install-cli -``` - -只有在明确测试特定 pVisor 二进制时才设置 `PERSISTING_PVISOR_BIN`。排查 Provider 行为时, -应尽量让 Python 包和 CLI 来自同一 revision。 - -## 4. 需要时启用 VM 或 OCI 执行 - -默认本地工作流不要求安装 Docker 或 Podman。使用 VM executor 运行 OCI 镜像时,可以显式指定: - -```bash -pvisor run --image ubuntu:latest -- COMMAND -``` - -未指定时 VM 也默认使用 `ubuntu:latest`。`--image-store DIR` 修改本地内容寻址缓存, -`--overlayfs-target` 选择 guest workspace,`--vm-rootfs DIR` 指向预先准备的 Linux rootfs。 -Linux 使用 KVM;Apple Silicon macOS 使用 HVF。从源码在 macOS 构建 VM 支持还需要 Zig: - -```bash -brew install zig -``` - -把这些选项当作独立的平台步骤。先完成 staged host workflow,再用已有 Run Bundle 对比不同 -执行环境的边界。 - -## 5. 选择下一步 +在源码目录中,`pip install -e .` 会构建 pChronicle 和 Web 资源。 +`just install-cli` 安装 Rust CLI;构建内嵌 Web 资源需要 Dioxus CLI。 +参见[工程说明](project/engineering.md)。 -- [运行第一个 Agent](pvisor/get-started.md) —— 暂存、审查并选择性应用修改。 -- [探索第一个 Dataset](pchronicle/get-started.md) —— 在准备真实 Source 前查询临时数据。 -- [选择工作流](overview.md) —— 按当前任务决定使用哪个产品。 -- [执行环境](pvisor/guides/execution.md) —— 比较 host、OCI 与 VM 的边界。 +继续阅读[探索第一个 Dataset](pchronicle/get-started.md)或 +[Gateway 捕获](pchronicle/guides/serve-gateway.md)。 diff --git a/docs/src/zh/overview.md b/docs/src/zh/overview.md index 40c15ce56..e8c91fc57 100644 --- a/docs/src/zh/overview.md +++ b/docs/src/zh/overview.md @@ -5,42 +5,20 @@ hide: # 开始使用 -沿着一条主线,从安装 CLI 到运行一个可以审查和查询的 Agent Run。 +使用 pChronicle 捕获、导入并查询持久 Agent 历史。 ## 1. 安装 -安装命令行工具,并确认两个产品入口可用: +安装命令行工具,并确认 pChronicle 入口可用: ```bash pip install persisting -pvisor --help pchronicle --help ``` -macOS 使用 staged host workspace 前,需要安装 macFUSE: - -```bash -brew install --cask macfuse -``` - [阅读安装指南 →](installation.md) -## 2. 使用 pVisor 运行 Agent - -在 staged workspace 中运行 Agent,检查实际发生的事情,只应用你信任的修改: - -```bash -pvisor run --stage ./runs/task-001 -- codex -pvisor review last -pvisor apply last --path src -``` - -Agent 工作期间,基础项目保持不变。Run Bundle 会记录文件 Effect、实际控制机制、网络证据和警告。 -继续阅读[运行第一个 Agent](pvisor/get-started.md)完成完整流程,再学习[选择性 apply](pvisor/guides/review-apply.md)。 - -**完成本节后:**你会得到一次经过审查的项目修改,并清楚哪些内容仍留在 stage 中。 - -## 3. 记录并分析 Agent 轨迹 +## 2. 记录并分析 Agent 轨迹 Agent 运行后,使用 pChronicle 把轨迹变成可以检查和查询的 Dataset。先用临时示例,安全地熟悉流程: @@ -58,4 +36,4 @@ pchronicle query ./trajectory-data \ 继续阅读[探索第一个 Dataset](pchronicle/get-started.md),学习 Dataset 健康检查、证据定位、格式、导入导出和只读 Web/API。 -**完成本节后:**你可以把一个答案连接到产生它的 Dataset 和 Source。需要连接两个产品时,继续阅读[pVisor 捕获指南](pvisor/guides/capture.md)。 +**完成本节后:**你可以把一个答案连接到产生它的 Dataset 和 Source。需要记录新的模型请求时,使用[Gateway 捕获](pchronicle/guides/serve-gateway.md)。 diff --git a/docs/src/zh/pchronicle/design/trajectory-storage.md b/docs/src/zh/pchronicle/design/trajectory-storage.md index c1442ed1d..c36d94e83 100644 --- a/docs/src/zh/pchronicle/design/trajectory-storage.md +++ b/docs/src/zh/pchronicle/design/trajectory-storage.md @@ -178,6 +178,6 @@ OpenAI msg ┘ - [发现并查询](../guides/discover-and-query.md) - [Snapshot](catalog.md) - [AgenticMD 格式](../reference/agenticmd.md) -- [Gateway 架构](../../pvisor/design/gateway.md) -- [pVisor 命令](../../pvisor/reference/cli.md) +- Gateway 架构 (外部仓库) +- pVisor 命令 (外部仓库) - [`pchronicle` Dataset 命令](../reference/cli.md) diff --git a/docs/src/zh/pchronicle/get-started.md b/docs/src/zh/pchronicle/get-started.md index 40226c601..bce245d92 100644 --- a/docs/src/zh/pchronicle/get-started.md +++ b/docs/src/zh/pchronicle/get-started.md @@ -127,7 +127,6 @@ pchronicle find ./trajectory-data \ - [打开本地 Web UI](guides/ui.md) - [导入或导出 Run](guides/exchange.md) - [在本地提供 Dataset 服务](guides/serve.md) -- [使用 pVisor 捕获新 Run](../pvisor/guides/capture.md) - [理解 Dataset 与 Source 模型](concepts/index.md) Walkthrough 创建的 Dataset 是临时的。继续导入、服务化或生产自动化前,请换成自己的路径。 diff --git a/docs/src/zh/pchronicle/guides/index.md b/docs/src/zh/pchronicle/guides/index.md index 5a100be2d..2ae881f03 100644 --- a/docs/src/zh/pchronicle/guides/index.md +++ b/docs/src/zh/pchronicle/guides/index.md @@ -7,7 +7,6 @@ 3. [在本地服务 Dataset](serve.md)。 4. [使用本地 Web UI 浏览、下钻和分析](ui.md)。 5. [通过 `serve` Gateway 转发、改写并捕获请求](serve-gateway.md)。 -6. [使用 pVisor 捕获新 Run](../../pvisor/guides/capture.md)。 Guide 解释决策并完成工作流;精确参数查阅 [`pchronicle` 命令行指南](../reference/cli.md),仓库内 fixture 见[项目示例](../../project/examples.md)。 diff --git a/docs/src/zh/pchronicle/guides/serve-gateway.md b/docs/src/zh/pchronicle/guides/serve-gateway.md index 7dbfb3b91..72d39a784 100644 --- a/docs/src/zh/pchronicle/guides/serve-gateway.md +++ b/docs/src/zh/pchronicle/guides/serve-gateway.md @@ -55,7 +55,7 @@ capture events 追加到 CLI 指定的输出 Dataset。Dataset Web UI 和 API 当 Agent 或 SDK 已经能够调用 OpenAI、Anthropic 或 Gemini 兼容的 base URL,而你希望不 启动 pVisor Run 就捕获这些流量时,可以使用这个模式。如果 Gateway 需要与 Agent 执行共享 -生命周期和隔离边界,应改用 [pVisor 捕获](../../pvisor/guides/capture.md)。 +生命周期和隔离边界,应改用 pVisor 捕获 (外部仓库)。 ## 配置输入 @@ -163,7 +163,7 @@ Capture metadata 会区分客户端请求模型和实际 upstream 模型,并 | `network` | 否 | `mode = "public"` | 显式 forward-proxy 流量的策略。 | 共享 Gateway schema 还接受 `[overlay]`,但 `pchronicle serve` 不会创建或 apply 文件系统 -overlay。Overlay 生命周期属于 [pVisor](../../pvisor/guides/execution.md)。 +overlay。Overlay 生命周期属于 pVisor (外部仓库)。 ### 捕获级别 diff --git a/docs/src/zh/pchronicle/index.md b/docs/src/zh/pchronicle/index.md index 6a3893fd1..daa609dff 100644 --- a/docs/src/zh/pchronicle/index.md +++ b/docs/src/zh/pchronicle/index.md @@ -66,7 +66,7 @@ pchronicle query ./trajectory-data \ - **打开本地 UI 与 API:** [提供 Dataset 服务](guides/ui.md) pChronicle 读取并组织运行历史,不执行或调度 Agent。要在受控工作区中运行 Agent,请从 -[pVisor](../pvisor/index.md)开始。 +pVisor (外部仓库)开始。 ## 推荐阅读顺序 diff --git a/docs/src/zh/pchronicle/reference/agenticmd.md b/docs/src/zh/pchronicle/reference/agenticmd.md index 93cf5897d..d027fc0b6 100644 --- a/docs/src/zh/pchronicle/reference/agenticmd.md +++ b/docs/src/zh/pchronicle/reference/agenticmd.md @@ -79,7 +79,6 @@ Lance 重建 AgenticMD,但当前公共 `pchronicle` CLI 不提供 AgenticMD ma ## 6. 示例与实现 -- Gateway 端到端定量示例:`examples/pvisor/04-gateway-llm-control/` - Lance/ATIF 存储与分析示例:`examples/pchronicle/` - 格式与视图实现:`crates/persisting-pchronicle/src/formats/`、`src/projection/` - [pChronicle 运行存储](../design/trajectory-storage.md) diff --git a/docs/src/zh/ppilot/design/orchestration.md b/docs/src/zh/ppilot/design/orchestration.md deleted file mode 100644 index 81f81aeea..000000000 --- a/docs/src/zh/ppilot/design/orchestration.md +++ /dev/null @@ -1,55 +0,0 @@ -# pPilot 架构 - -pPilot 是 durable Run 编排器。产品面刻意只暴露 `ppilot run` 和 `ppilot produce`。 - -本页负责多 Run 控制算法。用户工作流属于 -[编排多个 Agent Run](../guides/orchestrate.md),跨产品提交边界属于 -[系统架构](../../system-design/architecture.md)。 - -```text -planner / plan() - │ stable task identity + backpressure - ▼ -pPilot ── RunSpec/file + process ──► pVisor ── RunResult ──► durable result journal - │ │ - │ lease / CAS │ EventRecord + Attempt state - └──────────────┬─────────────────┘ - ▼ - pChronicle control sidecar - │ - ▼ - durable history store -``` - -pPilot 负责 planning、有界并发、lease、基础设施重试、resume/reconciliation -和结果收集。pVisor 负责每个 Run/Attempt 及其 runtime driver。pChronicle 负责 -轨迹 Dataset catalog、SQL、分析、find、导入导出与 serving。 - -pPilot 不链接 pVisor 实现 crate。它为每个 Run 启动一个前台 `pvisor` 二进制, -提交带版本的 `RunSpec`,并读取原子 `RunResult`。作业 Supervisor 的注册、 -heartbeat、quota 和 cancel 消息是共享的 agentctl 契约。进程退出仍是生命周期 -边界;Supervisor 连接提供实时控制,而不需要常驻 pVisor daemon。 - -默认 pVisor 构建没有内嵌 Chronicle 存储适配器,也不链接 Lance 或 DataFusion。 -配置了 durable publication 时,pVisor 启动 `pchronicle serve` 的 Control -组件,并通过共享的 `persisting-events` control 契约发布 Attempt 状态以及 -lifecycle/Gateway 事件。pPilot 用同一契约做 lease/CAS 和结果日志协调;每条 -命令各自拥有它启动的 sidecar 进程。可执行文件由 pVisor 安装或 Run 配置选择; -录制由 `--record-format` 与 `--record-destination` 选择。 - -本地 control 协议带版本、按请求关联、用进程级 token 鉴权,并绑定 loopback。 -它是进程边界,而不是第二套存储实现:只有 pChronicle 选择物理 backend,并 -发送 durable acknowledgement。见 -[RFC-0007](../../rfcs/0007-events-contract-pchronicle-sidecar.md)。 - -`run` 执行 map 风格的 `plan()` / `execute(item)` 工作负载。`produce` 从 -planner 流式接收完整 Run 描述,并为每项创建独立的 pVisor workspace。两者都 -启动进程内 job Supervisor,并发布稳定谱系(`parent_run_id`、`task_id` 和 -作业元数据)。 - -durable 路径使用单调递增的 lease epoch 和终端 CAS,拒绝过期 worker。重启时, -pPilot 先核对 journal、Attempt 记录和 Run 控制记录,再决定 defer、recover -还是 redispatch。外部 Effect 仍需要应用层幂等;系统不承诺 exactly-once 执行。 - -公开接口见 [`ppilot` 命令参考](../reference/cli.md),此处使用的身份与重试 -模型见 [Run、Attempt 与 Effect](../../pvisor/concepts/run-model.md)。 diff --git a/docs/src/zh/ppilot/get-started.md b/docs/src/zh/ppilot/get-started.md deleted file mode 100644 index 9f622a498..000000000 --- a/docs/src/zh/ppilot/get-started.md +++ /dev/null @@ -1,53 +0,0 @@ -# 开始使用 pPilot - -本页走最短的已验证 pPilot 闭环:一份流式 Python plan,由多个 worker 执行, -终端结果写入 durable sink。 - -## 安装 - -`ppilot` 与 `pvisor`、`pchronicle` 装在同一个 Python wheel 里: - -```bash -pip install persisting -ppilot --version -``` - -从源码 checkout 时,`just install-cli` 会安装同一组组件。 - -## 定义工作 - -创建 `plan.py`: - -```python -def plan(): - for value in range(6): - yield {"id": f"square-{value}", "value": value} - - -def execute(item): - return {"square": item["value"] ** 2} -``` - -`plan()` 产出带稳定 `id` 的工作项;`execute(item)` 处理其中一项。稳定身份让 -被中断的作业可以 resume,而不重复已完成的工作。 - -## 运行 - -```bash -ppilot run plan.py --workers 2 --per-worker 2 --sink ./results --results ndjson -``` - -## 核对持久结果 - -```bash -cat ./results/ready.ndjson -``` - -预期:六条结果记录,每条任务一条,平方值为 0、1、4、9、16、25(合计 55)。 -脚本化版本见 -[`examples/ppilot/01-run/`](https://github.com/DeepLink-org/Persisting/tree/main/examples/ppilot/01-run)。 - -## 接下来去哪 - -- [编排多个 Agent Run](guides/orchestrate.md) — resume、重试与生产 sink -- [pPilot CLI 参考](reference/cli.md) — `run` 与 `produce` 标志 diff --git a/docs/src/zh/ppilot/guides/orchestrate.md b/docs/src/zh/ppilot/guides/orchestrate.md deleted file mode 100644 index 4b624360a..000000000 --- a/docs/src/zh/ppilot/guides/orchestrate.md +++ /dev/null @@ -1,53 +0,0 @@ -# 编排多个 Agent Run - -`pPilot` 把 Run 模型从一次执行扩展到一组有界任务。它负责 planning、并发、lease、 -基础设施故障重试、持久结果发布和恢复。 - -它不会重新定义 Agent runtime;每个任务仍然是独立 Run。 - -## 定义任务 - -创建 `plan.py`: - -```python -def plan(): - for value in range(6): - yield {"id": f"square-{value}", "value": value} - - -def execute(item): - value = item["value"] - return {"square": value * value} -``` - -稳定 ID 很重要:retry 与 reconciliation 依靠它识别同一个逻辑任务。 - -## 使用有界并发运行 - -```bash -ppilot run plan.py --workers 2 --per-worker 2 --sink ./results -``` - -`--workers` 与 `--per-worker` 限制 active work;`--sink` 启用 durable result journal -和 lease fencing。 - -## 检查持久结果 - -```bash -cat ./results/ready.ndjson -``` - -基础设施故障可以重试;业务错误会被报告,而不是静默重试。Reconciler 修复结果发布 -附近已经支持的崩溃窗口。 - -## 显式处理外部 Effect - -Lease fencing 保护结果所有权,但不能让任意外部 API 自动拥有 exactly-once 语义。请把 -稳定 task ID 用作幂等键,或者让外部操作支持 transaction 或 compensation。 - -## 继续进入历史层 - -Result sink 不是轨迹历史。应在每个 Run 中捕获 Agent event,再使用 -[pChronicle](../../pchronicle/get-started.md)跨 Run 检查。 - -完整可运行编排案例见[可复现示例](../../project/examples.md)。 diff --git a/docs/src/zh/ppilot/index.md b/docs/src/zh/ppilot/index.md deleted file mode 100644 index 86c023d01..000000000 --- a/docs/src/zh/ppilot/index.md +++ /dev/null @@ -1,22 +0,0 @@ -# pPilot - -**大规模、可恢复的 Run 生产。** - -pPilot 把 Run 模型从一次执行扩展到一组有界任务。它负责 planning、有界并发、 -lease 与 fencing 决策、基础设施重试与恢复、reconciliation、持久结果发布,以及 -task 到 Run 的映射。 - -它不会重新定义 Agent runtime:每个任务仍然是独立的 -[pVisor Run](../pvisor/concepts/run-model.md),由独立的 `pvisor` 二进制执行。 - -| 命令 | 负责 | -| --- | --- | -| `ppilot run` | 以可恢复的方式执行 `plan()` / `execute(item)` 工作负载 | -| `ppilot produce` | 从流式 planner 创建彼此独立的 pVisor Run | - -## 从这里开始 - -- [快速开始](get-started.md) — 五分钟跑完第一个并行 plan -- [编排多个 Agent Run](guides/orchestrate.md) — planning、worker、resume 与 sink -- [编排架构](design/orchestration.md) — lease、fencing 与恢复保证 -- [pPilot CLI 参考](reference/cli.md) — 精确的标志与退出行为 diff --git a/docs/src/zh/ppilot/reference/cli.md b/docs/src/zh/ppilot/reference/cli.md deleted file mode 100644 index c0dfcd9e9..000000000 --- a/docs/src/zh/ppilot/reference/cli.md +++ /dev/null @@ -1,73 +0,0 @@ -# `ppilot` 命令参考 - -`ppilot` 是可扩展的 Run 生产 CLI。它只暴露两条命令: - -```text -ppilot -├── run execute plan() / execute(item) with durable recovery -└── produce create independent pVisor Runs from a streaming planner -``` - -Dataset 发现、SQL、内置分析、find、导入导出与 serving 由 -[`pchronicle`](../../pchronicle/reference/cli.md) 负责。 - -## `run` - -```bash -ppilot run plan.py --workers 8 --per-worker 2 --sink ./results -ppilot run plan.py --workers 8 --sink ./results --resume -ppilot run plan.py --check -ppilot run plan.py --pvisor-binary ./target/release/pvisor -``` - -脚本定义 `plan()` 和 `execute(item)`。pPilot 施加有界并发与 backpressure, -把终端结果写入 durable sink,并用稳定 task 身份做 resume 与 retry。`--check` -校验 plan 和一次样例执行,而不跑完整工作负载。 - -`--results` 选择 stdout 上的结果流:`ndjson`(每个完成的 task 一行 JSON)、 -`summary`(作业结束后输出 JSON 汇总;失败 task 还会在 stderr 打印 NDJSON)、 -或 `quiet`(stdout 不输出结果流)。默认是 `ndjson`。配合 `--observe` 时默认变为 -`quiet`,以免进度行被淹没;需要两者并存时传 `--results ndjson`。 - -## `produce` - -```bash -ppilot produce production.py --output ./runs --parallelism 8 -ppilot produce production.py --output ./runs --parallelism 8 \ - --cluster-network-limit 10mbps -- --dataset train -``` - -planner 的 `plan()` 可以是同步或异步 iterator。每一项描述一次 Run: - -```python -def plan(): - for index in range(100): - yield { - "id": f"task-{index:04d}", - "agent": "codex", - "command": ["codex", "exec", f"Solve task {index}"], - "cwd": "/work/eval", - } -``` - -每个产出项在 `--output` 下拥有自己的 pVisor workspace。planner 在并发窗口内 -流式消费,因此大批次不会完整驻留内存。命令写入 `production-report.json`; -任一 Run 失败会在报告落盘后让命令以失败退出。 - -`--cluster-network-limit` 把保守的聚合代理速率按请求的 parallelism 均分。 -它要求 Gateway capture,且不覆盖绕过显式代理的直接 socket。 - -## 运行时所有权 - -两条命令都启动进程内、作业范围的 Supervisor。pPilot 负责 planning、lease、 -重试、reconciliation 和收集。pVisor 负责 Run 执行和内嵌 Gateway。pPilot 为 -每个 Run 调用一个前台 `pvisor` 进程;两个组件通过 agentctl 共享 Run 与 -Supervisor 契约,而不是把 pVisor 链进 pPilot。`--pvisor-binary` 和 -`PERSISTING_PVISOR_BIN` 选择显式可执行文件。pChronicle 负责轨迹 Dataset -操作。 - -可执行文件的 `--help` 是标志与默认值的权威来源。 - -完整工作流见 [编排多个 Agent Run](../guides/orchestrate.md),lease 与 -reconciliation 见 [pPilot 架构](../design/orchestration.md),重试身份模型见 -[Run、Attempt 与 Effect](../../pvisor/concepts/run-model.md)。 diff --git a/docs/src/zh/project/engineering.md b/docs/src/zh/project/engineering.md index 21fc14939..9a834209e 100644 --- a/docs/src/zh/project/engineering.md +++ b/docs/src/zh/project/engineering.md @@ -10,17 +10,17 @@ | 命令 | 作用 | |---|---| | `just test` | 通过 `cargo nextest` 跑工作区 Rust 测试,再跑 Python 套件 | -| `just test ` | 单个 crate 或 Cargo package(例如 `pvisor` 或 `persisting-pvisor`) | +| `just test ` | 单个 crate 或 Cargo package(例如 `pchronicle` 或 `persisting-pchronicle`) | | `just docs-sync` | 安装锁定的文档环境 | | `just docs-serve` | 本地 Zensical 预览,文件修改时自动刷新 | | `just docs-serve-dirty` | 自动重载卡住时重新启动 Zensical 预览 | | `just docs-build` | 构建静态文档站点 | -| `just examples` | pVisor 与 pChronicle 产品示例套件 | +| `just examples` | pChronicle 产品示例套件 | | `just gate` | 格式化、lint 以及完整 Rust 测试工作区 | | `just dev` | 限定范围的 runtime crate 检查;不是完整工作区矩阵 | `just test` 使用 debug nextest profile 以便更快迭代。传入 Cargo package 名 -或短 crate 别名(`pvisor`、`pchronicle`、`pchronicle-cli`、 +或短 crate 别名(`pchronicle`、`pchronicle-cli`、 `agentctl`、`capture`)。`just test pchronicle` 会同时跑 `persisting-pchronicle` 与 `persisting-pchronicle-cli`(与 CI 的 pchronicle shard 一致);只要 CLI 时用 `just test pchronicle-cli`。无参数形式还会跑 @@ -57,7 +57,7 @@ Rust 测试用 `cargo nextest` 做进程隔离和并行执行;用 仓库把两项昂贵 / nightly 诊断排除在日常编辑循环之外: -- `just build-analysis persisting-pvisor` 为一个 package 启用 Cargo 的 +- `just build-analysis persisting-pchronicle` 为一个 package 启用 Cargo 的 `-Z build-analysis`,并把每会话 JSONL 指标写到 `$CARGO_HOME/log`。用 `just build-analysis-report` 查看(也可传 `report=timings` 或 `report=rebuilds`)。独立的 target 目录避免诊断产物污染普通增量缓存。 @@ -69,7 +69,5 @@ Rust 测试用 `cargo nextest` 做进程隔离和并行执行;用 Sanitizer 构建故意不进入 `just dev` / CI 的默认路径:它们会重建标准库, 只适合聚焦的调试会话。 -支持的行为请从 [pVisor 指南](../pvisor/guides/index.md)、 -批量 Run 编排 或 -[pChronicle 指南](../pchronicle/guides/index.md) 开始,实现理由见 +支持的行为请从 [pChronicle 指南](../pchronicle/guides/index.md) 开始,实现理由见 [系统架构](../system-design/index.md)。 diff --git a/docs/src/zh/project/examples.md b/docs/src/zh/project/examples.md index 2eb6ce58c..a14ba1695 100644 --- a/docs/src/zh/project/examples.md +++ b/docs/src/zh/project/examples.md @@ -1,53 +1,11 @@ -# 复现 Run 生命周期 +# 可复现示例 -[`examples/`](https://github.com/DeepLink-org/Persisting/tree/main/examples) -按产品命令组织。每个 `run.sh` 管理自己的 `.work/`,并输出持久结果或查询结果。它们共同 -对应使用指南的主线:执行、治理 Effect、检查历史。 +在仓库根目录运行 `just examples-pchronicle` 或 `just examples`。 +确定性 CLI 示例覆盖 Dataset 生命周期、内置分析、跨 Dataset SQL、存储性能、 +格式往返以及直接查询 OpenAI/ACTF。 -```bash -just examples -just examples-pvisor -just examples-pchronicle -``` +每个示例维护自己的 `.work/` 目录,并打印可检查的输出。 +套件构建 release 可执行文件,需要 Cargo、Python 3、`jq` 和常见 POSIX 工具。 -## pVisor - -| 示例 | 可复现结论 | -|---|---| -| `01-filesystem-isolation` | 事务工作区隔离 | -| `02-changeset-management` | review、apply 与 drop | -| `03-network-isolation` | 显式代理策略及其边界 | -| `04-gateway-llm-control` | 内嵌 Gateway 路由与捕获 | - -## orchestration layer - -| 示例 | 可复现结论 | -|---|---| -| `01-run` | 并发执行 `plan()` / `execute()` 并将结果写入持久化 sink | -| `02-produce` | Python planner 生成多个独立、可审查的 pVisor Run | - -## pChronicle - -| 示例 | 可复现结论 | -|---|---| -| `01-dataset-lifecycle` | 导入、检查、查询、定位并严格导出 Dataset | -| `02-built-in-analysis` | 汇总 Sources、Agents、Models、工具并定位 Step | -| `03-cross-dataset-sql` | 对三个命名 Dataset 挂载执行跨 Dataset SQL | -| `04-storage-query-performance` | 比较 JSON/Lance 体积、压缩比、查询比率与生命周期延迟 | -| `05-format-roundtrip` | 严格 ATIF 往返并规范化后按字节比较 | -| `06-query-openai-actf-directly` | 直接对 OpenAI Messages 与 ACTF Dataset 执行 SQL | - -pChronicle 示例使用 `examples/data` 中的确定性 fixture。默认只输出紧凑报告;完整命令 -stdout/stderr 保存在各场景的 `.work/run.*`,也可通过 -`PCHRONICLE_EXAMPLE_VERBOSE=1` 在终端展开。运行要求为 macOS 或 Linux、Cargo、 -Python 3 和 `jq` 等常见 POSIX 工具;pVisor 文件系统示例另需 macFUSE 或 FUSE3。 -`just examples-pvisor-filesystem` 运行依赖 FUSE 的 01/02; -`just examples-pvisor-portable` 运行不需要 FUSE 的 03/04。 - -建议从 `pvisor/01-filesystem-isolation` 开始,再进入 changeset management;需要多 Run -生产时运行 orchestration layer 示例;准备从执行进入历史时,再运行 pChronicle 示例。 - -任务解释见 [pVisor Guides](../pvisor/guides/index.md),多 Run 工作流见 -批量 Run 工作流见 -[pChronicle Guides](../pchronicle/guides/index.md)。示例用于验证产品路径;精确命令语法仍以 -各产品 Reference 为准。 +参见 [pChronicle 指南](../pchronicle/guides/index.md),以及仓库中的 +`examples/pchronicle/` 与 `examples/data/`。 diff --git a/docs/src/zh/project/index.md b/docs/src/zh/project/index.md index 33dadecfe..728077dc1 100644 --- a/docs/src/zh/project/index.md +++ b/docs/src/zh/project/index.md @@ -1,6 +1,6 @@ # Project -Persisting 的公开产品主路径是 pVisor 与 pChronicle。这一节记录交付状态、稳定决策、贡献者 +Persisting 的公开产品是 pChronicle。这一节记录交付状态、稳定决策、贡献者 工作流,以及不在当前主路径中的独立系统。 ## 架构 diff --git a/docs/src/zh/project/releasing.md b/docs/src/zh/project/releasing.md index 8a2ca8d7a..244cea4ca 100644 --- a/docs/src/zh/project/releasing.md +++ b/docs/src/zh/project/releasing.md @@ -6,9 +6,8 @@ PyPI。项目仍然以 Python wheel 交付,但不包含 PyO3 扩展,也不 每个平台 wheel 标记为 `py3-none-`,并包含: - Python `persisting` 包; -- 原生命令行脚本; +- 原生 `pchronicle` 命令; - 捆绑的 pChronicle Web 资源; -- pVisor 所需的平台 libkrun firmware payload。 当前发布集包含 Linux x86_64 和 Apple Silicon macOS wheel。源码分发不是已 发布产物的一部分。 @@ -51,17 +50,11 @@ GitHub 中不存放 PyPI API token。pending publisher 可以在首次成功上 ## 构建与校验路径 PEP 517 backend 是 setuptools,配合仓库自有的 -`scripts/packaging/build_backend.py`。组装 wheel 之前,它会构建三条 Rust -CLI、暂存 firmware,并确保 Dioxus bundle 存在。`setup.py` 把 wheel 标为 +`scripts/packaging/build_backend.py`。组装 wheel 之前,它会构建 pChronicle Rust +CLI,并确保 Dioxus bundle 存在。`setup.py` 把 wheel 标为 平台相关,同时把 Python 与 ABI tag 保持为 `py3-none`。 -打包脚本会拉取 pinned 的 libkrun firmware 归档(Linux x86_64 与 Apple -Silicon macOS),除非 `PERSISTING_LIBKRUNFW_PATH` 指向已有 payload。本地 -wheel 构建必须走这些受支持路径之一;缺少 payload 是构建错误,而不是不完整 -的 wheel。 - -Linux wheel 使用 manylinux_2_28 / glibc 2.28 标签。当前 rustc libstd 和 -libkrun 的 virtiofs passthrough 需要 `statx` 与 `copy_file_range`, +Linux wheel 使用 manylinux_2_28 / glibc 2.28 标签。当前 rustc libstd 需要 `statx` 与 `copy_file_range`, manylinux2014 上没有这些符号。 每个 wheel 都会检查组件集和安装时 CLI smoke test。发布集检查随后要求每个 diff --git a/docs/src/zh/pvisor/concepts/agentvisor.md b/docs/src/zh/pvisor/concepts/agentvisor.md deleted file mode 100644 index 6e6f6fbbf..000000000 --- a/docs/src/zh/pvisor/concepts/agentvisor.md +++ /dev/null @@ -1,294 +0,0 @@ -# 什么是 AgentVisor? - -!!! note "阅读说明" - - 本文定义 **AgentVisor 品类**,不是 pVisor 的功能清单或交付承诺。当前可完成的能力以 - [快速开始](../get-started.md) 与 [指南](../guides/index.md) 为准。 - -**AgentVisor 是虚拟化 Agent 执行的 Hypervisor。** - -它把个人电脑、工作站或集群中的计算、文件系统、网络、模型、工具、凭据和持久状态 -组织成共享资源池,再为每个 Agent Run 提供一个相互隔离的 **Agent 虚拟执行环境**。 -不同 Agent 可以复用同一套底层环境资源,但各自拥有独立的身份、工作区、权限、状态、 -外部 Effect 和故障边界。 - -![Agent 基础设施中的 AgentVisor 品类](../../../assets/diagrams/agentvisor/agentvisor-stack.svg) - -## AgentVisor 的定义 - -> **AgentVisor** 是面向 Agent 的虚拟化层:它将共享的环境资源映射为相互隔离、可治理、 -> 可暂停和可迁移的 Agent 虚拟执行环境,并在多个 Agent 之间负责资源复用、执行隔离、 -> 生命周期、权限、状态和外部 Effect。 - -传统 Hypervisor 向操作系统提供虚拟机;AgentVisor 向 Agent 提供虚拟执行环境。这个 -环境不是一种新的机器镜像格式,而是 Agent 看到的完整执行边界,包括: - -- 可调度的计算、内存和加速器; -- 独立的工作区、进程、网络和工具空间; -- 可被委托且受限的模型、数据、Secret 与外部服务访问; -- 可暂停、恢复、Fork 和迁移的执行状态; -- 可隔离、审查、提交或补偿的外部 Effect; -- 跟随 Run 延续的身份、Lineage 与 Evidence。 - -AgentVisor 可以把这个虚拟环境映射到本地进程、操作系统 Sandbox、Container、MicroVM、 -机密计算环境或远程集群。底层 Kernel、Node 和 Scheduler 可以变化,Agent 所看到的 -Run identity、能力、Checkpoint、Effect 语义和责任边界保持稳定。 - -## 为什么现在需要 AgentVisor - -传统软件通常要经过一个由人控制的边界才会行动:用户点击按钮、运维人员执行部署, -或者 API 调用方提交一个范围明确的请求。自主 Agent 则可能在数分钟、数小时甚至数天 -内形成连续的决策链。在这段时间里,它可能: - -- 读取私有上下文并取得临时凭据; -- 修改代码、文档、基础设施或业务记录; -- 动态选择并调用模型与工具; -- 创建子进程,或把任务委托给其他 Agent; -- 与人、组织和外部服务通信; -- 暂停、恢复、分支,并从积累的状态继续执行; -- 留下比任何参与进程都更长寿的外部效果。 - -这里缺少的是面向 Agent 的统一虚拟化层。Sandbox 可以隔离进程,却不决定哪些结果可以进入现实; -工作流引擎可以编排步骤,却无法证明直接网络与文件路径受到约束;模型网关可以代理 -推理,却不拥有子进程、工具和工作区;可观测平台可以在事后记录事件,却不能在事件 -发生前治理它们。 - -AgentVisor 将这些离散能力组合成一个可以被创建、调度、暂停、迁移和回收的 Agent -虚拟执行环境,并允许多个 Agent 安全地共享底层资源。 - -## AgentVisor 位于哪里 - -AgentVisor 是一个边界明确、可以组合的基础设施层。它不会替代周边系统,而是把它们 -组织进同一个自主执行边界。 - -| 相邻品类 | 主要负责什么 | AgentVisor 增加什么 | -| --- | --- | --- | -| Agent framework | 推理循环、Prompt、工具适配和应用逻辑 | 与 framework 无关的 Run identity、权力、Effect、连续性和证据 | -| 模型网关 | 模型路由、认证、配额和推理遥测 | 横跨模型、工具、文件、进程与网络的统一策略上下文 | -| 工作流引擎 | 依赖图、重试和定时步骤 | 自主 Run 语义、Effect 边界、Checkpoint 与因果谱系 | -| Sandbox / Container / VM runtime | 进程和 Kernel 隔离 | 面向 Agent 的 capability admission、结果提升和跨底座身份 | -| 策略引擎 | 计算允许或拒绝的决策 | 把策略绑定到具体 Run、enforcement mechanism 和实际结果 | -| 可观测平台 | 日志、Trace、Metric 与分析 | 持久 Action/Effect identity,以及“实际 enforce 了什么”的证据 | -| Secret manager | 凭据保存和签发 | Run-scoped 委托、交付、过期和使用证据 | - -“Agent Operating System”可以描述完整的开发者或企业平台。AgentVisor 是其中更精确 -的基础设施品类:专门监督自主执行及其现实后果的那一层。 - -## 六项核心职责 - -任何可信的 AgentVisor 都必须同时回答六个彼此关联的问题。 - -### 1. Identity 与生命周期 - -持久执行单元是 **Agent Run**,不是进程或容器。同一个 Run 可能因为重试、恢复、 -迁移或 placement 变化产生多个物理 Attempt,但 Run 始终保留同一个身份,以及与 -父级、子级和 Checkpoint 的因果关系。 - -生命周期包括 admission、启动、观测、quiescence、取消、恢复、终态发布和保留策略。 -如果 Effect、证据或持久状态仍然不明确,即使进程以零退出码结束,也不能认为 Run -已经真正完成。 - -### 2. 被委托的权力 - -Agent 获得的应该是显式、有限的权力,而不是用户的全部 ambient authority。权力可以 -覆盖模型、工具、文件系统区域、网络目标、Secret、子进程、财务额度、通信渠道和计算 -预算。 - -AgentVisor 把这些权力绑定到 Run 及其当前 Attempt:判断请求能否被接纳,选择能够 -实现承诺的 enforcement mechanism,并禁止静默退化到更弱的边界。 - -### 3. Effect 治理 - -“允许 Agent 在 Run 内执行”与“允许执行结果进入现实世界”是两个不同决策。Agent -可以在被容纳的 Run 中自由探索、生成和修改,而不自动获得改变真实环境的权力。 - -AgentVisor 观测并分类 Effect,在介质允许时暂存它们,再通过策略决定提升、拒绝或 -补偿。文件修改只是其中一种。消息、支付、部署、工单、数据库写入和工具 mutation -属于同一个概念平面,只是可逆性不同。 - -### 4. 连续性与分支 - -Agent 积累的不只是内存页,还包括对话状态、工作区变更、工具状态、未解决 Effect、 -Credential、Artifact 和因果历史。AgentVisor 定义一种**语义 Checkpoint**,明确这些 -内容中哪些已经存在、缺失、仍然开放或已经提交到外部世界。 - -该 Checkpoint 可以支持暂停/恢复、故障恢复、Fork、Replay、评测与迁移,同时不假装 -所有外部系统都能被回滚。 - -### 5. 证据与责任 - -声明了策略,并不等于策略真的得到 enforce。AgentVisor 为每个相关维度记录实际采用 -的机制和结果,使系统能够回答: - -- 是哪个 Agent、Run、Attempt 和 authority generation 在行动? -- 涉及了哪些代码、模型、工具、Artifact 与环境? -- 哪些访问被请求、允许、拒绝,哪些仍然可能绕过? -- 哪些 Effect 被观测、暂存、提升、拒绝或补偿? -- 执行发生在哪里,实际安装了什么边界? -- 为什么该 Run 被判定为完成、失败或取消? - -### 6. Placement 可移植性 - -执行 Provider 应该可以被替换,而不需要重写 Agent 语义。本地进程、操作系统 Sandbox、 -容器、MicroVM、机密计算环境和远程集群可以提供不同的安全与性能特征,同时消费同一 -套逻辑 Run 与 authority model。 - -可移植不意味着所有 Provider 等价,而意味着差异必须显式、admission 必须理解能力, -并且证据始终跟随 Run。 - -## AgentVisor 的核心对象 - -行业只有先拥有共同词汇,不同实现之间才可能互操作。 - -| 对象 | 品类层含义 | -| --- | --- | -| **Agent Run** | 一次对用户有意义、身份稳定、意图持久的自主执行 | -| **Attempt** | Run 在特定 Provider 和 ownership generation 上的一次物理实现 | -| **Capability Grant** | 按资源、动作、条件、额度与生命周期限定的委托权力 | -| **Effect** | 具有稳定身份和生命周期、对外部世界有意义的观测或 mutation | -| **Checkpoint** | 横跨 Agent state、workspace、Effect 和 Artifact 的一致性前沿 | -| **Lineage** | Run、Checkpoint、委托、Artifact 与派生结果之间的因果关系 | -| **Evidence Bundle** | 描述执行、enforcement、Effect 和终态结果的持久事实集合 | -| **Execution Provider** | 实现 Attempt,并报告自身能力与证据的执行底座 | - -这套对象模型刻意不依赖特定协议、数据库、容器格式、Cloud 或 Agent framework。 - -## 治理 Effect 闭环 - -![AgentVisor Effect 治理闭环](../../../assets/diagrams/agentvisor/effect-governance.svg) - -Effect 治理从执行前开始,在结果得到确认后才结束。完整生命周期包括: - -1. **Intent**:Agent 或工具描述准备执行的动作。 -2. **Admission**:策略结合 Run、Capability、资源、上下文和预算进行判断。 -3. **Execution**:Enforcement point 允许、拒绝或转换动作。 -4. **Observation**:以稳定 identity 记录实际结果。 -5. **Containment**:介质允许时,让结果保持隔离或 pending。 -6. **Promotion**:策略或人把 Effect 接受进真实系统。 -7. **Compensation**:无法真正 rollback 时,用新的动作抵消已经提交的 Effect。 -8. **Evidence**:完整决策与结果进入 Run 可问责的历史。 - -Effect 的可逆性并不相同: - -| Effect 类型 | 例子 | 适合的控制方式 | -| --- | --- | --- | -| 可逆 | Workspace 文件、生成 Artifact、隔离分支 | 暂存、审查、提升、丢弃 | -| 事务型 | 数据库事务、部署计划、支持 prepare/commit 的 API | 预留、验证、原子提交 | -| 可补偿 | 创建工单、Cloud resource、可反向操作的业务动作 | 提交并持久化 compensation plan | -| 不可逆 | 外部消息、泄露的 Secret、物理动作、已结算支付 | 强 admission、显式 authority、最小权限、完整证据 | - -把所有行为统称为“已经 Sandbox”会掩盖这些差异。AgentVisor 让差异成为产品模型的一部分。 - -## Authority 是多维的 - -安全不能被压缩成“Sandboxed”“Containerized”或“Running in a VM”这样的单一标签。 -同一个 Run 对文件系统读取、文件系统写入、网络出口、Secret、子进程、设备、工具、 -模型和资源预算可能拥有完全不同的保证。 - -行业需要区分四种证据等级: - -| 等级 | 含义 | -| --- | --- | -| **Declared** | 存在策略意图,但没有证明任何 mediation 或 enforcement | -| **Mediated** | 正常集成路径经过控制点,但仍可能存在绕过路径 | -| **Enforced** | 在声明的威胁模型内,作用域中的行动者无法绕过该机制 | -| **Attested** | Enforcement evidence 与精确 Run、Provider、软件身份和 authority generation 形成密码学绑定 | - -AgentVisor 应当按维度独立报告证据,并在请求的保证无法实现时拒绝 Run。静默降级会让 -所有被委托的权力失去意义。 - -## 从个人设备到集群 - -![AgentVisor 执行连续体](../../../assets/diagrams/agentvisor/execution-continuum.svg) - -AgentVisor 对个人电脑和多租户集群同样重要。 - -在个人设备上,它可以让 Agent 在受控工作区里拥有足够自由,从而消除每一步都请求 -Approve 的打断,同时保留对真实环境中结果提升的控制。 - -在团队和集群中,同一个 Run identity 与 Effect 语义可以进一步结合调度、Lease、 -Node attestation、Tenant isolation、组织策略、共享 Artifact 和持久 Reconciliation。 - -可移植的执行单元不一定是一台正在运行的 VM,而是以下内容的组合: - -- Run identity 与意图; -- 被委托的 authority; -- 语义 Checkpoint 与 lineage; -- Effect frontier; -- Content-addressed artifact; -- Enforcement 与结果证据。 - -因此,从本地到集群首先是语义问题,其次才是机器传输问题。 - -## 这个品类的设计原则 - -1. **内部自主,边界受控。** Agent 的高频决策不应该对应高频人工批准。 -2. **Authority 跟随 Run。** 权力不能依附于偶然的进程、Shell、Node 或 Container identity。 -3. **Effect 是一等对象。** 外部后果需要 identity、state、policy 和 evidence,而不只是日志。 -4. **证据优先于标签。** 具体机制和威胁模型比笼统的“安全”或“Sandboxed”更有意义。 -5. **禁止静默弱化。** Placement 与恢复不能把已有 Run 重新解释到更弱的边界。 -6. **Checkpoint 必须是语义的。** 它声明 Agent state 与 Effect 的一致性,而不只是内存快照。 -7. **Provider 必须可替换。** 品类位于 Kernel、Container、VM、Cloud 与 Scheduler 之上。 -8. **终态必须可问责。** Effect 或终态证据仍不明确时,Run 就没有真正完成。 - -## 成熟度模型 - -AgentVisor 产品应该按能力而不是营销措辞来评价。 - -| 等级 | 名称 | 最低特征 | -| ---: | --- | --- | -| 0 | Observed Agent | 拥有稳定 Run identity 与关联日志,但不治理 authority 和 Effect | -| 1 | Supervised Agent | 隔离的虚拟执行环境、生命周期控制、取消、有限资源与显式 execution placement | -| 2 | Governed Agent | 多维 capability enforcement 与一等 Effect lifecycle | -| 3 | Portable Agent | 语义 Checkpoint、Lineage、Provider-independent Attempt 与 local-to-fleet 连续性 | -| 4 | Accountable Agent | Attested enforcement、持久 Effect reconciliation、多租户隔离与可验证终态证据 | - -Level 0 是有价值的基础设施,但还不足以代表完整 AgentVisor 品类。到 Level 2,系统开始 -同时控制被委托的权力和现实 Effect,这个品类才真正形成差异。 - -## 什么样的产品才是 AgentVisor - -一个产品要进入这个品类,至少要能够回答: - -- Agent 是否拥有独立于进程 placement 的稳定 Run identity? -- 多个 Agent 是否可以在保持身份、状态、权限和故障隔离的同时共享底层环境资源? -- 被委托的 authority 是否显式、有限,并绑定到该 Run? -- Enforcement claim 是否按 capability 维度拆分,并由具体 evidence 支撑? -- 对外部世界有意义的 Effect 是否在 commit 前后都被建模? -- Run 能否在明确的语义前沿暂停、恢复或 Fork? -- Lineage 与 evidence 能否跨 execution provider 延续? -- 故障后能否 reconcile 终态,而不静默重复不可逆工作? - -单独一个 Sandbox 不是 AgentVisor。模型代理、工作流引擎、Tracing 产品、权限弹窗和容器 -调度器也都不是。但它们都可以成为 AgentVisor 架构中不可或缺的 Provider。 - -## 行业可以在哪里形成标准 - -AgentVisor 不需要只有一种实现,但行业可以围绕以下接口形成开放标准: - -- 可移植的 Agent Run envelope 与 identity model; -- Capability dimension 与 constraint 词汇; -- Provider capability discovery 与分维度 enforcement evidence; -- Effect identity、lifecycle、promotion 与 compensation record; -- Semantic checkpoint manifest 与 effect frontier; -- Parent Run、Delegated Agent、Artifact 和 Tool 之间的因果 lineage; -- 可以脱离生产平台独立验证的 evidence bundle; -- 面向个人、企业与多租户环境的 conformance profile。 - -这一层实现标准化之后,Agent framework 仍然可以快速创新,Execution runtime 仍然可以 -深度专业化,而组织不必因为更换基础设施就放弃 authority、continuity 与 accountability。 - -## 品类定义 - -Hypervisor 让多个操作系统安全地共享机器;AgentVisor 让多个 Agent 安全地共享执行 -环境。它向下统一异构计算与隔离底座,向上提供稳定的 Agent 虚拟执行环境,并把权限、 -状态、Effect 和 Evidence 纳入同一个虚拟化边界。 - -**AgentVisor,就是 Agent 执行的虚拟化基础设施。** - -## 从品类继续到产品 - -- [Run、Attempt 与 Effect](run-model.md)定义可迁移执行对象。 -- [Capability 与 Evidence](capabilities-and-evidence.md)定义权限和 enforcement 报告。 -- [pVisor Overview](../index.md)介绍 Persisting 对这个品类的实现。 -- [从本地到集群](../../system-design/local-to-fleet.md)解释 placement 变化时保持哪些契约。 diff --git a/docs/src/zh/pvisor/concepts/capabilities-and-evidence.md b/docs/src/zh/pvisor/concepts/capabilities-and-evidence.md deleted file mode 100644 index c633bf650..000000000 --- a/docs/src/zh/pvisor/concepts/capabilities-and-evidence.md +++ /dev/null @@ -1,28 +0,0 @@ -# Capability 与 Evidence - -Capability 是对某个资源和动作的有界权限。pVisor 按维度描述 capability,因为单一的 -`safe` 或 `sandboxed` 标签无法准确表达 Agent 环境。 - -| 维度 | 请求示例 | 应检查的 Evidence | -| --- | --- | --- | -| 文件读取 | 只读项目和工具链中的指定路径 | 可见 root 与实际安装的读取控制 | -| 文件写入 | 只写 staged workspace | 写边界与 promotion 决策 | -| 网络 | 只访问声明的目标 | 截获路径与抗绕过能力 | -| 进程 | 启动受限子进程 | namespace/profile 与继承句柄 | -| 凭据 | 使用一个短期身份 | 交付、过期和实际使用 | -| 工具与模型 | 调用声明的 endpoint | 策略决策与路由记录 | - -请求的权限与实际安装的 enforcement 是不同事实。必需维度无法满足时,admission 必须拒绝 -该 Provider。可选控制只有在 Run record 明确报告降级时才能弱化。 - -Evidence 依次回答四个强度不同的问题: - -1. **Declared**:请求了什么策略? -2. **Mediated**:哪些动作经过控制点? -3. **Enforced**:在声明的 threat model 中阻断了哪些绕过路径? -4. **Attested**:enforcement 是否绑定到这次 Run 和实际 Provider? - -具体执行的答案应从 Run Bundle 检查。返回 [pVisor 核心概念](index.md),通过 -[网络指南](../guides/network.md)配置一个 capability 维度,或阅读 -[pVisor 隔离设计](../design/isolation.md)了解平台机制。执行、编排和历史之间的完整 -信任链见[安全与 Evidence](../../system-design/security-evidence.md)。 diff --git a/docs/src/zh/pvisor/concepts/index.md b/docs/src/zh/pvisor/concepts/index.md deleted file mode 100644 index 520f617da..000000000 --- a/docs/src/zh/pvisor/concepts/index.md +++ /dev/null @@ -1,22 +0,0 @@ -# pVisor 核心概念 - -pVisor 围绕 **Agent Run** 构建,而不是围绕进程、Container 或虚拟机。比较 Provider 或 -解释 Run Bundle 前,先理解这些概念。 - -!!! note "什么时候阅读这里" - - 完成第一次 Run 后,如果你需要知道实际隔离了什么、哪些修改仍在 stage 中,或为什么不同执行 - Provider 的保证不同,就从这里开始。 - -请按以下顺序阅读: - -1. [Run、Attempt 与 Effect](run-model.md) 定义一次执行、它的尝试和产生的修改。 -2. [Capability 与 Evidence](capabilities-and-evidence.md) 解释请求如何变成实际机制,以及如何 - 写入 Run Bundle。 -3. [什么是 AgentVisor?](agentvisor.md) 解释 pVisor 所属的产品类别,以及 Agent 与运行时之间的边界。 - -AgentVisor 是品类定义,不是 pVisor 的功能清单;Run 和 capability 文章定义 pVisor 稳定的用户模型;平台机制与 -当前缺口属于 [pVisor Design](../design/index.md)。 - -读完本节后,你应该能查看 Run Bundle,并区分请求的 capability 与实际生效的 capability。 -准备做 Provider 或 policy 决策时,继续阅读[实用指南](../guides/index.md)。 diff --git a/docs/src/zh/pvisor/concepts/run-model.md b/docs/src/zh/pvisor/concepts/run-model.md deleted file mode 100644 index baa5b3e02..000000000 --- a/docs/src/zh/pvisor/concepts/run-model.md +++ /dev/null @@ -1,45 +0,0 @@ -# Run、Attempt 与 Effect - -pVisor 管理的是 **Agent Run**。Run 不是碰巧执行任务的进程,也不是某次执行选择的 -Container 或虚拟机。 - -## Run - -Run 是一次 Agent 任务稳定、对用户有意义的身份。它的 identity、请求的 capability、 -父子 lineage、已接受 Effect、Artifact 与终态结果不会因为 executor 切换或进程退出而消失。 - -## Attempt - -Attempt 是 Run 在某个 Provider 上的一次物理实现。基础设施故障可以创建新的 Attempt, -而不改变 Run。语义重试代表一次新的决策,因此应创建派生 Run。 - -```text -Run -├── Attempt 1 → 基础设施失败 -└── Attempt 2 → 终态结果 -``` - -这个区分允许基础设施重试,同时让历史仍然可解释。 - -## Effect - -Effect 是 Agent reasoning loop 之外有现实意义的后果。文件修改、网络请求、工具调用、 -凭据使用和外部 API 修改属于不同维度。捕获 Effect 不等于阻止 Effect;一个维度被 stage, -也不代表另一个维度已经隔离。 - -staged workspace 的生命周期是: - -```text -execute → inspect stage → apply selected paths zero or more times → drop stage -``` - -`apply` 只提升选中的文件修改,不表示网络或远程服务 Effect 已经回滚。 - -## Checkpoint 与终态结果 - -Checkpoint 记录 Provider 能保存状态的一致性前沿。Run 终态结果记录最终状态,以及 Evidence -和 Artifact 的引用。两者都不应只从进程退出码推断。 - -返回 [pVisor 核心概念](index.md),或继续阅读 -[Capability 与 Evidence](capabilities-and-evidence.md),再通过 -[执行指南](../guides/execution.md)选择 Provider。 diff --git a/docs/src/zh/pvisor/design/cli.md b/docs/src/zh/pvisor/design/cli.md deleted file mode 100644 index 01eb1a645..000000000 --- a/docs/src/zh/pvisor/design/cli.md +++ /dev/null @@ -1,87 +0,0 @@ -# pVisor 命令模型 - -`pvisor` 是单个受治理 Agent Run 的公共入口。命令接口围绕四类责任组织:启动 Run、查看 -记录、决定 staged change 的去向,以及管理可复用环境。命令行与 `RunConfig` 描述同一个 -模型;配置文件必须显式传入,不会被当作隐式项目策略。 - -## 使用 `run` 启动 - -短写法与完整写法等价: - -```bash -pvisor -- codex -pvisor run --stage ./runs/task-001 -- codex -``` - -需要保留文件修改以便审查时使用 `--stage`。不指定 stage 时,pVisor 仍会记录 Run,但不会 -产生可供 apply 的持久 workspace changeset。选中的 host、container 或 VM Provider 会在 -Run Bundle 中分别记录实际 capability 与限制。 - -常用控制按目的分组: - -| 目的 | 选项 | 结果 | -| --- | --- | --- | -| Workspace | `--stage`、`--overlayfs-path`、`--overlayfs-compose` | 创建 COW 视图并保留 changeset | -| Runtime | `--executor host\|container\|vm`、`--rootfs`、`--container-image` | 选择执行 Provider 与 rootfs | -| Network | `--overlaynet-deny-all`、`--overlaynet-allow`、`--overlaynet-limit` | 请求 deny、allowlist 或限速策略 | -| Gateway | `--gateway-mode`、`--gateway-route`、`--gateway-level` | 配置路由,并按需捕获模型流量 | -| Limits | `--timeout`、`--memory`、`--max-processes`、`--max-open-files` | 在 Provider 支持时限制 Attempt | -| Configuration | `--spec`、`--name`、`--pass-env` | 提供 RunSpec、身份和显式环境变量 | - -Provider 选择不会改变 Run 契约,只会改变各 capability 维度的 enforcement 机制;最终 -Evidence 会分维度记录。 - -## 查看并决定 - -完成后的 Run 仍然是一个记录,staged effect 必须显式接受或丢弃: - -```bash -pvisor review last -pvisor inspect last -- git status --short -pvisor apply last --path src -pvisor apply last --include 'tests/**' --exclude 'tests/generated/**' -pvisor apply last --all -# 或:pvisor drop last -``` - -`review` 解释 Run Bundle 与 staged change;`inspect` 在 Run 视图中执行只读命令;`apply` -提交选定路径并保留其余内容;`drop` 丢弃 stage。两者都不会修改正在运行的 Run。重置会 -创建新的 stage generation,避免旧 metadata 覆盖新的决定。 - -## Checkpoint 与 Fork - -Checkpoint 保证停止一致的 filesystem 与 AgentCtl safe point,但不保存进程内存: - -```bash -pvisor checkpoint last --name before-experiment -pvisor fork last --checkpoint before-experiment -- codex -``` - -Checkpoint 会等待参与的 AgentCtl session 进入 quiesce,记录 upper layer 与 lineage,然后 -恢复 Run。 - -## 可复用环境 - -`env` 为具名 stage 提供跨命令的稳定生命周期: - -```bash -pvisor env create dev --target ./project -pvisor env exec dev -- make test -pvisor env shell dev -pvisor env inspect dev -- git status --short -pvisor env apply dev --path src -pvisor env drop dev -pvisor env delete dev --force -``` - -Environment 是持久 stage,不是常驻 VM。`start` 与 `stop` 控制是否接受新的 session; -`apply` 与 `drop` 完成决定后会推进 stage generation。 - -## 配置优先级 - -`--spec` 接受 TOML `RunConfig` 或准备好的 JSON `RunSpec`。显式 scalar 选项覆盖文件值; -重复的 list 选项替换整个列表;`--` 后的命令替换 `run.command`。`--container-image` 与 -`--rootfs` 可以推断匹配的 executor;自动化场景仍建议显式指定 `--executor`。 - -公共工作流保持简单:启动 Run,检查 Evidence,然后明确决定 staged effect 的去向。Provider -行为见[执行环境](../guides/execution.md),完整选项见 [CLI 参考](../reference/cli.md)。 diff --git a/docs/src/zh/pvisor/design/gateway.md b/docs/src/zh/pvisor/design/gateway.md deleted file mode 100644 index ecfcc974a..000000000 --- a/docs/src/zh/pvisor/design/gateway.md +++ /dev/null @@ -1,549 +0,0 @@ -# pVisor Gateway — 架构与设计 - -本文负责模型路由、协议适配、非阻塞 capture 与 event emission。如何捕获一次 Run 属于 -[Capture 指南](../guides/capture.md);capture 之后的事实与投影 ownership 属于 -[pChronicle 轨迹存储](../../pchronicle/design/trajectory-storage.md)。 - -> **读者**:需要在 Agent 与 LLM 之间落地**可观测、可回放、可审计**轨迹的平台工程师、架构师与集成方。 -> **版本**:1.1(对外)  |  **最后更新**:2026-07-30 - -本文描述 **Persisting Gateway** 的产品定位、核心概念与架构取舍。实现细节(块格式字段表、CLI 参数、目录布局)见文末延伸阅读;文中尽量避免绑定具体源码路径。 - ---- - -## 目录 - -1. 摘要 -2. 问题与价值 -3. 设计原则 -4. 核心概念 -5. 系统全景 -6. 数据流:从 HTTP 到轨迹(含 §6.4 多模态) -7. 存储与一致性 -8. 网关与协议 -9. 多 Agent 与会话 -10. 可靠性与运行形态 -11. 演进方向 -12. 延伸阅读 - ---- - -## 1. 摘要 - -**Persisting Gateway 是 coding agents 的轨迹观察层**:让 **Claude Code** 或 **OpenAI Codex** 通过 `persisting-overlaynet` 的本地显式代理运行,即可得到可回放的事件流,并由 pChronicle 完成结构化落盘。 - -主链路: - -```text -HTTP ──► events 流 - ├─ 记录(append → events.lance,SoT) - └─ 触发(订阅 / handler) - └─ 格式转换(经 storyline hub)+ 落盘 - (agenticmd / atif / openai_msg / …) -``` - -它是 overlaynet 代理之上的可嵌入 **事件观察器与状态机**。在已支持的客户端上,通过 `pvisor run` 注入代理或显式设置模型 API 地址,即可在**不修改业务代码**的前提下: - -- 透明转发对话流量到上游模型; -- 把每次 HTTP 交换写入 **events 流**(可持久化、可回放); -- 由 events 上的订阅触发物化与导出(Markdown、ATIF、openai_msg 等),而不是在代理路径里硬编码多种格式。 - -Gateway 不是通用企业 API 网关的替代品,也不拥有网络数据面的实现;它作为 OverlayNet sink,围绕 **Agent 轨迹(trajectory)** 解释代理交换、转发协议并生产事件。 - ---- - -## 2. 问题与价值 - -### 2.1 典型痛点 - -| 痛点 | Gateway 的回应 | -|------|----------------| -| Agent 对话散落在各厂商 API 形态中,难以统一分析 | 归一为统一事件记录,再物化为对话视图 | -| 只要日志不要改代码 | 反向代理 + 环境注入(`pvisor run`) | -| 需要给人 review 的会话稿 | TLV Markdown:正文可读,元数据在注释中 | -| 流式输出想「边生成边看见」 | Live Markdown upsert(草稿块 → 定稿块) | -| 子 Agent、多 session 易混 | 按故事线分文件 + spawn 关联,不内联全文 | -| 采集不能拖慢 LLM 首 token | **观测不阻断**:采集异步化,失败进 dead letter | - -### 2.2 客户端支持(实时采集) - -| 客户端 | `pvisor run` 实时采集 | 说明 | -|--------|:----------------------:|------| -| **Claude Code** | ✅ | 主适配目标:Anthropic Messages、subagent 分轨、history replay 去重 | -| **OpenAI Codex** | ✅ | Responses API 路径;通过 `-c openai_base_url=…` 等注入网关 | -| **Cursor** | ❌ | **当前版本不支持**(无官方注入与流量适配) | -| **自研 / 通用 OpenAI SDK** | ⚠️ | 若客户端走 `HTTP_PROXY` 或 `OPENAI_BASE_URL` / `ANTHROPIC_BASE_URL`,可尝试接入,无专项保证 | - -事后从 IDE 本地 JSONL **import** 的路径以 CLI 文档为准;Cursor 本地日志导入亦在规划中,与上表「实时采集」无关。 - -### 2.3 能力边界 - -**擅长** - -- `pvisor run` 内嵌 Gateway 对 **Claude Code / Codex** 的对话采集; -- Claude Code 场景的 history replay 去重、subagent 分轨; -- Codex 场景的 Responses ↔ Completions 桥接与上下文注入过滤; -- Lance 全量事件 + Markdown 物化视图的双层存储; -- 轻量模型路由、协议桥接(Messages / Completions / Responses 等)。 - -**不替代** - -- 多租户计费、复杂 RBAC、MCP/A2A 联邦等企业网关(可参考 [agentgateway](https://github.com/agentgateway/agentgateway) 类方案); -- 100+ 厂商的一站式 SDK(可参考 LiteLLM 类方案); -- 终端命令输出的 token 压缩(与 [RTK](https://github.com/rtk-ai/rtk) 等工具互补)。 - -### 2.4 在 Persisting 生态中的位置 - -```text -Agent 客户端 - │ HTTP - ▼ -┌─────────────────────────────────────┐ -│ Persisting Gateway │ -│ HTTP → events 流 │ -│ · 记录 → events.lance(SoT) │ -│ · 触发 → storyline → 格式落盘 │ -└──────────────┬──────────────────────┘ - │ events / 派生产物 - ▼ -┌─────────────────────────────────────┐ -│ pChronicle / 分析 / 检索 │ -└─────────────────────────────────────┘ -``` - ---- - -## 3. 设计原则 - -| 原则 | 含义 | -|------|------| -| **观测不阻断** | 用户请求的延迟与成功率优先;采集失败写入 dead letter,**不**因写盘失败而中断 HTTP 响应。 | -| **HTTP → events** | 代理主产物是 **events 流**(HTTP-first wire);不是直接写 Markdown / ATIF。 | -| **记录与触发分离** | 同一条 event 可 **append 落盘**,也可 **fan-out 触发**下游 handler;二者解耦。 | -| **转换经 hub** | 物化 / 导出经 **storyline**(ATIF-aligned)再落到各格式;禁止外围格式两两直转。 | -| **Lance 为事实源** | canonical 仅 `events.lance`;Markdown / ATIF 等是**派生落盘**,允许有损。 | -| **单一写入门** | 进入 Lance 的 append 经统一引擎路径,避免双写竞态。 | - ---- - -## 4. 核心概念 - -### 4.1 主链路 - -```text -Agent HTTP - │ - ▼ -overlaynet proxy(CONNECT / 转发 / 网络策略) - │ - ▼ -Gateway Sink(LLM 协议适配 + 发出轨迹观测) - │ - ▼ -events 流 ────────────────────────────────────────┐ - │ │ - ├─ 记录 append ──► events.lance(SoT / replay) │ - │ │ - └─ 触发 handler ──► interpret / fold │ - │ │ - ▼ │ - storyline(hub) │ - │ │ - ┌────────────┼────────────┐ │ - ▼ ▼ ▼ │ - agenticmd atif openai_msg … │ - │ │ │ │ - └──────── 落盘 / 物化 ─────┘ │ - │ -(可选)从 Lance 重放 ──────────────────────────────┘ -``` - -要点: - -1. **overlaynet 负责代理机制**:请求分类、CONNECT、绝对 URI 转发、出口策略与连接计数。 -2. **Gateway Sink 负责业务语义**:LLM 路由/协议转换、session 关联与 capture event,不实现第二套代理。 -3. **events 流是总线**:可记录、可订阅;同一条记录可同时落盘与触发。 -4. **格式转换与落盘是下游**:经 storyline hub,输出 agenticmd / atif / openai_msg 等。 - -辅助坐标(会话边界,非 SoT): - -| 概念 | 说明 | -|------|------| -| **Run** | 一次 `pvisor run` / 根工作区 | -| **session** | 一条 Agent 会话线(≈ ATIF `session_id` / storyline `session`) | -| **call_id** | 关联同一次 HTTP 往返的 request/response(events 信封字段) | - -### 4.2 分层 - -```text -┌─────────────────────────────────────────────────────────────┐ -│ 协议层:HTTP、SSE、OpenAI / Anthropic / Responses │ -│ 职责:转发、翻译;发出 HTTP-first 观测 │ -└───────────────────────────┬─────────────────────────────────┘ - │ emit - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ events 流 │ -│ 职责:有序事件;append 记录;fan-out 触发 │ -└───────────────┬─────────────────────────┬───────────────────┘ - │ record │ trigger - ▼ ▼ - events.lance handlers(interpret) - │ - ▼ - storyline → 各格式落盘 -``` - -**Ingress**:协议层 → events(尽量保留 wire;摘要字段可选)。 -**Egress**:events 重放 / 订阅 → storyline → 派生格式落盘;与采集主路径解耦。 - -![Gateway event 写入与派生数据流](../../../assets/diagrams/persisting/gateway-dataflow.svg) - -### 4.3 写路径与派生路径 - -| | 写路径(记录) | 派生路径(触发) | -|---|--------|--------| -| **输入** | Proxy / import 发出的 event | 已进入流的 event(实时或重放) | -| **输出** | `events.lance` append | storyline 及 agenticmd / atif / … | -| **失败策略** | dead letter;不阻断 HTTP | 独立重试;不影响 SoT | -| **保真** | HTTP-first,目标可回放 | 允许有损折叠 | - -Live Markdown、轮次索引等视为 **events 触发的一类 handler**,不是与 events 并列的第二事实源。 - -## 5. 系统全景 - -### 5.1 逻辑组件 - -```text - ┌──────────────┐ - │ Agent 进程 │ - └──────┬───────┘ - │ HTTP(S) - ▼ - ┌────────────────────────┐ - │ Capture Proxy │ - │ · 路由 / 鉴权 │ - │ · 协议桥 / 流式转发 │ - │ · emit → events 流 │ - └───────────┬────────────┘ - │ - ┌───────────────┼───────────────┐ - ▼ ▼ ▼ - ┌────────────┐ ┌────────────┐ ┌────────────┐ - │ events 引擎 │ │ 上游 LLM │ │ 会话索引 │ - │ · 记录 │ │ │ │ │ - │ · 触发 │ └────────────┘ └────────────┘ - └──────┬─────┘ - │ - ┌─────┴──────────────────┐ - ▼ ▼ - events.lance handlers → storyline - (SoT) → agenticmd / atif / … 落盘 -``` - -| 组件 | 职责 | -|------|------| -| **Proxy** | 唯一 HTTP 入口;转发上下游;把观测 **emit 进 events 流**(不直接写多种格式)。 | -| **events 引擎** | 维护有序流:**记录**(append Lance)与 **触发**(fan-out handlers)。 | -| **记录路径** | WAL → per-session 有序 apply → `events.lance`。 | -| **触发路径** | 订阅 events → interpret → storyline → 各格式落盘 / Live Markdown。 | -| **会话索引** | 轻量 `sessions.json`:列表、token、费用估算。 | -| **对账与 dead letter** | SoT 与派生落盘一致性;失败事件可重放。 | - -### 5.2 集成方式(概念) - -- **库嵌入**:Rust 工程可挂载 OverlayNet 与 Gateway sink,并自行提供轨迹 event sink。 -- **CLI**:`pvisor run` 包装子进程并管理 Run-scoped Gateway 生命周期。 -- **配置**:TOML 声明监听地址、模型路由、采集级别、存储根目录;无需改 Agent 源码。 - -公开 API 以**模块边界**发布(代理、引擎、记录、轨迹、会话),避免扁平导出 hundreds 个符号;故事读模型主要通过快照与对账产物对外可见。 - -### 5.3 与 agentgateway 的关系 - -Gateway 在**配置语义与路由模型**上借鉴 agentgateway 子集,并可用其 fixture 做协议回归;**运行时互不依赖**。定位差异:agentgateway 面向集群级多协议网关;Persisting Gateway 面向**单点嵌入的轨迹事实源**。 - ---- - -## 6. 数据流:从 HTTP 到轨迹 - -主路径:**HTTP → events 流 →(记录 | 触发)→ 落盘**。 - -### 6.1 一次对话请求(概念时序) - -![一次对话请求的 Gateway capture 时序](../../../assets/diagrams/persisting/gateway-request.svg) - -要点: - -1. **Proxy 不等待**派生落盘完成再响应;先 emit,再继续转发。 -2. **草稿默认只触发 handler**(如 Live Markdown);完整响应才 **记录**进 Lance,避免 partial 污染 SoT。 -3. 派生格式(agenticmd / atif / …)一律经 **storyline**;可实时触发,也可事后从 Lance 重放再触发。 - -### 6.2 采集事件与记录类型 - -写路径用少量**事件种类**驱动一切持久化: - -| 事件 | 典型效果(Dialogue 级别) | -|------|---------------------------| -| 请求到达 | Lance:请求记录;Markdown:user 块 | -| 流式草稿 | 仅 Markdown:assistant 草稿(可原地覆盖) | -| 响应完成 | Lance:流式/完整响应记录;Markdown:定稿 assistant | -| 调用取消 | 仅 Lance:取消记录 | -| Spawn 关联 | Lance + Markdown:关联元数据(不当作可跳过噪音) | - -**采集级别**(Summary / Dialogue / Full)控制记录粒度;生产默认 **Dialogue**: - -| 级别 | Lance / Markdown 摘要字段 | `payload.body` | -|------|---------------------------|----------------| -| `summary` | 仅 model、path、字节数 | ❌ | -| `dialogue`(默认) | `user_content` / `assistant_content` 可见对话文本 | ❌ | -| `full` | 同上 + 完整解析后的请求/响应 JSON | ✅ | - -省略无关探测流量(如 `count_tokens`、history replay)的规则与采集级别无关,由物化过滤统一处理。详见本页 6.4 节。 - -存储记录类型(`http.request` / `llm.request`、`llm.response.stream`、`session.*` 等)属于 **events 词汇**,由 Proxy emit;handler 再折叠为 storyline,不必与 HTTP 帧一一对应到对话轮。 - -#### 6.2.1 时间戳与顺序 - -每条进入 durable capture 的 `EventRecord` 都带有两种一致的观测时间: -`timestamp`(RFC3339 UTC)和 `timestamp_unix_ms`(Unix 毫秒)。请求事件使用请求被 -接受的时刻,响应事件使用响应被捕获的时刻;Gateway sink 是最后的共同写入边界,会为 -缺少时间字段的旧 producer 记录补齐这两个值。pVisor 产生的 runtime lifecycle event -也会同时写入这两种格式,两者必须在毫秒级一致。 - -事件顺序仍由 `source + seq` 定义;时间戳只用于墙上时钟关联、耗时展示和跨组件对齐, -不能替代 sequence ordering。不同 source 可以拥有各自独立的 `seq` 空间。 - -### 6.3 流式与人读视图 - -```text -助手输出: "你" → "你好" → "你好,我来帮你…" -Markdown: [草稿] → [覆盖草稿] → [定稿] -Lance: — — 一条最终响应事件 -``` - -- 草稿块带明确标记;定稿时按 **call + 角色** 覆盖同一块,避免重复段落。 -- 块头 schema 带版本号(`v: 1`),便于将来演进线格式而不改文件后缀。 - -详见 [AgenticMD 格式](../../pchronicle/reference/agenticmd.md)。 - -### 6.4 可见对话提取(含多模态) - -Gateway 在 **Dialogue** 级别下,从客户端原始 HTTP body(而非 upstream 转换后形态)提取「人读可见」正文,写入 `payload.user_content` / `payload.assistant_content`,并驱动 Markdown 块正文、frontmatter `turns` 与派生统计。 - -**统一入口**:`dialogue_extract` 模块;按 wire 协议分支: - -| 客户端 / API | 典型路径 | 用户输入 | 助手输出 | -|--------------|----------|----------|----------| -| Claude Code | `/v1/messages` | `content[]`:`text` / `image` / `tool_result` | SSE / JSON:`text` / `tool_use` | -| Codex | `/v1/responses` | `input[]`:`input_text` / `input_image` / tool 往返 | SSE / JSON:`output_text` / `function_call` / `image_generation_call` | -| OpenAI SDK | `/v1/chat/completions` | `messages[]`:`text` / `image_url` | `choices[].message` / 流式 delta | - -**多模态 Phase 0(当前)**:图像**不写入 blob**,仅在 dialogue 字符串中留占位符,保证 `turns` 计数与 review 时「知道有图」: - -| 方向 | 占位符示例 | -|------|------------| -| 用户输入(URL) | `[image: url:https://…]` | -| 用户输入(base64 / data URL) | `[image: base64:128KB image/png hash=abc…]` | -| 助手出图(Codex Responses) | `[image_generated: ig_xxx, png, 1024x1024, ~1MB]` + 可选 `prompt: …` | - -纯图无文字的用户 turn **仍计为 1 轮**(修复「有图无文 → stats 0 turns」)。 -`capture_level = full` 时完整 JSON 仍在 `payload.body`,但 Markdown 物化**仍只展示占位符**,不嵌入像素数据。 - -**后续(规划)**:sidecar 资产目录 `{run}/assets/{call_id}/…` + payload 引用;内部 -materializer 可以输出指向 `assets/…` 的 Markdown 图片。当前公共 `pchronicle` CLI 不为 -这项规划预留命令。见本页 11 节演进方向。 - -协议回归:`crates/persisting-gateway/tests/ag_fixture_tests.rs` + `tests/support/ag_capture_cases.rs`(agentgateway fixture 矩阵)。 - ---- - -## 7. 存储与一致性 - -> 双层存储、目录约定、materialize/import 路径见 [轨迹存储模型](../../pchronicle/design/trajectory-storage.md)。 - -### 7.1 双层存储 - -| | Lance(事实源) | Markdown(物化视图) | -|---|----------------|----------------------| -| **读者** | 程序、检索、replay | 人、git、review | -| **完整性** | 无损(在采集级别内) | 有损:过滤内部与重复 history | -| **写入** | append 到 `events.lance` | live upsert 或批量 append / 全量 materialize | -| **关系** | 行数 ≥ 块数(物化只减不增) | 从 Lance 重建可修复漂移 | - -### 7.2 物化过滤(统一策略) - -无论实时写入还是事后 materialize,**同一套规则**决定某条事件是否出现在 Markdown 中,例如: - -- 内部 `count_tokens`、影子模型预热; -- Claude Code 式 **history replay**(用户消息计数未增加的重发); -- 无可见正文的空记录; -- 纯生命周期、仅-cancel 类记录(保留在 Lance)。 - -Spawn 关联等「对人仍有意义」的事件**不会**被误杀。 - -### 7.3 会话摘要(Frontmatter) - -每个 Markdown 会话文件可带 YAML 摘要:`turns`、token、估算费用、子 Agent 列表、客户端信息等。 -**轮次数以故事读模型为准**,块内 `turn` 字段仅作展示启发式,不作为权威计数。 - -### 7.4 三轨对账(Reconcile) - -一次 Run 正常结束时,对每个 session 比对: - -| 轨道 | 含义 | -|------|------| -| **Markdown** | 物化块中的 call 集合 | -| **Lance** | 事件日志中应对话出现的 call 集合 | -| **Story** | 从事件重放得到的 call 集合 | - -三者一致且结构检查通过,才认为「人读视图与事实源对齐」。不一致时应用 materialize 或排查 dead letter,而非直接信任 Markdown。 - -### 7.5 辅助产物 - -| 产物 | 作用 | -|------|------| -| 事件 WAL | 进程崩溃后重放未确认的采集事件 | -| dead letter | 应用失败或 Lance 刷盘失败的留存与重放 | -| 故事快照 | 退出时固化各 Story 的轮次读模型,供摘要与恢复 | - ---- - -## 8. 网关与协议 - -Persisting Gateway 是一个**轻量 LLM 协议网关**,服务于「本地或团队固定上游 + 采集」,而非替代云厂商控制台。 - -| 能力 | 说明 | -|------|------| -| **模型路由** | 按配置顺序匹配模型名;支持前缀/通配与单跳 forward。 | -| **协议桥** | 例如 Anthropic Messages ↔ OpenAI Completions;Responses API 在非 OpenAI 上游时降级转换。 | -| **流式翻译** | 统一 SSE 形态;支持 TTFT 观测、推理字段缓存回放。 | -| **鉴权** | 配置文件、环境变量或客户端 Header 注入 API Key;按提供商约定选择 Header 名。 | - -网关逻辑严格停留在**协议层**,不进入故事层状态机,避免「路由规则」与「轮次语义」耦合。 - ---- - -## 9. 多 Agent 与会话 - -### 9.1 路由与存储键 - -每个 HTTP 请求绑定一条**采集路由**:逻辑 session、磁盘上的 storage 键(决定 `.md` 文件名与 Lance 事件日志路径)、可选 subagent 标识。 -Capture run 下,子 Agent 通常写入 `agent-{id}.md`;主会话写入 `run-{id}.md` 或扁平 session 名。 - -### 9.2 文件隔离不变式 - -- 子 Agent 正文只出现在 **agent-*** 文件; -- 主 Agent 的 spawn 引用与链接出现在 **run-*** 文件,**不内联**子 Agent 全文; -- 块头 JSON 承载机器可读关联;正文脚注仅辅助人读(解析 roundtrip 时会剥离脚注行)。 - -### 9.3 Spawn 关联 - -主 Agent 助手消息中的 spawn 提示与子 Agent 首包注册可能**时间错开**。系统用 Run 级注册表做延迟匹配与回填,使主会话在事后仍能看到「调用了哪个子 Agent、轨迹文件在哪」。 - -### 9.4 单 run dataset 多 `session_id`(Claude run bucket) - -一次 `pvisor run --record-format lance --record-destination WAREHOUSE` 的 pChronicle sidecar 通常在 run 目录写一个 -`events.lance/` dataset,但行内 `session_id` **可能混存多个值**。pVisor 不直接打开 -Lance: - -| 典型来源 | `session_id` 取值 | -|----------|-------------------| -| pVisor 生命周期 / Run 头 | `run-{uuid}`(与目录名一致) | -| Claude Code 对话 HTTP | header 注入的 UUID(与 run id 不同) | - -因此内部统计展开 run bucket(`session_id == root_session_id`)时,会先读 Lance 中 distinct -`session_id`,再**逐分区统计**,避免“第二个 session 显示 0 turns”。实现位于 -`persisting-pchronicle::expand_story_locations`;当前公共 CLI 通过 `analysis` 和 `query` -暴露统计能力。详见[轨迹存储](../../pchronicle/design/trajectory-storage.md)的 run bucket 分区说明。 - ---- - -## 10. 可靠性与运行形态 - -### 10.1 可靠性模型 - -```text -请求线程 ──► 发事件(WAL 非阻塞入队 + apply 入队)──► 继续转发 - │ - └──► 后台:有序 apply ──► pChronicle sidecar / Markdown - │ - ├─ 成功 → 确认 WAL - └─ 失败 → dead letter + 保留 WAL(重启重放,不影响 HTTP) -``` - -| 机制 | 目的 | -|------|------| -| **异步 apply** | 采集不占用上游连接线程 | -| **Blocking sink 隔离** | sidecar durable ACK wait 运行在 blocking pool,不占用 Gateway Tokio HTTP worker | -| **Per-story 有序队列** | 同一故事线内事件顺序可复现 | -| **事件 WAL** | 请求线程只做有界 `try_send`;后台最多等待 2 ms 合批并 `sync_data`,已落盘事件在崩溃后可重放 | -| **ACK WAL** | 异步 best-effort 合批;丢 ACK 只会导致安全重放,flush/shutdown barrier 保证已接收 ACK 先落盘 | -| **Barrier flush** | 优雅退出前排空队列与 Actor 邮箱 | -| **Dead letter** | 可运维重放,而非静默丢数 | - -已知限制(实现仍在加强):极端崩溃场景下 WAL 序号与重复投递策略、超长会话 Markdown 全文件 upsert 的 IO 成本等——见本页 11 节演进方向。 - -### 10.2 运行形态 - -| 形态 | 适用场景 | -|------|----------| -| **`pvisor run`** | 包装一次 Agent 命令(如 `claude`、`codex`);注入代理环境变量并管理内嵌 Gateway | -| **pChronicle sidecar / 补 Markdown** | `--record-format lance` 由 sidecar 落盘到 `events.lance/`;需要 live md 时同时启用 `--gateway-stream-markdown` | -| **Dead letter** | 保留在 Run storage 中供 pChronicle API 诊断 | - -配置示例(节选): - -```toml -listen = "127.0.0.1:19080" -admin_listen = "127.0.0.1:9876" -agent_id = "my-team" -capture_level = "dialogue" - -[[models]] -name = "deepseek-chat" -upstream = "https://api.deepseek.com/v1" -api_key_env = "DEEPSEEK_API_KEY" -``` - -管理端口提供健康与会话列表查询(用量、模型、活跃请求数),便于 sidecar 监控。 - ---- - -## 11. 演进方向 - -!!! note "Target architecture" - 下列内容是产品级目标方向,不是当前能力,也不代表承诺排期。 - -| 方向 | 动机 | -|------|------| -| **多模态 sidecar(Phase 1)** | 将 base64 / 生成图落盘到 `{run}/assets/`,Lance 只存引用;支持 materialize 嵌图与可控 replay | -| **Cursor 实时采集与 import** | 与 Claude Code 对等的注入与 JSONL 导入 | -| Lance dataset 拆分与 compaction | 长 run 下 `events.lance/` 过大时的拆分策略 | -| WAL 与序号恢复增强 | 降低 crash 后重复 apply 与 seq 冲突风险 | -| Markdown 追加日志 + 周期性 compact | 长会话 live upsert 的 IO 与 git diff 友好性 | -| 外部定价表 | 摘要费用估算可配置 | -| 故事读模型 enrich | 父子 Story、调用元数据与 spawn 完全闭环 | -| Lance 列布局优化 | 更好利用列存检索,而非大 blob | -| 协议面收敛 | 随行业 API 稳定,收缩长期维护的转换矩阵 | - -块格式通过 `v: 1` 显式版本化;详见 [AgenticMD 格式](../../pchronicle/reference/agenticmd.md)。 - ---- - -## 12. 延伸阅读 - -| 文档 | 内容 | -|------|------| -| [Capture 快速上手](../guides/capture.md) | **上手**:构建 CLI、`pvisor run`、查看轨迹、排错 | -| [轨迹存储模型](../../pchronicle/design/trajectory-storage.md) | Lance ↔ Markdown 数据流、materialize、import | -| [轨迹 Markdown 格式](../../pchronicle/reference/agenticmd.md) | 块结构、字段规范、subagent 脚注、golden 示例 | -| [pVisor 命令](../reference/cli.md) | 单 Run 执行、状态与文件系统操作 | -| [pChronicle 命令](../../pchronicle/reference/cli.md) | Dataset 查询、分析、交换与只读服务 | - -**可执行示例**: - -- [Gateway 捕获与管控 LLM](https://github.com/DeepLink-org/Persisting/tree/main/examples/pvisor/04-gateway-llm-control) - ---- - -*本文随 Persisting Gateway 发布版本更新;若行为与文档不一致,以仓库内测试与 golden fixture 为准。* diff --git a/docs/src/zh/pvisor/design/index.md b/docs/src/zh/pvisor/design/index.md deleted file mode 100644 index d89c0f7d8..000000000 --- a/docs/src/zh/pvisor/design/index.md +++ /dev/null @@ -1,13 +0,0 @@ -# pVisor Design - -这些页面解释 pVisor 如何实现一个 Agent 虚拟执行环境。 - -| 区域 | 文档 | -| --- | --- | -| Provider 组合与安全属性 | [隔离架构](isolation.md) | -| VM 透明截获与规划中的 host 截获 | [OverlayNet](overlaynet.md) | -| 模型路由、capture 与 event emission | [Gateway](gateway.md) | -| 产品命令模型与生命周期语义 | [CLI 设计](cli.md) | - -跨产品的 Run 与历史 ownership 见 [System Design](../../system-design/index.md)。面向用户的 -行为以 [pVisor 使用指南](../guides/index.md)为准,而不是以设计文档中的 roadmap 为准。 diff --git a/docs/src/zh/pvisor/design/isolation.md b/docs/src/zh/pvisor/design/isolation.md deleted file mode 100644 index 91e849060..000000000 --- a/docs/src/zh/pvisor/design/isolation.md +++ /dev/null @@ -1,735 +0,0 @@ -# pVisor 隔离架构 - -本文比较各 provider 机制及其真实安全性质。用 -[执行指南](../guides/execution.md) 选择 provider,用 -[Capability 与 Evidence](../concepts/capabilities-and-evidence.md) 解读保证。 - -!!! note "Target architecture" - 本文把当前实现与明确标出的目标架构写在一起。Linux `pvisor --` - 实现第 2 节描述的 FUSE + 合成 root + rootless user/mount namespace + - Landlock 路径。macOS host 执行在可用时使用 Seatbelt 强制 staged 写入和 deny-all - socket 约束;文件系统读取仍是 ambient,并单独报告。Docker 与 - libkrun/KVM 传输也已存在。第 2.5 节的 Virtualization.framework backend、 - 第 3 节的 LiteBox VFS、第 4.2 节的 Docker 生产 profile,以及第 5 节的 - Firecracker 架构是目标,不是已实现 backend。Seccomp 与完整资源强制仍是 - 验收标准和路线图工作。 - -pVisor 需要不止一种隔离 backend。本地 coding Agent 看重快速启动和对开发者 -工作区的精确视图;不受信任的租户则需要在 guest runtime 被攻破后仍然有用的 -边界。因此设计把**事务性工作区**与**强制边界**分开,而不是让一种机制同时 -承担两种角色。 - -多种 backend 是实现组合,**不是强加给用户的配置面**。正常产品体验仍然是: - -```bash -pvisor -- [args...] -``` - -pVisor 探测 host、工作负载和可用 placement,选择 backend,构造工作区并应用 -策略。用户不配置 Landlock 权限、mount 传播、UID map、9P 传输、seccomp JSON、 -container capability、TAP 设备或 microVM 镜像。专家级 backend 标志可以存在 -于开发和诊断,但不得成为正常路径的前提。 - -易用并不意味着不可见的安全降级。若请求的保证无法提供,pVisor 要么选择另一 -个可用 backend,要么返回一条可行动的错误。它从不把仅 `cwd` 的 Run 报告为 -已安全沙箱化。 - -## 1. 共同模型 - -```text -RunSpec / capability policy - | - v - pVisor supervisor (trusted) - | - +-- WorkspaceOverlay - | lower + compose + writable upper - | review / checkpoint / apply / drop - | - +-- IsolationBackend - workspace-landlock | workspace-seatbelt - litebox | container | microvm - | - v - Agent process tree (untrusted) -``` - -`WorkspaceOverlay` 是文件改动的 data plane。它提供隔离的 Run 视图、 -copy-on-write、whiteout 和可审计 changeset。它**本身**并不能阻止进程打开 -该视图之外的路径。 - -`IsolationBackend` 是安全平面。它决定 Agent 能到达哪些 kernel、syscall -面、namespace、host 路径、文件描述符和网络路径。每个 backend 消费同一逻辑 -工作区,并必须返回具有相同 review/apply/drop 语义的 changeset。 - -以下不变量适用于声称完整 capability enforcement 的 backend。部分原生 -backend 可以只强制更小的维度,前提是 Run Bundle 明确标识该维度并记录剩余 -ambient 访问: - -1. 默认拒绝;每个 host 文件、socket、凭据、设备和端点都是显式 capability。 -2. Agent 从不收到 host 控制面凭据或 Docker socket。 -3. 只有 `stdin`、`stdout`、`stderr`、Run 范围的 AgentCtl 传输,以及显式授予 - 的资源句柄穿过执行边界。 -4. 可写工作区与只读 base 分离。进程成功退出从不意味着有权 apply 其改动。 -5. 请求的 enforcement 与有效 enforcement 分开记录。enforce 请求在 backend - 不可用时 fail closed;从不静默回退到当前仅 `cwd` 的行为。 -6. pVisor 记录足够的 Evidence 以审计边界:backend/version、工作区 digest、 - 有效 UID/capability、kernel 特性探测、网络模式、资源限制、image/rootfs - digest 以及降级原因。 - -## 2. 原生 host 路径 - -### 2.1 Linux:FUSE + Workspace + Landlock - -这是首选的轻量 Linux host 路径。它保留今天的嵌入式 FUSE OverlayFS,并为 -Agent 进程树加上内核强制、无特权的文件系统策略。 - -Landlock 完全是内部的:不向用户暴露系统策略文件、root helper、daemon 或 -按项目规则配置。pVisor 从工作区、可执行文件/runtime 闭包、显式输入和 Run -范围的 scratch 目录推导规则。 - -#### 当前实现 - -原生 Linux safe 路径已对普通本地可执行文件可用: - -- pVisor 在 Agent 代码启动前自执行一个隐藏 launcher; -- launcher 创建 one-ID user 以及私有 mount 与子 PID namespace,不需要 - `/etc/subuid`、`newuidmap`、setuid 二进制或 daemon; -- 私有 tmpfs root 只 bind-project runtime、staged 工作区、精确设备节点、 - Run 范围的 AgentCtl socket 和显式 capability,然后 launcher 用 `chroot` - 进入;任意 host pathname Unix socket 因此是缺席的,而不是留给 Landlock; -- stderr 以上的继承描述符被关闭,Landlock ABI v3 通过 `TRUNCATE` 处理全部 - 文件系统权限,设置 `no_new_privs`,清空 namespace 与 ambient capability, - 并由受信任的 namespace PID 1 监督并回收 Agent 树; -- 主进程成功退出、取消和强制终止都在 PID namespace 边界结束,因此 `setsid` - 和 double-fork 后代无法在 Run 之后存活; -- 可写的 FUSE 合并工作区和显式读/写 capability 被准入,可执行文件和宽 host - runtime 只读; -- `NetworkCapability::Deny` 还会创建私有 network namespace。Public 与 - allowlist 代理策略仍是协作式,不会被报告为不可绕过; -- 任何 namespace 或 Landlock setup 错误都会在 Agent 执行前以保留的 - infrastructure 结果和 Run Bundle 降级警告终止。 - -当前宽不可变 runtime 包括已有的 `/bin`、`/sbin`、`/usr`、`/lib*`、`/etc` -以及进程本地 procfs 视图。这有利于与 shell、Python、Node 和动态链接本地 -工具兼容。后续可用可度量的 runtime-closure builder 收窄它;当前策略从不让 -这些层级可写。 - -默认 pVisor 依赖图不包含 pChronicle 存储 backend、Lance 或 DataFusion。 -Durable Attempt 与轨迹发布使用轻量 `persisting-events` control feature 来 -启动并与 `pchronicle serve` 的 Control 组件通信;存储引擎和云 SDK 依赖留在 -该进程。`jujutsu-overlay` 增加 Jujutsu OverlayFS upper。依赖边界见 -[RFC-0007](../../rfcs/0007-events-contract-pchronicle-sidecar.md)。 - -```text -pVisor process - +-- embedded FUSE server - | base/compose (read-only) + upper (writable) - | | - | v - | merged workspace - | - +-- small sandbox launcher - close unrelated FDs - synthetic bind-projected root + chroot - PR_SET_NO_NEW_PRIVS - Landlock ruleset - | - v - Agent process tree -``` - -pVisor supervisor 和 FUSE 请求循环留在 chroot 与 Landlock 域外。子进程获得 -合并工作区的读/写、runtime 的读/执行,以及显式输入的只读。未投影路径在合成 -root 中缺席;Landlock 独立对已投影层级强制访问权。绝对路径在该 root 内解析, -而不是重定向进工作区。 - -### 2.2 最小策略 - -| 层级 | 有效访问 | -|---|---| -| 合并工作区 | 按需读、写、创建、删除、重命名、链接 | -| Agent 可执行文件与 loader | 读、执行 | -| 所需共享库与 runtime 数据 | 只读 | -| 显式输入 Dataset | 只读 | -| Run scratch 目录 | 读/写;最好是有大小限制的 tmpfs | -| pVisor 状态、pChronicle、源凭据、home 目录 | 拒绝 | -| `/proc`、`/sys`、host socket | 除非显式投影或另行虚拟化,否则不给 Agent 访问 | -| 最小设备 | 仅精确的 null、zero/full、random/urandom 和 tty 节点 | - -Landlock 叠加在普通 DAC/ACL/LSM 检查之上;它不授予进程原本没有的访问。 -launcher 必须协商运行中 kernel 的 Landlock ABI,并处理该 ABI 支持的全部 -安全相关权限。较旧 ABI 可能缺少跨目录 refer 或 truncate 等控制,因此 -pVisor 必须发布有效保证,而不是一个布尔的 “Landlock enabled” 标志。 - -在 `landlock_restrict_self` 之前打开的文件不会被回溯约束。因此 FD 卫生是 -边界的一部分:准备目录句柄,关闭未授予的一切,安装 `no_new_privs` 和 -ruleset,然后 `exec`。这套 setup 属于小型可审计 launcher,而不是多线程 -supervisor 的复杂 post-fork 闭包。 - -### 2.3 安全与运维性质 - -**优势** - -- 不需要 host root 或持久特权 daemon。 -- 启动与稳态开销小;文件内容仍走现有 FUSE/OverlayFS 路径。 -- 工作区保真度是原生 host 路径中最好的,包括当前 review、checkpoint、apply - 和 drop 行为。 -- 子进程不能再仅靠 `..` 或绝对 host 路径逃逸。 - -**限制** - -- Agent 仍使用 host kernel 及其原生 syscall ABI。 -- Landlock 是文件系统访问控制层,不是 root 文件系统、network namespace、 - 资源控制器或完整进程 sandbox。 -- 除非 pVisor 构建最小 runtime bundle,动态语言栈的 runtime allowlist 很难。 -- 工作区 I/O 热路径上仍有 FUSE 上下文切换。 -- 仅 Linux。macOS 姊妹路径有不同且明确更窄的 Seatbelt 边界。 - -实现已经把 Landlock 与空 capability 集、`no_new_privs`、rootless -user/mount/PID namespace,以及 deny-all Run 的 network namespace 组合在 -一起。Seccomp、完整聚合资源限制,以及选择性出口的透明强制,仍是必要加固, -且不改变工作区契约。 - -### 2.4 macOS:FUSE + Seatbelt - -原生 macOS safe 路径已对普通本地可执行文件可用。它保留同一 staged macFUSE -工作区,同时为完整 Agent 后代进程树加上内核强制的 Seatbelt 策略: - -- pVisor 只调用固定系统 `/usr/bin/sandbox-exec`,从不做 PATH 查找或使用 - 项目提供的 wrapper; -- 生成的 SBPL 对每个可写路径使用 `-D` 参数,因此工作区名不能注入策略文本; -- 路径参数化的 `file-write*` 规则只准入已挂载的 staged 工作区、显式读写 - 文件系统 capability、精确 terminal/设备句柄、Run 拥有的临时目录,以及 - 一次性 setup attestation; -- 隐藏 launcher 在 `exec` Agent 之前写入并 unlink 该 attestation。因此 - profile 编译/应用失败不能被误认为 Agent 退出,而会把 Run 作为 - infrastructure 失败终止; -- `NetworkCapability::Deny` 从默认拒绝 profile 开始,拦截 IP socket 和出站 - ambient host Unix socket,只保留精确的 Run 范围 AgentCtl 以及根植于 Run - 拥有目录的 Unix IPC; -- public 与选择性代理模式仍是协作式,因为第一版实现尚未把直接 socket - 约束到仅进程内代理端点。 - -兼容 profile 刻意让文件系统读取保持 ambient。这避免硬编码脆弱的 Homebrew、 -Xcode、Python、Node、Rustup、SDK、framework 和用户安装 runtime 路径闭包。 -因此 Run Bundle 设置 `filesystem_write_non_bypassable=true`,但保持 -`filesystem_read_non_bypassable=false` 以及聚合 -`filesystem_non_bypassable=false`。未来可度量的 runtime-closure 模式可以让 -读取默认拒绝,而不改变工作区契约。 - -Seatbelt 实质改善了本地 macOS 边界,但它不是 VM 或完整进程 sandbox:host -kernel、PID namespace、syscall 面和资源记账仍然共享。`sandbox-exec` 接口 -已被 Apple 弃用,尽管仍随系统提供,因此 pVisor 探测该固定二进制并 fail -closed,而不是承诺无限期的平台可用性。在 FSKit backend 可用之前,事务性 -staging 仍需要 macFUSE。 - -### 2.5 macOS:Virtualization.framework + host-root overlay - -这是为原生 Mach-O 工作负载提出的 macOS 内核隔离路径。它是已实现 Linux -libkrun full-root executor 的 macOS 对应物,但不能使用 libkrun:macOS -guest 必须由 Apple Silicon 上的 Virtualization.framework 启动。目标是让 -可执行文件看到调用方 host 的根文件系统,所有写入由 pVisor upper 捕获, -同时在单独的 macOS 内核边界后执行。 - -guest 的 boot disk 不是 Agent 的逻辑 root。它只包含兼容的 macOS 安装和一个 -特权 pVisor guest supervisor。host 通过 OverlayFS/macFUSE 构造 -`host / + Run upper`,用 VirtioFS 导出合并视图,并要求 guest supervisor 在 -执行目标前进入该视图: - -```text -host pVisor (Rust) - +-- host / (lower, access still limited by the invoking host identity) - +-- per-Run upper - +-- merged pVisor root (macFUSE / future FSKit) - +-- MacVmExecutor - | - | private Unix socket / framed control protocol - v - pvisor-vz-helper (Swift, one helper process per active VM) - Virtualization.framework - +-- compatible macOS boot disk - +-- stable VirtioFS share tag -> merged pVisor root - +-- VZVirtioSocket control and AgentCtl transports - | - v - pvisor-guestd (root LaunchDaemon) - mount VirtioFS at a private path - chroot into the pVisor root - setgroups / setgid / setuid - set cwd, environment, limits, and stdio - execve host Mach-O -``` - -Swift helper 刻意放在 Rust supervisor 之外。它只拥有 Objective-C/Swift -Virtualization.framework 生命周期,并把它转换成小型带版本协议。现有 -`RunExecutor` 契约仍是产品边界,因此选择、取消、Evidence、review、 -checkpoint、apply 和 drop 与其他 pVisor executor 保持相同语义。 - -#### GhostVM 研究 - -[GhostVM](https://github.com/groundwater/GhostVM) 是最近考察的参考实现。 -在 commit -[`fe88d586`](https://github.com/groundwater/GhostVM/tree/fe88d5862f74ddb05ce79e04028b84c7f70482f6) -它演示了所需的控制面原语: - -- `VZMacOSBootLoader`、Mac 平台身份、macOS disk、headless 显示配置、 - VirtioFS 和 virtio socket 在一个 - [configuration builder](https://github.com/groundwater/GhostVM/blob/fe88d5862f74ddb05ce79e04028b84c7f70482f6/macOS/GhostVMKit/Configuration/VMConfigurationBuilder.swift) - 中组装; -- 一个 helper 进程拥有活动 VM,并暴露 host Unix-socket API; -- host 请求经 `VZVirtioSocket` 到达 guest agent,后者可以执行原生 macOS - 程序; -- 运行中的 VirtioFS 设备可通过 - [FolderShareService](https://github.com/groundwater/GhostVM/blob/fe88d5862f74ddb05ce79e04028b84c7f70482f6/macOS/GhostVM/Services/FolderShareService.swift) - 接收重建的目录 share; -- VM suspend/resume 使用 `saveMachineStateTo` 和 `restoreMachineStateFrom`; - APFS `clonefile()` 在 - [VMController](https://github.com/groundwater/GhostVM/blob/fe88d5862f74ddb05ce79e04028b84c7f70482f6/macOS/GhostVMKit/Operations/VMController.swift#L519) - 中创建 copy-on-write VM clone。 - -这些是架构参考,不是文件系统执行方案。GhostVM 对着它私有的 `disk.img` -启动和执行;VirtioFS 目录仍是挂在普通 guest root 下的 share。它的 guest -`exec` 端点是调用 Swift `Process.run()` 并用缓冲 stdout/stderr 的用户 -LaunchAgent。它不 chroot、不复现凭据、不流式 stdio、不转发信号、不控制 -进程组,也不暴露 pVisor changeset。因此 pVisor guest supervisor 必须是独立 -实现的 root LaunchDaemon。 - -以下拆分是有意的: - -| GhostVM 机制 | pVisor 决策 | -|---|---| -| VM configuration builder | 在 `pvisor-vz-helper` 中复现最小 headless 子集 | -| 每个 VM 一个 helper 进程 | 保留,用于 VMM 崩溃与生命周期隔离 | -| host Unix socket 加 vsock | 保留拓扑;使用有界、带版本、流式协议 | -| 运行时 VirtioFS 替换 | 适配到一个稳定的 pVisor-root tag | -| VM suspend/resume | 用来摊薄启动成本,并严格要求模板兼容 | -| APFS VM clone | 可选用于创建干净 boot 模板 | -| GhostTools 命令执行 | 用特权 `pvisor-guestd` 替换 | -| NAT、bridge、剪贴板、音频、GUI 自动化 | 从默认进程隔离 VM 中省略 | -| 把私有 guest root 当作工作负载 root | 拒绝;导出的 pVisor 合并 root 才是工作负载 root | - -GhostVM 的 README 目前写明其源码许可尚未确定。pVisor 可以研究公开行为与 -架构,但在兼容许可发布之前不得复制其实现。helper 与 guest supervisor 是 -对着 Apple 公开 API 的干净独立实现。 - -#### 文件系统与身份语义 - -“使用 host UID 和权限”意味着保留普通 POSIX 文件语义,而不是继承每一种 -macOS 安全身份。host pVisor 在调用方 host 身份下打开并服务 lower 文件; -guest supervisor 随后在 `execve` 前安装匹配的数字 UID、GID 和补充组。实现 -必须证明 VirtioFS 如何表示 owner、mode、ACL、symlink、hard-link、xattr、 -设备和 rename 语义,而不是假定数字身份就足够。 - -仅因数字 UID 匹配,以下 host 设施不会自动变得透明: - -- TCC 决策、Keychain access group、代码签名身份与 entitlement; -- host login/GUI bootstrap 会话、launchd 服务、Mach port、Apple Events 和 - host Unix socket; -- host kernel 状态、设备、导出 root 中不可见的已挂载卷,以及仅由 host 进程 - 持有的凭据。 - -现代 macOS 还通过密封系统卷、可写数据卷和 firmlink 呈现 `/`。pVisor 必须 -验证导出 host root 能呈现一个连贯命名空间,并且 whiteout/copy-up 行为在 -`/System/Volumes/Data` 上仍然正确。访问受隐私保护的 host 文件可能要求受 -信任 host 组件具备 Full Disk Access;pVisor 必须报告该要求,而不是静默 -返回部分 root。 - -host 与 guest 最初应要求相同架构和精确 macOS build。host 可执行文件可以 -依赖匹配的 dyld shared cache、framework ABI、代码签名策略和 kernel 行为。 -在兼容矩阵证明之前,跨 build 执行不受支持。 - -#### 生命周期与安全 profile - -按 Run 冷安装 macOS 不可行。预期生命周期是: - -1. 供应并证明一个最小、匹配的 macOS boot 模板; -2. 启动一次,安装 `pvisor-guestd`,并保存干净的挂起状态; -3. 恢复 warm VM,或从小型版本匹配池中获取一个; -4. 只附加该 Run 的 VirtioFS root 和每 Run 的 vsock 端点; -5. 在 guest 执行前轮换 Run 身份、鉴权材料、熵、IPC 和网络状态; -6. 恰好执行一棵不受信任的进程树,经普通 pVisor review 路径导出 upper,然后 - 销毁或把擦洗后的 VM 还回池中。 - -默认 VM 没有 NAT 或桥接网络设备。网络访问穿过 OverlayNet 拥有的显式 vsock -relay。剪贴板、host 音频、GUI 设备、任意共享文件夹、端口转发和 ambient -host socket 都不存在。host VMM helper 只获得对已准备合并 root、VM 模板、 -其私有 control socket 以及所需 Virtualization.framework 资源的访问;它不得 -继承 pChronicle、源凭据或不相关描述符。 - -VirtioFS 是最大的可行性风险。GhostVM 有一份关于 macOS guest 下 -[空 mount 与不可读文件](https://github.com/groundwater/GhostVM/issues/255) -的公开报告。pVisor 的设计把 dyld、framework、SDK、包管理器和元数据密集 -工具链放在这条路径上,比共享项目目录更苛刻。因此 VM 启动成功不是 backend -可用或安全的证据。 - -#### 可行性门 - -该 backend 保持实验性,直到一个聚焦原型在受支持的 host/guest build 对上 -通过以下全部条件: - -1. 用稳定 VirtioFS tag 导出 pVisor 合并 root,并在没有 Finder 或 login-session - 自动化的情况下挂载它; -2. 在 `chroot` 后运行 `/usr/bin/true`、`/bin/zsh` 以及有代表性的 - `xcrun`/编译器工具,并具备正确的 cwd、环境、UID、GID、组、退出状态、 - 流式 stdio、信号、取消和后代清理; -3. 证明 lower 文件不变,且所有创建、修改、重命名、删除、whiteout、xattr、 - ACL、symlink 和 hard link 进入 Run upper,并在 review/checkpoint/apply/drop - 后存活; -4. 演练 dyld/framework 加载、代码签名、密封系统/数据 firmlink 布局、大输出、 - 大文件、大量小文件、并发改写、崩溃恢复以及 warm-restore 附加变更; -5. 证明网络、剪贴板、任意 share、过期 vsock token、先前 Run 的 upper 或 - 不相关 host 描述符都不可达; -6. 发布冷/热延迟和 RSS,并与 Seatbelt 以及 Linux libkrun Run 比较。 - -通过该门确立透明 CLI 与开发工具执行。GUI 应用和 host-session 服务需要单独 -Evidence,不由进程级 backend 的成功隐含。 - -## 3. 路径 B:LiteBox + VFS 中的 OverlayFS 语义 - -### 3.1 定位 - -这是高密度 libOS 路径。LiteBox 在 userspace 处理 guest Linux ABI 和路径 -解析。pVisor 应把它的 overlay 语义实现为 LiteBox 文件系统 backend 或 -composer,而不是 FUSE 挂载一条 host 路径,再把 guest 路径字符串转发给 host -`openat`。 - -```text -pVisor supervisor - +-- build content-addressed root/workspace bundle - +-- pass sealed bundle FD + policy + AgentCtl FD - | - `-- LiteBox runner process - LiteBox Linux shim - | - v - LiteBox VFS resolver - +-- read-only root/runtime - +-- read-only workspace layers - `-- writable in-memory/delta upper - | - v - exported changeset - | - v - pVisor review / apply / drop -``` - -适配器保留 pVisor 的逻辑操作: - -- 有序只读 base 与 compose 层; -- 首次写入时 copy-up; -- whiteout 与不透明目录语义; -- 确定性目录合并; -- mode、时间戳、symlink、hard link 和 xattr 的元数据策略; -- 有界可写 upper,可以在不遍历不相关 host 路径的情况下导出。 - -初始实现可以复用 LiteBox 的只读 tar 和内存文件系统,但生产采用需要一份 -文件系统语义矩阵。不支持的元数据必须显式失败,或按文档化策略规范化;静默 -丢失会破坏 pVisor 的 changeset 契约。 - -### 3.2 安全与运维性质 - -**优势** - -- guest 路径终止于 LiteBox VFS;正常路径不包含 host pathname 查找。 -- 比原生 Linux 进程或通用 OCI container 更小的 host 接口,使 syscall 级策略 - 和确定性 I/O 变得可行。 -- 只读内容寻址 bundle 可以跨 Run 缓存和共享;可写状态保持每 Run。 -- 完全在 runner 内处理的 VFS 操作不需要 kernel FUSE 往返,这可能有利于 - 元数据密集工作负载。 - -**限制** - -- Linux syscall 与文件系统兼容面比 Docker 或 VM 更窄。 -- LiteBox 及其 pVisor 适配器仍在演进,并扩大 pVisor 的可信计算基。 -- userspace libOS 并不自动成为硬件或 kernel 安全边界。必须假定 runner、 - loader、syscall 拦截或共享地址空间中的缺陷可能存在。 -- 打包原生库、动态 runtime、JIT 和异常文件系统行为需要显式兼容测试。 - -因此 LiteBox runner 必须在单独的无特权进程中执行,并带 Landlock、seccomp、 -`no_new_privs`、空 capability、关闭的 FD 和资源限制。若 guest 逃出 LiteBox -抽象,外层 kernel 策略才是包含边界。禁止把不受信任的 LiteBox guest 嵌入 -pVisor supervisor 进程。 - -### 3.3 工作区传输 - -避免长期、任意 pathname broker。优先不可变且有界的对象: - -1. pVisor 快照逻辑 lower 层并计算 digest。 -2. 向 runner 提供密封 `memfd` 或只读文件描述符。 -3. LiteBox 通过其 VFS 读取 root 和工作区。 -4. 写入进入带字节/inode 配额的每 Run upper。 -5. runner 导出规范、有界的 changeset。 -6. pVisor 在把 changeset 暴露给 review/apply 之前校验路径、条目类型、元数据、 - 大小和 digest。 - -## 4. 路径 C:Docker / OCI container - -### 4.1 定位 - -这是兼容与生态路径。它支持现有 Agent 镜像和常规 Linux runtime,隔离强于 -host executor,同时共享 host kernel。 - -当前 pVisor Docker/Podman executor 已经向镜像注入匹配的静态 pVisor,并委托 -同一 `RunSpec`。它挂载最终工作区并返回类型化 `RunResult`。它尚未把每项 -pVisor capability 翻译成 OCI 限制,且注入的 pVisor 当前以 container root -启动;这些是实现缺口,不是目标设计的性质。 - -```text -host pVisor - +-- WorkspaceOverlay / merged view - +-- Docker or Podman transport - | - v - OCI container - read-only image rootfs - /workspace -> pVisor Run view - tmpfs /tmp - injected pVisor -> Agent -``` - -Docker 的镜像层 OverlayFS 与 pVisor 的 WorkspaceOverlay 角色不同。前者组装 -OCI 根文件系统;后者拥有 Agent 改动和 review/apply/drop。container 拆除不得 -把 OCI 可写层提交为 Run 结果。 - -### 4.2 生产 profile - -目标 profile 是: - -- 支持时使用 rootless Docker/Podman,或 user namespace remapping; -- 去掉注入 pVisor 的 bootstrap 问题后,Agent UID 非 root; -- 丢掉全部 capability,`no-new-privileges`,默认或更紧的 seccomp; -- 只读 container rootfs 和私有、有界的 `/tmp`; -- PID、内存、CPU、文件大小和进程数限制; -- 默认无网络,否则使用连到 pVisor 拥有 broker 的专用 namespace; -- 无 host PID/IPC namespace、privileged 模式、设备透传、任意可写 mount 或 - Docker socket; -- 镜像 digest pinning 以及可审计的 mount/capability manifest。 - -### 4.3 安全与运维性质 - -**优势** - -- 除 VM 外最高的工作负载兼容性。 -- 成熟的镜像构建、分发、缓存、可观测性与运维工具。 -- Namespace、cgroup、capability、seccomp 和 host LSM 组合成实用生产边界。 -- 对 Kubernetes 和现有 CI 基础设施是自然部署路径。 - -**限制** - -- Container 共享 host kernel;kernel 或 container-runtime 逃逸在 pVisor - 自身强制之外。 -- 冷启动成本、镜像存储、daemon/runtime 依赖和 mount plumbing 高于本地和 - LiteBox 路径。 -- Rootful daemon 部署会制造更大的特权控制面。 -- Host 网络、宽 bind mount、`--privileged` 或 Docker socket 可以抹掉大部分 - 隔离价值。 -- 当前 Gateway loopback 集成在某些配置下需要 host 网络;生产 enforcement - 需要 guest 可见 broker 之后才能去掉该限制。 - -Docker 是推荐的兼容 fallback,不是 pVisor capability 模型的定义。 - -## 5. 路径 D:Firecracker microVM - -### 5.1 定位 - -这是最强的多租户路径。每个 Run 或 warm Run 槽在 KVM 下获得单独的 guest -kernel。Firecracker 刻意暴露小型设备模型,并提供 jailer,为主机侧 -namespace/cgroup 隔离并丢掉 VMM 特权。 - -现有 pVisor `vm` executor 静态链接 libkrun,可以使用显式 Linux rootfs,或 -在没有 container daemon 的情况下拉取公开 OCI 镜像。已校验的镜像层形成不可变 -缓存 lower rootfs,guest 系统写入留在可审查 upper。host 路径不会被隐式暴露。 -显式 `--overlayfs-compose` 加 `--overlayfs-path` 在所选 guest 路径挂载 staged -视图。vendored libkrun 在 Linux 和 macOS 上通过 virtio-fs 直接服务 root 与 -工作区 copy-on-write union,没有 host FUSE mount、物化或对账。Linux 使用 -KVM,Apple Silicon macOS 使用 HVF。Linux 另外用 user/mount/network -namespace 和 Landlock 约束 VMM。macOS VMM 尚未包进等价的 host 文件系统 -sandbox,因此 libkrun 的 virtio-fs proxy 仍处于调用用户的安全上下文。 -OverlayNet 与 AgentCtl guest relay 在本阶段尚未实现。这不是下面的敌对多租户 -Firecracker 设计: - -```text -host pVisor / microVM manager - +-- immutable kernel + rootfs image - +-- read-only workspace/base block image - +-- per-Run writable delta block image - +-- vsock control and AgentCtl transport - +-- TAP/network broker under policy - | - v - Firecracker + jailer - | - v - guest kernel + injected pVisor + Agent -``` - -rootfs 和工作区作为文件后备块设备附加。Run 完成时,guest 静止文件系统并 -经 vsock 返回 manifest;host 校验并把 delta 转成普通 pVisor changeset。 -Firecracker snapshot 可以摊薄启动成本,但 VM 状态、guest 内存、块设备、网络 -设备和 vsock 端点有各自的生命周期与兼容要求。Snapshot 复用必须轮换 Run -身份、熵、凭据和网络状态。 - -### 5.2 安全与运维性质 - -**优势** - -- 单独 guest kernel 为互不信任租户和敌对原生代码提供最清晰的边界。 -- 最小设备模拟相对通用机器模拟器减少 VMM 攻击面。 -- 资源记账和网络拓扑在 VM 边界上是显式的。 -- Warm pool 与 snapshot 可以让重复 Run 启动变得可行。 - -**限制** - -- 需要 Linux、KVM、kernel/rootfs 镜像生产、jailer、TAP/网络 setup 以及 - microVM 生命周期服务。 -- 即使 VMM 很轻,基线内存和运维复杂度也高于进程/container 路径。 -- 工作区块镜像创建和 delta 提取,不如直接挂载的 FUSE 工作区交互。 -- Kernel、rootfs、snapshot 和 VMM 版本形成更大的兼容与补丁管理面。 -- 直接共享 host 目录会削弱干净边界,不应成为生产工作区设计。 - -生产 Firecracker 执行必须使用 jailer 或等价更强的 host 策略、专用无特权 -VMM 身份、cgroup、seccomp、隔离网络、受信任不可变输入,并且没有对 host -路径的 ambient 访问。 - -## 6. 比较与选择 - -该表描述预期生产形态,而不只是仓库里当前已有的代码。性能刻意保持相对, -直到共同 benchmark 测过冷/热启动、RSS、syscall 密集与数据密集工作负载, -以及拆除。 - -| 维度 | FUSE + Landlock | FUSE + Seatbelt | macOS VM + VirtioFS | LiteBox VFS | Docker/OCI | Firecracker | -|---|---|---|---|---|---|---| -| 主要目标 | 最快的 Linux 最小特权 | 零配置 macOS 写约束 | 带 guest-kernel 边界的透明 Mach-O 执行 | 高密度 libOS 隔离 | 兼容与部署 | 敌对多租户隔离 | -| 安全边界 | 合成 root + host LSM/namespace 策略 | host 进程上的 Seatbelt 写/socket 策略 | macOS guest kernel + Virtualization.framework VMM | libOS 加外层 host 策略 | namespace/cgroup/LSM,共享 kernel | guest kernel + KVM + jailed VMM | -| 是否需要 host root | 否 | 否 | 作为 pVisor 合并 root 导出 | 否 | rootless 模式下否 | 通常需要 host 供应 | -| Guest 兼容性 | 原生 Linux ABI | 原生 macOS ABI;ambient 读取 | 原生 Mach-O,最初仅精确 host/guest build | 受约束的 Linux ABI | 宽 Linux userspace | 完整 guest Linux | -| 工作区保真度 | 最高 | 配合 macFUSE 最高 | 目标是 full-root 保真;VirtioFS 上未证明 | 需要语义适配器 | 经 mount/volume 高 | 显式块/delta 转换 | -| 启动成本 | 最低 | 最低 | 冷启动高;目标是热恢复/池 | 目标低 | 中等,取决于镜像 | 冷启动最高;目标是热 snapshot | -| 每 Run 内存 | 最低 | 最低 | 高 | 目标低 | 中等 | 最高 | -| Kernel 逃逸爆炸半径 | host | host | 先 guest,再 VMM 边界 | 外层逃逸后是 host | host | 先 guest,再 VMM/KVM 边界 | -| 可移植性 | Linux | macOS;依赖已弃用 launcher | 支持 macOS 虚拟化的 Apple Silicon Mac | 取决于平台/ABI | 宽 OCI host | Linux + KVM | -| 当前 pVisor 状态 | 已实现;seccomp/limits 待定 | 写约束与 deny-all socket 策略已实现 | 已研究设计;需要可行性原型 | 计划中 | 已实现,存在加固缺口 | libkrun full-root 模式已存在;Firecracker 计划中 | - -### 推荐组合 - -选择权属于 pVisor 和 placement 控制面: - -1. 普通 Linux `pvisor --` 今天使用 FUSE + Workspace + 合成 root + - rootless namespace + Landlock。所需控制 fail-closed 安装;不可用的 user - namespace、mount、chroot 或 Landlock ABI 从不静默回退。 -2. 当 LiteBox 能为兼容的打包工作负载提供更小、可度量的 host 接口时,pVisor - 可以自动选择它;用户仍调用同一命令。 -3. 提供 OCI 镜像自然选择 Docker/Podman。否则 pVisor 可以把已经可用的 - rootless runtime 当作兼容 fallback;它不要求用户构造 capability 或 mount - 标志。 -4. 为敌对多租户执行配置的集群把 Run 放到 Firecracker worker。Kernel 镜像、 - snapshot、网络和 jailer setup 是运营商拥有的集群基础设施,不是每用户 - 配置。 -5. macOS 保持同一命令,并对已实现的低延迟本地路径使用 Seatbelt 写约束。 - 可行性门通过后,要求 guest-kernel 边界的策略可以自动选择 - Virtualization.framework backend。在此之前,Bundle 单独报告 ambient 读取 - 和协作式选择性网络;更严的请求路由到另一有能力的 placement,或带着一条 - 修复建议失败。 - -这些路径是组合,不是强制迁移阶梯。客户声明工作负载意图,并在必要时声明 -最低安全要求;placement 只选择其已度量能力满足该要求的 backend。客户不选择 -kernel 机制。 - -## 7. 一份 backend 契约 - -所有实现都应把一次请求编译成一份带 Evidence 的结果: - -```text -IsolationRequest { - minimum_boundary, - filesystem_capabilities, - network_capabilities, - compute_limits, - credential_refs, - require_enforcement, -} - -IsolationEvidence { - requested_class, - effective_backend, - backend_version, - effective_controls, - unsupported_controls, - workspace_digest, - runtime_or_image_digest, - identity_and_capabilities, - kernel_features, -} -``` - -只有当测试证明完整 Agent 进程树无法到达未授予层级时, -`RuntimeCapabilities.filesystem = true` 才有效。仅有已挂载工作区或成功的 -setup 调用不是 Evidence。 - -该契约是 admission、placement 与 runtime driver 之间的内部契约。它不是用户 -必须理解或配置 backend 特定机制的要求。 - -## 8. 零配置验收标准 - -默认本地路径只有在以下全部成立时才算完整: - -- 一次 pVisor 安装和一条 `pvisor --` 命令就足够; -- 不需要 root shell、setuid pVisor daemon、手工加组、手写策略、mount 命令 - 或 container 安全标志; -- pVisor 发现可执行文件及其最小 runtime 依赖; -- 工作区 setup、隔离、清理和 changeset 恢复是自动的; -- 不受支持的 host 产生一条稳定错误,带具体修复或自动可用的 placement,而 - 不是一串 kernel 细节; -- `pvisor status` 和 Run Bundle 解释有效边界供审计,但不把该解释当作使用 - 前提; -- 升级保留高层命令和 Run 契约,同时允许所选 backend 变化。 - -该标准排除把 9P 当作面向用户的 Docker setup 步骤。当远程 backend 需要时, -pVisor 可以在内部使用文件系统协议,但用户绝不能为普通 Run 供应 9P server、 -挂载它,或授予 container mount capability。 - -## 9. 校验与 benchmark - -每个 backend 必须跑同一套对抗套件: - -- 绝对路径、`..`、symlink 链、hard link、rename 竞态、magic link、 - `/proc/self/fd`、继承目录 FD、UNIX socket、设备节点和描述符传递; -- fork/clone/exec 后代、原始 syscall、静态二进制、JIT 生成代码、信号、 - ptrace 尝试,以及适用处的 namespace 操作; -- 直接 socket、DNS rebinding、字面量 IP、UDP/QUIC、loopback、link-local - 和元数据服务地址; -- 字节/inode/进程/CPU/内存/网络耗尽以及取消清理; -- 工作区导出、review 和 apply 期间的掉电或 supervisor 崩溃; -- 同一 changeset 在全部四种 backend 上的语义比较。 - -共享 benchmark 报告分布,而不是单个演示数字: - -- 冷启动与热启动 P50/P95/P99; -- 空闲与峰值 RSS; -- 顺序与随机工作区吞吐; -- 每秒小文件元数据操作; -- syscall 密集以及 Python/Node/原生 Agent 工作负载; -- checkpoint/export/apply 延迟与产出字节; -- host CPU 成本、上下文切换、缺页,以及 FUSE/VMM/broker 开销。 - -已实现的 Linux 套件目前证明 staged 写入,以及拒绝绝对路径读/写、symlink -逃逸、`/proc/self/root` 逃逸、未授予 pathname Unix socket、精确 AgentCtl -socket 的保留,以及 deny-all 模式下的 host-loopback 访问。它也证明 setup -失败在 Agent 执行前报告。这是有用的回归下限,还不是上面列出的完整对抗/ -kernel 矩阵。没有 backend 仅凭架构预期就能成为生产默认;它必须发布可重复 -测量,并在每个受支持 host/kernel 上通过该矩阵。 - -## 参考 - -- [Apple: Running macOS in a virtual machine on Apple silicon](https://developer.apple.com/documentation/virtualization/running-macos-in-a-virtual-machine-on-apple-silicon) -- [Apple: VZVirtioFileSystemDeviceConfiguration](https://developer.apple.com/documentation/virtualization/vzvirtiofilesystemdeviceconfiguration) -- [GhostVM](https://github.com/groundwater/GhostVM) -- [GhostVM VirtioFS instability report](https://github.com/groundwater/GhostVM/issues/255) -- [Linux Landlock userspace API](https://docs.kernel.org/userspace-api/landlock.html) -- [Docker rootless mode](https://docs.docker.com/engine/security/rootless/) -- [Docker default seccomp profile](https://docs.docker.com/engine/security/seccomp/) -- [Firecracker](https://github.com/firecracker-microvm/firecracker) -- [Firecracker jailer](https://github.com/firecracker-microvm/firecracker/blob/main/docs/jailer.md) -- [Firecracker snapshot support](https://github.com/firecracker-microvm/firecracker/blob/main/docs/snapshotting/snapshot-support.md) diff --git a/docs/src/zh/pvisor/design/overlaynet.md b/docs/src/zh/pvisor/design/overlaynet.md deleted file mode 100644 index f5c6c3cf7..000000000 --- a/docs/src/zh/pvisor/design/overlaynet.md +++ /dev/null @@ -1,226 +0,0 @@ -# OverlayNet 透明拦截 - -本文负责拦截机制、强制缺口与验收门。用户策略流程属于 -[网络指南](../guides/network.md);能力模型属于 -[Capability 与 Evidence](../concepts/capabilities-and-evidence.md)。 - -!!! note "Target architecture" - 文首描述的 libkrun VM driver 已经实现。Design A、Design B 以及交付计划第 1–5 - 项描述的是目标 host/container 拦截与验收门,不是当前公开能力。 - -## 已实现的 VM driver - -libkrun VM Attempt 在 `[overlaynet].mode = "auto"` 时使用 `vm-smoltcp`。 -guest virtio-net 设备通过 libkrun 带长度前缀的 UnixStream Ethernet 传输连到 -pVisor。pVisor 提供 DHCP(`192.0.2.1` 路由器、`192.0.2.2` guest)、合成 DNS -(`198.18.0.0/15`,每个 Attempt 稳定)以及 IPv4 TCP。SYN 会在 smoltcp 中暂停, -直到 hostname/IP、解析后的地址或 scoped host connector alias、端口以及注入的 -Control 策略全部授权,并且 host 连接成功。TSI 保持关闭,因此不存在绕过该 -data plane 的 guest 路径。 - -部分 host DNS/TUN connector 会为已授权 hostname 返回不透明的 `198.18.0.0/15` -假 IP。VM connector 只有在逻辑 hostname 与端口通过策略和 Control 授权后才接受 -该结果;同一范围内的 guest IP 字面量仍然被拦。因为 connector 隐藏了真实最终 -地址,IP/CIDR 策略无法检查该 alias 背后的端点。需要最终地址策略的部署应使用 -会暴露具体地址的 resolver。 - -MVP 对通用 UDP、IPv6、ICMP、QUIC、入站连接、virtual/link-local/multicast/ -broadcast 目的地,以及耗尽的 flow/DNS 容量,刻意 fail closed。显式 Gateway -capture 是内部 virtual-router 路由;所有普通出口共享同一策略和带宽注册表。 -下文描述的 host 与 container 透明拦截仍是后续工作。 - -> 状态:libkrun VM driver 已在 Linux 和 Apple Silicon macOS 上实现。本文后面 -> 描述的 host-process 透明 driver 仍是已接受的设计。Host/container 选择性 -> 策略仍使用显式代理;host deny-all 保持现有平台 sandbox 行为。 - -## 问题 - -OverlayNet 的 host/container data plane 是显式 HTTP/HTTPS 代理。pVisor 注入 -代理环境变量,并对已知 Agent CLI 注入代理配置参数。覆盖因此是 opt-in:任何 -忽略代理环境变量的子进程——静态 Go 二进制、原始 socket、清洗环境的 -subprocess——都会直接访问网络。这就是 host `ProcessExecutor` 不能声称 -enforcement,并拒绝网络 Capability 的 `PolicyMode::Enforce` 的原因。 - -剩余 host-driver 设计的目标是**完整拦截且占用轻**:无论语言 runtime、链接 -方式还是 syscall 纪律,Agent 进程树发出的每个字节都必须经过 pVisor 拥有的 -choke point——且不需要 VM、root daemon 或持久提升权限。 - -关键动作是把拦截点从*约定*(子进程可以忽略的环境变量)移到*子进程无法选择 -绕过的一层*。 - -## Design A(主路径):无特权 network namespace + 进程内 userspace 网络栈 - -这是具备条件的 Linux host 上的默认 driver。它镜像 pVisor 文件系统路径的设计: - -```text -filesystem: pVisor embeds a FUSE server and IS the child's filesystem -network: pVisor embeds a userspace TCP/IP stack and IS the child's network -``` - -### 机制 - -1. Attempt 子进程以 `CLONE_NEWUSER | CLONE_NEWNET` 派生。在新的 user - namespace 内创建 network namespace **不需要特权**;namespace 所有者在其中 - 持有 `CAP_NET_ADMIN`。 -2. 在 namespace 内,setup 代码创建 `tun` 设备,分配 link-local 子网,并安装 - 指向它的默认路由。loopback 被拉起,以便 Run 本地服务继续工作。 -3. `tun` 文件描述符在 `exec` 之前经 `socketpair` 回传给 pVisor 父进程。此后 - pVisor 拥有整棵进程树的唯一出口路径。 -4. pVisor 在 `tun` fd 上运行基于 `smoltcp` 的 userspace 栈。入站 TCP 流在栈 - 内终止,通过 `persisting-agentctl` 策略门后在 host 侧重起源。现有 - OverlayNet 代理 / Gateway sink 仍是 LLM capture 路径,保持不变。 -5. DNS:栈回答经 namespace `resolv.conf` 通告的虚拟 resolver 地址。查询在 - host 侧解析,从而在任何连接存在之前给出域名级策略点。 - -### 性质 - -- **拓扑完整。** libc interposition、静态二进制、原始 syscall 和 fork 出的 - 孙进程都在 namespace 内;没有第二条出路。不需要、也不假设子进程配合。 -- **运行时零特权。** 无 root、无 setuid helper、无 daemon。唯一的 host 前提 - 是无特权 user namespace(`kernel.unprivileged_userns_clone` / 发行版等价 - 项)。 -- **进程内。** 与嵌入 FUSE 的决策一致:pVisor 不派生 `passt` / - `slirp4netns` 一类 helper。 - -### 策略评估点 - -| 层 | 信号 | 说明 | -|---|---|---| -| DNS | 查询名 | 虚拟 resolver;最便宜的 allowlist 点 | -| L4 | 目的 IP:port | 字面量 IP 流量的最后手段 | -| TLS | ClientHello 中的 SNI | 被动解析,无 MITM,无注入 CA | -| QUIC | Initial 包中的 SNI,或被拦截 | 默认:拒绝 UDP/443 以迫使 TCP fallback | - -### 失败与探测 - -Driver 可用性在 Attempt prepare 时探测。若 user namespace 不可用,行为取决于 -请求的策略模式: - -- `PolicyMode::Observe`:回退到显式代理 driver,并在 implant plan notes 中 - 记录降级。 -- `PolicyMode::Enforce`:让 Run 准备失败。Enforce 下的降级绝不能静默。 - -## Design B(受限 fallback):seccomp user-notify + socket broker - -在无特权 user namespace 被关闭的 host 上(加固发行版、部分 container -runtime),第二个 driver 可以在没有 namespace 的情况下强制一组刻意更小的 -socket 面。除非未覆盖通道都被拒绝,它不被视为与 netns driver 等价。 - -### 机制 - -1. Attempt 子进程安装 seccomp filter,把 `socket`、`connect`、`sendto` 和 - `sendmsg` 路由到 `SECCOMP_RET_USER_NOTIF`。当该 driver 声称 enforcement - 时,`io_uring_setup`、原始 packet socket、namespace 变更和未中介的描述符 - 传递都被拒绝。 -2. `socket` 被 broker:pVisor 创建 socket,保留同一 open file description - 的副本,并用 `SECCOMP_IOCTL_NOTIF_ADDFD` 注入子进程描述符。 -3. 在 `connect` 上,pVisor 复制一次 socket 地址,重新校验 notification - cookie,评估策略,并通过它保留的描述符执行 `connect`。然后返回真实结果, - 而不允许子进程原来带指针的 syscall 继续。这避免了 check-then-`CONTINUE` - 的 TOCTOU 窗口。 -4. 初始 seccomp driver 仅 TCP。未连接的 UDP 会被拒绝,直到 OverlayNet 能安全 - 复制并 broker 每个 datagram。DNS 必须走 pVisor 提供的 resolver 路径;否则 - 无法从 `connect` 观察到的目的 IP 重建域名 allowlist。 - -### 性质与注意点 - -- 覆盖显式 broker 的 socket 族上的静态二进制和原始 syscall。`AF_UNIX` 有单独 - 的路径策略;不是一律放行。 -- 无 namespace、无 tun、无 userspace 栈——但描述符来源、`SCM_RIGHTS`、UDP、 - DNS 和 `io_uring` 必须全部关闭或中介,该 driver 才能被描述为不可绕过。 -- Seccomp 在 `connect` 时看到的是 IP 地址,不是应用最初解析的 hostname。域名 - allowlist 需要中介 DNS、显式代理流量或 SNI 关联;IP/CIDR 策略可以直接强制。 -- 按 Attempt 选择;两者都可用时仍优先 Design A。 - -## Enforcement 与 capture 是分开的层 - -透明拦截提供 **enforcement**(deny / allowlist)和流量记账。它刻意不解密: - -- Enforcement 不需要 MITM CA:中介 DNS 名和已授权目的地址对 VM MVP 已经足够; - 未来的 host netns driver 可以增加被动 SNI 解析。 -- LLM payload 的 **Capture** 仍走现有显式代理路径:Gateway 向已知 Agent CLI - 注入代理配置并看到明文。在不可绕过 driver 下,不配合的流量不能离开 - allowlist,但不会被解密。 - -已知侵蚀:Encrypted ClientHello 最终会隐藏 SNI。当这很重要时,部署在 capture -级可见性的 opt-in MITM CA 与回退到 DNS/IP 级 enforcement 之间选择。这是行业 -约束,不是某个 driver 特有的。 - -## Capability 报告 - -每个 Attempt 的选择决定是否挂上不可绕过 driver;runtime capability catalog -另行通告 VM 网络支持。每次 Run 记录一份 `InterceptionProfile`,描述 driver、 -强度和协议覆盖: - -- 当 VM smoltcp、netns 或 seccomp 激活时为 `enforce`; -- 仅有显式代理时为 `observe`。 - -显式代理基础已经把该 profile 发成 `cooperative`,并发布 -intercepted/allowed/denied/CONNECT/HTTP/sink/failure 计数。这些计数证明什么 -到达了 OverlayNet;它们不估计被绕过的流量。 - -诚实不变量得以保持:host `ProcessExecutor` 本身仍然从不声称网络 -enforcement;声称由当前 OverlayNet driver 做出,且只有在该 driver 已挂上时 -`PolicyMode::Enforce` 才可满足。 - -## 配置 - -已实现的公开 mode 选择器有三个值: - -```toml -[overlaynet] -mode = "auto" # auto | off | proxy -policy = "allowlist" - -[[overlaynet.rules]] -host = "api.openai.com" -ports = [443] -transports = ["tcp_tunnel"] -``` - -对 libkrun VM,`auto` 选择 `vm-smoltcp`;`off` 让 VM 离线;`proxy` 被拒绝, -因为它是仅 host/container 的协作 driver。对 host/container Run,显式网络标志 -选择 `proxy`;已接受的 `netns` 与 `seccomp` host driver 仍是未来内部候选, -而不是暴露的配置值。`run.json` 记录实际挂上的 driver,因此 `pvisor status` -报告真实 enforcement 级别。 - -## 非目标 - -- macOS 透明*选择性*拦截(Network Extension、基于 pf 的 UID 路由)。选择性 - 策略仍是 observe 级;deny-all 是单独的 Seatbelt 强制边界,并按此报告。 -- eBPF(`cgroup/connect4`)driver。优雅,但需要 CAP_BPF/root 以及 setup-host - 部署模型;目前不在范围内。 -- 默认 TLS 解密。MITM 始终是显式 opt-in(如果将来有)。 - -## 交付计划与验收门 - -本文开头描述的 VM milestone 已经完成。下面剩余计划适用于透明 host/container -拦截。 - -0. **显式代理基础(已实现):** 诚实的 cooperative profile、拦截计数、严格 - CONNECT 解析、connect-before-200、流式转发、动态 hop-header 剥离、redirect - 再校验、无隐式 loopback 或 Gateway-upstream 出口信任、结构化 - host/IP/CIDR + port + transport 规则、DNS 后地址授权,以及钉住已授权目的 - 地以关闭策略/connector DNS 竞态。 -1. **Driver 探测与选择:** 在已实现的公开 `off | proxy | auto` 选择器上扩展 - 内部 netns/seccomp 探测;在子进程 exec 前记录所选 profile。若没有不可绕过 - profile,`Enforce` fail closed。 -2. **Netns TCP + DNS 最小集:** spawn plumbing、tun 交接、TCP relay、中介 - resolver、DNS/IP allowlist、进程树与 namespace 逃逸测试。在原始 syscall - 和环境被清洗的孙进程被证明包含之前,不得声称 enforcement。 -3. **协议闭合:** SNI 策略、字面量 IP 行为、UDP 策略、被拦 QUIC fallback、 - `AF_UNIX`、raw/netlink socket、`SCM_RIGHTS`、namespace 变更以及 - `io_uring` 符合性用例。 -4. **受限 seccomp fallback:** 先 broker TCP socket;拒绝未覆盖通道。只有在 - 描述符和 datagram 语义有专门测试后才加入 UDP/DNS。 -5. **运维:** 把最终计数和降级原因持久化到 `run.json`,通过 `pvisor status` - 暴露,并分别对 Python、Node、Rust、静态 Go 以及 fork 出的孙进程做 - proxy/netns/seccomp 模式 benchmark。 - -## 相关文档 - -- [网络指南](../guides/network.md):为一次 Run 配置并检查策略。 -- [隔离架构](isolation.md):比较完整的 provider 边界。 -- [Gateway 架构](gateway.md):网络层之上的模型路由与 capture。 -- [安全与 Evidence](../../system-design/security-evidence.md):跨产品解读 - enforcement 声称。 diff --git a/docs/src/zh/pvisor/get-started.md b/docs/src/zh/pvisor/get-started.md deleted file mode 100644 index 4c3f82860..000000000 --- a/docs/src/zh/pvisor/get-started.md +++ /dev/null @@ -1,88 +0,0 @@ -# 运行第一个 Agent - -这条路径把你从空项目带到一次经过审查的修改。每一步都有明确的检查点, -你可以在需要时停下来,不必一次学完所有能力。 - -!!! tip "pVisor 的工作循环" - - **运行 → 审查 → 选择 → 继续。** Agent 在 staged view 中工作,只有 - `apply` 的 Effect 才会进入真实项目。 - -## 开始前 - -你需要 macOS 或 Linux、一个项目目录,以及 `codex` 这样的 Agent 命令。 -先安装 CLI,并确认两个产品入口都可用: - -```bash -pip install persisting -pvisor --help -pchronicle --help -``` - -macOS 使用 staged host workspace 前,需要安装一次 macFUSE: - -```bash -brew install --cask macfuse -``` - -源码构建、VM 支持和平台要求见[安装指南](../installation.md)。 - -## 1. 在 stage 中运行一个 Agent - -进入项目目录,先使用一个明确的 stage: - -```bash -pvisor run --stage ./runs/task-001 -- codex -``` - -也可以换成你的 Agent 命令。Agent 修改的是 staged view,基础项目保持不变。 -命令结束后,你会得到一个可以审查的 Run Bundle。 - -!!! success "检查点:基础项目仍然安全" - - 在基础项目中运行 `git status`。在 `apply` 之前,不应看到 Agent 的修改。 - -## 2. 审查实际发生的事情 - -先看汇总,再检查 staged view: - -```bash -pvisor review last -pvisor inspect last -- git status --short -``` - -在决定哪些内容越过边界前,检查文件 Effect、实际控制机制、网络证据和警告。 -命令成功并不代表所有请求的 capability 都可用;Run Bundle 会记录实际生效的机制。 - -## 3. 先应用一小块可信修改 - -先应用一个路径,其余内容继续留在 stage 中: - -```bash -pvisor apply last --path src -pvisor review last -``` - -之后可以继续应用另一组依赖闭合的选择: - -```bash -pvisor apply last --include 'tests/**' --exclude 'tests/generated/**' -``` - -完成时使用 `pvisor apply last --all`,或用 `pvisor drop last` 丢弃剩余 Effect。 - -!!! success "检查点:边界由你控制" - - 已接受的批次进入真实项目;剩余批次仍然可以独立审查、应用或丢弃。 - -## 4. 选择下一层能力 - -只为下一次 Run 增加你需要的控制: - -- [多次选择性 apply,并保留检查点](guides/review-apply.md) -- [选择 host、OCI 或 VM 执行环境](guides/execution.md) -- [控制网络访问](guides/network.md) -- [把 Run 捕获为 pChronicle 历史](guides/capture.md) -- [回放或比较 sandbox](guides/sandbox-replay.md) - -要学习配套的历史工作流,请继续阅读[探索第一个 Dataset](../pchronicle/get-started.md)。 diff --git a/docs/src/zh/pvisor/guides/capture.md b/docs/src/zh/pvisor/guides/capture.md deleted file mode 100644 index d31bff92e..000000000 --- a/docs/src/zh/pvisor/guides/capture.md +++ /dev/null @@ -1,42 +0,0 @@ -# 捕获 Agent 轨迹 - -Gateway capture 是 pVisor 的 Run 驱动,由 Run 启停;系统不再提供独立 Gateway 命令或 -守护进程。[Capability 与 Evidence 模型](../concepts/capabilities-and-evidence.md)解释 -Capture 能证明什么,以及它不负责 enforce 什么。 - -先按[安装指南](../../installation.md)安装 `pvisor`;本页直接使用已安装的命令。真实 Agent 可直接通过 `pvisor run` 配置: - -```bash -export DEEPSEEK_API_KEY=sk-... -pvisor run \ - --name deepseek \ - --gateway-mode capture \ - --gateway-route 'name="deepseek", upstream="https://api.deepseek.com/v1", api_key_env="DEEPSEEK_API_KEY"' \ - --gateway-route 'name="*", forward="deepseek"' \ - --gateway-stream-markdown \ - -- claude -``` - -pVisor 启动内嵌 Gateway、向子进程注入代理或 base URL、等待执行、排空捕获并停止 -Gateway。`--gateway-stream-markdown` 生成实时人读投影;使用 -`--record-format json --record-destination ./capture` 写本地 JSONL,或使用 -`--record-format lance --record-destination WAREHOUSE` 启动完整 pChronicle sidecar。 -Dataset 目录、查询、分析、导入导出和只读 Web UI -由 [`pchronicle`](../../pchronicle/get-started.md) 提供。 - -### 事件时间戳 - -每条新落盘的 `EventRecord` 都包含两种对应的墙上时钟字段: - -- `timestamp`:RFC3339 UTC 时间; -- `timestamp_unix_ms`:同一观测时刻的 Unix 毫秒值。 - -Gateway 在接受请求和捕获响应时分别记录时间;最终 Gateway capture sink 还会为旧 -producer 生成的记录兜底补齐这两个字段,pVisor runtime 事件则在生成时同时写入这对值。 -两种表示必须在 1 毫秒内一致。事件排序使用 `source + seq`;时间戳用于墙上时钟关联和 -展示,不作为排序依据。 - -客户端只有使用注入的代理或 base URL 才能被观察;直接 socket 是否受限取决于 executor, -实际隔离边界以 Run Bundle 为准。 - -下一步:[查询捕获的历史](../../pchronicle/get-started.md),或者阅读 [Gateway 内部实现](../design/gateway.md)。 diff --git a/docs/src/zh/pvisor/guides/execution.md b/docs/src/zh/pvisor/guides/execution.md deleted file mode 100644 index 1b97f7c91..000000000 --- a/docs/src/zh/pvisor/guides/execution.md +++ /dev/null @@ -1,200 +0,0 @@ -# 使用 pVisor 运行工作负载 - -pVisor 是 [AgentVisor](../concepts/agentvisor.md) 的一种实现:它为每个 Run 提供 Agent -虚拟执行环境,并把逻辑 Run 映射到所选 execution provider。 - -如果这是第一次运行,请先完成[运行第一个 Agent](../get-started.md),再回到本文选择更底层 -的执行方式。 - -本文详细说明 pVisor 当前支持的 host 与 VM 执行方式。命令行刻意把三个彼此独立的 -问题分开表达: - -1. `--executor` 决定进程使用 host kernel,还是进入 libkrun VM。 -2. `--rootfs host`、`--rootfs `、`--rootfs image=` 决定 VM 使用哪套 Linux rootfs。 -3. `--overlayfs-path` 决定 Agent 看到的绝对路径,`--overlayfs-compose` 按顺序叠加宿主机目录。 - -这里不引入 `--workspace`、`--mount` 等别名,以下参数就是规范接口。 - -## 参数模型 - -| 参数 | 含义 | -| --- | --- | -| `--executor host` | 在 host kernel 上执行命令,也是默认 executor。 | -| `--executor vm` | 用 libkrun 启动 Linux guest kernel。 | -| `--rootfs host` | 仅 Linux:把 host 的 `/` 作为 VM rootfs 的只读 lower;未写 `--executor` 时自动选择 `vm`。 | -| `--rootfs image=` | 直接拉取 OCI 镜像作为 VM rootfs,不依赖 Docker/Podman daemon。VM 默认镜像为 `ubuntu:latest`;为保证可复现性,建议显式固定 tag 或 digest。 | -| `--rootfs DIR` | 使用已经准备好的 Linux rootfs 目录。 | -| `--overlayfs-path PATH` | Agent 看到的绝对视图路径。 | -| `--overlayfs-compose DIR` | 宿主机只读叠加层,按命令行顺序从底层到顶层重复指定;当前 workspace 是隐式底层。 | -| `--stage DIR` | 保存工作区改动的持久 writable stage;并发 Run 或不同模式应使用不同 stage。 | -| `--overlayfs-commit manual` | 保留改动供 review;之后用 `apply` 写回 base,或用 `drop` 丢弃。 | -| `--overlayfs-commit apply` | Run 成功后自动把改动写回 base。 | -| `--overlayfs-commit drop` | Run 结束后自动丢弃改动。 | - -`--rootfs host`、`--rootfs image=`、`--rootfs ` 是互斥的三种 VM rootfs 来源。 -`--rootfs host` 是一个表达明确语义的开关,并不是 `--rootfs /` 或任何 -OverlayFS 参数的别名。 - -省略 `--overlayfs-path` 时,当前目录是默认 workspace 视图;为避免 lower 与自身挂载点递归覆盖,pVisor 会使用每个 Run 的受管 merged mount 路径。 - -## 支持方式总览 - -| Host 平台 | Executor | VM rootfs | 命令看到的工作区 | -| --- | --- | --- | --- | -| macOS | `host` | 不适用 | `--overlayfs-path` 的 staged host view | -| macOS | `vm` | OCI 镜像或准备好的 Linux rootfs | guest 内的 `--overlayfs-path` | -| Linux | `host` | 不适用 | `--overlayfs-path` 的 staged host view | -| Linux | `vm --rootfs host` | 通过 virtio-fs 使用 Linux host `/` | guest 内的 `--overlayfs-path` | -| Linux | `vm` | OCI 镜像或准备好的 Linux rootfs | guest 内的 `--overlayfs-path` | - -### macOS:host executor - -```bash -./target/release/pvisor run --executor host \ - --overlayfs-path /workspace \ - --overlayfs-compose /Users/reiase/workspace \ - --stage ./tmp/macos-host \ - --overlayfs-commit manual \ - -- /bin/bash -``` - -命令使用 macOS kernel 和 host 二进制;工作目录是 base 的 COW 视图。这个方式不会 -提供 Linux kernel。Host 隔离默认采用 safe-best-effort;平台不支持的控制会明确记录并降级。 - -### macOS:OCI rootfs VM - -```bash -./target/release/pvisor run --executor vm \ - --rootfs image=ubuntu:24.04 \ - --overlayfs-path /home/workspace \ - --overlayfs-compose /Users/reiase/workspace \ - --stage ./tmp/macos-vm \ - --overlayfs-commit manual \ - -- /bin/bash -``` - -此时 `/bin/bash` 在 Linux guest 内解析,而不是在 macOS 上解析。OCI rootfs 是不可变 -lower,系统目录写入落到临时 root upper;工作区改动才进入指定的持久 stage。 -macOS 自己的 `/` 不能拿来做这个 VM 的 rootfs,因为 Mach-O 程序和 macOS userland -不能运行在 Linux guest kernel 上。 - -### Linux:host executor - -```bash -./target/release/pvisor run --executor host \ - --overlayfs-path /workspace \ - --overlayfs-compose /home/reiase/workspace \ - --stage ./tmp/linux-host \ - --overlayfs-commit manual \ - -- /bin/bash -``` - -它与 macOS host 方式的结构一致,命令使用 Linux host kernel 和 host userland。 - -### Linux:透明使用 host rootfs 的 VM - -```bash -./target/release/pvisor run --executor vm \ - --rootfs host \ - --overlayfs-path /home/workspace \ - --overlayfs-compose /home/reiase/workspace \ - --stage ./tmp/linux-host-rootfs \ - --overlayfs-commit manual \ - -- /bin/bash -``` - -这是透明 rootfs 路径:guest 换成独立的 Linux kernel,但通过 virtio-fs 把 host `/` -作为 rootfs lower 读取。对 rootfs 的系统级写入进入 VM 的临时 upper,并在 VM 退出时 -丢弃;单独挂载的工作区使用持久 stage,可以 review、apply 或 drop。 - -该方式会让 guest 读取 host rootfs 中当前用户本来可以读取的内容,适合相同所有者的 -本地隔离,不应当被描述成不可信多租户边界。建议始终同时使用 -`--overlayfs-path`。如果省略 path,持久 OverlayFS stage 将描述整个 host `/` 的 -改动,后续 apply 可能以 host rootfs 为目标,操作风险明显更高。 - -### Linux:OCI rootfs VM - -```bash -./target/release/pvisor run --executor vm \ - --rootfs image=ubuntu:24.04 \ - --overlayfs-path /home/workspace \ - --overlayfs-compose /home/reiase/workspace \ - --stage ./tmp/linux-vm \ - --overlayfs-commit manual \ - -- /bin/bash -``` - -该方式同时提供 guest kernel 和由镜像定义的 userland。与 `--rootfs host` 相比,它的 -可复现性更好、暴露的 host 数据更少,代价是需要下载或维护镜像。 - -### 两个平台:使用准备好的 rootfs 目录 - -在支持 VM 的 macOS 或 Linux 上,都可以用已经解包的 Linux rootfs 代替 OCI 镜像: - -```bash -./target/release/pvisor run --executor vm \ - --rootfs /opt/pvisor/rootfs \ - --overlayfs-path /home/workspace \ - --overlayfs-compose /path/to/project \ - --stage ./tmp/prepared-rootfs \ - --overlayfs-commit manual \ - -- /bin/bash -``` - -该目录必须包含与 host CPU 架构匹配的 Linux userland,并包含准备执行的命令。 - -## manual stage 的 review、apply 与 drop - -Run 结束后,使用输出的 Run id,或在没有歧义时使用 `last`: - -```bash -./target/release/pvisor review last -./target/release/pvisor inspect last -- git status --short -./target/release/pvisor apply last --path src -./target/release/pvisor apply last --include 'tests/**' --exclude 'tests/generated/**' -./target/release/pvisor apply last --all -# 或者丢弃: -./target/release/pvisor drop last -``` - -`apply` 默认写回隐式 workspace,也可以给 apply 子命令显式传 `--target`。 -过滤后的 apply 只消费依赖闭包内的选中变更,其余变更继续留在 stage,可再次 -apply 或最终 drop。opaque 目录和硬链接组不会被不安全地拆开;每次成功批次都会 -记录到 `apply-ledger.json`。 -stage 不能包含 base 或 compose layer。stage 位于 base 内时会从 merged view 隐藏, -但把不同 Run 的 stage 放进独立的 `tmp` 子目录通常更容易审计和清理。 - -## Rootfs 与工作区的关系 - -对 VM 来说,rootfs 和工作区是两个不同的 COW 树: - -```text -OCI image / prepared rootfs / Linux host / - │ - └── guest /(临时 root upper) - ---overlayfs-compose(host 叠加层) - │ - └── --overlayfs-path(Agent 视图) -``` - -因此 `--overlayfs-path` 不负责选择 VM 操作系统,`--rootfs host` 也不负责选择项目目录。 -这正是两组参数都保留且不使用别名的原因。 - -## 构建要求和常见错误 - -- macOS 源码构建请执行 `just pvisor`。该 recipe 会构建 release、附加 - `macos-hypervisor.entitlements`、做 ad-hoc 签名并验证签名。未签名的二进制会在 - `krun_start_enter` 阶段失败。 -- Linux VM 需要当前用户能访问 `/dev/kvm`;macOS VM 需要 Apple Silicon、HVF 和 - Hypervisor entitlement。 -- `--overlayfs-path` 必须是绝对 Agent 视图路径且不能包含 `..`; - `--overlayfs-compose` 必须是可读的宿主机目录。 -- Linux 机器不能照搬 `/Users/...` 这样的 macOS 路径,应使用真实 Linux 路径, - 例如 `/home/reiase/workspace`。 -- VM 网络默认使用 OverlayNet `auto`:pVisor 通过 smoltcp 提供 DHCP、合成 DNS 和受策略控制的 - IPv4 TCP;`mode = "off"` 可让 guest 彻底离线。当前不支持通用 UDP、IPv6、ICMP、QUIC - 或入站连接。启用 Gateway capture 时,guest 通过虚拟路由器访问它,不直接暴露 host loopback。 - -下一步可以阅读[审查并应用 Agent 修改](review-apply.md)完成选择性 apply 工作流,或者 -[捕获 Agent 轨迹](capture.md)增加运行证据。 diff --git a/docs/src/zh/pvisor/guides/index.md b/docs/src/zh/pvisor/guides/index.md deleted file mode 100644 index 00248ef40..000000000 --- a/docs/src/zh/pvisor/guides/index.md +++ /dev/null @@ -1,17 +0,0 @@ -# pVisor 使用指南 - -先沿着一个 Run 的生命周期阅读,需要时再把执行连接到持久历史。 - -!!! note "先按结果选择" - - 下面每篇指南都围绕一个具体任务展开。先选择当前最需要的结果,并在操作过程中保留 Run Bundle; - 它是当前平台实际安装了哪些控制机制的权威记录。 - -1. [选择执行环境](execution.md)。 -2. [审查并选择性应用 filesystem Effect](review-apply.md)。 -3. [控制网络访问](network.md)。 -4. [捕获模型流量与轨迹 evidence](capture.md)。 -5. [在新沙箱中回放并续跑 Agent 轨迹](sandbox-replay.md)。 - -文件系统、网络、capture 与 execution provider 的保证彼此独立。请始终通过 Run Bundle -检查当前平台实际安装的机制。 diff --git a/docs/src/zh/pvisor/guides/network.md b/docs/src/zh/pvisor/guides/network.md deleted file mode 100644 index 3b7000806..000000000 --- a/docs/src/zh/pvisor/guides/network.md +++ /dev/null @@ -1,200 +0,0 @@ -# 使用 OverlayNet 控制网络访问 - -OverlayNet 让 pVisor 对网络出口执行允许、拒绝和限速规则。Host/container Run 使用进程内 -HTTP proxy;libkrun VM Run 使用进程内 smoltcp 数据面处理 IPv4 TCP 和 DNS。请结合 -[Capability 与 Evidence 模型](../concepts/capabilities-and-evidence.md)理解这些控制。 - -!!! warning "安全边界取决于 driver" - Host/container 的显式 proxy 是 cooperative 的,程序可通过删除 proxy 变量或直接创建 - socket 绕过。VM `auto` 的 virtio-net 终止于 pVisor,因此对整个 guest 进程树不可绕过。 - VM MVP 只支持 IPv4 TCP 和 DNS;UDP、IPv6、ICMP、QUIC 与入站转发都会 fail closed。 - -## 只允许声明的目标 - -在 Agent 命令之前传入一个或多个 `--overlaynet-allow`: - -```bash -pvisor run \ - --overlaynet-allow api.openai.com:443 \ - --overlaynet-allow pypi.org:443 \ - -- agent-command -``` - -只要出现 allow 规则,pVisor 就会启用 OverlayNet,并把默认动作切换为拒绝。上例中, -经过代理的流量只能访问两个列出的 HTTPS 目标,其他目标都会被拒绝。 - -## 选择 driver 模式 - -使用 `--overlaynet off|auto|proxy` 作为 OverlayNet 的主要开关: - -| 模式 | Executor | 边界 | -|---|---|---| -| `off` | 任意 | 关闭 OverlayNet | -| `proxy` | Host/container | cooperative host proxy | -| `auto` | VM(推荐) | 不可绕过的 smoltcp 数据面 | - -省略该参数时,策略参数和 Gateway capture 会按 executor 自动推导模式。 - -## 选择策略 - -策略参数用于配置已选择的 driver(未显式指定模式时会自动推导): - -| 目标 | 参数 | 对其他代理流量的处理 | -|---|---|---| -| 只允许指定目标 | `--overlaynet-allow TARGET` | 拒绝 | -| 拒绝指定目标 | `--overlaynet-deny TARGET` | 允许 | -| 拒绝全部被接管的出口 | `--overlaynet-deny-all` | 拒绝 | -| 限制带宽 | `--overlaynet-limit [TARGET=]RATE` | 不改变允许/拒绝动作 | - -allow、deny 和 limit 参数都可以重复。显式 deny 的优先级高于 allow。 -`--overlaynet-deny-all` 是独立策略,不能与其他策略参数组合。 - -目标可以是精确 hostname、通配后缀、IP 或 CIDR,并可附带端口: - -```bash -pvisor run \ - --overlaynet-allow '*.example.com:443' \ - --overlaynet-allow 203.0.113.10:443 \ - --overlaynet-deny 169.254.0.0/16 \ - -- agent-command -``` - -### 拒绝全部代理流量 - -```bash -pvisor run --overlaynet-deny-all -- agent-command -``` - -对于 host/container Run,它会拒绝到达注入代理的 HTTP/HTTPS 请求,但不会禁用 direct -socket,也不会阻止本地 Gateway route。对于 VM `auto`,同一策略会拒绝普通 guest TCP -出口;启用 capture 时,内部 Gateway route 仍可用。 - -`--overlaynet-deny-all` 不支持再叠加 allow 例外。如果目标是“默认全部拒绝,只允许少数 -地址”,不要先写 deny-all,直接声明允许的目标即可: - -```bash -pvisor run \ - --overlaynet-allow api.openai.com:443 \ - --overlaynet-allow pypi.org:443 \ - -- agent-command -``` - -只要存在 `--overlaynet-allow`,pVisor 就会自动采用 allowlist 策略:匹配的目标允许, -其余经过代理的目标默认拒绝。 - -### 限制带宽 - -同时设置全局限制和更严格的目标限制: - -```bash -pvisor run \ - --overlaynet-limit 10mbps \ - --overlaynet-limit api.openai.com:443=2mbps \ - -- agent-command -``` - -多个匹配的限制会叠加,最终采用最严格的有效速率。`kbps`、`mbps`、`gbps` 表示每秒 -比特数;`kb/s`、`mb/s`、`gb/s` 表示每秒字节数。限速只约束流量,不会授予访问权限。 - -## 使用结构化规则 - -当规则需要多个端口、transport 约束,或者 hostname 有意解析到私网地址时,使用 TOML: - -```toml -[run] -command = ["agent-command"] - -[overlaynet] -mode = "auto" # VM 使用 smoltcp;host/container 使用 "proxy" -policy = "allowlist" - -[[overlaynet.rules]] -host = "api.example.com" -ports = [443] -transports = ["tcp_tunnel"] -allow_private_ips = false - -[[overlaynet.deny]] -host = "169.254.0.0/16" - -[[overlaynet.limits]] -host = "api.example.com" -port = 443 -bytes_per_second = 250000 -``` - -运行: - -```bash -pvisor run --spec run.toml -``` - -transport 支持 `http`、`https` 和 `tcp_tunnel`。`ports` 或 `transports` 为空时,表示该 -维度不受限制。 - -hostname 规则默认拒绝解析到私网或 loopback 的地址。有意访问私有服务时,优先使用 -明确的 IP/CIDR 规则;也可以在范围足够窄的 hostname 规则上设置 -`allow_private_ips = true`。link-local 等其他特殊地址段仍需显式 IP 或 CIDR 规则。 - -如果 host 使用 DNS/TUN fake-IP connector,VM 出站只会在逻辑 hostname 与 port 已通过 -授权后,把 `198.18/15` 结果视为不透明的 connector alias;guest 不能把该网段作为 IP -literal 直接连接。connector 不暴露最终真实地址,因此需要对 hostname 解析结果执行 -IP/CIDR 策略时,应使用能返回具体地址的 resolver。 - -## 理解哪些客户端会被控制 - -对于 host/container Run,pVisor 会向 Agent 进程注入 `HTTP_PROXY`、`HTTPS_PROXY`、 -对应的小写形式和 `ALL_PROXY`。遵守这些设置的 HTTP 客户端会经过 OverlayNet;代理 -支持普通 HTTP 转发和 HTTPS `CONNECT` 隧道。 - -以下路径不在这个 cooperative host/container 策略边界内: - -- 客户端忽略或删除代理环境变量; -- 目标被加入 `NO_PROXY`; -- 程序直接创建 socket; -- 不经过 HTTP proxy 的 DNS 和 UDP 流量。 - -因此 host/container cooperative-proxy Run 会报告 -`safety.network_non_bypassable = false`。如果必须 -彻底阻止直接联网,使用 `pvisor -- --overlaynet-deny-all`:Linux 会创建私有 -network namespace;macOS 会用 Seatbelt 阻断非 loopback IP 与宿主 ambient Unix socket,同时保留 -loopback proxy、精确的 AgentCtl 和 Run 私有目录内 IPC。Container Run 也可以使用 `--container-network none`。 -两种本地 host 路径上的 selective allow/deny 仍是协作式。VM executor 默认使用 -`[overlaynet] mode = "auto"`,由 smoltcp 提供 DHCP、合成 DNS 与受策略控制的 IPv4 TCP; -`mode = "off"` 会让 VM 离线。Gateway capture 通过 guest 虚拟路由器暴露;container -executor 使用进程内 proxy 时仍要求 `--container-network host`。 - -## 检查运行结果 - -当前目录默认就是可重复使用的 workspace;每次调用都会在 pVisor 默认记录根目录下保留 -一条独立 Run: - -```bash -pvisor run \ - --overlaynet-deny 169.254.0.0/16 \ - -- agent-command - -pvisor review --json last | jq '{policy: .network.policy, - interception: .network.interception, - counters: .network.intercepted, - non_bypassable: .safety.network_non_bypassable}' -``` - -这些 counter 描述由当前 OverlayNet driver 处理的流量。它们无法统计绕过 cooperative -host/container proxy 的流量;VM smoltcp profile 在已支持的 TCP/DNS 数据面之外没有 -guest 网络旁路。 - -## 常见问题 - -| 现象 | 检查项 | -|---|---| -| 已允许的 hostname 解析到 loopback 或私网地址后仍被拒绝 | 使用显式 IP/CIDR,或仅在范围足够窄的结构化规则上设置 `allow_private_ips = true` | -| 使用 `--overlaynet-deny-all` 后请求仍然成功 | 确认客户端遵守注入的 proxy,且没有使用 `NO_PROXY` 或 direct socket | -| pVisor 无法绑定代理端口 | 使用 `--overlaynet-listen 127.0.0.1:19082` 选择一个空闲的非零端口 | -| 容器无法连接代理 | 使用 `--container-network host` | -| VM 的 `proxy` 模式被拒绝 | 使用 `auto` 选择 smoltcp,或使用 `off` 让 guest 离线 | - -可以运行 -[`examples/pvisor/03-network-isolation`](https://github.com/DeepLink-org/Persisting/tree/main/examples/pvisor/03-network-isolation) -离线复现 allowlist、deny-all 和 direct-socket bypass。需要捕获 LLM 请求或配置模型路由时, -继续阅读 [Capture 指南](capture.md)。 diff --git a/docs/src/zh/pvisor/guides/review-apply.md b/docs/src/zh/pvisor/guides/review-apply.md deleted file mode 100644 index c4504a095..000000000 --- a/docs/src/zh/pvisor/guides/review-apply.md +++ /dev/null @@ -1,93 +0,0 @@ -# 审查并应用 Agent 修改 - -Agent 可以在 staged workspace 内自由工作,但不会自动获得修改基础项目的权限。Run -结束后,由你决定哪些 Effect 可以跨过这条边界。这个工作流应用文件系统 -[Effect 模型](../concepts/run-model.md)。 - -!!! note "开始前" - - 你需要一个项目目录和一个可以运行 Agent 的命令。如果还没有完成第一次运行,请先完成[运行第一个 Agent](../get-started.md)。 - 下面的流程假设你希望手动决定 staged 修改何时进入基础项目。 - -## 使用 manual stage 运行 - -最短命令是: - -```bash -pvisor -- codex -``` - -也可以显式指定路径和提交方式: - -```bash -pvisor run \ - --overlayfs-compose "$PWD" \ - --stage /tmp/my-agent-stage \ - --overlayfs-commit manual \ - -- codex -``` - -每个并发 Run 必须使用独立 stage。 - -## 接受前先审查 - -```bash -pvisor review last -pvisor inspect last -- git status --short -``` - -`review` 汇总 Run Bundle、文件 Effect、网络证据与安全警告。`inspect` 在 staged view 中 -执行只读检查命令。 - -## 只应用选中的文件 - -可以按路径或 pattern 选择: - -```bash -pvisor apply last --path src -pvisor apply last --include 'tests/**' --exclude 'tests/generated/**' -``` - -过滤后的 apply 只消费选中的、依赖闭合的变更批次。Opaque directory 与 hard-link group -不能在会产生无效结果时被强行拆分。 - -## 多次 apply - -应用一部分修改不会关闭 stage。可以重新审查剩余内容,再应用下一批: - -```bash -pvisor review last -pvisor apply last --path docs -pvisor review last -pvisor apply last --all -``` - -成功批次记录在 `apply-ledger.json` 中。你可以随时停止,并丢弃尚未应用的修改: - -```bash -pvisor drop last -``` - -!!! tip "验证结果" - - 每次批量 apply 后,都要对比 staged view 与基础项目。`apply` 成功只表示选中的、依赖闭包完整的批次已经跨过边界, - 不表示所有剩余 Effect 都已应用。 - -## 从已接受状态继续 - -开始下一条工作线之前创建 checkpoint: - -```bash -pvisor checkpoint last --name accepted-base -pvisor fork last --checkpoint accepted-base -- codex -``` - -新 Run 拥有独立 identity 与 stage,同时保留到该 checkpoint 的 lineage。 - -## 这条边界不覆盖什么 - -选择性 apply 只治理 staged filesystem Effect。它不能撤销消息、支付、部署、直接数据库 -写入或外部 API mutation。这些 Effect 需要执行前 admission、Provider 支持时的 containment, -以及执行后的持久 evidence。 - -下一步:[控制网络访问](network.md)或[捕获 Run](capture.md)。 diff --git a/docs/src/zh/pvisor/guides/sandbox-replay.md b/docs/src/zh/pvisor/guides/sandbox-replay.md deleted file mode 100644 index 1b384e4b1..000000000 --- a/docs/src/zh/pvisor/guides/sandbox-replay.md +++ /dev/null @@ -1,842 +0,0 @@ -# SandboxReplay - -SandboxReplay 是 pVisor 的 Agent 轨迹回放能力。它面向用户已经创建好的新沙箱,重新执行原始轨迹中恢复边界之前的工具调用,用新沙箱产生的 observation 重建 Agent 原生上下文,然后从边界后继续运行。它会在 -[Run 与 Attempt 模型](../concepts/run-model.md)中创建派生 Run。 - -## 1. 基本概念 - -一条 Agent 轨迹可以表示为: - -~~~text -任务 → A1 → O1 → ... → AN → ON → A(N+1) → ... -~~~ - -- `Ai`:模型产生的第 i 个动作,可包含可见文本、reasoning 和一个或多个工具调用; -- `Oi`:执行 `Ai` 后返回给模型的 observation; -- `N`:选定的恢复边界; -- `A(N+1)`:原始轨迹在恢复边界后的下一次模型回复; -- `A′(N+1)`:在新沙箱回放前 N 个动作后,模型生成的第一次续跑回复。 - -如果一个 assistant response 包含多个并行工具调用,它们属于同一个动作批次,恢复边界不能位于批次内部。 - -## 2. 回放语义 - -SandboxReplay 依次完成两件事: - -1. 在新沙箱中按原顺序重新执行 `A1...AN` 的工具调用,产生当前沙箱真实的 `O′1...O′N`; -2. 保留 Agent 原生 system prompt、工具定义、任务和历史动作,用 `O′1...O′N` 替换旧 observation,然后直接请求下一次模型推理。 - -第一次续跑请求必须准确结束在 `O′N`: - -~~~text -Agent 原生 system prompt + 工具定义 + 原始任务 - + A1 → O′1 → ... → AN → O′N - ^ 请求在这里结束 -~~~ - -`O′N` 后不得添加“请继续”“Continue from where you left off”等额外消息。回放保证恢复流程和消息边界正确,但不保证 `A′(N+1)` 与 `A(N+1)` 逐字一致;文件状态、工具输出中的动态字段以及模型采样都可能改变下一动作。 - -如果用户明确希望改变边界后的第一次推理,可以配置 -`boundary_user_prompt`: - -~~~text -Agent 原生 system prompt + 工具定义 + 原始任务 - + A1 → O′1 → ... → AN → O′N - + boundary_user_prompt → A′(N+1) -~~~ - -该提示词只在 `O′N` 之后、第一次实时模型推理之前注入一次,不替换原始任务。 -`prepare-only` 和 `replay-only` 不发起实时模型请求,因此不会注入。未配置时仍保持 -“请求准确结束在 `O′N`”的原有语义。 - -## 3. Agent 适配 - -### 3.1 Claude Code - -Claude Code 使用原生 JSONL/UUID session。SandboxReplay 解析活动 parent UUID 链,重放前 N 个完整工具批次,将新 observation 写入重建 session,然后通过 `claude --resume` 启动续跑。 - -Claude Code 的 resume transport 会在模型请求前插入临时消息,因此 SandboxReplay 启动一个仅供本次续跑使用的本地协议桥。该桥验证并删除准确匹配的临时 envelope,使第一次模型请求仍结束在 `O′N`。它不启用 pVisor Gateway,也不捕获或持久化模型流量。 - -异步子 Agent 的 `Agent` 与 `TaskOutput` 被视为 Claude Code 原生工具。恢复边界必须选在能够由原生 session 无歧义重建的位置。 - -### 3.2 OpenHands - -OpenHands 使用原生 event trajectory 和 ReplayManager。SandboxReplay 提取前 N 个 Action,OpenHands Runtime 在新沙箱中执行这些 Action 并产生新的 Observation;replay queue 耗尽后,OpenHands 直接发起续跑请求。 - -### 3.3 mini-swe-agent - -mini-swe-agent 使用原生 `mini-swe-agent-1.1` messages。配套 runner 保留原始 system 和任务消息,在新沙箱中重放前 N 个 action,并使用原生 observation formatter 将结果加入 `agent.messages`,随后直接调用下一次 `agent.step()`。 - -### 3.4 Pi agent - -Pi agent 适配固定支持 `@earendil-works/pi-coding-agent` `0.83.0`,输入为 -Pi 原生 RPC event JSONL。一个 replay step 对应一个完整的 `turn_end` 工具批次。 -SandboxReplay 使用 Pi 自身的工具实现重新执行 `read`、`bash`、`edit`、`write`, -将新 observation 写入新建的 Pi v3 session,再通过 Pi SDK 从边界续跑。轨迹包含 -这四种工具之外的调用时会拒绝执行,避免静默改变工具语义。未配置边界提示词时调用 -Pi 的原生 `continue()`;配置后则在 `O′N` 后通过 `prompt()` 追加一次用户消息。 - -### 3.5 OpenCode - -OpenCode 适配固定支持 `1.17.7`,输入为 `opencode run --format=json` 产生的原生 -事件 JSONL。`user`、`step_start`、`text`、`reasoning`、`tool_use` 和 -`step_finish` 事件会按 step 分组;一个 replay step 对应一个完整的工具调用批次。 -SandboxReplay 在新沙箱中重新执行命令型工具以及 `read`、`write`、`edit` 文件工具, -用新的 `state.output` 重建前缀,并通过 `opencode run --format=json --session` 从边界 -继续。工具执行期间的中间 `tool_use` 状态会合并,避免同一调用被重复回放。 - -`opencode run` 没有 CLI 步数参数,且 1.17.7 在 resume 会话上不执行 -`agent.build.steps` 配置预算。SandboxReplay 因此为 OpenCode 引入了两个外部看门狗 -(对其它 Agent 不启用),详见下文「OpenCode 看门狗」。 - -#### OpenCode 看门狗 - -OpenCode 是目前唯一既以黑盒 CLI 形式续跑、又缺乏可靠停止机制的 Agent: -Claude Code 的 `max_turns`、Codex 的 `agent_max_steps`、mini-swe-agent 的 -`step_limit` 都在 resume 上原生生效,mini-swe 与 Pi 的循环本身由 pVisor 驱动; -而 OpenCode 两头都不占。看门狗由 `run_process` 的两个监督条件实现,仅对 -OpenCode 续跑启用。 - -**为什么必须引入(两个实测问题)** - -1. **resume 会话无视步数预算(上游缺陷)**。`agent.build.steps` 在全新会话上 - 原生生效,但用 `opencode import` + `run --session` 恢复的会话完全忽略该 - 配置。绕开 pVisor 的裸实验可直接复现:新会话设 `steps=2` 恰好 2 步停; - resume 会话设 `steps=3` 连跑 32 步以上不停。pVisor 已把剩余预算 - (`max_steps - after_step`)写入隔离配置,但该值目前不被读取。贪心采样下 - 模型还会陷入无限循环(实测单回合刷 24 万 token、本地循环 200+ 回合), - 若无外部干预,续跑会一直占用沙箱直到外层 agent 超时(小时级)。 -2. **续跑进程偶发静默僵死**。实测抓到过:CLI 进程存活但无网络连接、无工具 - 子进程、事件循环空转,既不推进也不退出。根因在 OpenCode/Bun 一侧,且不会 - 自行恢复;同样会耗尽整个 agent 超时窗口。 - -**如何解决(两个看门狗的机制)** - -| 看门狗 | 触发条件 | 动作 | -|---|---|---| -| 步数看门狗 | stderr 进度日志中的回合行数达到剩余预算 | SIGINT 优雅终止 | -| 空闲看门狗 | stdout/stderr 连续 10 分钟无任何输出(僵死特征) | 终止进程组 | - -步数看门狗的信号源经过专门筛选。OpenCode 的 stdout JSONL 事件流被 Bun 运行时 -按 ~8KB 块缓冲(管道、PTY、文件重定向均非实时),不能作为计数源;`--print-logs` -输出到 stderr 的进度日志实时流式,且每个模型回合固定产生一行 -`message=loop ... step=N`。续跑命令因此固定附加 `--print-logs`,pVisor 实时统计 -该行数,达到剩余预算即向进程组发 SIGINT——SIGINT 下 OpenCode 会优雅退出并刷出 -缓冲的全部事件;若进程被更强信号杀死导致 stdout 缓冲丢失,则从 OpenCode 的 -sqlite 会话库重建续跑事件(任务沙箱自带 python3,无需新增依赖),续跑轨迹不丢。 - -终止结果如实上报:步数看门狗触发时 `agent_status` 为 `max_steps`,结果 metadata -携带 `opencode_step_budget` 标记(`enforced_by: pvisor_event_watchdog`)。 -隔离配置中的 `agent.build.steps` 仍然保留:一旦上游修复 resume 会话读取该配置, -原生预算将直接生效,看门狗自动退化为兜底保险。 - -### 3.6 Codex - -Codex 适配固定支持 CLI `0.149.0`,输入为 Codex 原生 rollout JSONL。一个 replay step -对应一组完整的 `function_call`/`custom_tool_call` 及其 outputs。SandboxReplay 在新沙箱 -中重放前缀工具,将原生 `response_item` 前缀写入隔离的 `CODEX_HOME`,再执行 -`codex exec resume --json`。native session ID 从 `session_meta` 自动提取; -未知工具或缺少 native session ID 时显式失败,不会退化成全新会话。 - -Codex 保留自身的 system prompt、工具定义和任务上下文,SandboxReplay 只替换边界前的 -observation。为兼容需要 resume prompt 的旧版 CLI,SandboxReplay 使用本地 Responses -bridge 删除 transport nonce,再将请求转发到模型服务;续跑结束后也会从 native trajectory -中清理 nonce。默认模式的输入条件为 `replayed_boundary_only`,不会向模型注入额外提示词。 - -配置 `boundary_user_prompt` 时,提示词只在 `O′N` 后注入一次并保留;此时输入条件标记为 -`boundary_user_prompt_appended`。bridge 校验失败会拒绝续跑,不降级为直连。 - -## 4. 使用方式 - -### 4.1 安装 pVisor - -pVisor CLI 随 Persisting wheel 发布。沙箱内有 Python 3.10 或更高版本时,推荐直接 -安装发布版: - -~~~bash -python -m pip install persisting - -command -v pvisor -pvisor --version -pvisor replay --help -~~~ - -如果需要测试尚未发布的 SandboxReplay 代码,可以在目标沙箱中从源码只安装 pVisor: - -~~~bash -git clone https://github.com/DeepLink-org/Persisting.git -cd Persisting -cargo install --locked \ - --path crates/persisting-pvisor \ - --bin pvisor - -export PATH="${CARGO_HOME:-$HOME/.cargo}/bin:$PATH" -pvisor replay --help -~~~ - -开发时也可以不安装,直接构建并使用仓库内二进制: - -~~~bash -cargo build --release -p persisting-pvisor --bin pvisor -./target/release/pvisor replay --help -~~~ - -`pvisor replay` 通常直接运行在用户已经创建的新沙箱中,因此 pVisor 必须安装在该 -沙箱内,或者以只读方式挂载到沙箱的 `PATH`。如果在沙箱外构建后复制二进制,构建机 -与目标沙箱的操作系统、CPU 架构和动态链接运行时必须兼容。Agent runtime 也必须与 -所选 replay profile 的固定版本一致。 - -### 4.2 CLI 与 TOML - -SandboxReplay 默认假设用户已经创建了一个新沙箱,并在沙箱中直接运行: - -~~~bash -pvisor replay \ - --agent claude-code \ - --trajectory /input/session.jsonl \ - --after-step 30 \ - --agent-entrypoint /usr/bin/claude \ - --boundary-user-prompt '请检查新的 observation 后继续任务' -~~~ - -Pi agent runtime 安装在 `/opt/pi-agent` 时,命令为: - -~~~bash -pvisor replay \ - --agent pi-agent \ - --trajectory /input/pi-agent.events.jsonl \ - --after-step 30 \ - --agent-entrypoint /opt/pi-agent/bin/pi -~~~ - -Pi agent 等价的 pVisor TOML 配置为: - -~~~toml -[replay] -agent = "pi-agent" -trajectory = "/input/pi-agent.events.jsonl" -after_step = 30 -agent_entrypoint = "/opt/pi-agent/bin/pi" -max_steps = 200 -disable_thinking = true -~~~ - -OpenCode 使用原生事件 JSONL: - -~~~bash -pvisor replay \ - --agent opencode \ - --trajectory /input/opencode.jsonl \ - --after-step 30 \ - --agent-entrypoint /usr/bin/opencode -~~~ - -Codex 使用原生 rollout JSONL;SandboxReplay 会自动读取轨迹中的 Codex native -session ID,用户无需手工填写 `session_id`: - -~~~bash -pvisor replay \ - --agent codex \ - --trajectory /input/rollout.jsonl \ - --after-step 30 \ - --agent-entrypoint /usr/bin/codex -~~~ - -两者也可使用同一个 TOML 文件,只需将 `[replay].agent`、`trajectory` 和 -`agent_entrypoint` 分别改为 `opencode` 或 `codex`。 - -Claude Code 等价的 pVisor TOML 配置为: - -~~~toml -[replay] -agent = "claude-code" -trajectory = "/input/session.jsonl" -after_step = 30 -agent_entrypoint = "/usr/bin/claude" -max_steps = 200 -session_id = "task-291-attempt-1" -replay_only = false -disable_thinking = true -boundary_user_prompt = "请检查新的 observation 后继续任务" -~~~ - -### 4.3 执行模式与结果 - -- 默认模式会执行选中的前缀,然后启动 Agent 继续运行; -- `--replay-only` 会执行前缀,但在下一次模型请求前停止; -- `--prepare-only` 只校验并构造前缀,不执行工具、不启动 Agent,也不要求 Agent runtime。 - -`--max-steps` 是包含回放前缀在内的 Agent 动作总预算。例如 -`--after-step 30 --max-steps 50` 最多留下 20 个续跑动作。仅回放模式的预算必须覆盖前缀;续跑模式还必须至少留下一个实时动作。 - -结果协议为 `sandbox-playback.result/v3`:`phase` 为 `prepared`、`replayed` -或 `continued`;`quality` 为 `verified` 或 `degraded`;`agent_status` 区分 -`not_started`、`completed`、`max_steps` 与 `failed`。失败结果会保留已经生成的日志和原生轨迹。即使 OpenHands 进程返回 0,只要控制器报告 fatal 状态,结果仍为失败。 - -成功结果的 metadata 记录边界提示词是否请求和注入,以及字符长度和 SHA-256; -replay journal 不记录提示词明文;Agent 原生的 prepared 或 continued trajectory -可能包含这条 user 消息。对于 Claude Code,内存桥只把提示词加入第一次清理后的 -上游请求,不修改重建的原生 session。配置提示词后, -`next-action-comparison.json` 的输入条件为 `boundary_user_prompt_appended`。 -此时文本相似度和工具一致性仅供观察,不能解释为相同输入下的 replay 一致性。 - -迁移说明:旧版非 Claude 配置有时使用 `replay_only = true` 表示只构造前缀、不执行。现在应改为 `prepare_only = true`;v3 的 replay-only 一定会执行选中的前缀,因此需要精确版本的 runtime。无法重新生成的 Claude observation 默认失败,只有显式指定 `--allow-stale-observations` 才会复用并把质量标记为 `degraded`。 - -`disable_thinking` 也可以通过 `--disable-thinking` 指定。只有显式提供 `--executor`、`--stage`、`--overlayfs-path` 或 `--overlayfs-compose` 等运行参数,或在 TOML 中增加 `[run]`、`[overlayfs]`、`[overlaynet]`,才会在回放外层创建受管的 `pvisor run`。 - -完整参数见 [`pvisor replay` 命令参考](../reference/cli.md#replay-an-agent-trajectory)。 - -## 5. Qwen3.6 实测结果 - -### 5.1 测试设置 - -- 模型:`Qwen3.6-35B-A3B`; -- reasoning/thinking:关闭; -- 题目:NodeBB(291)、Vuls(666)、qutebrowser(667); -- Agent:Claude Code、OpenHands、mini-swe-agent、Pi agent、OpenCode; -- 每个 Agent 先在新沙箱中生成原始轨迹,再创建另一个新沙箱,仅使用 pVisor SandboxReplay 续跑; -- OpenCode 录制与续跑均使用贪心采样:OpenCode 不透传采样参数,SweEval 与续跑桥在 - wire 级注入 `temperature=0`、`top_p=1`,thinking 经 `chat_template_kwargs` 关闭; -- 每个沙箱资源:2 CPU、7 GiB 内存、70 GiB 存储; -- `N` 按 Rust 解析器识别出的完整原生工具批次序号统计;原始和续跑总步数按相应 - Agent 原生轨迹中的 turn/action 数统计; -- 文本相似度只比较归一化后的可见文本,不包含 reasoning; -- “工具完全一致”要求工具数量、顺序、名称和 JSON 参数均一致。 - -### 5.2 完整续跑与下一动作汇总 - -| Agent | 题目 | N | 原始总步数 | 续跑总步数 | 原始 Reward | 本次 Reward | A′(N+1) 工具完全一致 | 文本相似度 | -|---|---|---:|---:|---:|---:|---:|---|---:| -| Claude Code | NodeBB(291) | 1 | 69 | 101 | 1 | 1 | 是 | 0.85 | -| Claude Code | Vuls(666) | 28 | 76 | 200 | 0 | 0 | 否 | 0.45 | -| Claude Code | qutebrowser(667) | 36 | 46 | 46 | 1 | 1 | 否 | 0.48 | -| OpenHands | NodeBB(291) | 28 | 62 | 68 | 1 | 0 | 是 | 1.00 | -| OpenHands | Vuls(666) | 25 | 43 | 43 | 1 | 1 | 是 | 1.00 | -| OpenHands | qutebrowser(667) | 17 | 36 | 87 | 1 | 1 | 否 | 0.13 | -| mini-swe-agent | NodeBB(291) | 48 | 104 | 115 | 1 | 1 | 否 | 1.00 | -| mini-swe-agent | Vuls(666) | 39 | 94 | 91 | 1 | 1 | 是 | 1.00 | -| mini-swe-agent | qutebrowser(667) | 31 | 115 | 65 | 1 | 1 | 否 | 0.77 | -| Pi agent | NodeBB(291) | 54 | 111 | 92 | 1 | 1 | 是 | N/A | -| Pi agent | Vuls(666) | 38 | 77 | 62 | 1 | 1 | 否 | 1.00 | -| Pi agent | qutebrowser(667) | 37 | 71 | 91 | 1 | 1 | 否 | 0.39 | -| Codex | NodeBB(291) | 37 | 74 | 88 | 1 | 1 | 否 | 0.42 | -| Codex | Vuls(666) | 28 | 57 | 69 | 1 | 1 | 否 | 0.47 | -| Codex | qutebrowser(667) | 27 | 54 | 43 | 1 | 1 | 是 | 0.97 | -| OpenCode | NodeBB(291) | 20 | 40 | 44 | 1 | 1 | 否 | 0.65 | -| OpenCode | Vuls(666) | 7 | 16 | 31 | 1 | 1 | 否 | N/A | -| OpenCode | qutebrowser(667) | 15 | 31 | 39 | 1 | 1 | 是 | 0.76 | - -Claude Code / NodeBB 使用 `N=1`,以避开异步子 Agent 完成后的 Resume Transport canonical-prefix 歧义。边界后的原始可见文本非空,且 `A′(N+1)` 成功复现同一个 `TaskOutput` 调用。 - -Pi agent 三题分别在新的任务沙箱中并发执行完成,均未发生模型请求重试,三次 -verifier Reward 均为 1。NodeBB 边界两侧的下一动作可见文本均为空,因此文本相似度 -按当前指标语义记为 N/A,而不是把两个空字符串报告为 1.00。 - -Codex 三题使用 `Ornith-1.5-35B-A3B`,原始轨迹来自 r24,续跑轨迹来自最终成功的 -r30;续跑使用新的任务沙箱和默认 `replayed_boundary_only` 输入条件。Codex 轨迹中未 -发现 transport nonce 或 `Continue from the replay boundary.` 消息。 - -OpenCode 三题(`1.17.7`)与上表其余 Agent 相同使用 `Qwen3.6-35B-A3B`,贪心采样, -三题并发录制与并发续跑均一次通过,verifier Reward 全部为 1;Vuls(666)与 -qutebrowser(667)另复跑一轮续跑,Reward 与下一动作行为一致(可复现)。续跑的 -`max_steps=N+25` 由 pVisor 事件看门狗在剩余预算处软停,三题 live 步数均为 24。 -无污染在 wire 级验证:首个上游模型请求共 51 项,精确终止于边界观测 `O'N` -(`function_call_output`),传输 nonce 在全部上游请求中出现 0 次, -`boundary_user_prompt` 未注入。OpenCode 关闭 thinking 时常直接调用工具而不输出 -可见文本,此时文本相似度按 Pi agent 的同一语义记为 N/A,而不是把两个空字符串 -报告为 1.00。 - -### 5.3 A(N+1) 与 A′(N+1) - -以下直接展示原始下一步和续跑下一步。reasoning 已移除;较长的整文件替换只保留修改目标和能够区分调用的关键内容。 - -#### Claude Code - -##### NodeBB(291) - -`A(N+1)`: - -~~~text -Let me wait for the exploration agent to complete. - -TaskOutput: - task_id="a7e5a5bf351a66db5" - block=true - timeout=120000 -~~~ - -`A′(N+1)`: - -~~~text -Let me wait for the exploration agent to complete before proceeding. - -TaskOutput: - task_id="a7e5a5bf351a66db5" - block=true - timeout=120000 -~~~ - -区别:工具及参数完全一致,仅文本增加了 “before proceeding”。文本相似度为 0.85。 - -##### Vuls(666) - -`A(N+1)`: - -~~~text -Now let me search for the `Supported` method in the Debian code: - -Read: - file_path="/app/libmanager/libmanager.go" -~~~ - -`A′(N+1)`: - -~~~text -Now I have a clear picture. Let me also check the gost/debian.go file: - -Read: - file_path="/app/gost/debian.go" -~~~ - -区别:都是读取文件,但目标分别为 `libmanager/libmanager.go` 和 `gost/debian.go`。文本相似度为 0.45。 - -##### qutebrowser(667) - -`A(N+1)`: - -~~~text -All tests pass. Let me verify the implementation meets all PR requirements -by checking the final state of the file: - -Read: - file_path="/app/qutebrowser/browser/webengine/webview.py" - offset=1 - limit=60 -~~~ - -`A′(N+1)`: - -~~~text -All test cases pass. Now let me verify the actual test file runs correctly: - -Bash: - command="python -m pytest tests/unit/browser/webengine/test_webview.py::test_suffixes_workaround_extras_returned -v 2>&1" - description="Run the suffixes workaround tests" -~~~ - -区别:原轨迹读取实现文件,续跑执行指定 pytest。文本相似度为 0.48。 - -#### OpenHands - -##### NodeBB(291) - -`A(N+1)`: - -~~~text -Now let me check if there are any tests already written for -`incrObjectFieldByBulk`: - -run: - command="grep -rn \"incrObjectFieldByBulk\\|incrByBulk\\|ByBulk\" /app/test/ 2>/dev/null" - is_input="false" -~~~ - -`A′(N+1)`: - -~~~text -Now let me check if there are any tests already written for -`incrObjectFieldByBulk`: - -run: - command="grep -rn \"incrObjectFieldByBulk\\|incrByBulk\\|ByBulk\" /app/test/ 2>/dev/null" - is_input="false" -~~~ - -区别:文本、工具和参数完全一致。文本相似度为 1.00。 - -##### Vuls(666) - -`A(N+1)`: - -~~~text -Now I have a complete understanding of what needs to be changed. -Let me implement all the required changes: - -edit: - command="str_replace" - path="/app/gost/debian.go" - old_str="func (deb Debian) Supported(major string) bool { ... }" - new_str="func (deb Debian) supported(major string) bool { ... }" -~~~ - -`A′(N+1)`: - -~~~text -Now I have a complete understanding of what needs to be changed. -Let me implement all the required changes: - -edit: - command="str_replace" - path="/app/gost/debian.go" - old_str="func (deb Debian) Supported(major string) bool { ... }" - new_str="func (deb Debian) supported(major string) bool { ... }" -~~~ - -区别:文本、工具和参数完全一致。文本相似度为 1.00。 - -##### qutebrowser(667) - -`A(N+1)`: - -~~~text -Now I have the original file. Let me make the changes properly: - -1. First, I'll move the `extra_suffixes_workaround` function to - module-level (before the `WebEnginePage` class) -2. Then update `chooseFiles` to use the module-level function and be - callable without instance state - -edit: - command="str_replace" - path="/app/qutebrowser/browser/webengine/webview.py" - old_str="acceptNavigationRequest、类内 staticmethod - extra_suffixes_workaround 以及 chooseFiles" - new_str="保留 acceptNavigationRequest;chooseFiles 改为调用模块级函数; - 将 extra_suffixes_workaround 移到模块级" -~~~ - -`A′(N+1)`: - -~~~text -Now I understand the structure. I need to: -1. Move `extra_suffixes_workaround` to module-level -2. Update `chooseFiles` to use the module-level function and be callable - without instance state -3. Keep backward compatibility by assigning the function to the class - -Let me make the changes: - -edit: - command="str_replace" - path="/app/qutebrowser/browser/webengine/webview.py" - old_str="类内 staticmethod extra_suffixes_workaround 以及 chooseFiles 开头" - new_str="模块级 extra_suffixes_workaround; - 随后重新声明 class WebEnginePage(QWebEnginePage)" -~~~ - -区别:都编辑同一文件并尝试移动函数,但替换范围和生成的代码结构不同。文本相似度为 0.13。 - -#### mini-swe-agent - -##### NodeBB(291) - -`A(N+1)`: - -~~~text -Let me just rewrite the entire function properly: - -bash: - cat > /tmp/rebuild_mongo.js << 'ENDOFFILE' - # 读取 /app/src/database/mongo/hash.js - # 用正则提取 incrObjectFieldBy - # 重新生成 incrObjectFieldByBulk - # 重写 mongo/hash.js - ENDOFFILE - - node /tmp/rebuild_mongo.js -~~~ - -`A′(N+1)`: - -~~~text -Let me just rewrite the entire function properly: - -bash: - cat > /tmp/rewrite_mongo_hash.js << 'ENDOFFILE' - # 读取 /app/src/database/mongo/hash.js - # 通过括号计数删除已有 incrObjectFieldByBulk - # 在文件末尾插入新的 incrObjectFieldByBulk - # 重写 mongo/hash.js - ENDOFFILE - - node /tmp/rewrite_mongo_hash.js -~~~ - -区别:可见文本完全一致,但生成脚本、临时文件名和重写策略不同,因此工具参数不一致。文本相似度为 1.00。 - -##### Vuls(666) - -`A(N+1)`: - -~~~text -## Change 2: Fix "Unmarshall" to "Unmarshal" in error messages - -bash: - sed -i 's/Failed to Unmarshall/Failed to Unmarshal/g' /app/oval/oval.go -~~~ - -`A′(N+1)`: - -~~~text -## Change 2: Fix "Unmarshall" to "Unmarshal" in error messages - -bash: - sed -i 's/Failed to Unmarshall/Failed to Unmarshal/g' /app/oval/oval.go -~~~ - -区别:文本、工具和参数完全一致。文本相似度为 1.00。 - -##### qutebrowser(667) - -`A(N+1)`: - -~~~text -The tests are failing due to Qt issues. Let me try a different approach - -let me just understand the code and make the changes based on the PR -description. Let me re-read the test file more carefully: - -bash: - cat /app/tests/unit/browser/webengine/test_webview.py | - grep -A 20 "test_suffixes_workaround_choosefiles_args" -~~~ - -`A′(N+1)`: - -~~~text -The test is failing due to Qt issues. Let me try a simpler approach - -just understand what the test expects and make the code changes. -Let me re-read the test carefully: - -bash: - sed -n '100,140p' /app/tests/unit/browser/webengine/test_webview.py -~~~ - -区别:都读取同一测试文件附近的内容,但分别使用 `grep -A 20` 和 `sed -n 100,140p`。文本相似度为 0.77。 - -#### Pi agent - -##### NodeBB(291) - -`A(N+1)`: - -~~~text -[可见文本为空] - -bash: - command="cd /app && node test-bulk-increment.js 2>&1" -~~~ - -`A′(N+1)`: - -~~~text -[可见文本为空] - -bash: - command="cd /app && node test-bulk-increment.js 2>&1" -~~~ - -区别:工具及参数完全一致;双方可见文本均为空,因此文本相似度为 N/A。 - -##### Vuls(666) - -`A(N+1)`: - -~~~text -**4. Fix "Unmarshall" typo in report/cve_client.go:** - -bash: - command="grep -n \"Unmarshall\" /app/report/cve_client.go" -~~~ - -`A′(N+1)`: - -~~~text -**4. Fix "Unmarshall" typo in report/cve_client.go:** - -bash: - command="sed -i 's/Failed to Unmarshall/Failed to Unmarshal/g' /app/report/cve_client.go" -~~~ - -区别:可见文本完全一致,工具名称均为 `bash`,但命令参数不同。文本相似度为 1.00。 - -##### qutebrowser(667) - -`A(N+1)`: - -~~~text -Let me update the test file to call the module-level function instead of the -static method on the class. This is necessary for my implementation: - -edit: - path="/app/tests/unit/browser/webengine/test_webview.py" - changes="调用模块级 extra_suffixes_workaround;调整 chooseFiles 测试参数和 expected 集合合并" -~~~ - -`A′(N+1)`: - -~~~text -Let me look at the test more carefully and understand what's expected: - -read: - path="/app/tests/unit/browser/webengine/test_webview.py" - offset=115 -~~~ - -区别:原轨迹直接编辑测试文件,续跑先读取测试文件;工具和参数均不同。文本相似度为 0.39。 - -#### Codex - -##### NodeBB(291),N=37 - -`A(N+1)`: - -~~~text -Both files parse. Now let me write a reproduction script that exercises the real NodeBB Redis DB through `db.incrObjectFieldByBulk` to verify all the requirements. -~~~ - -工具:`exec_command`,在 `/app` 下创建并执行 `/tmp/repro.js`,验证 bulk increment、 -负数递增、空输入、非法输入、危险字段名、原子性、缓存失效和多次累加。 - -`A′(N+1)`: - -~~~text -Both files parse. Now let me update the plan and write a comprehensive test script to verify the Redis behavior against a real instance. -~~~ - -工具:`update_plan`,前三个实现步骤已完成,当前步骤为“Write reproduction script and -verify behavior”,后续执行 lint 和 hash test suite。 - -区别:工具和参数不一致,文本相似度为 0.42。 - -##### Vuls(666),N=28 - -`A(N+1)`: - -~~~text -The build cache is now warm and builds succeed. Let me set up a plan and start implementing the changes. -~~~ - -工具:`update_plan`,计划从导出 Debian.Supported、修正 Unmarshall 拼写、补充文档注释、 -增加 Oracle Linux 分支开始。 - -`A′(N+1)`: - -~~~text -The build takes longer than the 10s poll window due to the large dependency tree. Let me run it in the background and poll the log. -~~~ - -工具:`exec_command`: - -~~~bash -cd /app && export PATH=$PATH:/usr/local/go/bin && \ -nohup go build ./gost/ ./scan/ ./oval/ ./report/ > /tmp/build.log 2>&1 & -echo "started $!" -~~~ - -区别:工具和参数不一致,文本相似度为 0.47。 - -##### qutebrowser(667),N=27 - -`A(N+1)`: - -~~~text -Now let me run this gold-style verification test. -~~~ - -工具:`exec_command`: - -~~~bash -cd /app && xvfb-run -a env QT_QPA_PLATFORM=offscreen \ -QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu --disable-dev-shm-usage" \ -python -m pytest tests/unit/browser/webengine/test_webview_goldcheck.py -q 2>&1 | tail -20 -~~~ - -`A′(N+1)`: - -~~~text -Now let me run the gold-style verification test. -~~~ - -工具:`exec_command`,命令和参数与原始轨迹完全一致。 - -区别:工具完全一致,文本相似度为 0.97。 - -#### OpenCode - -##### NodeBB(291),N=20 - -`A(N+1)`: - -~~~text -The tests need Redis running. Let me check if there's a way to start it or if there are unit tests: - -Bash: - which redis-server 2>/dev/null || echo "redis-server not found" - redis-cli ping 2>/dev/null || echo "Redis not running" -~~~ - -`A′(N+1)`: - -~~~text -The tests need Redis running. Let me check if there's a way to run tests with MongoDB instead, or look at the test structure: - -Bash: - cat /app/config.json 2>/dev/null | head -30 -~~~ - -区别:两边以同一句 "The tests need Redis running. Let me check..." 开头(相同前缀 -上下文的强收敛),但探查动作不同:原轨迹检查 Redis 进程,续跑读取应用配置。文本 -相似度为 0.65。 - -##### Vuls(666),N=7 - -`A(N+1)`: - -~~~text -Now let me check if there's an `oracle` type in the scan package that I need to reference: - -grep: - pattern="type oracle struct" path="/app/scan" -~~~ - -`A′(N+1)`: - -~~~text -[可见文本为空白,直接调用工具] - -grep: - path="/app/scan/*.go" -grep: - pattern="Oracle" path="*.go" -~~~ - -区别:工具语义同向——都在第 8 步去 scan 包定位 oracle 类型定义,续跑用两个更宽的 -grep 变体覆盖同一目标。续跑侧可见文本为空白(OpenCode 关闭 thinking 后的常见风格), -文本相似度按指标语义记为 N/A。复跑一轮行为一致。 - -##### qutebrowser(667),N=15 - -`A(N+1)`: - -~~~text -Let me run the tests to see if my changes work: - -Bash: - python -m pytest tests/unit/browser/webengine/test_webview.py::test_suffixes_workaround_extras_returned -v --tb=short 2>&1 | tail -30 -~~~ - -`A′(N+1)`: - -~~~text -Now let me run the tests to see if they pass: - -Bash: - python -m pytest tests/unit/browser/webengine/test_webview.py::test_suffixes_workaround_extras_returned -v --tb=short 2>&1 | tail -30 -~~~ - -区别:同一条 pytest 命令逐字符一致,仅解说文本措辞不同,文本相似度为 0.76。首轮 -续跑的 `A′(N+1)` 可见文本为空白但命令同样逐字复现;两轮续跑 verifier Reward 均为 1。 - -精确参数见 -[`pvisor replay` 命令参考](../reference/cli.md#replay-an-agent-trajectory);执行边界见 -[执行指南](execution.md)。 diff --git a/docs/src/zh/pvisor/guides/troubleshooting.md b/docs/src/zh/pvisor/guides/troubleshooting.md deleted file mode 100644 index 61098310e..000000000 --- a/docs/src/zh/pvisor/guides/troubleshooting.md +++ /dev/null @@ -1,69 +0,0 @@ -# 排查一次 Run - -Run 没有按预期工作时,先查看 Run Bundle,不要立即用更强的参数重复命令。 -Bundle 会记录请求的边界、实际安装的机制,以及限制 Run 能够声称的警告。 - -## 先做三个只读检查 - -在启动 Agent 的项目目录中执行: - -```bash -pvisor status last -pvisor inspect last -- git status --short -pvisor review last -``` - -`status` 告诉你 Run 仍在运行还是已经停止;`inspect` 在 Run view 中执行只读命令, -可以区分 staged 修改与真实项目中的修改;`review` 展示持久化 Run Bundle 和 Evidence, -帮助你在 apply 或 drop 之前做决定。 - -## Agent 直接修改了项目 - -先检查启动命令是否使用 stage: - -```bash -pvisor run --stage ./runs/task-001 -- AGENT_COMMAND -``` - -不使用 stage 时,host executor 仍可能提供 safe-best-effort 控制,但不会产生可以审查和 -选择性 apply 的 staged filesystem Effect。如果本来就需要 stage,请检查记录的 stage 路径和 -executor warning,再重新执行。 - -## 请求的 capability 没有被强制执行 - -把请求参数理解为意图,而不是证据。打开 Run Bundle,查看实际 capability record 与对应机制。 -不同操作系统和 executor 的支持范围不同;例如 cooperative network proxy 不能保证所有 -ambient connection 都被阻断。 - -继续阅读[Capabilities 与 Evidence](../concepts/capabilities-and-evidence.md)和[执行环境](execution.md)。 - -## stage 为空或出现了错误文件 - -先检查命令工作目录和 stage 路径: - -```bash -pvisor inspect last -- pwd -pvisor inspect last -- git status --short -``` - -Agent 修改的是 Run-owned view。写入该 view 之外的路径可能被记录为 external Effect,也可能 -无法被 executor 提供。保持 stage 位于预期项目边界内,不要只按文件名比较生成的 Run 目录与项目根目录。 - -## capture 或 Dataset 输出缺失 - -执行审查和轨迹捕获是两个独立决策。先确认 Run 已完成且 Bundle 可读,再使用 pChronicle 检查 -capture 配置和目标 Dataset: - -```bash -pchronicle ls ./trajectory-data -pchronicle analysis overview ./trajectory-data -``` - -如果 Dataset 不存在,阅读[捕获 Agent 轨迹](capture.md),确认目标路径后再启动新的 Run。 -本地 Run Bundle 不会自动变成 pChronicle Dataset。 - -## 提交 issue 前 - -请提供 pVisor 版本、操作系统、executor、完整命令,以及相关的 `status` 和 `review` 输出, -并移除凭据和私有 workspace 内容。最有帮助的问题描述会说明请求了什么 capability、Bundle -记录了什么机制,以及实际结果在哪里不同。 diff --git a/docs/src/zh/pvisor/index.md b/docs/src/zh/pvisor/index.md deleted file mode 100644 index 59c176630..000000000 --- a/docs/src/zh/pvisor/index.md +++ /dev/null @@ -1,71 +0,0 @@ -# pVisor - -pVisor logo - -**pVisor 在受控执行环境中运行现有的 Agent 命令。** 它为每个 Run 提供独立的工作区边界, -记录实际生效的控制机制,并让你在文件变更进入项目之前先进行审查。 - -在 Persisting 里,pVisor 负责跑一次 Agent 并审查其改动;可与 pChronicle 分开使用。 - -!!! tip "你将完成什么" - - 完成第一次快速开始后:Agent 已停止;改动留在 Run 独占的暂存目录;你明确选择写入项目或丢弃。 - 第一次 `pvisor review` 时,你会看到解释实际控制机制的记录。 - -pVisor 不替代 Agent 自己的推理循环。你可以继续使用已有的 Agent CLI、脚本和 framework。 - -## 运行、审查、决定 - -在项目目录中运行: - -```bash -pvisor run --stage ./runs/task-001 -- codex -pvisor review last -pvisor apply last --path src -``` - -使用 `--stage ./runs/task-001` 时,Agent 写入项目的 staged 视图。Run 结束后,你可以接受全部变更、分多次接受 -选定路径,或丢弃整个 stage: - -```bash -pvisor apply last --all -# 或者 -pvisor drop last -``` - -具体的文件系统和网络边界取决于平台与 executor。pVisor 会记录实际生效的控制机制,不会把一个 -Run 描述得比它实际拥有的隔离程度更高。 - -## 选择下一步 - -第一次使用时,从[运行第一个 Agent](get-started.md)开始。这条路径会以一份已审查的 stage -结束,并介绍其余文档使用的核心术语。 - -如果已经知道目标,可以直接进入对应路径: - -- **保留或丢弃修改:** [审查和应用](guides/review-apply.md) -- **比较 host、container 与 VM:** [执行布局](guides/execution.md) -- **限制网络访问:** [网络策略](guides/network.md) -- **发布轨迹 event:** [采集轨迹](guides/capture.md) -- **查找精确选项:** [命令行参考](reference/cli.md) - -pVisor 的本地 run—review—apply 闭环可以独立工作。pChronicle 是可选项:当你希望在 Run -结束后保留并查询轨迹 Dataset 时再使用它。 - -## 推荐阅读顺序 - -第一次使用时按下面的顺序阅读: - -1. [运行第一个 Agent](get-started.md),完成一次完整成功闭环。 -2. 需要更细地控制修改时,阅读[审查和应用](guides/review-apply.md)。 -3. 执行 Provider 会影响边界时,阅读[执行布局](guides/execution.md)。 -4. 需要解释 Run Bundle 时,阅读[Capability 与 Evidence](concepts/capabilities-and-evidence.md)。 -5. 只有需要精确选项或输出字段时,再查阅[命令行参考](reference/cli.md)。 - -## 继续阅读 - -- [运行第一个 Agent](get-started.md) -- [理解 pVisor 概念](concepts/index.md) -- [完成常见工作流](guides/index.md) -- [了解运行时与隔离设计](design/index.md) -- [使用 pChronicle 探索轨迹历史](../pchronicle/index.md) diff --git a/docs/src/zh/pvisor/reference/cases.md b/docs/src/zh/pvisor/reference/cases.md deleted file mode 100644 index 5ff357881..000000000 --- a/docs/src/zh/pvisor/reference/cases.md +++ /dev/null @@ -1,1394 +0,0 @@ -# pVisor `run` 用户场景与回归示例 - -从最简单的命令开始,逐步加入资源限制、stage、VM、容器和网络功能。 -每个 case 先说明用途、准备和预期结果,再给出可执行命令;编号便于单独回归。 -本文只讨论 `run`,其它子命令请参阅各自的使用文档。编号(如 A01)只用于回归报告和问题定位;阅读时请按场景选择命令。 - - - -## 按需求选择 - -| 你的需求 | 建议先看 | -|---|---| -| 只想运行一个命令 | A01–A03 | -| 需要超时、内存或文件限制 | A04、B01–B04 | -| 想保留、丢弃或检查文件改动 | C01–C05 | -| 需要组合多个 OverlayFS 层 | D01 | -| 想了解 host 默认隔离 | D02–D03 | -| 使用 VM、宿主 rootfs 或 OCI 镜像 | E01–E06 | -| 使用原生 OCI 容器 | F01–F04 | -| 配置网络代理或禁止网络 | G01–G06 | -| 接入 Gateway 或记录轨迹 | H01–H03 | -| 从配置文件或 RunSpec 执行 | I01–I03 | -| 参考完整生产组合 | J01–J03 | - -每个场景都包含三层信息:命令是用户实际输入,正文说明适用场景和预期, -折叠的断言是自动回归使用的实现检查。你可以只复制命令,也可以运行脚本做完整验证。 - -## 如何使用 - -手工执行时,先准备一个测试工作目录。各例中的 `pvisor` 应已在 `PATH` 中; -`/path/to/...` 需要替换成自己的路径或镜像引用。 - -```bash -mkdir -p /tmp/pvisor-cases/workspace -cd /tmp/pvisor-cases/workspace -``` - -也可以让脚本逐条执行,并自动检查各例的结果: - -```bash -python3 scripts/run-pvisor-cases.py --list -python3 scripts/run-pvisor-cases.py --pvisor target/release/pvisor --case A01,C01 --keep -python3 scripts/run-pvisor-cases.py --pvisor target/release/pvisor --report target/pvisor-case-report.md -``` - -脚本为每个 case 创建独立的临时 workspace,把文档中的 `/tmp/pvisor-cases` -换成该 case 的目录。测试断言折叠在命令下方,由脚本执行,不需要手工复制。 -断言中的 `bundle_expect` 等函数由脚本提供,用于读取本次运行的 -`run-bundle.json` 和 `run.json`;它们不是 pVisor 命令。 - -case 注释中的 `requires` 描述运行环境;缺少前置条件时脚本将 case 标为 `SKIP`,并在 -`--run-unavailable` 下强制执行以便记录真实错误。`--keep` 保留现场; -`--strict-skips` 可将跳过视为失败。Linux 未提供 rootfs 时,目录 rootfs case 使用宿主 `/` 进行 smoke test, -这只能验证流程,不能代表独立的 guest rootfs,也不应作为生产隔离边界。 - -| 测试资源 | 脚本配置 | -|---|---| -| 已准备好的 Linux rootfs | `PVISOR_CASE_ROOTFS`,用于替换 `/path/to/rootfs`;未设置时 Linux 使用宿主 `/` | -| VM 镜像引用 | `PVISOR_CASE_IMAGE`,用于替换 `/path/to/image`;未设置时使用 `ubuntu:latest` | -| 容器镜像引用 | `PVISOR_CASE_CONTAINER_IMAGE`,用于替换 `alpine:latest`;未设置时使用 `ubuntu:latest`(与动态 pVisor ABI 兼容) | -| OCI runtime | 安装 `crun`;F04 显式使用 `runc`,也需要安装它 | -| 自定义 libkrunfw 目录 | `PVISOR_CASE_FIRMWARE`;未设置时脚本尝试查找本机缓存 | -| VM 中要执行的 Agent | `PVISOR_CASE_AGENT`,用于替换 `/usr/local/bin/agent`;此路径必须在 guest 中也可执行 | -| Lance 记录 | 安装带相应支持的组件,并设置 `PVISOR_CASE_LANCE=1` | - -Linux host stage 示例需要可用的 user/mount namespace。VM 示例需要可访问的 -`/dev/kvm`。容器需要本机 OCI runtime 具备实际启动容器的权限; -装有 runtime 可执行文件本身并不保证权限齐备。 - -## 可执行 Case - -### A. 基础调用与身份 - -这一组适合第一次使用 pVisor。先从 A01 开始;只有需要固定显示名、采集输出或显式传递环境变量时,再选择后续例子。 - -- [ ] **A01:省略 `run` 的最简调用** - - 建议场景:适合第一次使用 pVisor、确认命令和 Run 身份。 - - 用途:在当前目录执行一个命令,不需要显式写出 `run`。`--` 后全部是交给 Agent 的命令和参数。 - - 预期:输出当前 workspace 的绝对路径并成功退出。默认使用 host executor,不启用 stage;未配置网络策略时记录为 ambient。 - - ```bash - pvisor -- /bin/pwd - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - stdout_has "$(cd "$PVISOR_CASE_WORKSPACE" && pwd -P)" - bundle_expect run.state completed - bundle_expect run.exit_code 0 - bundle_expect run.agent pwd - bundle_expect network.policy.mode ambient - ``` - -
- -- [ ] **A02:显式 `run` 与省略形式等价** - - 建议场景:适合第一次使用 pVisor、确认命令和 Run 身份。 - - 用途:对比省略和显式写出 `run` 的两种调用。分别保存 Agent 的标准输出,便于比较。 - - 预期:两个输出文件内容相同,都是当前工作目录。两次运行会各自生成记录,Run ID 和时间可以不同。 - - ```bash - pvisor -- /bin/pwd > implicit.txt - pvisor run -- /bin/pwd > explicit.txt - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - diff implicit.txt explicit.txt - test "$(cat implicit.txt)" = "$(cd "$PVISOR_CASE_WORKSPACE" && pwd -P)" - bundle_expect run.agent pwd - ``` - -
- -- [ ] **A03:Run 名称和 stdio capture** - - 建议场景:适合第一次使用 pVisor、确认命令和 Run 身份。 - - 用途:为这次运行命名,并将 Agent 输出保存到运行结果。`--name smoke` 指定显示名,`--stdio capture` 开启输出采集。 - - 预期:Run 名称为 `smoke`,结果中的标准输出为 `hello`,未被截断。 - - ```bash - pvisor --name smoke --stdio capture -- /bin/sh -c 'printf hello' - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - bundle_expect run.agent smoke - bundle_expect run.output.stdout hello - bundle_expect run.output.stdout_truncated false - ``` - -
- -- [ ] **A04:超时** - - 建议场景:适合第一次使用 pVisor、确认命令和 Run 身份。 - - 用途:给运行设置墙钟超时。`100ms` 是从运行开始计时的持续时间,不是 CPU 时间;命令故意睡眠 10 秒。 - - 预期:pVisor 非零退出,运行结果的失败类型为 `deadline_exceeded`。 - - - - ```bash - pvisor --timeout 100ms -- /bin/sleep 10 - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - bundle_expect run.state failed - bundle_expect run.failure.kind deadline_exceeded - bundle_expect run.failure.retryable false - ``` - -
- -- [ ] **A05:严格执行模式拒绝 best-effort 边界** - - 建议场景:适合第一次使用 pVisor、确认命令和 Run 身份。 - - 用途:要求严格执行能力检查。`--strict` 不接受所请求能力缺少强制执行证据;这里同时要求禁止网络。 - - 准备:Linux user/mount namespace 或 macOS Seatbelt 可用。 - - 预期:当前 host / container / VM 执行路径在启动 Agent 前均因缺少 Subprocess - enforcement 证据而拒绝请求(`UnsupportedPolicy`)。此例验证 fail-closed, - 不代表 `--strict` 当前在任一 executor 上可达“更强沙箱已就绪”。 - - - - ```bash - pvisor --strict --overlaynet-deny-all -- /bin/true - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - stdout_has "lacks enforced evidence for requested capability dimensions" - ``` - -
- -- [ ] **A06:显式环境投影** - - 建议场景:适合第一次使用 pVisor、确认命令和 Run 身份。 - - 用途:只把指定的宿主环境变量传给子进程。变量仅为这条命令设置,通过 `--pass-env` 显式允许投影。 - - 预期:子进程可见 `TEST_PVISOR_VALUE=visible`;运行记录列出这个变量,但不声明整体继承宿主环境。 - - ```bash - TEST_PVISOR_VALUE=visible pvisor --pass-env TEST_PVISOR_VALUE -- /usr/bin/env - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - stdout_has "TEST_PVISOR_VALUE=visible" - bundle_contains environment.projected_keys TEST_PVISOR_VALUE - bundle_expect environment.inherits_host false - ``` - -
- -### B. 资源限制 - -这一组展示“请求限制”和“实际强制”之间的区别。B01 用于查看完整配置,B02 才真正尝试触发文件大小限制。 - -- [ ] **B01:组合使用所有资源限制** - - 建议场景:适合需要控制或验证资源限制的任务。 - - 用途:组合设置内存、进程数、CPU 时间、打开文件数和单文件大小。`MiB` 是二进制单位;`--max-cpu-time` 与墙钟超时不同。 - - 预期:命令成功退出,五个请求值出现在运行记录中,同时报告生效值和限制机制。`/bin/true` 不消耗这些额度,此例不测试超限行为。 - - ```bash - pvisor \ - --memory 256MiB \ - --max-processes 32 \ - --max-cpu-time 5s \ - --max-open-files 128 \ - --max-file-size 1MiB \ - -- /bin/true - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - bundle_expect resources.requested.memory_bytes 268435456 - bundle_expect resources.requested.processes 32 - bundle_expect resources.requested.cpu_time_ms 5000 - bundle_expect resources.requested.open_files 128 - bundle_expect resources.requested.file_size_bytes 1048576 - bundle_expect resources.effective.file_size_bytes 1048576 - bundle_contains resources.mechanisms rlimit - ``` - -
- -- [ ] **B02:文件大小限制实际生效** - - 建议场景:适合需要控制或验证资源限制的任务。 - - 用途:验证单文件大小限制:将上限设为 1KiB,再尝试用 `dd` 写入 4KiB。 - - 预期:写入命令失败,落盘文件如果存在,其大小不超过 1024 字节;运行结果记录进程退出失败。 - - - - ```bash - pvisor --max-file-size 1KiB -- /bin/sh -c 'dd if=/dev/zero of=large bs=4096 count=1' - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - bundle_expect resources.requested.file_size_bytes 1024 - bundle_expect run.state failed - bundle_expect run.failure.kind process_exit - test ! -f large || [ "$(wc -c < large)" -le 1024 ] - ``` - -
- -- [ ] **B03:内存参数短别名** - - 建议场景:适合需要控制或验证资源限制的任务。 - - 用途:使用 `--memory` 的别名 `--mem`,为一个简单命令设置 256MiB 内存额度。 - - 预期:命令成功,记录中的请求值为 268435456 字节,与 `--memory 256MiB` 一致。 - - ```bash - pvisor --mem 256MiB -- /bin/true - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - bundle_expect resources.requested.memory_bytes 268435456 - ``` - -
- -- [ ] **B04:Stage 总大小限制** - - 建议场景:适合需要控制或验证资源限制的任务。 - - 用途:为持久 stage 请求 1GiB 的总大小限制。它限制的是 stage 总量,和 B02 的单个文件大小不是同一个概念。 - - 准备:Linux user/mount namespace 或 macOS Seatbelt 可用。 - - 预期:stage 成功建立并保存在指定路径。此例只验证参数可用和目录建立;当前产物未记录该上限,也未在此例中尝试写满 stage。 - - ```bash - pvisor --stage /tmp/pvisor-cases/limited-stage --max-stage-size 1GiB -- /bin/true - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - bundle_expect filesystem.state staged - bundle_expect safety.filesystem_changes_staged true - record_expect storage "$(realpath "$PVISOR_CASE_ROOT/limited-stage")" - ``` - -
- -### C. Stage 与 whole-rootfs - -当你希望 Agent 可以自由修改文件、但不污染当前 workspace 时使用这一组。C01 是最常用的持久模式;C02/C03 适合一次性试运行。 - -- [ ] **C01:持久 stage** - - 建议场景:适合隔离文件变更、保留 stage 或验证 whole-rootfs 的任务。 - - 用途:把本次运行的文件改动放进一个保留的 stage。命令在 workspace 里创建 `result.txt`。 - - 准备:Linux user/mount namespace 或 macOS Seatbelt 可用。 - - 预期:原 workspace 没有 `result.txt`;变更清单中出现该文件,指定 stage 内保留 `run-bundle.json`,便于之后查看。 - - ```bash - pvisor --stage /tmp/pvisor-cases/stage-keep -- /bin/sh -c 'printf changed > result.txt' - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - bundle_expect filesystem.state staged - bundle_contains filesystem.changes result.txt - bundle_expect safety.filesystem_write_non_bypassable true - test ! -e result.txt - test -f "$PVISOR_CASE_ROOT/stage-keep/run-bundle.json" - ``` - -
- -- [ ] **C02:自动临时 stage** - - 建议场景:适合隔离文件变更、保留 stage 或验证 whole-rootfs 的任务。 - - 用途:运行一次不需要保留改动的任务。`--stage drop` 自动选择系统临时目录,退出后删除该目录。 - - 准备:Linux user/mount namespace 或 macOS Seatbelt 可用。 - - 预期:命令成功,日志中给出的临时存储目录已删除,原 workspace 也没有新建的文件。 - - ```bash - pvisor --stage drop -- /bin/sh -c 'printf changed > result.txt' - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - storage=$(grep -m1 '^Run storage: ' "$PVISOR_CASE_STDOUT" | cut -d' ' -f3-) - test -n "$storage" - test ! -e "$storage" - test ! -e result.txt - ``` - -
- -- [ ] **C03:指定自动删除目录** - - 建议场景:适合隔离文件变更、保留 stage 或验证 whole-rootfs 的任务。 - - 用途:自己选择临时 stage 路径,但仍要求运行结束后自动删除。示例先创建一个空目录。 - - 准备:Linux user/mount namespace 或 macOS Seatbelt 可用。 - - 预期:运行完成后 `stage-drop` 目录不存在。请使用专用空目录,不要指定含有用户文件的目录。 - - ```bash - mkdir -p /tmp/pvisor-cases/stage-drop - pvisor --stage drop:/tmp/pvisor-cases/stage-drop -- /bin/true - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - test ! -e "$PVISOR_CASE_ROOT/stage-drop" - ``` - -
- -- [ ] **C04:拒绝删除非空目录** - - 建议场景:适合隔离文件变更、保留 stage 或验证 whole-rootfs 的任务。 - - 用途:验证误用保护:把 `drop:` 指向已有用户文件的目录。 - - 预期:启动前报错,提示临时 stage 必须为空;原有的 `user-file` 保持完整。此例预期非零退出。 - - - - ```bash - mkdir -p /tmp/pvisor-cases/not-owned - touch /tmp/pvisor-cases/not-owned/user-file - pvisor --stage drop:/tmp/pvisor-cases/not-owned -- /bin/true - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - stdout_has "temporary stage must be empty before use" - test -f "$PVISOR_CASE_ROOT/not-owned/user-file" - ``` - -
- -- [ ] **C05:whole-rootfs 捕获与 tmpfs 隔离** - - 建议场景:适合隔离文件变更、保留 stage 或验证 whole-rootfs 的任务。 - - 用途:比较 workspace 写入和 sandbox 临时目录写入。前者用于保留任务改动,后者只供本次运行临时使用。 - - 准备:Linux user/mount namespace 可用;macOS Seatbelt 不提供此例要求的 whole-rootfs/tmpfs 隔离。 - - 预期:workspace 的改动出现在 stage,宿主 workspace 和宿主 `/tmp` 均不出现新文件。这里不验证 workspace 以外普通 rootfs 路径的持久化。 - - - - ```bash - pvisor --stage /tmp/pvisor-cases/root-stage -- /bin/sh -c \ - 'printf workspace > ./workspace-change; printf tmp > /tmp/pvisor-root-change' - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - bundle_contains filesystem.changes workspace-change - test ! -e workspace-change - test ! -e /tmp/pvisor-root-change - ``` - -
- -### D. OverlayFS 与 Host 安全边界 - -D01 讲视图层组合,D02/D03 讲 host executor 的默认隔离和 workspace 可见性。生产使用前建议先阅读这组三个例子。 - -- [ ] **D01:高级 OverlayFS 组合** - - 建议场景:适合检查 OverlayFS 视图和 host 安全边界。 - - 用途:把宿主的两个目录依次叠加到工作区视图,并指定 Agent 看到的路径。`directory` 选择目录后端,`manual` 表示退出后不自动应用改动。 - - 准备:Linux user/mount namespace 或 macOS Seatbelt 可用。 - - 预期:记录的目标为 `view`,从顶层到底层依次为 `layer`、`base`、当前 workspace。目录为空,因此此例检查配置顺序,不检查同名文件覆盖内容。 - - ```bash - mkdir -p /tmp/pvisor-cases/base /tmp/pvisor-cases/layer "$PWD/view" - pvisor \ - --stage /tmp/pvisor-cases/composed-stage \ - --overlayfs-path "$PWD/view" \ - --overlayfs-compose /tmp/pvisor-cases/base \ - --overlayfs-compose /tmp/pvisor-cases/layer \ - --overlayfs-backend directory \ - --overlayfs-commit manual \ - -- /bin/true - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - bundle_expect filesystem.state staged - record_expect overlay_lowers.0 "$(realpath "$PVISOR_CASE_ROOT/layer")" - record_expect overlay_lowers.1 "$(realpath "$PVISOR_CASE_ROOT/base")" - record_expect overlay_lowers.2 "$(realpath "$PVISOR_CASE_WORKSPACE")" - ``` - -
- -- [ ] **D02:显式 host executor** - - 建议场景:适合检查 OverlayFS 视图和 host 安全边界。 - - 用途:显式选择 host executor,观察当前系统上的隔离类型。 - - 准备:Linux user/mount namespace 或 macOS Seatbelt 可用。 - - 预期:Linux 记录为 `rootless_process`,macOS 记录为 `sandboxed_process`;两者都不应降级为 host process。 - - - - ```bash - pvisor --executor host -- /bin/true - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - bundle_expect run.executor.kind process - if [ "$(uname -s)" = "Darwin" ]; then - bundle_expect run.executor.isolation sandboxed_process - else - bundle_expect run.executor.isolation rootless_process - fi - bundle_expect safety.host_process false - ``` - -
- -- [ ] **D03:host stage 隐藏原 workspace** - - 建议场景:适合检查 OverlayFS 视图和 host 安全边界。 - - 用途:观察启用 stage 后子进程的 cwd 和 procfs 路径。三条命令的输出保存到 `views.txt`。 - - 准备:Linux user/mount namespace 可用;macOS 不支持此例的 procfs/mount namespace 路径隐藏语义。 - - 预期:cwd 指向 stage 的 merged 目录,输出中不出现原 workspace 路径。此例只检查路径显示,不证明所有原路径或继承 FD 访问都已被禁止。 - - - - ```bash - pvisor --executor host --stage /tmp/pvisor-cases/host-stage -- /bin/sh -c \ - 'pwd; readlink /proc/self/root; readlink /proc/self/cwd' > views.txt - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - merged="$PVISOR_CASE_ROOT/host-stage/merged" - test "$(sed -n 1p views.txt)" = "$merged" - test "$(sed -n 3p views.txt)" = "$merged" - ! grep -Fq -- "$(cd "$PVISOR_CASE_WORKSPACE" && pwd -P)" views.txt - ``` - -
- -### E. VM 与 rootfs - -需要更强边界、独立 guest kernel 或 OCI rootfs 时使用 VM。E01 最接近“直接运行”,E02/E03 展示目录和镜像来源,E04/E05 再加入资源与 stage。 - -- [ ] **E01:`--vm` 简写与 host rootfs** - - 建议场景:适合需要 VM guest kernel、独立 rootfs 或更强隔离的任务。 - - 用途:用 `--vm` 选择 VM executor,并以宿主根目录作为 guest rootfs。该方式扩大了 guest 可读取的宿主文件范围,只应在可信测试环境使用。 - - 准备:Linux;可访问 /dev/kvm。 - - 预期:guest 输出与宿主 workspace 相同的绝对路径。运行结果标记为虚拟机,网络使用 pVisor 的 smoltcp 驱动。 - - - - ```bash - pvisor --vm --rootfs host -- /bin/pwd > guest-cwd.txt - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - test "$(cat guest-cwd.txt)" = "$(cd "$PVISOR_CASE_WORKSPACE" && pwd -P)" - bundle_expect run.executor.kind virtual_machine - bundle_expect run.executor.isolation virtual_machine - bundle_expect network.interception.driver vm-smoltcp - bundle_expect network.interception.strength non-bypassable - bundle_expect safety.network_non_bypassable true - ``` - -
- -- [ ] **E02:显式 VM executor 与目录 rootfs** - - 建议场景:适合需要 VM guest kernel、独立 rootfs 或更强隔离的任务。 - - 用途:已有 Linux rootfs 时,直接把目录交给 VM 使用。目录内需要有可执行的 `/bin/pwd` 及其运行依赖。 - - 准备:Linux;可访问 /dev/kvm;准备好 Linux rootfs,并为脚本设置 PVISOR_CASE_ROOTFS。 - - 预期:虚拟机成功执行命令,guest cwd 与宿主 workspace 路径一致。 - - - - ```bash - pvisor --executor vm --rootfs /path/to/rootfs -- /bin/pwd > guest-cwd.txt - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - test "$(cat guest-cwd.txt)" = "$(cd "$PVISOR_CASE_WORKSPACE" && pwd -P)" - bundle_expect run.executor.isolation virtual_machine - ``` - -
- -- [ ] **E03:image rootfs** - - 建议场景:适合需要 VM guest kernel、独立 rootfs 或更强隔离的任务。 - - 用途:使用 OCI 镜像准备 VM 的 rootfs,不依赖 Docker/Podman daemon。将 `image=` 后的占位符替换为可获取的镜像引用。 - - 准备:Linux;可访问 /dev/kvm;为脚本设置 PVISOR_CASE_IMAGE。 - - 预期:镜像准备后启动 VM,guest 的工作目录与宿主 workspace 路径一致。 - - - - ```bash - pvisor --vm --rootfs image=/path/to/image -- /bin/pwd > guest-cwd.txt - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - test "$(cat guest-cwd.txt)" = "$(cd "$PVISOR_CASE_WORKSPACE" && pwd -P)" - bundle_expect run.executor.isolation virtual_machine - ``` - -
- -- [ ] **E04:VM 资源和 firmware 配置** - - 建议场景:适合需要 VM guest kernel、独立 rootfs 或更强隔离的任务。 - - 用途:使用已有 rootfs 启动 VM,同时指定 firmware 目录、2GiB 内存和 2 个虚拟 CPU。 - - 准备:Linux;可访问 /dev/kvm;准备好 Linux rootfs,并为脚本设置 PVISOR_CASE_ROOTFS;libkrunfw 目录或本机缓存可用。 - - 预期:VM 成功运行,内存请求记录为 2147483648 字节。这里使用目录 rootfs,`--image-store` 不会触发镜像下载;CPU 数量未由本例断言核验。 - - - - ```bash - pvisor --vm \ - --rootfs /path/to/rootfs \ - --image-store /tmp/pvisor-cases/images \ - --vm-library-dir /path/to/libkrunfw \ - --memory 2GiB \ - --cpu 2 \ - -- /bin/true - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - bundle_expect run.executor.isolation virtual_machine - bundle_expect resources.requested.memory_bytes 2147483648 - ``` - -
- -- [ ] **E05:VM workspace 与 whole-rootfs stage 组合** - - 建议场景:适合需要 VM guest kernel、独立 rootfs 或更强隔离的任务。 - - 用途:在 VM 镜像运行基础上增加持久 stage,并显式要求工作区视图位于宿主 cwd 的同一路径。 - - 准备:Linux;可访问 /dev/kvm;为脚本设置 PVISOR_CASE_IMAGE。 - - 预期:guest cwd 保持一致,stage 保留在 `vm-stage`。本例只执行 `pwd`,验证路径和 stage 建立,不验证写入捕获。 - - - - ```bash - pvisor --vm \ - --rootfs image=/path/to/image \ - --stage /tmp/pvisor-cases/vm-stage \ - --overlayfs-path "$PWD" \ - -- /bin/pwd > guest-cwd.txt - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - test "$(cat guest-cwd.txt)" = "$(cd "$PVISOR_CASE_WORKSPACE" && pwd -P)" - bundle_expect filesystem.state staged - record_expect storage "$PVISOR_CASE_ROOT/vm-stage" - ``` - -
- -- [ ] **E06:拒绝 executor 冲突** - - 建议场景:适合需要 VM guest kernel、独立 rootfs 或更强隔离的任务。 - - 用途:验证互相冲突的 executor 参数不能一起使用:`--vm` 选择虚拟机,`--executor host` 却选择宿主。 - - 预期:参数归一化阶段失败,错误信息明确指出 `--vm` 与非 VM executor 冲突。 - - - - ```bash - pvisor --vm --executor host --rootfs host -- /bin/true - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - stdout_has "--vm cannot be combined with a non-vm --executor" - ``` - -
- -### F. Container - -需要复用 OCI rootfs、但不想运行 Docker/Podman daemon 时使用原生 OCI container。请按 F01 → F04 逐步增加复杂度;F04 适合验证跨 ABI 注入和 mount 配置。 - -这些例子使用原生 OCI bundle,由 pVisor 准备文件系统并调用 runc/crun, -不依赖 Docker/Podman daemon。F01–F03 使用默认 runtime crun,F04 显式选择 runc。 -镜像或目录需与本机架构兼容;如果当前 pVisor 是动态链接构建,guest 必须提供 -相应的动态加载器和库,否则应像 F04 一样指定兼容的静态构建。 -默认注入当前 pVisor,不需要在最小命令中显式指定 binary。 - -当前容器执行仍有 OCI 命令行兼容性问题,这些例子可能在 runner 启动阶段失败。 -下面保留期望成功的命令和断言,便于重构完成后直接回归。 - -- [ ] **F01:最小 container Run** - - 建议场景:适合由 runc/crun 直接启动 OCI 容器的任务。 - - 用途:以 OCI 镜像启动最小容器运行。`--executor container` 选择原生 OCI runtime,`--rootfs image=...` 指定容器文件系统来源。 - - 准备:OCI runtime 可运行,并为脚本设置 PVISOR_CASE_CONTAINER_IMAGE。 - - 预期:pVisor 准备 OCI bundle、注入自身并执行 `/bin/true`,运行结果记录为 container 且退出码为 0。 - - - - ```bash - pvisor --executor container --rootfs image=alpine:latest -- /bin/true - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - bundle_expect run.executor.kind container - bundle_expect run.state completed - bundle_expect run.exit_code 0 - ``` - -
- -- [ ] **F02:container rootfs 与隔离网络** - - 建议场景:适合由 runc/crun 直接启动 OCI 容器的任务。 - - 用途:在 F01 基础上只增加 `--container-network none`,让容器使用独立的网络 namespace,不配置外部连接。 - - 准备:OCI runtime 可运行,并为脚本设置 PVISOR_CASE_CONTAINER_IMAGE。 - - 预期:容器中的 `/bin/true` 成功退出。此命令不发起网络请求,因此断言只检查启动成功,不验证网络是否能被绕过。 - - - - ```bash - pvisor --executor container \ - --rootfs image=alpine:latest \ - --container-network none \ - -- /bin/true - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - bundle_expect run.executor.kind container - bundle_expect run.state completed - ``` - -
- -- [ ] **F03:使用宿主 rootfs 的 OCI bundle** - - 建议场景:适合由 runc/crun 直接启动 OCI 容器的任务。 - - 用途:不提供容器镜像,直接以宿主 `/` 作为只读 lower。pVisor 会先建立独立 synthetic rootfs,再把宿主标准目录以只读方式映射进去。 - - 准备:Linux、可运行的 OCI runtime,以及允许 rootless container 的 user namespace。 - - 预期:以指定目录作为容器根文件系统执行命令,不需要配置镜像仓库。使用专用测试 rootfs,不要把宿主 `/` 当作此例的测试目录。 - - - - ```bash - pvisor --executor container \ - --rootfs host \ - --container-network none \ - -- /bin/true - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - bundle_expect run.executor.kind container - bundle_expect run.state completed - ``` - -
- -- [ ] **F04:显式 OCI runtime 与高级 container 参数** - - 建议场景:适合由 runc/crun 直接启动 OCI 容器的任务。 - - 用途:显式覆盖高级容器选项:使用 runc 和指定 pVisor 构建,声明平台、uid/gid、工作目录、只读 rootfs,并把宿主目录绑定到 `/workspace`。 - - 准备:OCI runtime 可运行,并为脚本设置 PVISOR_CASE_CONTAINER_IMAGE。 - - 预期:容器成功退出。`read_only=false` 使绑定目录可写,即使 rootfs 只读;`--container-workdir` 在 Run 没有 cwd 时才作为回退。此例仅检查组合启动,未分别检查用户身份和读写行为。 - - - - ```bash - pvisor --executor container \ - --container-runtime runc \ - --rootfs image=alpine:latest \ - --container-pvisor-binary ./target/release/pvisor \ - --container-platform linux/amd64 \ - --container-network none \ - --container-workdir /workspace \ - --container-user 1000:1000 \ - --container-read-only-rootfs \ - --container-mount 'source="/tmp/pvisor-cases/workspace",target="/workspace",read_only=false' \ - -- /bin/true - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - bundle_expect run.executor.kind container - bundle_expect run.state completed - ``` - -
- -### G. OverlayNet - -这一组只讨论网络边界。proxy 适合需要 host Gateway 的协作式访问,VM auto 和 host deny-all 才适合需要更强网络边界的场景。 - -- [ ] **G01:启用默认 proxy** - - 建议场景:适合配置出站网络、代理访问或禁止网络的任务。 - - 用途:只给出 `--overlaynet`,省略值时启用默认 proxy 模式。 - - 预期:记录为 explicit-proxy,拦截强度为 cooperative。它只约束经过代理的流量,不能据此认为直接 socket 已被禁止。 - - ```bash - pvisor --overlaynet -- /bin/true - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - bundle_expect network.interception.driver explicit-proxy - bundle_expect network.interception.strength cooperative - bundle_contains artifacts capture - ``` - -
- -- [ ] **G02:显式 proxy 地址和 mode** - - 建议场景:适合配置出站网络、代理访问或禁止网络的任务。 - - 用途:显式选择 proxy,并指定代理监听地址。手工运行时确保 18080 端口未被占用;脚本会替换为空闲端口。 - - 预期:记录的 OverlayNet 监听地址与请求一致,网络驱动为 explicit-proxy。 - - ```bash - pvisor --overlaynet proxy --overlaynet-listen 127.0.0.1:18080 -- /bin/true - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - record_expect overlaynet_listen 127.0.0.1:18080 - bundle_expect network.interception.driver explicit-proxy - ``` - -
- -- [ ] **G03:allow、deny 和带宽限制组合** - - 建议场景:适合配置出站网络、代理访问或禁止网络的任务。 - - 用途:组合配置允许目标、拒绝网段和针对目标的带宽上限。只有通过代理的流量才受这些规则约束。 - - 预期:记录为 allowlist 模式,允许 `api.example.com:443`,拒绝 `10.0.0.0/8`,并把 `1mbps` 记录为每秒 125000 字节。本例不实际发请求。 - - ```bash - pvisor --overlaynet proxy \ - --overlaynet-allow api.example.com:443 \ - --overlaynet-deny 10.0.0.0/8 \ - --overlaynet-limit api.example.com=1mbps \ - -- /bin/true - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - bundle_expect network.policy.mode allowlist - bundle_expect network.policy.rules.0.host api.example.com - bundle_expect network.policy.rules.0.ports.0 443 - bundle_expect network.policy.deny_rules.0.host 10.0.0.0/8 - bundle_expect network.policy.limits.0.host api.example.com - bundle_expect network.policy.limits.0.bytes_per_second 125000 - ``` - -
- -- [ ] **G04:deny-all 不可通过环境变量绕过** - - 建议场景:适合配置出站网络、代理访问或禁止网络的任务。 - - 用途:验证禁止网络后,清除常见代理变量仍不能访问外网。Linux 使用 network namespace,macOS 使用 Seatbelt;需要宿主安装 curl,命令故意发起网络请求。 - - 准备:安装 curl。 - - 预期:命令失败,结果记录 no-network 和不可绕过边界。外网自身不可用也会使 curl 失败,因此本例不能单独证明隔离有效。 - - - - ```bash - pvisor --overlaynet-deny-all -- /bin/sh -c \ - 'unset HTTP_PROXY HTTPS_PROXY ALL_PROXY http_proxy https_proxy all_proxy; curl --max-time 2 https://example.com' - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - bundle_expect network.policy.mode no-network - bundle_expect safety.network_non_bypassable true - bundle_expect run.state failed - ``` - -
- -- [ ] **G05:VM OverlayNet auto** - - 建议场景:适合配置出站网络、代理访问或禁止网络的任务。 - - 用途:显式为 VM 选择 OverlayNet auto,使流量经过虚拟机的 smoltcp 网络驱动。 - - 准备:Linux;可访问 /dev/kvm;准备好 Linux rootfs,并为脚本设置 PVISOR_CASE_ROOTFS。 - - 预期:记录为 vm-smoltcp 和 non-bypassable,而不是 host 的协作式代理。 - - - - ```bash - pvisor --vm --rootfs /path/to/rootfs --overlaynet auto -- /bin/true - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - bundle_expect network.interception.driver vm-smoltcp - bundle_expect network.interception.strength non-bypassable - ``` - -
- -- [ ] **G06:关闭 OverlayNet 时拒绝策略参数** - - 建议场景:适合配置出站网络、代理访问或禁止网络的任务。 - - 用途:检查关闭 OverlayNet 后不能继续提供网络策略。 - - 预期:启动前失败,错误提示策略需要 `auto` 或 `proxy`。如果只想关闭 OverlayNet,请不要附带 allow/deny/limit。 - - - - ```bash - pvisor --overlaynet off --overlaynet-allow example.com:443 -- /bin/true - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - stdout_has "OverlayNet policy options require --overlaynet auto or proxy" - ``` - -
- -### H. Gateway 与记录 - -需要审计、模型路由或轨迹回放时使用这一组。H01 是 Gateway 配置示例,H02 适合轻量 JSONL,H03 适合 Lance warehouse。 - -- [ ] **H01:Gateway capture 完整组合** - - 建议场景:适合接入 Gateway、模型路由或记录轨迹的任务。 - - 用途:为需要模型请求记录的 Agent 配置 Gateway。示例设置路由、管理监听端口、完整记录级别、会话头、诊断输出和 Markdown 投影,并保留 stage。 - - 准备:Linux user/mount namespace 或 macOS Seatbelt 可用。 - - 预期:Gateway 与 stage 成功建立。示例上游是占位地址,`/bin/true` 不发送模型请求;此例不验证对话内容。管理监听地址与运行记录中的 `gateway_listen` 不是同一个服务地址。 - - ```bash - pvisor \ - --stage /tmp/pvisor-cases/gateway-stage \ - --gateway-mode capture \ - --gateway-admin-listen 127.0.0.1:19090 \ - --gateway-level full \ - --gateway-session-header X-Session-ID \ - --gateway-debug \ - --gateway-stream-markdown \ - --gateway-route 'name="default",upstream="https://example.com/v1"' \ - -- /bin/true - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - record_get gateway_listen | grep -Eq '^127\.0\.0\.1:[0-9]+$' - bundle_expect network.interception.driver explicit-proxy - bundle_expect filesystem.state staged - ``` - -
- -- [ ] **H02:JSON 记录** - - 建议场景:适合接入 Gateway、模型路由或记录轨迹的任务。 - - 用途:把本次运行事件写成轻量的 JSONL 文件,不启动 Lance warehouse。 - - 预期:指定文件非空,首行具有 JSON 对象形式。每行应是一个事件;本例只检查文件建立和首行外观。 - - ```bash - pvisor --record-format json --record-destination /tmp/pvisor-cases/events.jsonl -- /bin/true - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - test -s "$PVISOR_CASE_ROOT/events.jsonl" - head -n 1 "$PVISOR_CASE_ROOT/events.jsonl" | grep -q '^{' - ``` - -
- -- [ ] **H03:Lance 记录** - - 建议场景:适合接入 Gateway、模型路由或记录轨迹的任务。 - - 用途:需要 warehouse 形式的记录时选择 Lance。先安装相应构建和运行依赖,再让脚本启用该例。 - - 准备:Lance 运行依赖齐备,脚本设置 PVISOR_CASE_LANCE=1。 - - 预期:运行完成,指定位置建立 warehouse 目录;本例未查询其中的记录。 - - - - ```bash - pvisor --record-format lance --record-destination /tmp/pvisor-cases/warehouse -- /bin/true - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - bundle_expect run.state completed - test -d "$PVISOR_CASE_ROOT/warehouse" - ``` - -
- -### I. Spec 与控制面 - -已有自动化控制面或需要把 RunSpec 作为文件传递时使用这一组。I01 是 TOML 配置,I02 是 JSON 委托,I03 验证无扩展名文件。 - -- [ ] **I01:TOML config** - - 建议场景:适合从 TOML/JSON 文件或控制面执行 RunSpec 的任务。 - - 用途:把命令写进 TOML 后通过 `--spec` 运行。手工执行前创建 `pvisor.toml`,内容为 `[run]` 下的 `command = ["/bin/true"]`;脚本会预置此文件。 - - 预期:命令来自配置文件,无需在 CLI 重复;运行正常结束。 - - ```bash - pvisor --spec ./pvisor.toml - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - bundle_expect run.state completed - bundle_expect run.agent true - ``` - -
- -- [ ] **I02:JSON RunSpec** - - 建议场景:适合从 TOML/JSON 文件或控制面执行 RunSpec 的任务。 - - 用途:执行已准备好的 JSON RunSpec,并把结果原子写入指定文件。手工运行前准备包含 run_id、agent 和 process invocation 的 `run-spec.json`;脚本预置的是运行 `/bin/true` 的 `case-i02`。 - - 准备:Linux user/mount namespace 或 macOS Seatbelt 可用。 - - 预期:运行名称为 `case-i02`,`run-result.json` 非空。该委托路径当前只支持 host executor,不套用普通 Run 的 rootless safe profile;不要把此例视为隔离模式示例。 - - ```bash - pvisor --spec ./run-spec.json --result-file ./run-result.json --stage ./delegated-stage - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - bundle_expect run.agent case-i02 - bundle_expect run.executor.isolation host_process - test -s run-result.json - ``` - -
- -- [ ] **I03:无扩展名 spec** - - 建议场景:适合从 TOML/JSON 文件或控制面执行 RunSpec 的任务。 - - 用途:验证 spec 的识别不依赖扩展名。手工执行时把 I01 的 TOML 内容保存成 `spec-without-extension`;脚本会预置该文件。 - - 预期:按内容识别 TOML 并完成运行,不要求文件名以 `.toml` 结尾。 - - ```bash - pvisor --spec ./spec-without-extension - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - bundle_expect run.state completed - ``` - -
- -### J. 复杂组合 - -这些不是入门命令,而是上线前的组合参考:J01 偏 host 安全,J02 偏 VM 生产链路,J03 偏容器链路。遇到问题时请拆回对应的 A–I 场景定位。 - -- [ ] **J01:host + persistent stage + deny-all + capture + limits** - - 建议场景:适合上线前验证多项能力组合的端到端任务。 - - 用途:组合使用 host stage、禁止网络、输出采集、JSON 事件和资源限制。命令在隔离视图中写入一个结果文件。 - - 准备:Linux user/mount namespace 或 macOS Seatbelt 可用。 - - 预期:原 workspace 不变;stage 记录 `result.txt`,结果保存 stdout 和资源请求,事件写入指定 JSONL 文件,网络标记为禁止连接。 - - ```bash - pvisor --name host-full \ - --executor host \ - --stage /tmp/pvisor-cases/host-full \ - --overlaynet-deny-all \ - --stdio capture \ - --record-format json \ - --record-destination /tmp/pvisor-cases/host-full/trajectory/events.jsonl \ - --memory 512MiB \ - --max-processes 64 \ - --max-stage-size 2GiB \ - --max-cpu-time 30s \ - -- /bin/sh -c 'pwd; printf changed > result.txt' - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - bundle_expect run.agent host-full - bundle_contains run.output.stdout "$PVISOR_CASE_ROOT/host-full/merged" - bundle_expect network.policy.mode no-network - bundle_expect safety.network_non_bypassable true - bundle_expect safety.filesystem_changes_staged true - bundle_contains filesystem.changes result.txt - bundle_expect resources.requested.memory_bytes 536870912 - bundle_expect resources.requested.processes 64 - bundle_expect resources.requested.cpu_time_ms 30000 - test ! -e result.txt - test -s "$PVISOR_CASE_ROOT/host-full/trajectory/events.jsonl" - ``` - -
- -- [ ] **J02:VM + image rootfs + stage + OverlayNet + Gateway** - - 建议场景:适合上线前验证多项能力组合的端到端任务。 - - 用途:在 VM 中运行真实 Agent,同时保留 stage、使用 smoltcp 网络、Gateway 和 Lance 轨迹记录。需提供含 Agent 及其依赖的镜像,并把示例上游替换为实际服务。 - - 准备:Linux;可访问 /dev/kvm;为脚本设置 PVISOR_CASE_IMAGE;PVISOR_CASE_AGENT 指向 guest 中也可执行的 Agent;Lance 运行依赖齐备,脚本设置 PVISOR_CASE_LANCE=1。 - - 预期:VM 以请求的内存运行,保留 stage 和轨迹目录。是否产生模型对话取决于 Agent 是否真的调用 Gateway;当前断言不检查对话内容。 - - - - ```bash - pvisor --name vm-full \ - --vm \ - --rootfs image=/path/to/image \ - --stage /tmp/pvisor-cases/vm-full \ - --overlaynet auto \ - --gateway-mode capture \ - --gateway-level dialogue \ - --gateway-route 'name="default",upstream="https://example.com/v1"' \ - --record-format lance \ - --record-destination /tmp/pvisor-cases/vm-full/trajectory \ - --memory 4GiB \ - --cpu 4 \ - -- /usr/local/bin/agent - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - bundle_expect run.agent vm-full - bundle_expect run.executor.isolation virtual_machine - bundle_expect network.interception.driver vm-smoltcp - bundle_expect filesystem.state staged - bundle_expect resources.requested.memory_bytes 4294967296 - test -d "$PVISOR_CASE_ROOT/vm-full/trajectory" - ``` - -
- -- [ ] **J03:Container + stage + read-only root + no network** - - 建议场景:适合上线前验证多项能力组合的端到端任务。 - - 用途:在容器中组合持久 stage、只读 rootfs、隔离网络和 stdout 采集。只读 rootfs 与可写 stage 是不同层面的设置。 - - 准备:OCI runtime 可运行,并为脚本设置 PVISOR_CASE_CONTAINER_IMAGE。 - - 预期:容器成功退出并留下 stage。命令为 `/bin/true`,不会生成文件变更或有意义的 stdout;本例不验证 stage 写入和网络阻断行为。 - - - - ```bash - pvisor --name container-full \ - --executor container \ - --rootfs image=alpine:latest \ - --container-read-only-rootfs \ - --container-network none \ - --stage /tmp/pvisor-cases/container-full \ - --stdio capture \ - -- /bin/true - ``` - -
- 自动回归断言(由脚本执行) - - - - ```bash - bundle_expect run.agent container-full - bundle_expect run.executor.kind container - bundle_expect run.state completed - bundle_expect filesystem.state staged - ``` - -
diff --git a/docs/src/zh/pvisor/reference/cli.md b/docs/src/zh/pvisor/reference/cli.md deleted file mode 100644 index 34b0d5605..000000000 --- a/docs/src/zh/pvisor/reference/cli.md +++ /dev/null @@ -1,399 +0,0 @@ -# `pvisor` 命令参考 - -`pvisor` 是单个 Run 和持久环境的产品命令。 -Host、OCI VM 和透明 host-rootfs VM 的完整命令示例见 -[使用 pVisor 运行工作负载](../guides/execution.md)。 - -## 按任务查找命令 - -- **运行 Agent:** 从[`pvisor run`](../get-started.md)开始,再用 `review`、 - `inspect` 和 `apply` 决定哪些修改进入项目。 -- **理解执行边界:** 使用 `status` 和 `inspect`,然后阅读[执行指南](../guides/execution.md)。 -- **保留工作区:** 用 `env create` 和 `env exec` 管理可复用的 staged environment, - 用 `env apply` 或 `env drop` 收尾。 -- **继续轨迹:** 只有在已有受支持轨迹时才使用 `replay`,先阅读[回放指南](../guides/sandbox-replay.md)。 - -第一次使用时,先复制最小闭环: - -```bash -pvisor run --stage ./runs/task-001 -- codex -pvisor review last -pvisor apply last --path src -``` - -下面的参考按 Run 生命周期组织;每组参数都配有验证下一步。 - -```text -pvisor -├── run execute one Agent Run -├── replay replay and continue an Agent-native trajectory -├── env manage durable reusable environments -├── status aggregate Run, filesystem, and network state -├── inspect open a read-only Run view -├── review review the durable Run Bundle -├── checkpoint snapshot a stopped transactional upper -├── fork start a child Run from a logical checkpoint -├── apply commit a stopped Run's filesystem stage -└── drop discard a stopped Run's filesystem stage -``` - -## 安全的第一次运行 - -```bash -pvisor -- codex -pvisor review last -``` - -默认 host 执行使用 safe-best-effort 隔离;`--stage ` 才启用当前目录的 -OverlayFS stage 在显式 `--stage` 路径(未指定时为生成的 Run 记录目录)创建独立 -Run 和可写 stage,保留改动供人工审查,并以 `0600` 写入 `run-bundle.json`。 - -`--strict` 要求每个被请求的 capability 维度都有不可绕过的 enforcement 证据, -否则在 Agent 启动前失败关闭。当前 host / container / VM 都会请求 Network 与 -Subprocess,且无一 claim Subprocess,因此 `--strict` 在这些路径上会以 -`UnsupportedPolicy` 退出。该旗标用于验证 fail-closed,不表示「更强沙箱已就绪」。 -在 Linux 上,默认 host executor 会在异步 runtime 到达 Agent 之前,通过 -pVisor 的 rootless launcher 自执行。User/mount/PID namespace、namespace 内 -PID 1 后代回收器、最小 bind-projected root 加 `chroot`、按内核协商的 Landlock ABI v1-v3 -策略、关闭继承描述符、`no_new_privs` 以及空 capability 集,使工作区约束对 -Agent 进程树不可绕过。 -`--overlaynet-deny-all` 再加一个私有 network namespace;public/allowlist -代理模式仍是协作式。在 macOS 上,默认 safe host executor 安装生成的 -Seatbelt 策略,使 staged 写入不可绕过。对 deny-all Run,它拦截 IP 和 -ambient host Unix socket,同时保留精确的 AgentCtl 与 Run 本地 IPC。读取和 -选择性网络策略仍是 ambient/协作式,并在 Bundle 中单独标注。Docker 和 KVM -传输保留同样的外层 Run、OverlayFS、AgentCtl 状态观察和 pChronicle 控制面。 - -完成后: - -```bash -pvisor review last -pvisor checkpoint last --name before-experiment -pvisor fork last --checkpoint before-experiment -- codex -pvisor apply last --all # or: pvisor drop last -``` - -CLI checkpoint 是 stopped-consistent。嵌入式 host 可以调用 -`RunHandle::checkpoint`:pVisor 发布 AgentCtl quiesce 指令,要求每个被冻进 -checkpoint 的 Session 报告匹配的 quiesced 状态,快照 raw upper,再发布 -`continue`。逻辑 checkpoint 保留文件系统和协作客户端 safe-point 边界,不 -保留进程内存。 - -持久环境拥有稳定名称和可复用 OverlayFS upper: - -```bash -pvisor env create dev --target ./project -pvisor env exec dev -- make test -pvisor env shell dev -pvisor env inspect dev -- git status --short -pvisor env stop dev -pvisor env start dev -pvisor env apply dev --path src # 提交选中部分,其余继续 staged -pvisor env apply dev --all # 提交剩余修改并重置为空 stage -pvisor env drop dev # 丢弃修改并重置为空 stage -pvisor env delete dev --force -``` - -默认元数据位于 `~/.persisting/envs`,可用 `--root` 或 `PERSISTING_ENV_HOME` -覆盖。`start` / `stop` 控制是否接受新会话,并不表示常驻虚拟机;每次 `exec` / `shell` -都会挂载同一个 writable upper,所以修改会跨命令保留。`inspect` 使用内核强制的只读视图。 -`apply --all` 或 `drop` 不会把 terminal Overlay 原地改回 `staged`;它们会创建单调递增的 -Overlay generation。命令取得环境 lease 后会重新读取 generation,避免用 reset 前的 -metadata 覆盖新 stage。 - -## 回放一条 Agent 轨迹 {#replay-an-agent-trajectory} - -`pvisor replay` 假定调用方已经正常创建了新 sandbox。它通过 `after_step` -回放完整 tool batch,用新鲜 observation 重建所选 Agent 原生上下文,然后 -启动 live Agent: - -```bash -pvisor replay \ - --agent claude-code \ - --trajectory /input/session.jsonl \ - --after-step 30 \ - --agent-entrypoint /usr/bin/claude \ - --boundary-user-prompt 'Review the fresh observation before continuing.' -``` - -OpenHands、mini-swe-agent、Pi agent、OpenCode、Codex 和 SWE-agent 使用环境中已有的模型端点 -和凭据。Pi 要求精确的 `0.83.0` runtime,并接受包含核心 `read`、`bash`、 -`edit` 和 `write` 工具的原生 RPC event JSONL。Claude Code 使用 -SandboxReplay 拥有的临时 bridge,因为它的原生 resume 传输会插入 wake-up -消息。该 bridge 在转发模型请求前校验并去掉那份精确的 Resume Transport -envelope。它不启用 pVisor Gateway、不捕获模型流量、也不持久化 bridge -审计。 - -OpenCode 要求精确的 `1.17.7` runtime,轨迹格式为 -`opencode run --format=json` 的事件 JSONL;Codex 要求精确的 `0.149.0` runtime, -轨迹格式为 Codex rollout `response_item` JSONL。两者均在新沙箱中重建原生前缀, -并调用各自的原生 resume 命令续跑。 -Codex 的 native session ID 从轨迹 `session_meta` 提取;`session_id` 只作为模型 -路由/Run 标识,不能覆盖该 native session。缺少 native session 时会 fail-closed。 - -等价的严格 replay TOML 是: - -```toml -[replay] -agent = "claude-code" -trajectory = "/input/session.jsonl" -after_step = 30 -agent_entrypoint = "/usr/bin/claude" -max_steps = 200 -session_id = "task-291-attempt-1" -replay_only = false -disable_thinking = true -boundary_user_prompt = "Review the fresh observation before continuing." -``` - -Pi 使用同一套 CLI/TOML 面。runtime 安装在 `/opt/pi-agent` 时,例如: - -```bash -pvisor replay --agent pi-agent \ - --trajectory /input/pi-agent.events.jsonl \ - --after-step 30 \ - --agent-entrypoint /opt/pi-agent/bin/pi -``` - -Replay 有三种模式。默认回放前缀并继续;`--replay-only` 执行前缀并在模型 -请求前停止;`--prepare-only` 构造前缀,不执行工具、也不要求 runtime。 -`--max-steps` 是包含已回放动作的总动作预算。 -`--allow-stale-observations` 是显式的仅 Claude 逃生口,会把 v3 结果标为 -`degraded`。 - -`--boundary-user-prompt TEXT` 在最后一条新鲜 observation 之后、第一次 live -模型推理之前追加一条用户消息。TOML 写法是 `replay.boundary_user_prompt`。 -prepare-only 和 replay-only 模式下它不参与推理;省略该选项则保持未修改的 -replay 边界。结构化结果和 replay journal 只存储注入状态、长度和 digest; -Agent 原生的 prepared 或 continued 轨迹可以包含这条用户消息。 - -结果 schema 是 `sandbox-playback.result/v3`,带类型化的 `phase`、`quality` -和 `agent_status` 字段,以及 state/output 位置、artifacts 和可选结构化失败。 -原先只用 `replay_only = true` 来构造前缀的非 Claude 调用方必须迁移到 -`prepare_only = true`。 - -`disable_thinking` 属于 `[replay]`,也暴露为 `--disable-thinking`。Claude Code -由协议 bridge 将其应用到上游请求;OpenCode 设置后会省略 `--thinking`。该选项 -不会打开 Gateway capture。可选的 `[run]`、`[overlayfs]` 和 `[overlaynet]` 段会 -创建外层受管 `pvisor run`;它们不改变内部 replay 模型路径。 - -默认情况下,replay 的内部状态、WAL、manifest、新鲜 observation 比较和原生 -工作文件留在 `/tmp/pvisor-sandbox-replay`,并随 sandbox 消失。Replay 不启用 -pVisor Gateway、pChronicle、模型流量 capture store 或 Claude Resume -Transport 审计。显式选择 `--state-dir` 或 `--output-dir` 的调用方拥有这些 -文件。用 `--replay-only` 执行前缀并在 live 推理前停止,或用 `--prepare-only` -在不执行的情况下构造它。 - -## 一套配置模型 - -`pvisor run` 只有一份规范 `RunConfig`。TOML 和命令行选项是同一组字段的两种 -表示。`--spec` 是可选且显式的;JSON 对象按准备好的 RunSpec 处理,否则按 -TOML RunConfig 处理。pVisor 不会发现隐藏的项目配置文件。 - -```bash -pvisor run \ - --name codex \ - --overlayfs-path /workspace \ - --overlayfs-compose /path/to/project \ - --overlayfs-backend directory \ - --overlayfs-commit manual \ - --overlaynet-allow api.openai.com:443 \ - --overlaynet-deny 169.254.0.0/16 \ - --overlaynet-limit 10mbps \ - --gateway-mode capture \ - --gateway-level dialogue \ - --gateway-route \ - 'name="openai", provider="openai", upstream="https://api.openai.com/v1", api_key_env="OPENAI_API_KEY"' \ - --record-format lance \ - --record-destination ./warehouse \ - -- codex -``` - -`--record-format lance` 启动 `pchronicle serve --control 127.0.0.1:0 DATASET`; -pVisor 发送共享 `EventRecord` 并等待 durable acknowledgement。本地 JSONL -或 JSON warehouse 归档使用 `--record-format json`。 - -所有新持久化的记录都同时包含 `timestamp`(RFC3339 UTC)和 -`timestamp_unix_ms`(Unix 毫秒)。它们描述同一观测时间,必须在一毫秒内 -一致。记录顺序仍由 `source + seq` 定义;时间戳是关联元数据,不是顺序的 -事实源。 - -等价 TOML 是: - -```toml -[run] -agent = "codex" -executor = "container" -command = ["codex"] - -[container] -runtime = "docker" -image = "example/codex-agent:latest" -network = "host" - -[overlayfs] -path = "/workspace" -compose = ["/path/to/project"] -backend = "directory" -commit = "manual" - -[overlaynet] -mode = "proxy" -policy = "allowlist" - -[[overlaynet.rules]] -host = "api.openai.com" -ports = [443] - -[[overlaynet.deny]] -host = "169.254.0.0/16" - -[[overlaynet.limits]] -bytes_per_second = 1250000 - -[gateway] -mode = "capture" -level = "dialogue" - -[[gateway.routes]] -name = "openai" -provider = "openai" -upstream = "https://api.openai.com/v1" -api_key_env = "OPENAI_API_KEY" - -[chronicle] -mode = "lance" -``` - -用 `pvisor run --spec run.toml` 运行。显式 CLI 标量替换 TOML 标量。提供 -任一重复 CLI 字段(`--overlayfs-compose`、`--overlaynet-allow`、 -`--overlaynet-deny`、`--overlaynet-limit` 或 `--gateway-route`)会替换该 -完整 TOML 列表。`--` 之后的命令替换 `run.command`。 - -`--container-image IMAGE` 自动选择原生 OCI container executor; -`--executor container` 让选择显式。传输层生成标准 OCI bundle,解析匹配的静态 -`linux-amd64`/`linux-arm64` pVisor,挂进 rootfs,设置 process args,并走普通 -`pvisor run --executor host --spec ...` 路径。Agent 命令放在 RunSpec -内,而不是暴露在 OCI runner argv。注入的 pVisor 创建自己的 AgentCtl 并 -返回类型化 RunResult。最终 OverlayFS cwd 和会话 Gateway 配置挂在稳定路径。 -`--container-rootfs PATH` 可直接指定已有 rootfs;否则 pVisor 使用自带 OCI -image store 准备 `--container-image`。运行时必须是 `runc` 或 `crun`,不再调用 -Docker/Podman。用户 mount 是可重复的 TOML inline table,例如: - -```bash -pvisor run \ - --container-image example/codex-agent:latest \ - --container-pvisor-binary ./dist/pvisor-linux-amd64 \ - --container-platform linux/amd64 \ - --container-network none \ - --container-mount \ - 'source="/host/cache", target="/cache", read_only=false' \ - -- codex -``` - -进程内 Gateway 和显式 OverlayNet 代理当前要求 `container.network = "host"`, -因为它们注入的地址是 host loopback 端点。关闭这些 driver 时,`none` 模式有效; -`bridge` 需要外部 CNI 配置,当前会被拒绝。executor 记录 container 隔离,但不声称完整 capability -enforcement。 - -`--executor vm` 使用静态链接的 libkrun 及其嵌入 init 启动最小 Linux guest。 -`--rootfs image=` 选择该 executor,并直接拉取 OCI/Docker 镜像,不调用 Docker、 -Podman 或 Buildah。未提供显式 rootfs 时,默认是 `ubuntu:latest`。 -manifest 和 layer digest 会被校验,host 架构选择 `linux/arm64` 或 -`linux/amd64`,解包后的 rootfs 成为 pVisor OverlayFS 的不可变 lower。 -`--image-store` 覆盖平台缓存目录。OCI 缓存目标被标为不可变,且该保护在逻辑 -checkpoint/fork 后仍然有效,因此 `pvisor apply` 不能改写被其他 Run 共享的 -rootfs。 - -在 Linux 上,`--rootfs host` 选择 host `/` 作为 VM rootfs lower,并在省略 -`--executor` 时选择 VM executor。`--rootfs ` 使用准备好的目录, -`--rootfs image=` 使用 OCI 镜像或镜像路径;三者互斥,host rootfs 在 -macOS 上被拒绝。这是统一 rootfs 语法; -`--overlayfs-path` 指定 Agent 看到的绝对路径;重复 `--overlayfs-compose` 可按命令行顺序从底层叠加到顶层,当前 workspace 是隐式底层。带 guest 工作区路径时, -省略 `--overlayfs-path` 时,视图内容默认来自当前目录;为避免 lower 与挂载点递归覆盖,pVisor 会把 merged mount 放在每个 Run 的受管路径中。 -工作区外的写入使用临时 root upper,并在 VM 退出时丢弃;工作区改动使用 -durable OverlayFS stage。 - -合并后的 rootfs 是 guest `/`,`/workspace` 成为 guest cwd。在 Linux 和 -macOS 上,vendored libkrun 通过 virtio-fs 直接服务 pVisor 的 rootfs 与 -工作区 copy-on-write union。VMM 从不重新导出 host FUSE mount,也不物化或 -对账这两棵树。Linux 使用 KVM,Apple Silicon macOS 通过同一 executor 使用 -HVF。libkrunfw 随 wheel 安装在 pVisor 旁边。源码构建否则把 pinned 官方 -release 下载到经 SHA-256 校验的平台缓存;在 macOS 上 `/usr/bin/cc` 把它的 -预构建 kernel bundle 转成所需 dylib。仍可用 `--vm-library-dir` 选择系统 -目录。OverlayNet `auto` 使用不可绕过的 VM smoltcp IPv4 TCP/DNS driver,而 -Gateway capture 使用经 guest virtual router 的内部路由。Linux 另外用 -namespace 和 Landlock 约束 VMM。macOS VMM 仍拥有调用用户的 host 权限,因此 -尽管有 guest-kernel 隔离,第一版 OCI-image 也不应被当成敌对多租户边界。 - -在 host/container 执行上,四个可见 OverlayNet 策略标志和 Gateway capture -会自动启用代理 driver。任一 `--overlayfs-path`、`--overlayfs-compose`、 -`--stage`、`--overlayfs-backend` 或 `--overlayfs-commit` 选项会 -自动启用 OverlayFS;没有单独的 mode 开关。workspace 是隐式 base,compose 层按给定顺序叠加;显式 `--stage` -是元数据、轨迹和文件系统状态的统一记录目录。当 stage 嵌在 base 或 compose 层内时,pVisor 从合并视图 -隐藏该子树,并拒绝 guest 重建它。libkrun Run 不创建 live host mountpoint, -防止 host indexer 递归进入 `/merged`。反向拓扑——stage 包含 lower -层——会被拒绝。在 pVisor 能安全物化完整 merged-vs-base diff 之前,组合 Run -拒绝 `commit=apply` 和随后的 `pvisor apply` 命令。 -在 host/container 执行上,OverlayNet 策略作用于经显式代理路由的流量,并不 -声称不可绕过的 host 网络隔离。在 libkrun VM 上,`auto` 挂上不可绕过的 -smoltcp IPv4 TCP/DNS;`off` 让 guest 离线。`--overlaynet-deny-all` 把同一 -default-deny 策略交给当前 driver。host/container 直接 socket 仍是 ambient, -而 VM Gateway 路由仍可通过 guest 的 virtual router 用于已配置的模型流量。 - -## Run 项目发现 - -当前目录是默认项目关联。启用 OverlayFS 时,`--overlayfs-compose` 指定宿主机叠加层, -`--overlayfs-path` 指定 Agent 看到的视图路径。每个 Run 在 pVisor 默认记录根目录下获得独立目录。若该根会落在 -所选 OverlayFS base 或 compose 层内,pVisor 改用系统临时 Run 根,以保持 -可写 stage 分离: - -```text -project/ # reusable workspace / default base - -~/.persisting/runs/ -└── run-/ # one generated Run and default stage - ├── run.json - ├── run-bundle.json # mode 0600; outcome + safety + changes + effects - ├── overlay.json # when OverlayFS is enabled - ├── upper/ # or a Run-named Jujutsu workspace upper - ├── merged/ - ├── checkpoints/ - ├── lease.lock - ├── control.sock # while a live OverlayFS Run is available - ├── .capture/ # when OverlayNet/Gateway is enabled - └── chronicle/ # default pChronicle location -``` - -生命周期命令接受 Run id、Run 目录、项目工作区、`run.json`、upper 或 merged -路径。项目工作区选择其最新 Run: - -```bash -pvisor status /path/to/project -pvisor inspect /path/to/project -- rg TODO . -pvisor apply /path/to/project --all -pvisor apply /path/to/project --path src --path tests/unit -pvisor apply /path/to/project --include 'docs/**' --exclude 'docs/generated/**' -pvisor apply /path/to/project --target /path/to/another-target --all -pvisor drop /path/to/project -``` - -`inspect` 创建单独的内核只读视图。`apply` 和 `drop` 拒绝改写 live Run。 -过滤后的 apply 是依赖闭合且可重复的:未选路径保持 staged,不透明目录和 -hard-link 组保持原子。每个成功 batch 持久化到 `apply-ledger.json`。 -overlay 为每个被改写的目标路径记录 durable first-touch fingerprint。若所选 -目标路径在 staging 之后发生变化,`apply` fail closed;已准备的 batch 向前 -恢复,单个非目录替换用同目录原子 rename 提交。host 文件系统仍不为任意 -多文件 batch 提供单一原子提交点。 -提交全部剩余改动或丢弃 stage 是终态;`drop` 不能撤销已 apply 的 batch, -`apply` 也不能恢复已丢弃的改动。终态清理删除 `upper`、`work` 和其他一次性 -staging 数据,但保留紧凑的 Run/Overlay 元数据、apply ledger 和 pChronicle -历史。 - -## 相关工作流 - -- [运行第一个 Agent](../get-started.md):最短完整闭环。 -- [执行环境](../guides/execution.md):选择 provider。 -- [审查并应用 Effect](../guides/review-apply.md):过滤且可重复的 apply。 -- [网络控制](../guides/network.md) 与 [捕获轨迹](../guides/capture.md):其他 - Effect 维度。 diff --git a/docs/src/zh/pvisor/reference/index.md b/docs/src/zh/pvisor/reference/index.md deleted file mode 100644 index d6fdfe668..000000000 --- a/docs/src/zh/pvisor/reference/index.md +++ /dev/null @@ -1,7 +0,0 @@ -# pVisor Reference - -- [`pvisor` 命令参考](cli.md) -- [Case catalog](cases.md) — 可执行的 Provider 边界验证场景 - -命令参考描述当前公共 surface。概念见 [pVisor Concepts](../concepts/index.md),机制与已知 -缺口见 [pVisor Design](../design/index.md)。 diff --git a/docs/src/zh/rfcs/0002-events-format.md b/docs/src/zh/rfcs/0002-events-format.md index 203d045fc..afcc75008 100644 --- a/docs/src/zh/rfcs/0002-events-format.md +++ b/docs/src/zh/rfcs/0002-events-format.md @@ -7,7 +7,7 @@ | **Date** | 2026-07-30 | | **Component** | `persisting-events` + Gateway + pChronicle | | **Implements** | `persisting-events::EventRecord` · `persisting-pchronicle` `formats/events.rs` / `EventRow` | -| **Related** | [RFC-0001 Storyline](0001-storyline-format.md) · [RFC-0007 Events/Sidecar 边界](0007-events-contract-pchronicle-sidecar.md) · [Capture 管线](../pvisor/design/gateway.md) · [轨迹存储](../pchronicle/design/trajectory-storage.md) | +| **Related** | [RFC-0001 Storyline](0001-storyline-format.md) · [RFC-0007 Events/Sidecar 边界](0007-events-contract-pchronicle-sidecar.md) · Capture 管线 (外部仓库) · [轨迹存储](../pchronicle/design/trajectory-storage.md) | --- @@ -553,7 +553,7 @@ key = events 字段,value = 在 Storyline 上求值的 JSONPath。 | 文档 | 关系 | |---|---| | [轨迹存储](../pchronicle/design/trajectory-storage.md) | Lance SoT;本 RFC 强调 SoT 内容应是 HTTP wire | -| [Gateway 管线](../pvisor/design/gateway.md) | 生产 events;Story 边界在 **之后** 解释 | +| Gateway 管线 (外部仓库) | 生产 events;Story 边界在 **之后** 解释 | | [RFC-0001 Storyline](0001-storyline-format.md) | 有损 Normal 视图 / hub | | [轨迹 Markdown 格式](../pchronicle/reference/agenticmd.md) | 人读投影,不是 SoT | diff --git a/docs/src/zh/roadmap.md b/docs/src/zh/roadmap.md index 5c5695518..dd2716649 100644 --- a/docs/src/zh/roadmap.md +++ b/docs/src/zh/roadmap.md @@ -5,23 +5,19 @@ ## 当前:让本地闭环可靠 -- 让 pVisor 的 Run → review → apply 在 macOS 和 Linux 上稳定可预测。 -- 在每个 Run Bundle 中清楚展示实际控制机制、Effect 和警告。 - 让 pChronicle 的 onboarding、Dataset 发现、有界 SQL 和 Evidence 定位无需服务 或账号即可使用。 - 保持中英文文档路径一致,并让示例可以运行。 ## 下一步:把执行连接到持久历史 -- 让配置好的 pVisor capture 生成稳定的 pChronicle Source。 - 在捕获、规范化和查询之间保留 Run identity 与 lineage。 - 改进 Run、Session 和 revision 的比较流程。 -- 记录不同 provider 的边界,不宣称一个统一的隔离强度。 ## 之后:从单机走向团队和集群 - 在不隐藏 provenance 的前提下共享 Dataset catalog 和策略。 -- 为 host、OCI container 和 VM 提供可复现的执行配置。 + - 补齐 retention、访问控制和成本感知存储的运维指南。 ## 如何理解路线图 diff --git a/docs/src/zh/system-design/architecture.md b/docs/src/zh/system-design/architecture.md index 97af5ad9b..4072c0522 100644 --- a/docs/src/zh/system-design/architecture.md +++ b/docs/src/zh/system-design/architecture.md @@ -1,5 +1,7 @@ # 端到端架构 +> pVisor 与 pPilot 已拆分到外部仓库。本仓库保留 pChronicle 及捕获、持久历史所需的内部库。 + 本文只定义 Persisting 产品之间的契约。Provider 机制属于 pVisor Design,存储布局属于 pChronicle Design,命令属于各产品 Reference。 @@ -29,8 +31,8 @@ Gateway、OverlayFS 和 OverlayNet 是 pVisor 运行时机制,不构成独立 Provider 会在 Run Bundle 中分别记录请求的 capability 与实际生效的维度。进程成功退出 不代表请求的边界已经安装;workspace stage 也独立于产生它的 Provider,仍可 review。 -Provider 的行为与前置条件见 [pVisor 隔离设计](../pvisor/design/isolation.md) 和 -[执行指南](../pvisor/guides/execution.md)。 +Provider 的行为与前置条件见 pVisor 隔离设计 (外部仓库) 和 +执行指南 (外部仓库)。 ## 独立 Ingress 路径 @@ -126,7 +128,7 @@ Ingestion 保留这些边界。规范化表示或 Snapshot 不会升级 Source pVisor 默认构建不链接 Lance/DataFusion。配置后的 Chronicle 发布会通过带认证的 loopback IPC 启动 pChronicle sidecar,并且只把 sidecar 成功响应视为 durable acknowledgement。旧模式名 `lance` 是 `spawn` 的兼容别名;pVisor 不再自行写 Lance。 -Sidecar 标志与模式名见 [pVisor CLI](../pvisor/reference/cli.md) 与 +Sidecar 标志与模式名见 pVisor CLI (外部仓库) 与 [RFC-0007](../rfcs/0007-events-contract-pchronicle-sidecar.md)。 ## 故障与恢复 @@ -174,11 +176,11 @@ Evidence 层级见[安全与 Evidence](security-evidence.md),可迁移要求 | 边界 | 契约 Owner | 详细文章 | | --- | --- | --- | | 逻辑运行事件与本地 Chronicle control 协议 | `persisting-events` | [RFC-0007](../rfcs/0007-events-contract-pchronicle-sidecar.md) | -| Agent 执行与 Effect review | pVisor | [pVisor 概念](../pvisor/concepts/index.md)与[指南](../pvisor/guides/index.md) | -| Provider 与运行时机制 | pVisor | [pVisor Design](../pvisor/design/index.md) | +| Agent 执行与 Effect review | pVisor | pVisor 概念 (外部仓库)与指南 (外部仓库) | +| Provider 与运行时机制 | pVisor | pVisor Design (外部仓库) | | Dataset、事实与 Projection | pChronicle | [pChronicle 概念](../pchronicle/concepts/index.md) | | 存储与 Snapshot 实现 | pChronicle | [pChronicle Design](../pchronicle/design/index.md) | -| 稳定命令语法与格式 | 各产品 | [pVisor Reference](../pvisor/reference/index.md)与 [pChronicle Reference](../pchronicle/reference/index.md) | +| 稳定命令语法与格式 | 各产品 | pVisor Reference (外部仓库)与 [pChronicle Reference](../pchronicle/reference/index.md) | | 规范性 ownership 决策 | Project RFC | [RFC 索引](../rfcs/index.md) | 只有跨产品契约变化时才修改本文。产品实现状态和 roadmap 属于对应 Design 页或 Project diff --git a/docs/src/zh/system-design/design-principles.md b/docs/src/zh/system-design/design-principles.md index 76d986eba..210a4535c 100644 --- a/docs/src/zh/system-design/design-principles.md +++ b/docs/src/zh/system-design/design-principles.md @@ -1,5 +1,7 @@ # 设计原则 +> pVisor 与 pPilot 已拆分到外部仓库。本仓库保留 pChronicle 及捕获、持久历史所需的内部库。 + 这些原则解释了为什么 Persisting 拆分为两个产品,也解释了文档为什么强调可审查的步骤。 ## 边界必须明确 diff --git a/docs/src/zh/system-design/index.md b/docs/src/zh/system-design/index.md index 379010175..83853ed1d 100644 --- a/docs/src/zh/system-design/index.md +++ b/docs/src/zh/system-design/index.md @@ -1,12 +1,14 @@ # System Design -Persisting 提供 Agent 执行与轨迹历史的持久化基础设施。本节聚焦 -当前公开产品路径: +> pVisor 与 pPilot 已拆分到外部仓库。本仓库保留 pChronicle 及捕获、持久历史所需的内部库。 -- [pVisor](../pvisor/index.md) 虚拟化并治理单个 Agent Run; +Persisting 提供持久 Agent 轨迹历史。本节描述 pChronicle 与外部执行组件的集成: + +- pVisor (外部仓库) 虚拟化并治理单个 Agent Run; - [pChronicle](../pchronicle/index.md) 把持久轨迹 Source 组织为可查询 Dataset。 -Gateway、OverlayFS 与 OverlayNet 是 pVisor 运行时机制。存在稳定 Run identity 时,它会连接 +Gateway 与 OverlayNet 在本仓库支持轨迹捕获;OverlayFS 属于外部执行组件。 +存在稳定 Run identity 时,它会连接 这些产品域,但各域也有独立入口。 ![Persisting 产品域与集成关系](../../assets/diagrams/persisting/system-products.svg) @@ -46,7 +48,7 @@ capture 交接。 - [完整架构与目标模型](architecture.md) - [从本地到集群的连续性](local-to-fleet.md) - [安全与 Evidence 模型](security-evidence.md) -- [pVisor 实现边界](../pvisor/design/index.md) +- pVisor 实现边界 (外部仓库) - [pChronicle 实现边界](../pchronicle/design/index.md) 交付状态以产品 Design 页面与[项目工程笔记](../project/engineering.md)为准。目标架构不能 diff --git a/docs/src/zh/system-design/local-to-fleet.md b/docs/src/zh/system-design/local-to-fleet.md index 08ec31c31..8eace50e5 100644 --- a/docs/src/zh/system-design/local-to-fleet.md +++ b/docs/src/zh/system-design/local-to-fleet.md @@ -1,5 +1,7 @@ # 从本地到集群 +> pVisor 与 pPilot 已拆分到外部仓库。本仓库保留 pChronicle 及捕获、持久历史所需的内部库。 + 可移植单位是逻辑 Run,而不是一台正在运行的虚拟机。 ![AgentVisor 执行连续体](../../assets/diagrams/agentvisor/execution-continuum.svg) @@ -19,6 +21,6 @@ 在个人设备上,主要体验是 staged workspace 与可审查 Effect;在集群中,同一模型增加 placement、tenant isolation、lease、attestation、恢复与 reconciliation,而不重新定义 Run。 -稳定 identity 模型见 [Run、Attempt 与 Effect](../pvisor/concepts/run-model.md)。Provider -admission 属于 [pVisor 隔离设计](../pvisor/design/isolation.md);集群协调(placement、lease +稳定 identity 模型见 Run、Attempt 与 Effect (外部仓库)。Provider +admission 属于 pVisor 隔离设计 (外部仓库);集群协调(placement、lease 与 reconciliation)属于部署控制面,不会改变逻辑 Run 契约。 diff --git a/docs/src/zh/system-design/security-evidence.md b/docs/src/zh/system-design/security-evidence.md index 401789bfe..32d9718ed 100644 --- a/docs/src/zh/system-design/security-evidence.md +++ b/docs/src/zh/system-design/security-evidence.md @@ -1,5 +1,7 @@ # 安全与 Evidence 模型 +> pVisor 与 pPilot 已拆分到外部仓库。本仓库保留 pChronicle 及捕获、持久历史所需的内部库。 + Persisting 不会把安全压缩成一个 `safe` 或 `sandboxed` 标签。每个 Run 都按 capability 维度报告保证。pVisor 拥有 admission 与 runtime enforcement。Placement 与恢复机制不会提升 pVisor 的 Evidence 等级。配置后的 @@ -41,6 +43,6 @@ requested capability 最后一段 event 路径比 Run Bundle 更窄:当前不会发布完整的 Artifact、lineage、filesystem Effect、AgentCtl/network/resource Evidence、output 或 metrics 清单。 -用户模型见 [Capability 与 Evidence](../pvisor/concepts/capabilities-and-evidence.md),平台机制见 -[pVisor 隔离设计](../pvisor/design/isolation.md)与 [OverlayNet](../pvisor/design/overlaynet.md), +用户模型见 Capability 与 Evidence (外部仓库),平台机制见 +pVisor 隔离设计 (外部仓库)与 OverlayNet (外部仓库), 历史边界见[事实与 Projection](../pchronicle/concepts/facts-and-projections.md)。 diff --git a/docs/src/zh/why-persisting.md b/docs/src/zh/why-persisting.md index 694d49cbd..38990b968 100644 --- a/docs/src/zh/why-persisting.md +++ b/docs/src/zh/why-persisting.md @@ -8,10 +8,8 @@ Agent 可能先产出有用的结果,之后才发现很难回答“发生了 Agent 会修改文件、调用工具、读取数据,并在一次长流程中持续做决定。终端 输出不足以安全审查;没有持久记录的沙箱难以复盘;原始事件日志又很难稳定查询。 -Persisting 把执行和历史看成两个相关但独立的工作: +Persisting 专注于持久 Agent 历史: -- **pVisor 管理 Run。** 它提供 staged workspace,记录实际生效的控制机制,并 - 让人在应用 Effect 前先审查。 - **pChronicle 保存轨迹。** 它把支持的 Source 规范化为可查询 Dataset,方便 之后检查、比较和改进 Run。 @@ -29,8 +27,7 @@ Effect 和 Evidence。 ## 什么时候适合使用 当 Agent 能修改真实项目、Run 需要在人审查后才能合并,或轨迹需要在终端关闭后 -仍可使用时,Persisting 就适合介入。需要控制执行时从 pVisor 开始;已有历史时 -从 pChronicle 开始;只有问题跨越执行与历史边界时才连接两者。 +仍可使用时,Persisting 就适合介入。使用 pChronicle 查询持久历史;pVisor 与 pPilot 已拆分到外部仓库。 如果只是运行一次无需审查、也无需留存历史的脚本,Persisting 可能不是必要的基础设施。 diff --git a/docs/zensical.toml b/docs/zensical.toml index 14bf23039..98409ba50 100644 --- a/docs/zensical.toml +++ b/docs/zensical.toml @@ -14,10 +14,8 @@ nav = [ { "Get started" = [ "en/overview.md", { "Installation" = "en/installation.md" }, - { "Running an Agent with pVisor" = "en/pvisor/get-started.md" }, { "Recording and Analyzing Agent Trajectories" = "en/pchronicle/get-started.md" }, ] }, - { "pVisor" = ["en/pvisor/index.md", "en/pvisor/get-started.md", { "Concepts" = ["en/pvisor/concepts/index.md", "en/pvisor/concepts/agentvisor.md", "en/pvisor/concepts/run-model.md", "en/pvisor/concepts/capabilities-and-evidence.md"] }, { "Guides" = ["en/pvisor/guides/index.md", "en/pvisor/guides/execution.md", "en/pvisor/guides/review-apply.md", "en/pvisor/guides/network.md", "en/pvisor/guides/capture.md", "en/pvisor/guides/sandbox-replay.md", "en/pvisor/guides/troubleshooting.md"] }, { "Design" = ["en/pvisor/design/index.md", "en/pvisor/design/isolation.md", "en/pvisor/design/overlaynet.md", "en/pvisor/design/gateway.md", "en/pvisor/design/cli.md"] }, { "Reference" = ["en/pvisor/reference/index.md", "en/pvisor/reference/cli.md", "en/pvisor/reference/cases.md"] }] }, { "pChronicle" = ["en/pchronicle/index.md", "en/pchronicle/get-started.md", { "Concepts" = ["en/pchronicle/concepts/index.md", "en/pchronicle/concepts/dataset-and-source.md", "en/pchronicle/concepts/facts-and-projections.md"] }, { "Guides" = ["en/pchronicle/guides/index.md", "en/pchronicle/guides/discover-and-query.md", "en/pchronicle/guides/exchange.md", "en/pchronicle/guides/serve.md", "en/pchronicle/guides/serve-gateway.md", "en/pchronicle/guides/ui.md", "en/pchronicle/guides/troubleshooting.md"] }, { "Design" = ["en/pchronicle/design/index.md", "en/pchronicle/design/architecture.md", "en/pchronicle/design/catalog.md", "en/pchronicle/design/trajectory-storage.md", "en/pchronicle/design/storyline-lance.md"] }, { "Reference" = ["en/pchronicle/reference/index.md", "en/pchronicle/reference/cli.md", "en/pchronicle/reference/query-model.md", "en/pchronicle/reference/terminology.md", "en/pchronicle/reference/agenticmd.md", "en/pchronicle/reference/formats/index.md", "en/pchronicle/reference/cases-self.md", "en/pchronicle/reference/cases-platform.md"] }] }, { "Agent Infra Thinkings" = ["en/why-persisting.md", "en/roadmap.md", "en/guides/using-persisting.md", "en/system-design/index.md", "en/system-design/design-principles.md", "en/system-design/architecture.md", "en/system-design/local-to-fleet.md", "en/system-design/security-evidence.md", "en/project/index.md", "en/project/examples.md", "en/project/engineering.md", "en/project/releasing.md", { "RFCs" = ["en/rfcs/index.md", "en/rfcs/0001-storyline-format.md", "en/rfcs/0002-events-format.md", "en/rfcs/0003-pchronicle-ownership.md", "en/rfcs/0004-actf-format.md", "en/rfcs/0005-pchronicle-revision-lineage.md", "en/rfcs/0006-pchronicle-vortex-backend.md", "en/rfcs/0007-events-contract-pchronicle-sidecar.md", "en/rfcs/0008-atif-format.md", "en/rfcs/0009-openai-messages-format.md", "en/rfcs/0010-agent-corpus-lance-layout.md", "en/rfcs/0012-pchronicle-find-query-syntax.md", "en/rfcs/0013-pchronicle-warehouse-catalog.md", "en/rfcs/0014-compact-jsonl.md", "en/rfcs/0015-chronicle-manifest.md"] }] }, ] diff --git a/examples/README.md b/examples/README.md index d6ba126b2..538fbbaeb 100644 --- a/examples/README.md +++ b/examples/README.md @@ -3,18 +3,9 @@ **按产品问题组织的可复现 CLI 示例,而不是按 API 罗列。** 每个 `run.sh` 管理自己的 `.work/`、运行产品命令、直接打印生成的文件与报告。 -运行后可继续检查 `.work/`。这里不拥有产品实现;pVisor / pPilot / pChronicle +运行后可继续检查 `.work/`。这里不拥有产品实现;pChronicle 的行为以文档站和对应 crate 为准。 -## pVisor - -| 示例 | 指标 | -|---|---| -| [1.1 文件系统隔离](pvisor/01-filesystem-isolation/) | lower 值、upper 文件数、Bundle changes | -| [1.2 changeset 管理](pvisor/02-changeset-management/) | review/apply/drop 文件数 | -| [1.3 pVisor 网络边界](pvisor/03-network-isolation/) | allowlist、deny-all,以及 direct socket 可绕过 cooperative proxy 的边界 | -| [1.4 Gateway 捕获与管控 LLM](pvisor/04-gateway-llm-control/) | upstream POST、sink requests、AgenticMD blocks | - ## pChronicle [`data/`](data/) 提供可直接传给 `pchronicle` 的 ATIF、OpenAI Messages 和 @@ -29,29 +20,17 @@ ACTF 小型确定性 Dataset,用于手动体验和 CLI 集成测试。 | [2.5 外围格式往返](pchronicle/05-format-roundtrip/) | 严格 ATIF 往返后的 JSON 数据模型相等 | | [2.6 直接查询 OpenAI/ACTF](pchronicle/06-query-openai-actf-directly/) | 两种交换格式直接映射为统一逻辑表 | -## pPilot - -| 示例 | 指标 | -|---|---| -| [3.1 run](ppilot/01-run/) | 完成/失败数、结果总和、worker slot 数 | -| [3.2 produce](ppilot/02-produce/) | 完成数、Run Bundle 数、lineage 数 | - ## Run ```bash just examples -just examples-pvisor just examples-pchronicle -just examples-ppilot ``` 这些入口统一增量编译并使用 release targets,之后复用 Cargo 缓存。需要 -macOS/Linux、Cargo、Python 3、`jq`、`awk`、`curl` 和常见 POSIX 工具; -OverlayFS 示例还需要 macFUSE 或 FUSE3。 +macOS/Linux、Cargo、Python 3、`jq`、`awk`、`curl` 和常见 POSIX 工具。 ## Links -- [Reproducible examples](../docs/src/project/examples.md) -- [pVisor get started](../docs/src/pvisor/get-started.md) -- [pPilot get started](../docs/src/ppilot/get-started.md) -- [pChronicle get started](../docs/src/pchronicle/get-started.md) +- [Reproducible examples](../docs/src/en/project/examples.md) +- [pChronicle get started](../docs/src/en/pchronicle/get-started.md) diff --git a/examples/ppilot/01-run/README.md b/examples/ppilot/01-run/README.md deleted file mode 100644 index 07ccb0c9d..000000000 --- a/examples/ppilot/01-run/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# 3.1 pPilot run - -**问题:一个流式 Python plan 能否通过多个 worker 执行,并把终态结果写入结果 journal?可复现结论:6 个任务全部成功,平方和为 55,并至少使用 2 个 worker slot。** - -`plan.py` 产生 6 个稳定 task id,`execute()` 返回每个输入的平方。脚本使用 2 个 -worker、每个 worker 2 个 slot,然后直接打印执行流和 durable result journal。 - -## Run - -```bash -./run.sh -``` - -预期:6 个任务全部成功,平方和为 55,并至少使用 2 个 worker slot。 - -## Links - -- [pPilot examples](../README.md) -- [pPilot CLI](../../../docs/src/ppilot/reference/cli.zh.md) diff --git a/examples/ppilot/01-run/plan.py b/examples/ppilot/01-run/plan.py deleted file mode 100644 index 663da40db..000000000 --- a/examples/ppilot/01-run/plan.py +++ /dev/null @@ -1,8 +0,0 @@ -def plan(): - for value in range(6): - yield {"id": f"square-{value}", "value": value} - - -def execute(item): - value = item["value"] - return {"square": value * value} diff --git a/examples/ppilot/01-run/run.sh b/examples/ppilot/01-run/run.sh deleted file mode 100755 index 014c73f34..000000000 --- a/examples/ppilot/01-run/run.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Use the pPilot binary built from this checkout. -export PATH="../../../target/release:$PATH" - -# Remove results from the previous run. -rm -rf .work -mkdir .work - -# Execute the plan with two workers and persist every result in the sink. -ppilot run plan.py --workers 2 --per-worker 2 \ - --sink .work/sink --results ndjson - -# Show the durable NDJSON written by pPilot. -echo 'Durable results:' -cat .work/sink/ready.ndjson diff --git a/examples/ppilot/02-produce/README.md b/examples/ppilot/02-produce/README.md deleted file mode 100644 index e02840e72..000000000 --- a/examples/ppilot/02-produce/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# 3.2 pPilot produce - -**问题:Python planner 能否流式生成多个独立 pVisor Run,并保留可审查的 Run Bundle?可复现结论:3 条 Run 全部完成,生成 3 个 `run-bundle.json`,每个 Bundle 都带有 batch id。** - -`production.py` 生成 3 条 shell Run。脚本关闭与本问题无关的 Gateway capture,以 2 路 -并发执行,然后打印 production report 和每个 Run Bundle 的 lineage。 - -## Run - -```bash -./run.sh -``` - -预期:3 条 Run 全部完成,生成 3 个 `run-bundle.json`,每个 Bundle 都带有 batch id。 - -## Links - -- [pPilot examples](../README.md) -- [Orchestrate many Agent Runs](../../../docs/src/ppilot/guides/orchestrate.zh.md) diff --git a/examples/ppilot/02-produce/production.py b/examples/ppilot/02-produce/production.py deleted file mode 100644 index bf5ab1386..000000000 --- a/examples/ppilot/02-produce/production.py +++ /dev/null @@ -1,7 +0,0 @@ -def plan(): - for index in range(3): - yield { - "id": f"trajectory-{index}", - "agent": "example-agent", - "command": ["/bin/sh", "-c", f"printf trajectory-{index}"], - } diff --git a/examples/ppilot/02-produce/run.sh b/examples/ppilot/02-produce/run.sh deleted file mode 100755 index 71056a005..000000000 --- a/examples/ppilot/02-produce/run.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Use the pPilot binary built from this checkout. -export PATH="../../../target/release:$PATH" - -# Remove Run Bundles from the previous run. -rm -rf .work -mkdir .work - -# Produce three trajectories with at most two running in parallel. -ppilot produce production.py --output .work/runs \ - --parallelism 2 --batch-id example-batch --no-capture - -# Print the run and orchestration sections from every generated bundle. -echo 'Generated Run Bundles:' -jq '{run: .run, orchestration}' .work/runs/*/run-bundle.json diff --git a/examples/ppilot/README.md b/examples/ppilot/README.md deleted file mode 100644 index 69cebb32f..000000000 --- a/examples/ppilot/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# pPilot:规模化 Run 生产 - -**问题:pPilot 的两个公开工作模式能否用确定性脚本复现?可复现结论:`run` 把 `plan()` / `execute()` 写入 durable sink;`produce` 生成可审查的独立 pVisor Run。** - -这组示例覆盖 pPilot 的两个公开工作模式。每个 `run.sh` 直接执行一种模式,并打印 -durable sink 或 Run Bundle。这里不拥有编排实现或 pVisor 隔离后端。 - -| 示例 | 可复现结论 | -|---|---| -| [01-run](01-run/) | `plan()` / `execute()` 任务被并发执行并写入 durable sink | -| [02-produce](02-produce/) | Python planner 生成多个独立、可审查的 pVisor Run | - -CLI 的正式命令名是 `produce`;它对应“生产一批轨迹 Run”的模式,不是 `product`。 -这些示例默认使用本地 Pulsing workers,不要求 `torchrun` 或多节点环境。 - -## Run - -```bash -just examples-ppilot -``` - -## Links - -- [Reproducible examples](../../docs/src/project/examples.md) -- [Orchestrate many Agent Runs](../../docs/src/ppilot/guides/orchestrate.zh.md) -- [pPilot CLI](../../docs/src/ppilot/reference/cli.zh.md) diff --git a/examples/pvisor/01-filesystem-isolation/README.md b/examples/pvisor/01-filesystem-isolation/README.md deleted file mode 100644 index 1415bae66..000000000 --- a/examples/pvisor/01-filesystem-isolation/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# 1.1 pVisor 的文件系统隔离 - -**问题:Agent 修改文件时,原始项目目录会不会立即改变?可复现结论:base 保持原值,upper 和 Bundle 都记录 2 个变化,`filesystem_non_bypassable=false`。** - -`run.sh` 把 `base/` 作为 OverlayFS base,让 Agent 修改一个文件并新增一个文件,然后 -展示 base、stage upper 和 Run Bundle。`test.sh` 对这些产物执行回归断言。该实验测量 -事务工作区隔离,不声称 Host 进程无法访问其他宿主路径。 - -## Run - -```bash -./run.sh -./test.sh # 执行同一场景并验证预期结果 -``` - -预期:base 保持原值,upper 和 Bundle 都记录 2 个变化, -`filesystem_non_bypassable=false`。 - -## Links - -- [pVisor examples](../README.md) -- [Review and apply effects](../../../docs/src/pvisor/guides/review-apply.zh.md) diff --git a/examples/pvisor/01-filesystem-isolation/run.sh b/examples/pvisor/01-filesystem-isolation/run.sh deleted file mode 100755 index 1de902593..000000000 --- a/examples/pvisor/01-filesystem-isolation/run.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -example_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -source "$example_dir/../common.sh" -pvisor_example_init "$example_dir" filesystem-isolation -command -v jq >/dev/null - -# Create a clean host directory for the isolated command. -pvisor_example_reset -mkdir -p "$work_dir/base" -printf 'original\n' >"$work_dir/base/existing.txt" -base="$work_dir/base" - -# The project workspace is reusable; pVisor creates an independent stage for this Run. -( - cd "$base" - "$pvisor_bin" run --overlayfs-commit manual --stdio capture -- \ - /bin/sh -c 'printf "changed\n" > existing.txt; printf "new\n" > new.txt' -) -run_dir="$(find "$PERSISTING_RUN_HOME" -mindepth 1 -maxdepth 1 -type d -name 'run-*' -print -quit)" -test -n "$run_dir" - -# Print the unchanged host file and the two staged files. -echo 'Base directory:' -cat "$base/existing.txt" - -echo 'Staged upper directory:' -cat "$run_dir/upper/existing.txt" -cat "$run_dir/upper/new.txt" - -echo 'Run Bundle:' -jq '{filesystem, safety}' "$run_dir/run-bundle.json" diff --git a/examples/pvisor/01-filesystem-isolation/test.sh b/examples/pvisor/01-filesystem-isolation/test.sh deleted file mode 100755 index 9611c7258..000000000 --- a/examples/pvisor/01-filesystem-isolation/test.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -example_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -work_root="${WORK_ROOT:-$example_dir/.work}" -work_dir="$work_root/filesystem-isolation" -export WORK_ROOT="$work_root" - -bash "$example_dir/run.sh" - -base="$work_dir/base" -run_dir="$(find "$work_dir/runs" -mindepth 1 -maxdepth 1 -type d -name 'run-*' -print -quit)" -test -n "$run_dir" -test "$(cat "$base/existing.txt")" = original -test ! -e "$base/new.txt" -test "$(cat "$run_dir/upper/existing.txt")" = changed -test "$(cat "$run_dir/upper/new.txt")" = new -jq -e ' - .run.state == "completed" and - .filesystem.state == "staged" and - .filesystem.changed_files == 2 and - .safety.filesystem_changes_staged == true and - .safety.filesystem_non_bypassable == true -' "$run_dir/run-bundle.json" >/dev/null - -echo 'RESULT example=filesystem-isolation base_unchanged=true staged_changes=2' diff --git a/examples/pvisor/02-changeset-management/README.md b/examples/pvisor/02-changeset-management/README.md deleted file mode 100644 index 39a31d21f..000000000 --- a/examples/pvisor/02-changeset-management/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# 1.2 pVisor 的 changeset 管理 - -**问题:同一个 staged changeset 能否先查看,再明确选择应用或删除?可复现结论:review 共看到 3 个变化;2 个被应用,1 个被删除且没有进入 base。** - -`run.sh` 创建两个独立 Run:第一个通过 `review --json` 查看后 `apply`,第二个查看后 -`drop`。`test.sh` 执行同一流程并检查最终 base 文件系统。 - -## Run - -```bash -./run.sh -./test.sh # 执行同一场景并验证预期结果 -``` - -预期:review 共看到 3 个变化;2 个被应用,1 个被删除且没有进入 base。 - -## Links - -- [pVisor examples](../README.md) -- [Review and apply effects](../../../docs/src/pvisor/guides/review-apply.zh.md) diff --git a/examples/pvisor/02-changeset-management/run.sh b/examples/pvisor/02-changeset-management/run.sh deleted file mode 100755 index 83a161026..000000000 --- a/examples/pvisor/02-changeset-management/run.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -example_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -source "$example_dir/../common.sh" -pvisor_example_init "$example_dir" changeset-management -command -v jq >/dev/null - -# Create the host directory shared by the apply and drop examples. -pvisor_example_reset -mkdir -p "$work_dir/base" -printf 'original\n' >"$work_dir/base/existing.txt" -base="$work_dir/base" - -# Review and apply the first Run, making its staged files visible on the host. -( - cd "$base" - "$pvisor_bin" run --overlayfs-commit manual --stdio capture -- \ - /bin/sh -c 'printf "accepted\n" > existing.txt; printf "accepted\n" > accepted.txt' -) -"$pvisor_bin" review --json "$base" >"$work_dir/apply-review.json" -jq '{run, filesystem}' "$work_dir/apply-review.json" -"$pvisor_bin" apply "$base" >/dev/null - -echo 'Base directory after apply:' -cat "$base/existing.txt" -cat "$base/accepted.txt" - -# Review and drop the second Run, leaving the host directory unchanged. -( - cd "$base" - "$pvisor_bin" run --overlayfs-commit manual --stdio capture -- \ - /bin/sh -c 'printf "rejected\n" > rejected.txt' -) -"$pvisor_bin" review --json "$base" >"$work_dir/drop-review.json" -jq '{run, filesystem}' "$work_dir/drop-review.json" -"$pvisor_bin" drop "$base" >/dev/null - -echo 'Base directory after drop:' -cat "$base/existing.txt" -cat "$base/accepted.txt" diff --git a/examples/pvisor/02-changeset-management/test.sh b/examples/pvisor/02-changeset-management/test.sh deleted file mode 100755 index 38b506424..000000000 --- a/examples/pvisor/02-changeset-management/test.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -example_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -work_root="${WORK_ROOT:-$example_dir/.work}" -work_dir="$work_root/changeset-management" -export WORK_ROOT="$work_root" - -bash "$example_dir/run.sh" - -base="$work_dir/base" -jq -e '.run.state == "completed" and .filesystem.changed_files == 2' \ - "$work_dir/apply-review.json" >/dev/null -jq -e '.run.state == "completed" and .filesystem.changed_files == 1' \ - "$work_dir/drop-review.json" >/dev/null -test "$(cat "$base/existing.txt")" = accepted -test "$(cat "$base/accepted.txt")" = accepted -test ! -e "$base/rejected.txt" - -echo 'RESULT example=changeset-management reviewed=3 applied=2 dropped=1' diff --git a/examples/pvisor/03-network-isolation/README.md b/examples/pvisor/03-network-isolation/README.md deleted file mode 100644 index 058bd21e8..000000000 --- a/examples/pvisor/03-network-isolation/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# 1.3 pVisor 网络边界 - -**问题:pVisor 当前控制的是哪些网络流量?可复现结论:OverlayNet 控制 cooperative proxy 流量,不是 network sandbox;direct socket 可绕过代理。** - -```bash -./run.sh -./test.sh # 执行同一场景并验证预期结果 -``` - -脚本启动一个本地 HTTP server,然后平铺执行三个场景: - -1. `--overlaynet-allow` 允许经过代理访问声明的目标; -2. `--overlaynet-deny-all` 拒绝经过代理的请求; -3. `curl --noproxy "*"` 直接连接同一目标,证明 direct socket 可以绕过代理。 - -核心命令就是普通的 pVisor CLI: - -```bash -pvisor run --overlaynet-allow 127.0.0.1: -- \ - agent-command - -pvisor run --overlaynet-deny-all -- \ - agent-command -``` - -纯 OverlayNet 运行会将当前目录作为 Run 的项目关联路径;每次执行的 Run 记录和 -Bundle 则独立保存在 `PERSISTING_RUN_HOME` 下。 - -`run.sh` 中的短 `bash -c` 只负责让 curl 显式读取 pVisor 注入的 `$HTTP_PROXY`。 -它把 allow、deny 和 direct 三次执行的 stdout、stderr 与退出码保存在工作目录中,便于 -直接观察。`test.sh` 再检查响应、预期失败和三个 Run Bundle。 - -预期结论: - -```text -Conclusion: OverlayNet controls cooperative proxy traffic; it is not a network sandbox. -``` - -## 安全边界 - -当前 OverlayNet 是显式 HTTP/HTTPS proxy,不是透明 network namespace: - -- 遵守 `HTTP_PROXY` / `HTTPS_PROXY` 的客户端会经过策略; -- 删除代理设置、配置 `NO_PROXY` 或直接创建 socket 可以绕过; -- DNS/UDP 不在当前驱动覆盖范围内; -- Run Bundle 的 `network_non_bypassable` 因此为 `false`。 - -需要不可绕过的网络隔离时,应使用 container/KVM 网络边界,而不是把 cooperative proxy -的 allowlist 或 deny-all 当作安全沙箱。 - -## Links - -- [pVisor examples](../README.md) -- [Network control](../../../docs/src/pvisor/guides/network.zh.md) -- [OverlayNet architecture](../../../docs/src/pvisor/design/overlaynet.zh.md) diff --git a/examples/pvisor/03-network-isolation/run.sh b/examples/pvisor/03-network-isolation/run.sh deleted file mode 100755 index 96b67edbf..000000000 --- a/examples/pvisor/03-network-isolation/run.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -example_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -source "$example_dir/../common.sh" -pvisor_example_init "$example_dir" network-isolation -command -v curl >/dev/null - -server_port="$(pvisor_free_ports 1)" -server_url="http://127.0.0.1:$server_port" - -pvisor_example_reset -mkdir -p "$work_dir/server" -printf 'allowed\n' >"$work_dir/server/index.html" - -python3 -m http.server "$server_port" --bind 127.0.0.1 \ - --directory "$work_dir/server" >"$work_dir/server.log" 2>&1 & -server_pid=$! -trap 'kill "$server_pid" 2>/dev/null || true; wait "$server_pid" 2>/dev/null || true' EXIT -pvisor_wait_http "$server_url" - -echo '1. Allowlist permits the declared destination' -set +e -"$pvisor_bin" run --overlaynet-allow "127.0.0.1:$server_port" -- \ - bash -ceu ' - exec curl --fail --silent --show-error \ - --proxy "$HTTP_PROXY" --noproxy "" "$1" - ' bash "$server_url" \ - >"$work_dir/allowed.stdout" 2>"$work_dir/allowed.stderr" -allowed_status=$? -set -e -printf '%s\n' "$allowed_status" >"$work_dir/allowed.status" -printf 'exit status: %s\n' "$allowed_status" -cat "$work_dir/allowed.stdout" -cat "$work_dir/allowed.stderr" - -echo -echo '2. Deny-all rejects traffic that uses the injected proxy' -set +e -"$pvisor_bin" run --overlaynet-deny-all -- \ - bash -ceu ' - exec curl --fail-with-body --silent --show-error \ - --proxy "$HTTP_PROXY" --noproxy "" "$1" - ' bash "$server_url" \ - >"$work_dir/denied.stdout" 2>"$work_dir/denied.stderr" -denied_status=$? -set -e -printf '%s\n' "$denied_status" >"$work_dir/denied.status" -printf 'exit status: %s\n' "$denied_status" -cat "$work_dir/denied.stdout" -cat "$work_dir/denied.stderr" - -echo -echo '3. A direct socket can bypass the cooperative proxy' -set +e -"$pvisor_bin" run --overlaynet-deny-all -- \ - bash -ceu ' - exec curl --fail --silent --show-error --noproxy "*" "$1" - ' bash "$server_url" \ - >"$work_dir/direct.stdout" 2>"$work_dir/direct.stderr" -direct_status=$? -set -e -printf '%s\n' "$direct_status" >"$work_dir/direct.status" -printf 'exit status: %s\n' "$direct_status" -cat "$work_dir/direct.stdout" -cat "$work_dir/direct.stderr" - -echo -echo 'Conclusion: OverlayNet controls cooperative proxy traffic; it is not a network sandbox.' diff --git a/examples/pvisor/03-network-isolation/test.sh b/examples/pvisor/03-network-isolation/test.sh deleted file mode 100755 index 17c45f664..000000000 --- a/examples/pvisor/03-network-isolation/test.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -example_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -work_root="${WORK_ROOT:-$example_dir/.work}" -work_dir="$work_root/network-isolation" -export WORK_ROOT="$work_root" - -command -v jq >/dev/null -bash "$example_dir/run.sh" - -test "$(cat "$work_dir/allowed.status")" = 0 -test "$(tr -d '\r\n' <"$work_dir/allowed.stdout")" = allowed -test "$(cat "$work_dir/denied.status")" != 0 -grep -q '(no-network)' "$work_dir/denied.stdout" "$work_dir/denied.stderr" -test "$(cat "$work_dir/direct.status")" = 0 -test "$(tr -d '\r\n' <"$work_dir/direct.stdout")" = allowed - -bundle_count=0 -completed_count=0 -failed_count=0 -while IFS= read -r bundle; do - bundle_count=$((bundle_count + 1)) - case "$(jq -r '.run.state' "$bundle")" in - completed) completed_count=$((completed_count + 1)) ;; - failed) failed_count=$((failed_count + 1)) ;; - *) exit 1 ;; - esac -done < <(find "$work_dir/runs" -name run-bundle.json -type f -print) -test "$bundle_count" = 3 -test "$completed_count" = 2 -test "$failed_count" = 1 - -echo 'RESULT example=network-isolation allowed=true denied=true direct_bypass=true' diff --git a/examples/pvisor/04-gateway-llm-control/README.md b/examples/pvisor/04-gateway-llm-control/README.md deleted file mode 100644 index 1415ef824..000000000 --- a/examples/pvisor/04-gateway-llm-control/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# 1.4 Gateway 捕获与管控 LLM 交互 - -**问题:Agent 使用注入的 OpenAI endpoint 时,Gateway 能否选择 upstream 并记录完整的 request/response 对?可复现结论:2 次 upstream POST、2 次 Gateway sink request、4 个 AgenticMD blocks、0 次失败。** - -`run.sh` 启动无需 API key 的 Mock LLM,再通过配置好的 pVisor Gateway 执行两轮 Agent, -并展示 Mock server 日志、Run Bundle 和生成的 AgenticMD。`test.sh` 检查这些产物的指标。 -`configs/` 另外保留真实 DeepSeek、多厂商和 allowlist 配置模板。 - -## Run - -```bash -./run.sh -./test.sh # 执行同一场景并验证预期结果 -``` - -预期:2 次 upstream POST、2 次 Gateway sink request、4 个 AgenticMD blocks、0 次失败。 - -## Links - -- [pVisor examples](../README.md) -- [Capture trajectories](../../../docs/src/pvisor/guides/capture.zh.md) -- [Gateway architecture](../../../docs/src/pvisor/design/gateway.zh.md) diff --git a/examples/pvisor/04-gateway-llm-control/agent.py b/examples/pvisor/04-gateway-llm-control/agent.py deleted file mode 100755 index 515f4ae55..000000000 --- a/examples/pvisor/04-gateway-llm-control/agent.py +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env python3 -import json -import os -import urllib.request - -from dialogue_fixture import TURNS - -base_url = os.environ["OPENAI_BASE_URL"].rstrip("/") -messages = [] -for user_text in TURNS: - messages.append({"role": "user", "content": user_text}) - request = urllib.request.Request( - f"{base_url}/chat/completions", - data=json.dumps({"model": "mock-model", "messages": messages}).encode(), - headers={"Content-Type": "application/json"}, - ) - with urllib.request.urlopen(request, timeout=10) as response: - assistant_text = json.load(response)["choices"][0]["message"]["content"] - messages.append({"role": "assistant", "content": assistant_text}) diff --git a/examples/pvisor/04-gateway-llm-control/configs/allowlist.toml b/examples/pvisor/04-gateway-llm-control/configs/allowlist.toml deleted file mode 100644 index f5e61d9b3..000000000 --- a/examples/pvisor/04-gateway-llm-control/configs/allowlist.toml +++ /dev/null @@ -1,19 +0,0 @@ -# Standalone Gateway with an explicit outbound allowlist. -listen = "127.0.0.1:19081" -admin_listen = "127.0.0.1:9876" -agent_id = "deepseek-allowlist" - -[network] -mode = "allowlist" -allowed_hosts = ["pypi.org", "files.pythonhosted.org", "github.com", "api.github.com"] - -[[models]] -name = "deepseek-v4-flash" -provider = "openai" -upstream = "https://api.deepseek.com/v1" -upstream_anthropic = "https://api.deepseek.com/anthropic/v1" -api_key_env = "DEEPSEEK_API_KEY" - -[[models]] -name = "*" -forward = "deepseek-v4-flash" diff --git a/examples/pvisor/04-gateway-llm-control/configs/deepseek.toml b/examples/pvisor/04-gateway-llm-control/configs/deepseek.toml deleted file mode 100644 index 01241a2fc..000000000 --- a/examples/pvisor/04-gateway-llm-control/configs/deepseek.toml +++ /dev/null @@ -1,15 +0,0 @@ -# Gateway route configuration used by the pVisor capture driver. -listen = "127.0.0.1:19081" -admin_listen = "127.0.0.1:9876" -agent_id = "deepseek-proxy" - -[[models]] -name = "deepseek-v4-flash" -provider = "openai" -upstream = "https://api.deepseek.com/v1" -upstream_anthropic = "https://api.deepseek.com/anthropic/v1" -api_key_env = "DEEPSEEK_API_KEY" - -[[models]] -name = "*" -forward = "deepseek-v4-flash" diff --git a/examples/pvisor/04-gateway-llm-control/configs/multi-provider.toml b/examples/pvisor/04-gateway-llm-control/configs/multi-provider.toml deleted file mode 100644 index 23acb85e1..000000000 --- a/examples/pvisor/04-gateway-llm-control/configs/multi-provider.toml +++ /dev/null @@ -1,28 +0,0 @@ -# Standalone Gateway routes selected by model prefix. -listen = "127.0.0.1:8080" -admin_listen = "127.0.0.1:9876" -agent_id = "multi-provider" - -[[models]] -name = "deepseek*" -provider = "openai" -upstream = "https://api.deepseek.com/v1" -api_key_env = "DEEPSEEK_API_KEY" - -[[models]] -name = "claude*" -provider = "anthropic" -upstream = "https://api.anthropic.com/v1" -api_key_env = "ANTHROPIC_API_KEY" - -[[models]] -name = "gemini*" -provider = "gemini" -upstream = "https://generativelanguage.googleapis.com/v1beta" -api_key_env = "GEMINI_API_KEY" - -[[models]] -name = "gpt*" -provider = "openai" -upstream = "https://api.openai.com/v1" -api_key_env = "OPENAI_API_KEY" diff --git a/examples/pvisor/04-gateway-llm-control/dialogue_fixture.py b/examples/pvisor/04-gateway-llm-control/dialogue_fixture.py deleted file mode 100644 index 2d74f0868..000000000 --- a/examples/pvisor/04-gateway-llm-control/dialogue_fixture.py +++ /dev/null @@ -1,11 +0,0 @@ -TURNS = [ - "What does pVisor own?", - "Where is the captured trajectory?", -] - -REPLIES = [ - "pVisor owns one Run and its Attempt lifecycle.", - "Gateway writes the visible dialogue into the Run workspace.", -] - -REPLY_BY_USER = dict(zip(TURNS, REPLIES, strict=True)) diff --git a/examples/pvisor/04-gateway-llm-control/mock_llm.py b/examples/pvisor/04-gateway-llm-control/mock_llm.py deleted file mode 100755 index c0ecf09ed..000000000 --- a/examples/pvisor/04-gateway-llm-control/mock_llm.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python3 -import json -import os -from http.server import BaseHTTPRequestHandler, HTTPServer - -from dialogue_fixture import REPLY_BY_USER - - -class Handler(BaseHTTPRequestHandler): - def do_POST(self) -> None: - size = int(self.headers.get("Content-Length", "0")) - request = json.loads(self.rfile.read(size)) - user_text = next( - message["content"] - for message in reversed(request["messages"]) - if message["role"] == "user" - ) - content = REPLY_BY_USER[user_text] - body = json.dumps( - { - "id": "mock-call", - "model": "mock-model", - "choices": [{"message": {"role": "assistant", "content": content}}], - } - ).encode() - self.send_response(200) - self.send_header("Content-Type", "application/json") - self.send_header("Content-Length", str(len(body))) - self.end_headers() - self.wfile.write(body) - - def log_message(self, message: str, *args: object) -> None: - print(message % args, flush=True) - - -port = int(os.environ.get("MOCK_LLM_PORT", "19080")) -HTTPServer(("127.0.0.1", port), Handler).serve_forever() diff --git a/examples/pvisor/04-gateway-llm-control/run.sh b/examples/pvisor/04-gateway-llm-control/run.sh deleted file mode 100755 index 4a5a5fee5..000000000 --- a/examples/pvisor/04-gateway-llm-control/run.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -example_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -source "$example_dir/../common.sh" -pvisor_example_init "$example_dir" gateway-llm-control -command -v jq >/dev/null - -# Start a local OpenAI-compatible endpoint for the example agent. -pvisor_example_reset -ports="$(pvisor_free_ports 3)" -read -r mock_port proxy_port admin_port <<<"$ports" -sed \ - -e "s|^\[run\]|[run]\ncommand = [\"${PYTHON_BIN:-python3}\", \"$example_dir/agent.py\"]|" \ - -e "s/127.0.0.1:19080/127.0.0.1:$mock_port/" \ - -e "s/127.0.0.1:19081/127.0.0.1:$proxy_port/" \ - -e "s/127.0.0.1:19082/127.0.0.1:$admin_port/" \ - run.toml >"$work_dir/run.toml" - -export PERSISTING_RUN_HOME="$work_dir/runs" -PYTHONDONTWRITEBYTECODE=1 MOCK_LLM_PORT="$mock_port" \ - python3 mock_llm.py >"$work_dir/mock.log" 2>&1 & -mock_pid=$! -trap 'kill "$mock_pid" 2>/dev/null || true; wait "$mock_pid" 2>/dev/null || true' EXIT -pvisor_wait_tcp "$mock_port" - -# Run the agent through pVisor's configured Gateway. -"$pvisor_bin" run --spec "$work_dir/run.toml" --stdio capture -run_dir="$(find "$PERSISTING_RUN_HOME" -mindepth 1 -maxdepth 1 -type d -name 'run-*' -print -quit)" -test -n "$run_dir" - -# Print the upstream requests, Gateway counters, and captured conversation. -echo 'Mock LLM requests:' -cat "$work_dir/mock.log" - -echo 'Gateway counters:' -jq '.network.intercepted' "$run_dir/run-bundle.json" - -echo 'Generated AgenticMD:' -cat "$run_dir"/gateway-example/*/*.md diff --git a/examples/pvisor/04-gateway-llm-control/run.toml b/examples/pvisor/04-gateway-llm-control/run.toml deleted file mode 100644 index 7c4e7267b..000000000 --- a/examples/pvisor/04-gateway-llm-control/run.toml +++ /dev/null @@ -1,17 +0,0 @@ -[run] -agent = "gateway-example" - -[overlaynet] -mode = "proxy" -listen = "127.0.0.1:19081" -policy = "public" - -[gateway] -mode = "capture" -admin_listen = "127.0.0.1:19082" -level = "dialogue" -stream_markdown = true - -[[gateway.routes]] -name = "*" -upstream = "http://127.0.0.1:19080/v1" diff --git a/examples/pvisor/04-gateway-llm-control/test.sh b/examples/pvisor/04-gateway-llm-control/test.sh deleted file mode 100755 index 6874dbce3..000000000 --- a/examples/pvisor/04-gateway-llm-control/test.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -example_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -work_root="${WORK_ROOT:-$example_dir/.work}" -work_dir="$work_root/gateway-llm-control" -export WORK_ROOT="$work_root" - -bash "$example_dir/run.sh" - -run_dir="$(find "$work_dir/runs" -mindepth 1 -maxdepth 1 -type d -name 'run-*' -print -quit)" -test -n "$run_dir" -upstream_posts="$(grep -c 'POST /v1/chat/completions' "$work_dir/mock.log")" -agentic_blocks="$(grep -E -h -c ' - -Unannotated cases default to ``expect=success`` with no extra prerequisites. - -A case may declare a second bash fence, introduced by ````, -that runs after the command fence and turns the prose expectation into a real -check. Assertion scripts get the helper vocabulary documented in the case list -(``bundle_expect``, ``bundle_contains``, ``stdout_has``, ...) plus -``$PVISOR_CASE_STDOUT`` holding the command fence's combined output. - -The runner executes every selected case in a private temporary workspace and -can emit a Markdown report suitable for CI artifacts. -""" - -from __future__ import annotations - -import argparse -import dataclasses -import datetime as dt -import os -from pathlib import Path -import re -import shlex -import shutil -import socket -import subprocess -import sys -import tempfile -import time - -CASE_RE = re.compile(r"^- \[ \] \*\*([A-J][0-9]{2}):(.+?)\*\*\s*$") -META_RE = re.compile(r"^\s*\s*$") -ASSERT_RE = re.compile(r"^\s*\s*$") -VALID_EXPECTATIONS = {"success", "nonzero", "any"} -VALID_METADATA_KEYS = {"expect", "requires"} - -HELPER_FILENAME = "pvisor-case-helper.py" - -# Assertion vocabulary injected ahead of every `` block. -# Keep this list in sync with the "断言助手" table in the case list. -ASSERT_PREAMBLE = """ -_pvisor_helper() { "$PVISOR_CASE_PYTHON" "$PVISOR_CASE_HELPER" "$@"; } -bundle_path() { _pvisor_helper bundle path "$@"; } -bundle_get() { _pvisor_helper bundle get "$@"; } -bundle_expect() { _pvisor_helper bundle expect "$@"; } -bundle_contains() { _pvisor_helper bundle contains "$@"; } -record_get() { _pvisor_helper record get "$@"; } -record_expect() { _pvisor_helper record expect "$@"; } -record_contains() { _pvisor_helper record contains "$@"; } -stdout_has() { - if ! grep -Fq -- "$1" "$PVISOR_CASE_STDOUT"; then - printf 'assert: command output does not contain %s\\n' "$1" >&2 - return 1 - fi -} -stdout_matches() { - if ! grep -Eq -- "$1" "$PVISOR_CASE_STDOUT"; then - printf 'assert: command output does not match %s\\n' "$1" >&2 - return 1 - fi -} -""" - -# Standalone helper so assertion scripts can inspect the Run Bundle without -# embedding heredocs in the Markdown document. -HELPER_SOURCE = '''#!/usr/bin/env python3 -"""Run Bundle accessors for the pVisor Markdown case runner.""" - -from __future__ import annotations - -import json -import os -from pathlib import Path -import sys - - -FAMILIES = {"bundle": "run-bundle.json", "record": "run.json"} - - -def newest(root: Path, filename: str) -> Path: - candidates = [path for path in root.rglob(filename) if path.is_file()] - if not candidates: - raise SystemExit(f"assert: no {filename} under {root}") - return max(candidates, key=lambda path: path.stat().st_mtime) - - -def lookup(document: object, dotted: str) -> object: - cursor = document - for segment in dotted.split("."): - if isinstance(cursor, list): - try: - cursor = cursor[int(segment)] - except (ValueError, IndexError) as error: - raise SystemExit(f"assert: {dotted}: bad list index {segment!r}") from error - elif isinstance(cursor, dict): - if segment not in cursor: - raise SystemExit(f"assert: {dotted}: missing key {segment!r}") - cursor = cursor[segment] - else: - raise SystemExit(f"assert: {dotted}: {segment!r} has no container to index") - return cursor - - -def render(value: object) -> str: - if value is None: - return "null" - if isinstance(value, bool): - return "true" if value else "false" - if isinstance(value, str): - return value - if isinstance(value, float) and value.is_integer(): - return str(int(value)) - if isinstance(value, (int, float)): - return str(value) - return json.dumps(value, ensure_ascii=False, sort_keys=True) - - -def main(argv: list[str]) -> int: - if len(argv) < 2 or argv[0] not in FAMILIES: - raise SystemExit("assert: usage: helper ...") - filename = FAMILIES[argv[0]] - command, arguments = argv[1], argv[2:] - default_root = os.environ.get("PVISOR_CASE_ROOT", ".") - - def artifact(index: int) -> Path: - root = Path(arguments[index] if len(arguments) > index else default_root) - return newest(root, filename) - - if command == "path": - print(artifact(0)) - return 0 - - if command == "get": - if not arguments: - raise SystemExit("assert: usage: helper get [ROOT]") - document = json.loads(artifact(1).read_text(encoding="utf-8")) - print(render(lookup(document, arguments[0]))) - return 0 - - if command == "expect": - if len(arguments) < 2: - raise SystemExit("assert: usage: helper expect [ROOT]") - source = artifact(2) - document = json.loads(source.read_text(encoding="utf-8")) - actual = render(lookup(document, arguments[0])) - if actual != arguments[1]: - raise SystemExit( - f"assert: {arguments[0]} is {actual!r}, expected {arguments[1]!r} ({source})" - ) - return 0 - - if command == "contains": - if len(arguments) < 2: - raise SystemExit("assert: usage: helper contains [ROOT]") - source = artifact(2) - document = json.loads(source.read_text(encoding="utf-8")) - haystack = render(lookup(document, arguments[0])) - if arguments[1] not in haystack: - raise SystemExit( - f"assert: {arguments[0]} does not contain {arguments[1]!r}; " - f"value is {haystack!r} ({source})" - ) - return 0 - - raise SystemExit(f"assert: unknown helper command {command!r}") - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) -''' - - -@dataclasses.dataclass(frozen=True) -class Case: - case_id: str - title: str - code: str - assertion: str - expect: str - requires: tuple[str, ...] - - -@dataclasses.dataclass(frozen=True) -class Result: - case: Case - status: str - duration: float - returncode: int | None - output: str - assert_output: str = "" - reason: str = "" - - -def parse_metadata(text: str, case_id: str) -> tuple[str, tuple[str, ...]]: - values: dict[str, str] = {} - for item in shlex.split(text): - if "=" not in item: - raise ValueError(f"{case_id}: invalid metadata item {item!r}") - key, value = item.split("=", 1) - values[key] = value - unknown = set(values) - VALID_METADATA_KEYS - if unknown: - raise ValueError(f"{case_id}: unknown metadata keys: {sorted(unknown)}") - expect = values.get("expect", "success") - if expect not in VALID_EXPECTATIONS: - raise ValueError(f"{case_id}: invalid expectation {expect!r}") - requires = tuple(filter(None, values.get("requires", "").split(","))) - return expect, requires - - -def read_fence(lines: list[str], index: int, case_id: str) -> tuple[list[str], int]: - """Collect a fenced block body, returning it with the index past the fence.""" - index += 1 - body: list[str] = [] - while index < len(lines) and lines[index].strip() != "```": - body.append(lines[index]) - index += 1 - if index == len(lines): - raise ValueError(f"{case_id}: unterminated bash fence") - return body, index + 1 - - -def parse_cases(document: Path) -> list[Case]: - lines = document.read_text(encoding="utf-8").splitlines() - cases: list[Case] = [] - index = 0 - while index < len(lines): - match = CASE_RE.match(lines[index]) - if not match: - index += 1 - continue - case_id, title = match.groups() - index += 1 - metadata = "" - code: list[str] | None = None - assertion: list[str] | None = None - next_is_assertion = False - while index < len(lines) and not CASE_RE.match(lines[index]): - meta = META_RE.match(lines[index]) - if meta: - metadata = meta.group(1) - if ASSERT_RE.match(lines[index]): - next_is_assertion = True - if lines[index].strip() == "```bash": - body, index = read_fence(lines, index, case_id) - if next_is_assertion: - if assertion is not None: - raise ValueError(f"{case_id}: more than one assertion fence") - assertion = body - next_is_assertion = False - elif code is None: - code = body - continue - index += 1 - if code is None: - raise ValueError(f"{case_id}: no bash fence") - expect, requires = parse_metadata(metadata, case_id) - cases.append( - Case( - case_id, - title, - "\n".join(code).strip(), - "\n".join(assertion or []).strip(), - expect, - requires, - ) - ) - duplicate_ids = sorted( - case_id - for case_id in {case.case_id for case in cases} - if sum(case.case_id == case_id for case in cases) != 1 - ) - if duplicate_ids: - raise ValueError(f"duplicate case ids: {duplicate_ids}") - return cases - - -def executable(value: str | None) -> str | None: - if not value: - return None - candidate = Path(value).expanduser() - if candidate.parent != Path(".") or candidate.is_absolute(): - return str(candidate.resolve()) if candidate.is_file() else None - return shutil.which(value) - - -def resolve_pvisor(argument: str | None, repository: Path) -> Path: - candidates = [argument, os.environ.get("PVISOR_BIN")] - candidates.extend( - str(repository / relative) - for relative in ("target/release/pvisor", "target/debug/pvisor") - ) - candidates.append("pvisor") - for candidate in candidates: - resolved = executable(candidate) - if resolved: - return Path(resolved) - raise FileNotFoundError( - "pvisor not found; build it or pass --pvisor /absolute/path/to/pvisor" - ) - - -def rootless_available() -> bool: - unshare = shutil.which("unshare") - if sys.platform != "linux" or not unshare: - return False - result = subprocess.run( - [unshare, "--user", "--mount", "--pid", "--fork", "true"], - stdin=subprocess.DEVNULL, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - check=False, - ) - return result.returncode == 0 - - -def container_runtime() -> str | None: - configured = os.environ.get("PVISOR_CASE_CONTAINER_RUNTIME") - if configured: - return executable(configured) - return executable("crun") or executable("runc") - - -def requirement_reason(requirement: str) -> str | None: - """Return None when the prerequisite holds, else a human-readable reason. - - ``--run-unavailable`` intentionally bypasses these checks for diagnostics. - """ - environment = os.environ - if requirement == "linux" and sys.platform != "linux": - return "requires Linux" - if requirement == "kvm" and not Path("/dev/kvm").exists(): - return "requires /dev/kvm" - if requirement == "rootless" and not rootless_available(): - return "requires Linux user/mount namespaces" - if requirement == "curl" and shutil.which("curl") is None: - return "requires curl" - if requirement == "python3" and not executable(sys.executable): - return "requires Python 3" - if requirement == "rootfs": - rootfs = environment.get("PVISOR_CASE_ROOTFS") - if rootfs and not Path(rootfs).is_dir(): - return f"rootfs is not a directory: {rootfs}" - if sys.platform != "linux" and not rootfs: - return "requires a Linux rootfs via PVISOR_CASE_ROOTFS" - if requirement == "firmware": - firmware = environment.get("PVISOR_CASE_FIRMWARE") - cache = Path.home() / ".cache/persisting/pvisor/firmware/5.5.0/linux-x86_64" - if sys.platform == "darwin": - cache = Path.home() / "Library/Caches/persisting/pvisor/firmware/5.5.0/macos-aarch64" - if not (firmware and Path(firmware).is_dir()) and not cache.is_dir(): - return "requires libkrunfw (set PVISOR_CASE_FIRMWARE)" - if requirement in {"container", "container-runtime"} and not container_runtime(): - return "requires crun or runc" - if requirement == "agent" and not environment.get("PVISOR_CASE_AGENT"): - return "requires PVISOR_CASE_AGENT" - if requirement == "lance" and environment.get("PVISOR_CASE_LANCE") != "1": - return "requires PVISOR_CASE_LANCE=1" - if requirement in { - "curl", - "firmware", - "image", - "kvm", - "linux", - "python3", - "rootfs", - "rootless", - "agent", - "lance", - "container", - "container-runtime", - }: - return None - return f"unknown requirement {requirement!r}" - - -def free_loopback_port() -> int: - with socket.socket() as listener: - listener.bind(("127.0.0.1", 0)) - return int(listener.getsockname()[1]) - - -def prepare_workspace(workspace: Path) -> None: - workspace.mkdir(parents=True) - true_program = "/usr/bin/true" if sys.platform == "darwin" else "/bin/true" - (workspace / "pvisor.toml").write_text( - f'[run]\ncommand = ["{true_program}"]\n', encoding="utf-8" - ) - (workspace / "spec-without-extension").write_text( - f'[run]\ncommand = ["{true_program}"]\n', encoding="utf-8" - ) - (workspace / "run-spec.json").write_text( - f'{{"run_id":"case-i02","agent":{{"name":"case-i02"}},' - f'"invocation":{{"kind":"process","program":"{true_program}"}}}}\n', - encoding="utf-8", - ) - - -def render_command(code: str, case_root: Path, pvisor: Path, ports: dict[str, str]) -> str: - """Substitute the document's placeholder paths with real fixtures. - - Placeholders keep the document readable for a human running a case by - hand. Rendering defaults stay permissive so `--run-unavailable` still has - something to execute; the prerequisite probes above decide whether a case - is meaningful in the first place. - """ - environment = os.environ - true_program = "/usr/bin/true" if sys.platform == "darwin" else "/bin/true" - default_rootfs = "/" if sys.platform == "linux" else "/path/to/rootfs" - default_firmware = "/path/to/libkrunfw" - firmware_cache = Path.home() / ".cache/persisting/pvisor/firmware/5.5.0" - if (firmware_cache / "linux-x86_64").is_dir(): - default_firmware = str(firmware_cache / "linux-x86_64") - replacements = { - "/tmp/pvisor-cases": str(case_root), - "./target/release/pvisor": str(pvisor), - "/bin/true": true_program, - "/path/to/rootfs": environment.get("PVISOR_CASE_ROOTFS", default_rootfs), - "/path/to/image": environment.get("PVISOR_CASE_IMAGE", "ubuntu:latest"), - "/path/to/libkrunfw": environment.get("PVISOR_CASE_FIRMWARE", default_firmware), - "/usr/local/bin/agent": environment.get("PVISOR_CASE_AGENT", true_program), - "alpine:latest": environment.get("PVISOR_CASE_CONTAINER_IMAGE", "ubuntu:latest"), - **ports, - } - for source, target in replacements.items(): - code = code.replace(source, target) - return code - - -def expected(expectation: str, returncode: int) -> bool: - return ( - expectation == "any" - or (expectation == "success" and returncode == 0) - or (expectation == "nonzero" and returncode != 0) - ) - - -def run_script(script: str, workspace: Path, environment: dict[str, str], timeout: float): - return subprocess.run( - ["bash", "-euo", "pipefail", "-c", script], - cwd=workspace, - env=environment, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - timeout=timeout, - check=False, - ) - - -def execute_case( - case: Case, - root: Path, - pvisor: Path, - timeout: float, - run_unavailable: bool = False, -) -> Result: - missing = [reason for item in case.requires if (reason := requirement_reason(item))] - if missing and not run_unavailable: - return Result(case, "SKIP", 0.0, None, "", reason="; ".join(missing)) - case_root = root / case.case_id.lower() - workspace = case_root / "workspace" - prepare_workspace(workspace) - stdout_log = case_root / "command.log" - # Documented listen addresses must map to the same free port in both the - # command and the assertion so an assertion can grep for the real address. - ports = { - "127.0.0.1:18080": f"127.0.0.1:{free_loopback_port()}", - "127.0.0.1:19090": f"127.0.0.1:{free_loopback_port()}", - } - command = render_command(case.code, case_root, pvisor, ports) - environment = os.environ.copy() - environment.update( - { - "PATH": f"{pvisor.parent}{os.pathsep}{environment.get('PATH', '')}", - "PERSISTING_RUN_HOME": str(case_root / "records"), - "PVISOR_CASE_ROOT": str(case_root), - "PVISOR_CASE_WORKSPACE": str(workspace), - "PVISOR_CASE_RECORDS": str(case_root / "records"), - "PVISOR_CASE_STDOUT": str(stdout_log), - "PVISOR_CASE_HELPER": str(root / HELPER_FILENAME), - "PVISOR_CASE_PYTHON": sys.executable or "python3", - } - ) - started = time.monotonic() - try: - process = run_script(command, workspace, environment, timeout) - except subprocess.TimeoutExpired as error: - output = error.stdout or "" - if isinstance(output, bytes): - output = output.decode(errors="replace") - return Result(case, "FAIL", time.monotonic() - started, None, output, reason="runner timeout") - stdout_log.write_text(process.stdout, encoding="utf-8") - if not expected(case.expect, process.returncode): - return Result( - case, - "FAIL", - time.monotonic() - started, - process.returncode, - process.stdout, - reason=f"expected {case.expect}, exit={process.returncode}", - ) - if not case.assertion: - return Result(case, "PASS", time.monotonic() - started, process.returncode, process.stdout) - environment["PVISOR_CASE_EXIT"] = str(process.returncode) - # The assertion shares the command's placeholder rendering so a documented - # listen address or fixture path resolves to the same value in both halves. - assertion = render_command(case.assertion, case_root, pvisor, ports) - try: - checks = run_script(ASSERT_PREAMBLE + "\n" + assertion, workspace, environment, timeout) - except subprocess.TimeoutExpired as error: - output = error.stdout or "" - if isinstance(output, bytes): - output = output.decode(errors="replace") - return Result( - case, - "FAIL", - time.monotonic() - started, - process.returncode, - process.stdout, - output, - "assertion timeout", - ) - status = "PASS" if checks.returncode == 0 else "FAIL" - reason = "" if status == "PASS" else f"assertion failed, exit={checks.returncode}" - return Result( - case, - status, - time.monotonic() - started, - process.returncode, - process.stdout, - checks.stdout, - reason, - ) - - -def markdown_report(results: list[Result], pvisor: Path) -> str: - counts = {status: sum(item.status == status for item in results) for status in ("PASS", "FAIL", "SKIP")} - asserted = sum(bool(item.case.assertion) for item in results) - lines = [ - "# pVisor case execution report", - "", - f"- Generated: {dt.datetime.now(dt.timezone.utc).isoformat()}", - f"- pVisor: `{pvisor}`", - f"- Result: {counts['PASS']} PASS / {counts['FAIL']} FAIL / {counts['SKIP']} SKIP", - f"- Cases with assertions: {asserted}/{len(results)}", - "", - "| Case | Status | Exit | Assert | Time | Reason |", - "|---|---:|---:|:-:|---:|---|", - ] - for result in results: - exit_code = "-" if result.returncode is None else str(result.returncode) - has_assert = "yes" if result.case.assertion else "-" - lines.append( - f"| {result.case.case_id} | {result.status} | {exit_code} | {has_assert} | " - f"{result.duration:.2f}s | {result.reason.replace('|', '|')} |" - ) - for result in results: - if not result.output and not result.assert_output: - continue - lines.extend(["", f"## {result.case.case_id} — {result.status}", ""]) - for label, body in (("command output", result.output), ("assertion output", result.assert_output)): - if not body: - continue - lines.extend( - [ - f"
{label}", - "", - "```text", - body.rstrip(), - "```", - "", - "
", - "", - ] - ) - return "\n".join(lines) + "\n" - - -def main() -> int: - repository = Path(__file__).resolve().parents[1] - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--document", - type=Path, - default=repository / "docs/src/zh/pvisor/reference/cases.md", - ) - parser.add_argument("--pvisor", help="pvisor executable (or set PVISOR_BIN)") - parser.add_argument("--case", action="append", dest="case_ids", help="case ID; repeatable") - parser.add_argument("--list", action="store_true", help="list cases without executing") - parser.add_argument("--timeout", type=float, default=120.0, help="per-case timeout in seconds") - parser.add_argument("--report", type=Path, help="write a Markdown execution report") - parser.add_argument("--keep", action="store_true", help="retain temporary case directories") - parser.add_argument( - "--run-unavailable", - action="store_true", - help="execute cases even when declared prerequisites are missing; report real failures", - ) - parser.add_argument( - "--strict-skips", action="store_true", help="treat skipped prerequisites as failures" - ) - args = parser.parse_args() - - cases = parse_cases(args.document) - if args.case_ids: - requested = {item.upper() for value in args.case_ids for item in value.split(",")} - known = {case.case_id for case in cases} - unknown = requested - known - if unknown: - parser.error(f"unknown cases: {', '.join(sorted(unknown))}") - cases = [case for case in cases if case.case_id in requested] - if args.list: - for case in cases: - requirements = ",".join(case.requires) or "-" - marker = "assert" if case.assertion else "-" - print(f"{case.case_id}\t{case.expect}\t{marker}\t{requirements}\t{case.title}") - return 0 - - pvisor = resolve_pvisor(args.pvisor, repository) - root = Path(tempfile.mkdtemp(prefix="pvisor-cases-")) - (root / HELPER_FILENAME).write_text(HELPER_SOURCE, encoding="utf-8") - print(f"pVisor: {pvisor}") - print(f"case root: {root}") - results: list[Result] = [] - for case in cases: - result = execute_case(case, root, pvisor, args.timeout, args.run_unavailable) - results.append(result) - detail = f" ({result.reason})" if result.reason else "" - print(f"[{result.status}] {case.case_id} {case.title}{detail}") - if result.status == "FAIL": - for body in (result.output, result.assert_output): - if body: - print(body.rstrip()) - - report = markdown_report(results, pvisor) - if args.report: - args.report.parent.mkdir(parents=True, exist_ok=True) - args.report.write_text(report, encoding="utf-8") - print(f"report: {args.report}") - failed = sum(result.status == "FAIL" for result in results) - skipped = sum(result.status == "SKIP" for result in results) - asserted = sum(bool(result.case.assertion) for result in results) - print(f"summary: {len(results) - failed - skipped} PASS / {failed} FAIL / {skipped} SKIP") - print(f"assertions: {asserted}/{len(results)} cases carry an assertion block") - if args.keep: - print(f"retained: {root}") - else: - shutil.rmtree(root, ignore_errors=True) - return 1 if failed or (args.strict_skips and skipped) else 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tests/regression/README.md b/tests/regression/README.md index 2a0a822d2..f26940139 100644 --- a/tests/regression/README.md +++ b/tests/regression/README.md @@ -33,5 +33,5 @@ JSONL logs. - [`gateway-echo`](gateway-echo/README.md) - [`gateway-fuzz`](gateway-fuzz/README.md) -- [Gateway architecture](../../docs/src/pvisor/design/gateway.md) +- [Gateway architecture](../../docs/src/en/pchronicle/guides/serve-gateway.md) - [`persisting-gateway`](../../crates/persisting-gateway/README.md) diff --git a/tests/regression/gateway-echo/README.md b/tests/regression/gateway-echo/README.md index 2ec1f770c..5384e0cf1 100644 --- a/tests/regression/gateway-echo/README.md +++ b/tests/regression/gateway-echo/README.md @@ -46,5 +46,5 @@ Logs are retained automatically on failure. Set ## Links - [Regression tests](../README.md) -- [Gateway architecture](../../../docs/src/pvisor/design/gateway.md) +- [Gateway architecture](../../../docs/src/en/pchronicle/guides/serve-gateway.md) - [`persisting-gateway`](../../../crates/persisting-gateway/README.md) diff --git a/tests/regression/gateway-fuzz/README.md b/tests/regression/gateway-fuzz/README.md index c3bac4df6..3cab80e03 100644 --- a/tests/regression/gateway-fuzz/README.md +++ b/tests/regression/gateway-fuzz/README.md @@ -106,5 +106,5 @@ suite. ## Links - [Regression tests](../README.md) -- [Gateway architecture](../../../docs/src/pvisor/design/gateway.md) +- [Gateway architecture](../../../docs/src/en/pchronicle/guides/serve-gateway.md) - [`persisting-gateway`](../../../crates/persisting-gateway/README.md) diff --git a/tests/test_release_packaging.py b/tests/test_release_packaging.py index 651631b23..2946a571a 100644 --- a/tests/test_release_packaging.py +++ b/tests/test_release_packaging.py @@ -166,26 +166,12 @@ def test_release_artifacts_reject_oversized_wheel(tmp_path: Path) -> None: release_artifacts.validate_artifacts(tmp_path, version, max_bytes=1) -@pytest.mark.parametrize( - ("editable", "bundle_firmware"), - [(True, False), (False, True)], -) -def test_build_backend_options_only_skip_firmware_for_editable_builds( - editable: bool, - bundle_firmware: bool, -) -> None: - options = wheel_stage.options_from_build_backend(None, editable=editable) - - assert options.bundle_firmware is bundle_firmware - - def test_build_backend_options_accept_explicit_cargo_settings() -> None: options = wheel_stage.options_from_build_backend( { "cargo-profile": "dev", "cargo-locked": "false", "cargo-jobs": "3", - "bundle-firmware": "false", }, editable=False, ) @@ -193,10 +179,9 @@ def test_build_backend_options_accept_explicit_cargo_settings() -> None: assert options.profile == "dev" assert options.locked is False assert options.jobs == "3" - assert options.bundle_firmware is False -def test_editable_staging_does_not_resolve_firmware( +def test_staging_replaces_old_component_payloads( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, ) -> None: @@ -210,71 +195,33 @@ def test_editable_staging_does_not_resolve_firmware( monkeypatch.setattr(wheel_stage, "WHEEL_DATA", tmp_path / "wheel-data") monkeypatch.setattr(wheel_stage, "_build_web_assets", lambda: None) monkeypatch.setattr(wheel_stage, "_build", lambda _options: artifacts) - monkeypatch.setattr(wheel_stage, "_is_macos", lambda _options: False) - - def unexpected_firmware(_options): - raise AssertionError("editable staging must not resolve wheel firmware") - - monkeypatch.setattr(wheel_stage, "_firmware_source", unexpected_firmware) - - scripts = wheel_stage.stage_wheel_binaries(wheel_stage.BuildOptions(bundle_firmware=False)) - - assert {path.name for path in scripts.iterdir()} == set(wheel_stage.EXPECTED_BINARIES) - - -def test_release_staging_resolves_firmware_before_build( - monkeypatch: pytest.MonkeyPatch, -) -> None: - events: list[str] = [] - - def missing_firmware(_options): - events.append("firmware") - raise RuntimeError("missing firmware") - - def unexpected_build(_options): - events.append("build") - raise AssertionError("Cargo must not run before firmware is ready") - monkeypatch.setattr(wheel_stage, "_firmware_source", missing_firmware) - monkeypatch.setattr(wheel_stage, "_build", unexpected_build) + old_scripts = tmp_path / "wheel-data" / "scripts" + old_scripts.mkdir(parents=True) + for name in ("pvisor", "ppilot", "libkrunfw.5.dylib"): + (old_scripts / name).write_bytes(b"old payload") - with pytest.raises(RuntimeError, match="missing firmware"): - wheel_stage.stage_wheel_binaries(wheel_stage.BuildOptions()) + scripts = wheel_stage.stage_wheel_binaries(wheel_stage.BuildOptions()) - assert events == ["firmware"] + assert {path.name for path in scripts.iterdir()} == {"pchronicle"} + assert (scripts / "pchronicle").stat().st_mode & 0o111 -def test_firmware_source_prefers_explicit_path( - monkeypatch: pytest.MonkeyPatch, - tmp_path: Path, -) -> None: - firmware = tmp_path / "libkrunfw.5.dylib" - firmware.write_bytes(b"firmware") - monkeypatch.setenv("PERSISTING_LIBKRUNFW_PATH", str(tmp_path)) - - source, name = wheel_stage._firmware_source( - wheel_stage.BuildOptions(target="aarch64-apple-darwin") - ) - - assert source == firmware.resolve() - assert name == firmware.name - +def test_wheel_contents_require_only_pchronicle(tmp_path: Path) -> None: + wheel = tmp_path / "persisting-1.2.3-py3-none-macosx_11_0_arm64.whl" + _write_wheel(wheel, "1.2.3") + with zipfile.ZipFile(wheel, "a") as archive: + script = zipfile.ZipInfo("persisting-1.2.3.data/scripts/pchronicle") + script.external_attr = 0o100755 << 16 + archive.writestr(script, b"pchronicle") + version, scripts = wheel_verify._wheel_contents(wheel) + assert version == "1.2.3" + assert set(scripts) == {"pchronicle"} -def test_firmware_source_fetches_when_path_is_not_configured( - monkeypatch: pytest.MonkeyPatch, - tmp_path: Path, -) -> None: - firmware = tmp_path / "libkrunfw.5.dylib" - firmware.write_bytes(b"firmware") - monkeypatch.delenv("PERSISTING_LIBKRUNFW_PATH", raising=False) - monkeypatch.setattr(wheel_stage, "_fetch_firmware", lambda _options, _name: firmware) - - source, name = wheel_stage._firmware_source( - wheel_stage.BuildOptions(target="aarch64-apple-darwin") - ) - - assert source == firmware - assert name == firmware.name + with zipfile.ZipFile(wheel, "a") as archive: + archive.writestr("persisting-1.2.3.data/scripts/pvisor", b"retired component") + with pytest.raises(RuntimeError, match="unexpected wheel scripts"): + wheel_verify._wheel_contents(wheel) def test_cargo_command_uses_plain_build_by_default() -> None: @@ -282,6 +229,9 @@ def test_cargo_command_uses_plain_build_by_default() -> None: assert command[:2] == ["cargo", "build"] assert "--target" not in command + assert [command[i + 1] for i, arg in enumerate(command) if arg == "-p"] == [ + "persisting-pchronicle-cli" + ] def test_manylinux_glibc_requirement_accepts_2_28() -> None: diff --git a/vendor/fuser/.cargo-ok b/vendor/fuser/.cargo-ok deleted file mode 100644 index 5f8b79583..000000000 --- a/vendor/fuser/.cargo-ok +++ /dev/null @@ -1 +0,0 @@ -{"v":1} \ No newline at end of file diff --git a/vendor/fuser/.cargo_vcs_info.json b/vendor/fuser/.cargo_vcs_info.json deleted file mode 100644 index 8264fa6e7..000000000 --- a/vendor/fuser/.cargo_vcs_info.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "git": { - "sha1": "168705a63a0c47b6cd330291e4a0dc1d14317cd0" - }, - "path_in_vcs": "" -} \ No newline at end of file diff --git a/vendor/fuser/.cirrus.yml b/vendor/fuser/.cirrus.yml deleted file mode 100644 index cc315e53b..000000000 --- a/vendor/fuser/.cirrus.yml +++ /dev/null @@ -1,14 +0,0 @@ -freebsd_instance: - image_family: freebsd-14-0 - -task: - name: FreeBSD - setup_script: - - pkg install -y pkgconf fusefs-libs fusefs-libs3 rust - build_script: - - cargo build --all --all-targets - doc_script: - - cargo doc --all --no-deps --features=abi-7-21 - test_script: - - cargo test --all --all-targets -- --skip=mnt::test::mount_unmount - - cargo test --all --all-targets --features=abi-7-21 -- --skip=mnt::test::mount_unmount diff --git a/vendor/fuser/.dockerignore b/vendor/fuser/.dockerignore deleted file mode 100644 index 49b03a586..000000000 --- a/vendor/fuser/.dockerignore +++ /dev/null @@ -1,10 +0,0 @@ -* -!src/ -!examples/ -!./Cargo.* -!./rust-toolchain -!./build.rs -!./pjdfs.sh -!./xfstests.sh -!./mount_tests.sh -!./simplefs_tests.sh diff --git a/vendor/fuser/.github/workflows/ci.yml b/vendor/fuser/.github/workflows/ci.yml deleted file mode 100644 index 0bd8647f3..000000000 --- a/vendor/fuser/.github/workflows/ci.yml +++ /dev/null @@ -1,97 +0,0 @@ -name: CI - -on: - pull_request: - push: # required for actions/cache to work - branches: - - master - -jobs: - compile: - runs-on: ubuntu-22.04 - strategy: - matrix: - libfuse: [libfuse-dev, libfuse3-dev] - features: [ '', 'abi-7-19' ] - - steps: - - uses: actions/checkout@v4 - - - name: Cache - id: rust-cache - uses: actions/cache@v4 - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - ~/.rustup/toolchains/ - target/ - key: ${{ runner.os }}-cargo-compile-v1-${{ matrix.libfuse }}-${{ matrix.features }}-${{ hashFiles('**/Cargo.toml', '.github/workflows/*.yml') }} - - - name: Install packages - run: | - sudo apt update - sudo apt install -y ${{ matrix.libfuse }} build-essential - - - name: Install Rust - if: steps.rust-cache.outputs.cache-hit != 'true' - run: | - rustup target add x86_64-unknown-linux-musl - - - name: Run tests - run: | - cargo build --all --all-targets --features=${{ matrix.features }} - cargo build --all --all-targets --no-default-features - cargo build --target=x86_64-unknown-linux-musl --no-default-features - cargo test --all --features=${{ matrix.features }} - cargo doc --all --no-deps --features=${{ matrix.features }} - ci: - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4 - - - name: Cache - id: rust-cache - uses: actions/cache@v4 - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - ~/.rustup/toolchains/ - target/ - key: ${{ runner.os }}-cargo-ci-v1-${{ hashFiles('**/Cargo.toml', '.github/workflows/*.yml') }} - - - name: Install packages - run: | - sudo apt update - sudo apt install -y libfuse-dev libfuse3-dev build-essential - - - name: Install Rust - if: steps.rust-cache.outputs.cache-hit != 'true' - run: | - rustup toolchain install 1.81 - rustup component add rustfmt - rustup component add clippy - - - name: Install cargo-deny - if: steps.rust-cache.outputs.cache-hit != 'true' - run: cargo +1.81 install --force --version 0.16.2 cargo-deny --locked - - - name: Run tests - run: INTERACTIVE="" make pre - - test: - runs-on: ubuntu-22.04 - strategy: - matrix: - test_group: [mount_tests, pjdfs_tests, xfstests] - - steps: - - uses: actions/checkout@v4 - - - name: Run tests - run: INTERACTIVE="" make ${{ matrix.test_group }} diff --git a/vendor/fuser/.gitignore b/vendor/fuser/.gitignore deleted file mode 100644 index 4c3ebad99..000000000 --- a/vendor/fuser/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/Cargo.lock -/target -.idea/ -logs/ diff --git a/vendor/fuser/CHANGELOG.md b/vendor/fuser/CHANGELOG.md deleted file mode 100644 index e66b018a8..000000000 --- a/vendor/fuser/CHANGELOG.md +++ /dev/null @@ -1,181 +0,0 @@ -# FUSE for Rust - Changelog - -## 0.15.1 - 2024-11-27 -* Fix crtime related panic that could occur on MacOS. See PR #322 for details. - -## 0.15.0 - 2024-10-25 -* Add file handle argument to `getattr()` -* Change `poll()` to take a `PollHandle` instead of a `u64` -* Add low level API for manually mounting or wrapping a fuse file descriptor into a `Session` -* Fix compatibility with MacFUSE 4.x -* Performance optimizations - -## 0.14.0 - 2023-11-04 -* Add support for poll -* Add support for notifications -* ABI 7.11 support is now complete - -## 0.13.0 - 2023-08-16 -* Remove dependency on `users` crate -* Performance optimizations - -## 0.12.0 - 2022-12-13 -* Add method to `Session` to unmount non-`Send` `Filesystem`s - -## 0.11.1 - 2022-08-24 -* Improve an error message when using libfuse2 - -## 0.11.0 - 2022-03-05 -* Add `spawn_mount2()` -* Deprecate `spawn_mount()` - -## 0.10.0 - 2022-01-06 -* Improve error messages -* Support compiling with musl -* Default `link()` & `symlink()` now return EPERM instead of ENOSYS - -## 0.9.1 - 2021-09-07 -* `forget` and `batch_forget` no longer require that `AllowRoot` be set - -## 0.9.0 - 2021-08-31 -* Ensure that `Filesystem::destroy` is always called, when the filesystem is unmounted -* Remove request parameter from `Filesystem::destroy`. -* Make `fuse_forget_one` public, so that `Filesystem::batch_forget` can be implemented by users. -* Fix `batch_forget`. Previously, it always received an empty list of inodes. -* Fix `MountOption::AllowRoot`. Previously, using it resulted in a crash. -* Fix `MountOption::AutoUnmount` so that it works when `AllowRoot` and `AllowOther` are both not set. -* Make log messages more verbose (now includes the operation) - -## 0.8.0 - 2021-06-11 -* Deprecate `mount()` -* Remove `FileAttr.padding`. This field was added by mistake, and does nothing -* Fix crash when receiving an unknown FUSE operation type -* Minor performance optimizations - -## 0.7.0 - 2021-01-10 -* Support building with MacFuse 4.x on OSX -* Support configuring max_write & max_readahead via `KernelConfig` during `init` -* Support configuring filesystem timestamp granularity via `KernelConfig.set_time_granularity` during `init` -* Support requesting additional capability flags via `KernelConfig.add_capabilities` during `init` - -## 0.6.0 - 2020-11-22 -* Make `spawn_mount()` safe -* Change `flags` parameter of `create()`, `open()`, `opendir()`, `release()`, `releasedir()` to be signed, so that it matches - libfuse and the associated constants in libc -* Change `flags` parameter of `setxattr()` to be signed, so that it matches libfuse -* Change `mask` parameter of `access()` to be signed, so that it matches libfuse and the associated constants in libc -* Change lock type parameter of `getlk()` and `setlk()` to be signed, so that it matches libfuse and the associated constants in libc -* Change atime & atime_now and mtime & mtime_now parameters of `setattr()` to make their relationship more obvious -* Add `lock_owner` and file `flags` parameters to `read()` and `write()` -* Add `umask` parameter to `mknod()`, `mkdir()` and `create()` -* Add `KernelConfig` parameter to `init()` to allow `Filesystem` to configure the kernel connection attributes -* Add support for `fallocate()`, `ioctl()`, `copy_file_range()`, and `lseek()` -* Add support for FUSE_BATCH_FORGET -* Add support for FUSE_READDIRPLUS -* Add support for FUSE_RENAME2 -* Add FUSE_WRITE_KILL_PRIV flag for `write()` -* Add FUSE_WRITEBACK_CACHE flag -* Add FUSE_NO_OPEN_SUPPORT flag -* Add FUSE_PARALLEL_DIROPS flag -* Add FUSE_HANDLE_KILLPRIV flag -* Add FUSE_POSIX_ACL flag -* Add FUSE_ABORT_ERROR flag -* Add FUSE_NO_OPENDIR_SUPPORT flag -* Add FUSE_CACHE_SYMLINKS flag -* Add FUSE_EXPLICIT_INVAL_DATA flag -* Add FUSE_IOCTL_COMPAT_X32 flag -* Add FOPEN_CACHE_DIR flag -* Add FOPEN_STREAM flag -* Add FUSE_MAX_PAGES flag -* Add max_pages, and time_gran support to init code path (these are not currently configurable) -* Add support for ctime in `setattr()` -* Add support for timestamps before the unix epoch in `getattr()` and `setattr()` - -## 0.5.0 - 2020-10-17 - -* Enable FUSE_BIG_WRITES for ABI >= 7.10 -* Add FUSE_AUTO_INVAL_DATA constant -* Add ABI 7.20 to 7.31 feature flags. Support for these are incomplete. -* Add support for building with libfuse3 -* Add support for building without libfuse/libfuse3 on Linux (i.e. there's now a pure Rust implementation of all features) -* Add `mount2()` with improved option API - -## 0.4.1 - 2020-10-12 - -* Added new feature `serializable` that will enable serde serialization/deserialization for `FileType`, `FileAttr` - -## 0.4.0 - 2020-06-18 - -* Forked as `fuser` crate, at https://github.com/cberner/fuser -* Add ATIME_NOW and MTIME_NOW support -* Add stubs for ioctl, fallocate, and poll for ABI 7.11 - -## 0.3.1 - 2017-11-08 - -* Offsets to `read`, `write` and `readdir` methods are signed integers now (breaking change, sorry) -* Link `libosxfuse` on macOS, `libfuse` on all other systems - -## 0.3.0 - 2017-01-06 - -* Fix extended attribute handling (`getxattr` and `listxattr` methods changed and `ReplyXattr` was added) -* `mount` now also returns a `Result` since it may fail if the session fails to run -* Filenames are now passed as `&OsStr` in the filesystem interface -* Removed publishing of documentation on GitHub pages. Docs are now available on https://docs.rs/fuse -* Add `FileType::Socket` - -## 0.2.8 - 2016-07-31 - -* Documentation of releases is build by CI now and made available at https://zargony.github.io/rust-fuse -* Fix `unmount` on BSD systems -* Simplified `libfuse` detection with `pkg-config` -* `ReplyDirectory::sized` was removed since it was impossible to use it safely - -## 0.2.7 - 2015-09-08 - -* Update to latest Rust stable - no longer needs nightly Rust -* A filesystem implementation doesn't need to be `Send` anymore to be mounted synchronously -* A filesystem implementation doesn't need to be 'static anymore to be mounted asynchronously -* CI tests are covering nightly, beta and stable Rust under OSX and Linux now - -## 0.2.6 - 2015-04-23 - -* Update to latest Rust nightly -* Fix mounting of filesystems as non-root on Linux systems - -## 0.2.5 - 2015-03-21 - -* Update to latest Rust nightly -* `unmount` returns a `Result` now since unmounting may fail internally -* Fix `unmount` on Linux systems -* Remove deprecated file types from interface (got rid of `std::old_io`) -* Introducing `FileType` - -## 0.2.4 - 2015-02-22 - -* Update to latest Rust nightly -* `spawn_mount` returns a `Result` now since starting a new thread may fail -* Paths are now passed using `std::path::Path` (got rid of `std::old_path`) -* FUSE options are now passed as a slice of `OsStr` rather than a slice of bytes - -## 0.2.3 - 2015-01-17 - -* Update to latest Rust nightly - -## 0.2.2 - 2015-01-14 - -* Update to latest Rust nightly -* Ensure that `Reply` is `Send` to support asynchronous processing -* Add CI testing under Linux - -## 0.2.1 - 2015-01-07 - -* Update to latest Rust nightly -* Use `build.rs` and `pkg-config` to discover `libfuse` / `libosxfuse` - -## 0.2.0 - 2014-12-25 - -Initial release - -## pre-0.2.0 - 2013-10-03 - -No versioning (based on make, cargo and crates.io didn't exist yet) diff --git a/vendor/fuser/Cargo.toml b/vendor/fuser/Cargo.toml deleted file mode 100644 index 89df9ac56..000000000 --- a/vendor/fuser/Cargo.toml +++ /dev/null @@ -1,141 +0,0 @@ -# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO -# -# When uploading crates to the registry Cargo will automatically -# "normalize" Cargo.toml files for maximal compatibility -# with all versions of Cargo and also rewrite `path` dependencies -# to registry (e.g., crates.io) dependencies. -# -# If you are reading this file be aware that the original Cargo.toml -# will likely look very different (and much more reasonable). -# See Cargo.toml.orig for the original contents. - -[package] -edition = "2021" -name = "fuser" -version = "0.15.1" -authors = ["Christopher Berner "] -build = "build.rs" -description = "Filesystem in Userspace (FUSE) for Rust" -homepage = "https://github.com/cberner/fuser" -documentation = "https://docs.rs/fuser" -readme = "README.md" -keywords = [ - "fuse", - "filesystem", - "system", - "bindings", -] -categories = [ - "external-ffi-bindings", - "api-bindings", - "filesystem", - "os::unix-apis", -] -license = "MIT" -repository = "https://github.com/cberner/fuser" - -[[example]] -name = "ioctl" -required-features = ["abi-7-11"] - -[dependencies.libc] -version = "0.2.51" - -[dependencies.log] -version = "0.4.6" - -[dependencies.memchr] -version = "2.7.2" - -[dependencies.nix] -version = "0.29.0" -features = [ - "fs", - "user", -] - -[dependencies.page_size] -version = "0.6.0" - -[dependencies.serde] -version = "1.0.102" -features = [ - "std", - "derive", -] -optional = true - -[dependencies.smallvec] -version = "1.6.1" - -[dependencies.zerocopy] -version = "0.8" -features = ["derive"] - -[target.'cfg(target_os = "macos")'.dependencies.libloading] -version = "0.8" - -[dev-dependencies.bincode] -version = "1.3.1" - -[dev-dependencies.clap] -version = "4.4" -features = [ - "cargo", - "derive", -] - -[dev-dependencies.env_logger] -version = "0.11.3" - -[dev-dependencies.nix] -version = "0.29.0" -features = [ - "poll", - "fs", - "ioctl", -] - -[dev-dependencies.serde] -version = "1.0.102" -features = [ - "std", - "derive", -] - -[dev-dependencies.tempfile] -version = "3.10.1" - -[build-dependencies.pkg-config] -version = "0.3.14" -optional = true - -[features] -abi-7-10 = ["abi-7-9"] -abi-7-11 = ["abi-7-10"] -abi-7-12 = ["abi-7-11"] -abi-7-13 = ["abi-7-12"] -abi-7-14 = ["abi-7-13"] -abi-7-15 = ["abi-7-14"] -abi-7-16 = ["abi-7-15"] -abi-7-17 = ["abi-7-16"] -abi-7-18 = ["abi-7-17"] -abi-7-19 = ["abi-7-18"] -abi-7-20 = ["abi-7-19"] -abi-7-21 = ["abi-7-20"] -abi-7-22 = ["abi-7-21"] -abi-7-23 = ["abi-7-22"] -abi-7-24 = ["abi-7-23"] -abi-7-25 = ["abi-7-24"] -abi-7-26 = ["abi-7-25"] -abi-7-27 = ["abi-7-26"] -abi-7-28 = ["abi-7-27"] -abi-7-29 = ["abi-7-28"] -abi-7-30 = ["abi-7-29"] -abi-7-31 = ["abi-7-30"] -abi-7-9 = [] -default = ["libfuse"] -libfuse = ["pkg-config"] -macfuse-4-compat = [] -macfuse-5 = [] -serializable = ["serde"] diff --git a/vendor/fuser/Cargo.toml.orig b/vendor/fuser/Cargo.toml.orig deleted file mode 100644 index 7e79e842c..000000000 --- a/vendor/fuser/Cargo.toml.orig +++ /dev/null @@ -1,71 +0,0 @@ -[package] -name = "fuser" -description = "Filesystem in Userspace (FUSE) for Rust" -license = "MIT" -repository = "https://github.com/cberner/fuser" -documentation = "https://docs.rs/fuser" -homepage = "https://github.com/cberner/fuser" -version = "0.15.1" -edition = "2021" -readme = "README.md" -authors = ["Christopher Berner "] -keywords = ["fuse", "filesystem", "system", "bindings"] -categories = ["external-ffi-bindings", "api-bindings", "filesystem", "os::unix-apis"] -build = "build.rs" - -[dependencies] -libc = "0.2.51" -log = "0.4.6" -memchr = "2.7.2" -page_size = "0.6.0" -serde = { version = "1.0.102", features = ["std", "derive"], optional = true } -smallvec = "1.6.1" -zerocopy = { version = "0.8", features = ["derive"] } -nix = { version = "0.29.0", features = ["fs", "user"] } - -[target.'cfg(target_os = "macos")'.dependencies] -libloading = "0.8" - -[dev-dependencies] -env_logger = "0.11.3" -clap = { version = "4.4", features = ["cargo", "derive"] } -bincode = "1.3.1" -serde = { version = "1.0.102", features = ["std", "derive"] } -tempfile = "3.10.1" -nix = { version = "0.29.0", features = ["poll", "fs", "ioctl"] } - -[build-dependencies] -pkg-config = { version = "0.3.14", optional = true } - -[features] -default = ["libfuse"] -libfuse = ["pkg-config"] -serializable = ["serde"] -macfuse-4-compat = [] -abi-7-9 = [] -abi-7-10 = ["abi-7-9"] -abi-7-11 = ["abi-7-10"] -abi-7-12 = ["abi-7-11"] -abi-7-13 = ["abi-7-12"] -abi-7-14 = ["abi-7-13"] -abi-7-15 = ["abi-7-14"] -abi-7-16 = ["abi-7-15"] -abi-7-17 = ["abi-7-16"] -abi-7-18 = ["abi-7-17"] -abi-7-19 = ["abi-7-18"] -abi-7-20 = ["abi-7-19"] -abi-7-21 = ["abi-7-20"] -abi-7-22 = ["abi-7-21"] -abi-7-23 = ["abi-7-22"] -abi-7-24 = ["abi-7-23"] -abi-7-25 = ["abi-7-24"] -abi-7-26 = ["abi-7-25"] -abi-7-27 = ["abi-7-26"] -abi-7-28 = ["abi-7-27"] -abi-7-29 = ["abi-7-28"] -abi-7-30 = ["abi-7-29"] -abi-7-31 = ["abi-7-30"] - -[[example]] -name = "ioctl" -required-features = ["abi-7-11"] diff --git a/vendor/fuser/LICENSE.md b/vendor/fuser/LICENSE.md deleted file mode 100644 index 74bbec2f2..000000000 --- a/vendor/fuser/LICENSE.md +++ /dev/null @@ -1,23 +0,0 @@ -The MIT License (MIT) -===================== - -Copyright (c) 2020-present Christopher Berner - -Copyright © `2013-2019` `Andreas Neuhaus` `https://zargony.com/` - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the “Software”), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/fuser/Makefile b/vendor/fuser/Makefile deleted file mode 100644 index e6bd37407..000000000 --- a/vendor/fuser/Makefile +++ /dev/null @@ -1,51 +0,0 @@ -VERSION = $(shell git describe --tags --always --dirty) -INTERACTIVE ?= i - - -build: pre - cargo build --examples - -pre: - cargo fmt --all -- --check - cargo deny check licenses - cargo clippy --all-targets - cargo clippy --all-targets --no-default-features - cargo clippy --all-targets --features=abi-7-30 - -xfstests: - docker build -t fuser:xfstests -f xfstests.Dockerfile . - # Additional permissions are needed to be able to mount FUSE - docker run --rm -$(INTERACTIVE)t --cap-add SYS_ADMIN --cap-add IPC_OWNER --device /dev/fuse --security-opt apparmor:unconfined \ - --memory=2g --kernel-memory=200m \ - -v "$(shell pwd)/logs:/code/logs" fuser:xfstests bash -c "cd /code/fuser && ./xfstests.sh" - -pjdfs_tests: pjdfs_tests_fuse2 pjdfs_tests_fuse3 pjdfs_tests_pure - -pjdfs_tests_fuse2: - docker build --build-arg BUILD_FEATURES='--features=abi-7-19' -t fuser:pjdfs-2 -f pjdfs.Dockerfile . - # Additional permissions are needed to be able to mount FUSE - docker run --rm -$(INTERACTIVE)t --cap-add SYS_ADMIN --device /dev/fuse --security-opt apparmor:unconfined \ - -v "$(shell pwd)/logs:/code/logs" fuser:pjdfs-2 bash -c "cd /code/fuser && ./pjdfs.sh" - -pjdfs_tests_fuse3: - docker build --build-arg BUILD_FEATURES='--features=abi-7-31' -t fuser:pjdfs-3 -f pjdfs.Dockerfile . - # Additional permissions are needed to be able to mount FUSE - docker run --rm -$(INTERACTIVE)t --cap-add SYS_ADMIN --device /dev/fuse --security-opt apparmor:unconfined \ - -v "$(shell pwd)/logs:/code/logs" fuser:pjdfs-3 bash -c "cd /code/fuser && ./pjdfs.sh" - -pjdfs_tests_pure: - docker build --build-arg BUILD_FEATURES='--no-default-features --features=abi-7-19' -t fuser:pjdfs-pure -f pjdfs.Dockerfile . - # Additional permissions are needed to be able to mount FUSE - docker run --rm -$(INTERACTIVE)t --cap-add SYS_ADMIN --device /dev/fuse --security-opt apparmor:unconfined \ - -v "$(shell pwd)/logs:/code/logs" fuser:pjdfs-pure bash -c "cd /code/fuser && ./pjdfs.sh" - -mount_tests: - docker build -t fuser:mount_tests -f mount_tests.Dockerfile . - # Additional permissions are needed to be able to mount FUSE - docker run --rm -$(INTERACTIVE)t --cap-add SYS_ADMIN --device /dev/fuse --security-opt apparmor:unconfined \ - fuser:mount_tests bash -c "cd /code/fuser && bash ./simplefs_tests.sh" - docker run --rm -$(INTERACTIVE)t --cap-add SYS_ADMIN --device /dev/fuse --security-opt apparmor:unconfined \ - fuser:mount_tests bash -c "cd /code/fuser && bash ./mount_tests.sh" - -test: pre mount_tests pjdfs_tests xfstests - cargo test diff --git a/vendor/fuser/README.md b/vendor/fuser/README.md deleted file mode 100644 index 6b4919bcc..000000000 --- a/vendor/fuser/README.md +++ /dev/null @@ -1,137 +0,0 @@ -# FUSE (Filesystem in Userspace) for Rust - -![CI](https://github.com/cberner/fuser/actions/workflows/ci.yml/badge.svg) -[![Crates.io](https://img.shields.io/crates/v/fuser.svg)](https://crates.io/crates/fuser) -[![Documentation](https://docs.rs/fuser/badge.svg)](https://docs.rs/fuser) -[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/cberner/fuser/blob/master/LICENSE.md) -[![dependency status](https://deps.rs/repo/github/cberner/fuser/status.svg)](https://deps.rs/repo/github/cberner/fuser) - -## About - -**FUSE-Rust** is a [Rust] library crate for easy implementation of [FUSE filesystems][FUSE for Linux] in userspace. - -FUSE-Rust does not just provide bindings, it is a rewrite of the original FUSE C library to fully take advantage of Rust's architecture. - -This library was originally forked from the [`fuse` crate](https://github.com/zargony/fuse-rs) with the intention -of continuing development. In particular adding features from ABIs after 7.19 - -## Documentation - -[FUSE-Rust reference][Documentation] - -## Details - -A working FUSE filesystem consists of three parts: - -1. The **kernel driver** that registers as a filesystem and forwards operations into a communication channel to a userspace process that handles them. -1. The **userspace library** (libfuse) that helps the userspace process to establish and run communication with the kernel driver. -1. The **userspace implementation** that actually processes the filesystem operations. - -The kernel driver is provided by the FUSE project, the userspace implementation needs to be provided by the developer. FUSE-Rust provides a replacement for the libfuse userspace library between these two. This way, a developer can fully take advantage of the Rust type interface and runtime features when building a FUSE filesystem in Rust. - -Except for a single setup (mount) function call and a final teardown (umount) function call to libfuse, everything runs in Rust, and on Linux these calls to libfuse are optional. They can be removed by building without the "libfuse" feature flag. - -## Dependencies - -FUSE must be installed to build or run programs that use FUSE-Rust (i.e. kernel driver and libraries. Some platforms may also require userland utils like `fusermount`). A default installation of FUSE is usually sufficient. - -To build FUSE-Rust or any program that depends on it, `pkg-config` needs to be installed as well. - -### Linux - -[FUSE for Linux] is available in most Linux distributions and usually called `fuse` or `fuse3` (this crate is compatible with both). To install on a Debian based system: - -```sh -sudo apt-get install fuse3 libfuse3-dev -``` - -Install on CentOS: - -```sh -sudo yum install fuse -``` - -To build, FUSE libraries and headers are required. The package is usually called `libfuse-dev` or `fuse-devel`. Also `pkg-config` is required for locating libraries and headers. - -```sh -sudo apt-get install libfuse-dev pkg-config -``` - -```sh -sudo yum install fuse-devel pkgconfig -``` - -### macOS (untested) - -Installer packages can be downloaded from the [FUSE for macOS homepage][FUSE for macOS]. This is the *kernel* part that needs to be installed always. - -#### To install using Homebrew - -```sh -brew install macfuse -``` - -#### To install using Nix - -``` sh -nix-env -iA nixos.osxfuse -``` - -And `pkg-config` (required for building): - -``` sh -nix-env -iA nixos.pkg-config -``` - -When using `nix` it is required that you specify `PKG_CONFIG_PATH` environment variable to point at where `osxfuse` is installed: - -``` sh -export PKG_CONFIG_PATH=${HOME}/.nix-profile/lib/pkgconfig -``` - -### FreeBSD - -Install packages `fusefs-libs` and `pkgconf`. - -```sh -pkg install fusefs-libs pkgconf -``` - -## Usage - -Put this in your `Cargo.toml`: - -```toml -[dependencies] -fuser = "0.7" -``` - -To create a new filesystem, implement the trait `fuser::Filesystem`. See the [documentation] for details or the `examples` directory for some basic examples. - -## To Do - -Most features of libfuse up to 3.10.3 are implemented. Feel free to contribute. See the [list of issues][issues] on GitHub and search the source files for comments containing "`TODO`" or "`FIXME`" to see what's still missing. - -## Compatibility - -Developed and tested on Linux. Tested under [Linux][FUSE for Linux] and [FreeBSD][FUSE for FreeBSD] using stable [Rust] (see CI for details). - -## License - -Licensed under [MIT License](LICENSE.md), except for those files in `examples/` that explicitly contain a different license. - -## Contribution - -Fork, hack, submit pull request. Make sure to make it useful for the target audience, keep the project's philosophy and Rust coding standards in mind. For larger or essential changes, you may want to open an issue for discussion first. Also remember to update the [Changelog] if your changes are relevant to the users. - -[Rust]: https://rust-lang.org -[Homebrew]: https://brew.sh -[Changelog]: https://keepachangelog.com/en/1.0.0/ - -[FUSE-Rust]: https://github.com/cberner/fuser -[issues]: https://github.com/cberner/fuser/issues -[Documentation]: https://docs.rs/fuser - -[FUSE for Linux]: https://github.com/libfuse/libfuse/ -[FUSE for macOS]: https://osxfuse.github.io -[FUSE for FreeBSD]: https://wiki.freebsd.org/FUSEFS diff --git a/vendor/fuser/build.rs b/vendor/fuser/build.rs deleted file mode 100644 index c519d97c3..000000000 --- a/vendor/fuser/build.rs +++ /dev/null @@ -1,58 +0,0 @@ -fn main() { - // Register rustc cfg for switching between mount implementations. - // When fuser MSRV is updated to v1.77 or above, we should switch from 'cargo:' to 'cargo::' syntax. - println!("cargo:rustc-check-cfg=cfg(fuser_mount_impl, values(\"pure-rust\", \"libfuse2\", \"libfuse3\"))"); - - #[cfg(all(not(feature = "libfuse"), not(target_os = "linux")))] - unimplemented!("Building without libfuse is only supported on Linux"); - - #[cfg(not(feature = "libfuse"))] - { - println!("cargo:rustc-cfg=fuser_mount_impl=\"pure-rust\""); - } - #[cfg(feature = "libfuse")] - { - if cfg!(all(target_os = "macos", feature = "macfuse-5")) { - // macFUSE is loaded dynamically at mount time. This keeps ordinary - // macOS builds (including CI) independent of a locally installed - // macFUSE SDK while still requiring macFUSE to actually mount. - println!("cargo:rustc-cfg=fuser_mount_impl=\"libfuse2\""); - println!("cargo:rustc-cfg=feature=\"macfuse-4-compat\""); - } else if cfg!(target_os = "macos") { - if pkg_config::Config::new() - .atleast_version("2.6.0") - .probe("fuse") // for macFUSE 4.x - .map_err(|e| eprintln!("{}", e)) - .is_ok() - { - println!("cargo:rustc-cfg=fuser_mount_impl=\"libfuse2\""); - println!("cargo:rustc-cfg=feature=\"macfuse-4-compat\""); - } else { - pkg_config::Config::new() - .atleast_version("2.6.0") - .probe("osxfuse") // for osxfuse 3.x - .map_err(|e| eprintln!("{}", e)) - .unwrap(); - println!("cargo:rustc-cfg=fuser_mount_impl=\"libfuse2\""); - } - } else { - // First try to link with libfuse3 - if pkg_config::Config::new() - .atleast_version("3.0.0") - .probe("fuse3") - .map_err(|e| eprintln!("{e}")) - .is_ok() - { - println!("cargo:rustc-cfg=fuser_mount_impl=\"libfuse3\""); - } else { - // Fallback to libfuse - pkg_config::Config::new() - .atleast_version("2.6.0") - .probe("fuse") - .map_err(|e| eprintln!("{e}")) - .unwrap(); - println!("cargo:rustc-cfg=fuser_mount_impl=\"libfuse2\""); - } - } - } -} diff --git a/vendor/fuser/deny.toml b/vendor/fuser/deny.toml deleted file mode 100644 index df333f4f8..000000000 --- a/vendor/fuser/deny.toml +++ /dev/null @@ -1,142 +0,0 @@ -# This template contains all of the possible sections and their default values - -# Note that all fields that take a lint level have these possible values: -# * deny - An error will be produced and the check will fail -# * warn - A warning will be produced, but the check will not fail -# * allow - No warning or error will be produced, though in some cases a note -# will be - -# The values provided in this template are the default values that will be used -# when any section or field is not specified in your own configuration - -# This section is considered when running `cargo deny check advisories` -# More documentation for the advisories section can be found here: -# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html -[advisories] -# The path where the advisory database is cloned/fetched into -db-path = "~/.cargo/advisory-db" -# The url of the advisory database to use -db-urls = ["https://github.com/rustsec/advisory-db"] -# The lint level for crates that have been yanked from their source registry -yanked = "warn" -# A list of advisory IDs to ignore. Note that ignored advisories will still -# output a note when they are encountered. -ignore = [ - #"RUSTSEC-0000-0000", -] -# Threshold for security vulnerabilities, any vulnerability with a CVSS score -# lower than the range specified will be ignored. Note that ignored advisories -# will still output a note when they are encountered. -# * None - CVSS Score 0.0 -# * Low - CVSS Score 0.1 - 3.9 -# * Medium - CVSS Score 4.0 - 6.9 -# * High - CVSS Score 7.0 - 8.9 -# * Critical - CVSS Score 9.0 - 10.0 -#severity-threshold = - -# This section is considered when running `cargo deny check licenses` -# More documentation for the licenses section can be found here: -# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html -[licenses] -# List of explictly allowed licenses -# See https://spdx.org/licenses/ for list of possible licenses -# [possible values: any SPDX 3.7 short identifier (+ optional exception)]. -allow = [ - "BSD-2-Clause", -# "BSD-3-Clause", - "MIT", -] -# The confidence threshold for detecting a license from license text. -# The higher the value, the more closely the license text must be to the -# canonical license text of a valid SPDX license file. -# [possible values: any between 0.0 and 1.0]. -confidence-threshold = 0.8 -# Allow 1 or more licenses on a per-crate basis, so that particular licenses -# aren't accepted for every possible crate as with the normal allow list -exceptions = [ - # Each entry is the crate and version constraint, and its specific allow - # list - #{ allow = ["Zlib"], name = "adler32", version = "*" }, - { allow = ["Unicode-3.0"], name = "unicode-ident", version = "*" }, -] - -# Some crates don't have (easily) machine readable licensing information, -# adding a clarification entry for it allows you to manually specify the -# licensing information -#[[licenses.clarify]] -# The name of the crate the clarification applies to -#name = "ring" -# THe optional version constraint for the crate -#version = "*" -# The SPDX expression for the license requirements of the crate -#expression = "MIT AND ISC AND OpenSSL" -# One or more files in the crate's source used as the "source of truth" for -# the license expression. If the contents match, the clarification will be used -# when running the license check, otherwise the clarification will be ignored -# and the crate will be checked normally, which may produce warnings or errors -# depending on the rest of your configuration -#license-files = [ - # Each entry is a crate relative path, and the (opaque) hash of its contents - #{ path = "LICENSE", hash = 0xbd0eed23 } -#] - -[licenses.private] -# If true, ignores workspace crates that aren't published, or are only -# published to private registries -ignore = false -# One or more private registries that you might publish crates to, if a crate -# is only published to private registries, and ignore is true, the crate will -# not have its license(s) checked -registries = [ - #"https://sekretz.com/registry -] - -# This section is considered when running `cargo deny check bans`. -# More documentation about the 'bans' section can be found here: -# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html -[bans] -# Lint level for when multiple versions of the same crate are detected -multiple-versions = "warn" -# The graph highlighting used when creating dotgraphs for crates -# with multiple versions -# * lowest-version - The path to the lowest versioned duplicate is highlighted -# * simplest-path - The path to the version with the fewest edges is highlighted -# * all - Both lowest-version and simplest-path are used -highlight = "all" -# List of crates that are allowed. Use with care! -allow = [ - #{ name = "ansi_term", version = "=0.11.0" }, -] -# List of crates to deny -deny = [ - # Each entry the name of a crate and a version range. If version is - # not specified, all versions will be matched. - #{ name = "ansi_term", version = "=0.11.0" }, -] -# Certain crates/versions that will be skipped when doing duplicate detection. -skip = [ - #{ name = "ansi_term", version = "=0.11.0" }, -] -# Similarly to `skip` allows you to skip certain crates during duplicate -# detection. Unlike skip, it also includes the entire tree of transitive -# dependencies starting at the specified crate, up to a certain depth, which is -# by default infinite -skip-tree = [ - #{ name = "ansi_term", version = "=0.11.0", depth = 20 }, -] - -# This section is considered when running `cargo deny check sources`. -# More documentation about the 'sources' section can be found here: -# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html -[sources] -# Lint level for what to happen when a crate from a crate registry that is not -# in the allow list is encountered -unknown-registry = "warn" -# Lint level for what to happen when a crate from a git repository that is not -# in the allow list is encountered -unknown-git = "warn" -# List of URLs for allowed crate registries. Defaults to the crates.io index -# if not specified. If it is specified but empty, no registries are allowed. -allow-registry = ["https://github.com/rust-lang/crates.io-index"] -# List of URLs for allowed Git repositories -allow-git = [] diff --git a/vendor/fuser/examples/hello.rs b/vendor/fuser/examples/hello.rs deleted file mode 100644 index c41504957..000000000 --- a/vendor/fuser/examples/hello.rs +++ /dev/null @@ -1,149 +0,0 @@ -use clap::{crate_version, Arg, ArgAction, Command}; -use fuser::{ - FileAttr, FileType, Filesystem, MountOption, ReplyAttr, ReplyData, ReplyDirectory, ReplyEntry, - Request, -}; -use libc::ENOENT; -use std::ffi::OsStr; -use std::time::{Duration, UNIX_EPOCH}; - -const TTL: Duration = Duration::from_secs(1); // 1 second - -const HELLO_DIR_ATTR: FileAttr = FileAttr { - ino: 1, - size: 0, - blocks: 0, - atime: UNIX_EPOCH, // 1970-01-01 00:00:00 - mtime: UNIX_EPOCH, - ctime: UNIX_EPOCH, - crtime: UNIX_EPOCH, - kind: FileType::Directory, - perm: 0o755, - nlink: 2, - uid: 501, - gid: 20, - rdev: 0, - flags: 0, - blksize: 512, -}; - -const HELLO_TXT_CONTENT: &str = "Hello World!\n"; - -const HELLO_TXT_ATTR: FileAttr = FileAttr { - ino: 2, - size: 13, - blocks: 1, - atime: UNIX_EPOCH, // 1970-01-01 00:00:00 - mtime: UNIX_EPOCH, - ctime: UNIX_EPOCH, - crtime: UNIX_EPOCH, - kind: FileType::RegularFile, - perm: 0o644, - nlink: 1, - uid: 501, - gid: 20, - rdev: 0, - flags: 0, - blksize: 512, -}; - -struct HelloFS; - -impl Filesystem for HelloFS { - fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) { - if parent == 1 && name.to_str() == Some("hello.txt") { - reply.entry(&TTL, &HELLO_TXT_ATTR, 0); - } else { - reply.error(ENOENT); - } - } - - fn getattr(&mut self, _req: &Request, ino: u64, _fh: Option, reply: ReplyAttr) { - match ino { - 1 => reply.attr(&TTL, &HELLO_DIR_ATTR), - 2 => reply.attr(&TTL, &HELLO_TXT_ATTR), - _ => reply.error(ENOENT), - } - } - - fn read( - &mut self, - _req: &Request, - ino: u64, - _fh: u64, - offset: i64, - _size: u32, - _flags: i32, - _lock: Option, - reply: ReplyData, - ) { - if ino == 2 { - reply.data(&HELLO_TXT_CONTENT.as_bytes()[offset as usize..]); - } else { - reply.error(ENOENT); - } - } - - fn readdir( - &mut self, - _req: &Request, - ino: u64, - _fh: u64, - offset: i64, - mut reply: ReplyDirectory, - ) { - if ino != 1 { - reply.error(ENOENT); - return; - } - - let entries = vec![ - (1, FileType::Directory, "."), - (1, FileType::Directory, ".."), - (2, FileType::RegularFile, "hello.txt"), - ]; - - for (i, entry) in entries.into_iter().enumerate().skip(offset as usize) { - // i + 1 means the index of the next entry - if reply.add(entry.0, (i + 1) as i64, entry.1, entry.2) { - break; - } - } - reply.ok(); - } -} - -fn main() { - let matches = Command::new("hello") - .version(crate_version!()) - .author("Christopher Berner") - .arg( - Arg::new("MOUNT_POINT") - .required(true) - .index(1) - .help("Act as a client, and mount FUSE at given path"), - ) - .arg( - Arg::new("auto_unmount") - .long("auto_unmount") - .action(ArgAction::SetTrue) - .help("Automatically unmount on process exit"), - ) - .arg( - Arg::new("allow-root") - .long("allow-root") - .action(ArgAction::SetTrue) - .help("Allow root user to access filesystem"), - ) - .get_matches(); - env_logger::init(); - let mountpoint = matches.get_one::("MOUNT_POINT").unwrap(); - let mut options = vec![MountOption::RO, MountOption::FSName("hello".to_string())]; - if matches.get_flag("auto_unmount") { - options.push(MountOption::AutoUnmount); - } - if matches.get_flag("allow-root") { - options.push(MountOption::AllowRoot); - } - fuser::mount2(HelloFS, mountpoint, &options).unwrap(); -} diff --git a/vendor/fuser/examples/ioctl.rs b/vendor/fuser/examples/ioctl.rs deleted file mode 100644 index d9c7cf343..000000000 --- a/vendor/fuser/examples/ioctl.rs +++ /dev/null @@ -1,209 +0,0 @@ -// This example requires fuse 7.11 or later. Run with: -// -// cargo run --example ioctl --features abi-7-11 /tmp/foobar - -use clap::{crate_version, Arg, ArgAction, Command}; -use fuser::{ - FileAttr, FileType, Filesystem, MountOption, ReplyAttr, ReplyData, ReplyDirectory, ReplyEntry, - Request, -}; -use libc::{EINVAL, ENOENT}; -use log::debug; -use std::ffi::OsStr; -use std::time::{Duration, UNIX_EPOCH}; - -const TTL: Duration = Duration::from_secs(1); // 1 second - -struct FiocFS { - content: Vec, - root_attr: FileAttr, - fioc_file_attr: FileAttr, -} - -impl FiocFS { - fn new() -> Self { - let uid = unsafe { libc::getuid() }; - let gid = unsafe { libc::getgid() }; - - let root_attr = FileAttr { - ino: 1, - size: 0, - blocks: 0, - atime: UNIX_EPOCH, // 1970-01-01 00:00:00 - mtime: UNIX_EPOCH, - ctime: UNIX_EPOCH, - crtime: UNIX_EPOCH, - kind: FileType::Directory, - perm: 0o755, - nlink: 2, - uid, - gid, - rdev: 0, - flags: 0, - blksize: 512, - }; - - let fioc_file_attr = FileAttr { - ino: 2, - size: 0, - blocks: 1, - atime: UNIX_EPOCH, // 1970-01-01 00:00:00 - mtime: UNIX_EPOCH, - ctime: UNIX_EPOCH, - crtime: UNIX_EPOCH, - kind: FileType::RegularFile, - perm: 0o644, - nlink: 1, - uid, - gid, - rdev: 0, - flags: 0, - blksize: 512, - }; - - Self { - content: vec![], - root_attr, - fioc_file_attr, - } - } -} - -impl Filesystem for FiocFS { - fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) { - if parent == 1 && name.to_str() == Some("fioc") { - reply.entry(&TTL, &self.fioc_file_attr, 0); - } else { - reply.error(ENOENT); - } - } - - fn getattr(&mut self, _req: &Request, ino: u64, _fh: Option, reply: ReplyAttr) { - match ino { - 1 => reply.attr(&TTL, &self.root_attr), - 2 => reply.attr(&TTL, &self.fioc_file_attr), - _ => reply.error(ENOENT), - } - } - - fn read( - &mut self, - _req: &Request, - ino: u64, - _fh: u64, - offset: i64, - _size: u32, - _flags: i32, - _lock: Option, - reply: ReplyData, - ) { - if ino == 2 { - reply.data(&self.content[offset as usize..]) - } else { - reply.error(ENOENT); - } - } - - fn readdir( - &mut self, - _req: &Request, - ino: u64, - _fh: u64, - offset: i64, - mut reply: ReplyDirectory, - ) { - if ino != 1 { - reply.error(ENOENT); - return; - } - - let entries = vec![ - (1, FileType::Directory, "."), - (1, FileType::Directory, ".."), - (2, FileType::RegularFile, "fioc"), - ]; - - for (i, entry) in entries.into_iter().enumerate().skip(offset as usize) { - // i + 1 means the index of the next entry - if reply.add(entry.0, (i + 1) as i64, entry.1, entry.2) { - break; - } - } - reply.ok(); - } - - fn ioctl( - &mut self, - _req: &Request<'_>, - ino: u64, - _fh: u64, - _flags: u32, - cmd: u32, - in_data: &[u8], - _out_size: u32, - reply: fuser::ReplyIoctl, - ) { - if ino != 2 { - reply.error(EINVAL); - return; - } - - const FIOC_GET_SIZE: u64 = nix::request_code_read!('E', 0, std::mem::size_of::()); - const FIOC_SET_SIZE: u64 = nix::request_code_write!('E', 1, std::mem::size_of::()); - - match cmd.into() { - FIOC_GET_SIZE => { - let size_bytes = self.content.len().to_ne_bytes(); - reply.ioctl(0, &size_bytes); - } - FIOC_SET_SIZE => { - let new_size = usize::from_ne_bytes(in_data.try_into().unwrap()); - self.content = vec![0_u8; new_size]; - reply.ioctl(0, &[]); - } - _ => { - debug!("unknown ioctl: {}", cmd); - reply.error(EINVAL); - } - } - } -} - -fn main() { - let matches = Command::new("hello") - .version(crate_version!()) - .author("Colin Marc") - .arg( - Arg::new("MOUNT_POINT") - .required(true) - .index(1) - .help("Act as a client, and mount FUSE at given path"), - ) - .arg( - Arg::new("auto_unmount") - .long("auto_unmount") - .action(ArgAction::SetTrue) - .help("Automatically unmount on process exit"), - ) - .arg( - Arg::new("allow-root") - .long("allow-root") - .action(ArgAction::SetTrue) - .help("Allow root user to access filesystem"), - ) - .get_matches(); - - env_logger::init(); - - let mountpoint = matches.get_one::("MOUNT_POINT").unwrap(); - let mut options = vec![MountOption::FSName("fioc".to_string())]; - if matches.get_flag("auto_unmount") { - options.push(MountOption::AutoUnmount); - } - if matches.get_flag("allow-root") { - options.push(MountOption::AllowRoot); - } - - let fs = FiocFS::new(); - fuser::mount2(fs, mountpoint, &options).unwrap(); -} diff --git a/vendor/fuser/examples/null.rs b/vendor/fuser/examples/null.rs deleted file mode 100644 index 6b4feecde..000000000 --- a/vendor/fuser/examples/null.rs +++ /dev/null @@ -1,12 +0,0 @@ -use fuser::{Filesystem, MountOption}; -use std::env; - -struct NullFS; - -impl Filesystem for NullFS {} - -fn main() { - env_logger::init(); - let mountpoint = env::args_os().nth(1).unwrap(); - fuser::mount2(NullFS, mountpoint, &[MountOption::AutoUnmount]).unwrap(); -} diff --git a/vendor/fuser/examples/simple.rs b/vendor/fuser/examples/simple.rs deleted file mode 100644 index 0c0928555..000000000 --- a/vendor/fuser/examples/simple.rs +++ /dev/null @@ -1,2062 +0,0 @@ -#![allow(clippy::needless_return)] -#![allow(clippy::unnecessary_cast)] // libc::S_* are u16 or u32 depending on the platform - -use clap::{crate_version, Arg, ArgAction, Command}; -use fuser::consts::FOPEN_DIRECT_IO; -#[cfg(feature = "abi-7-26")] -use fuser::consts::FUSE_HANDLE_KILLPRIV; -// #[cfg(feature = "abi-7-31")] -// use fuser::consts::FUSE_WRITE_KILL_PRIV; -use fuser::TimeOrNow::Now; -use fuser::{ - Filesystem, KernelConfig, MountOption, ReplyAttr, ReplyCreate, ReplyData, ReplyDirectory, - ReplyEmpty, ReplyEntry, ReplyOpen, ReplyStatfs, ReplyWrite, ReplyXattr, Request, TimeOrNow, - FUSE_ROOT_ID, -}; -#[cfg(feature = "abi-7-26")] -use log::info; -use log::{debug, warn}; -use log::{error, LevelFilter}; -use serde::{Deserialize, Serialize}; -use std::cmp::min; -use std::collections::BTreeMap; -use std::ffi::OsStr; -use std::fs::{File, OpenOptions}; -use std::io::{BufRead, BufReader, ErrorKind, Read, Seek, SeekFrom, Write}; -use std::os::raw::c_int; -use std::os::unix::ffi::OsStrExt; -use std::os::unix::fs::FileExt; -#[cfg(target_os = "linux")] -use std::os::unix::io::IntoRawFd; -use std::path::{Path, PathBuf}; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use std::{env, fs, io}; - -const BLOCK_SIZE: u64 = 512; -const MAX_NAME_LENGTH: u32 = 255; -const MAX_FILE_SIZE: u64 = 1024 * 1024 * 1024 * 1024; - -// Top two file handle bits are used to store permissions -// Note: This isn't safe, since the client can modify those bits. However, this implementation -// is just a toy -const FILE_HANDLE_READ_BIT: u64 = 1 << 63; -const FILE_HANDLE_WRITE_BIT: u64 = 1 << 62; - -const FMODE_EXEC: i32 = 0x20; - -type Inode = u64; - -type DirectoryDescriptor = BTreeMap, (Inode, FileKind)>; - -#[derive(Serialize, Deserialize, Copy, Clone, PartialEq)] -enum FileKind { - File, - Directory, - Symlink, -} - -impl From for fuser::FileType { - fn from(kind: FileKind) -> Self { - match kind { - FileKind::File => fuser::FileType::RegularFile, - FileKind::Directory => fuser::FileType::Directory, - FileKind::Symlink => fuser::FileType::Symlink, - } - } -} - -#[derive(Debug)] -enum XattrNamespace { - Security, - System, - Trusted, - User, -} - -fn parse_xattr_namespace(key: &[u8]) -> Result { - let user = b"user."; - if key.len() < user.len() { - return Err(libc::ENOTSUP); - } - if key[..user.len()].eq(user) { - return Ok(XattrNamespace::User); - } - - let system = b"system."; - if key.len() < system.len() { - return Err(libc::ENOTSUP); - } - if key[..system.len()].eq(system) { - return Ok(XattrNamespace::System); - } - - let trusted = b"trusted."; - if key.len() < trusted.len() { - return Err(libc::ENOTSUP); - } - if key[..trusted.len()].eq(trusted) { - return Ok(XattrNamespace::Trusted); - } - - let security = b"security"; - if key.len() < security.len() { - return Err(libc::ENOTSUP); - } - if key[..security.len()].eq(security) { - return Ok(XattrNamespace::Security); - } - - return Err(libc::ENOTSUP); -} - -fn clear_suid_sgid(attr: &mut InodeAttributes) { - attr.mode &= !libc::S_ISUID as u16; - // SGID is only suppose to be cleared if XGRP is set - if attr.mode & libc::S_IXGRP as u16 != 0 { - attr.mode &= !libc::S_ISGID as u16; - } -} - -fn creation_gid(parent: &InodeAttributes, gid: u32) -> u32 { - if parent.mode & libc::S_ISGID as u16 != 0 { - return parent.gid; - } - - gid -} - -fn xattr_access_check( - key: &[u8], - access_mask: i32, - inode_attrs: &InodeAttributes, - request: &Request<'_>, -) -> Result<(), c_int> { - match parse_xattr_namespace(key)? { - XattrNamespace::Security => { - if access_mask != libc::R_OK && request.uid() != 0 { - return Err(libc::EPERM); - } - } - XattrNamespace::Trusted => { - if request.uid() != 0 { - return Err(libc::EPERM); - } - } - XattrNamespace::System => { - if key.eq(b"system.posix_acl_access") { - if !check_access( - inode_attrs.uid, - inode_attrs.gid, - inode_attrs.mode, - request.uid(), - request.gid(), - access_mask, - ) { - return Err(libc::EPERM); - } - } else if request.uid() != 0 { - return Err(libc::EPERM); - } - } - XattrNamespace::User => { - if !check_access( - inode_attrs.uid, - inode_attrs.gid, - inode_attrs.mode, - request.uid(), - request.gid(), - access_mask, - ) { - return Err(libc::EPERM); - } - } - } - - Ok(()) -} - -fn time_now() -> (i64, u32) { - time_from_system_time(&SystemTime::now()) -} - -fn system_time_from_time(secs: i64, nsecs: u32) -> SystemTime { - if secs >= 0 { - UNIX_EPOCH + Duration::new(secs as u64, nsecs) - } else { - UNIX_EPOCH - Duration::new((-secs) as u64, nsecs) - } -} - -fn time_from_system_time(system_time: &SystemTime) -> (i64, u32) { - // Convert to signed 64-bit time with epoch at 0 - match system_time.duration_since(UNIX_EPOCH) { - Ok(duration) => (duration.as_secs() as i64, duration.subsec_nanos()), - Err(before_epoch_error) => ( - -(before_epoch_error.duration().as_secs() as i64), - before_epoch_error.duration().subsec_nanos(), - ), - } -} - -#[derive(Serialize, Deserialize)] -struct InodeAttributes { - pub inode: Inode, - pub open_file_handles: u64, // Ref count of open file handles to this inode - pub size: u64, - pub last_accessed: (i64, u32), - pub last_modified: (i64, u32), - pub last_metadata_changed: (i64, u32), - pub kind: FileKind, - // Permissions and special mode bits - pub mode: u16, - pub hardlinks: u32, - pub uid: u32, - pub gid: u32, - pub xattrs: BTreeMap, Vec>, -} - -impl From for fuser::FileAttr { - fn from(attrs: InodeAttributes) -> Self { - fuser::FileAttr { - ino: attrs.inode, - size: attrs.size, - blocks: (attrs.size + BLOCK_SIZE - 1) / BLOCK_SIZE, - atime: system_time_from_time(attrs.last_accessed.0, attrs.last_accessed.1), - mtime: system_time_from_time(attrs.last_modified.0, attrs.last_modified.1), - ctime: system_time_from_time( - attrs.last_metadata_changed.0, - attrs.last_metadata_changed.1, - ), - crtime: SystemTime::UNIX_EPOCH, - kind: attrs.kind.into(), - perm: attrs.mode, - nlink: attrs.hardlinks, - uid: attrs.uid, - gid: attrs.gid, - rdev: 0, - blksize: BLOCK_SIZE as u32, - flags: 0, - } - } -} - -// Stores inode metadata data in "$data_dir/inodes" and file contents in "$data_dir/contents" -// Directory data is stored in the file's contents, as a serialized DirectoryDescriptor -struct SimpleFS { - data_dir: String, - next_file_handle: AtomicU64, - direct_io: bool, - suid_support: bool, -} - -impl SimpleFS { - fn new( - data_dir: String, - direct_io: bool, - #[allow(unused_variables)] suid_support: bool, - ) -> SimpleFS { - #[cfg(feature = "abi-7-26")] - { - SimpleFS { - data_dir, - next_file_handle: AtomicU64::new(1), - direct_io, - suid_support, - } - } - #[cfg(not(feature = "abi-7-26"))] - { - SimpleFS { - data_dir, - next_file_handle: AtomicU64::new(1), - direct_io, - suid_support: false, - } - } - } - - fn creation_mode(&self, mode: u32) -> u16 { - if !self.suid_support { - (mode & !(libc::S_ISUID | libc::S_ISGID) as u32) as u16 - } else { - mode as u16 - } - } - - fn allocate_next_inode(&self) -> Inode { - let path = Path::new(&self.data_dir).join("superblock"); - let current_inode = if let Ok(file) = File::open(&path) { - bincode::deserialize_from(file).unwrap() - } else { - fuser::FUSE_ROOT_ID - }; - - let file = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(&path) - .unwrap(); - bincode::serialize_into(file, &(current_inode + 1)).unwrap(); - - current_inode + 1 - } - - fn allocate_next_file_handle(&self, read: bool, write: bool) -> u64 { - let mut fh = self.next_file_handle.fetch_add(1, Ordering::SeqCst); - // Assert that we haven't run out of file handles - assert!(fh < FILE_HANDLE_READ_BIT.min(FILE_HANDLE_WRITE_BIT)); - if read { - fh |= FILE_HANDLE_READ_BIT; - } - if write { - fh |= FILE_HANDLE_WRITE_BIT; - } - - fh - } - - fn check_file_handle_read(&self, file_handle: u64) -> bool { - (file_handle & FILE_HANDLE_READ_BIT) != 0 - } - - fn check_file_handle_write(&self, file_handle: u64) -> bool { - (file_handle & FILE_HANDLE_WRITE_BIT) != 0 - } - - fn content_path(&self, inode: Inode) -> PathBuf { - Path::new(&self.data_dir) - .join("contents") - .join(inode.to_string()) - } - - fn get_directory_content(&self, inode: Inode) -> Result { - let path = Path::new(&self.data_dir) - .join("contents") - .join(inode.to_string()); - if let Ok(file) = File::open(path) { - Ok(bincode::deserialize_from(file).unwrap()) - } else { - Err(libc::ENOENT) - } - } - - fn write_directory_content(&self, inode: Inode, entries: DirectoryDescriptor) { - let path = Path::new(&self.data_dir) - .join("contents") - .join(inode.to_string()); - let file = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(path) - .unwrap(); - bincode::serialize_into(file, &entries).unwrap(); - } - - fn get_inode(&self, inode: Inode) -> Result { - let path = Path::new(&self.data_dir) - .join("inodes") - .join(inode.to_string()); - if let Ok(file) = File::open(path) { - Ok(bincode::deserialize_from(file).unwrap()) - } else { - Err(libc::ENOENT) - } - } - - fn write_inode(&self, inode: &InodeAttributes) { - let path = Path::new(&self.data_dir) - .join("inodes") - .join(inode.inode.to_string()); - let file = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(path) - .unwrap(); - bincode::serialize_into(file, inode).unwrap(); - } - - // Check whether a file should be removed from storage. Should be called after decrementing - // the link count, or closing a file handle - fn gc_inode(&self, inode: &InodeAttributes) -> bool { - if inode.hardlinks == 0 && inode.open_file_handles == 0 { - let inode_path = Path::new(&self.data_dir) - .join("inodes") - .join(inode.inode.to_string()); - fs::remove_file(inode_path).unwrap(); - let content_path = Path::new(&self.data_dir) - .join("contents") - .join(inode.inode.to_string()); - fs::remove_file(content_path).unwrap(); - - return true; - } - - return false; - } - - fn truncate( - &self, - inode: Inode, - new_length: u64, - uid: u32, - gid: u32, - ) -> Result { - if new_length > MAX_FILE_SIZE { - return Err(libc::EFBIG); - } - - let mut attrs = self.get_inode(inode)?; - - if !check_access(attrs.uid, attrs.gid, attrs.mode, uid, gid, libc::W_OK) { - return Err(libc::EACCES); - } - - let path = self.content_path(inode); - let file = OpenOptions::new().write(true).open(path).unwrap(); - file.set_len(new_length).unwrap(); - - attrs.size = new_length; - attrs.last_metadata_changed = time_now(); - attrs.last_modified = time_now(); - - // Clear SETUID & SETGID on truncate - clear_suid_sgid(&mut attrs); - - self.write_inode(&attrs); - - Ok(attrs) - } - - fn lookup_name(&self, parent: u64, name: &OsStr) -> Result { - let entries = self.get_directory_content(parent)?; - if let Some((inode, _)) = entries.get(name.as_bytes()) { - return self.get_inode(*inode); - } else { - return Err(libc::ENOENT); - } - } - - fn insert_link( - &self, - req: &Request, - parent: u64, - name: &OsStr, - inode: u64, - kind: FileKind, - ) -> Result<(), c_int> { - if self.lookup_name(parent, name).is_ok() { - return Err(libc::EEXIST); - } - - let mut parent_attrs = self.get_inode(parent)?; - - if !check_access( - parent_attrs.uid, - parent_attrs.gid, - parent_attrs.mode, - req.uid(), - req.gid(), - libc::W_OK, - ) { - return Err(libc::EACCES); - } - parent_attrs.last_modified = time_now(); - parent_attrs.last_metadata_changed = time_now(); - self.write_inode(&parent_attrs); - - let mut entries = self.get_directory_content(parent).unwrap(); - entries.insert(name.as_bytes().to_vec(), (inode, kind)); - self.write_directory_content(parent, entries); - - Ok(()) - } -} - -impl Filesystem for SimpleFS { - fn init( - &mut self, - _req: &Request, - #[allow(unused_variables)] config: &mut KernelConfig, - ) -> Result<(), c_int> { - #[cfg(feature = "abi-7-26")] - config.add_capabilities(FUSE_HANDLE_KILLPRIV).unwrap(); - - fs::create_dir_all(Path::new(&self.data_dir).join("inodes")).unwrap(); - fs::create_dir_all(Path::new(&self.data_dir).join("contents")).unwrap(); - if self.get_inode(FUSE_ROOT_ID).is_err() { - // Initialize with empty filesystem - let root = InodeAttributes { - inode: FUSE_ROOT_ID, - open_file_handles: 0, - size: 0, - last_accessed: time_now(), - last_modified: time_now(), - last_metadata_changed: time_now(), - kind: FileKind::Directory, - mode: 0o777, - hardlinks: 2, - uid: 0, - gid: 0, - xattrs: Default::default(), - }; - self.write_inode(&root); - let mut entries = BTreeMap::new(); - entries.insert(b".".to_vec(), (FUSE_ROOT_ID, FileKind::Directory)); - self.write_directory_content(FUSE_ROOT_ID, entries); - } - Ok(()) - } - - fn lookup(&mut self, req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) { - if name.len() > MAX_NAME_LENGTH as usize { - reply.error(libc::ENAMETOOLONG); - return; - } - let parent_attrs = self.get_inode(parent).unwrap(); - if !check_access( - parent_attrs.uid, - parent_attrs.gid, - parent_attrs.mode, - req.uid(), - req.gid(), - libc::X_OK, - ) { - reply.error(libc::EACCES); - return; - } - - match self.lookup_name(parent, name) { - Ok(attrs) => reply.entry(&Duration::new(0, 0), &attrs.into(), 0), - Err(error_code) => reply.error(error_code), - } - } - - fn forget(&mut self, _req: &Request, _ino: u64, _nlookup: u64) {} - - fn getattr(&mut self, _req: &Request, inode: u64, _fh: Option, reply: ReplyAttr) { - match self.get_inode(inode) { - Ok(attrs) => reply.attr(&Duration::new(0, 0), &attrs.into()), - Err(error_code) => reply.error(error_code), - } - } - - fn setattr( - &mut self, - req: &Request, - inode: u64, - mode: Option, - uid: Option, - gid: Option, - size: Option, - atime: Option, - mtime: Option, - _ctime: Option, - fh: Option, - _crtime: Option, - _chgtime: Option, - _bkuptime: Option, - _flags: Option, - reply: ReplyAttr, - ) { - let mut attrs = match self.get_inode(inode) { - Ok(attrs) => attrs, - Err(error_code) => { - reply.error(error_code); - return; - } - }; - - if let Some(mode) = mode { - debug!("chmod() called with {:?}, {:o}", inode, mode); - if req.uid() != 0 && req.uid() != attrs.uid { - reply.error(libc::EPERM); - return; - } - if req.uid() != 0 - && req.gid() != attrs.gid - && !get_groups(req.pid()).contains(&attrs.gid) - { - // If SGID is set and the file belongs to a group that the caller is not part of - // then the SGID bit is suppose to be cleared during chmod - attrs.mode = (mode & !libc::S_ISGID as u32) as u16; - } else { - attrs.mode = mode as u16; - } - attrs.last_metadata_changed = time_now(); - self.write_inode(&attrs); - reply.attr(&Duration::new(0, 0), &attrs.into()); - return; - } - - if uid.is_some() || gid.is_some() { - debug!("chown() called with {:?} {:?} {:?}", inode, uid, gid); - if let Some(gid) = gid { - // Non-root users can only change gid to a group they're in - if req.uid() != 0 && !get_groups(req.pid()).contains(&gid) { - reply.error(libc::EPERM); - return; - } - } - if let Some(uid) = uid { - if req.uid() != 0 - // but no-op changes by the owner are not an error - && !(uid == attrs.uid && req.uid() == attrs.uid) - { - reply.error(libc::EPERM); - return; - } - } - // Only owner may change the group - if gid.is_some() && req.uid() != 0 && req.uid() != attrs.uid { - reply.error(libc::EPERM); - return; - } - - if attrs.mode & (libc::S_IXUSR | libc::S_IXGRP | libc::S_IXOTH) as u16 != 0 { - // SUID & SGID are suppose to be cleared when chown'ing an executable file - clear_suid_sgid(&mut attrs); - } - - if let Some(uid) = uid { - attrs.uid = uid; - // Clear SETUID on owner change - attrs.mode &= !libc::S_ISUID as u16; - } - if let Some(gid) = gid { - attrs.gid = gid; - // Clear SETGID unless user is root - if req.uid() != 0 { - attrs.mode &= !libc::S_ISGID as u16; - } - } - attrs.last_metadata_changed = time_now(); - self.write_inode(&attrs); - reply.attr(&Duration::new(0, 0), &attrs.into()); - return; - } - - if let Some(size) = size { - debug!("truncate() called with {:?} {:?}", inode, size); - if let Some(handle) = fh { - // If the file handle is available, check access locally. - // This is important as it preserves the semantic that a file handle opened - // with W_OK will never fail to truncate, even if the file has been subsequently - // chmod'ed - if self.check_file_handle_write(handle) { - if let Err(error_code) = self.truncate(inode, size, 0, 0) { - reply.error(error_code); - return; - } - } else { - reply.error(libc::EACCES); - return; - } - } else if let Err(error_code) = self.truncate(inode, size, req.uid(), req.gid()) { - reply.error(error_code); - return; - } - } - - let now = time_now(); - if let Some(atime) = atime { - debug!("utimens() called with {:?}, atime={:?}", inode, atime); - - if attrs.uid != req.uid() && req.uid() != 0 && atime != Now { - reply.error(libc::EPERM); - return; - } - - if attrs.uid != req.uid() - && !check_access( - attrs.uid, - attrs.gid, - attrs.mode, - req.uid(), - req.gid(), - libc::W_OK, - ) - { - reply.error(libc::EACCES); - return; - } - - attrs.last_accessed = match atime { - TimeOrNow::SpecificTime(time) => time_from_system_time(&time), - Now => now, - }; - attrs.last_metadata_changed = now; - self.write_inode(&attrs); - } - if let Some(mtime) = mtime { - debug!("utimens() called with {:?}, mtime={:?}", inode, mtime); - - if attrs.uid != req.uid() && req.uid() != 0 && mtime != Now { - reply.error(libc::EPERM); - return; - } - - if attrs.uid != req.uid() - && !check_access( - attrs.uid, - attrs.gid, - attrs.mode, - req.uid(), - req.gid(), - libc::W_OK, - ) - { - reply.error(libc::EACCES); - return; - } - - attrs.last_modified = match mtime { - TimeOrNow::SpecificTime(time) => time_from_system_time(&time), - Now => now, - }; - attrs.last_metadata_changed = now; - self.write_inode(&attrs); - } - - let attrs = self.get_inode(inode).unwrap(); - reply.attr(&Duration::new(0, 0), &attrs.into()); - return; - } - - fn readlink(&mut self, _req: &Request, inode: u64, reply: ReplyData) { - debug!("readlink() called on {:?}", inode); - let path = self.content_path(inode); - if let Ok(mut file) = File::open(path) { - let file_size = file.metadata().unwrap().len(); - let mut buffer = vec![0; file_size as usize]; - file.read_exact(&mut buffer).unwrap(); - reply.data(&buffer); - } else { - reply.error(libc::ENOENT); - } - } - - fn mknod( - &mut self, - req: &Request, - parent: u64, - name: &OsStr, - mut mode: u32, - _umask: u32, - _rdev: u32, - reply: ReplyEntry, - ) { - let file_type = mode & libc::S_IFMT as u32; - - if file_type != libc::S_IFREG as u32 - && file_type != libc::S_IFLNK as u32 - && file_type != libc::S_IFDIR as u32 - { - // TODO - warn!("mknod() implementation is incomplete. Only supports regular files, symlinks, and directories. Got {:o}", mode); - reply.error(libc::ENOSYS); - return; - } - - if self.lookup_name(parent, name).is_ok() { - reply.error(libc::EEXIST); - return; - } - - let mut parent_attrs = match self.get_inode(parent) { - Ok(attrs) => attrs, - Err(error_code) => { - reply.error(error_code); - return; - } - }; - - if !check_access( - parent_attrs.uid, - parent_attrs.gid, - parent_attrs.mode, - req.uid(), - req.gid(), - libc::W_OK, - ) { - reply.error(libc::EACCES); - return; - } - parent_attrs.last_modified = time_now(); - parent_attrs.last_metadata_changed = time_now(); - self.write_inode(&parent_attrs); - - if req.uid() != 0 { - mode &= !(libc::S_ISUID | libc::S_ISGID) as u32; - } - - let inode = self.allocate_next_inode(); - let attrs = InodeAttributes { - inode, - open_file_handles: 0, - size: 0, - last_accessed: time_now(), - last_modified: time_now(), - last_metadata_changed: time_now(), - kind: as_file_kind(mode), - mode: self.creation_mode(mode), - hardlinks: 1, - uid: req.uid(), - gid: creation_gid(&parent_attrs, req.gid()), - xattrs: Default::default(), - }; - self.write_inode(&attrs); - File::create(self.content_path(inode)).unwrap(); - - if as_file_kind(mode) == FileKind::Directory { - let mut entries = BTreeMap::new(); - entries.insert(b".".to_vec(), (inode, FileKind::Directory)); - entries.insert(b"..".to_vec(), (parent, FileKind::Directory)); - self.write_directory_content(inode, entries); - } - - let mut entries = self.get_directory_content(parent).unwrap(); - entries.insert(name.as_bytes().to_vec(), (inode, attrs.kind)); - self.write_directory_content(parent, entries); - - // TODO: implement flags - reply.entry(&Duration::new(0, 0), &attrs.into(), 0); - } - - fn mkdir( - &mut self, - req: &Request, - parent: u64, - name: &OsStr, - mut mode: u32, - _umask: u32, - reply: ReplyEntry, - ) { - debug!("mkdir() called with {:?} {:?} {:o}", parent, name, mode); - if self.lookup_name(parent, name).is_ok() { - reply.error(libc::EEXIST); - return; - } - - let mut parent_attrs = match self.get_inode(parent) { - Ok(attrs) => attrs, - Err(error_code) => { - reply.error(error_code); - return; - } - }; - - if !check_access( - parent_attrs.uid, - parent_attrs.gid, - parent_attrs.mode, - req.uid(), - req.gid(), - libc::W_OK, - ) { - reply.error(libc::EACCES); - return; - } - parent_attrs.last_modified = time_now(); - parent_attrs.last_metadata_changed = time_now(); - self.write_inode(&parent_attrs); - - if req.uid() != 0 { - mode &= !(libc::S_ISUID | libc::S_ISGID) as u32; - } - if parent_attrs.mode & libc::S_ISGID as u16 != 0 { - mode |= libc::S_ISGID as u32; - } - - let inode = self.allocate_next_inode(); - let attrs = InodeAttributes { - inode, - open_file_handles: 0, - size: BLOCK_SIZE, - last_accessed: time_now(), - last_modified: time_now(), - last_metadata_changed: time_now(), - kind: FileKind::Directory, - mode: self.creation_mode(mode), - hardlinks: 2, // Directories start with link count of 2, since they have a self link - uid: req.uid(), - gid: creation_gid(&parent_attrs, req.gid()), - xattrs: Default::default(), - }; - self.write_inode(&attrs); - - let mut entries = BTreeMap::new(); - entries.insert(b".".to_vec(), (inode, FileKind::Directory)); - entries.insert(b"..".to_vec(), (parent, FileKind::Directory)); - self.write_directory_content(inode, entries); - - let mut entries = self.get_directory_content(parent).unwrap(); - entries.insert(name.as_bytes().to_vec(), (inode, FileKind::Directory)); - self.write_directory_content(parent, entries); - - reply.entry(&Duration::new(0, 0), &attrs.into(), 0); - } - - fn unlink(&mut self, req: &Request, parent: u64, name: &OsStr, reply: ReplyEmpty) { - debug!("unlink() called with {:?} {:?}", parent, name); - let mut attrs = match self.lookup_name(parent, name) { - Ok(attrs) => attrs, - Err(error_code) => { - reply.error(error_code); - return; - } - }; - - let mut parent_attrs = match self.get_inode(parent) { - Ok(attrs) => attrs, - Err(error_code) => { - reply.error(error_code); - return; - } - }; - - if !check_access( - parent_attrs.uid, - parent_attrs.gid, - parent_attrs.mode, - req.uid(), - req.gid(), - libc::W_OK, - ) { - reply.error(libc::EACCES); - return; - } - - let uid = req.uid(); - // "Sticky bit" handling - if parent_attrs.mode & libc::S_ISVTX as u16 != 0 - && uid != 0 - && uid != parent_attrs.uid - && uid != attrs.uid - { - reply.error(libc::EACCES); - return; - } - - parent_attrs.last_metadata_changed = time_now(); - parent_attrs.last_modified = time_now(); - self.write_inode(&parent_attrs); - - attrs.hardlinks -= 1; - attrs.last_metadata_changed = time_now(); - self.write_inode(&attrs); - self.gc_inode(&attrs); - - let mut entries = self.get_directory_content(parent).unwrap(); - entries.remove(name.as_bytes()); - self.write_directory_content(parent, entries); - - reply.ok(); - } - - fn rmdir(&mut self, req: &Request, parent: u64, name: &OsStr, reply: ReplyEmpty) { - debug!("rmdir() called with {:?} {:?}", parent, name); - let mut attrs = match self.lookup_name(parent, name) { - Ok(attrs) => attrs, - Err(error_code) => { - reply.error(error_code); - return; - } - }; - - let mut parent_attrs = match self.get_inode(parent) { - Ok(attrs) => attrs, - Err(error_code) => { - reply.error(error_code); - return; - } - }; - - // Directories always have a self and parent link - if self.get_directory_content(attrs.inode).unwrap().len() > 2 { - reply.error(libc::ENOTEMPTY); - return; - } - if !check_access( - parent_attrs.uid, - parent_attrs.gid, - parent_attrs.mode, - req.uid(), - req.gid(), - libc::W_OK, - ) { - reply.error(libc::EACCES); - return; - } - - // "Sticky bit" handling - if parent_attrs.mode & libc::S_ISVTX as u16 != 0 - && req.uid() != 0 - && req.uid() != parent_attrs.uid - && req.uid() != attrs.uid - { - reply.error(libc::EACCES); - return; - } - - parent_attrs.last_metadata_changed = time_now(); - parent_attrs.last_modified = time_now(); - self.write_inode(&parent_attrs); - - attrs.hardlinks = 0; - attrs.last_metadata_changed = time_now(); - self.write_inode(&attrs); - self.gc_inode(&attrs); - - let mut entries = self.get_directory_content(parent).unwrap(); - entries.remove(name.as_bytes()); - self.write_directory_content(parent, entries); - - reply.ok(); - } - - fn symlink( - &mut self, - req: &Request, - parent: u64, - link_name: &OsStr, - target: &Path, - reply: ReplyEntry, - ) { - debug!( - "symlink() called with {:?} {:?} {:?}", - parent, link_name, target - ); - let mut parent_attrs = match self.get_inode(parent) { - Ok(attrs) => attrs, - Err(error_code) => { - reply.error(error_code); - return; - } - }; - - if !check_access( - parent_attrs.uid, - parent_attrs.gid, - parent_attrs.mode, - req.uid(), - req.gid(), - libc::W_OK, - ) { - reply.error(libc::EACCES); - return; - } - parent_attrs.last_modified = time_now(); - parent_attrs.last_metadata_changed = time_now(); - self.write_inode(&parent_attrs); - - let inode = self.allocate_next_inode(); - let attrs = InodeAttributes { - inode, - open_file_handles: 0, - size: target.as_os_str().as_bytes().len() as u64, - last_accessed: time_now(), - last_modified: time_now(), - last_metadata_changed: time_now(), - kind: FileKind::Symlink, - mode: 0o777, - hardlinks: 1, - uid: req.uid(), - gid: creation_gid(&parent_attrs, req.gid()), - xattrs: Default::default(), - }; - - if let Err(error_code) = self.insert_link(req, parent, link_name, inode, FileKind::Symlink) - { - reply.error(error_code); - return; - } - self.write_inode(&attrs); - - let path = self.content_path(inode); - let mut file = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(path) - .unwrap(); - file.write_all(target.as_os_str().as_bytes()).unwrap(); - - reply.entry(&Duration::new(0, 0), &attrs.into(), 0); - } - - fn rename( - &mut self, - req: &Request, - parent: u64, - name: &OsStr, - new_parent: u64, - new_name: &OsStr, - flags: u32, - reply: ReplyEmpty, - ) { - debug!( - "rename() called with: source {parent:?} {name:?}, \ - destination {new_parent:?} {new_name:?}, flags {flags:#b}", - ); - let mut inode_attrs = match self.lookup_name(parent, name) { - Ok(attrs) => attrs, - Err(error_code) => { - reply.error(error_code); - return; - } - }; - - let mut parent_attrs = match self.get_inode(parent) { - Ok(attrs) => attrs, - Err(error_code) => { - reply.error(error_code); - return; - } - }; - - if !check_access( - parent_attrs.uid, - parent_attrs.gid, - parent_attrs.mode, - req.uid(), - req.gid(), - libc::W_OK, - ) { - reply.error(libc::EACCES); - return; - } - - // "Sticky bit" handling - if parent_attrs.mode & libc::S_ISVTX as u16 != 0 - && req.uid() != 0 - && req.uid() != parent_attrs.uid - && req.uid() != inode_attrs.uid - { - reply.error(libc::EACCES); - return; - } - - let mut new_parent_attrs = match self.get_inode(new_parent) { - Ok(attrs) => attrs, - Err(error_code) => { - reply.error(error_code); - return; - } - }; - - if !check_access( - new_parent_attrs.uid, - new_parent_attrs.gid, - new_parent_attrs.mode, - req.uid(), - req.gid(), - libc::W_OK, - ) { - reply.error(libc::EACCES); - return; - } - - // "Sticky bit" handling in new_parent - if new_parent_attrs.mode & libc::S_ISVTX as u16 != 0 { - if let Ok(existing_attrs) = self.lookup_name(new_parent, new_name) { - if req.uid() != 0 - && req.uid() != new_parent_attrs.uid - && req.uid() != existing_attrs.uid - { - reply.error(libc::EACCES); - return; - } - } - } - - #[cfg(target_os = "linux")] - if flags & libc::RENAME_EXCHANGE as u32 != 0 { - let mut new_inode_attrs = match self.lookup_name(new_parent, new_name) { - Ok(attrs) => attrs, - Err(error_code) => { - reply.error(error_code); - return; - } - }; - - let mut entries = self.get_directory_content(new_parent).unwrap(); - entries.insert( - new_name.as_bytes().to_vec(), - (inode_attrs.inode, inode_attrs.kind), - ); - self.write_directory_content(new_parent, entries); - - let mut entries = self.get_directory_content(parent).unwrap(); - entries.insert( - name.as_bytes().to_vec(), - (new_inode_attrs.inode, new_inode_attrs.kind), - ); - self.write_directory_content(parent, entries); - - parent_attrs.last_metadata_changed = time_now(); - parent_attrs.last_modified = time_now(); - self.write_inode(&parent_attrs); - new_parent_attrs.last_metadata_changed = time_now(); - new_parent_attrs.last_modified = time_now(); - self.write_inode(&new_parent_attrs); - inode_attrs.last_metadata_changed = time_now(); - self.write_inode(&inode_attrs); - new_inode_attrs.last_metadata_changed = time_now(); - self.write_inode(&new_inode_attrs); - - if inode_attrs.kind == FileKind::Directory { - let mut entries = self.get_directory_content(inode_attrs.inode).unwrap(); - entries.insert(b"..".to_vec(), (new_parent, FileKind::Directory)); - self.write_directory_content(inode_attrs.inode, entries); - } - if new_inode_attrs.kind == FileKind::Directory { - let mut entries = self.get_directory_content(new_inode_attrs.inode).unwrap(); - entries.insert(b"..".to_vec(), (parent, FileKind::Directory)); - self.write_directory_content(new_inode_attrs.inode, entries); - } - - reply.ok(); - return; - } - - // Only overwrite an existing directory if it's empty - if let Ok(new_name_attrs) = self.lookup_name(new_parent, new_name) { - if new_name_attrs.kind == FileKind::Directory - && self - .get_directory_content(new_name_attrs.inode) - .unwrap() - .len() - > 2 - { - reply.error(libc::ENOTEMPTY); - return; - } - } - - // Only move an existing directory to a new parent, if we have write access to it, - // because that will change the ".." link in it - if inode_attrs.kind == FileKind::Directory - && parent != new_parent - && !check_access( - inode_attrs.uid, - inode_attrs.gid, - inode_attrs.mode, - req.uid(), - req.gid(), - libc::W_OK, - ) - { - reply.error(libc::EACCES); - return; - } - - // If target already exists decrement its hardlink count - if let Ok(mut existing_inode_attrs) = self.lookup_name(new_parent, new_name) { - let mut entries = self.get_directory_content(new_parent).unwrap(); - entries.remove(new_name.as_bytes()); - self.write_directory_content(new_parent, entries); - - if existing_inode_attrs.kind == FileKind::Directory { - existing_inode_attrs.hardlinks = 0; - } else { - existing_inode_attrs.hardlinks -= 1; - } - existing_inode_attrs.last_metadata_changed = time_now(); - self.write_inode(&existing_inode_attrs); - self.gc_inode(&existing_inode_attrs); - } - - let mut entries = self.get_directory_content(parent).unwrap(); - entries.remove(name.as_bytes()); - self.write_directory_content(parent, entries); - - let mut entries = self.get_directory_content(new_parent).unwrap(); - entries.insert( - new_name.as_bytes().to_vec(), - (inode_attrs.inode, inode_attrs.kind), - ); - self.write_directory_content(new_parent, entries); - - parent_attrs.last_metadata_changed = time_now(); - parent_attrs.last_modified = time_now(); - self.write_inode(&parent_attrs); - new_parent_attrs.last_metadata_changed = time_now(); - new_parent_attrs.last_modified = time_now(); - self.write_inode(&new_parent_attrs); - inode_attrs.last_metadata_changed = time_now(); - self.write_inode(&inode_attrs); - - if inode_attrs.kind == FileKind::Directory { - let mut entries = self.get_directory_content(inode_attrs.inode).unwrap(); - entries.insert(b"..".to_vec(), (new_parent, FileKind::Directory)); - self.write_directory_content(inode_attrs.inode, entries); - } - - reply.ok(); - } - - fn link( - &mut self, - req: &Request, - inode: u64, - new_parent: u64, - new_name: &OsStr, - reply: ReplyEntry, - ) { - debug!( - "link() called for {}, {}, {:?}", - inode, new_parent, new_name - ); - let mut attrs = match self.get_inode(inode) { - Ok(attrs) => attrs, - Err(error_code) => { - reply.error(error_code); - return; - } - }; - if let Err(error_code) = self.insert_link(req, new_parent, new_name, inode, attrs.kind) { - reply.error(error_code); - } else { - attrs.hardlinks += 1; - attrs.last_metadata_changed = time_now(); - self.write_inode(&attrs); - reply.entry(&Duration::new(0, 0), &attrs.into(), 0); - } - } - - fn open(&mut self, req: &Request, inode: u64, flags: i32, reply: ReplyOpen) { - debug!("open() called for {:?}", inode); - let (access_mask, read, write) = match flags & libc::O_ACCMODE { - libc::O_RDONLY => { - // Behavior is undefined, but most filesystems return EACCES - if flags & libc::O_TRUNC != 0 { - reply.error(libc::EACCES); - return; - } - if flags & FMODE_EXEC != 0 { - // Open is from internal exec syscall - (libc::X_OK, true, false) - } else { - (libc::R_OK, true, false) - } - } - libc::O_WRONLY => (libc::W_OK, false, true), - libc::O_RDWR => (libc::R_OK | libc::W_OK, true, true), - // Exactly one access mode flag must be specified - _ => { - reply.error(libc::EINVAL); - return; - } - }; - - match self.get_inode(inode) { - Ok(mut attr) => { - if check_access( - attr.uid, - attr.gid, - attr.mode, - req.uid(), - req.gid(), - access_mask, - ) { - attr.open_file_handles += 1; - self.write_inode(&attr); - let open_flags = if self.direct_io { FOPEN_DIRECT_IO } else { 0 }; - reply.opened(self.allocate_next_file_handle(read, write), open_flags); - } else { - reply.error(libc::EACCES); - } - return; - } - Err(error_code) => reply.error(error_code), - } - } - - fn read( - &mut self, - _req: &Request, - inode: u64, - fh: u64, - offset: i64, - size: u32, - _flags: i32, - _lock_owner: Option, - reply: ReplyData, - ) { - debug!( - "read() called on {:?} offset={:?} size={:?}", - inode, offset, size - ); - assert!(offset >= 0); - if !self.check_file_handle_read(fh) { - reply.error(libc::EACCES); - return; - } - - let path = self.content_path(inode); - if let Ok(file) = File::open(path) { - let file_size = file.metadata().unwrap().len(); - // Could underflow if file length is less than local_start - let read_size = min(size, file_size.saturating_sub(offset as u64) as u32); - - let mut buffer = vec![0; read_size as usize]; - file.read_exact_at(&mut buffer, offset as u64).unwrap(); - reply.data(&buffer); - } else { - reply.error(libc::ENOENT); - } - } - - fn write( - &mut self, - _req: &Request, - inode: u64, - fh: u64, - offset: i64, - data: &[u8], - _write_flags: u32, - #[allow(unused_variables)] flags: i32, - _lock_owner: Option, - reply: ReplyWrite, - ) { - debug!("write() called with {:?} size={:?}", inode, data.len()); - assert!(offset >= 0); - if !self.check_file_handle_write(fh) { - reply.error(libc::EACCES); - return; - } - - let path = self.content_path(inode); - if let Ok(mut file) = OpenOptions::new().write(true).open(path) { - file.seek(SeekFrom::Start(offset as u64)).unwrap(); - file.write_all(data).unwrap(); - - let mut attrs = self.get_inode(inode).unwrap(); - attrs.last_metadata_changed = time_now(); - attrs.last_modified = time_now(); - if data.len() + offset as usize > attrs.size as usize { - attrs.size = (data.len() + offset as usize) as u64; - } - // #[cfg(feature = "abi-7-31")] - // if flags & FUSE_WRITE_KILL_PRIV as i32 != 0 { - // clear_suid_sgid(&mut attrs); - // } - // XXX: In theory we should only need to do this when WRITE_KILL_PRIV is set for 7.31+ - // However, xfstests fail in that case - clear_suid_sgid(&mut attrs); - self.write_inode(&attrs); - - reply.written(data.len() as u32); - } else { - reply.error(libc::EBADF); - } - } - - fn release( - &mut self, - _req: &Request<'_>, - inode: u64, - _fh: u64, - _flags: i32, - _lock_owner: Option, - _flush: bool, - reply: ReplyEmpty, - ) { - if let Ok(mut attrs) = self.get_inode(inode) { - attrs.open_file_handles -= 1; - } - reply.ok(); - } - - fn opendir(&mut self, req: &Request, inode: u64, flags: i32, reply: ReplyOpen) { - debug!("opendir() called on {:?}", inode); - let (access_mask, read, write) = match flags & libc::O_ACCMODE { - libc::O_RDONLY => { - // Behavior is undefined, but most filesystems return EACCES - if flags & libc::O_TRUNC != 0 { - reply.error(libc::EACCES); - return; - } - (libc::R_OK, true, false) - } - libc::O_WRONLY => (libc::W_OK, false, true), - libc::O_RDWR => (libc::R_OK | libc::W_OK, true, true), - // Exactly one access mode flag must be specified - _ => { - reply.error(libc::EINVAL); - return; - } - }; - - match self.get_inode(inode) { - Ok(mut attr) => { - if check_access( - attr.uid, - attr.gid, - attr.mode, - req.uid(), - req.gid(), - access_mask, - ) { - attr.open_file_handles += 1; - self.write_inode(&attr); - let open_flags = if self.direct_io { FOPEN_DIRECT_IO } else { 0 }; - reply.opened(self.allocate_next_file_handle(read, write), open_flags); - } else { - reply.error(libc::EACCES); - } - return; - } - Err(error_code) => reply.error(error_code), - } - } - - fn readdir( - &mut self, - _req: &Request, - inode: u64, - _fh: u64, - offset: i64, - mut reply: ReplyDirectory, - ) { - debug!("readdir() called with {:?}", inode); - assert!(offset >= 0); - let entries = match self.get_directory_content(inode) { - Ok(entries) => entries, - Err(error_code) => { - reply.error(error_code); - return; - } - }; - - for (index, entry) in entries.iter().skip(offset as usize).enumerate() { - let (name, (inode, file_type)) = entry; - - let buffer_full: bool = reply.add( - *inode, - offset + index as i64 + 1, - (*file_type).into(), - OsStr::from_bytes(name), - ); - - if buffer_full { - break; - } - } - - reply.ok(); - } - - fn releasedir( - &mut self, - _req: &Request<'_>, - inode: u64, - _fh: u64, - _flags: i32, - reply: ReplyEmpty, - ) { - if let Ok(mut attrs) = self.get_inode(inode) { - attrs.open_file_handles -= 1; - } - reply.ok(); - } - - fn statfs(&mut self, _req: &Request, _ino: u64, reply: ReplyStatfs) { - warn!("statfs() implementation is a stub"); - // TODO: real implementation of this - reply.statfs( - 10_000, - 10_000, - 10_000, - 1, - 10_000, - BLOCK_SIZE as u32, - MAX_NAME_LENGTH, - BLOCK_SIZE as u32, - ); - } - - fn setxattr( - &mut self, - request: &Request<'_>, - inode: u64, - key: &OsStr, - value: &[u8], - _flags: i32, - _position: u32, - reply: ReplyEmpty, - ) { - if let Ok(mut attrs) = self.get_inode(inode) { - if let Err(error) = xattr_access_check(key.as_bytes(), libc::W_OK, &attrs, request) { - reply.error(error); - return; - } - - attrs.xattrs.insert(key.as_bytes().to_vec(), value.to_vec()); - attrs.last_metadata_changed = time_now(); - self.write_inode(&attrs); - reply.ok(); - } else { - reply.error(libc::EBADF); - } - } - - fn getxattr( - &mut self, - request: &Request<'_>, - inode: u64, - key: &OsStr, - size: u32, - reply: ReplyXattr, - ) { - if let Ok(attrs) = self.get_inode(inode) { - if let Err(error) = xattr_access_check(key.as_bytes(), libc::R_OK, &attrs, request) { - reply.error(error); - return; - } - - if let Some(data) = attrs.xattrs.get(key.as_bytes()) { - if size == 0 { - reply.size(data.len() as u32); - } else if data.len() <= size as usize { - reply.data(data); - } else { - reply.error(libc::ERANGE); - } - } else { - #[cfg(target_os = "linux")] - reply.error(libc::ENODATA); - #[cfg(not(target_os = "linux"))] - reply.error(libc::ENOATTR); - } - } else { - reply.error(libc::EBADF); - } - } - - fn listxattr(&mut self, _req: &Request<'_>, inode: u64, size: u32, reply: ReplyXattr) { - if let Ok(attrs) = self.get_inode(inode) { - let mut bytes = vec![]; - // Convert to concatenated null-terminated strings - for key in attrs.xattrs.keys() { - bytes.extend(key); - bytes.push(0); - } - if size == 0 { - reply.size(bytes.len() as u32); - } else if bytes.len() <= size as usize { - reply.data(&bytes); - } else { - reply.error(libc::ERANGE); - } - } else { - reply.error(libc::EBADF); - } - } - - fn removexattr(&mut self, request: &Request<'_>, inode: u64, key: &OsStr, reply: ReplyEmpty) { - if let Ok(mut attrs) = self.get_inode(inode) { - if let Err(error) = xattr_access_check(key.as_bytes(), libc::W_OK, &attrs, request) { - reply.error(error); - return; - } - - if attrs.xattrs.remove(key.as_bytes()).is_none() { - #[cfg(target_os = "linux")] - reply.error(libc::ENODATA); - #[cfg(not(target_os = "linux"))] - reply.error(libc::ENOATTR); - return; - } - attrs.last_metadata_changed = time_now(); - self.write_inode(&attrs); - reply.ok(); - } else { - reply.error(libc::EBADF); - } - } - - fn access(&mut self, req: &Request, inode: u64, mask: i32, reply: ReplyEmpty) { - debug!("access() called with {:?} {:?}", inode, mask); - match self.get_inode(inode) { - Ok(attr) => { - if check_access(attr.uid, attr.gid, attr.mode, req.uid(), req.gid(), mask) { - reply.ok(); - } else { - reply.error(libc::EACCES); - } - } - Err(error_code) => reply.error(error_code), - } - } - - fn create( - &mut self, - req: &Request, - parent: u64, - name: &OsStr, - mut mode: u32, - _umask: u32, - flags: i32, - reply: ReplyCreate, - ) { - debug!("create() called with {:?} {:?}", parent, name); - if self.lookup_name(parent, name).is_ok() { - reply.error(libc::EEXIST); - return; - } - - let (read, write) = match flags & libc::O_ACCMODE { - libc::O_RDONLY => (true, false), - libc::O_WRONLY => (false, true), - libc::O_RDWR => (true, true), - // Exactly one access mode flag must be specified - _ => { - reply.error(libc::EINVAL); - return; - } - }; - - let mut parent_attrs = match self.get_inode(parent) { - Ok(attrs) => attrs, - Err(error_code) => { - reply.error(error_code); - return; - } - }; - - if !check_access( - parent_attrs.uid, - parent_attrs.gid, - parent_attrs.mode, - req.uid(), - req.gid(), - libc::W_OK, - ) { - reply.error(libc::EACCES); - return; - } - parent_attrs.last_modified = time_now(); - parent_attrs.last_metadata_changed = time_now(); - self.write_inode(&parent_attrs); - - if req.uid() != 0 { - mode &= !(libc::S_ISUID | libc::S_ISGID) as u32; - } - - let inode = self.allocate_next_inode(); - let attrs = InodeAttributes { - inode, - open_file_handles: 1, - size: 0, - last_accessed: time_now(), - last_modified: time_now(), - last_metadata_changed: time_now(), - kind: as_file_kind(mode), - mode: self.creation_mode(mode), - hardlinks: 1, - uid: req.uid(), - gid: creation_gid(&parent_attrs, req.gid()), - xattrs: Default::default(), - }; - self.write_inode(&attrs); - File::create(self.content_path(inode)).unwrap(); - - if as_file_kind(mode) == FileKind::Directory { - let mut entries = BTreeMap::new(); - entries.insert(b".".to_vec(), (inode, FileKind::Directory)); - entries.insert(b"..".to_vec(), (parent, FileKind::Directory)); - self.write_directory_content(inode, entries); - } - - let mut entries = self.get_directory_content(parent).unwrap(); - entries.insert(name.as_bytes().to_vec(), (inode, attrs.kind)); - self.write_directory_content(parent, entries); - - // TODO: implement flags - reply.created( - &Duration::new(0, 0), - &attrs.into(), - 0, - self.allocate_next_file_handle(read, write), - 0, - ); - } - - #[cfg(target_os = "linux")] - fn fallocate( - &mut self, - _req: &Request<'_>, - inode: u64, - _fh: u64, - offset: i64, - length: i64, - mode: i32, - reply: ReplyEmpty, - ) { - let path = self.content_path(inode); - if let Ok(file) = OpenOptions::new().write(true).open(path) { - unsafe { - libc::fallocate64(file.into_raw_fd(), mode, offset, length); - } - if mode & libc::FALLOC_FL_KEEP_SIZE == 0 { - let mut attrs = self.get_inode(inode).unwrap(); - attrs.last_metadata_changed = time_now(); - attrs.last_modified = time_now(); - if (offset + length) as u64 > attrs.size { - attrs.size = (offset + length) as u64; - } - self.write_inode(&attrs); - } - reply.ok(); - } else { - reply.error(libc::ENOENT); - } - } - - fn copy_file_range( - &mut self, - _req: &Request<'_>, - src_inode: u64, - src_fh: u64, - src_offset: i64, - dest_inode: u64, - dest_fh: u64, - dest_offset: i64, - size: u64, - _flags: u32, - reply: ReplyWrite, - ) { - debug!( - "copy_file_range() called with src ({}, {}, {}) dest ({}, {}, {}) size={}", - src_fh, src_inode, src_offset, dest_fh, dest_inode, dest_offset, size - ); - if !self.check_file_handle_read(src_fh) { - reply.error(libc::EACCES); - return; - } - if !self.check_file_handle_write(dest_fh) { - reply.error(libc::EACCES); - return; - } - - let src_path = self.content_path(src_inode); - if let Ok(file) = File::open(src_path) { - let file_size = file.metadata().unwrap().len(); - // Could underflow if file length is less than local_start - let read_size = min(size, file_size.saturating_sub(src_offset as u64)); - - let mut data = vec![0; read_size as usize]; - file.read_exact_at(&mut data, src_offset as u64).unwrap(); - - let dest_path = self.content_path(dest_inode); - if let Ok(mut file) = OpenOptions::new().write(true).open(dest_path) { - file.seek(SeekFrom::Start(dest_offset as u64)).unwrap(); - file.write_all(&data).unwrap(); - - let mut attrs = self.get_inode(dest_inode).unwrap(); - attrs.last_metadata_changed = time_now(); - attrs.last_modified = time_now(); - if data.len() + dest_offset as usize > attrs.size as usize { - attrs.size = (data.len() + dest_offset as usize) as u64; - } - self.write_inode(&attrs); - - reply.written(data.len() as u32); - } else { - reply.error(libc::EBADF); - } - } else { - reply.error(libc::ENOENT); - } - } -} - -pub fn check_access( - file_uid: u32, - file_gid: u32, - file_mode: u16, - uid: u32, - gid: u32, - mut access_mask: i32, -) -> bool { - // F_OK tests for existence of file - if access_mask == libc::F_OK { - return true; - } - let file_mode = i32::from(file_mode); - - // root is allowed to read & write anything - if uid == 0 { - // root only allowed to exec if one of the X bits is set - access_mask &= libc::X_OK; - access_mask -= access_mask & (file_mode >> 6); - access_mask -= access_mask & (file_mode >> 3); - access_mask -= access_mask & file_mode; - return access_mask == 0; - } - - if uid == file_uid { - access_mask -= access_mask & (file_mode >> 6); - } else if gid == file_gid { - access_mask -= access_mask & (file_mode >> 3); - } else { - access_mask -= access_mask & file_mode; - } - - return access_mask == 0; -} - -fn as_file_kind(mut mode: u32) -> FileKind { - mode &= libc::S_IFMT as u32; - - if mode == libc::S_IFREG as u32 { - return FileKind::File; - } else if mode == libc::S_IFLNK as u32 { - return FileKind::Symlink; - } else if mode == libc::S_IFDIR as u32 { - return FileKind::Directory; - } else { - unimplemented!("{}", mode); - } -} - -fn get_groups(pid: u32) -> Vec { - if cfg!(not(target_os = "macos")) { - let path = format!("/proc/{pid}/task/{pid}/status"); - let file = File::open(path).unwrap(); - for line in BufReader::new(file).lines() { - let line = line.unwrap(); - if line.starts_with("Groups:") { - return line["Groups: ".len()..] - .split(' ') - .filter(|x| !x.trim().is_empty()) - .map(|x| x.parse::().unwrap()) - .collect(); - } - } - } - - vec![] -} - -fn fuse_allow_other_enabled() -> io::Result { - let file = File::open("/etc/fuse.conf")?; - for line in BufReader::new(file).lines() { - if line?.trim_start().starts_with("user_allow_other") { - return Ok(true); - } - } - Ok(false) -} - -fn main() { - let matches = Command::new("Fuser") - .version(crate_version!()) - .author("Christopher Berner") - .arg( - Arg::new("data-dir") - .long("data-dir") - .value_name("DIR") - .default_value("/tmp/fuser") - .help("Set local directory used to store data"), - ) - .arg( - Arg::new("mount-point") - .long("mount-point") - .value_name("MOUNT_POINT") - .default_value("") - .help("Act as a client, and mount FUSE at given path"), - ) - .arg( - Arg::new("direct-io") - .long("direct-io") - .action(ArgAction::SetTrue) - .requires("mount-point") - .help("Mount FUSE with direct IO"), - ) - .arg( - Arg::new("fsck") - .long("fsck") - .action(ArgAction::SetTrue) - .help("Run a filesystem check"), - ) - .arg( - Arg::new("suid") - .long("suid") - .action(ArgAction::SetTrue) - .help("Enable setuid support when run as root"), - ) - .arg( - Arg::new("v") - .short('v') - .action(ArgAction::Count) - .help("Sets the level of verbosity"), - ) - .get_matches(); - - let verbosity = matches.get_count("v"); - let log_level = match verbosity { - 0 => LevelFilter::Error, - 1 => LevelFilter::Warn, - 2 => LevelFilter::Info, - 3 => LevelFilter::Debug, - _ => LevelFilter::Trace, - }; - env_logger::builder() - .format_timestamp_nanos() - .filter_level(log_level) - .init(); - - let mut options = vec![MountOption::FSName("fuser".to_string())]; - - #[cfg(feature = "abi-7-26")] - { - if matches.get_flag("suid") { - info!("setuid bit support enabled"); - options.push(MountOption::Suid); - } else { - options.push(MountOption::AutoUnmount); - } - } - #[cfg(not(feature = "abi-7-26"))] - { - options.push(MountOption::AutoUnmount); - } - if let Ok(enabled) = fuse_allow_other_enabled() { - if enabled { - options.push(MountOption::AllowOther); - } - } else { - eprintln!("Unable to read /etc/fuse.conf"); - } - - let data_dir = matches.get_one::("data-dir").unwrap().to_string(); - - let mountpoint: String = matches - .get_one::("mount-point") - .unwrap() - .to_string(); - - let result = fuser::mount2( - SimpleFS::new( - data_dir, - matches.get_flag("direct-io"), - matches.get_flag("suid"), - ), - mountpoint, - &options, - ); - if let Err(e) = result { - // Return a special error code for permission denied, which usually indicates that - // "user_allow_other" is missing from /etc/fuse.conf - if e.kind() == ErrorKind::PermissionDenied { - error!("{}", e.to_string()); - std::process::exit(2); - } - } -} diff --git a/vendor/fuser/mount_tests.Dockerfile b/vendor/fuser/mount_tests.Dockerfile deleted file mode 100644 index b82d50e14..000000000 --- a/vendor/fuser/mount_tests.Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM ubuntu:20.04 - -ENV DEBIAN_FRONTEND=noninteractive - -RUN apt update && apt install -y build-essential curl - -ADD rust-toolchain /code/fuser/rust-toolchain - -RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain=$(cat /code/fuser/rust-toolchain) - -ENV PATH=/root/.cargo/bin:$PATH - -ADD . /code/fuser/ diff --git a/vendor/fuser/mount_tests.sh b/vendor/fuser/mount_tests.sh deleted file mode 100755 index ef9994597..000000000 --- a/vendor/fuser/mount_tests.sh +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env bash - -set -x - -exit_handler() { - exit "${TEST_EXIT_STATUS:-1}" -} -trap exit_handler TERM -trap 'kill $(jobs -p); exit $TEST_EXIT_STATUS' INT EXIT - -export RUST_BACKTRACE=1 - -NC="\e[39m" -GREEN="\e[32m" -RED="\e[31m" - -function run_allow_root_test { - useradd fusertest1 - useradd fusertest2 - DIR=$(su fusertest1 -c "mktemp --directory") - cargo build --example hello --features libfuse,abi-7-30 > /dev/null 2>&1 - su fusertest1 -c "target/debug/examples/hello $DIR --allow-root" & - FUSE_PID=$! - sleep 2 - - echo "mounting at $DIR" - # Make sure FUSE was successfully mounted - mount | grep hello || exit 1 - - if [[ $(su root -c "cat ${DIR}/hello.txt") = "Hello World!" ]]; then - echo -e "$GREEN OK root can read $NC" - else - echo -e "$RED FAILED root can't read $NC" - export TEST_EXIT_STATUS=1 - exit 1 - fi - - if [[ $(su fusertest1 -c "cat ${DIR}/hello.txt") = "Hello World!" ]]; then - echo -e "$GREEN OK owner can read $NC" - else - echo -e "$RED FAILED owner can't read $NC" - export TEST_EXIT_STATUS=1 - exit 1 - fi - - if [[ $(su fusertest2 -c "cat ${DIR}/hello.txt") = "Hello World!" ]]; then - echo -e "$RED FAILED other user can read $NC" - export TEST_EXIT_STATUS=1 - exit 1 - else - echo -e "$GREEN OK other user can't read $NC" - fi - - kill $FUSE_PID - wait $FUSE_PID -} - -function test_no_user_allow_other { - sed -i '/user_allow_other/d' /etc/fuse.conf - - useradd fusertestnoallow - DIR=$(su fusertestnoallow -c "mktemp --directory") - DATA_DIR=$(su fusertestnoallow -c "mktemp --directory") - cargo build --example simple $1 > /dev/null 2>&1 - su fusertestnoallow -c "target/debug/examples/simple -vvv --data-dir $DATA_DIR --mount-point $DIR" - exitCode=$? - if [[ $exitCode -eq 2 ]]; then - echo -e "$GREEN OK Detected lack of user_allow_other: $2 $NC" - else - echo -e "$RED FAILED Did not detect lack of user_allow_other: $2 $NC" - export TEST_EXIT_STATUS=1 - exit 1 - fi - - # Make sure the FUSE mount did not mount - if [[ $(mount | grep hello) ]]; then - umount $DIR - echo -e "$RED FAILED Mount exists: $2 $NC" - export TEST_EXIT_STATUS=1 - exit 1 - else - echo -e "$GREEN OK Mount does not exist: $2 $NC" - fi - - # Restore fuse.conf - echo 'user_allow_other' >> /etc/fuse.conf -} - -function run_test { - DIR=$(mktemp --directory) - cargo build --example hello $1 > /dev/null 2>&1 - cargo run --example hello $1 -- $DIR $3 & - FUSE_PID=$! - sleep 2 - - echo "mounting at $DIR" - # Make sure FUSE was successfully mounted - mount | grep hello || exit 1 - - if [[ $(cat ${DIR}/hello.txt) = "Hello World!" ]]; then - echo -e "$GREEN OK $2 $3 $NC" - else - echo -e "$RED FAILED $2 $3 $NC" - export TEST_EXIT_STATUS=1 - exit 1 - fi - - kill $FUSE_PID - wait $FUSE_PID - - if [[ "$3" == "--auto_unmount" ]]; then - # Make sure the FUSE mount automatically unmounted - if [[ $(mount | grep hello) ]]; then - echo -e "$RED FAILED Mount not cleaned up: $2 $3 $NC" - export TEST_EXIT_STATUS=1 - exit 1 - else - echo -e "$GREEN OK Mount cleaned up: $2 $3 $NC" - fi - else - umount $DIR - fi -} - -apt update -apt install -y fuse -echo 'user_allow_other' >> /etc/fuse.conf - -run_test --no-default-features 'without libfuse, with fusermount' -run_test --no-default-features 'without libfuse, with fusermount' --auto_unmount -test_no_user_allow_other --no-default-features 'without libfuse, with fusermount' - -apt remove --purge -y fuse -apt autoremove -y -apt install -y fuse3 -echo 'user_allow_other' >> /etc/fuse.conf - -run_test --no-default-features 'without libfuse, with fusermount3' -run_test --no-default-features 'without libfuse, with fusermount3' --auto_unmount -test_no_user_allow_other --no-default-features 'without libfuse, with fusermount3' - -apt remove --purge -y fuse3 -apt autoremove -y -apt install -y libfuse-dev pkg-config fuse -echo 'user_allow_other' >> /etc/fuse.conf - -run_test --features=libfuse 'with libfuse' -run_test --features=libfuse 'with libfuse' --auto_unmount - -apt remove --purge -y libfuse-dev fuse -apt autoremove -y -apt install -y libfuse3-dev fuse3 -echo 'user_allow_other' >> /etc/fuse.conf - -run_test --features=libfuse,abi-7-30 'with libfuse3' -run_test --features=libfuse,abi-7-30 'with libfuse3' --auto_unmount - -run_allow_root_test - -export TEST_EXIT_STATUS=0 diff --git a/vendor/fuser/osx_mount_tests.sh b/vendor/fuser/osx_mount_tests.sh deleted file mode 100755 index f03465e10..000000000 --- a/vendor/fuser/osx_mount_tests.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash - -set -x - -exit_handler() { - exit "${TEST_EXIT_STATUS:-1}" -} -trap exit_handler TERM -trap 'kill $(jobs -p); exit $TEST_EXIT_STATUS' INT EXIT - -export RUST_BACKTRACE=1 - -NC="\e[39m" -GREEN="\e[32m" -RED="\e[31m" - -function run_test { - DIR=$(mktemp -d) - cargo build --example hello $1 > /dev/null 2>&1 - cargo run --example hello $1 -- $DIR $3 & - FUSE_PID=$! - sleep 2 - - echo "mounting at $DIR" - # Make sure FUSE was successfully mounted - mount | grep hello || exit 1 - - if [[ $(cat ${DIR}/hello.txt) = "Hello World!" ]]; then - echo -e "$GREEN OK $2 $3 $NC" - else - echo -e "$RED FAILED $2 $3 $NC" - export TEST_EXIT_STATUS=1 - exit 1 - fi - - kill $FUSE_PID - wait $FUSE_PID -} - -run_test --features=libfuse 'with libfuse' - -# TODO: re-enable this test. It seems to hang on OSX -#run_test --features=libfuse 'with libfuse' --auto_unmount - -export TEST_EXIT_STATUS=0 diff --git a/vendor/fuser/pjdfs.Dockerfile b/vendor/fuser/pjdfs.Dockerfile deleted file mode 100644 index 08e303667..000000000 --- a/vendor/fuser/pjdfs.Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -FROM ubuntu:20.04 - -ARG DEBIAN_FRONTEND=noninteractive - -RUN apt update && apt install -y git build-essential autoconf curl cmake libfuse-dev pkg-config fuse3 bc libtool \ - uuid-dev xfslibs-dev libattr1-dev libacl1-dev libaio-dev attr acl quota bsdmainutils dbench psmisc libfuse3-dev - -RUN adduser --disabled-password --gecos '' fsgqa - -RUN echo 'user_allow_other' >> /etc/fuse.conf - -RUN mkdir -p /code/pjdfstest && cd /code && git clone https://github.com/fleetfs/pjdfstest && cd pjdfstest \ - && git checkout d3beed6f5f15c204a8af3df2f518241931a42e94 && autoreconf -ifs && ./configure && make pjdfstest - -ADD rust-toolchain /code/fuser/rust-toolchain - -RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain=$(cat /code/fuser/rust-toolchain) - -ENV PATH=/root/.cargo/bin:$PATH -ARG BUILD_FEATURES - -ADD . /code/fuser/ - -RUN cd /code/fuser && cargo build --release --examples $BUILD_FEATURES && cp target/release/examples/simple /bin/fuser diff --git a/vendor/fuser/pjdfs.sh b/vendor/fuser/pjdfs.sh deleted file mode 100755 index 4187faf0b..000000000 --- a/vendor/fuser/pjdfs.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash - -set -ex - -exit_handler() { - exit "$PJDFS_EXIT_STATUS" -} -trap exit_handler TERM -trap "kill 0" INT EXIT - -export RUST_BACKTRACE=1 - -DATA_DIR=$(mktemp --directory) -DIR=$(mktemp --directory) - -fuser -vvv --suid --data-dir $DATA_DIR --mount-point $DIR > /code/logs/mount.log 2>&1 & -FUSE_PID=$! -sleep 0.5 - -echo "mounting at $DIR" -# Make sure FUSE was successfully mounted -mount | grep fuser - -set +e -cd ${DIR} -prove -rf /code/pjdfstest/tests | tee /code/logs/pjdfs.log -export PJDFS_EXIT_STATUS=${PIPESTATUS[0]} -echo "Total failed:" -cat /code/logs/pjdfs.log | egrep -o 'Failed: [0-9]+' | egrep -o '[0-9]+' | paste -s -d+ | bc - -rm -rf ${DATA_DIR} - -kill $FUSE_PID -wait $FUSE_PID diff --git a/vendor/fuser/rust-toolchain b/vendor/fuser/rust-toolchain deleted file mode 100644 index bc8a6589c..000000000 --- a/vendor/fuser/rust-toolchain +++ /dev/null @@ -1 +0,0 @@ -1.74 diff --git a/vendor/fuser/rustfmt.toml b/vendor/fuser/rustfmt.toml deleted file mode 100644 index 758d4179d..000000000 --- a/vendor/fuser/rustfmt.toml +++ /dev/null @@ -1 +0,0 @@ -max_width = 100 diff --git a/vendor/fuser/simplefs_tests.sh b/vendor/fuser/simplefs_tests.sh deleted file mode 100755 index 136b7fdbb..000000000 --- a/vendor/fuser/simplefs_tests.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -x - -exit_handler() { - exit "${TEST_EXIT_STATUS:-1}" -} -trap exit_handler TERM -trap 'kill $(jobs -p); exit $TEST_EXIT_STATUS' INT EXIT - -export RUST_BACKTRACE=1 - -NC="\e[39m" -GREEN="\e[32m" -RED="\e[31m" - -apt update -apt install -y fuse3 -echo 'user_allow_other' >> /etc/fuse.conf - -DATA_DIR=$(mktemp --directory) -DIR=$(mktemp --directory) -cargo build --example simple --no-default-features > /dev/null 2>&1 -cargo run --example simple --no-default-features -- -vvv --data-dir $DATA_DIR --mount-point $DIR 2>&1 & -FUSE_PID=$! -sleep 2 - -echo "mounting at $DIR" -# Make sure FUSE was successfully mounted -mount | grep fuser || exit 1 - -if touch $DIR/a && touch $DIR/b; then - echo -e "$GREEN OK touch file $NC" -else - echo -e "$RED FAILED touch file $NC" - export TEST_EXIT_STATUS=1 - exit 1 -fi - -umount $DIR - -kill $FUSE_PID -wait $FUSE_PID - - -export TEST_EXIT_STATUS=0 diff --git a/vendor/fuser/src/channel.rs b/vendor/fuser/src/channel.rs deleted file mode 100644 index fae35afb7..000000000 --- a/vendor/fuser/src/channel.rs +++ /dev/null @@ -1,78 +0,0 @@ -use std::{ - fs::File, - io, - os::{ - fd::{AsFd, BorrowedFd}, - unix::prelude::AsRawFd, - }, - sync::Arc, -}; - -use libc::{c_int, c_void, size_t}; - -use crate::reply::ReplySender; - -/// A raw communication channel to the FUSE kernel driver -#[derive(Debug)] -pub struct Channel(Arc); - -impl AsFd for Channel { - fn as_fd(&self) -> BorrowedFd<'_> { - self.0.as_fd() - } -} - -impl Channel { - /// Create a new communication channel to the kernel driver by mounting the - /// given path. The kernel driver will delegate filesystem operations of - /// the given path to the channel. - pub(crate) fn new(device: Arc) -> Self { - Self(device) - } - - /// Receives data up to the capacity of the given buffer (can block). - pub fn receive(&self, buffer: &mut [u8]) -> io::Result { - let rc = unsafe { - libc::read( - self.0.as_raw_fd(), - buffer.as_ptr() as *mut c_void, - buffer.len() as size_t, - ) - }; - if rc < 0 { - Err(io::Error::last_os_error()) - } else { - Ok(rc as usize) - } - } - - /// Returns a sender object for this channel. The sender object can be - /// used to send to the channel. Multiple sender objects can be used - /// and they can safely be sent to other threads. - pub fn sender(&self) -> ChannelSender { - // Since write/writev syscalls are threadsafe, we can simply create - // a sender by using the same file and use it in other threads. - ChannelSender(self.0.clone()) - } -} - -#[derive(Clone, Debug)] -pub struct ChannelSender(Arc); - -impl ReplySender for ChannelSender { - fn send(&self, bufs: &[io::IoSlice<'_>]) -> io::Result<()> { - let rc = unsafe { - libc::writev( - self.0.as_raw_fd(), - bufs.as_ptr() as *const libc::iovec, - bufs.len() as c_int, - ) - }; - if rc < 0 { - Err(io::Error::last_os_error()) - } else { - debug_assert_eq!(bufs.iter().map(|b| b.len()).sum::(), rc as usize); - Ok(()) - } - } -} diff --git a/vendor/fuser/src/lib.rs b/vendor/fuser/src/lib.rs deleted file mode 100644 index 0fca009d0..000000000 --- a/vendor/fuser/src/lib.rs +++ /dev/null @@ -1,1056 +0,0 @@ -//! FUSE userspace library implementation -//! -//! This is an improved rewrite of the FUSE userspace library (lowlevel interface) to fully take -//! advantage of Rust's architecture. The only thing we rely on in the real libfuse are mount -//! and unmount calls which are needed to establish a fd to talk to the kernel driver. - -#![warn(missing_docs, missing_debug_implementations, rust_2018_idioms)] - -use libc::{c_int, ENOSYS, EPERM}; -use log::{debug, warn}; -use mnt::mount_options::parse_options_from_args; -#[cfg(feature = "serializable")] -use serde::{Deserialize, Serialize}; -use std::ffi::OsStr; -use std::io; -use std::path::Path; -#[cfg(feature = "abi-7-23")] -use std::time::Duration; -use std::time::SystemTime; -use std::{convert::AsRef, io::ErrorKind}; - -use crate::ll::fuse_abi::consts::*; -pub use crate::ll::fuse_abi::FUSE_ROOT_ID; -pub use crate::ll::{fuse_abi::consts, TimeOrNow}; -use crate::mnt::mount_options::check_option_conflicts; -use crate::session::MAX_WRITE_SIZE; -#[cfg(feature = "abi-7-16")] -pub use ll::fuse_abi::fuse_forget_one; -pub use mnt::mount_options::MountOption; -#[cfg(feature = "abi-7-11")] -pub use notify::{Notifier, PollHandle}; -#[cfg(feature = "abi-7-11")] -pub use reply::ReplyPoll; -#[cfg(target_os = "macos")] -pub use reply::ReplyXTimes; -pub use reply::ReplyXattr; -pub use reply::{Reply, ReplyAttr, ReplyData, ReplyEmpty, ReplyEntry, ReplyOpen}; -pub use reply::{ - ReplyBmap, ReplyCreate, ReplyDirectory, ReplyDirectoryPlus, ReplyIoctl, ReplyLock, ReplyLseek, - ReplyStatfs, ReplyWrite, -}; -pub use request::Request; -pub use session::{BackgroundSession, Session, SessionACL, SessionUnmounter}; -#[cfg(feature = "abi-7-28")] -use std::cmp::max; -#[cfg(feature = "abi-7-13")] -use std::cmp::min; - -mod channel; -mod ll; -mod mnt; -#[cfg(feature = "abi-7-11")] -mod notify; -mod reply; -mod request; -mod session; - -/// We generally support async reads -#[cfg(all(not(target_os = "macos"), not(feature = "abi-7-10")))] -const INIT_FLAGS: u32 = FUSE_ASYNC_READ; -#[cfg(all(not(target_os = "macos"), feature = "abi-7-10"))] -const INIT_FLAGS: u32 = FUSE_ASYNC_READ | FUSE_BIG_WRITES; -// TODO: Add FUSE_EXPORT_SUPPORT - -/// On macOS, we additionally support case insensitiveness, volume renames and xtimes -/// TODO: we should eventually let the filesystem implementation decide which flags to set -#[cfg(target_os = "macos")] -const INIT_FLAGS: u32 = FUSE_ASYNC_READ | FUSE_CASE_INSENSITIVE | FUSE_VOL_RENAME | FUSE_XTIMES; -// TODO: Add FUSE_EXPORT_SUPPORT and FUSE_BIG_WRITES (requires ABI 7.10) - -const fn default_init_flags(#[allow(unused_variables)] capabilities: u32) -> u32 { - #[cfg(not(feature = "abi-7-28"))] - { - INIT_FLAGS - } - - #[cfg(feature = "abi-7-28")] - { - let mut flags = INIT_FLAGS; - if capabilities & FUSE_MAX_PAGES != 0 { - flags |= FUSE_MAX_PAGES; - } - flags - } -} - -/// File types -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -#[cfg_attr(feature = "serializable", derive(Serialize, Deserialize))] -pub enum FileType { - /// Named pipe (S_IFIFO) - NamedPipe, - /// Character device (S_IFCHR) - CharDevice, - /// Block device (S_IFBLK) - BlockDevice, - /// Directory (S_IFDIR) - Directory, - /// Regular file (S_IFREG) - RegularFile, - /// Symbolic link (S_IFLNK) - Symlink, - /// Unix domain socket (S_IFSOCK) - Socket, -} - -/// File attributes -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[cfg_attr(feature = "serializable", derive(Serialize, Deserialize))] -pub struct FileAttr { - /// Inode number - pub ino: u64, - /// Size in bytes - pub size: u64, - /// Size in blocks - pub blocks: u64, - /// Time of last access - pub atime: SystemTime, - /// Time of last modification - pub mtime: SystemTime, - /// Time of last change - pub ctime: SystemTime, - /// Time of creation (macOS only) - pub crtime: SystemTime, - /// Kind of file (directory, file, pipe, etc) - pub kind: FileType, - /// Permissions - pub perm: u16, - /// Number of hard links - pub nlink: u32, - /// User id - pub uid: u32, - /// Group id - pub gid: u32, - /// Rdev - pub rdev: u32, - /// Block size - pub blksize: u32, - /// Flags (macOS only, see chflags(2)) - pub flags: u32, -} - -/// Configuration of the fuse kernel module connection -#[derive(Debug)] -pub struct KernelConfig { - capabilities: u32, - requested: u32, - max_readahead: u32, - max_max_readahead: u32, - #[cfg(feature = "abi-7-13")] - max_background: u16, - #[cfg(feature = "abi-7-13")] - congestion_threshold: Option, - max_write: u32, - #[cfg(feature = "abi-7-23")] - time_gran: Duration, -} - -impl KernelConfig { - fn new(capabilities: u32, max_readahead: u32) -> Self { - Self { - capabilities, - requested: default_init_flags(capabilities), - max_readahead, - max_max_readahead: max_readahead, - #[cfg(feature = "abi-7-13")] - max_background: 16, - #[cfg(feature = "abi-7-13")] - congestion_threshold: None, - // use a max write size that fits into the session's buffer - max_write: MAX_WRITE_SIZE as u32, - // 1ns means nano-second granularity. - #[cfg(feature = "abi-7-23")] - time_gran: Duration::new(0, 1), - } - } - - /// Set the timestamp granularity - /// - /// Must be a power of 10 nanoseconds. i.e. 1s, 0.1s, 0.01s, 1ms, 0.1ms...etc - /// - /// On success returns the previous value. On error returns the nearest value which will succeed - #[cfg(feature = "abi-7-23")] - pub fn set_time_granularity(&mut self, value: Duration) -> Result { - if value.as_nanos() == 0 { - return Err(Duration::new(0, 1)); - } - if value.as_secs() > 1 || (value.as_secs() == 1 && value.subsec_nanos() > 0) { - return Err(Duration::new(1, 0)); - } - let mut power_of_10 = 1; - while power_of_10 < value.as_nanos() { - if value.as_nanos() < power_of_10 * 10 { - // value must not be a power of ten, since power_of_10 < value < power_of_10 * 10 - return Err(Duration::new(0, power_of_10 as u32)); - } - power_of_10 *= 10; - } - let previous = self.time_gran; - self.time_gran = value; - Ok(previous) - } - - /// Set the maximum write size for a single request - /// - /// On success returns the previous value. On error returns the nearest value which will succeed - pub fn set_max_write(&mut self, value: u32) -> Result { - if value == 0 { - return Err(1); - } - if value > MAX_WRITE_SIZE as u32 { - return Err(MAX_WRITE_SIZE as u32); - } - let previous = self.max_write; - self.max_write = value; - Ok(previous) - } - - /// Set the maximum readahead size - /// - /// On success returns the previous value. On error returns the nearest value which will succeed - pub fn set_max_readahead(&mut self, value: u32) -> Result { - if value == 0 { - return Err(1); - } - if value > self.max_max_readahead { - return Err(self.max_max_readahead); - } - let previous = self.max_readahead; - self.max_readahead = value; - Ok(previous) - } - - /// Add a set of capabilities. - /// - /// On success returns Ok, else return bits of capabilities not supported when capabilities you provided are not all supported by kernel. - pub fn add_capabilities(&mut self, capabilities_to_add: u32) -> Result<(), u32> { - if capabilities_to_add & self.capabilities != capabilities_to_add { - return Err(capabilities_to_add - (capabilities_to_add & self.capabilities)); - } - self.requested |= capabilities_to_add; - Ok(()) - } - - /// Set the maximum number of pending background requests. Such as readahead requests. - /// - /// On success returns the previous value. On error returns the nearest value which will succeed - #[cfg(feature = "abi-7-13")] - pub fn set_max_background(&mut self, value: u16) -> Result { - if value == 0 { - return Err(1); - } - let previous = self.max_background; - self.max_background = value; - Ok(previous) - } - - /// Set the threshold of background requests at which the kernel will consider the filesystem - /// request queue congested. (it may then switch to sleeping instead of spin-waiting, for example) - /// - /// On success returns the previous value. On error returns the nearest value which will succeed - #[cfg(feature = "abi-7-13")] - pub fn set_congestion_threshold(&mut self, value: u16) -> Result { - if value == 0 { - return Err(1); - } - let previous = self.congestion_threshold(); - self.congestion_threshold = Some(value); - Ok(previous) - } - - #[cfg(feature = "abi-7-13")] - fn congestion_threshold(&self) -> u16 { - match self.congestion_threshold { - // Default to a threshold of 3/4 of the max background threads - None => (self.max_background as u32 * 3 / 4) as u16, - Some(value) => min(value, self.max_background), - } - } - - #[cfg(feature = "abi-7-28")] - fn max_pages(&self) -> u16 { - ((max(self.max_write, self.max_readahead) - 1) / page_size::get() as u32) as u16 + 1 - } -} - -/// Filesystem trait. -/// -/// This trait must be implemented to provide a userspace filesystem via FUSE. -/// These methods correspond to fuse_lowlevel_ops in libfuse. Reasonable default -/// implementations are provided here to get a mountable filesystem that does -/// nothing. -#[allow(clippy::too_many_arguments)] -pub trait Filesystem { - /// Initialize filesystem. - /// Called before any other filesystem method. - /// The kernel module connection can be configured using the KernelConfig object - fn init(&mut self, _req: &Request<'_>, _config: &mut KernelConfig) -> Result<(), c_int> { - Ok(()) - } - - /// Clean up filesystem. - /// Called on filesystem exit. - fn destroy(&mut self) {} - - /// Look up a directory entry by name and get its attributes. - fn lookup(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEntry) { - warn!( - "[Not Implemented] lookup(parent: {:#x?}, name {:?})", - parent, name - ); - reply.error(ENOSYS); - } - - /// Forget about an inode. - /// The nlookup parameter indicates the number of lookups previously performed on - /// this inode. If the filesystem implements inode lifetimes, it is recommended that - /// inodes acquire a single reference on each lookup, and lose nlookup references on - /// each forget. The filesystem may ignore forget calls, if the inodes don't need to - /// have a limited lifetime. On unmount it is not guaranteed, that all referenced - /// inodes will receive a forget message. - fn forget(&mut self, _req: &Request<'_>, _ino: u64, _nlookup: u64) {} - - /// Like forget, but take multiple forget requests at once for performance. The default - /// implementation will fallback to forget. - #[cfg(feature = "abi-7-16")] - fn batch_forget(&mut self, req: &Request<'_>, nodes: &[fuse_forget_one]) { - for node in nodes { - self.forget(req, node.nodeid, node.nlookup); - } - } - - /// Get file attributes. - fn getattr(&mut self, _req: &Request<'_>, ino: u64, fh: Option, reply: ReplyAttr) { - warn!( - "[Not Implemented] getattr(ino: {:#x?}, fh: {:#x?})", - ino, fh - ); - reply.error(ENOSYS); - } - - /// Set file attributes. - fn setattr( - &mut self, - _req: &Request<'_>, - ino: u64, - mode: Option, - uid: Option, - gid: Option, - size: Option, - _atime: Option, - _mtime: Option, - _ctime: Option, - fh: Option, - _crtime: Option, - _chgtime: Option, - _bkuptime: Option, - flags: Option, - reply: ReplyAttr, - ) { - debug!( - "[Not Implemented] setattr(ino: {:#x?}, mode: {:?}, uid: {:?}, \ - gid: {:?}, size: {:?}, fh: {:?}, flags: {:?})", - ino, mode, uid, gid, size, fh, flags - ); - reply.error(ENOSYS); - } - - /// Read symbolic link. - fn readlink(&mut self, _req: &Request<'_>, ino: u64, reply: ReplyData) { - debug!("[Not Implemented] readlink(ino: {:#x?})", ino); - reply.error(ENOSYS); - } - - /// Create file node. - /// Create a regular file, character device, block device, fifo or socket node. - fn mknod( - &mut self, - _req: &Request<'_>, - parent: u64, - name: &OsStr, - mode: u32, - umask: u32, - rdev: u32, - reply: ReplyEntry, - ) { - debug!( - "[Not Implemented] mknod(parent: {:#x?}, name: {:?}, mode: {}, \ - umask: {:#x?}, rdev: {})", - parent, name, mode, umask, rdev - ); - reply.error(ENOSYS); - } - - /// Create a directory. - fn mkdir( - &mut self, - _req: &Request<'_>, - parent: u64, - name: &OsStr, - mode: u32, - umask: u32, - reply: ReplyEntry, - ) { - debug!( - "[Not Implemented] mkdir(parent: {:#x?}, name: {:?}, mode: {}, umask: {:#x?})", - parent, name, mode, umask - ); - reply.error(ENOSYS); - } - - /// Remove a file. - fn unlink(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEmpty) { - debug!( - "[Not Implemented] unlink(parent: {:#x?}, name: {:?})", - parent, name, - ); - reply.error(ENOSYS); - } - - /// Remove a directory. - fn rmdir(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEmpty) { - debug!( - "[Not Implemented] rmdir(parent: {:#x?}, name: {:?})", - parent, name, - ); - reply.error(ENOSYS); - } - - /// Create a symbolic link. - fn symlink( - &mut self, - _req: &Request<'_>, - parent: u64, - link_name: &OsStr, - target: &Path, - reply: ReplyEntry, - ) { - debug!( - "[Not Implemented] symlink(parent: {:#x?}, link_name: {:?}, target: {:?})", - parent, link_name, target, - ); - reply.error(EPERM); - } - - /// Rename a file. - fn rename( - &mut self, - _req: &Request<'_>, - parent: u64, - name: &OsStr, - newparent: u64, - newname: &OsStr, - flags: u32, - reply: ReplyEmpty, - ) { - debug!( - "[Not Implemented] rename(parent: {:#x?}, name: {:?}, newparent: {:#x?}, \ - newname: {:?}, flags: {})", - parent, name, newparent, newname, flags, - ); - reply.error(ENOSYS); - } - - /// Create a hard link. - fn link( - &mut self, - _req: &Request<'_>, - ino: u64, - newparent: u64, - newname: &OsStr, - reply: ReplyEntry, - ) { - debug!( - "[Not Implemented] link(ino: {:#x?}, newparent: {:#x?}, newname: {:?})", - ino, newparent, newname - ); - reply.error(EPERM); - } - - /// Open a file. - /// Open flags (with the exception of O_CREAT, O_EXCL, O_NOCTTY and O_TRUNC) are - /// available in flags. Filesystem may store an arbitrary file handle (pointer, index, - /// etc) in fh, and use this in other all other file operations (read, write, flush, - /// release, fsync). Filesystem may also implement stateless file I/O and not store - /// anything in fh. There are also some flags (direct_io, keep_cache) which the - /// filesystem may set, to change the way the file is opened. See fuse_file_info - /// structure in for more details. - fn open(&mut self, _req: &Request<'_>, _ino: u64, _flags: i32, reply: ReplyOpen) { - reply.opened(0, 0); - } - - /// Read data. - /// Read should send exactly the number of bytes requested except on EOF or error, - /// otherwise the rest of the data will be substituted with zeroes. An exception to - /// this is when the file has been opened in 'direct_io' mode, in which case the - /// return value of the read system call will reflect the return value of this - /// operation. fh will contain the value set by the open method, or will be undefined - /// if the open method didn't set any value. - /// - /// flags: these are the file flags, such as O_SYNC. Only supported with ABI >= 7.9 - /// lock_owner: only supported with ABI >= 7.9 - fn read( - &mut self, - _req: &Request<'_>, - ino: u64, - fh: u64, - offset: i64, - size: u32, - flags: i32, - lock_owner: Option, - reply: ReplyData, - ) { - warn!( - "[Not Implemented] read(ino: {:#x?}, fh: {}, offset: {}, size: {}, \ - flags: {:#x?}, lock_owner: {:?})", - ino, fh, offset, size, flags, lock_owner - ); - reply.error(ENOSYS); - } - - /// Write data. - /// Write should return exactly the number of bytes requested except on error. An - /// exception to this is when the file has been opened in 'direct_io' mode, in - /// which case the return value of the write system call will reflect the return - /// value of this operation. fh will contain the value set by the open method, or - /// will be undefined if the open method didn't set any value. - /// - /// write_flags: will contain FUSE_WRITE_CACHE, if this write is from the page cache. If set, - /// the pid, uid, gid, and fh may not match the value that would have been sent if write cachin - /// is disabled - /// flags: these are the file flags, such as O_SYNC. Only supported with ABI >= 7.9 - /// lock_owner: only supported with ABI >= 7.9 - fn write( - &mut self, - _req: &Request<'_>, - ino: u64, - fh: u64, - offset: i64, - data: &[u8], - write_flags: u32, - flags: i32, - lock_owner: Option, - reply: ReplyWrite, - ) { - debug!( - "[Not Implemented] write(ino: {:#x?}, fh: {}, offset: {}, data.len(): {}, \ - write_flags: {:#x?}, flags: {:#x?}, lock_owner: {:?})", - ino, - fh, - offset, - data.len(), - write_flags, - flags, - lock_owner - ); - reply.error(ENOSYS); - } - - /// Flush method. - /// This is called on each close() of the opened file. Since file descriptors can - /// be duplicated (dup, dup2, fork), for one open call there may be many flush - /// calls. Filesystems shouldn't assume that flush will always be called after some - /// writes, or that if will be called at all. fh will contain the value set by the - /// open method, or will be undefined if the open method didn't set any value. - /// NOTE: the name of the method is misleading, since (unlike fsync) the filesystem - /// is not forced to flush pending writes. One reason to flush data, is if the - /// filesystem wants to return write errors. If the filesystem supports file locking - /// operations (setlk, getlk) it should remove all locks belonging to 'lock_owner'. - fn flush(&mut self, _req: &Request<'_>, ino: u64, fh: u64, lock_owner: u64, reply: ReplyEmpty) { - debug!( - "[Not Implemented] flush(ino: {:#x?}, fh: {}, lock_owner: {:?})", - ino, fh, lock_owner - ); - reply.error(ENOSYS); - } - - /// Release an open file. - /// Release is called when there are no more references to an open file: all file - /// descriptors are closed and all memory mappings are unmapped. For every open - /// call there will be exactly one release call. The filesystem may reply with an - /// error, but error values are not returned to close() or munmap() which triggered - /// the release. fh will contain the value set by the open method, or will be undefined - /// if the open method didn't set any value. flags will contain the same flags as for - /// open. - fn release( - &mut self, - _req: &Request<'_>, - _ino: u64, - _fh: u64, - _flags: i32, - _lock_owner: Option, - _flush: bool, - reply: ReplyEmpty, - ) { - reply.ok(); - } - - /// Synchronize file contents. - /// If the datasync parameter is non-zero, then only the user data should be flushed, - /// not the meta data. - fn fsync(&mut self, _req: &Request<'_>, ino: u64, fh: u64, datasync: bool, reply: ReplyEmpty) { - debug!( - "[Not Implemented] fsync(ino: {:#x?}, fh: {}, datasync: {})", - ino, fh, datasync - ); - reply.error(ENOSYS); - } - - /// Open a directory. - /// Filesystem may store an arbitrary file handle (pointer, index, etc) in fh, and - /// use this in other all other directory stream operations (readdir, releasedir, - /// fsyncdir). Filesystem may also implement stateless directory I/O and not store - /// anything in fh, though that makes it impossible to implement standard conforming - /// directory stream operations in case the contents of the directory can change - /// between opendir and releasedir. - fn opendir(&mut self, _req: &Request<'_>, _ino: u64, _flags: i32, reply: ReplyOpen) { - reply.opened(0, 0); - } - - /// Read directory. - /// Send a buffer filled using buffer.fill(), with size not exceeding the - /// requested size. Send an empty buffer on end of stream. fh will contain the - /// value set by the opendir method, or will be undefined if the opendir method - /// didn't set any value. - fn readdir( - &mut self, - _req: &Request<'_>, - ino: u64, - fh: u64, - offset: i64, - reply: ReplyDirectory, - ) { - warn!( - "[Not Implemented] readdir(ino: {:#x?}, fh: {}, offset: {})", - ino, fh, offset - ); - reply.error(ENOSYS); - } - - /// Read directory. - /// Send a buffer filled using buffer.fill(), with size not exceeding the - /// requested size. Send an empty buffer on end of stream. fh will contain the - /// value set by the opendir method, or will be undefined if the opendir method - /// didn't set any value. - fn readdirplus( - &mut self, - _req: &Request<'_>, - ino: u64, - fh: u64, - offset: i64, - reply: ReplyDirectoryPlus, - ) { - debug!( - "[Not Implemented] readdirplus(ino: {:#x?}, fh: {}, offset: {})", - ino, fh, offset - ); - reply.error(ENOSYS); - } - - /// Release an open directory. - /// For every opendir call there will be exactly one releasedir call. fh will - /// contain the value set by the opendir method, or will be undefined if the - /// opendir method didn't set any value. - fn releasedir( - &mut self, - _req: &Request<'_>, - _ino: u64, - _fh: u64, - _flags: i32, - reply: ReplyEmpty, - ) { - reply.ok(); - } - - /// Synchronize directory contents. - /// If the datasync parameter is set, then only the directory contents should - /// be flushed, not the meta data. fh will contain the value set by the opendir - /// method, or will be undefined if the opendir method didn't set any value. - fn fsyncdir( - &mut self, - _req: &Request<'_>, - ino: u64, - fh: u64, - datasync: bool, - reply: ReplyEmpty, - ) { - debug!( - "[Not Implemented] fsyncdir(ino: {:#x?}, fh: {}, datasync: {})", - ino, fh, datasync - ); - reply.error(ENOSYS); - } - - /// Get file system statistics. - fn statfs(&mut self, _req: &Request<'_>, _ino: u64, reply: ReplyStatfs) { - reply.statfs(0, 0, 0, 0, 0, 512, 255, 0); - } - - /// Set an extended attribute. - fn setxattr( - &mut self, - _req: &Request<'_>, - ino: u64, - name: &OsStr, - _value: &[u8], - flags: i32, - position: u32, - reply: ReplyEmpty, - ) { - debug!( - "[Not Implemented] setxattr(ino: {:#x?}, name: {:?}, flags: {:#x?}, position: {})", - ino, name, flags, position - ); - reply.error(ENOSYS); - } - - /// Get an extended attribute. - /// If `size` is 0, the size of the value should be sent with `reply.size()`. - /// If `size` is not 0, and the value fits, send it with `reply.data()`, or - /// `reply.error(ERANGE)` if it doesn't. - fn getxattr( - &mut self, - _req: &Request<'_>, - ino: u64, - name: &OsStr, - size: u32, - reply: ReplyXattr, - ) { - debug!( - "[Not Implemented] getxattr(ino: {:#x?}, name: {:?}, size: {})", - ino, name, size - ); - reply.error(ENOSYS); - } - - /// List extended attribute names. - /// If `size` is 0, the size of the value should be sent with `reply.size()`. - /// If `size` is not 0, and the value fits, send it with `reply.data()`, or - /// `reply.error(ERANGE)` if it doesn't. - fn listxattr(&mut self, _req: &Request<'_>, ino: u64, size: u32, reply: ReplyXattr) { - debug!( - "[Not Implemented] listxattr(ino: {:#x?}, size: {})", - ino, size - ); - reply.error(ENOSYS); - } - - /// Remove an extended attribute. - fn removexattr(&mut self, _req: &Request<'_>, ino: u64, name: &OsStr, reply: ReplyEmpty) { - debug!( - "[Not Implemented] removexattr(ino: {:#x?}, name: {:?})", - ino, name - ); - reply.error(ENOSYS); - } - - /// Check file access permissions. - /// This will be called for the access() system call. If the 'default_permissions' - /// mount option is given, this method is not called. This method is not called - /// under Linux kernel versions 2.4.x - fn access(&mut self, _req: &Request<'_>, ino: u64, mask: i32, reply: ReplyEmpty) { - debug!("[Not Implemented] access(ino: {:#x?}, mask: {})", ino, mask); - reply.error(ENOSYS); - } - - /// Create and open a file. - /// If the file does not exist, first create it with the specified mode, and then - /// open it. Open flags (with the exception of O_NOCTTY) are available in flags. - /// Filesystem may store an arbitrary file handle (pointer, index, etc) in fh, - /// and use this in other all other file operations (read, write, flush, release, - /// fsync). There are also some flags (direct_io, keep_cache) which the - /// filesystem may set, to change the way the file is opened. See fuse_file_info - /// structure in for more details. If this method is not - /// implemented or under Linux kernel versions earlier than 2.6.15, the mknod() - /// and open() methods will be called instead. - fn create( - &mut self, - _req: &Request<'_>, - parent: u64, - name: &OsStr, - mode: u32, - umask: u32, - flags: i32, - reply: ReplyCreate, - ) { - debug!( - "[Not Implemented] create(parent: {:#x?}, name: {:?}, mode: {}, umask: {:#x?}, \ - flags: {:#x?})", - parent, name, mode, umask, flags - ); - reply.error(ENOSYS); - } - - /// Test for a POSIX file lock. - fn getlk( - &mut self, - _req: &Request<'_>, - ino: u64, - fh: u64, - lock_owner: u64, - start: u64, - end: u64, - typ: i32, - pid: u32, - reply: ReplyLock, - ) { - debug!( - "[Not Implemented] getlk(ino: {:#x?}, fh: {}, lock_owner: {}, start: {}, \ - end: {}, typ: {}, pid: {})", - ino, fh, lock_owner, start, end, typ, pid - ); - reply.error(ENOSYS); - } - - /// Acquire, modify or release a POSIX file lock. - /// For POSIX threads (NPTL) there's a 1-1 relation between pid and owner, but - /// otherwise this is not always the case. For checking lock ownership, - /// 'fi->owner' must be used. The l_pid field in 'struct flock' should only be - /// used to fill in this field in getlk(). Note: if the locking methods are not - /// implemented, the kernel will still allow file locking to work locally. - /// Hence these are only interesting for network filesystems and similar. - fn setlk( - &mut self, - _req: &Request<'_>, - ino: u64, - fh: u64, - lock_owner: u64, - start: u64, - end: u64, - typ: i32, - pid: u32, - sleep: bool, - reply: ReplyEmpty, - ) { - debug!( - "[Not Implemented] setlk(ino: {:#x?}, fh: {}, lock_owner: {}, start: {}, \ - end: {}, typ: {}, pid: {}, sleep: {})", - ino, fh, lock_owner, start, end, typ, pid, sleep - ); - reply.error(ENOSYS); - } - - /// Map block index within file to block index within device. - /// Note: This makes sense only for block device backed filesystems mounted - /// with the 'blkdev' option - fn bmap(&mut self, _req: &Request<'_>, ino: u64, blocksize: u32, idx: u64, reply: ReplyBmap) { - debug!( - "[Not Implemented] bmap(ino: {:#x?}, blocksize: {}, idx: {})", - ino, blocksize, idx, - ); - reply.error(ENOSYS); - } - - /// control device - fn ioctl( - &mut self, - _req: &Request<'_>, - ino: u64, - fh: u64, - flags: u32, - cmd: u32, - in_data: &[u8], - out_size: u32, - reply: ReplyIoctl, - ) { - debug!( - "[Not Implemented] ioctl(ino: {:#x?}, fh: {}, flags: {}, cmd: {}, \ - in_data.len(): {}, out_size: {})", - ino, - fh, - flags, - cmd, - in_data.len(), - out_size, - ); - reply.error(ENOSYS); - } - - /// Poll for events - #[cfg(feature = "abi-7-11")] - fn poll( - &mut self, - _req: &Request<'_>, - ino: u64, - fh: u64, - ph: PollHandle, - events: u32, - flags: u32, - reply: ReplyPoll, - ) { - debug!( - "[Not Implemented] poll(ino: {:#x?}, fh: {}, ph: {:?}, events: {}, flags: {})", - ino, fh, ph, events, flags - ); - reply.error(ENOSYS); - } - - /// Preallocate or deallocate space to a file - fn fallocate( - &mut self, - _req: &Request<'_>, - ino: u64, - fh: u64, - offset: i64, - length: i64, - mode: i32, - reply: ReplyEmpty, - ) { - debug!( - "[Not Implemented] fallocate(ino: {:#x?}, fh: {}, offset: {}, \ - length: {}, mode: {})", - ino, fh, offset, length, mode - ); - reply.error(ENOSYS); - } - - /// Reposition read/write file offset - fn lseek( - &mut self, - _req: &Request<'_>, - ino: u64, - fh: u64, - offset: i64, - whence: i32, - reply: ReplyLseek, - ) { - debug!( - "[Not Implemented] lseek(ino: {:#x?}, fh: {}, offset: {}, whence: {})", - ino, fh, offset, whence - ); - reply.error(ENOSYS); - } - - /// Copy the specified range from the source inode to the destination inode - fn copy_file_range( - &mut self, - _req: &Request<'_>, - ino_in: u64, - fh_in: u64, - offset_in: i64, - ino_out: u64, - fh_out: u64, - offset_out: i64, - len: u64, - flags: u32, - reply: ReplyWrite, - ) { - debug!( - "[Not Implemented] copy_file_range(ino_in: {:#x?}, fh_in: {}, \ - offset_in: {}, ino_out: {:#x?}, fh_out: {}, offset_out: {}, \ - len: {}, flags: {})", - ino_in, fh_in, offset_in, ino_out, fh_out, offset_out, len, flags - ); - reply.error(ENOSYS); - } - - /// macOS only: Rename the volume. Set fuse_init_out.flags during init to - /// FUSE_VOL_RENAME to enable - #[cfg(target_os = "macos")] - fn setvolname(&mut self, _req: &Request<'_>, name: &OsStr, reply: ReplyEmpty) { - debug!("[Not Implemented] setvolname(name: {:?})", name); - reply.error(ENOSYS); - } - - /// macOS only (undocumented) - #[cfg(target_os = "macos")] - fn exchange( - &mut self, - _req: &Request<'_>, - parent: u64, - name: &OsStr, - newparent: u64, - newname: &OsStr, - options: u64, - reply: ReplyEmpty, - ) { - debug!( - "[Not Implemented] exchange(parent: {:#x?}, name: {:?}, newparent: {:#x?}, \ - newname: {:?}, options: {})", - parent, name, newparent, newname, options - ); - reply.error(ENOSYS); - } - - /// macOS only: Query extended times (bkuptime and crtime). Set fuse_init_out.flags - /// during init to FUSE_XTIMES to enable - #[cfg(target_os = "macos")] - fn getxtimes(&mut self, _req: &Request<'_>, ino: u64, reply: ReplyXTimes) { - debug!("[Not Implemented] getxtimes(ino: {:#x?})", ino); - reply.error(ENOSYS); - } -} - -/// Mount the given filesystem to the given mountpoint. This function will -/// not return until the filesystem is unmounted. -/// -/// Note that you need to lead each option with a separate `"-o"` string. See -/// `examples/hello.rs`. -#[deprecated(note = "use mount2() instead")] -pub fn mount>( - filesystem: FS, - mountpoint: P, - options: &[&OsStr], -) -> io::Result<()> { - let options = parse_options_from_args(options)?; - mount2(filesystem, mountpoint, options.as_ref()) -} - -/// Mount the given filesystem to the given mountpoint. This function will -/// not return until the filesystem is unmounted. -/// -/// NOTE: This will eventually replace mount(), once the API is stable -pub fn mount2>( - filesystem: FS, - mountpoint: P, - options: &[MountOption], -) -> io::Result<()> { - check_option_conflicts(options)?; - Session::new(filesystem, mountpoint.as_ref(), options).and_then(|mut se| se.run()) -} - -/// Mount the given filesystem to the given mountpoint. This function spawns -/// a background thread to handle filesystem operations while being mounted -/// and therefore returns immediately. The returned handle should be stored -/// to reference the mounted filesystem. If it's dropped, the filesystem will -/// be unmounted. -#[deprecated(note = "use spawn_mount2() instead")] -pub fn spawn_mount<'a, FS: Filesystem + Send + 'static + 'a, P: AsRef>( - filesystem: FS, - mountpoint: P, - options: &[&OsStr], -) -> io::Result { - let options: Option> = options - .iter() - .map(|x| Some(MountOption::from_str(x.to_str()?))) - .collect(); - let options = options.ok_or(ErrorKind::InvalidData)?; - Session::new(filesystem, mountpoint.as_ref(), options.as_ref()).and_then(|se| se.spawn()) -} - -/// Mount the given filesystem to the given mountpoint. This function spawns -/// a background thread to handle filesystem operations while being mounted -/// and therefore returns immediately. The returned handle should be stored -/// to reference the mounted filesystem. If it's dropped, the filesystem will -/// be unmounted. -/// -/// NOTE: This is the corresponding function to mount2. -pub fn spawn_mount2<'a, FS: Filesystem + Send + 'static + 'a, P: AsRef>( - filesystem: FS, - mountpoint: P, - options: &[MountOption], -) -> io::Result { - check_option_conflicts(options)?; - Session::new(filesystem, mountpoint.as_ref(), options).and_then(|se| se.spawn()) -} diff --git a/vendor/fuser/src/ll/argument.rs b/vendor/fuser/src/ll/argument.rs deleted file mode 100644 index aba183eda..000000000 --- a/vendor/fuser/src/ll/argument.rs +++ /dev/null @@ -1,163 +0,0 @@ -//! Argument decomposition for FUSE operation requests. -//! -//! Helper to decompose a slice of binary data (incoming FUSE request) into multiple data -//! structures (request arguments). - -use std::ffi::OsStr; -use std::os::unix::ffi::OsStrExt; -use zerocopy::{FromBytes, Immutable, KnownLayout}; - -/// An iterator that can be used to fetch typed arguments from a byte slice. -pub struct ArgumentIterator<'a> { - data: &'a [u8], -} - -impl<'a> ArgumentIterator<'a> { - /// Create a new argument iterator for the given byte slice. - pub fn new(data: &'a [u8]) -> ArgumentIterator<'a> { - ArgumentIterator { data } - } - - /// Returns the size of the remaining data. - pub fn len(&self) -> usize { - self.data.len() - } - - /// Fetch a slice of all remaining bytes. - pub fn fetch_all(&mut self) -> &'a [u8] { - let bytes = self.data; - self.data = &[]; - bytes - } - - /// Fetch a typed argument. Returns `None` if there's not enough data left. - pub fn fetch(&mut self) -> Option<&'a T> { - match zerocopy::Ref::<_, T>::from_prefix(self.data) { - Err(_err) => { - // TODO: do something with _err - if self.data.as_ptr() as usize % core::mem::align_of::() != 0 { - // Panic on alignment errors as this is under the control - // of the programmer, we can still return None for size - // failures as this may be caused by insufficient external - // data. - panic!("Data unaligned"); - } else { - None - } - } - Ok((x, rest)) => { - self.data = rest; - Some(zerocopy::Ref::<&[u8], T>::into_ref(x)) - } - } - } - - /// Fetch a slice of typed of arguments. Returns `None` if there's not enough data left. - #[cfg(feature = "abi-7-16")] - pub fn fetch_slice(&mut self, count: usize) -> Option<&'a [T]> { - match zerocopy::Ref::<_, [T]>::from_prefix_with_elems(self.data, count) { - Err(_err) => { - // TODO: do something with _err - if self.data.as_ptr() as usize % core::mem::align_of::() != 0 { - // Panic on alignment errors as this is under the control - // of the programmer, we can still return None for size - // failures as this may be caused by insufficient external - // data. - panic!("Data unaligned"); - } else { - None - } - } - Ok((x, rest)) => { - self.data = rest; - Some(zerocopy::Ref::<&[u8], [T]>::into_ref(x)) - } - } - } - - /// Fetch a (zero-terminated) string (can be non-utf8). Returns `None` if there's not enough - /// data left or no zero-termination could be found. - pub fn fetch_str(&mut self) -> Option<&'a OsStr> { - let len = memchr::memchr(0, self.data)?; - let (out, rest) = self.data.split_at(len); - self.data = &rest[1..]; - Some(OsStr::from_bytes(out)) - } -} - -#[cfg(test)] -pub mod tests { - use std::ops::Deref; - - use super::super::test::AlignedData; - use super::*; - use zerocopy::FromBytes; - - const TEST_DATA: AlignedData<[u8; 10]> = - AlignedData([0x66, 0x6f, 0x6f, 0x00, 0x62, 0x61, 0x72, 0x00, 0x62, 0x61]); - - #[repr(C)] - #[derive(FromBytes, KnownLayout, Immutable)] - struct TestArgument { - p1: u8, - p2: u8, - p3: u16, - } - - #[test] - fn all_data() { - let mut it = ArgumentIterator::new(TEST_DATA.deref()); - it.fetch_str().unwrap(); - let arg = it.fetch_all(); - assert_eq!(arg, [0x62, 0x61, 0x72, 0x00, 0x62, 0x61]); - } - - #[test] - fn generic_argument() { - let mut it = ArgumentIterator::new(TEST_DATA.deref()); - let arg: &TestArgument = it.fetch().unwrap(); - assert_eq!(arg.p1, 0x66); - assert_eq!(arg.p2, 0x6f); - assert_eq!(arg.p3, 0x006f); - let arg: &TestArgument = it.fetch().unwrap(); - assert_eq!(arg.p1, 0x62); - assert_eq!(arg.p2, 0x61); - assert_eq!(arg.p3, 0x0072); - assert_eq!(it.len(), 2); - } - - #[test] - fn string_argument() { - let mut it = ArgumentIterator::new(TEST_DATA.deref()); - let arg = it.fetch_str().unwrap(); - assert_eq!(arg, "foo"); - let arg = it.fetch_str().unwrap(); - assert_eq!(arg, "bar"); - assert_eq!(it.len(), 2); - } - - #[test] - fn mixed_arguments() { - let mut it = ArgumentIterator::new(TEST_DATA.deref()); - let arg: &TestArgument = it.fetch().unwrap(); - assert_eq!(arg.p1, 0x66); - assert_eq!(arg.p2, 0x6f); - assert_eq!(arg.p3, 0x006f); - let arg = it.fetch_str().unwrap(); - assert_eq!(arg, "bar"); - let arg = it.fetch_all(); - assert_eq!(arg, [0x62, 0x61]); - } - - #[test] - fn out_of_data() { - let mut it = ArgumentIterator::new(TEST_DATA.deref()); - it.fetch::().unwrap(); - let arg: Option<&TestArgument> = it.fetch(); - assert!(arg.is_none()); - assert_eq!(it.len(), 2); - let arg = it.fetch_str(); - assert!(arg.is_none()); - assert_eq!(it.len(), 2); - } -} diff --git a/vendor/fuser/src/ll/fuse_abi.rs b/vendor/fuser/src/ll/fuse_abi.rs deleted file mode 100644 index e75fef490..000000000 --- a/vendor/fuser/src/ll/fuse_abi.rs +++ /dev/null @@ -1,1145 +0,0 @@ -//! FUSE kernel interface. -//! -//! Types and definitions used for communication between the kernel driver and the userspace -//! part of a FUSE filesystem. Since the kernel driver may be installed independently, the ABI -//! interface is versioned and capabilities are exchanged during the initialization (mounting) -//! of a filesystem. -//! -//! OSXFUSE (macOS): -//! - supports ABI 7.8 in OSXFUSE 2.x -//! - supports ABI 7.19 since OSXFUSE 3.0.0 -//! -//! libfuse (Linux/BSD): -//! - supports ABI 7.8 since FUSE 2.6.0 -//! - supports ABI 7.12 since FUSE 2.8.0 -//! - supports ABI 7.18 since FUSE 2.9.0 -//! - supports ABI 7.19 since FUSE 2.9.1 -//! - supports ABI 7.26 since FUSE 3.0.0 -//! -//! Items without a version annotation are valid with ABI 7.8 and later - -#![warn(missing_debug_implementations)] -#![allow(missing_docs)] -// This module intentionally mirrors the complete versioned kernel ABI. Some -// request structures are unavailable or unused on a particular host/feature -// combination but must remain defined for the other supported combinations. -#![allow(dead_code)] - -#[cfg(feature = "abi-7-9")] -use crate::consts::{FATTR_ATIME_NOW, FATTR_MTIME_NOW}; -use std::convert::TryFrom; -use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; - -pub const FUSE_KERNEL_VERSION: u32 = 7; - -#[cfg(not(feature = "abi-7-9"))] -pub const FUSE_KERNEL_MINOR_VERSION: u32 = 8; -#[cfg(all(feature = "abi-7-9", not(feature = "abi-7-10")))] -pub const FUSE_KERNEL_MINOR_VERSION: u32 = 9; -#[cfg(all(feature = "abi-7-10", not(feature = "abi-7-11")))] -pub const FUSE_KERNEL_MINOR_VERSION: u32 = 10; -#[cfg(all(feature = "abi-7-11", not(feature = "abi-7-12")))] -pub const FUSE_KERNEL_MINOR_VERSION: u32 = 11; -#[cfg(all(feature = "abi-7-12", not(feature = "abi-7-13")))] -pub const FUSE_KERNEL_MINOR_VERSION: u32 = 12; -#[cfg(all(feature = "abi-7-13", not(feature = "abi-7-14")))] -pub const FUSE_KERNEL_MINOR_VERSION: u32 = 13; -#[cfg(all(feature = "abi-7-14", not(feature = "abi-7-15")))] -pub const FUSE_KERNEL_MINOR_VERSION: u32 = 14; -#[cfg(all(feature = "abi-7-15", not(feature = "abi-7-16")))] -pub const FUSE_KERNEL_MINOR_VERSION: u32 = 15; -#[cfg(all(feature = "abi-7-16", not(feature = "abi-7-17")))] -pub const FUSE_KERNEL_MINOR_VERSION: u32 = 16; -#[cfg(all(feature = "abi-7-17", not(feature = "abi-7-18")))] -pub const FUSE_KERNEL_MINOR_VERSION: u32 = 17; -#[cfg(all(feature = "abi-7-18", not(feature = "abi-7-19")))] -pub const FUSE_KERNEL_MINOR_VERSION: u32 = 18; -#[cfg(all(feature = "abi-7-19", not(feature = "abi-7-20")))] -pub const FUSE_KERNEL_MINOR_VERSION: u32 = 19; -#[cfg(all(feature = "abi-7-20", not(feature = "abi-7-21")))] -pub const FUSE_KERNEL_MINOR_VERSION: u32 = 20; -#[cfg(all(feature = "abi-7-21", not(feature = "abi-7-22")))] -pub const FUSE_KERNEL_MINOR_VERSION: u32 = 21; -#[cfg(all(feature = "abi-7-22", not(feature = "abi-7-23")))] -pub const FUSE_KERNEL_MINOR_VERSION: u32 = 22; -#[cfg(all(feature = "abi-7-23", not(feature = "abi-7-24")))] -pub const FUSE_KERNEL_MINOR_VERSION: u32 = 23; -#[cfg(all(feature = "abi-7-24", not(feature = "abi-7-25")))] -pub const FUSE_KERNEL_MINOR_VERSION: u32 = 24; -#[cfg(all(feature = "abi-7-25", not(feature = "abi-7-26")))] -pub const FUSE_KERNEL_MINOR_VERSION: u32 = 25; -#[cfg(all(feature = "abi-7-26", not(feature = "abi-7-27")))] -pub const FUSE_KERNEL_MINOR_VERSION: u32 = 26; -#[cfg(all(feature = "abi-7-27", not(feature = "abi-7-28")))] -pub const FUSE_KERNEL_MINOR_VERSION: u32 = 27; -#[cfg(all(feature = "abi-7-28", not(feature = "abi-7-29")))] -pub const FUSE_KERNEL_MINOR_VERSION: u32 = 28; -#[cfg(all(feature = "abi-7-29", not(feature = "abi-7-30")))] -pub const FUSE_KERNEL_MINOR_VERSION: u32 = 29; -#[cfg(all(feature = "abi-7-30", not(feature = "abi-7-31")))] -pub const FUSE_KERNEL_MINOR_VERSION: u32 = 30; -#[cfg(feature = "abi-7-31")] -pub const FUSE_KERNEL_MINOR_VERSION: u32 = 31; - -pub const FUSE_ROOT_ID: u64 = 1; - -#[repr(C)] -#[derive(Debug, IntoBytes, Clone, Copy, KnownLayout, Immutable)] -pub struct fuse_attr { - pub ino: u64, - pub size: u64, - pub blocks: u64, - // NOTE: this field is defined as u64 in fuse_kernel.h in libfuse. However, it is treated as signed - // to match stat.st_atime - pub atime: i64, - // NOTE: this field is defined as u64 in fuse_kernel.h in libfuse. However, it is treated as signed - // to match stat.st_mtime - pub mtime: i64, - // NOTE: this field is defined as u64 in fuse_kernel.h in libfuse. However, it is treated as signed - // to match stat.st_ctime - pub ctime: i64, - #[cfg(target_os = "macos")] - pub crtime: u64, - pub atimensec: u32, - pub mtimensec: u32, - pub ctimensec: u32, - #[cfg(target_os = "macos")] - pub crtimensec: u32, - pub mode: u32, - pub nlink: u32, - pub uid: u32, - pub gid: u32, - pub rdev: u32, - #[cfg(target_os = "macos")] - pub flags: u32, // see chflags(2) - #[cfg(feature = "abi-7-9")] - pub blksize: u32, - #[cfg(feature = "abi-7-9")] - pub padding: u32, -} - -#[repr(C)] -#[derive(Debug, IntoBytes, KnownLayout, Immutable)] -pub struct fuse_kstatfs { - pub blocks: u64, // Total blocks (in units of frsize) - pub bfree: u64, // Free blocks - pub bavail: u64, // Free blocks for unprivileged users - pub files: u64, // Total inodes - pub ffree: u64, // Free inodes - pub bsize: u32, // Filesystem block size - pub namelen: u32, // Maximum filename length - pub frsize: u32, // Fundamental file system block size - pub padding: u32, - pub spare: [u32; 6], -} - -#[repr(C)] -#[derive(Debug, IntoBytes, FromBytes, KnownLayout, Immutable)] -pub struct fuse_file_lock { - pub start: u64, - pub end: u64, - // NOTE: this field is defined as u32 in fuse_kernel.h in libfuse. However, it is treated as signed - pub typ: i32, - pub pid: u32, -} - -pub mod consts { - // Bitmasks for fuse_setattr_in.valid - pub const FATTR_MODE: u32 = 1 << 0; - pub const FATTR_UID: u32 = 1 << 1; - pub const FATTR_GID: u32 = 1 << 2; - pub const FATTR_SIZE: u32 = 1 << 3; - pub const FATTR_ATIME: u32 = 1 << 4; - pub const FATTR_MTIME: u32 = 1 << 5; - pub const FATTR_FH: u32 = 1 << 6; - #[cfg(feature = "abi-7-9")] - pub const FATTR_ATIME_NOW: u32 = 1 << 7; - #[cfg(feature = "abi-7-9")] - pub const FATTR_MTIME_NOW: u32 = 1 << 8; - #[cfg(feature = "abi-7-9")] - pub const FATTR_LOCKOWNER: u32 = 1 << 9; - #[cfg(feature = "abi-7-23")] - pub const FATTR_CTIME: u32 = 1 << 10; - - #[cfg(target_os = "macos")] - pub const FATTR_CRTIME: u32 = 1 << 28; - #[cfg(target_os = "macos")] - pub const FATTR_CHGTIME: u32 = 1 << 29; - #[cfg(target_os = "macos")] - pub const FATTR_BKUPTIME: u32 = 1 << 30; - #[cfg(target_os = "macos")] - pub const FATTR_FLAGS: u32 = 1 << 31; - - // Flags returned by the open request - pub const FOPEN_DIRECT_IO: u32 = 1 << 0; // bypass page cache for this open file - pub const FOPEN_KEEP_CACHE: u32 = 1 << 1; // don't invalidate the data cache on open - #[cfg(feature = "abi-7-10")] - pub const FOPEN_NONSEEKABLE: u32 = 1 << 2; // the file is not seekable - #[cfg(feature = "abi-7-28")] - pub const FOPEN_CACHE_DIR: u32 = 1 << 3; // allow caching this directory - #[cfg(feature = "abi-7-31")] - pub const FOPEN_STREAM: u32 = 1 << 4; // the file is stream-like (no file position at all) - - #[cfg(target_os = "macos")] - pub const FOPEN_PURGE_ATTR: u32 = 1 << 30; - #[cfg(target_os = "macos")] - pub const FOPEN_PURGE_UBC: u32 = 1 << 31; - - // Init request/reply flags - pub const FUSE_ASYNC_READ: u32 = 1 << 0; // asynchronous read requests - pub const FUSE_POSIX_LOCKS: u32 = 1 << 1; // remote locking for POSIX file locks - #[cfg(feature = "abi-7-9")] - pub const FUSE_FILE_OPS: u32 = 1 << 2; // kernel sends file handle for fstat, etc... - #[cfg(feature = "abi-7-9")] - pub const FUSE_ATOMIC_O_TRUNC: u32 = 1 << 3; // handles the O_TRUNC open flag in the filesystem - #[cfg(feature = "abi-7-10")] - pub const FUSE_EXPORT_SUPPORT: u32 = 1 << 4; // filesystem handles lookups of "." and ".." - #[cfg(feature = "abi-7-9")] - pub const FUSE_BIG_WRITES: u32 = 1 << 5; // filesystem can handle write size larger than 4kB - #[cfg(feature = "abi-7-12")] - pub const FUSE_DONT_MASK: u32 = 1 << 6; // don't apply umask to file mode on create operations - #[cfg(all(feature = "abi-7-14", not(target_os = "macos")))] - pub const FUSE_SPLICE_WRITE: u32 = 1 << 7; // kernel supports splice write on the device - #[cfg(all(feature = "abi-7-14", not(target_os = "macos")))] - pub const FUSE_SPLICE_MOVE: u32 = 1 << 8; // kernel supports splice move on the device - #[cfg(not(target_os = "macos"))] - #[cfg(feature = "abi-7-14")] - pub const FUSE_SPLICE_READ: u32 = 1 << 9; // kernel supports splice read on the device - #[cfg(feature = "abi-7-17")] - pub const FUSE_FLOCK_LOCKS: u32 = 1 << 10; // remote locking for BSD style file locks - #[cfg(feature = "abi-7-18")] - pub const FUSE_HAS_IOCTL_DIR: u32 = 1 << 11; // kernel supports ioctl on directories - #[cfg(feature = "abi-7-20")] - pub const FUSE_AUTO_INVAL_DATA: u32 = 1 << 12; // automatically invalidate cached pages - #[cfg(feature = "abi-7-21")] - pub const FUSE_DO_READDIRPLUS: u32 = 1 << 13; // do READDIRPLUS (READDIR+LOOKUP in one) - #[cfg(feature = "abi-7-21")] - pub const FUSE_READDIRPLUS_AUTO: u32 = 1 << 14; // adaptive readdirplus - #[cfg(feature = "abi-7-22")] - pub const FUSE_ASYNC_DIO: u32 = 1 << 15; // asynchronous direct I/O submission - #[cfg(feature = "abi-7-23")] - pub const FUSE_WRITEBACK_CACHE: u32 = 1 << 16; // use writeback cache for buffered writes - #[cfg(feature = "abi-7-23")] - pub const FUSE_NO_OPEN_SUPPORT: u32 = 1 << 17; // kernel supports zero-message opens - #[cfg(feature = "abi-7-25")] - pub const FUSE_PARALLEL_DIROPS: u32 = 1 << 18; // allow parallel lookups and readdir - #[cfg(feature = "abi-7-26")] - pub const FUSE_HANDLE_KILLPRIV: u32 = 1 << 19; // fs handles killing suid/sgid/cap on write/chown/trunc - #[cfg(feature = "abi-7-26")] - pub const FUSE_POSIX_ACL: u32 = 1 << 20; // filesystem supports posix acls - #[cfg(feature = "abi-7-27")] - pub const FUSE_ABORT_ERROR: u32 = 1 << 21; // reading the device after abort returns ECONNABORTED - #[cfg(feature = "abi-7-28")] - pub const FUSE_MAX_PAGES: u32 = 1 << 22; // init_out.max_pages contains the max number of req pages - #[cfg(feature = "abi-7-28")] - pub const FUSE_CACHE_SYMLINKS: u32 = 1 << 23; // cache READLINK responses - #[cfg(feature = "abi-7-29")] - pub const FUSE_NO_OPENDIR_SUPPORT: u32 = 1 << 24; // kernel supports zero-message opendir - #[cfg(feature = "abi-7-30")] - pub const FUSE_EXPLICIT_INVAL_DATA: u32 = 1 << 25; // only invalidate cached pages on explicit request - - #[cfg(target_os = "macos")] - pub const FUSE_ALLOCATE: u32 = 1 << 27; - #[cfg(target_os = "macos")] - pub const FUSE_EXCHANGE_DATA: u32 = 1 << 28; - #[cfg(target_os = "macos")] - pub const FUSE_CASE_INSENSITIVE: u32 = 1 << 29; - #[cfg(target_os = "macos")] - pub const FUSE_VOL_RENAME: u32 = 1 << 30; - #[cfg(target_os = "macos")] - pub const FUSE_XTIMES: u32 = 1 << 31; - - // CUSE init request/reply flags - #[cfg(feature = "abi-7-12")] - pub const CUSE_UNRESTRICTED_IOCTL: u32 = 1 << 0; // use unrestricted ioctl - - // Release flags - pub const FUSE_RELEASE_FLUSH: u32 = 1 << 0; - #[cfg(feature = "abi-7-17")] - pub const FUSE_RELEASE_FLOCK_UNLOCK: u32 = 1 << 1; - - // Getattr flags - #[cfg(feature = "abi-7-9")] - pub const FUSE_GETATTR_FH: u32 = 1 << 0; - - // Lock flags - #[cfg(feature = "abi-7-9")] - pub const FUSE_LK_FLOCK: u32 = 1 << 0; - - // Write flags - #[cfg(feature = "abi-7-9")] - pub const FUSE_WRITE_CACHE: u32 = 1 << 0; // delayed write from page cache, file handle is guessed - #[cfg(feature = "abi-7-9")] - pub const FUSE_WRITE_LOCKOWNER: u32 = 1 << 1; // lock_owner field is valid - #[cfg(feature = "abi-7-31")] - pub const FUSE_WRITE_KILL_PRIV: u32 = 1 << 2; // kill suid and sgid bits - - // Read flags - #[cfg(feature = "abi-7-9")] - pub const FUSE_READ_LOCKOWNER: u32 = 1 << 1; - - // IOCTL flags - #[cfg(feature = "abi-7-11")] - pub const FUSE_IOCTL_COMPAT: u32 = 1 << 0; // 32bit compat ioctl on 64bit machine - #[cfg(feature = "abi-7-11")] - pub const FUSE_IOCTL_UNRESTRICTED: u32 = 1 << 1; // not restricted to well-formed ioctls, retry allowed - #[cfg(feature = "abi-7-11")] - pub const FUSE_IOCTL_RETRY: u32 = 1 << 2; // retry with new iovecs - #[cfg(feature = "abi-7-16")] - pub const FUSE_IOCTL_32BIT: u32 = 1 << 3; // 32bit ioctl - #[cfg(feature = "abi-7-18")] - pub const FUSE_IOCTL_DIR: u32 = 1 << 4; // is a directory - #[cfg(feature = "abi-7-30")] - pub const FUSE_IOCTL_COMPAT_X32: u32 = 1 << 5; // x32 compat ioctl on 64bit machine (64bit time_t) - #[cfg(feature = "abi-7-11")] - pub const FUSE_IOCTL_MAX_IOV: u32 = 256; // maximum of in_iovecs + out_iovecs - - // Poll flags - #[cfg(feature = "abi-7-9")] - pub const FUSE_POLL_SCHEDULE_NOTIFY: u32 = 1 << 0; // request poll notify - - // fsync flags - pub const FUSE_FSYNC_FDATASYNC: u32 = 1 << 0; // Sync data only, not metadata - - // The read buffer is required to be at least 8k, but may be much larger - pub const FUSE_MIN_READ_BUFFER: usize = 8192; -} - -/// Invalid opcode error. -#[derive(Debug)] -pub struct InvalidOpcodeError; - -#[repr(C)] -#[derive(Debug)] -#[allow(non_camel_case_types)] -pub enum fuse_opcode { - FUSE_LOOKUP = 1, - FUSE_FORGET = 2, // no reply - FUSE_GETATTR = 3, - FUSE_SETATTR = 4, - FUSE_READLINK = 5, - FUSE_SYMLINK = 6, - FUSE_MKNOD = 8, - FUSE_MKDIR = 9, - FUSE_UNLINK = 10, - FUSE_RMDIR = 11, - FUSE_RENAME = 12, - FUSE_LINK = 13, - FUSE_OPEN = 14, - FUSE_READ = 15, - FUSE_WRITE = 16, - FUSE_STATFS = 17, - FUSE_RELEASE = 18, - FUSE_FSYNC = 20, - FUSE_SETXATTR = 21, - FUSE_GETXATTR = 22, - FUSE_LISTXATTR = 23, - FUSE_REMOVEXATTR = 24, - FUSE_FLUSH = 25, - FUSE_INIT = 26, - FUSE_OPENDIR = 27, - FUSE_READDIR = 28, - FUSE_RELEASEDIR = 29, - FUSE_FSYNCDIR = 30, - FUSE_GETLK = 31, - FUSE_SETLK = 32, - FUSE_SETLKW = 33, - FUSE_ACCESS = 34, - FUSE_CREATE = 35, - FUSE_INTERRUPT = 36, - FUSE_BMAP = 37, - FUSE_DESTROY = 38, - #[cfg(feature = "abi-7-11")] - FUSE_IOCTL = 39, - #[cfg(feature = "abi-7-11")] - FUSE_POLL = 40, - #[cfg(feature = "abi-7-15")] - FUSE_NOTIFY_REPLY = 41, - #[cfg(feature = "abi-7-16")] - FUSE_BATCH_FORGET = 42, - #[cfg(feature = "abi-7-19")] - FUSE_FALLOCATE = 43, - #[cfg(feature = "abi-7-21")] - FUSE_READDIRPLUS = 44, - #[cfg(feature = "abi-7-23")] - FUSE_RENAME2 = 45, - #[cfg(feature = "abi-7-24")] - FUSE_LSEEK = 46, - #[cfg(feature = "abi-7-28")] - FUSE_COPY_FILE_RANGE = 47, - - #[cfg(target_os = "macos")] - FUSE_SETVOLNAME = 61, - #[cfg(target_os = "macos")] - FUSE_GETXTIMES = 62, - #[cfg(target_os = "macos")] - FUSE_EXCHANGE = 63, - - #[cfg(feature = "abi-7-12")] - CUSE_INIT = 4096, -} - -impl TryFrom for fuse_opcode { - type Error = InvalidOpcodeError; - - fn try_from(n: u32) -> Result { - match n { - 1 => Ok(fuse_opcode::FUSE_LOOKUP), - 2 => Ok(fuse_opcode::FUSE_FORGET), - 3 => Ok(fuse_opcode::FUSE_GETATTR), - 4 => Ok(fuse_opcode::FUSE_SETATTR), - 5 => Ok(fuse_opcode::FUSE_READLINK), - 6 => Ok(fuse_opcode::FUSE_SYMLINK), - 8 => Ok(fuse_opcode::FUSE_MKNOD), - 9 => Ok(fuse_opcode::FUSE_MKDIR), - 10 => Ok(fuse_opcode::FUSE_UNLINK), - 11 => Ok(fuse_opcode::FUSE_RMDIR), - 12 => Ok(fuse_opcode::FUSE_RENAME), - 13 => Ok(fuse_opcode::FUSE_LINK), - 14 => Ok(fuse_opcode::FUSE_OPEN), - 15 => Ok(fuse_opcode::FUSE_READ), - 16 => Ok(fuse_opcode::FUSE_WRITE), - 17 => Ok(fuse_opcode::FUSE_STATFS), - 18 => Ok(fuse_opcode::FUSE_RELEASE), - 20 => Ok(fuse_opcode::FUSE_FSYNC), - 21 => Ok(fuse_opcode::FUSE_SETXATTR), - 22 => Ok(fuse_opcode::FUSE_GETXATTR), - 23 => Ok(fuse_opcode::FUSE_LISTXATTR), - 24 => Ok(fuse_opcode::FUSE_REMOVEXATTR), - 25 => Ok(fuse_opcode::FUSE_FLUSH), - 26 => Ok(fuse_opcode::FUSE_INIT), - 27 => Ok(fuse_opcode::FUSE_OPENDIR), - 28 => Ok(fuse_opcode::FUSE_READDIR), - 29 => Ok(fuse_opcode::FUSE_RELEASEDIR), - 30 => Ok(fuse_opcode::FUSE_FSYNCDIR), - 31 => Ok(fuse_opcode::FUSE_GETLK), - 32 => Ok(fuse_opcode::FUSE_SETLK), - 33 => Ok(fuse_opcode::FUSE_SETLKW), - 34 => Ok(fuse_opcode::FUSE_ACCESS), - 35 => Ok(fuse_opcode::FUSE_CREATE), - 36 => Ok(fuse_opcode::FUSE_INTERRUPT), - 37 => Ok(fuse_opcode::FUSE_BMAP), - 38 => Ok(fuse_opcode::FUSE_DESTROY), - #[cfg(feature = "abi-7-11")] - 39 => Ok(fuse_opcode::FUSE_IOCTL), - #[cfg(feature = "abi-7-11")] - 40 => Ok(fuse_opcode::FUSE_POLL), - #[cfg(feature = "abi-7-15")] - 41 => Ok(fuse_opcode::FUSE_NOTIFY_REPLY), - #[cfg(feature = "abi-7-16")] - 42 => Ok(fuse_opcode::FUSE_BATCH_FORGET), - #[cfg(feature = "abi-7-19")] - 43 => Ok(fuse_opcode::FUSE_FALLOCATE), - #[cfg(feature = "abi-7-21")] - 44 => Ok(fuse_opcode::FUSE_READDIRPLUS), - #[cfg(feature = "abi-7-23")] - 45 => Ok(fuse_opcode::FUSE_RENAME2), - #[cfg(feature = "abi-7-24")] - 46 => Ok(fuse_opcode::FUSE_LSEEK), - #[cfg(feature = "abi-7-28")] - 47 => Ok(fuse_opcode::FUSE_COPY_FILE_RANGE), - - #[cfg(target_os = "macos")] - 61 => Ok(fuse_opcode::FUSE_SETVOLNAME), - #[cfg(target_os = "macos")] - 62 => Ok(fuse_opcode::FUSE_GETXTIMES), - #[cfg(target_os = "macos")] - 63 => Ok(fuse_opcode::FUSE_EXCHANGE), - - #[cfg(feature = "abi-7-12")] - 4096 => Ok(fuse_opcode::CUSE_INIT), - - _ => Err(InvalidOpcodeError), - } - } -} - -/// Invalid notify code error. -#[cfg(feature = "abi-7-11")] -#[derive(Debug)] -pub struct InvalidNotifyCodeError; - -#[cfg(feature = "abi-7-11")] -#[repr(C)] -#[derive(Debug)] -#[allow(non_camel_case_types)] -pub enum fuse_notify_code { - #[cfg(feature = "abi-7-11")] - FUSE_POLL = 1, - #[cfg(feature = "abi-7-12")] - FUSE_NOTIFY_INVAL_INODE = 2, - #[cfg(feature = "abi-7-12")] - FUSE_NOTIFY_INVAL_ENTRY = 3, - #[cfg(feature = "abi-7-15")] - FUSE_NOTIFY_STORE = 4, - #[cfg(feature = "abi-7-15")] - FUSE_NOTIFY_RETRIEVE = 5, - #[cfg(feature = "abi-7-18")] - FUSE_NOTIFY_DELETE = 6, -} - -#[cfg(feature = "abi-7-11")] -impl TryFrom for fuse_notify_code { - type Error = InvalidNotifyCodeError; - - fn try_from(n: u32) -> Result { - match n { - #[cfg(feature = "abi-7-11")] - 1 => Ok(fuse_notify_code::FUSE_POLL), - #[cfg(feature = "abi-7-12")] - 2 => Ok(fuse_notify_code::FUSE_NOTIFY_INVAL_INODE), - #[cfg(feature = "abi-7-12")] - 3 => Ok(fuse_notify_code::FUSE_NOTIFY_INVAL_ENTRY), - #[cfg(feature = "abi-7-15")] - 4 => Ok(fuse_notify_code::FUSE_NOTIFY_STORE), - #[cfg(feature = "abi-7-15")] - 5 => Ok(fuse_notify_code::FUSE_NOTIFY_RETRIEVE), - #[cfg(feature = "abi-7-18")] - 6 => Ok(fuse_notify_code::FUSE_NOTIFY_DELETE), - - _ => Err(InvalidNotifyCodeError), - } - } -} - -#[repr(C)] -#[derive(Debug, IntoBytes, KnownLayout, Immutable)] -pub struct fuse_entry_out { - pub nodeid: u64, - pub generation: u64, - pub entry_valid: u64, - pub attr_valid: u64, - pub entry_valid_nsec: u32, - pub attr_valid_nsec: u32, - pub attr: fuse_attr, -} - -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_forget_in { - pub nlookup: u64, -} - -#[cfg(feature = "abi-7-16")] -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_forget_one { - pub nodeid: u64, - pub nlookup: u64, -} - -#[cfg(feature = "abi-7-16")] -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_batch_forget_in { - pub count: u32, - pub dummy: u32, -} - -#[cfg(feature = "abi-7-9")] -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_getattr_in { - pub getattr_flags: u32, - pub dummy: u32, - pub fh: u64, -} - -#[repr(C)] -#[derive(Debug, IntoBytes, KnownLayout, Immutable)] -pub struct fuse_attr_out { - pub attr_valid: u64, - pub attr_valid_nsec: u32, - pub dummy: u32, - pub attr: fuse_attr, -} - -#[cfg(target_os = "macos")] -#[repr(C)] -#[derive(Debug, IntoBytes, KnownLayout, Immutable)] -pub struct fuse_getxtimes_out { - pub bkuptime: u64, - pub crtime: u64, - pub bkuptimensec: u32, - pub crtimensec: u32, -} - -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_mknod_in { - pub mode: u32, - pub rdev: u32, - #[cfg(feature = "abi-7-12")] - pub umask: u32, - #[cfg(feature = "abi-7-12")] - pub padding: u32, -} - -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_mkdir_in { - pub mode: u32, - #[cfg(not(feature = "abi-7-12"))] - pub padding: u32, - #[cfg(feature = "abi-7-12")] - pub umask: u32, -} - -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_rename_in { - pub newdir: u64, - #[cfg(feature = "macfuse-4-compat")] - pub flags: u32, - #[cfg(feature = "macfuse-4-compat")] - pub padding: u32, -} - -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_rename2_in { - pub newdir: u64, - pub flags: u32, - pub padding: u32, -} - -#[cfg(target_os = "macos")] -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_exchange_in { - pub olddir: u64, - pub newdir: u64, - pub options: u64, -} - -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_link_in { - pub oldnodeid: u64, -} - -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_setattr_in { - pub valid: u32, - pub padding: u32, - pub fh: u64, - pub size: u64, - #[cfg(not(feature = "abi-7-9"))] - pub unused1: u64, - #[cfg(feature = "abi-7-9")] - pub lock_owner: u64, - // NOTE: this field is defined as u64 in fuse_kernel.h in libfuse. However, it is treated as signed - // to match stat.st_atime - pub atime: i64, - // NOTE: this field is defined as u64 in fuse_kernel.h in libfuse. However, it is treated as signed - // to match stat.st_mtime - pub mtime: i64, - #[cfg(not(feature = "abi-7-23"))] - pub unused2: u64, - #[cfg(feature = "abi-7-23")] - // NOTE: this field is defined as u64 in fuse_kernel.h in libfuse. However, it is treated as signed - // to match stat.st_ctime - pub ctime: i64, - pub atimensec: u32, - pub mtimensec: u32, - #[cfg(not(feature = "abi-7-23"))] - pub unused3: u32, - #[cfg(feature = "abi-7-23")] - pub ctimensec: u32, - pub mode: u32, - pub unused4: u32, - pub uid: u32, - pub gid: u32, - pub unused5: u32, - #[cfg(target_os = "macos")] - pub bkuptime: u64, - #[cfg(target_os = "macos")] - pub chgtime: u64, - #[cfg(target_os = "macos")] - pub crtime: u64, - #[cfg(target_os = "macos")] - pub bkuptimensec: u32, - #[cfg(target_os = "macos")] - pub chgtimensec: u32, - #[cfg(target_os = "macos")] - pub crtimensec: u32, - #[cfg(target_os = "macos")] - pub flags: u32, // see chflags(2) -} - -impl fuse_setattr_in { - #[cfg(feature = "abi-7-9")] - pub fn atime_now(&self) -> bool { - self.valid & FATTR_ATIME_NOW != 0 - } - - #[cfg(not(feature = "abi-7-9"))] - pub fn atime_now(&self) -> bool { - false - } - - #[cfg(feature = "abi-7-9")] - pub fn mtime_now(&self) -> bool { - self.valid & FATTR_MTIME_NOW != 0 - } - - #[cfg(not(feature = "abi-7-9"))] - pub fn mtime_now(&self) -> bool { - false - } -} - -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_open_in { - // NOTE: this field is defined as u32 in fuse_kernel.h in libfuse. However, it is then cast - // to an i32 when invoking the filesystem's open method and this matches the open() syscall - pub flags: i32, - pub unused: u32, -} - -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_create_in { - // NOTE: this field is defined as u32 in fuse_kernel.h in libfuse. However, it is then cast - // to an i32 when invoking the filesystem's create method and this matches the open() syscall - pub flags: i32, - pub mode: u32, - #[cfg(feature = "abi-7-12")] - pub umask: u32, - #[cfg(feature = "abi-7-12")] - pub padding: u32, -} - -#[repr(C)] -#[derive(Debug, IntoBytes, KnownLayout, Immutable)] -pub struct fuse_create_out(pub fuse_entry_out, pub fuse_open_out); - -#[repr(C)] -#[derive(Debug, IntoBytes, KnownLayout, Immutable)] -pub struct fuse_open_out { - pub fh: u64, - pub open_flags: u32, - pub padding: u32, -} - -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_release_in { - pub fh: u64, - // NOTE: this field is defined as u32 in fuse_kernel.h in libfuse. However, it is then cast - // to an i32 when invoking the filesystem's read method - pub flags: i32, - pub release_flags: u32, - pub lock_owner: u64, -} - -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_flush_in { - pub fh: u64, - pub unused: u32, - pub padding: u32, - pub lock_owner: u64, -} - -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_read_in { - pub fh: u64, - // NOTE: this field is defined as u64 in fuse_kernel.h in libfuse. However, it is then cast - // to an i64 when invoking the filesystem's read method - pub offset: i64, - pub size: u32, - #[cfg(feature = "abi-7-9")] - pub read_flags: u32, - #[cfg(feature = "abi-7-9")] - pub lock_owner: u64, - #[cfg(feature = "abi-7-9")] - // NOTE: this field is defined as u32 in fuse_kernel.h in libfuse. However, it is then cast - // to an i32 when invoking the filesystem's read method - pub flags: i32, - #[cfg(feature = "abi-7-9")] - pub padding: u32, -} - -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_write_in { - pub fh: u64, - // NOTE: this field is defined as u64 in fuse_kernel.h in libfuse. However, it is then cast - // to an i64 when invoking the filesystem's write method - pub offset: i64, - pub size: u32, - pub write_flags: u32, - #[cfg(feature = "abi-7-9")] - pub lock_owner: u64, - #[cfg(feature = "abi-7-9")] - // NOTE: this field is defined as u32 in fuse_kernel.h in libfuse. However, it is then cast - // to an i32 when invoking the filesystem's read method - pub flags: i32, - #[cfg(feature = "abi-7-9")] - pub padding: u32, -} - -#[repr(C)] -#[derive(Debug, IntoBytes, KnownLayout, Immutable)] -pub struct fuse_write_out { - pub size: u32, - pub padding: u32, -} - -#[repr(C)] -#[derive(Debug, IntoBytes, KnownLayout, Immutable)] -pub struct fuse_statfs_out { - pub st: fuse_kstatfs, -} - -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_fsync_in { - pub fh: u64, - pub fsync_flags: u32, - pub padding: u32, -} - -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_setxattr_in { - pub size: u32, - // NOTE: this field is defined as u32 in fuse_kernel.h in libfuse. However, it is then cast - // to an i32 when invoking the filesystem's setxattr method - pub flags: i32, - #[cfg(target_os = "macos")] - pub position: u32, - #[cfg(target_os = "macos")] - pub padding: u32, -} - -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_getxattr_in { - pub size: u32, - pub padding: u32, - #[cfg(target_os = "macos")] - pub position: u32, - #[cfg(target_os = "macos")] - pub padding2: u32, -} - -#[repr(C)] -#[derive(Debug, IntoBytes, KnownLayout, Immutable)] -pub struct fuse_getxattr_out { - pub size: u32, - pub padding: u32, -} - -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_lk_in { - pub fh: u64, - pub owner: u64, - pub lk: fuse_file_lock, - #[cfg(feature = "abi-7-9")] - pub lk_flags: u32, - #[cfg(feature = "abi-7-9")] - pub padding: u32, -} - -#[repr(C)] -#[derive(Debug, IntoBytes, KnownLayout, Immutable)] -pub struct fuse_lk_out { - pub lk: fuse_file_lock, -} - -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_access_in { - // NOTE: this field is defined as u32 in fuse_kernel.h in libfuse. However, it is then cast - // to an i32 when invoking the filesystem's access method - pub mask: i32, - pub padding: u32, -} - -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_init_in { - pub major: u32, - pub minor: u32, - pub max_readahead: u32, - pub flags: u32, -} - -#[repr(C)] -#[derive(Debug, IntoBytes, KnownLayout, Immutable)] -pub struct fuse_init_out { - pub major: u32, - pub minor: u32, - pub max_readahead: u32, - pub flags: u32, - #[cfg(not(feature = "abi-7-13"))] - pub unused: u32, - #[cfg(feature = "abi-7-13")] - pub max_background: u16, - #[cfg(feature = "abi-7-13")] - pub congestion_threshold: u16, - pub max_write: u32, - #[cfg(feature = "abi-7-23")] - pub time_gran: u32, - #[cfg(all(feature = "abi-7-23", not(feature = "abi-7-28")))] - pub reserved: [u32; 9], - #[cfg(feature = "abi-7-28")] - pub max_pages: u16, - #[cfg(feature = "abi-7-28")] - pub unused2: u16, - #[cfg(feature = "abi-7-28")] - pub reserved: [u32; 8], -} - -#[cfg(feature = "abi-7-12")] -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct cuse_init_in { - pub major: u32, - pub minor: u32, - pub unused: u32, - pub flags: u32, -} - -#[cfg(feature = "abi-7-12")] -#[repr(C)] -#[derive(Debug, KnownLayout, Immutable)] -pub struct cuse_init_out { - pub major: u32, - pub minor: u32, - pub unused: u32, - pub flags: u32, - pub max_read: u32, - pub max_write: u32, - pub dev_major: u32, // chardev major - pub dev_minor: u32, // chardev minor - pub spare: [u32; 10], -} - -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_interrupt_in { - pub unique: u64, -} - -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_bmap_in { - pub block: u64, - pub blocksize: u32, - pub padding: u32, -} - -#[repr(C)] -#[derive(Debug, IntoBytes, KnownLayout, Immutable)] -pub struct fuse_bmap_out { - pub block: u64, -} - -#[cfg(feature = "abi-7-11")] -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_ioctl_in { - pub fh: u64, - pub flags: u32, - pub cmd: u32, - pub arg: u64, // TODO: this is currently unused, but is defined as a void* in libfuse - pub in_size: u32, - pub out_size: u32, -} - -#[cfg(feature = "abi-7-16")] -#[repr(C)] -#[derive(Debug, KnownLayout, Immutable)] -pub struct fuse_ioctl_iovec { - pub base: u64, - pub len: u64, -} - -#[repr(C)] -#[derive(Debug, IntoBytes, KnownLayout, Immutable)] -pub struct fuse_ioctl_out { - pub result: i32, - pub flags: u32, - pub in_iovs: u32, - pub out_iovs: u32, -} - -#[cfg(feature = "abi-7-11")] -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_poll_in { - pub fh: u64, - pub kh: u64, - pub flags: u32, - #[cfg(not(feature = "abi-7-21"))] - pub padding: u32, - #[cfg(feature = "abi-7-21")] - pub events: u32, -} - -#[cfg(feature = "abi-7-11")] -#[repr(C)] -#[derive(Debug, IntoBytes, KnownLayout, Immutable)] -pub struct fuse_poll_out { - pub revents: u32, - pub padding: u32, -} - -#[cfg(feature = "abi-7-11")] -#[repr(C)] -#[derive(Debug, IntoBytes, KnownLayout, Immutable)] -pub struct fuse_notify_poll_wakeup_out { - pub kh: u64, -} - -#[cfg(feature = "abi-7-19")] -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_fallocate_in { - pub fh: u64, - // NOTE: this field is defined as u64 in fuse_kernel.h in libfuse. However, it is treated as signed - pub offset: i64, - // NOTE: this field is defined as u64 in fuse_kernel.h in libfuse. However, it is treated as signed - pub length: i64, - // NOTE: this field is defined as u32 in fuse_kernel.h in libfuse. However, it is treated as signed - pub mode: i32, - pub padding: u32, -} - -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_in_header { - pub len: u32, - pub opcode: u32, - pub unique: u64, - pub nodeid: u64, - pub uid: u32, - pub gid: u32, - pub pid: u32, - pub padding: u32, -} - -#[repr(C)] -#[derive(Debug, IntoBytes, KnownLayout, Immutable)] -pub struct fuse_out_header { - pub len: u32, - pub error: i32, - pub unique: u64, -} - -#[repr(C)] -#[derive(Debug, IntoBytes, KnownLayout, Immutable)] -pub struct fuse_dirent { - pub ino: u64, - // NOTE: this field is defined as u64 in fuse_kernel.h in libfuse. However, it is treated as signed - pub off: i64, - pub namelen: u32, - pub typ: u32, - // followed by name of namelen bytes -} - -#[repr(C)] -#[derive(Debug, IntoBytes, KnownLayout, Immutable)] -pub struct fuse_direntplus { - pub entry_out: fuse_entry_out, - pub dirent: fuse_dirent, -} - -#[cfg(feature = "abi-7-12")] -#[repr(C)] -#[derive(Debug, IntoBytes, KnownLayout, Immutable)] -pub struct fuse_notify_inval_inode_out { - pub ino: u64, - pub off: i64, - pub len: i64, -} - -#[cfg(feature = "abi-7-12")] -#[repr(C)] -#[derive(Debug, IntoBytes, KnownLayout, Immutable)] -pub struct fuse_notify_inval_entry_out { - pub parent: u64, - pub namelen: u32, - pub padding: u32, -} - -#[cfg(feature = "abi-7-18")] -#[repr(C)] -#[derive(Debug, IntoBytes, KnownLayout, Immutable)] -pub struct fuse_notify_delete_out { - pub parent: u64, - pub child: u64, - pub namelen: u32, - pub padding: u32, -} - -#[cfg(feature = "abi-7-15")] -#[repr(C)] -#[derive(Debug, IntoBytes, KnownLayout, Immutable)] -pub struct fuse_notify_store_out { - pub nodeid: u64, - pub offset: u64, - pub size: u32, - pub padding: u32, -} - -#[cfg(feature = "abi-7-15")] -#[repr(C)] -#[derive(Debug, KnownLayout, Immutable)] -pub struct fuse_notify_retrieve_out { - pub notify_unique: u64, - pub nodeid: u64, - pub offset: u64, - pub size: u32, - pub padding: u32, -} - -#[cfg(feature = "abi-7-15")] -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_notify_retrieve_in { - // matches the size of fuse_write_in - pub dummy1: u64, - pub offset: u64, - pub size: u32, - pub dummy2: u32, - pub dummy3: u64, - pub dummy4: u64, -} - -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_lseek_in { - pub fh: u64, - pub offset: i64, - // NOTE: this field is defined as u32 in fuse_kernel.h in libfuse. However, it is treated as signed - pub whence: i32, - pub padding: u32, -} - -#[repr(C)] -#[derive(Debug, IntoBytes, KnownLayout, Immutable)] -pub struct fuse_lseek_out { - pub offset: i64, -} - -#[repr(C)] -#[derive(Debug, FromBytes, KnownLayout, Immutable)] -pub struct fuse_copy_file_range_in { - pub fh_in: u64, - // NOTE: this field is defined as u64 in fuse_kernel.h in libfuse. However, it is treated as signed - pub off_in: i64, - pub nodeid_out: u64, - pub fh_out: u64, - // NOTE: this field is defined as u64 in fuse_kernel.h in libfuse. However, it is treated as signed - pub off_out: i64, - pub len: u64, - pub flags: u64, -} diff --git a/vendor/fuser/src/ll/mod.rs b/vendor/fuser/src/ll/mod.rs deleted file mode 100644 index 860d0283a..000000000 --- a/vendor/fuser/src/ll/mod.rs +++ /dev/null @@ -1,297 +0,0 @@ -//! Low-level kernel communication. - -mod argument; -pub mod fuse_abi; -#[cfg(feature = "abi-7-11")] -pub(crate) mod notify; -pub(crate) mod reply; -mod request; - -use std::{convert::TryInto, num::NonZeroI32, time::SystemTime}; - -pub use reply::Response; -pub use request::{AnyRequest, FileHandle, INodeNo, Lock, Operation, Request, RequestId, Version}; - -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -/// Possible input arguments for atime & mtime, which can either be set to a specified time, -/// or to the current time -pub enum TimeOrNow { - /// Specific time provided - SpecificTime(SystemTime), - /// Current time - Now, -} - -macro_rules! errno { - ($x: expr) => { - Errno(unsafe { - // This is a static assertion that the constant $x is > 0 - const _X: [(); 0 - !{ - const ASSERT: bool = ($x > 0); - ASSERT - } as usize] = []; - // Which makes this safe - NonZeroI32::new_unchecked($x) - }) - }; -} - -/// Represents an error code to be returned to the caller -#[derive(Debug)] -pub struct Errno(pub NonZeroI32); -impl Errno { - /// Operation not permitted - pub const EPERM: Errno = errno!(libc::EPERM); - /// No such file or directory - pub const ENOENT: Errno = errno!(libc::ENOENT); - /// No such process - pub const ESRCH: Errno = errno!(libc::ESRCH); - /// Interrupted system call - pub const EINTR: Errno = errno!(libc::EINTR); - /// Input/output error - pub const EIO: Errno = errno!(libc::EIO); - /// No such device or address - pub const ENXIO: Errno = errno!(libc::ENXIO); - /// Argument list too long - pub const E2BIG: Errno = errno!(libc::E2BIG); - /// Exec format error - pub const ENOEXEC: Errno = errno!(libc::ENOEXEC); - /// Bad file descriptor - pub const EBADF: Errno = errno!(libc::EBADF); - /// No child processes - pub const ECHILD: Errno = errno!(libc::ECHILD); - /// Resource temporarily unavailable - pub const EAGAIN: Errno = errno!(libc::EAGAIN); - /// Cannot allocate memory - pub const ENOMEM: Errno = errno!(libc::ENOMEM); - /// Permission denied - pub const EACCES: Errno = errno!(libc::EACCES); - /// Bad address - pub const EFAULT: Errno = errno!(libc::EFAULT); - /// Block device required - pub const ENOTBLK: Errno = errno!(libc::ENOTBLK); - /// Device or resource busy - pub const EBUSY: Errno = errno!(libc::EBUSY); - /// File exists - pub const EEXIST: Errno = errno!(libc::EEXIST); - /// Invalid cross-device link - pub const EXDEV: Errno = errno!(libc::EXDEV); - /// No such device - pub const ENODEV: Errno = errno!(libc::ENODEV); - /// Not a directory - pub const ENOTDIR: Errno = errno!(libc::ENOTDIR); - /// Is a directory - pub const EISDIR: Errno = errno!(libc::EISDIR); - /// Invalid argument - pub const EINVAL: Errno = errno!(libc::EINVAL); - /// Too many open files in system - pub const ENFILE: Errno = errno!(libc::ENFILE); - /// Too many open files - pub const EMFILE: Errno = errno!(libc::EMFILE); - /// Inappropriate ioctl for device - pub const ENOTTY: Errno = errno!(libc::ENOTTY); - /// Text file busy - pub const ETXTBSY: Errno = errno!(libc::ETXTBSY); - /// File too large - pub const EFBIG: Errno = errno!(libc::EFBIG); - /// No space left on device - pub const ENOSPC: Errno = errno!(libc::ENOSPC); - /// Illegal seek - pub const ESPIPE: Errno = errno!(libc::ESPIPE); - /// Read-only file system - pub const EROFS: Errno = errno!(libc::EROFS); - /// Too many links - pub const EMLINK: Errno = errno!(libc::EMLINK); - /// Broken pipe - pub const EPIPE: Errno = errno!(libc::EPIPE); - /// Numerical argument out of domain - pub const EDOM: Errno = errno!(libc::EDOM); - /// Numerical result out of range - pub const ERANGE: Errno = errno!(libc::ERANGE); - /// Resource deadlock avoided - pub const EDEADLK: Errno = errno!(libc::EDEADLK); - /// File name too long - pub const ENAMETOOLONG: Errno = errno!(libc::ENAMETOOLONG); - /// No locks available - pub const ENOLCK: Errno = errno!(libc::ENOLCK); - /// Function not implemented - pub const ENOSYS: Errno = errno!(libc::ENOSYS); - /// Directory not empty - pub const ENOTEMPTY: Errno = errno!(libc::ENOTEMPTY); - /// Too many levels of symbolic links - pub const ELOOP: Errno = errno!(libc::ELOOP); - /// Resource temporarily unavailable - pub const EWOULDBLOCK: Errno = errno!(libc::EWOULDBLOCK); - /// No message of desired type - pub const ENOMSG: Errno = errno!(libc::ENOMSG); - /// Identifier removed - pub const EIDRM: Errno = errno!(libc::EIDRM); - /// Object is remote - pub const EREMOTE: Errno = errno!(libc::EREMOTE); - /// Link has been severed - pub const ENOLINK: Errno = errno!(libc::ENOLINK); - /// Protocol error - pub const EPROTO: Errno = errno!(libc::EPROTO); - /// Multihop attempted - pub const EMULTIHOP: Errno = errno!(libc::EMULTIHOP); - /// Bad message - pub const EBADMSG: Errno = errno!(libc::EBADMSG); - /// Value too large for defined data type - pub const EOVERFLOW: Errno = errno!(libc::EOVERFLOW); - /// Invalid or incomplete multibyte or wide character - pub const EILSEQ: Errno = errno!(libc::EILSEQ); - /// Too many users - pub const EUSERS: Errno = errno!(libc::EUSERS); - /// Socket operation on non-socket - pub const ENOTSOCK: Errno = errno!(libc::ENOTSOCK); - /// Destination address required - pub const EDESTADDRREQ: Errno = errno!(libc::EDESTADDRREQ); - /// Message too long - pub const EMSGSIZE: Errno = errno!(libc::EMSGSIZE); - /// Protocol wrong type for socket - pub const EPROTOTYPE: Errno = errno!(libc::EPROTOTYPE); - /// Protocol not available - pub const ENOPROTOOPT: Errno = errno!(libc::ENOPROTOOPT); - /// Protocol not supported - pub const EPROTONOSUPPORT: Errno = errno!(libc::EPROTONOSUPPORT); - /// Socket type not supported - pub const ESOCKTNOSUPPORT: Errno = errno!(libc::ESOCKTNOSUPPORT); - /// Operation not supported - pub const EOPNOTSUPP: Errno = errno!(libc::EOPNOTSUPP); - /// Protocol family not supported - pub const EPFNOSUPPORT: Errno = errno!(libc::EPFNOSUPPORT); - /// Address family not supported by protocol - pub const EAFNOSUPPORT: Errno = errno!(libc::EAFNOSUPPORT); - /// Address already in use - pub const EADDRINUSE: Errno = errno!(libc::EADDRINUSE); - /// Cannot assign requested address - pub const EADDRNOTAVAIL: Errno = errno!(libc::EADDRNOTAVAIL); - /// Network is down - pub const ENETDOWN: Errno = errno!(libc::ENETDOWN); - /// Network is unreachable - pub const ENETUNREACH: Errno = errno!(libc::ENETUNREACH); - /// Network dropped connection on reset - pub const ENETRESET: Errno = errno!(libc::ENETRESET); - /// Software caused connection abort - pub const ECONNABORTED: Errno = errno!(libc::ECONNABORTED); - /// Connection reset by peer - pub const ECONNRESET: Errno = errno!(libc::ECONNRESET); - /// No buffer space available - pub const ENOBUFS: Errno = errno!(libc::ENOBUFS); - /// Transport endpoint is already connected - pub const EISCONN: Errno = errno!(libc::EISCONN); - /// Transport endpoint is not connected - pub const ENOTCONN: Errno = errno!(libc::ENOTCONN); - /// Cannot send after transport endpoint shutdown - pub const ESHUTDOWN: Errno = errno!(libc::ESHUTDOWN); - /// Too many references: cannot splice - pub const ETOOMANYREFS: Errno = errno!(libc::ETOOMANYREFS); - /// Connection timed out - pub const ETIMEDOUT: Errno = errno!(libc::ETIMEDOUT); - /// Connection refused - pub const ECONNREFUSED: Errno = errno!(libc::ECONNREFUSED); - /// Host is down - pub const EHOSTDOWN: Errno = errno!(libc::EHOSTDOWN); - /// No route to host - pub const EHOSTUNREACH: Errno = errno!(libc::EHOSTUNREACH); - /// Operation already in progress - pub const EALREADY: Errno = errno!(libc::EALREADY); - /// Operation now in progress - pub const EINPROGRESS: Errno = errno!(libc::EINPROGRESS); - /// Stale file handle - pub const ESTALE: Errno = errno!(libc::ESTALE); - /// Disk quota exceeded - pub const EDQUOT: Errno = errno!(libc::EDQUOT); - /// Operation cancelled - pub const ECANCELED: Errno = errno!(libc::ECANCELED); - /// Owner died - pub const EOWNERDEAD: Errno = errno!(libc::EOWNERDEAD); - /// State not recoverable - pub const ENOTRECOVERABLE: Errno = errno!(libc::ENOTRECOVERABLE); - /// Operation not supported - pub const ENOTSUP: Errno = errno!(libc::ENOTSUP); - - /// No data available - #[cfg(target_os = "linux")] - pub const ENODATA: Errno = errno!(libc::ENODATA); - /// Attribute not found - #[cfg(not(target_os = "linux"))] - pub const ENOATTR: Errno = errno!(libc::ENOATTR); - - /// Use this as an error return from getxattr/removexattr to indicate that the xattr doesn't - /// exist. This resolves to the appropriate platform specific error code. - #[cfg(target_os = "linux")] - pub const NO_XATTR: Errno = Self::ENODATA; - #[cfg(not(target_os = "linux"))] - pub const NO_XATTR: Errno = Self::ENOATTR; - - pub fn from_i32(err: i32) -> Errno { - err.try_into().ok().map(Errno).unwrap_or(Errno::EIO) - } -} -impl From for Errno { - fn from(err: std::io::Error) -> Self { - let errno = err.raw_os_error().unwrap_or(0); - match errno.try_into() { - Ok(i) => Errno(i), - Err(_) => Errno::EIO, - } - } -} -impl From for Errno { - fn from(x: std::io::ErrorKind) -> Self { - let err: std::io::Error = x.into(); - err.into() - } -} -impl From for i32 { - fn from(x: Errno) -> Self { - x.0.into() - } -} - -/// A newtype for generation numbers -/// -/// If the file system will be exported over NFS, the (ino, generation) pairs -/// need to be unique over the file system's lifetime (rather than just the -/// mount time). So if the file system reuses an inode after it has been -/// deleted, it must assign a new, previously unused generation number to the -/// inode at the same time. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct Generation(pub u64); -impl From for u64 { - fn from(fh: Generation) -> Self { - fh.0 - } -} - -#[cfg(test)] -mod test { - use std::io::IoSlice; - use std::ops::{Deref, DerefMut}; - /// If we want to be able to cast bytes to our fuse C struct types we need it - /// to be aligned. This struct helps getting &[u8]s which are 8 byte aligned. - #[cfg(test)] - #[repr(align(8))] - pub(crate) struct AlignedData(pub T); - impl Deref for AlignedData { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.0 - } - } - impl DerefMut for AlignedData { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } - } - - pub fn ioslice_to_vec(s: &[IoSlice<'_>]) -> Vec { - let mut v = Vec::with_capacity(s.iter().map(|x| x.len()).sum()); - for x in s { - v.extend_from_slice(x); - } - v - } -} diff --git a/vendor/fuser/src/ll/notify.rs b/vendor/fuser/src/ll/notify.rs deleted file mode 100644 index 668739b79..000000000 --- a/vendor/fuser/src/ll/notify.rs +++ /dev/null @@ -1,215 +0,0 @@ -use std::{convert::TryInto, io::IoSlice, mem::size_of, num::TryFromIntError}; - -#[allow(unused)] -use std::{ffi::OsStr, os::unix::ffi::OsStrExt}; - -use smallvec::{smallvec, SmallVec}; -use zerocopy::{Immutable, IntoBytes}; - -use super::fuse_abi as abi; - -const INLINE_DATA_THRESHOLD: usize = size_of::() * 4; -type NotificationBuf = SmallVec<[u8; INLINE_DATA_THRESHOLD]>; - -#[derive(Debug)] -pub(crate) enum Notification<'a> { - /// For notifications with no additional data - Bare(NotificationBuf), - - /// For notifications that include a buffer of arbitrary data - #[allow(dead_code)] - WithData(NotificationBuf, &'a [u8]), - - /// For notifications that include a NUL-terminated name - /// (directory entry) - #[allow(unused)] - WithName(NotificationBuf, &'a [u8]), -} - -impl<'a> Notification<'a> { - pub(crate) fn with_iovec]) -> T, T>( - &self, - code: abi::fuse_notify_code, - f: F, - ) -> Result { - let datalen = match &self { - Notification::Bare(b) => b.len(), - Notification::WithData(b, d) => b.len() + d.len(), - Notification::WithName(b, n) => b.len() + n.len() + 1, // +1 because we need to NUL-terminate the name - }; - let header = abi::fuse_out_header { - unique: 0, - error: code as i32, - len: (size_of::() + datalen).try_into()?, - }; - let mut v: SmallVec<[IoSlice<'_>; 4]> = smallvec![IoSlice::new(header.as_bytes())]; - match &self { - Notification::Bare(b) => v.push(IoSlice::new(b)), - Notification::WithData(b, d) => { - v.push(IoSlice::new(b)); - v.push(IoSlice::new(d)); - } - Notification::WithName(b, n) => { - v.push(IoSlice::new(b)); - v.push(IoSlice::new(n)); - v.push(IoSlice::new(&[0u8])); // NUL terminator required by fuse - } - } - Ok(f(&v)) - } - - #[cfg(feature = "abi-7-12")] - pub(crate) fn new_inval_entry(parent: u64, name: &'a OsStr) -> Result { - let r = abi::fuse_notify_inval_entry_out { - parent, - namelen: name.len().try_into()?, - padding: 0, - }; - Ok(Self::from_struct_with_name(&r, name.as_bytes())) - } - - #[cfg(feature = "abi-7-12")] - pub(crate) fn new_inval_inode(ino: u64, offset: i64, len: i64) -> Self { - let r = abi::fuse_notify_inval_inode_out { - ino, - off: offset, - len, - }; - Self::from_struct(&r) - } - - #[cfg(feature = "abi-7-15")] - pub(crate) fn new_store( - ino: u64, - offset: u64, - data: &'a [u8], - ) -> Result { - let r = abi::fuse_notify_store_out { - nodeid: ino, - offset, - size: data.len().try_into()?, - padding: 0, - }; - Ok(Self::from_struct_with_data(&r, data)) - } - - #[cfg(feature = "abi-7-18")] - pub(crate) fn new_delete( - parent: u64, - child: u64, - name: &'a OsStr, - ) -> Result { - let r = abi::fuse_notify_delete_out { - parent, - child, - namelen: name.len().try_into()?, - padding: 0, - }; - Ok(Self::from_struct_with_name(&r, name.as_bytes())) - } - - #[cfg(feature = "abi-7-11")] - pub(crate) fn new_poll(kh: u64) -> Self { - let r = abi::fuse_notify_poll_wakeup_out { kh }; - Self::from_struct(&r) - } - - fn from_struct(data: &T) -> Self { - Self::Bare(data.as_bytes().into()) - } - - #[allow(unused)] - fn from_struct_with_name(buf: &T, name: &'a [u8]) -> Self { - Self::WithName(buf.as_bytes().into(), name) - } - - #[allow(dead_code)] - fn from_struct_with_data(buf: &T, data: &'a [u8]) -> Self { - Self::WithData(buf.as_bytes().into(), data) - } -} - -#[cfg(test)] -mod test { - use super::super::test::ioslice_to_vec; - use super::*; - - #[test] - #[cfg(feature = "abi-7-12")] - fn inval_entry() { - let n = Notification::new_inval_entry(0x42, OsStr::new("abc")) - .unwrap() - .with_iovec( - abi::fuse_notify_code::FUSE_NOTIFY_INVAL_ENTRY, - ioslice_to_vec, - ) - .unwrap(); - let expected = vec![ - 0x24, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x61, 0x62, 0x63, 0x00, - ]; - assert_eq!(n, expected); - } - - #[test] - #[cfg(feature = "abi-7-12")] - fn inval_inode() { - let n = Notification::new_inval_inode(0x42, 100, 200) - .with_iovec( - abi::fuse_notify_code::FUSE_NOTIFY_INVAL_INODE, - ioslice_to_vec, - ) - .unwrap(); - let expected = vec![ - 0x28, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xc8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ]; - assert_eq!(n, expected); - } - - #[test] - #[cfg(feature = "abi-7-15")] - fn store() { - let n = Notification::new_store(0x42, 50, &[0xde, 0xad, 0xbe, 0xef]) - .unwrap() - .with_iovec(abi::fuse_notify_code::FUSE_NOTIFY_STORE, ioslice_to_vec) - .unwrap(); - let expected = vec![ - 0x2c, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xde, 0xad, - 0xbe, 0xef, - ]; - assert_eq!(n, expected); - } - - #[test] - #[cfg(feature = "abi-7-18")] - fn delete() { - let n = Notification::new_inval_entry(0x42, OsStr::new("abc")) - .unwrap() - .with_iovec(abi::fuse_notify_code::FUSE_NOTIFY_DELETE, ioslice_to_vec) - .unwrap(); - let expected = vec![ - 0x24, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x61, 0x62, 0x63, 0x00, - ]; - assert_eq!(n, expected); - } - - #[test] - #[cfg(feature = "abi-7-11")] - fn poll() { - let n = Notification::new_poll(0x4321) - .with_iovec(abi::fuse_notify_code::FUSE_POLL, ioslice_to_vec) - .unwrap(); - let expected = vec![ - 0x18, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x21, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ]; - assert_eq!(n, expected); - } -} diff --git a/vendor/fuser/src/ll/reply.rs b/vendor/fuser/src/ll/reply.rs deleted file mode 100644 index 9cc5bd240..000000000 --- a/vendor/fuser/src/ll/reply.rs +++ /dev/null @@ -1,882 +0,0 @@ -use std::{ - convert::TryInto, - io::IoSlice, - mem::size_of, - os::unix::prelude::OsStrExt, - path::Path, - time::{Duration, SystemTime, UNIX_EPOCH}, -}; - -use crate::FileType; - -use super::{fuse_abi as abi, Errno, FileHandle, Generation, INodeNo}; -use super::{Lock, RequestId}; -use smallvec::{smallvec, SmallVec}; -use zerocopy::{Immutable, IntoBytes}; - -const INLINE_DATA_THRESHOLD: usize = size_of::() * 4; -pub(crate) type ResponseBuf = SmallVec<[u8; INLINE_DATA_THRESHOLD]>; - -#[derive(Debug)] -pub enum Response<'a> { - Error(i32), - Data(ResponseBuf), - Slice(&'a [u8]), -} - -impl<'a> Response<'a> { - pub(crate) fn with_iovec]) -> T, T>( - &self, - unique: RequestId, - f: F, - ) -> T { - let datalen = match &self { - Response::Error(_) => 0, - Response::Data(v) => v.len(), - Response::Slice(d) => d.len(), - }; - let header = abi::fuse_out_header { - unique: unique.0, - error: if let Response::Error(errno) = self { - -errno - } else { - 0 - }, - len: (size_of::() + datalen) - .try_into() - .expect("Too much data"), - }; - let mut v: SmallVec<[IoSlice<'_>; 3]> = smallvec![IoSlice::new(header.as_bytes())]; - match &self { - Response::Error(_) => {} - Response::Data(d) => v.push(IoSlice::new(d)), - Response::Slice(d) => v.push(IoSlice::new(d)), - } - f(&v) - } - - // Constructors - pub(crate) fn new_empty() -> Self { - Self::Error(0) - } - - pub(crate) fn new_error(error: Errno) -> Self { - Self::Error(error.into()) - } - - pub(crate) fn new_data + Into>>(data: T) -> Self { - Self::Data(if data.as_ref().len() <= INLINE_DATA_THRESHOLD { - ResponseBuf::from_slice(data.as_ref()) - } else { - ResponseBuf::from_vec(data.into()) - }) - } - - pub(crate) fn new_slice(data: &'a [u8]) -> Self { - Self::Slice(data) - } - - pub(crate) fn new_entry( - ino: INodeNo, - generation: Generation, - attr: &Attr, - attr_ttl: Duration, - entry_ttl: Duration, - ) -> Self { - let d = abi::fuse_entry_out { - nodeid: ino.into(), - generation: generation.into(), - entry_valid: entry_ttl.as_secs(), - attr_valid: attr_ttl.as_secs(), - entry_valid_nsec: entry_ttl.subsec_nanos(), - attr_valid_nsec: attr_ttl.subsec_nanos(), - attr: attr.attr, - }; - Self::from_struct(d.as_bytes()) - } - - pub(crate) fn new_attr(ttl: &Duration, attr: &Attr) -> Self { - let r = abi::fuse_attr_out { - attr_valid: ttl.as_secs(), - attr_valid_nsec: ttl.subsec_nanos(), - dummy: 0, - attr: attr.attr, - }; - Self::from_struct(&r) - } - - #[cfg(target_os = "macos")] - pub(crate) fn new_xtimes(bkuptime: SystemTime, crtime: SystemTime) -> Self { - let (bkuptime_secs, bkuptime_nanos) = time_from_system_time(&bkuptime); - let (crtime_secs, crtime_nanos) = time_from_system_time(&crtime); - let r = abi::fuse_getxtimes_out { - bkuptime: bkuptime_secs as u64, - crtime: crtime_secs as u64, - bkuptimensec: bkuptime_nanos, - crtimensec: crtime_nanos, - }; - Self::from_struct(&r) - } - - // TODO: Could flags be more strongly typed? - pub(crate) fn new_open(fh: FileHandle, flags: u32) -> Self { - let r = abi::fuse_open_out { - fh: fh.into(), - open_flags: flags, - padding: 0, - }; - Self::from_struct(&r) - } - - pub(crate) fn new_lock(lock: &Lock) -> Self { - let r = abi::fuse_lk_out { - lk: abi::fuse_file_lock { - start: lock.range.0, - end: lock.range.1, - typ: lock.typ, - pid: lock.pid, - }, - }; - Self::from_struct(&r) - } - - pub(crate) fn new_bmap(block: u64) -> Self { - let r = abi::fuse_bmap_out { block }; - Self::from_struct(&r) - } - - pub(crate) fn new_write(written: u32) -> Self { - let r = abi::fuse_write_out { - size: written, - padding: 0, - }; - Self::from_struct(&r) - } - - #[allow(clippy::too_many_arguments)] - pub(crate) fn new_statfs( - blocks: u64, - bfree: u64, - bavail: u64, - files: u64, - ffree: u64, - bsize: u32, - namelen: u32, - frsize: u32, - ) -> Self { - let r = abi::fuse_statfs_out { - st: abi::fuse_kstatfs { - blocks, - bfree, - bavail, - files, - ffree, - bsize, - namelen, - frsize, - padding: 0, - spare: [0; 6], - }, - }; - Self::from_struct(&r) - } - - // TODO: Can flags be more strongly typed? - pub(crate) fn new_create( - ttl: &Duration, - attr: &Attr, - generation: Generation, - fh: FileHandle, - flags: u32, - ) -> Self { - let r = abi::fuse_create_out( - abi::fuse_entry_out { - nodeid: attr.attr.ino, - generation: generation.into(), - entry_valid: ttl.as_secs(), - attr_valid: ttl.as_secs(), - entry_valid_nsec: ttl.subsec_nanos(), - attr_valid_nsec: ttl.subsec_nanos(), - attr: attr.attr, - }, - abi::fuse_open_out { - fh: fh.into(), - open_flags: flags, - padding: 0, - }, - ); - Self::from_struct(&r) - } - - // TODO: Are you allowed to send data while result != 0? - pub(crate) fn new_ioctl(result: i32, data: &[IoSlice<'_>]) -> Self { - let r = abi::fuse_ioctl_out { - result, - // these fields are only needed for unrestricted ioctls - flags: 0, - in_iovs: 1, - out_iovs: if !data.is_empty() { 1 } else { 0 }, - }; - // TODO: Don't copy this data - let mut v: ResponseBuf = ResponseBuf::from_slice(r.as_bytes()); - for x in data { - v.extend_from_slice(x) - } - Self::Data(v) - } - - #[cfg(feature = "abi-7-11")] - pub(crate) fn new_poll(revents: u32) -> Self { - let r = abi::fuse_poll_out { - revents, - padding: 0, - }; - Self::from_struct(&r) - } - - fn new_directory(list: EntListBuf) -> Self { - assert!(list.buf.len() <= list.max_size); - Self::Data(list.buf) - } - - pub(crate) fn new_xattr_size(size: u32) -> Self { - let r = abi::fuse_getxattr_out { size, padding: 0 }; - Self::from_struct(&r) - } - - pub(crate) fn new_lseek(offset: i64) -> Self { - let r = abi::fuse_lseek_out { offset }; - Self::from_struct(&r) - } - - fn from_struct(data: &T) -> Self { - Self::Data(SmallVec::from_slice(data.as_bytes())) - } -} - -pub(crate) fn time_from_system_time(system_time: &SystemTime) -> (i64, u32) { - // Convert to signed 64-bit time with epoch at 0 - match system_time.duration_since(UNIX_EPOCH) { - Ok(duration) => (duration.as_secs() as i64, duration.subsec_nanos()), - Err(before_epoch_error) => ( - -(before_epoch_error.duration().as_secs() as i64), - before_epoch_error.duration().subsec_nanos(), - ), - } -} -// Some platforms like Linux x86_64 have mode_t = u32, and lint warns of a trivial_numeric_casts. -// But others like macOS x86_64 have mode_t = u16, requiring a typecast. So, just silence lint. -#[allow(trivial_numeric_casts)] -#[allow(clippy::unnecessary_cast)] -/// Returns the mode for a given file kind and permission -pub(crate) fn mode_from_kind_and_perm(kind: FileType, perm: u16) -> u32 { - (match kind { - FileType::NamedPipe => libc::S_IFIFO, - FileType::CharDevice => libc::S_IFCHR, - FileType::BlockDevice => libc::S_IFBLK, - FileType::Directory => libc::S_IFDIR, - FileType::RegularFile => libc::S_IFREG, - FileType::Symlink => libc::S_IFLNK, - FileType::Socket => libc::S_IFSOCK, - }) as u32 - | perm as u32 -} -/// Returns a fuse_attr from FileAttr -pub(crate) fn fuse_attr_from_attr(attr: &crate::FileAttr) -> abi::fuse_attr { - let (atime_secs, atime_nanos) = time_from_system_time(&attr.atime); - let (mtime_secs, mtime_nanos) = time_from_system_time(&attr.mtime); - let (ctime_secs, ctime_nanos) = time_from_system_time(&attr.ctime); - #[cfg(target_os = "macos")] - let (crtime_secs, crtime_nanos) = time_from_system_time(&attr.crtime); - - abi::fuse_attr { - ino: attr.ino, - size: attr.size, - blocks: attr.blocks, - atime: atime_secs, - mtime: mtime_secs, - ctime: ctime_secs, - #[cfg(target_os = "macos")] - crtime: crtime_secs as u64, - atimensec: atime_nanos, - mtimensec: mtime_nanos, - ctimensec: ctime_nanos, - #[cfg(target_os = "macos")] - crtimensec: crtime_nanos, - mode: mode_from_kind_and_perm(attr.kind, attr.perm), - nlink: attr.nlink, - uid: attr.uid, - gid: attr.gid, - rdev: attr.rdev, - #[cfg(target_os = "macos")] - flags: attr.flags, - #[cfg(feature = "abi-7-9")] - blksize: attr.blksize, - #[cfg(feature = "abi-7-9")] - padding: 0, - } -} - -// TODO: Add methods for creating this without making a `FileAttr` first. -#[derive(Debug, Clone, Copy)] -pub struct Attr { - pub(crate) attr: abi::fuse_attr, -} -impl From<&crate::FileAttr> for Attr { - fn from(attr: &crate::FileAttr) -> Self { - Self { - attr: fuse_attr_from_attr(attr), - } - } -} -impl From for Attr { - fn from(attr: crate::FileAttr) -> Self { - Self { - attr: fuse_attr_from_attr(&attr), - } - } -} - -#[derive(Debug)] -struct EntListBuf { - max_size: usize, - buf: ResponseBuf, -} -impl EntListBuf { - fn new(max_size: usize) -> Self { - Self { - max_size, - buf: ResponseBuf::new(), - } - } - - /// Add an entry to the directory reply buffer. Returns true if the buffer is full. - /// A transparent offset value can be provided for each entry. The kernel uses these - /// value to request the next entries in further readdir calls - #[must_use] - fn push(&mut self, ent: [&[u8]; 2]) -> bool { - let entlen = ent[0].len() + ent[1].len(); - let entsize = (entlen + size_of::() - 1) & !(size_of::() - 1); // 64bit align - if self.buf.len() + entsize > self.max_size { - return true; - } - self.buf.extend_from_slice(ent[0]); - self.buf.extend_from_slice(ent[1]); - let padlen = entsize - entlen; - self.buf.extend_from_slice(&[0u8; 8][..padlen]); - false - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)] -pub struct DirEntOffset(pub i64); -impl From for i64 { - fn from(x: DirEntOffset) -> Self { - x.0 - } -} - -#[derive(Debug)] -pub struct DirEntry> { - ino: INodeNo, - offset: DirEntOffset, - kind: FileType, - name: T, -} - -impl> DirEntry { - pub fn new(ino: INodeNo, offset: DirEntOffset, kind: FileType, name: T) -> DirEntry { - DirEntry:: { - ino, - offset, - kind, - name, - } - } -} - -/// Used to respond to [ReadDirPlus] requests. -#[derive(Debug)] -pub struct DirEntList(EntListBuf); -impl From for Response<'_> { - fn from(l: DirEntList) -> Self { - assert!(l.0.buf.len() <= l.0.max_size); - Response::new_directory(l.0) - } -} - -impl DirEntList { - pub(crate) fn new(max_size: usize) -> Self { - Self(EntListBuf::new(max_size)) - } - /// Add an entry to the directory reply buffer. Returns true if the buffer is full. - /// A transparent offset value can be provided for each entry. The kernel uses these - /// value to request the next entries in further readdir calls - #[must_use] - pub fn push>(&mut self, ent: &DirEntry) -> bool { - let name = ent.name.as_ref().as_os_str().as_bytes(); - let header = abi::fuse_dirent { - ino: ent.ino.into(), - off: ent.offset.0, - namelen: name.len().try_into().expect("Name too long"), - typ: mode_from_kind_and_perm(ent.kind, 0) >> 12, - }; - self.0.push([header.as_bytes(), name]) - } -} - -#[derive(Debug)] -pub struct DirEntryPlus> { - #[allow(unused)] // We use `attr.ino` instead - ino: INodeNo, - generation: Generation, - offset: DirEntOffset, - name: T, - entry_valid: Duration, - attr: Attr, - attr_valid: Duration, -} - -impl> DirEntryPlus { - pub fn new( - ino: INodeNo, - generation: Generation, - offset: DirEntOffset, - name: T, - entry_valid: Duration, - attr: Attr, - attr_valid: Duration, - ) -> Self { - Self { - ino, - generation, - offset, - name, - entry_valid, - attr, - attr_valid, - } - } -} - -/// Used to respond to [ReadDir] requests. -#[derive(Debug)] -pub struct DirEntPlusList(EntListBuf); -impl From for Response<'_> { - fn from(l: DirEntPlusList) -> Self { - assert!(l.0.buf.len() <= l.0.max_size); - Response::new_directory(l.0) - } -} - -impl DirEntPlusList { - pub(crate) fn new(max_size: usize) -> Self { - Self(EntListBuf::new(max_size)) - } - /// Add an entry to the directory reply buffer. Returns true if the buffer is full. - /// A transparent offset value can be provided for each entry. The kernel uses these - /// value to request the next entries in further readdir calls - #[must_use] - pub fn push>(&mut self, x: &DirEntryPlus) -> bool { - let name = x.name.as_ref().as_os_str().as_bytes(); - let header = abi::fuse_direntplus { - entry_out: abi::fuse_entry_out { - nodeid: x.attr.attr.ino, - generation: x.generation.into(), - entry_valid: x.entry_valid.as_secs(), - attr_valid: x.attr_valid.as_secs(), - entry_valid_nsec: x.entry_valid.subsec_nanos(), - attr_valid_nsec: x.attr_valid.subsec_nanos(), - attr: x.attr.attr, - }, - dirent: abi::fuse_dirent { - ino: x.attr.attr.ino, - off: x.offset.into(), - namelen: name.len().try_into().expect("Name too long"), - typ: x.attr.attr.mode >> 12, - }, - }; - self.0.push([header.as_bytes(), name]) - } -} - -#[cfg(test)] -mod test { - use std::num::NonZeroI32; - - use super::super::test::ioslice_to_vec; - use super::*; - - #[test] - fn reply_empty() { - let r = Response::new_empty(); - assert_eq!( - r.with_iovec(RequestId(0xdeadbeef), ioslice_to_vec), - vec![ - 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, - ], - ); - } - - #[test] - fn reply_error() { - let r = Response::new_error(Errno(NonZeroI32::new(66).unwrap())); - assert_eq!( - r.with_iovec(RequestId(0xdeadbeef), ioslice_to_vec), - vec![ - 0x10, 0x00, 0x00, 0x00, 0xbe, 0xff, 0xff, 0xff, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, - ], - ); - } - - #[test] - fn reply_data() { - let r = Response::new_data([0xde, 0xad, 0xbe, 0xef].as_ref()); - assert_eq!( - r.with_iovec(RequestId(0xdeadbeef), ioslice_to_vec), - vec![ - 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, 0xde, 0xad, 0xbe, 0xef, - ], - ); - } - - #[test] - fn reply_entry() { - let mut expected = if cfg!(target_os = "macos") { - vec![ - 0x98, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xaa, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x65, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x65, 0x87, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x43, 0x00, 0x00, 0x21, 0x43, 0x00, 0x00, - 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x78, 0x56, 0x00, 0x00, 0x78, 0x56, 0x00, 0x00, 0x78, 0x56, 0x00, 0x00, 0x78, 0x56, - 0x00, 0x00, 0xa4, 0x81, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, - 0x77, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, - ] - } else { - vec![ - 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xaa, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x65, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x65, 0x87, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x43, 0x00, 0x00, 0x21, 0x43, 0x00, 0x00, - 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x56, 0x00, 0x00, 0x78, 0x56, 0x00, 0x00, - 0x78, 0x56, 0x00, 0x00, 0xa4, 0x81, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x66, 0x00, - 0x00, 0x00, 0x77, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, - ] - }; - - if cfg!(feature = "abi-7-9") { - expected.extend(vec![0xbb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); - } - expected[0] = (expected.len()) as u8; - - let time = UNIX_EPOCH + Duration::new(0x1234, 0x5678); - let ttl = Duration::new(0x8765, 0x4321); - let attr = crate::FileAttr { - ino: 0x11, - size: 0x22, - blocks: 0x33, - atime: time, - mtime: time, - ctime: time, - crtime: time, - kind: FileType::RegularFile, - perm: 0o644, - nlink: 0x55, - uid: 0x66, - gid: 0x77, - rdev: 0x88, - flags: 0x99, - blksize: 0xbb, - }; - let r = Response::new_entry(INodeNo(0x11), Generation(0xaa), &attr.into(), ttl, ttl); - assert_eq!( - r.with_iovec(RequestId(0xdeadbeef), ioslice_to_vec), - expected - ); - } - - #[test] - fn reply_attr() { - let mut expected = if cfg!(target_os = "macos") { - vec![ - 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, 0x65, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x43, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x78, 0x56, 0x00, 0x00, 0x78, 0x56, 0x00, 0x00, 0x78, 0x56, - 0x00, 0x00, 0x78, 0x56, 0x00, 0x00, 0xa4, 0x81, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, - 0x66, 0x00, 0x00, 0x00, 0x77, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x99, 0x00, - 0x00, 0x00, - ] - } else { - vec![ - 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, 0x65, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x43, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x56, 0x00, 0x00, - 0x78, 0x56, 0x00, 0x00, 0x78, 0x56, 0x00, 0x00, 0xa4, 0x81, 0x00, 0x00, 0x55, 0x00, - 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, 0x77, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, - ] - }; - - if cfg!(feature = "abi-7-9") { - expected.extend_from_slice(&[0xbb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); - } - expected[0] = expected.len() as u8; - - let time = UNIX_EPOCH + Duration::new(0x1234, 0x5678); - let ttl = Duration::new(0x8765, 0x4321); - let attr = crate::FileAttr { - ino: 0x11, - size: 0x22, - blocks: 0x33, - atime: time, - mtime: time, - ctime: time, - crtime: time, - kind: FileType::RegularFile, - perm: 0o644, - nlink: 0x55, - uid: 0x66, - gid: 0x77, - rdev: 0x88, - flags: 0x99, - blksize: 0xbb, - }; - let r = Response::new_attr(&ttl, &attr.into()); - assert_eq!( - r.with_iovec(RequestId(0xdeadbeef), ioslice_to_vec), - expected - ); - } - - #[test] - #[cfg(target_os = "macos")] - fn reply_xtimes() { - let expected = vec![ - 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x78, 0x56, 0x00, 0x00, 0x78, 0x56, 0x00, 0x00, - ]; - let time = UNIX_EPOCH + Duration::new(0x1234, 0x5678); - let r = Response::new_xtimes(time, time); - assert_eq!( - r.with_iovec(RequestId(0xdeadbeef), ioslice_to_vec), - expected - ); - } - - #[test] - fn reply_open() { - let expected = vec![ - 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, 0x22, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - ]; - let r = Response::new_open(FileHandle(0x1122), 0x33); - assert_eq!( - r.with_iovec(RequestId(0xdeadbeef), ioslice_to_vec), - expected - ); - } - - #[test] - fn reply_write() { - let expected = vec![ - 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, 0x22, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ]; - let r = Response::new_write(0x1122); - assert_eq!( - r.with_iovec(RequestId(0xdeadbeef), ioslice_to_vec), - expected - ); - } - - #[test] - fn reply_statfs() { - let expected = vec![ - 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x66, 0x00, 0x00, 0x00, 0x77, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ]; - let r = Response::new_statfs(0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88); - assert_eq!( - r.with_iovec(RequestId(0xdeadbeef), ioslice_to_vec), - expected - ); - } - - #[test] - fn reply_create() { - let mut expected = if cfg!(target_os = "macos") { - vec![ - 0xa8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xaa, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x65, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x65, 0x87, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x43, 0x00, 0x00, 0x21, 0x43, 0x00, 0x00, - 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x78, 0x56, 0x00, 0x00, 0x78, 0x56, 0x00, 0x00, 0x78, 0x56, 0x00, 0x00, 0x78, 0x56, - 0x00, 0x00, 0xa4, 0x81, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, - 0x77, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, 0xbb, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xcc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ] - } else { - vec![ - 0x98, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xaa, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x65, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x65, 0x87, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x43, 0x00, 0x00, 0x21, 0x43, 0x00, 0x00, - 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x56, 0x00, 0x00, 0x78, 0x56, 0x00, 0x00, - 0x78, 0x56, 0x00, 0x00, 0xa4, 0x81, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x66, 0x00, - 0x00, 0x00, 0x77, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0xbb, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xcc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ] - }; - - if cfg!(feature = "abi-7-9") { - let insert_at = expected.len() - 16; - expected.splice( - insert_at..insert_at, - vec![0xdd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], - ); - } - expected[0] = (expected.len()) as u8; - - let time = UNIX_EPOCH + Duration::new(0x1234, 0x5678); - let ttl = Duration::new(0x8765, 0x4321); - let attr = crate::FileAttr { - ino: 0x11, - size: 0x22, - blocks: 0x33, - atime: time, - mtime: time, - ctime: time, - crtime: time, - kind: FileType::RegularFile, - perm: 0o644, - nlink: 0x55, - uid: 0x66, - gid: 0x77, - rdev: 0x88, - flags: 0x99, - blksize: 0xdd, - }; - let r = Response::new_create(&ttl, &attr.into(), Generation(0xaa), FileHandle(0xbb), 0xcc); - assert_eq!( - r.with_iovec(RequestId(0xdeadbeef), ioslice_to_vec), - expected - ); - } - - #[test] - fn reply_lock() { - let expected = vec![ - 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, - ]; - let r = Response::new_lock(&Lock { - range: (0x11, 0x22), - typ: 0x33, - pid: 0x44, - }); - assert_eq!( - r.with_iovec(RequestId(0xdeadbeef), ioslice_to_vec), - expected - ); - } - - #[test] - fn reply_bmap() { - let expected = vec![ - 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ]; - let r = Response::new_bmap(0x1234); - assert_eq!( - r.with_iovec(RequestId(0xdeadbeef), ioslice_to_vec), - expected - ); - } - - #[test] - fn reply_xattr_size() { - let expected = vec![ - 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xEF, 0xBE, 0xAD, 0xDE, 0x00, 0x00, - 0x00, 0x00, 0x78, 0x56, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, - ]; - let r = Response::new_xattr_size(0x12345678); - assert_eq!( - r.with_iovec(RequestId(0xdeadbeef), ioslice_to_vec), - expected - ); - } - - #[test] - fn reply_xattr_data() { - let expected = vec![ - 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xEF, 0xBE, 0xAD, 0xDE, 0x00, 0x00, - 0x00, 0x00, 0x11, 0x22, 0x33, 0x44, - ]; - let r = Response::new_data([0x11, 0x22, 0x33, 0x44].as_ref()); - assert_eq!( - r.with_iovec(RequestId(0xdeadbeef), ioslice_to_vec), - expected - ); - } - - #[test] - fn reply_directory() { - let expected = vec![ - 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, 0xbb, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x68, 0x65, - 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x00, 0xdd, 0xcc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, - 0x00, 0x00, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x72, 0x73, - ]; - let mut buf = DirEntList::new(4096); - assert!(!buf.push(&DirEntry::new( - INodeNo(0xaabb), - DirEntOffset(1), - FileType::Directory, - "hello" - ))); - assert!(!buf.push(&DirEntry::new( - INodeNo(0xccdd), - DirEntOffset(2), - FileType::RegularFile, - "world.rs" - ))); - let r: Response<'_> = buf.into(); - assert_eq!( - r.with_iovec(RequestId(0xdeadbeef), ioslice_to_vec), - expected - ); - } -} diff --git a/vendor/fuser/src/ll/request.rs b/vendor/fuser/src/ll/request.rs deleted file mode 100644 index 5f6961e9b..000000000 --- a/vendor/fuser/src/ll/request.rs +++ /dev/null @@ -1,2337 +0,0 @@ -//! Low-level filesystem operation request. -//! -//! A request represents information about a filesystem operation the kernel driver wants us to -//! perform. - -use super::fuse_abi::{fuse_in_header, fuse_opcode, InvalidOpcodeError}; - -use super::{fuse_abi as abi, Errno, Response}; -#[cfg(feature = "serializable")] -use serde::{Deserialize, Serialize}; -use std::{convert::TryFrom, fmt::Display, path::Path}; -use std::{error, fmt, mem}; - -use super::argument::ArgumentIterator; - -/// Error that may occur while reading and parsing a request from the kernel driver. -#[derive(Debug)] -pub enum RequestError { - /// Not enough data for parsing header (short read). - ShortReadHeader(usize), - /// Kernel requested an unknown operation. - UnknownOperation(u32), - /// Not enough data for arguments (short read). - ShortRead(usize, usize), - /// Insufficient argument data. - InsufficientData, -} - -/// Unique ID for a request from the kernel -/// -/// The FUSE kernel driver assigns a unique id to every concurrent request. This allows to -/// distinguish between multiple concurrent requests. The unique id of a request may be -/// reused in later requests after it has completed. -/// -/// This can be retrieve for any request using [Request::unique]. The kernel -/// will send an [Interrupt] request to cancel requests in progress. It's -/// important to handle this for any requests that may block indefinitely, like -/// [SetLkW]. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -#[cfg_attr(feature = "serializable", derive(Serialize, Deserialize))] -pub struct RequestId(pub u64); -impl From for u64 { - fn from(fh: RequestId) -> Self { - fh.0 - } -} - -/// A newtype for inode numbers -/// -/// These are generated by the filesystem implementation and returned to the -/// kernel in response to a call to [Lookup], [Create], [MkNod], [MkDir] or -/// [SymLink]. The kernel will then pass these numbers back to the filesystem -/// implementation when it needs to refer to a given file. Every request has -/// an associated [INodeNo], accessible as [Request::nodeid]. -/// -/// Reference Counting -/// ------------------ -/// -/// Every time the kernel receives a given inode number in a response to a -/// [Lookup], [Create], [MkNod], [MkDir] or [SymLink] request it increments an -/// internal counter for that inode. The filesystem implementation should do -/// the same. When the kernel is no longer interested in this inode it will -/// send a [Forget] message with that counter. The filesystem implementation -/// should decrement its own counter and if it reaches 0 then the inode number -/// may be recycled and your filesystem implementation may clean up its -/// internal data-structures relating to that inode. -/// -/// We implement conversion from [INodeNo] to [u64] but not vice-versa because -/// not all [u64]s are valid [INodeNo]s, but the reverse is true. So to produce -/// a [INodeNo] from a [u64] we must be explicit. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -#[cfg_attr(feature = "serializable", derive(Serialize, Deserialize))] -pub struct INodeNo(pub u64); -impl From for u64 { - fn from(fh: INodeNo) -> Self { - fh.0 - } -} - -/// A newtype for file handles -/// -/// This corresponds to a single file description in a client program. These -/// are generated by the filesystem implementation in replies to [Open], -/// [OpenDir] and [Create] requests. It's used as a correlation id across -/// [Read], [Write], [FSync], [IoCtl], [Poll], [FAllocate], [ReadDir], -/// [FSyncDir], [GetLk], [SetLk], [SetLkW], [ReadDirPlus], [Lseek] and -/// [CopyFileRange] requests. -/// -/// A filesystem implementation may store arbitrary data as the [FileHandle], as -/// long as it fits into 64-bits and doesn't need to change for over the lifetime -/// of the [FileHandle]. Typically this might consist of an index into an array -/// of [FileHandle]s that the filesystem implementation maintains. -/// -/// Filesystems may instead implement stateless file I/O and use `0` as the -/// [FileHandle] - although this makes it impossible to correctly implement -/// resumable [ReadDir] in the presence of mutable directories (see [OpenDir]). -/// -/// Lifecycle -/// --------- -/// -/// A [FileHandle] is owned by one or more file-descriptors (or memory -/// mappings) in the client program. Multiple file descriptors can point to -/// the same [FileHandle], just as a single INode can have multiple -/// [FileHandle]s open at one time. Every time a single file-descriptor is -/// closed a [Flush] request is made. This gives filesystem implementations -/// an opportunity to return an error message from that `close()` call. After -/// all the file-descriptors are closed that own a given [FileHandle] the -/// [Release]/[ReleaseDir] request will be made. This is an opportunity for -/// the filesystem implementation to free any internal per-FileHandle data -/// structures it has allocated. -/// -/// We implement conversion from FileHandle to u64 but not vice-versa because -/// not all u64s are valid FileHandles, but the reverse is true. So to produce -/// a FileHandle from a u64 we must be explicit. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -#[cfg_attr(feature = "serializable", derive(Serialize, Deserialize))] -pub struct FileHandle(pub u64); - -impl From for u64 { - fn from(fh: FileHandle) -> Self { - fh.0 - } -} - -/// A newtype for lock owners -/// -/// TODO: Document lock lifecycle and how and when to implement file locking. -/// -/// See [Read], [Write], [Release], [Flush], [GetLk], [SetLk], [SetLkW]. -/// -/// We implement conversion from [LockOwner] to [u64] but not vice-versa -/// because all LockOwners are valid [u64]s, but not vice-versa. So to produce -/// a [LockOwner] from a [u64] we must be explicit. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -#[cfg_attr(feature = "serializable", derive(Serialize, Deserialize))] -pub struct LockOwner(pub u64); - -impl From for u64 { - fn from(fh: LockOwner) -> Self { - fh.0 - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct Lock { - // Unfortunately this can't be a std::ops::Range because Range is not Copy: - // https://github.com/rust-lang/rfcs/issues/2848 - pub range: (u64, u64), - // TODO: Make typ an enum - pub typ: i32, - pub pid: u32, -} -impl Lock { - fn from_abi(x: &abi::fuse_file_lock) -> Lock { - Lock { - range: (x.start, x.end), - typ: x.typ, - pid: x.pid, - } - } -} - -/// A newtype for ABI version -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -#[cfg_attr(feature = "serializable", derive(Serialize, Deserialize))] -pub struct Version(pub u32, pub u32); -impl Version { - pub fn major(&self) -> u32 { - self.0 - } - pub fn minor(&self) -> u32 { - self.1 - } -} -impl Display for Version { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}.{}", self.0, self.1) - } -} - -/// Represents a filename in a directory -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] -pub struct FilenameInDir<'a> { - /// The Inode number of the directory - pub dir: INodeNo, - /// Name of the file. This refers to a name directly in this directory, rather than any - /// subdirectory so is guaranteed not to contain '\0' or '/'. It may be literally "." or ".." - /// however. - pub name: &'a Path, -} - -impl fmt::Display for RequestError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - RequestError::ShortReadHeader(len) => write!( - f, - "Short read of FUSE request header ({len} < {})", - mem::size_of::() - ), - RequestError::UnknownOperation(opcode) => write!(f, "Unknown FUSE opcode ({opcode})"), - RequestError::ShortRead(len, total) => { - write!(f, "Short read of FUSE request ({len} < {total})") - } - RequestError::InsufficientData => write!(f, "Insufficient argument data"), - } - } -} - -impl error::Error for RequestError {} -pub trait Request: Sized { - /// Returns the unique identifier of this request. - /// - /// The FUSE kernel driver assigns a unique id to every concurrent request. This allows to - /// distinguish between multiple concurrent requests. The unique id of a request may be - /// reused in later requests after it has completed. - fn unique(&self) -> RequestId; - - /// Returns the node id of the inode this request is targeted to. - fn nodeid(&self) -> INodeNo; - - /// Returns the UID that the process that triggered this request runs under. - fn uid(&self) -> u32; - - /// Returns the GID that the process that triggered this request runs under. - fn gid(&self) -> u32; - - /// Returns the PID of the process that triggered this request. - fn pid(&self) -> u32; - - /// Create an error response for this Request - fn reply_err(&self, errno: Errno) -> Response<'_> { - Response::new_error(errno) - } -} - -macro_rules! impl_request { - ($structname: ty) => { - impl<'a> super::Request for $structname { - #[inline] - fn unique(&self) -> RequestId { - RequestId(self.header.unique) - } - - #[inline] - fn nodeid(&self) -> INodeNo { - INodeNo(self.header.nodeid) - } - - #[inline] - fn uid(&self) -> u32 { - self.header.uid - } - - #[inline] - fn gid(&self) -> u32 { - self.header.gid - } - - #[inline] - fn pid(&self) -> u32 { - self.header.pid - } - } - }; -} - -mod op { - use crate::ll::Response; - - use super::{ - super::{argument::ArgumentIterator, TimeOrNow}, - FilenameInDir, Request, - }; - use super::{ - abi::consts::*, abi::*, FileHandle, INodeNo, Lock, LockOwner, Operation, RequestId, - }; - use std::{ - convert::TryInto, - ffi::OsStr, - fmt::Display, - num::NonZeroU32, - path::Path, - time::{Duration, SystemTime}, - }; - use zerocopy::IntoBytes; - - /// Look up a directory entry by name and get its attributes. - /// - /// Implementations allocate and assign [INodeNo]s in this request. Learn more - /// about INode lifecycle and the relationship between [Lookup] and [Forget] in the - /// documentation for [INodeNo]. - #[derive(Debug)] - pub struct Lookup<'a> { - header: &'a fuse_in_header, - name: &'a OsStr, - } - impl_request!(Lookup<'_>); - impl<'a> Lookup<'a> { - pub fn name(&self) -> &'a Path { - self.name.as_ref() - } - } - /// Forget about an inode. - /// - /// The nlookup parameter indicates the number of lookups previously performed on - /// this inode. If the filesystem implements inode lifetimes, it is recommended that - /// inodes acquire a single reference on each lookup, and lose nlookup references on - /// each forget. The filesystem may ignore forget calls, if the inodes don't need to - /// have a limited lifetime. - /// - /// Learn more about INode lifecycle in the documentation for [INodeNo]. - /// - /// On unmount it is not guaranteed, that all referenced inodes will receive a forget - /// message. - #[derive(Debug)] - pub struct Forget<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_forget_in, - } - impl_request!(Forget<'_>); - impl<'a> Forget<'a> { - /// The number of lookups previously performed on this inode - pub fn nlookup(&self) -> u64 { - self.arg.nlookup - } - } - - /// Get file attributes. - #[derive(Debug)] - pub struct GetAttr<'a> { - header: &'a fuse_in_header, - - #[cfg(feature = "abi-7-9")] - arg: &'a fuse_getattr_in, - } - impl_request!(GetAttr<'_>); - - #[cfg(feature = "abi-7-9")] - impl<'a> GetAttr<'a> { - pub fn file_handle(&self) -> Option { - if self.arg.getattr_flags & crate::FUSE_GETATTR_FH != 0 { - Some(FileHandle(self.arg.fh)) - } else { - None - } - } - } - - /// Set file attributes. - #[derive(Debug)] - pub struct SetAttr<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_setattr_in, - } - impl_request!(SetAttr<'_>); - impl<'a> SetAttr<'a> { - pub fn mode(&self) -> Option { - match self.arg.valid & FATTR_MODE { - 0 => None, - _ => Some(self.arg.mode), - } - } - pub fn uid(&self) -> Option { - match self.arg.valid & FATTR_UID { - 0 => None, - _ => Some(self.arg.uid), - } - } - pub fn gid(&self) -> Option { - match self.arg.valid & FATTR_GID { - 0 => None, - _ => Some(self.arg.gid), - } - } - pub fn size(&self) -> Option { - match self.arg.valid & FATTR_SIZE { - 0 => None, - _ => Some(self.arg.size), - } - } - pub fn atime(&self) -> Option { - match self.arg.valid & FATTR_ATIME { - 0 => None, - _ => Some(if self.arg.atime_now() { - TimeOrNow::Now - } else { - TimeOrNow::SpecificTime(system_time_from_time( - self.arg.atime, - self.arg.atimensec, - )) - }), - } - } - pub fn mtime(&self) -> Option { - match self.arg.valid & FATTR_MTIME { - 0 => None, - _ => Some(if self.arg.mtime_now() { - TimeOrNow::Now - } else { - TimeOrNow::SpecificTime(system_time_from_time( - self.arg.mtime, - self.arg.mtimensec, - )) - }), - } - } - pub fn ctime(&self) -> Option { - #[cfg(feature = "abi-7-23")] - match self.arg.valid & FATTR_CTIME { - 0 => None, - _ => Some(system_time_from_time(self.arg.ctime, self.arg.ctimensec)), - } - #[cfg(not(feature = "abi-7-23"))] - None - } - /// The value set by the [Open] method. See [FileHandle]. - /// - /// This will only be set if the user passed a file-descriptor to set the - /// attributes - i.e. they used [libc::fchmod] rather than [libc::chmod]. - pub fn file_handle(&self) -> Option { - match self.arg.valid & FATTR_FH { - 0 => None, - _ => Some(FileHandle(self.arg.fh)), - } - } - pub fn crtime(&self) -> Option { - #[cfg(target_os = "macos")] - match self.arg.valid & FATTR_CRTIME { - 0 => None, - // During certain operation, macOS use some helper that send request to the mountpoint with `crtime` set to 0xffffffff83da4f80. - // That value correspond to `-2_082_844_800u64` which is the difference between the date 1904-01-01 and 1970-01-01 because macOS epoch start at 1904 and not 1970. - // https://github.com/macfuse/macfuse/issues/1042 - _ if self.arg.crtime == 0xffffffff83da4f80 => None, - _ => Some( - SystemTime::UNIX_EPOCH + Duration::new(self.arg.crtime, self.arg.crtimensec), - ), - } - #[cfg(not(target_os = "macos"))] - None - } - pub fn chgtime(&self) -> Option { - #[cfg(target_os = "macos")] - match self.arg.valid & FATTR_CHGTIME { - 0 => None, - _ => Some( - SystemTime::UNIX_EPOCH + Duration::new(self.arg.chgtime, self.arg.chgtimensec), - ), - } - #[cfg(not(target_os = "macos"))] - None - } - pub fn bkuptime(&self) -> Option { - #[cfg(target_os = "macos")] - match self.arg.valid & FATTR_BKUPTIME { - 0 => None, - _ => Some( - SystemTime::UNIX_EPOCH - + Duration::new(self.arg.bkuptime, self.arg.bkuptimensec), - ), - } - #[cfg(not(target_os = "macos"))] - None - } - pub fn flags(&self) -> Option { - #[cfg(target_os = "macos")] - match self.arg.valid & FATTR_FLAGS { - 0 => None, - _ => Some(self.arg.flags), - } - #[cfg(not(target_os = "macos"))] - None - } - - // TODO: Why does *set*attr want to have an attr response? - } - impl<'a> Display for SetAttr<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "SETATTR mode: {:?}, uid: {:?}, gid: {:?}, size: {:?}, atime: {:?}, \ - mtime: {:?}, ctime: {:?}, file_handle: {:?}, crtime: {:?}, chgtime: {:?}, \ - bkuptime: {:?}, flags: {:?}", - self.mode(), - self.uid(), - self.gid(), - self.size(), - self.atime(), - self.mtime(), - self.ctime(), - self.file_handle(), - self.crtime(), - self.chgtime(), - self.bkuptime(), - self.flags() - ) - } - } - - /// Read symbolic link. - #[derive(Debug)] - pub struct ReadLink<'a> { - header: &'a fuse_in_header, - } - impl_request!(ReadLink<'_>); - - /// Create a symbolic link. - #[derive(Debug)] - pub struct SymLink<'a> { - header: &'a fuse_in_header, - target: &'a Path, - link_name: &'a Path, - } - impl_request!(SymLink<'_>); - impl<'a> SymLink<'a> { - pub fn target(&self) -> &'a Path { - self.target - } - pub fn link_name(&self) -> &'a Path { - self.link_name - } - } - - /// Create file node. - /// Create a regular file, character device, block device, fifo or socket node. - #[derive(Debug)] - pub struct MkNod<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_mknod_in, - name: &'a Path, - } - impl_request!(MkNod<'_>); - impl<'a> MkNod<'a> { - pub fn name(&self) -> &'a Path { - self.name - } - pub fn mode(&self) -> u32 { - self.arg.mode - } - pub fn umask(&self) -> u32 { - #[cfg(not(feature = "abi-7-12"))] - return 0; - #[cfg(feature = "abi-7-12")] - self.arg.umask - } - pub fn rdev(&self) -> u32 { - self.arg.rdev - } - } - - /// Create a directory. - #[derive(Debug)] - pub struct MkDir<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_mkdir_in, - name: &'a Path, - } - impl_request!(MkDir<'_>); - impl<'a> MkDir<'a> { - pub fn name(&self) -> &'a Path { - self.name - } - pub fn mode(&self) -> u32 { - self.arg.mode - } - pub fn umask(&self) -> u32 { - #[cfg(not(feature = "abi-7-12"))] - return 0; - #[cfg(feature = "abi-7-12")] - self.arg.umask - } - } - - /// Remove a file. - #[derive(Debug)] - pub struct Unlink<'a> { - header: &'a fuse_in_header, - name: &'a Path, - } - impl_request!(Unlink<'_>); - impl<'a> Unlink<'a> { - pub fn name(&self) -> &'a Path { - self.name - } - } - - /// Remove a directory. - #[derive(Debug)] - pub struct RmDir<'a> { - header: &'a fuse_in_header, - pub name: &'a Path, - } - impl_request!(RmDir<'_>); - impl<'a> RmDir<'a> { - pub fn name(&self) -> &'a Path { - self.name - } - } - - /// Rename a file. - #[derive(Debug)] - pub struct Rename<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_rename_in, - name: &'a Path, - newname: &'a Path, - } - impl_request!(Rename<'_>); - impl<'a> Rename<'a> { - pub fn src(&self) -> FilenameInDir<'a> { - FilenameInDir::<'a> { - dir: self.nodeid(), - name: self.name, - } - } - pub fn dest(&self) -> FilenameInDir<'a> { - FilenameInDir::<'a> { - dir: INodeNo(self.arg.newdir), - name: self.newname, - } - } - } - - /// Create a hard link. - #[derive(Debug)] - pub struct Link<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_link_in, - name: &'a Path, - } - impl_request!(Link<'_>); - impl<'a> Link<'a> { - /// This is the inode no of the file to be linked. The inode number in - /// the fuse header is of the directory that it will be linked into. - pub fn inode_no(&self) -> INodeNo { - INodeNo(self.arg.oldnodeid) - } - pub fn dest(&self) -> FilenameInDir<'a> { - FilenameInDir::<'a> { - dir: self.nodeid(), - name: self.name, - } - } - } - - /// Open a file. - /// - /// Open flags (with the exception of `O_CREAT`, `O_EXCL`, `O_NOCTTY` and `O_TRUNC`) are - /// available in flags. Filesystem may store an arbitrary file handle (pointer, index, - /// etc) in fh, and use this in other all other file operations (read, write, flush, - /// release, fsync). Filesystem may also implement stateless file I/O and not store - /// anything in fh. There are also some flags (direct_io, keep_cache) which the - /// filesystem may set, to change the way the file is opened. See fuse_file_info - /// structure in for more details. - #[derive(Debug)] - pub struct Open<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_open_in, - } - impl_request!(Open<'_>); - impl<'a> Open<'a> { - pub fn flags(&self) -> i32 { - self.arg.flags - } - } - - /// Read data. - /// - /// Read should send exactly the number of bytes requested except on EOF or error, - /// otherwise the rest of the data will be substituted with zeroes. An exception to - /// this is when the file has been opened in 'direct_io' mode, in which case the - /// return value of the read system call will reflect the return value of this - /// operation. - #[derive(Debug)] - pub struct Read<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_read_in, - } - impl_request!(Read<'_>); - impl<'a> Read<'a> { - /// The value set by the [Open] method. - pub fn file_handle(&self) -> FileHandle { - FileHandle(self.arg.fh) - } - pub fn offset(&self) -> i64 { - self.arg.offset - } - pub fn size(&self) -> u32 { - self.arg.size - } - /// Only supported with ABI >= 7.9 - pub fn lock_owner(&self) -> Option { - #[cfg(not(feature = "abi-7-9"))] - return None; - #[cfg(feature = "abi-7-9")] - if self.arg.read_flags & FUSE_READ_LOCKOWNER != 0 { - Some(LockOwner(self.arg.lock_owner)) - } else { - None - } - } - /// The file flags, such as `O_SYNC`. Only supported with ABI >= 7.9 - pub fn flags(&self) -> i32 { - #[cfg(not(feature = "abi-7-9"))] - return 0; - #[cfg(feature = "abi-7-9")] - self.arg.flags - } - } - - /// Write data. - /// - /// Write should return exactly the number of bytes requested except on error. An - /// exception to this is when the file has been opened in 'direct_io' mode, in - /// which case the return value of the write system call will reflect the return - /// value of this operation. - #[derive(Debug)] - pub struct Write<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_write_in, - data: &'a [u8], - } - impl_request!(Write<'_>); - impl<'a> Write<'a> { - /// The value set by the [Open] method. - pub fn file_handle(&self) -> FileHandle { - FileHandle(self.arg.fh) - } - pub fn offset(&self) -> i64 { - self.arg.offset - } - pub fn data(&self) -> &'a [u8] { - self.data - } - /// Will contain FUSE_WRITE_CACHE, if this write is from the page cache. If set, - /// the pid, uid, gid, and fh may not match the value that would have been sent if write caching - /// is disabled - /// - /// TODO: WriteFlags type or remove this - pub fn write_flags(&self) -> u32 { - self.arg.write_flags - } - /// lock_owner: only supported with ABI >= 7.9 - pub fn lock_owner(&self) -> Option { - #[cfg(feature = "abi-7-9")] - if self.arg.write_flags & FUSE_WRITE_LOCKOWNER != 0 { - Some(LockOwner(self.arg.lock_owner)) - } else { - None - } - #[cfg(not(feature = "abi-7-9"))] - None - } - /// flags: these are the file flags, such as O_SYNC. Only supported with ABI >= 7.9 - /// TODO: Make a Flags type specifying valid values - pub fn flags(&self) -> i32 { - #[cfg(feature = "abi-7-9")] - return self.arg.flags; - #[cfg(not(feature = "abi-7-9"))] - 0 - } - } - - /// Get file system statistics. - #[derive(Debug)] - pub struct StatFs<'a> { - header: &'a fuse_in_header, - } - impl_request!(StatFs<'_>); - - /// Release an open file. - /// - /// Release is called when there are no more references to an open file: all file - /// descriptors are closed and all memory mappings are unmapped. For every [Open] - /// call there will be exactly one release call. The filesystem may reply with an - /// error, but error values are not returned to `close()` or `munmap()` which - /// triggered the release. - #[derive(Debug)] - pub struct Release<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_release_in, - } - impl_request!(Release<'_>); - impl<'a> Release<'a> { - pub fn flush(&self) -> bool { - self.arg.release_flags & FUSE_RELEASE_FLUSH != 0 - } - /// The value set by the [Open] method. - pub fn file_handle(&self) -> FileHandle { - FileHandle(self.arg.fh) - } - /// The same flags as for open. - /// TODO: Document what flags are valid, or remove this - pub fn flags(&self) -> i32 { - self.arg.flags - } - pub fn lock_owner(&self) -> Option { - #[cfg(not(feature = "abi-7-17"))] - return Some(LockOwner(self.arg.lock_owner)); - #[cfg(feature = "abi-7-17")] - if self.arg.release_flags & FUSE_RELEASE_FLOCK_UNLOCK != 0 { - Some(LockOwner(self.arg.lock_owner)) - } else { - None - } - } - } - - /// Synchronize file contents. - #[derive(Debug)] - pub struct FSync<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_fsync_in, - } - impl_request!(FSync<'a>); - impl<'a> FSync<'a> { - /// The value set by the [Open] method. - pub fn file_handle(&self) -> FileHandle { - FileHandle(self.arg.fh) - } - /// If set only the user data should be flushed, not the meta data. - pub fn fdatasync(&self) -> bool { - self.arg.fsync_flags & consts::FUSE_FSYNC_FDATASYNC != 0 - } - } - - /// Set an extended attribute. - #[derive(Debug)] - pub struct SetXAttr<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_setxattr_in, - name: &'a OsStr, - value: &'a [u8], - } - impl_request!(SetXAttr<'a>); - impl<'a> SetXAttr<'a> { - pub fn name(&self) -> &'a OsStr { - self.name - } - pub fn value(&self) -> &'a [u8] { - self.value - } - // TODO: Document what are valid flags - pub fn flags(&self) -> i32 { - self.arg.flags - } - /// This will always be 0 except on MacOS. It's recommended that - /// implementations return EINVAL if this is not 0. - pub fn position(&self) -> u32 { - #[cfg(target_os = "macos")] - return self.arg.position; - #[cfg(not(target_os = "macos"))] - 0 - } - } - - /// Get an extended attribute. - /// - /// If the requested XAttr doesn't exist return [Err(Errno::NO_XATTR)] which will - /// map to the right platform-specific error code. - #[derive(Debug)] - pub struct GetXAttr<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_getxattr_in, - name: &'a OsStr, - } - impl_request!(GetXAttr<'a>); - - /// Type for [GetXAttrSizeEnum::GetSize]. - /// - /// Represents a request from the user to get the size of the data stored in the XAttr. - #[derive(Debug)] - pub struct GetXAttrSize(); - - #[derive(Debug)] - /// Return type for [GetXAttr::size]. - pub enum GetXAttrSizeEnum { - /// User is requesting the size of the data stored in the XAttr - GetSize(GetXAttrSize), - /// User is requesting the data stored in the XAttr. If the data will fit - /// in this number of bytes it should be returned, otherwise return [Err(Errno::ERANGE)]. - #[allow(dead_code)] - Size(NonZeroU32), - } - impl<'a> GetXAttr<'a> { - /// Name of the XAttr - pub fn name(&self) -> &'a OsStr { - self.name - } - /// See [GetXAttrSizeEnum]. - /// - /// You only need to check this value as an optimisation where there's a - /// cost difference between checking the size of the data stored in an XAttr - /// and actually providing the data. Otherwise just call [reply()] with the - /// data and it will do the right thing. - pub fn size(&self) -> GetXAttrSizeEnum { - let s: Result = self.arg.size.try_into(); - match s { - Ok(s) => GetXAttrSizeEnum::Size(s), - Err(_) => GetXAttrSizeEnum::GetSize(GetXAttrSize()), - } - } - /// The size of the buffer the user has allocated to store the XAttr value. - pub(crate) fn size_u32(&self) -> u32 { - self.arg.size - } - } - - /// List extended attribute names. - #[derive(Debug)] - pub struct ListXAttr<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_getxattr_in, - } - impl_request!(ListXAttr<'a>); - impl<'a> ListXAttr<'a> { - /// The size of the buffer the caller has allocated to receive the list of - /// XAttrs. If this is 0 the user is just probing to find how much space is - /// required to fit the whole list. - /// - /// You don't need to worry about this except as an optimisation. - pub fn size(&self) -> u32 { - self.arg.size - } - } - - /// Remove an extended attribute. - /// - /// Return [Err(Errno::NO_XATTR)] if the xattr doesn't exist - /// Return [Err(Errno::ENOTSUP)] if this filesystem doesn't support XAttrs - #[derive(Debug)] - pub struct RemoveXAttr<'a> { - header: &'a fuse_in_header, - name: &'a OsStr, - } - impl_request!(RemoveXAttr<'a>); - impl<'a> RemoveXAttr<'a> { - /// Name of the XAttr to remove - pub fn name(&self) -> &'a OsStr { - self.name - } - } - - /// Flush method. - /// - /// This is called on each close() of the opened file. Since file descriptors can - /// be duplicated (dup, dup2, fork), for one open call there may be many flush - /// calls. Filesystems shouldn't assume that flush will always be called after some - /// writes, or that if will be called at all. - /// - /// NOTE: the name of the method is misleading, since (unlike fsync) the filesystem - /// is not forced to flush pending writes. One reason to flush data, is if the - /// filesystem wants to return write errors. If the filesystem supports file locking - /// operations (setlk, getlk) it should remove all locks belonging to 'lock_owner'. - #[derive(Debug)] - pub struct Flush<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_flush_in, - } - impl_request!(Flush<'a>); - impl<'a> Flush<'a> { - /// The value set by the open method - pub fn file_handle(&self) -> FileHandle { - FileHandle(self.arg.fh) - } - pub fn lock_owner(&self) -> LockOwner { - LockOwner(self.arg.lock_owner) - } - } - - #[derive(Debug)] - pub struct Init<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_init_in, - } - impl_request!(Init<'a>); - impl<'a> Init<'a> { - pub fn capabilities(&self) -> u32 { - self.arg.flags - } - pub fn max_readahead(&self) -> u32 { - self.arg.max_readahead - } - pub fn version(&self) -> super::Version { - super::Version(self.arg.major, self.arg.minor) - } - - pub fn reply(&self, config: &crate::KernelConfig) -> Response<'a> { - let init = fuse_init_out { - major: FUSE_KERNEL_VERSION, - minor: FUSE_KERNEL_MINOR_VERSION, - max_readahead: config.max_readahead, - flags: self.capabilities() & config.requested, // use requested features and reported as capable - #[cfg(not(feature = "abi-7-13"))] - unused: 0, - #[cfg(feature = "abi-7-13")] - max_background: config.max_background, - #[cfg(feature = "abi-7-13")] - congestion_threshold: config.congestion_threshold(), - max_write: config.max_write, - #[cfg(feature = "abi-7-23")] - time_gran: config.time_gran.as_nanos() as u32, - #[cfg(all(feature = "abi-7-23", not(feature = "abi-7-28")))] - reserved: [0; 9], - #[cfg(feature = "abi-7-28")] - max_pages: config.max_pages(), - #[cfg(feature = "abi-7-28")] - unused2: 0, - #[cfg(feature = "abi-7-28")] - reserved: [0; 8], - }; - Response::new_data(init.as_bytes()) - } - } - - /// Open a directory. - /// - /// Filesystem may store an arbitrary file handle (pointer, index, etc) in fh, and - /// use this in other all other directory stream operations ([ReadDir], [ReleaseDir], - /// [FSyncDir]). Filesystem may also implement stateless directory I/O and not store - /// anything in fh, though that makes it impossible to implement standard conforming - /// directory stream operations in case the contents of the directory can change - /// between [OpenDir] and [ReleaseDir]. - /// - /// TODO: Document how to implement "standard conforming directory stream operations" - #[derive(Debug)] - pub struct OpenDir<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_open_in, - } - impl_request!(OpenDir<'a>); - impl<'a> OpenDir<'a> { - /// Flags as passed to open - pub fn flags(&self) -> i32 { - self.arg.flags - } - } - - /// Read directory. - #[derive(Debug)] - pub struct ReadDir<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_read_in, - } - impl_request!(ReadDir<'a>); - impl<'a> ReadDir<'a> { - /// The value set by the [OpenDir] method. - pub fn file_handle(&self) -> FileHandle { - FileHandle(self.arg.fh) - } - pub fn offset(&self) -> i64 { - self.arg.offset - } - pub fn size(&self) -> u32 { - self.arg.size - } - } - - /// Release an open directory. - /// - /// For every [OpenDir] call there will be exactly one [ReleaseDir] call. - #[derive(Debug)] - pub struct ReleaseDir<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_release_in, - } - impl_request!(ReleaseDir<'a>); - impl<'a> ReleaseDir<'a> { - /// The value set by the [OpenDir] method. - pub fn file_handle(&self) -> FileHandle { - FileHandle(self.arg.fh) - } - pub fn flush(&self) -> bool { - self.arg.release_flags & consts::FUSE_RELEASE_FLUSH != 0 - } - pub fn lock_owner(&self) -> Option { - #[cfg(not(feature = "abi-7-17"))] - return Some(LockOwner(self.arg.lock_owner)); - #[cfg(feature = "abi-7-17")] - if self.arg.release_flags & FUSE_RELEASE_FLOCK_UNLOCK != 0 { - Some(LockOwner(self.arg.lock_owner)) - } else { - None - } - } - /// TODO: Document what values this may take - pub fn flags(&self) -> i32 { - self.arg.flags - } - } - - /// Synchronize directory contents. - #[derive(Debug)] - pub struct FSyncDir<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_fsync_in, - } - impl_request!(FSyncDir<'a>); - impl<'a> FSyncDir<'a> { - /// The value set by the [OpenDir] method. See [FileHandle]. - pub fn file_handle(&self) -> FileHandle { - FileHandle(self.arg.fh) - } - /// If set, then only the directory contents should be flushed, not the meta data. - pub fn fdatasync(&self) -> bool { - self.arg.fsync_flags & consts::FUSE_FSYNC_FDATASYNC != 0 - } - } - - /// Test for a POSIX file lock. - #[derive(Debug)] - pub struct GetLk<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_lk_in, - } - impl_request!(GetLk<'a>); - impl<'a> GetLk<'a> { - /// The value set by the [Open] method. See [FileHandle]. - pub fn file_handle(&self) -> FileHandle { - FileHandle(self.arg.fh) - } - pub fn lock(&self) -> Lock { - Lock::from_abi(&self.arg.lk) - } - pub fn lock_owner(&self) -> LockOwner { - LockOwner(self.arg.owner) - } - } - - /// Acquire, modify or release a POSIX file lock. - /// - /// For POSIX threads (NPTL) there's a 1-1 relation between pid and owner, but - /// otherwise this is not always the case. For checking lock ownership, - /// 'fi->owner' must be used. The l_pid field in 'struct flock' should only be - /// used to fill in this field in getlk(). Note: if the locking methods are not - /// implemented, the kernel will still allow file locking to work locally. - /// Hence these are only interesting for network filesystems and similar. - #[derive(Debug)] - pub struct SetLk<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_lk_in, - } - impl_request!(SetLk<'a>); - impl<'a> SetLk<'a> { - /// The value set by the [Open] method. See [FileHandle]. - pub fn file_handle(&self) -> FileHandle { - FileHandle(self.arg.fh) - } - pub fn lock(&self) -> Lock { - Lock::from_abi(&self.arg.lk) - } - pub fn lock_owner(&self) -> LockOwner { - LockOwner(self.arg.owner) - } - } - #[derive(Debug)] - pub struct SetLkW<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_lk_in, - } - impl_request!(SetLkW<'a>); - impl<'a> SetLkW<'a> { - /// The value set by the [Open] method. See [FileHandle]. - pub fn file_handle(&self) -> FileHandle { - FileHandle(self.arg.fh) - } - pub fn lock(&self) -> Lock { - Lock::from_abi(&self.arg.lk) - } - pub fn lock_owner(&self) -> LockOwner { - LockOwner(self.arg.owner) - } - } - - /// Check file access permissions. - /// - /// This will be called for the `access()` system call. If the 'default_permissions' - /// mount option is given, this method is not called. - #[derive(Debug)] - pub struct Access<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_access_in, - } - impl_request!(Access<'a>); - impl<'a> Access<'a> { - pub fn mask(&self) -> i32 { - self.arg.mask - } - } - - /// Create and open a file. - /// - /// If the file does not exist, first create it with the specified mode, and then - /// open it. Open flags (with the exception of `O_NOCTTY`) are available in flags. - /// Filesystem may store an arbitrary file handle (pointer, index, etc) in fh, - /// and use this in other all other file operations ([Read], [Write], [Flush], [Release], - /// [FSync]). There are also some flags (direct_io, keep_cache) which the - /// filesystem may set, to change the way the file is opened. See fuse_file_info - /// structure in for more details. If this method is not - /// implemented or under Linux kernel versions earlier than 2.6.15, the [MkNod] - /// and [Open] methods will be called instead. - #[derive(Debug)] - pub struct Create<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_create_in, - name: &'a Path, - } - impl_request!(Create<'a>); - impl<'a> Create<'a> { - pub fn name(&self) -> &'a Path { - self.name - } - pub fn mode(&self) -> u32 { - self.arg.mode - } - /// Flags as passed to the creat() call - pub fn flags(&self) -> i32 { - self.arg.flags - } - pub fn umask(&self) -> u32 { - #[cfg(not(feature = "abi-7-12"))] - return 0; - #[cfg(feature = "abi-7-12")] - self.arg.umask - } - } - - /// If a process issuing a FUSE filesystem request is interrupted, the - /// following will happen: - /// - /// 1) If the request is not yet sent to userspace AND the signal is - /// fatal (SIGKILL or unhandled fatal signal), then the request is - /// dequeued and returns immediately. - /// - /// 2) If the request is not yet sent to userspace AND the signal is not - /// fatal, then an 'interrupted' flag is set for the request. When - /// the request has been successfully transferred to userspace and - /// this flag is set, an INTERRUPT request is queued. - /// - /// 3) If the request is already sent to userspace, then an INTERRUPT - /// request is queued. - /// - /// [Interrupt] requests take precedence over other requests, so the - /// userspace filesystem will receive queued [Interrupt]s before any others. - /// - /// The userspace filesystem may ignore the [Interrupt] requests entirely, - /// or may honor them by sending a reply to the **original** request, with - /// the error set to [Errno::EINTR]. - /// - /// It is also possible that there's a race between processing the - /// original request and its [Interrupt] request. There are two - /// possibilities: - /// - /// 1. The [Interrupt] request is processed before the original request is - /// processed - /// - /// 2. The [Interrupt] request is processed after the original request has - /// been answered - /// - /// If the filesystem cannot find the original request, it should wait for - /// some timeout and/or a number of new requests to arrive, after which it - /// should reply to the [Interrupt] request with an [Errno::EAGAIN] error. - /// In case (1) the [Interrupt] request will be requeued. In case (2) the - /// [Interrupt] reply will be ignored. - #[derive(Debug)] - pub struct Interrupt<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_interrupt_in, - } - impl_request!(Interrupt<'a>); - impl<'a> Interrupt<'a> { - pub fn unique(&self) -> RequestId { - RequestId(self.arg.unique) - } - } - - /// Map block index within file to block index within device. - /// Note: This makes sense only for block device backed filesystems mounted - /// with the 'blkdev' option - #[derive(Debug)] - pub struct BMap<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_bmap_in, - } - impl_request!(BMap<'a>); - impl<'a> BMap<'a> { - pub fn block_size(&self) -> u32 { - self.arg.blocksize - } - pub fn block(&self) -> u64 { - self.arg.block - } - } - - #[derive(Debug)] - pub struct Destroy<'a> { - header: &'a fuse_in_header, - } - impl_request!(Destroy<'a>); - impl<'a> Destroy<'a> { - pub fn reply(&self) -> Response<'a> { - Response::new_empty() - } - } - - /// Control device - #[cfg(feature = "abi-7-11")] - #[derive(Debug)] - pub struct IoCtl<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_ioctl_in, - data: &'a [u8], - } - #[cfg(feature = "abi-7-11")] - impl_request!(IoCtl<'a>); - #[cfg(feature = "abi-7-11")] - impl<'a> IoCtl<'a> { - pub fn in_data(&self) -> &[u8] { - &self.data[..self.arg.in_size as usize] - } - pub fn unrestricted(&self) -> bool { - self.arg.flags & consts::FUSE_IOCTL_UNRESTRICTED != 0 - } - /// The value set by the [Open] method. See [FileHandle]. - pub fn file_handle(&self) -> FileHandle { - FileHandle(self.arg.fh) - } - /// TODO: What are valid values here? - pub fn flags(&self) -> u32 { - self.arg.flags - } - /// TODO: What does this mean? - pub fn command(&self) -> u32 { - self.arg.cmd - } - pub fn out_size(&self) -> u32 { - self.arg.out_size - } - } - - /// Poll. - #[cfg(feature = "abi-7-11")] - #[derive(Debug)] - pub struct Poll<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_poll_in, - } - #[cfg(feature = "abi-7-11")] - impl_request!(Poll<'a>); - #[cfg(feature = "abi-7-11")] - impl<'a> Poll<'a> { - /// The value set by the [Open] method. See [FileHandle]. - pub fn file_handle(&self) -> FileHandle { - FileHandle(self.arg.fh) - } - - /// The unique id used for the poll context by the kernel - pub fn kernel_handle(&self) -> u64 { - self.arg.kh - } - - /// The requested poll events - pub fn events(&self) -> u32 { - #[cfg(feature = "abi-7-21")] - return self.arg.events; - #[cfg(not(feature = "abi-7-21"))] - return 0; - } - - /// The poll request's flags - pub fn flags(&self) -> u32 { - self.arg.flags - } - } - - /// NotifyReply. TODO: currently unsupported by fuser - #[cfg(feature = "abi-7-15")] - #[derive(Debug)] - pub struct NotifyReply<'a> { - header: &'a fuse_in_header, - #[allow(unused)] - arg: &'a [u8], - } - #[cfg(feature = "abi-7-15")] - impl_request!(NotifyReply<'a>); - - /// BatchForget: TODO: merge with Forget - #[cfg(feature = "abi-7-16")] - #[derive(Debug)] - pub struct BatchForget<'a> { - header: &'a fuse_in_header, - #[allow(unused)] - arg: &'a fuse_batch_forget_in, - nodes: &'a [fuse_forget_one], - } - #[cfg(feature = "abi-7-16")] - impl_request!(BatchForget<'a>); - #[cfg(feature = "abi-7-16")] - impl<'a> BatchForget<'a> { - /// TODO: Don't return fuse_forget_one, this should be private - pub fn nodes(&self) -> &'a [fuse_forget_one] { - self.nodes - } - } - - /// Preallocate or deallocate space to a file - /// - /// Implementations should return EINVAL if offset or length are < 0 - #[cfg(feature = "abi-7-19")] - #[derive(Debug)] - pub struct FAllocate<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_fallocate_in, - } - #[cfg(feature = "abi-7-19")] - impl_request!(FAllocate<'a>); - #[cfg(feature = "abi-7-19")] - impl<'a> FAllocate<'a> { - /// The value set by the [Open] method. See [FileHandle]. - pub fn file_handle(&self) -> FileHandle { - FileHandle(self.arg.fh) - } - pub fn offset(&self) -> i64 { - self.arg.offset - } - pub fn len(&self) -> i64 { - self.arg.length - } - /// `mode` as passed to fallocate. See `man 2 fallocate` - pub fn mode(&self) -> i32 { - self.arg.mode - } - } - - /// Read directory. - /// - /// TODO: Document when this is called rather than ReadDirectory - #[cfg(feature = "abi-7-21")] - #[derive(Debug)] - pub struct ReadDirPlus<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_read_in, - } - #[cfg(feature = "abi-7-21")] - impl_request!(ReadDirPlus<'a>); - #[cfg(feature = "abi-7-21")] - impl<'a> ReadDirPlus<'a> { - /// The value set by the [Open] method. See [FileHandle]. - pub fn file_handle(&self) -> FileHandle { - FileHandle(self.arg.fh) - } - pub fn offset(&self) -> i64 { - self.arg.offset - } - pub fn size(&self) -> u32 { - self.arg.size - } - } - - /// Rename a file. - /// - /// TODO: Document the differences to [Rename] and [Exchange] - #[cfg(feature = "abi-7-23")] - #[derive(Debug)] - pub struct Rename2<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_rename2_in, - name: &'a Path, - newname: &'a Path, - old_parent: INodeNo, - } - #[cfg(feature = "abi-7-23")] - impl_request!(Rename2<'a>); - #[cfg(feature = "abi-7-23")] - impl<'a> Rename2<'a> { - pub fn from(&self) -> FilenameInDir<'a> { - FilenameInDir::<'a> { - dir: self.old_parent, - name: self.name, - } - } - pub fn to(&self) -> FilenameInDir<'a> { - FilenameInDir::<'a> { - dir: INodeNo(self.arg.newdir), - name: self.newname, - } - } - /// Flags as passed to renameat2. As of Linux 3.18 this is - /// [libc::RENAME_EXCHANGE], [libc::RENAME_NOREPLACE] and - /// [libc::RENAME_WHITEOUT]. If you don't handle a particular flag - /// reply with an EINVAL error. - /// - /// TODO: Replace with enum/flags type - pub fn flags(&self) -> u32 { - self.arg.flags - } - } - - /// Reposition read/write file offset - /// - /// TODO: Document when you need to implement this. Read and Write provide the offset anyway. - #[cfg(feature = "abi-7-24")] - #[derive(Debug)] - pub struct Lseek<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_lseek_in, - } - #[cfg(feature = "abi-7-24")] - impl_request!(Lseek<'a>); - #[cfg(feature = "abi-7-24")] - impl<'a> Lseek<'a> { - /// The value set by the [Open] method. See [FileHandle]. - pub fn file_handle(&self) -> FileHandle { - FileHandle(self.arg.fh) - } - pub fn offset(&self) -> i64 { - self.arg.offset - } - /// TODO: Make this return an enum - pub fn whence(&self) -> i32 { - self.arg.whence - } - } - - /// Copy the specified range from the source inode to the destination inode - #[cfg(feature = "abi-7-28")] - #[derive(Debug, Clone, Copy)] - pub struct CopyFileRangeFile { - pub inode: INodeNo, - /// The value set by the [Open] method. See [FileHandle]. - pub file_handle: FileHandle, - pub offset: i64, - } - #[cfg(feature = "abi-7-28")] - #[derive(Debug)] - pub struct CopyFileRange<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_copy_file_range_in, - } - #[cfg(feature = "abi-7-28")] - impl_request!(CopyFileRange<'a>); - #[cfg(feature = "abi-7-28")] - impl<'a> CopyFileRange<'a> { - /// File and offset to copy data from - pub fn src(&self) -> CopyFileRangeFile { - CopyFileRangeFile { - inode: self.nodeid(), - file_handle: FileHandle(self.arg.fh_in), - offset: self.arg.off_in, - } - } - /// File and offset to copy data to - pub fn dest(&self) -> CopyFileRangeFile { - CopyFileRangeFile { - inode: INodeNo(self.arg.nodeid_out), - file_handle: FileHandle(self.arg.fh_out), - offset: self.arg.off_out, - } - } - /// Number of bytes to copy - pub fn len(&self) -> u64 { - self.arg.len - } - // API TODO: Return a specific flags type - pub fn flags(&self) -> u64 { - self.arg.flags - } - } - - /// MacOS only: Rename the volume. Set `fuse_init_out.flags` during init to - /// `FUSE_VOL_RENAME` to enable - #[cfg(target_os = "macos")] - #[derive(Debug)] - pub struct SetVolName<'a> { - header: &'a fuse_in_header, - name: &'a OsStr, - } - #[cfg(target_os = "macos")] - impl_request!(SetVolName<'a>); - #[cfg(target_os = "macos")] - impl<'a> SetVolName<'a> { - pub fn name(&self) -> &'a OsStr { - self.name - } - } - - /// macOS only: Query extended times (bkuptime and crtime). Set fuse_init_out.flags - /// during init to FUSE_XTIMES to enable - #[cfg(target_os = "macos")] - #[derive(Debug)] - pub struct GetXTimes<'a> { - header: &'a fuse_in_header, - } - #[cfg(target_os = "macos")] - impl_request!(GetXTimes<'a>); - // API TODO: Consider rename2(RENAME_EXCHANGE) - /// macOS only (undocumented) - #[cfg(target_os = "macos")] - #[derive(Debug)] - pub struct Exchange<'a> { - header: &'a fuse_in_header, - arg: &'a fuse_exchange_in, - oldname: &'a Path, - newname: &'a Path, - } - #[cfg(target_os = "macos")] - impl_request!(Exchange<'a>); - #[cfg(target_os = "macos")] - impl<'a> Exchange<'a> { - pub fn from(&self) -> FilenameInDir<'a> { - FilenameInDir::<'a> { - dir: INodeNo(self.arg.olddir), - name: self.oldname, - } - } - pub fn to(&self) -> FilenameInDir<'a> { - FilenameInDir::<'a> { - dir: INodeNo(self.arg.newdir), - name: self.newname, - } - } - pub fn options(&self) -> u64 { - self.arg.options - } - } - /// TODO: Document - #[cfg(feature = "abi-7-12")] - #[derive(Debug)] - pub struct CuseInit<'a> { - header: &'a fuse_in_header, - #[allow(unused)] - arg: &'a fuse_init_in, - } - #[cfg(feature = "abi-7-12")] - impl_request!(CuseInit<'a>); - - fn system_time_from_time(secs: i64, nsecs: u32) -> SystemTime { - if secs >= 0 { - SystemTime::UNIX_EPOCH + Duration::new(secs as u64, nsecs) - } else { - SystemTime::UNIX_EPOCH - Duration::new((-secs) as u64, nsecs) - } - } - pub(crate) fn parse<'a>( - header: &'a fuse_in_header, - opcode: &fuse_opcode, - data: &'a [u8], - ) -> Option> { - let mut data = ArgumentIterator::new(data); - Some(match opcode { - fuse_opcode::FUSE_LOOKUP => Operation::Lookup(Lookup { - header, - name: data.fetch_str()?, - }), - fuse_opcode::FUSE_FORGET => Operation::Forget(Forget { - header, - arg: data.fetch()?, - }), - fuse_opcode::FUSE_GETATTR => Operation::GetAttr(GetAttr { - header, - - #[cfg(feature = "abi-7-9")] - arg: data.fetch()?, - }), - fuse_opcode::FUSE_SETATTR => Operation::SetAttr(SetAttr { - header, - arg: data.fetch()?, - }), - fuse_opcode::FUSE_READLINK => Operation::ReadLink(ReadLink { header }), - fuse_opcode::FUSE_SYMLINK => Operation::SymLink(SymLink { - header, - link_name: data.fetch_str()?.as_ref(), - target: data.fetch_str()?.as_ref(), - }), - fuse_opcode::FUSE_MKNOD => Operation::MkNod(MkNod { - header, - arg: data.fetch()?, - name: data.fetch_str()?.as_ref(), - }), - fuse_opcode::FUSE_MKDIR => Operation::MkDir(MkDir { - header, - arg: data.fetch()?, - name: data.fetch_str()?.as_ref(), - }), - fuse_opcode::FUSE_UNLINK => Operation::Unlink(Unlink { - header, - name: data.fetch_str()?.as_ref(), - }), - fuse_opcode::FUSE_RMDIR => Operation::RmDir(RmDir { - header, - name: data.fetch_str()?.as_ref(), - }), - fuse_opcode::FUSE_RENAME => Operation::Rename(Rename { - header, - arg: data.fetch()?, - name: data.fetch_str()?.as_ref(), - newname: data.fetch_str()?.as_ref(), - }), - fuse_opcode::FUSE_LINK => Operation::Link(Link { - header, - arg: data.fetch()?, - name: data.fetch_str()?.as_ref(), - }), - fuse_opcode::FUSE_OPEN => Operation::Open(Open { - header, - arg: data.fetch()?, - }), - fuse_opcode::FUSE_READ => Operation::Read(Read { - header, - arg: data.fetch()?, - }), - fuse_opcode::FUSE_WRITE => Operation::Write({ - let out = Write { - header, - arg: data.fetch()?, - data: data.fetch_all(), - }; - assert!(out.data().len() == out.arg.size as usize); - out - }), - fuse_opcode::FUSE_STATFS => Operation::StatFs(StatFs { header }), - fuse_opcode::FUSE_RELEASE => Operation::Release(Release { - header, - arg: data.fetch()?, - }), - fuse_opcode::FUSE_FSYNC => Operation::FSync(FSync { - header, - arg: data.fetch()?, - }), - fuse_opcode::FUSE_SETXATTR => Operation::SetXAttr({ - let out = SetXAttr { - header, - arg: data.fetch()?, - name: data.fetch_str()?, - value: data.fetch_all(), - }; - assert!(out.value.len() == out.arg.size as usize); - out - }), - fuse_opcode::FUSE_GETXATTR => Operation::GetXAttr(GetXAttr { - header, - arg: data.fetch()?, - name: data.fetch_str()?, - }), - fuse_opcode::FUSE_LISTXATTR => Operation::ListXAttr(ListXAttr { - header, - arg: data.fetch()?, - }), - fuse_opcode::FUSE_REMOVEXATTR => Operation::RemoveXAttr(RemoveXAttr { - header, - name: data.fetch_str()?, - }), - fuse_opcode::FUSE_FLUSH => Operation::Flush(Flush { - header, - arg: data.fetch()?, - }), - fuse_opcode::FUSE_INIT => Operation::Init(Init { - header, - arg: data.fetch()?, - }), - fuse_opcode::FUSE_OPENDIR => Operation::OpenDir(OpenDir { - header, - arg: data.fetch()?, - }), - fuse_opcode::FUSE_READDIR => Operation::ReadDir(ReadDir { - header, - arg: data.fetch()?, - }), - fuse_opcode::FUSE_RELEASEDIR => Operation::ReleaseDir(ReleaseDir { - header, - arg: data.fetch()?, - }), - fuse_opcode::FUSE_FSYNCDIR => Operation::FSyncDir(FSyncDir { - header, - arg: data.fetch()?, - }), - fuse_opcode::FUSE_GETLK => Operation::GetLk(GetLk { - header, - arg: data.fetch()?, - }), - fuse_opcode::FUSE_SETLK => Operation::SetLk(SetLk { - header, - arg: data.fetch()?, - }), - fuse_opcode::FUSE_SETLKW => Operation::SetLkW(SetLkW { - header, - arg: data.fetch()?, - }), - fuse_opcode::FUSE_ACCESS => Operation::Access(Access { - header, - arg: data.fetch()?, - }), - fuse_opcode::FUSE_CREATE => Operation::Create(Create { - header, - arg: data.fetch()?, - name: data.fetch_str()?.as_ref(), - }), - fuse_opcode::FUSE_INTERRUPT => Operation::Interrupt(Interrupt { - header, - arg: data.fetch()?, - }), - fuse_opcode::FUSE_BMAP => Operation::BMap(BMap { - header, - arg: data.fetch()?, - }), - fuse_opcode::FUSE_DESTROY => Operation::Destroy(Destroy { header }), - #[cfg(feature = "abi-7-11")] - fuse_opcode::FUSE_IOCTL => Operation::IoCtl(IoCtl { - header, - arg: data.fetch()?, - data: data.fetch_all(), - }), - #[cfg(feature = "abi-7-11")] - fuse_opcode::FUSE_POLL => Operation::Poll(Poll { - header, - arg: data.fetch()?, - }), - #[cfg(feature = "abi-7-15")] - fuse_opcode::FUSE_NOTIFY_REPLY => Operation::NotifyReply(NotifyReply { - header, - arg: data.fetch_all(), - }), - #[cfg(feature = "abi-7-16")] - fuse_opcode::FUSE_BATCH_FORGET => { - let arg = data.fetch()?; - Operation::BatchForget(BatchForget { - header, - arg, - nodes: data.fetch_slice(arg.count as usize)?, - }) - } - #[cfg(feature = "abi-7-19")] - fuse_opcode::FUSE_FALLOCATE => Operation::FAllocate(FAllocate { - header, - arg: data.fetch()?, - }), - #[cfg(feature = "abi-7-21")] - fuse_opcode::FUSE_READDIRPLUS => Operation::ReadDirPlus(ReadDirPlus { - header, - arg: data.fetch()?, - }), - #[cfg(feature = "abi-7-23")] - fuse_opcode::FUSE_RENAME2 => Operation::Rename2(Rename2 { - header, - arg: data.fetch()?, - name: data.fetch_str()?.as_ref(), - newname: data.fetch_str()?.as_ref(), - old_parent: INodeNo(header.nodeid), - }), - #[cfg(feature = "abi-7-24")] - fuse_opcode::FUSE_LSEEK => Operation::Lseek(Lseek { - header, - arg: data.fetch()?, - }), - #[cfg(feature = "abi-7-28")] - fuse_opcode::FUSE_COPY_FILE_RANGE => Operation::CopyFileRange(CopyFileRange { - header, - arg: data.fetch()?, - }), - - #[cfg(target_os = "macos")] - fuse_opcode::FUSE_SETVOLNAME => Operation::SetVolName(SetVolName { - header, - name: data.fetch_str()?, - }), - #[cfg(target_os = "macos")] - fuse_opcode::FUSE_GETXTIMES => Operation::GetXTimes(GetXTimes { header }), - #[cfg(target_os = "macos")] - fuse_opcode::FUSE_EXCHANGE => Operation::Exchange(Exchange { - header, - arg: data.fetch()?, - oldname: data.fetch_str()?.as_ref(), - newname: data.fetch_str()?.as_ref(), - }), - - #[cfg(feature = "abi-7-12")] - fuse_opcode::CUSE_INIT => Operation::CuseInit(CuseInit { - header, - arg: data.fetch()?, - }), - }) - } -} -use op::*; - -/// Filesystem operation (and arguments) the kernel driver wants us to perform. The fields of each -/// variant needs to match the actual arguments the kernel driver sends for the specific operation. -#[derive(Debug)] -#[allow(missing_docs)] -pub enum Operation<'a> { - Lookup(Lookup<'a>), - Forget(Forget<'a>), - GetAttr(GetAttr<'a>), - SetAttr(SetAttr<'a>), - #[allow(dead_code)] - ReadLink(ReadLink<'a>), - SymLink(SymLink<'a>), - MkNod(MkNod<'a>), - MkDir(MkDir<'a>), - Unlink(Unlink<'a>), - RmDir(RmDir<'a>), - Rename(Rename<'a>), - Link(Link<'a>), - Open(Open<'a>), - Read(Read<'a>), - Write(Write<'a>), - #[allow(dead_code)] - StatFs(StatFs<'a>), - Release(Release<'a>), - FSync(FSync<'a>), - SetXAttr(SetXAttr<'a>), - GetXAttr(GetXAttr<'a>), - ListXAttr(ListXAttr<'a>), - RemoveXAttr(RemoveXAttr<'a>), - Flush(Flush<'a>), - Init(Init<'a>), - OpenDir(OpenDir<'a>), - ReadDir(ReadDir<'a>), - ReleaseDir(ReleaseDir<'a>), - FSyncDir(FSyncDir<'a>), - GetLk(GetLk<'a>), - SetLk(SetLk<'a>), - SetLkW(SetLkW<'a>), - Access(Access<'a>), - Create(Create<'a>), - Interrupt(Interrupt<'a>), - BMap(BMap<'a>), - Destroy(Destroy<'a>), - #[cfg(feature = "abi-7-11")] - IoCtl(IoCtl<'a>), - #[cfg(feature = "abi-7-11")] - Poll(Poll<'a>), - #[cfg(feature = "abi-7-15")] - #[allow(dead_code)] - NotifyReply(NotifyReply<'a>), - #[cfg(feature = "abi-7-16")] - BatchForget(BatchForget<'a>), - #[cfg(feature = "abi-7-19")] - FAllocate(FAllocate<'a>), - #[cfg(feature = "abi-7-21")] - ReadDirPlus(ReadDirPlus<'a>), - #[cfg(feature = "abi-7-23")] - Rename2(Rename2<'a>), - #[cfg(feature = "abi-7-24")] - Lseek(Lseek<'a>), - #[cfg(feature = "abi-7-28")] - CopyFileRange(CopyFileRange<'a>), - - #[cfg(target_os = "macos")] - SetVolName(SetVolName<'a>), - #[cfg(target_os = "macos")] - GetXTimes(GetXTimes<'a>), - #[cfg(target_os = "macos")] - Exchange(Exchange<'a>), - - #[cfg(feature = "abi-7-12")] - #[allow(dead_code)] - CuseInit(CuseInit<'a>), -} - -impl<'a> fmt::Display for Operation<'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Operation::Lookup(x) => write!(f, "LOOKUP name {:?}", x.name()), - Operation::Forget(x) => write!(f, "FORGET nlookup {}", x.nlookup()), - Operation::GetAttr(_) => write!(f, "GETATTR"), - Operation::SetAttr(x) => x.fmt(f), - Operation::ReadLink(_) => write!(f, "READLINK"), - Operation::SymLink(x) => { - write!( - f, - "SYMLINK target {:?}, link_name {:?}", - x.target(), - x.link_name() - ) - } - Operation::MkNod(x) => write!( - f, - "MKNOD name {:?}, mode {:#05o}, rdev {}", - x.name(), - x.mode(), - x.rdev() - ), - Operation::MkDir(x) => write!(f, "MKDIR name {:?}, mode {:#05o}", x.name(), x.mode()), - Operation::Unlink(x) => write!(f, "UNLINK name {:?}", x.name()), - Operation::RmDir(x) => write!(f, "RMDIR name {:?}", x.name), - Operation::Rename(x) => write!(f, "RENAME src {:?}, dest {:?}", x.src(), x.dest()), - Operation::Link(x) => write!(f, "LINK ino {:?}, dest {:?}", x.inode_no(), x.dest()), - Operation::Open(x) => write!(f, "OPEN flags {:#x}", x.flags()), - Operation::Read(x) => write!( - f, - "READ fh {:?}, offset {}, size {}", - x.file_handle(), - x.offset(), - x.size() - ), - Operation::Write(x) => write!( - f, - "WRITE fh {:?}, offset {}, size {}, write flags {:#x}", - x.file_handle(), - x.offset(), - x.data().len(), - x.write_flags() - ), - Operation::StatFs(_) => write!(f, "STATFS"), - Operation::Release(x) => write!( - f, - "RELEASE fh {:?}, flags {:#x}, flush {}, lock owner {:?}", - x.file_handle(), - x.flags(), - x.flush(), - x.lock_owner() - ), - Operation::FSync(x) => write!( - f, - "FSYNC fh {:?}, fsync fdatasync {}", - x.file_handle(), - x.fdatasync() - ), - Operation::SetXAttr(x) => write!( - f, - "SETXATTR name {:?}, size {}, flags {:#x}", - x.name(), - x.value().len(), - x.flags() - ), - Operation::GetXAttr(x) => { - write!(f, "GETXATTR name {:?}, size {:?}", x.name(), x.size()) - } - Operation::ListXAttr(x) => write!(f, "LISTXATTR size {}", x.size()), - Operation::RemoveXAttr(x) => write!(f, "REMOVEXATTR name {:?}", x.name()), - Operation::Flush(x) => write!( - f, - "FLUSH fh {:?}, lock owner {:?}", - x.file_handle(), - x.lock_owner() - ), - Operation::Init(x) => write!( - f, - "INIT kernel ABI {}, capabilities {:#x}, max readahead {}", - x.version(), - x.capabilities(), - x.max_readahead() - ), - Operation::OpenDir(x) => write!(f, "OPENDIR flags {:#x}", x.flags()), - Operation::ReadDir(x) => write!( - f, - "READDIR fh {:?}, offset {}, size {}", - x.file_handle(), - x.offset(), - x.size() - ), - Operation::ReleaseDir(x) => write!( - f, - "RELEASEDIR fh {:?}, flags {:#x}, flush {}, lock owner {:?}", - x.file_handle(), - x.flags(), - x.flush(), - x.lock_owner() - ), - Operation::FSyncDir(x) => write!( - f, - "FSYNCDIR fh {:?}, fsync fdatasync: {}", - x.file_handle(), - x.fdatasync() - ), - Operation::GetLk(x) => write!( - f, - "GETLK fh {:?}, lock owner {:?}", - x.file_handle(), - x.lock_owner() - ), - Operation::SetLk(x) => write!( - f, - "SETLK fh {:?}, lock owner {:?}", - x.file_handle(), - x.lock_owner() - ), - Operation::SetLkW(x) => write!( - f, - "SETLKW fh {:?}, lock owner {:?}", - x.file_handle(), - x.lock_owner() - ), - Operation::Access(x) => write!(f, "ACCESS mask {:#05o}", x.mask()), - Operation::Create(x) => write!( - f, - "CREATE name {:?}, mode {:#05o}, flags {:#x}", - x.name(), - x.mode(), - x.flags() - ), - Operation::Interrupt(x) => write!(f, "INTERRUPT unique {:?}", x.unique()), - Operation::BMap(x) => write!(f, "BMAP blocksize {}, ids {}", x.block_size(), x.block()), - Operation::Destroy(_) => write!(f, "DESTROY"), - #[cfg(feature = "abi-7-11")] - Operation::IoCtl(x) => write!( - f, - "IOCTL fh {:?}, cmd {}, data size {}, flags {:#x}", - x.file_handle(), - x.command(), - x.in_data().len(), - x.flags() - ), - #[cfg(feature = "abi-7-11")] - Operation::Poll(x) => write!(f, "POLL fh {:?}", x.file_handle()), - #[cfg(feature = "abi-7-15")] - Operation::NotifyReply(_) => write!(f, "NOTIFYREPLY"), - #[cfg(feature = "abi-7-16")] - Operation::BatchForget(x) => write!(f, "BATCHFORGET nodes {:?}", x.nodes()), - #[cfg(feature = "abi-7-19")] - Operation::FAllocate(_) => write!(f, "FALLOCATE"), - #[cfg(feature = "abi-7-21")] - Operation::ReadDirPlus(x) => write!( - f, - "READDIRPLUS fh {:?}, offset {}, size {}", - x.file_handle(), - x.offset(), - x.size() - ), - #[cfg(feature = "abi-7-23")] - Operation::Rename2(x) => write!(f, "RENAME2 from {:?}, to {:?}", x.from(), x.to()), - #[cfg(feature = "abi-7-24")] - Operation::Lseek(x) => write!( - f, - "LSEEK fh {:?}, offset {}, whence {}", - x.file_handle(), - x.offset(), - x.whence() - ), - #[cfg(feature = "abi-7-28")] - Operation::CopyFileRange(x) => write!( - f, - "COPY_FILE_RANGE src {:?}, dest {:?}, len {}", - x.src(), - x.dest(), - x.len() - ), - - #[cfg(target_os = "macos")] - Operation::SetVolName(x) => write!(f, "SETVOLNAME name {:?}", x.name()), - #[cfg(target_os = "macos")] - Operation::GetXTimes(_) => write!(f, "GETXTIMES"), - #[cfg(target_os = "macos")] - Operation::Exchange(x) => write!( - f, - "EXCHANGE from {:?}, to {:?}, options {:#x}", - x.from(), - x.to(), - x.options() - ), - - #[cfg(feature = "abi-7-12")] - Operation::CuseInit(_) => write!(f, "CUSE_INIT"), - } - } -} - -/// Low-level request of a filesystem operation the kernel driver wants to perform. -#[derive(Debug)] -pub struct AnyRequest<'a> { - header: &'a fuse_in_header, - data: &'a [u8], -} -impl_request!(AnyRequest<'_>); - -impl<'a> AnyRequest<'a> { - pub fn operation(&self) -> Result, RequestError> { - // Parse/check opcode - let opcode = fuse_opcode::try_from(self.header.opcode) - .map_err(|_: InvalidOpcodeError| RequestError::UnknownOperation(self.header.opcode))?; - // Parse/check operation arguments - op::parse(self.header, &opcode, self.data).ok_or(RequestError::InsufficientData) - } -} - -impl<'a> fmt::Display for AnyRequest<'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Ok(op) = self.operation() { - write!( - f, - "FUSE({:3}) ino {:#018x} {}", - self.header.unique, self.header.nodeid, op - ) - } else { - write!( - f, - "FUSE({:3}) ino {:#018x}", - self.header.unique, self.header.nodeid - ) - } - } -} - -impl<'a> TryFrom<&'a [u8]> for AnyRequest<'a> { - type Error = RequestError; - - fn try_from(data: &'a [u8]) -> Result { - // Parse a raw packet as sent by the kernel driver into typed data. Every request always - // begins with a `fuse_in_header` struct followed by arguments depending on the opcode. - let data_len = data.len(); - let mut arg_iter = ArgumentIterator::new(data); - // Parse header - let header: &fuse_in_header = arg_iter - .fetch() - .ok_or_else(|| RequestError::ShortReadHeader(arg_iter.len()))?; - // Check data size - if data_len < header.len as usize { - return Err(RequestError::ShortRead(data_len, header.len as usize)); - } - Ok(Self { - header, - data: &data[mem::size_of::()..header.len as usize], - }) - } -} - -#[cfg(test)] -mod tests { - use super::super::test::AlignedData; - use super::*; - use std::ffi::OsStr; - - #[cfg(target_endian = "big")] - const INIT_REQUEST: AlignedData<[u8; 56]> = AlignedData([ - 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1a, // len, opcode - 0xde, 0xad, 0xbe, 0xef, 0xba, 0xad, 0xd0, 0x0d, // unique - 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, // nodeid - 0xc0, 0x01, 0xd0, 0x0d, 0xc0, 0x01, 0xca, 0xfe, // uid, gid - 0xc0, 0xde, 0xba, 0x5e, 0x00, 0x00, 0x00, 0x00, // pid, padding - 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x08, // major, minor - 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, // max_readahead, flags - ]); - - #[cfg(target_endian = "little")] - const INIT_REQUEST: AlignedData<[u8; 56]> = AlignedData([ - 0x38, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, // len, opcode - 0x0d, 0xf0, 0xad, 0xba, 0xef, 0xbe, 0xad, 0xde, // unique - 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, // nodeid - 0x0d, 0xd0, 0x01, 0xc0, 0xfe, 0xca, 0x01, 0xc0, // uid, gid - 0x5e, 0xba, 0xde, 0xc0, 0x00, 0x00, 0x00, 0x00, // pid, padding - 0x07, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, // major, minor - 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // max_readahead, flags - ]); - - #[cfg(target_endian = "big")] - const MKNOD_REQUEST: AlignedData<[u8; 56]> = [ - 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x08, // len, opcode - 0xde, 0xad, 0xbe, 0xef, 0xba, 0xad, 0xd0, 0x0d, // unique - 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, // nodeid - 0xc0, 0x01, 0xd0, 0x0d, 0xc0, 0x01, 0xca, 0xfe, // uid, gid - 0xc0, 0xde, 0xba, 0x5e, 0x00, 0x00, 0x00, 0x00, // pid, padding - 0x00, 0x00, 0x01, 0xa4, 0x00, 0x00, 0x00, 0x00, // mode, rdev - 0x66, 0x6f, 0x6f, 0x2e, 0x74, 0x78, 0x74, 0x00, // name - ]; - - #[cfg(all(target_endian = "little", not(feature = "abi-7-12")))] - const MKNOD_REQUEST: AlignedData<[u8; 56]> = AlignedData([ - 0x38, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, // len, opcode - 0x0d, 0xf0, 0xad, 0xba, 0xef, 0xbe, 0xad, 0xde, // unique - 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, // nodeid - 0x0d, 0xd0, 0x01, 0xc0, 0xfe, 0xca, 0x01, 0xc0, // uid, gid - 0x5e, 0xba, 0xde, 0xc0, 0x00, 0x00, 0x00, 0x00, // pid, padding - 0xa4, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mode, rdev - 0x66, 0x6f, 0x6f, 0x2e, 0x74, 0x78, 0x74, 0x00, // name - ]); - - #[cfg(all(target_endian = "little", feature = "abi-7-12"))] - const MKNOD_REQUEST: AlignedData<[u8; 64]> = AlignedData([ - 0x40, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, // len, opcode - 0x0d, 0xf0, 0xad, 0xba, 0xef, 0xbe, 0xad, 0xde, // unique - 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, // nodeid - 0x0d, 0xd0, 0x01, 0xc0, 0xfe, 0xca, 0x01, 0xc0, // uid, gid - 0x5e, 0xba, 0xde, 0xc0, 0x00, 0x00, 0x00, 0x00, // pid, padding - 0xa4, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mode, rdev - 0xed, 0x01, 0x00, 0x00, 0xe7, 0x03, 0x00, 0x00, // umask, padding - 0x66, 0x6f, 0x6f, 0x2e, 0x74, 0x78, 0x74, 0x00, // name - ]); - - #[test] - fn short_read_header() { - match AnyRequest::try_from(&INIT_REQUEST[..20]) { - Err(RequestError::ShortReadHeader(20)) => (), - _ => panic!("Unexpected request parsing result"), - } - } - - #[test] - fn short_read() { - match AnyRequest::try_from(&INIT_REQUEST[..48]) { - Err(RequestError::ShortRead(48, 56)) => (), - _ => panic!("Unexpected request parsing result"), - } - } - - #[test] - fn init() { - let req = AnyRequest::try_from(&INIT_REQUEST[..]).unwrap(); - assert_eq!(req.header.len, 56); - assert_eq!(req.header.opcode, 26); - assert_eq!(req.unique(), RequestId(0xdead_beef_baad_f00d)); - assert_eq!(req.nodeid(), INodeNo(0x1122_3344_5566_7788)); - assert_eq!(req.uid(), 0xc001_d00d); - assert_eq!(req.gid(), 0xc001_cafe); - assert_eq!(req.pid(), 0xc0de_ba5e); - match req.operation().unwrap() { - Operation::Init(x) => { - assert_eq!(x.version(), Version(7, 8)); - assert_eq!(x.max_readahead(), 4096); - } - _ => panic!("Unexpected request operation"), - } - } - - #[test] - fn mknod() { - let req = AnyRequest::try_from(&MKNOD_REQUEST[..]).unwrap(); - #[cfg(not(feature = "abi-7-12"))] - assert_eq!(req.header.len, 56); - #[cfg(feature = "abi-7-12")] - assert_eq!(req.header.len, 64); - assert_eq!(req.header.opcode, 8); - assert_eq!(req.unique(), RequestId(0xdead_beef_baad_f00d)); - assert_eq!(req.nodeid(), INodeNo(0x1122_3344_5566_7788)); - assert_eq!(req.uid(), 0xc001_d00d); - assert_eq!(req.gid(), 0xc001_cafe); - assert_eq!(req.pid(), 0xc0de_ba5e); - match req.operation().unwrap() { - Operation::MkNod(x) => { - assert_eq!(x.mode(), 0o644); - #[cfg(feature = "abi-7-12")] - assert_eq!(x.umask(), 0o755); - assert_eq!(x.name(), OsStr::new("foo.txt")); - } - _ => panic!("Unexpected request operation"), - } - } -} diff --git a/vendor/fuser/src/mnt/fuse2.rs b/vendor/fuser/src/mnt/fuse2.rs deleted file mode 100644 index fba81934a..000000000 --- a/vendor/fuser/src/mnt/fuse2.rs +++ /dev/null @@ -1,107 +0,0 @@ -use super::{MountOption, fuse2_sys::*, with_fuse_args}; -use log::warn; -use std::{ - ffi::CString, - fs::File, - io, - os::unix::prelude::{FromRawFd, OsStrExt}, - path::Path, - sync::Arc, -}; - -/// Ensures that an os error is never 0/Success -fn ensure_last_os_error() -> io::Error { - let err = io::Error::last_os_error(); - match err.raw_os_error() { - Some(0) => io::Error::new(io::ErrorKind::Other, "Unspecified Error"), - _ => err, - } -} - -#[derive(Debug)] -pub struct Mount { - mountpoint: CString, - #[cfg(all(target_os = "macos", feature = "macfuse-5"))] - channel: Option, - #[cfg(all(target_os = "macos", feature = "macfuse-5"))] - macfuse: Option>, -} -impl Mount { - pub fn new(mountpoint: &Path, options: &[MountOption]) -> io::Result<(Arc, Mount)> { - let mountpoint = CString::new(mountpoint.as_os_str().as_bytes()).unwrap(); - #[cfg(all(target_os = "macos", feature = "macfuse-5"))] - let macfuse = MacFuseApi::load()?; - with_fuse_args(options, |args| { - #[cfg(all(target_os = "macos", feature = "macfuse-5"))] - let (fd, channel) = unsafe { - let channel = (macfuse.mount)(mountpoint.as_ptr(), args); - if channel.is_null() { - return Err(ensure_last_os_error()); - } - let fd = libc::dup((macfuse.chan_fd)(channel)); - if fd < 0 { - (macfuse.unmount)(mountpoint.as_ptr(), channel); - return Err(ensure_last_os_error()); - } - (fd, Some(channel as usize)) - }; - #[cfg(not(all(target_os = "macos", feature = "macfuse-5")))] - let fd = unsafe { fuse_mount_compat25(mountpoint.as_ptr(), args) }; - if fd < 0 { - Err(ensure_last_os_error()) - } else { - let file = unsafe { File::from_raw_fd(fd) }; - Ok(( - Arc::new(file), - Mount { - mountpoint, - #[cfg(all(target_os = "macos", feature = "macfuse-5"))] - channel, - #[cfg(all(target_os = "macos", feature = "macfuse-5"))] - macfuse: Some(Arc::clone(&macfuse)), - }, - )) - } - }) - } -} -impl Drop for Mount { - fn drop(&mut self) { - #[cfg(all(target_os = "macos", feature = "macfuse-5"))] - if let Some(channel) = self.channel.take() { - if let Some(macfuse) = self.macfuse.take() { - unsafe { - (macfuse.unmount)(self.mountpoint.as_ptr(), channel as *mut fuse_chan); - } - } - return; - } - - use std::io::ErrorKind::PermissionDenied; - - // fuse_unmount_compat22 unfortunately doesn't return a status. Additionally, - // it attempts to call realpath, which in turn calls into the filesystem. So - // if the filesystem returns an error, the unmount does not take place, with - // no indication of the error available to the caller. So we call unmount - // directly, which is what osxfuse does anyway, since we already converted - // to the real path when we first mounted. - if let Err(err) = super::libc_umount(&self.mountpoint) { - // Linux always returns EPERM for non-root users. We have to let the - // library go through the setuid-root "fusermount -u" to unmount. - if err.kind() == PermissionDenied { - #[cfg(not(any( - target_os = "macos", - target_os = "freebsd", - target_os = "dragonfly", - target_os = "openbsd", - target_os = "netbsd" - )))] - unsafe { - fuse_unmount_compat22(self.mountpoint.as_ptr()); - return; - } - } - warn!("umount failed with {:?}", err); - } - } -} diff --git a/vendor/fuser/src/mnt/fuse2_sys.rs b/vendor/fuser/src/mnt/fuse2_sys.rs deleted file mode 100644 index 74b923e7e..000000000 --- a/vendor/fuser/src/mnt/fuse2_sys.rs +++ /dev/null @@ -1,121 +0,0 @@ -//! Native FFI bindings to libfuse2. -//! -//! This is a small set of bindings that are required to mount/unmount FUSE filesystems and -//! open/close a fd to the FUSE kernel driver. - -#![warn(missing_debug_implementations)] -#![allow(missing_docs)] - -use libc::{c_char, c_int}; -#[cfg(all(target_os = "macos", feature = "macfuse-5"))] -use std::{ - io, - sync::{Arc, OnceLock}, -}; - -#[repr(C)] -#[derive(Debug)] -pub struct fuse_args { - pub argc: c_int, - pub argv: *const *const c_char, - pub allocated: c_int, -} - -#[cfg(all(target_os = "macos", feature = "macfuse-5"))] -#[repr(C)] -#[derive(Debug)] -pub struct fuse_chan { - _private: [u8; 0], -} - -#[cfg(all(target_os = "macos", feature = "macfuse-5"))] -pub struct MacFuseApi { - _library: libloading::Library, - pub mount: unsafe extern "C" fn(*const c_char, *const fuse_args) -> *mut fuse_chan, - pub chan_fd: unsafe extern "C" fn(*mut fuse_chan) -> c_int, - pub unmount: unsafe extern "C" fn(*const c_char, *mut fuse_chan), -} - -#[cfg(all(target_os = "macos", feature = "macfuse-5"))] -impl std::fmt::Debug for MacFuseApi { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter - .debug_struct("MacFuseApi") - .finish_non_exhaustive() - } -} - -#[cfg(all(target_os = "macos", feature = "macfuse-5"))] -impl MacFuseApi { - pub fn load() -> io::Result> { - static API: OnceLock, String>> = OnceLock::new(); - match API.get_or_init(|| Self::load_uncached().map_err(|error| error.to_string())) { - Ok(api) => Ok(Arc::clone(api)), - Err(error) => Err(io::Error::new(io::ErrorKind::NotFound, error.clone())), - } - } - - fn load_uncached() -> io::Result> { - const CANDIDATES: [&str; 4] = [ - "/usr/local/lib/libfuse.2.dylib", - "/usr/local/lib/libfuse.dylib", - "libfuse.2.dylib", - "libfuse.dylib", - ]; - let mut failures = Vec::new(); - for candidate in CANDIDATES { - let library = match unsafe { libloading::Library::new(candidate) } { - Ok(library) => library, - Err(error) => { - failures.push(format!("{candidate}: {error}")); - continue; - } - }; - let symbols = unsafe { - let mount = *library - .get:: *mut fuse_chan>(b"fuse_mount\0") - .map_err(io::Error::other)?; - let chan_fd = *library - .get:: c_int>(b"fuse_chan_fd\0") - .map_err(io::Error::other)?; - let unmount = *library - .get::(b"fuse_unmount\0") - .map_err(io::Error::other)?; - (mount, chan_fd, unmount) - }; - return Ok(Arc::new(Self { - _library: library, - mount: symbols.0, - chan_fd: symbols.1, - unmount: symbols.2, - })); - } - Err(io::Error::new( - io::ErrorKind::NotFound, - format!( - "macFUSE runtime library not found; install macFUSE before mounting ({})", - failures.join("; ") - ), - )) - } -} - -#[cfg(fuser_mount_impl = "libfuse2")] -extern "C" { - // *_compat25 functions were introduced in FUSE 2.6 when function signatures changed. - // Therefore, the minimum version requirement for *_compat25 functions is libfuse-2.6.0. - - #[cfg(not(all(target_os = "macos", feature = "macfuse-5")))] - pub fn fuse_mount_compat25(mountpoint: *const c_char, args: *const fuse_args) -> c_int; - #[cfg(not(any( - target_os = "macos", - target_os = "freebsd", - target_os = "dragonfly", - target_os = "openbsd", - target_os = "netbsd" - )))] - pub fn fuse_unmount_compat22(mountpoint: *const c_char); -} diff --git a/vendor/fuser/src/mnt/fuse3.rs b/vendor/fuser/src/mnt/fuse3.rs deleted file mode 100644 index b7942b54a..000000000 --- a/vendor/fuser/src/mnt/fuse3.rs +++ /dev/null @@ -1,62 +0,0 @@ -use super::fuse3_sys::{ - fuse_session_destroy, fuse_session_fd, fuse_session_mount, fuse_session_new, - fuse_session_unmount, -}; -use super::{with_fuse_args, MountOption}; -use std::{ - ffi::{c_void, CString}, - fs::File, - io, - os::unix::{ffi::OsStrExt, io::FromRawFd}, - path::Path, - ptr, - sync::Arc, -}; - -/// Ensures that an os error is never 0/Success -fn ensure_last_os_error() -> io::Error { - let err = io::Error::last_os_error(); - match err.raw_os_error() { - Some(0) => io::Error::new(io::ErrorKind::Other, "Unspecified Error"), - _ => err, - } -} - -#[derive(Debug)] -pub struct Mount { - fuse_session: *mut c_void, -} -impl Mount { - pub fn new(mnt: &Path, options: &[MountOption]) -> io::Result<(Arc, Mount)> { - let mnt = CString::new(mnt.as_os_str().as_bytes()).unwrap(); - with_fuse_args(options, |args| { - let fuse_session = unsafe { fuse_session_new(args, ptr::null(), 0, ptr::null_mut()) }; - if fuse_session.is_null() { - return Err(io::Error::last_os_error()); - } - let mount = Mount { fuse_session }; - let result = unsafe { fuse_session_mount(mount.fuse_session, mnt.as_ptr()) }; - if result != 0 { - return Err(ensure_last_os_error()); - } - let fd = unsafe { fuse_session_fd(mount.fuse_session) }; - if fd < 0 { - return Err(io::Error::last_os_error()); - } - // We dup the fd here as the existing fd is owned by the fuse_session, and we - // don't want it being closed out from under us: - let fd = nix::fcntl::fcntl(fd, nix::fcntl::FcntlArg::F_DUPFD_CLOEXEC(0))?; - let file = unsafe { File::from_raw_fd(fd) }; - Ok((Arc::new(file), mount)) - }) - } -} -impl Drop for Mount { - fn drop(&mut self) { - unsafe { - fuse_session_unmount(self.fuse_session); - fuse_session_destroy(self.fuse_session); - } - } -} -unsafe impl Send for Mount {} diff --git a/vendor/fuser/src/mnt/fuse3_sys.rs b/vendor/fuser/src/mnt/fuse3_sys.rs deleted file mode 100644 index c1af61314..000000000 --- a/vendor/fuser/src/mnt/fuse3_sys.rs +++ /dev/null @@ -1,31 +0,0 @@ -//! Native FFI bindings to libfuse3. -//! -//! This is a small set of bindings that are required to mount/unmount FUSE filesystems and -//! open/close a fd to the FUSE kernel driver. - -#![warn(missing_debug_implementations)] -#![allow(missing_docs)] - -use super::fuse2_sys::fuse_args; -use libc::c_void; -use libc::{c_char, c_int}; - -extern "C" { - // Really this returns *fuse_session, but we don't need to access its fields - pub fn fuse_session_new( - args: *const fuse_args, - op: *const c_void, // This argument is really a *const fuse_lowlevel_ops, but we don't use them - op_size: libc::size_t, - userdata: *mut c_void, - ) -> *mut c_void; - pub fn fuse_session_mount( - se: *mut c_void, // This argument is really a *fuse_session - mountpoint: *const c_char, - ) -> c_int; - // This function's argument is really a *fuse_session - pub fn fuse_session_fd(se: *mut c_void) -> c_int; - // This function's argument is really a *fuse_session - pub fn fuse_session_unmount(se: *mut c_void); - // This function's argument is really a *fuse_session - pub fn fuse_session_destroy(se: *mut c_void); -} diff --git a/vendor/fuser/src/mnt/fuse_pure.rs b/vendor/fuser/src/mnt/fuse_pure.rs deleted file mode 100644 index 73c497740..000000000 --- a/vendor/fuser/src/mnt/fuse_pure.rs +++ /dev/null @@ -1,520 +0,0 @@ -//! Native FFI bindings to libfuse. -//! -//! This is a small set of bindings that are required to mount/unmount FUSE filesystems and -//! open/close a fd to the FUSE kernel driver. - -#![warn(missing_debug_implementations)] -#![allow(missing_docs)] - -use super::is_mounted; -use super::mount_options::{option_to_string, MountOption}; -use libc::c_int; -use log::{debug, error}; -use std::ffi::{CStr, CString, OsStr}; -use std::fs::{File, OpenOptions}; -use std::io; -use std::io::{Error, ErrorKind, Read}; -use std::os::unix::ffi::OsStrExt; -use std::os::unix::fs::PermissionsExt; -use std::os::unix::io::{AsRawFd, FromRawFd}; -use std::os::unix::net::UnixStream; -use std::path::Path; -use std::process::{Command, Stdio}; -use std::sync::Arc; -use std::{mem, ptr}; - -const FUSERMOUNT_BIN: &str = "fusermount"; -const FUSERMOUNT3_BIN: &str = "fusermount3"; -const FUSERMOUNT_COMM_ENV: &str = "_FUSE_COMMFD"; - -#[derive(Debug)] -pub struct Mount { - mountpoint: CString, - auto_unmount_socket: Option, - fuse_device: Arc, -} -impl Mount { - pub fn new(mountpoint: &Path, options: &[MountOption]) -> io::Result<(Arc, Mount)> { - let mountpoint = mountpoint.canonicalize()?; - let (file, sock) = fuse_mount_pure(mountpoint.as_os_str(), options)?; - let file = Arc::new(file); - Ok(( - file.clone(), - Mount { - mountpoint: CString::new(mountpoint.as_os_str().as_bytes())?, - auto_unmount_socket: sock, - fuse_device: file, - }, - )) - } -} - -impl Drop for Mount { - fn drop(&mut self) { - use std::io::ErrorKind::PermissionDenied; - if !is_mounted(&self.fuse_device) { - // If the filesystem has already been unmounted, avoid unmounting it again. - // Unmounting it a second time could cause a race with a newly mounted filesystem - // living at the same mountpoint - return; - } - if let Some(sock) = mem::take(&mut self.auto_unmount_socket) { - drop(sock); - // fusermount in auto-unmount mode, no more work to do. - return; - } - if let Err(err) = super::libc_umount(&self.mountpoint) { - if err.kind() == PermissionDenied { - // Linux always returns EPERM for non-root users. We have to let the - // library go through the setuid-root "fusermount -u" to unmount. - fuse_unmount_pure(&self.mountpoint) - } else { - error!("Unmount failed: {}", err) - } - } - } -} - -fn fuse_mount_pure( - mountpoint: &OsStr, - options: &[MountOption], -) -> Result<(File, Option), io::Error> { - if options.contains(&MountOption::AutoUnmount) { - // Auto unmount is only supported via fusermount - return fuse_mount_fusermount(mountpoint, options); - } - - let res = fuse_mount_sys(mountpoint, options)?; - if let Some(file) = res { - Ok((file, None)) - } else { - // Retry - fuse_mount_fusermount(mountpoint, options) - } -} - -fn fuse_unmount_pure(mountpoint: &CStr) { - #[cfg(target_os = "linux")] - unsafe { - let result = libc::umount2(mountpoint.as_ptr(), libc::MNT_DETACH); - if result == 0 { - return; - } - } - #[cfg(target_os = "macos")] - unsafe { - let result = libc::unmount(mountpoint.as_ptr(), libc::MNT_FORCE); - if result == 0 { - return; - } - } - - let mut builder = Command::new(detect_fusermount_bin()); - builder.stdout(Stdio::piped()).stderr(Stdio::piped()); - builder - .arg("-u") - .arg("-q") - .arg("-z") - .arg("--") - .arg(OsStr::new(&mountpoint.to_string_lossy().into_owned())); - - if let Ok(output) = builder.output() { - debug!("fusermount: {}", String::from_utf8_lossy(&output.stdout)); - debug!("fusermount: {}", String::from_utf8_lossy(&output.stderr)); - } -} - -fn detect_fusermount_bin() -> String { - for name in [ - FUSERMOUNT3_BIN.to_string(), - FUSERMOUNT_BIN.to_string(), - format!("/bin/{FUSERMOUNT3_BIN}"), - format!("/bin/{FUSERMOUNT_BIN}"), - ] - .iter() - { - if Command::new(name).arg("-h").output().is_ok() { - return name.to_string(); - } - } - // Default to fusermount3 - FUSERMOUNT3_BIN.to_string() -} - -fn receive_fusermount_message(socket: &UnixStream) -> Result { - let mut io_vec_buf = [0u8]; - let mut io_vec = libc::iovec { - iov_base: io_vec_buf.as_mut_ptr() as *mut libc::c_void, - iov_len: io_vec_buf.len(), - }; - let cmsg_buffer_len = unsafe { libc::CMSG_SPACE(mem::size_of::() as libc::c_uint) }; - let mut cmsg_buffer = vec![0u8; cmsg_buffer_len as usize]; - let mut message: libc::msghdr; - #[cfg(all(target_os = "linux", not(target_env = "musl")))] - { - message = libc::msghdr { - msg_name: ptr::null_mut(), - msg_namelen: 0, - msg_iov: &mut io_vec, - msg_iovlen: 1, - msg_control: cmsg_buffer.as_mut_ptr() as *mut libc::c_void, - msg_controllen: cmsg_buffer.len(), - msg_flags: 0, - }; - } - #[cfg(all(target_os = "linux", target_env = "musl"))] - { - message = unsafe { std::mem::MaybeUninit::zeroed().assume_init() }; - message.msg_name = ptr::null_mut(); - message.msg_namelen = 0; - message.msg_iov = &mut io_vec; - message.msg_iovlen = 1; - message.msg_control = (&mut cmsg_buffer).as_mut_ptr() as *mut libc::c_void; - message.msg_controllen = cmsg_buffer.len() as u32; - message.msg_flags = 0; - } - #[cfg(any( - target_os = "macos", - target_os = "freebsd", - target_os = "dragonfly", - target_os = "openbsd", - target_os = "netbsd" - ))] - { - message = libc::msghdr { - msg_name: ptr::null_mut(), - msg_namelen: 0, - msg_iov: &mut io_vec, - msg_iovlen: 1, - msg_control: (&mut cmsg_buffer).as_mut_ptr() as *mut libc::c_void, - msg_controllen: cmsg_buffer.len() as u32, - msg_flags: 0, - }; - } - - let mut result; - loop { - unsafe { - result = libc::recvmsg(socket.as_raw_fd(), &mut message, 0); - } - if result != -1 { - break; - } - let err = Error::last_os_error(); - if err.kind() != ErrorKind::Interrupted { - return Err(err); - } - } - if result == 0 { - return Err(Error::new( - ErrorKind::UnexpectedEof, - "Unexpected EOF reading from fusermount", - )); - } - - unsafe { - let control_msg = libc::CMSG_FIRSTHDR(&message); - if (*control_msg).cmsg_type != libc::SCM_RIGHTS { - return Err(Error::new( - ErrorKind::InvalidData, - format!( - "Unknown control message from fusermount: {}", - (*control_msg).cmsg_type - ), - )); - } - let fd_data = libc::CMSG_DATA(control_msg); - - let fd = *(fd_data as *const c_int); - if fd < 0 { - Err(ErrorKind::InvalidData.into()) - } else { - Ok(File::from_raw_fd(fd)) - } - } -} - -fn fuse_mount_fusermount( - mountpoint: &OsStr, - options: &[MountOption], -) -> Result<(File, Option), Error> { - let (child_socket, receive_socket) = UnixStream::pair()?; - - unsafe { - libc::fcntl(child_socket.as_raw_fd(), libc::F_SETFD, 0); - } - - let mut builder = Command::new(detect_fusermount_bin()); - builder.stdout(Stdio::piped()).stderr(Stdio::piped()); - if !options.is_empty() { - builder.arg("-o"); - let options_strs: Vec = options.iter().map(option_to_string).collect(); - builder.arg(options_strs.join(",")); - } - builder - .arg("--") - .arg(mountpoint) - .env(FUSERMOUNT_COMM_ENV, child_socket.as_raw_fd().to_string()); - - let fusermount_child = builder.spawn()?; - - drop(child_socket); // close socket in parent - - let file = match receive_fusermount_message(&receive_socket) { - Ok(f) => f, - Err(_) => { - // Drop receive socket, since fusermount has exited with an error - drop(receive_socket); - let output = fusermount_child.wait_with_output().unwrap(); - let stderr_string = String::from_utf8_lossy(&output.stderr).to_string(); - return if stderr_string.contains("only allowed if 'user_allow_other' is set") { - Err(io::Error::new(ErrorKind::PermissionDenied, stderr_string)) - } else { - Err(io::Error::new(ErrorKind::Other, stderr_string)) - }; - } - }; - let mut receive_socket = Some(receive_socket); - - if !options.contains(&MountOption::AutoUnmount) { - // Only close the socket, if auto unmount is not set. - // fusermount will keep running until the socket is closed, if auto unmount is set - drop(mem::take(&mut receive_socket)); - let output = fusermount_child.wait_with_output()?; - debug!("fusermount: {}", String::from_utf8_lossy(&output.stdout)); - debug!("fusermount: {}", String::from_utf8_lossy(&output.stderr)); - } else { - if let Some(mut stdout) = fusermount_child.stdout { - let stdout_fd = stdout.as_raw_fd(); - unsafe { - let mut flags = libc::fcntl(stdout_fd, libc::F_GETFL, 0); - flags |= libc::O_NONBLOCK; - libc::fcntl(stdout_fd, libc::F_SETFL, flags); - } - let mut buf = vec![0; 64 * 1024]; - if let Ok(len) = stdout.read(&mut buf) { - debug!("fusermount: {}", String::from_utf8_lossy(&buf[..len])); - } - } - if let Some(mut stderr) = fusermount_child.stderr { - let stderr_fd = stderr.as_raw_fd(); - unsafe { - let mut flags = libc::fcntl(stderr_fd, libc::F_GETFL, 0); - flags |= libc::O_NONBLOCK; - libc::fcntl(stderr_fd, libc::F_SETFL, flags); - } - let mut buf = vec![0; 64 * 1024]; - if let Ok(len) = stderr.read(&mut buf) { - debug!("fusermount: {}", String::from_utf8_lossy(&buf[..len])); - } - } - } - - unsafe { - libc::fcntl(file.as_raw_fd(), libc::F_SETFD, libc::FD_CLOEXEC); - } - - Ok((file, receive_socket)) -} - -// If returned option is none. Then fusermount binary should be tried -fn fuse_mount_sys(mountpoint: &OsStr, options: &[MountOption]) -> Result, Error> { - let fuse_device_name = "/dev/fuse"; - - let mountpoint_mode = File::open(mountpoint)?.metadata()?.permissions().mode(); - - // Auto unmount requests must be sent to fusermount binary - assert!(!options.contains(&MountOption::AutoUnmount)); - - let file = match OpenOptions::new() - .read(true) - .write(true) - .open(fuse_device_name) - { - Ok(file) => file, - Err(error) => { - if error.kind() == ErrorKind::NotFound { - error!("{} not found. Try 'modprobe fuse'", fuse_device_name); - } - return Err(error); - } - }; - assert!( - file.as_raw_fd() > 2, - "Conflict with stdin/stdout/stderr. fd={}", - file.as_raw_fd() - ); - - let mut mount_options = format!( - "fd={},rootmode={:o},user_id={},group_id={}", - file.as_raw_fd(), - mountpoint_mode, - nix::unistd::getuid(), - nix::unistd::getgid() - ); - - for option in options - .iter() - .filter(|x| option_group(x) == MountOptionGroup::KernelOption) - { - mount_options.push(','); - mount_options.push_str(&option_to_string(option)); - } - - let mut flags = 0; - if !options.contains(&MountOption::Dev) { - // Default to nodev - #[cfg(target_os = "linux")] - { - flags |= libc::MS_NODEV; - } - #[cfg(target_os = "macos")] - { - flags |= libc::MNT_NODEV; - } - } - if !options.contains(&MountOption::Suid) { - // Default to nosuid - #[cfg(target_os = "linux")] - { - flags |= libc::MS_NOSUID; - } - #[cfg(target_os = "macos")] - { - flags |= libc::MNT_NOSUID; - } - } - for flag in options - .iter() - .filter(|x| option_group(x) == MountOptionGroup::KernelFlag) - { - flags |= option_to_flag(flag); - } - - // Default name is "/dev/fuse", then use the subtype, and lastly prefer the name - let mut source = fuse_device_name; - if let Some(MountOption::Subtype(subtype)) = options - .iter() - .find(|x| matches!(**x, MountOption::Subtype(_))) - { - source = subtype; - } - if let Some(MountOption::FSName(name)) = options - .iter() - .find(|x| matches!(**x, MountOption::FSName(_))) - { - source = name; - } - - let c_source = CString::new(source).unwrap(); - let c_mountpoint = CString::new(mountpoint.as_bytes()).unwrap(); - - let result = unsafe { - #[cfg(target_os = "linux")] - { - let c_options = CString::new(mount_options).unwrap(); - let c_type = CString::new("fuse").unwrap(); - libc::mount( - c_source.as_ptr(), - c_mountpoint.as_ptr(), - c_type.as_ptr(), - flags, - c_options.as_ptr() as *const libc::c_void, - ) - } - #[cfg(target_os = "macos")] - { - let mut c_options = CString::new(mount_options).unwrap(); - libc::mount( - c_source.as_ptr(), - c_mountpoint.as_ptr(), - flags, - c_options.as_ptr() as *mut libc::c_void, - ) - } - }; - if result == -1 { - let err = Error::last_os_error(); - if err.kind() == ErrorKind::PermissionDenied { - return Ok(None); // Retry with fusermount - } else { - return Err(Error::new( - err.kind(), - format!("Error calling mount() at {mountpoint:?}: {err}"), - )); - } - } - - Ok(Some(file)) -} - -#[derive(PartialEq)] -pub enum MountOptionGroup { - KernelOption, - KernelFlag, - Fusermount, -} - -pub fn option_group(option: &MountOption) -> MountOptionGroup { - match option { - MountOption::FSName(_) => MountOptionGroup::Fusermount, - MountOption::Subtype(_) => MountOptionGroup::Fusermount, - MountOption::CUSTOM(_) => MountOptionGroup::KernelOption, - MountOption::AutoUnmount => MountOptionGroup::Fusermount, - MountOption::AllowOther => MountOptionGroup::KernelOption, - MountOption::Dev => MountOptionGroup::KernelFlag, - MountOption::NoDev => MountOptionGroup::KernelFlag, - MountOption::Suid => MountOptionGroup::KernelFlag, - MountOption::NoSuid => MountOptionGroup::KernelFlag, - MountOption::RO => MountOptionGroup::KernelFlag, - MountOption::RW => MountOptionGroup::KernelFlag, - MountOption::Exec => MountOptionGroup::KernelFlag, - MountOption::NoExec => MountOptionGroup::KernelFlag, - MountOption::Atime => MountOptionGroup::KernelFlag, - MountOption::NoAtime => MountOptionGroup::KernelFlag, - MountOption::DirSync => MountOptionGroup::KernelFlag, - MountOption::Sync => MountOptionGroup::KernelFlag, - MountOption::Async => MountOptionGroup::KernelFlag, - MountOption::AllowRoot => MountOptionGroup::KernelOption, - MountOption::DefaultPermissions => MountOptionGroup::KernelOption, - } -} - -#[cfg(target_os = "linux")] -pub fn option_to_flag(option: &MountOption) -> libc::c_ulong { - match option { - MountOption::Dev => 0, // There is no option for dev. It's the absence of NoDev - MountOption::NoDev => libc::MS_NODEV, - MountOption::Suid => 0, - MountOption::NoSuid => libc::MS_NOSUID, - MountOption::RW => 0, - MountOption::RO => libc::MS_RDONLY, - MountOption::Exec => 0, - MountOption::NoExec => libc::MS_NOEXEC, - MountOption::Atime => 0, - MountOption::NoAtime => libc::MS_NOATIME, - MountOption::Async => 0, - MountOption::Sync => libc::MS_SYNCHRONOUS, - MountOption::DirSync => libc::MS_DIRSYNC, - _ => unreachable!(), - } -} - -#[cfg(target_os = "macos")] -pub fn option_to_flag(option: &MountOption) -> libc::c_int { - match option { - MountOption::Dev => 0, // There is no option for dev. It's the absence of NoDev - MountOption::NoDev => libc::MNT_NODEV, - MountOption::Suid => 0, - MountOption::NoSuid => libc::MNT_NOSUID, - MountOption::RW => 0, - MountOption::RO => libc::MNT_RDONLY, - MountOption::Exec => 0, - MountOption::NoExec => libc::MNT_NOEXEC, - MountOption::Atime => 0, - MountOption::NoAtime => libc::MNT_NOATIME, - MountOption::Async => 0, - MountOption::Sync => libc::MNT_SYNCHRONOUS, - _ => unreachable!(), - } -} diff --git a/vendor/fuser/src/mnt/mod.rs b/vendor/fuser/src/mnt/mod.rs deleted file mode 100644 index ee8001cd0..000000000 --- a/vendor/fuser/src/mnt/mod.rs +++ /dev/null @@ -1,187 +0,0 @@ -//! FUSE kernel driver communication -//! -//! Raw communication channel to the FUSE kernel driver. - -#[cfg(fuser_mount_impl = "libfuse2")] -mod fuse2; -#[cfg(any(feature = "libfuse", test))] -mod fuse2_sys; -#[cfg(fuser_mount_impl = "libfuse3")] -mod fuse3; -#[cfg(fuser_mount_impl = "libfuse3")] -mod fuse3_sys; - -#[cfg(fuser_mount_impl = "pure-rust")] -mod fuse_pure; -pub mod mount_options; - -#[cfg(any(test, feature = "libfuse"))] -use fuse2_sys::fuse_args; -#[cfg(any(test, not(feature = "libfuse")))] -use std::fs::File; -#[cfg(any(test, fuser_mount_impl = "pure-rust", fuser_mount_impl = "libfuse2"))] -use std::io; - -#[cfg(any(feature = "libfuse", test))] -use mount_options::MountOption; - -/// Helper function to provide options as a fuse_args struct -/// (which contains an argc count and an argv pointer) -#[cfg(any(feature = "libfuse", test))] -fn with_fuse_args T>(options: &[MountOption], f: F) -> T { - use mount_options::option_to_string; - use std::ffi::CString; - - let mut args = vec![CString::new("rust-fuse").unwrap()]; - for x in options { - args.extend_from_slice(&[ - CString::new("-o").unwrap(), - CString::new(option_to_string(x)).unwrap(), - ]); - } - let argptrs: Vec<_> = args.iter().map(|s| s.as_ptr()).collect(); - f(&fuse_args { - argc: argptrs.len() as i32, - argv: argptrs.as_ptr(), - allocated: 0, - }) -} - -#[cfg(fuser_mount_impl = "libfuse2")] -pub use fuse2::Mount; -#[cfg(fuser_mount_impl = "libfuse3")] -pub use fuse3::Mount; -#[cfg(fuser_mount_impl = "pure-rust")] -pub use fuse_pure::Mount; -#[cfg(not(fuser_mount_impl = "libfuse3"))] -use std::ffi::CStr; - -#[cfg(not(fuser_mount_impl = "libfuse3"))] -#[inline] -fn libc_umount(mnt: &CStr) -> io::Result<()> { - #[cfg(any( - target_os = "macos", - target_os = "freebsd", - target_os = "dragonfly", - target_os = "openbsd", - target_os = "netbsd" - ))] - let r = unsafe { libc::unmount(mnt.as_ptr(), 0) }; - - #[cfg(not(any( - target_os = "macos", - target_os = "freebsd", - target_os = "dragonfly", - target_os = "openbsd", - target_os = "netbsd" - )))] - let r = unsafe { libc::umount(mnt.as_ptr()) }; - if r < 0 { - Err(io::Error::last_os_error()) - } else { - Ok(()) - } -} - -/// Warning: This will return true if the filesystem has been detached (lazy unmounted), but not -/// yet destroyed by the kernel. -#[cfg(any(test, fuser_mount_impl = "pure-rust"))] -fn is_mounted(fuse_device: &File) -> bool { - use libc::{poll, pollfd}; - use std::os::unix::prelude::AsRawFd; - - let mut poll_result = pollfd { - fd: fuse_device.as_raw_fd(), - events: 0, - revents: 0, - }; - loop { - let res = unsafe { poll(&mut poll_result, 1, 0) }; - break match res { - 0 => true, - 1 => (poll_result.revents & libc::POLLERR) != 0, - -1 => { - let err = io::Error::last_os_error(); - if err.kind() == io::ErrorKind::Interrupted { - continue; - } else { - // This should never happen. The fd is guaranteed good as `File` owns it. - // According to man poll ENOMEM is the only error code unhandled, so we panic - // consistent with rust's usual ENOMEM behaviour. - panic!("Poll failed with error {}", err) - } - } - _ => unreachable!(), - }; - } -} - -#[cfg(test)] -mod test { - use super::*; - use std::{ffi::CStr, mem::ManuallyDrop}; - - #[test] - fn fuse_args() { - with_fuse_args( - &[ - MountOption::CUSTOM("foo".into()), - MountOption::CUSTOM("bar".into()), - ], - |args| { - let v: Vec<_> = (0..args.argc) - .map(|n| unsafe { - CStr::from_ptr(*args.argv.offset(n as isize)) - .to_str() - .unwrap() - }) - .collect(); - assert_eq!(*v, ["rust-fuse", "-o", "foo", "-o", "bar"]); - }, - ); - } - fn cmd_mount() -> String { - std::str::from_utf8( - std::process::Command::new("sh") - .arg("-c") - .arg("mount | grep fuse") - .output() - .unwrap() - .stdout - .as_ref(), - ) - .unwrap() - .to_owned() - } - - #[test] - fn mount_unmount() { - // We use ManuallyDrop here to leak the directory on test failure. We don't - // want to try and clean up the directory if it's a mountpoint otherwise we'll - // deadlock. - let tmp = ManuallyDrop::new(tempfile::tempdir().unwrap()); - let (file, mount) = Mount::new(tmp.path(), &[]).unwrap(); - let mnt = cmd_mount(); - eprintln!("Our mountpoint: {:?}\nfuse mounts:\n{}", tmp.path(), mnt,); - assert!(mnt.contains(&*tmp.path().to_string_lossy())); - assert!(is_mounted(&file)); - drop(mount); - let mnt = cmd_mount(); - eprintln!("Our mountpoint: {:?}\nfuse mounts:\n{}", tmp.path(), mnt,); - - let detached = !mnt.contains(&*tmp.path().to_string_lossy()); - // Linux supports MNT_DETACH, so we expect unmount to succeed even if the FS - // is busy. Other systems don't so the unmount may fail and we will still - // have the mount listed. The mount will get cleaned up later. - #[cfg(target_os = "linux")] - assert!(detached); - - if detached { - // We've detached successfully, it's safe to clean up: - std::mem::ManuallyDrop::<_>::into_inner(tmp); - } - - // Filesystem may have been lazy unmounted, so we can't assert this: - // assert!(!is_mounted(&file)); - } -} diff --git a/vendor/fuser/src/mnt/mount_options.rs b/vendor/fuser/src/mnt/mount_options.rs deleted file mode 100644 index 1778d28e4..000000000 --- a/vendor/fuser/src/mnt/mount_options.rs +++ /dev/null @@ -1,241 +0,0 @@ -use std::io; -use std::io::ErrorKind; -use std::{collections::HashSet, ffi::OsStr}; - -/// Mount options accepted by the FUSE filesystem type -/// See 'man mount.fuse' for details -// TODO: add all options that 'man mount.fuse' documents and libfuse supports -#[derive(Debug, Eq, PartialEq, Hash, Clone)] -pub enum MountOption { - /// Set the name of the source in mtab - FSName(String), - /// Set the filesystem subtype in mtab - Subtype(String), - /// Allows passing an option which is not otherwise supported in these enums - #[allow(clippy::upper_case_acronyms)] - CUSTOM(String), - - /* Parameterless options */ - /// Allow all users to access files on this filesystem. By default access is restricted to the - /// user who mounted it - AllowOther, - /// Allow the root user to access this filesystem, in addition to the user who mounted it - AllowRoot, - /// Automatically unmount when the mounting process exits - /// - /// `AutoUnmount` requires `AllowOther` or `AllowRoot`. If `AutoUnmount` is set and neither `Allow...` is set, the FUSE configuration must permit `allow_other`, otherwise mounting will fail. - AutoUnmount, - /// Enable permission checking in the kernel - DefaultPermissions, - - /* Flags */ - /// Enable special character and block devices - Dev, - /// Disable special character and block devices - NoDev, - /// Honor set-user-id and set-groupd-id bits on files - Suid, - /// Don't honor set-user-id and set-groupd-id bits on files - NoSuid, - /// Read-only filesystem - RO, - /// Read-write filesystem - RW, - /// Allow execution of binaries - Exec, - /// Don't allow execution of binaries - NoExec, - /// Support inode access time - Atime, - /// Don't update inode access time - NoAtime, - /// All modifications to directories will be done synchronously - DirSync, - /// All I/O will be done synchronously - Sync, - /// All I/O will be done asynchronously - Async, - /* libfuse library options, such as "direct_io", are not included since they are specific - to libfuse, and not part of the kernel ABI */ -} - -impl MountOption { - pub(crate) fn from_str(s: &str) -> MountOption { - match s { - "auto_unmount" => MountOption::AutoUnmount, - "allow_other" => MountOption::AllowOther, - "allow_root" => MountOption::AllowRoot, - "default_permissions" => MountOption::DefaultPermissions, - "dev" => MountOption::Dev, - "nodev" => MountOption::NoDev, - "suid" => MountOption::Suid, - "nosuid" => MountOption::NoSuid, - "ro" => MountOption::RO, - "rw" => MountOption::RW, - "exec" => MountOption::Exec, - "noexec" => MountOption::NoExec, - "atime" => MountOption::Atime, - "noatime" => MountOption::NoAtime, - "dirsync" => MountOption::DirSync, - "sync" => MountOption::Sync, - "async" => MountOption::Async, - x if x.starts_with("fsname=") => MountOption::FSName(x[7..].into()), - x if x.starts_with("subtype=") => MountOption::Subtype(x[8..].into()), - x => MountOption::CUSTOM(x.into()), - } - } -} - -pub fn check_option_conflicts(options: &[MountOption]) -> Result<(), io::Error> { - let mut options_set = HashSet::new(); - options_set.extend(options.iter().cloned()); - let conflicting: HashSet = options.iter().flat_map(conflicts_with).collect(); - let intersection: Vec = conflicting.intersection(&options_set).cloned().collect(); - if !intersection.is_empty() { - Err(io::Error::new( - ErrorKind::InvalidInput, - format!("Conflicting mount options found: {intersection:?}"), - )) - } else { - Ok(()) - } -} - -fn conflicts_with(option: &MountOption) -> Vec { - match option { - MountOption::FSName(_) => vec![], - MountOption::Subtype(_) => vec![], - MountOption::CUSTOM(_) => vec![], - MountOption::AllowOther => vec![MountOption::AllowRoot], - MountOption::AllowRoot => vec![MountOption::AllowOther], - MountOption::AutoUnmount => vec![], - MountOption::DefaultPermissions => vec![], - MountOption::Dev => vec![MountOption::NoDev], - MountOption::NoDev => vec![MountOption::Dev], - MountOption::Suid => vec![MountOption::NoSuid], - MountOption::NoSuid => vec![MountOption::Suid], - MountOption::RO => vec![MountOption::RW], - MountOption::RW => vec![MountOption::RO], - MountOption::Exec => vec![MountOption::NoExec], - MountOption::NoExec => vec![MountOption::Exec], - MountOption::Atime => vec![MountOption::NoAtime], - MountOption::NoAtime => vec![MountOption::Atime], - MountOption::DirSync => vec![], - MountOption::Sync => vec![MountOption::Async], - MountOption::Async => vec![MountOption::Sync], - } -} - -// Format option to be passed to libfuse or kernel -pub fn option_to_string(option: &MountOption) -> String { - match option { - MountOption::FSName(name) => format!("fsname={name}"), - MountOption::Subtype(subtype) => format!("subtype={subtype}"), - MountOption::CUSTOM(value) => value.to_string(), - MountOption::AutoUnmount => "auto_unmount".to_string(), - MountOption::AllowOther => "allow_other".to_string(), - // AllowRoot is implemented by allowing everyone access and then restricting to - // root + owner within fuser - MountOption::AllowRoot => "allow_other".to_string(), - MountOption::DefaultPermissions => "default_permissions".to_string(), - MountOption::Dev => "dev".to_string(), - MountOption::NoDev => "nodev".to_string(), - MountOption::Suid => "suid".to_string(), - MountOption::NoSuid => "nosuid".to_string(), - MountOption::RO => "ro".to_string(), - MountOption::RW => "rw".to_string(), - MountOption::Exec => "exec".to_string(), - MountOption::NoExec => "noexec".to_string(), - MountOption::Atime => "atime".to_string(), - MountOption::NoAtime => "noatime".to_string(), - MountOption::DirSync => "dirsync".to_string(), - MountOption::Sync => "sync".to_string(), - MountOption::Async => "async".to_string(), - } -} - -/// Parses mount command args. -/// -/// Input: ["-o", "suid", "-o", "ro,nodev,noexec", "-osync"] -/// Output Ok([Suid, RO, NoDev, NoExec, Sync]) -pub(crate) fn parse_options_from_args(args: &[&OsStr]) -> io::Result> { - let err = |x| io::Error::new(ErrorKind::InvalidInput, x); - let args: Option> = args.iter().map(|x| x.to_str()).collect(); - let args = args.ok_or_else(|| err("Error parsing args: Invalid UTF-8".to_owned()))?; - let mut it = args.iter(); - let mut out = vec![]; - loop { - let opt = match it.next() { - None => break, - Some(&"-o") => *it.next().ok_or_else(|| { - err("Error parsing args: Expected option, reached end of args".to_owned()) - })?, - Some(x) if x.starts_with("-o") => &x[2..], - Some(x) => return Err(err(format!("Error parsing args: expected -o, got {x}"))), - }; - for x in opt.split(',') { - out.push(MountOption::from_str(x)) - } - } - Ok(out) -} - -#[cfg(test)] -mod test { - use std::os::unix::prelude::OsStrExt; - - use super::*; - - #[test] - fn option_checking() { - assert!(check_option_conflicts(&[MountOption::Suid, MountOption::NoSuid]).is_err()); - assert!(check_option_conflicts(&[MountOption::Suid, MountOption::NoExec]).is_ok()); - } - #[test] - fn option_round_trip() { - use super::MountOption::*; - for x in [ - FSName("Blah".to_owned()), - Subtype("Bloo".to_owned()), - CUSTOM("bongos".to_owned()), - AllowOther, - AutoUnmount, - DefaultPermissions, - Dev, - NoDev, - Suid, - NoSuid, - RO, - RW, - Exec, - NoExec, - Atime, - NoAtime, - DirSync, - Sync, - Async, - ] - .iter() - { - assert_eq!(*x, MountOption::from_str(option_to_string(x).as_ref())) - } - } - - #[test] - fn test_parse_options() { - use super::MountOption::*; - - assert_eq!(parse_options_from_args(&[]).unwrap(), &[]); - - let o: Vec<_> = "-o suid -o ro,nodev,noexec -osync" - .split(' ') - .map(OsStr::new) - .collect(); - let out = parse_options_from_args(o.as_ref()).unwrap(); - assert_eq!(out, [Suid, RO, NoDev, NoExec, Sync]); - - assert!(parse_options_from_args(&[OsStr::new("-o")]).is_err()); - assert!(parse_options_from_args(&[OsStr::new("not o")]).is_err()); - assert!(parse_options_from_args(&[OsStr::from_bytes(b"-o\xc3\x28")]).is_err()); - } -} diff --git a/vendor/fuser/src/notify.rs b/vendor/fuser/src/notify.rs deleted file mode 100644 index 4442252a7..000000000 --- a/vendor/fuser/src/notify.rs +++ /dev/null @@ -1,121 +0,0 @@ -use std::io; - -#[allow(unused)] -use std::{convert::TryInto, ffi::OsStr}; - -use crate::{ - channel::ChannelSender, - ll::{fuse_abi::fuse_notify_code as notify_code, notify::Notification}, - - // What we're sending here aren't really replies, but they - // move in the same direction (userspace->kernel), so we can - // reuse ReplySender for it. - reply::ReplySender, -}; - -/// A handle to a pending poll() request. Can be saved and used to notify the -/// kernel when a poll is ready. -#[derive(Clone)] -pub struct PollHandle { - handle: u64, - notifier: Notifier, -} - -impl PollHandle { - pub(crate) fn new(cs: ChannelSender, kh: u64) -> Self { - Self { - handle: kh, - notifier: Notifier::new(cs), - } - } - - /// Notify the kernel that the associated file handle is ready to be polled. - pub fn notify(self) -> io::Result<()> { - self.notifier.poll(self.handle) - } -} - -impl From for u64 { - fn from(value: PollHandle) -> Self { - value.handle - } -} - -impl std::fmt::Debug for PollHandle { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("PollHandle").field(&self.handle).finish() - } -} - -/// A handle by which the application can send notifications to the server -#[derive(Debug, Clone)] -pub struct Notifier(ChannelSender); - -impl Notifier { - pub(crate) fn new(cs: ChannelSender) -> Self { - Self(cs) - } - - /// Notify poll clients of I/O readiness - #[cfg(feature = "abi-7-11")] - pub fn poll(&self, kh: u64) -> io::Result<()> { - let notif = Notification::new_poll(kh); - self.send(notify_code::FUSE_POLL, ¬if) - } - - /// Invalidate the kernel cache for a given directory entry - #[cfg(feature = "abi-7-12")] - pub fn inval_entry(&self, parent: u64, name: &OsStr) -> io::Result<()> { - let notif = Notification::new_inval_entry(parent, name).map_err(Self::too_big_err)?; - self.send_inval(notify_code::FUSE_NOTIFY_INVAL_ENTRY, ¬if) - } - - /// Invalidate the kernel cache for a given inode (metadata and - /// data in the given range) - #[cfg(feature = "abi-7-12")] - pub fn inval_inode(&self, ino: u64, offset: i64, len: i64) -> io::Result<()> { - let notif = Notification::new_inval_inode(ino, offset, len); - self.send_inval(notify_code::FUSE_NOTIFY_INVAL_INODE, ¬if) - } - - /// Update the kernel's cached copy of a given inode's data - #[cfg(feature = "abi-7-15")] - pub fn store(&self, ino: u64, offset: u64, data: &[u8]) -> io::Result<()> { - let notif = Notification::new_store(ino, offset, data).map_err(Self::too_big_err)?; - // Not strictly an invalidate, but the inode we're operating - // on may have been evicted anyway, so treat is as such - self.send_inval(notify_code::FUSE_NOTIFY_STORE, ¬if) - } - - /// Invalidate the kernel cache for a given directory entry and inform - /// inotify watchers of a file deletion. - #[cfg(feature = "abi-7-18")] - pub fn delete(&self, parent: u64, child: u64, name: &OsStr) -> io::Result<()> { - let notif = Notification::new_delete(parent, child, name).map_err(Self::too_big_err)?; - self.send_inval(notify_code::FUSE_NOTIFY_DELETE, ¬if) - } - - #[allow(unused)] - fn send_inval(&self, code: notify_code, notification: &Notification<'_>) -> io::Result<()> { - match self.send(code, notification) { - // ENOENT is harmless for an invalidation (the - // kernel may have already dropped the cached - // entry on its own anyway), so ignore it. - Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()), - x => x, - } - } - - fn send(&self, code: notify_code, notification: &Notification<'_>) -> io::Result<()> { - notification - .with_iovec(code, |iov| self.0.send(iov)) - .map_err(Self::too_big_err)? - } - - /// Create an error for indicating when a notification message - /// would exceed the capacity that its length descriptor field is - /// capable of encoding. - fn too_big_err(tfie: std::num::TryFromIntError) -> io::Error { - io::Error::new(io::ErrorKind::Other, format!("Data too large: {}", tfie)) - } -} diff --git a/vendor/fuser/src/reply.rs b/vendor/fuser/src/reply.rs deleted file mode 100644 index a154b71f7..000000000 --- a/vendor/fuser/src/reply.rs +++ /dev/null @@ -1,1094 +0,0 @@ -//! Filesystem operation reply -//! -//! A reply is passed to filesystem operation implementations and must be used to send back the -//! result of an operation. The reply can optionally be sent to another thread to asynchronously -//! work on an operation and provide the result later. Also it allows replying with a block of -//! data without cloning the data. A reply *must always* be used (by calling either ok() or -//! error() exactly once). - -use crate::ll::{ - self, - reply::{DirEntPlusList, DirEntryPlus}, - Generation, -}; -use crate::ll::{ - reply::{DirEntList, DirEntOffset, DirEntry}, - INodeNo, -}; -use libc::c_int; -use log::{error, warn}; -use std::convert::AsRef; -use std::ffi::OsStr; -use std::fmt; -use std::io::IoSlice; -use std::time::Duration; - -#[cfg(target_os = "macos")] -use std::time::SystemTime; - -use crate::{FileAttr, FileType}; - -/// Generic reply callback to send data -pub trait ReplySender: Send + Sync + Unpin + 'static { - /// Send data. - fn send(&self, data: &[IoSlice<'_>]) -> std::io::Result<()>; -} - -impl fmt::Debug for Box { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - write!(f, "Box") - } -} - -/// Generic reply trait -pub trait Reply { - /// Create a new reply for the given request - fn new(unique: u64, sender: S) -> Self; -} - -/// -/// Raw reply -/// -#[derive(Debug)] -pub(crate) struct ReplyRaw { - /// Unique id of the request to reply to - unique: ll::RequestId, - /// Closure to call for sending the reply - sender: Option>, -} - -impl Reply for ReplyRaw { - fn new(unique: u64, sender: S) -> ReplyRaw { - let sender = Box::new(sender); - ReplyRaw { - unique: ll::RequestId(unique), - sender: Some(sender), - } - } -} - -impl ReplyRaw { - /// Reply to a request with the given error code and data. Must be called - /// only once (the `ok` and `error` methods ensure this by consuming `self`) - fn send_ll_mut(&mut self, response: &ll::Response<'_>) { - assert!(self.sender.is_some()); - let sender = self.sender.take().unwrap(); - let res = response.with_iovec(self.unique, |iov| sender.send(iov)); - if let Err(err) = res { - error!("Failed to send FUSE reply: {}", err); - } - } - fn send_ll(mut self, response: &ll::Response<'_>) { - self.send_ll_mut(response) - } - - /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - assert_ne!(err, 0); - self.send_ll(&ll::Response::new_error(ll::Errno::from_i32(err))); - } -} - -impl Drop for ReplyRaw { - fn drop(&mut self) { - if self.sender.is_some() { - warn!( - "Reply not sent for operation {}, replying with I/O error", - self.unique.0 - ); - self.send_ll_mut(&ll::Response::new_error(ll::Errno::EIO)); - } - } -} - -/// -/// Empty reply -/// -#[derive(Debug)] -pub struct ReplyEmpty { - reply: ReplyRaw, -} - -impl Reply for ReplyEmpty { - fn new(unique: u64, sender: S) -> ReplyEmpty { - ReplyEmpty { - reply: Reply::new(unique, sender), - } - } -} - -impl ReplyEmpty { - /// Reply to a request with nothing - pub fn ok(self) { - self.reply.send_ll(&ll::Response::new_empty()); - } - - /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - self.reply.error(err); - } -} - -/// -/// Data reply -/// -#[derive(Debug)] -pub struct ReplyData { - reply: ReplyRaw, -} - -impl Reply for ReplyData { - fn new(unique: u64, sender: S) -> ReplyData { - ReplyData { - reply: Reply::new(unique, sender), - } - } -} - -impl ReplyData { - /// Reply to a request with the given data - pub fn data(self, data: &[u8]) { - self.reply.send_ll(&ll::Response::new_slice(data)); - } - - /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - self.reply.error(err); - } -} - -/// -/// Entry reply -/// -#[derive(Debug)] -pub struct ReplyEntry { - reply: ReplyRaw, -} - -impl Reply for ReplyEntry { - fn new(unique: u64, sender: S) -> ReplyEntry { - ReplyEntry { - reply: Reply::new(unique, sender), - } - } -} - -impl ReplyEntry { - /// Reply to a request with the given entry - pub fn entry(self, ttl: &Duration, attr: &FileAttr, generation: u64) { - self.reply.send_ll(&ll::Response::new_entry( - ll::INodeNo(attr.ino), - ll::Generation(generation), - &attr.into(), - *ttl, - *ttl, - )); - } - - /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - self.reply.error(err); - } -} - -/// -/// Attribute Reply -/// -#[derive(Debug)] -pub struct ReplyAttr { - reply: ReplyRaw, -} - -impl Reply for ReplyAttr { - fn new(unique: u64, sender: S) -> ReplyAttr { - ReplyAttr { - reply: Reply::new(unique, sender), - } - } -} - -impl ReplyAttr { - /// Reply to a request with the given attribute - pub fn attr(self, ttl: &Duration, attr: &FileAttr) { - self.reply - .send_ll(&ll::Response::new_attr(ttl, &attr.into())); - } - - /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - self.reply.error(err); - } -} - -/// -/// XTimes Reply -/// -#[cfg(target_os = "macos")] -#[derive(Debug)] -pub struct ReplyXTimes { - reply: ReplyRaw, -} - -#[cfg(target_os = "macos")] -impl Reply for ReplyXTimes { - fn new(unique: u64, sender: S) -> ReplyXTimes { - ReplyXTimes { - reply: Reply::new(unique, sender), - } - } -} - -#[cfg(target_os = "macos")] -impl ReplyXTimes { - /// Reply to a request with the given xtimes - pub fn xtimes(self, bkuptime: SystemTime, crtime: SystemTime) { - self.reply - .send_ll(&ll::Response::new_xtimes(bkuptime, crtime)) - } - - /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - self.reply.error(err); - } -} - -/// -/// Open Reply -/// -#[derive(Debug)] -pub struct ReplyOpen { - reply: ReplyRaw, -} - -impl Reply for ReplyOpen { - fn new(unique: u64, sender: S) -> ReplyOpen { - ReplyOpen { - reply: Reply::new(unique, sender), - } - } -} - -impl ReplyOpen { - /// Reply to a request with the given open result - pub fn opened(self, fh: u64, flags: u32) { - self.reply - .send_ll(&ll::Response::new_open(ll::FileHandle(fh), flags)) - } - - /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - self.reply.error(err); - } -} - -/// -/// Write Reply -/// -#[derive(Debug)] -pub struct ReplyWrite { - reply: ReplyRaw, -} - -impl Reply for ReplyWrite { - fn new(unique: u64, sender: S) -> ReplyWrite { - ReplyWrite { - reply: Reply::new(unique, sender), - } - } -} - -impl ReplyWrite { - /// Reply to a request with the given open result - pub fn written(self, size: u32) { - self.reply.send_ll(&ll::Response::new_write(size)) - } - - /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - self.reply.error(err); - } -} - -/// -/// Statfs Reply -/// -#[derive(Debug)] -pub struct ReplyStatfs { - reply: ReplyRaw, -} - -impl Reply for ReplyStatfs { - fn new(unique: u64, sender: S) -> ReplyStatfs { - ReplyStatfs { - reply: Reply::new(unique, sender), - } - } -} - -impl ReplyStatfs { - /// Reply to a request with the given open result - #[allow(clippy::too_many_arguments)] - pub fn statfs( - self, - blocks: u64, - bfree: u64, - bavail: u64, - files: u64, - ffree: u64, - bsize: u32, - namelen: u32, - frsize: u32, - ) { - self.reply.send_ll(&ll::Response::new_statfs( - blocks, bfree, bavail, files, ffree, bsize, namelen, frsize, - )) - } - - /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - self.reply.error(err); - } -} - -/// -/// Create reply -/// -#[derive(Debug)] -pub struct ReplyCreate { - reply: ReplyRaw, -} - -impl Reply for ReplyCreate { - fn new(unique: u64, sender: S) -> ReplyCreate { - ReplyCreate { - reply: Reply::new(unique, sender), - } - } -} - -impl ReplyCreate { - /// Reply to a request with the given entry - pub fn created(self, ttl: &Duration, attr: &FileAttr, generation: u64, fh: u64, flags: u32) { - self.reply.send_ll(&ll::Response::new_create( - ttl, - &attr.into(), - ll::Generation(generation), - ll::FileHandle(fh), - flags, - )) - } - - /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - self.reply.error(err); - } -} - -/// -/// Lock Reply -/// -#[derive(Debug)] -pub struct ReplyLock { - reply: ReplyRaw, -} - -impl Reply for ReplyLock { - fn new(unique: u64, sender: S) -> ReplyLock { - ReplyLock { - reply: Reply::new(unique, sender), - } - } -} - -impl ReplyLock { - /// Reply to a request with the given open result - pub fn locked(self, start: u64, end: u64, typ: i32, pid: u32) { - self.reply.send_ll(&ll::Response::new_lock(&ll::Lock { - range: (start, end), - typ, - pid, - })) - } - - /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - self.reply.error(err); - } -} - -/// -/// Bmap Reply -/// -#[derive(Debug)] -pub struct ReplyBmap { - reply: ReplyRaw, -} - -impl Reply for ReplyBmap { - fn new(unique: u64, sender: S) -> ReplyBmap { - ReplyBmap { - reply: Reply::new(unique, sender), - } - } -} - -impl ReplyBmap { - /// Reply to a request with the given open result - pub fn bmap(self, block: u64) { - self.reply.send_ll(&ll::Response::new_bmap(block)) - } - - /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - self.reply.error(err); - } -} - -/// -/// Ioctl Reply -/// -#[derive(Debug)] -pub struct ReplyIoctl { - reply: ReplyRaw, -} - -impl Reply for ReplyIoctl { - fn new(unique: u64, sender: S) -> ReplyIoctl { - ReplyIoctl { - reply: Reply::new(unique, sender), - } - } -} - -impl ReplyIoctl { - /// Reply to a request with the given open result - pub fn ioctl(self, result: i32, data: &[u8]) { - self.reply - .send_ll(&ll::Response::new_ioctl(result, &[IoSlice::new(data)])); - } - - /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - self.reply.error(err); - } -} - -/// -/// Poll Reply -/// -#[derive(Debug)] -#[cfg(feature = "abi-7-11")] -pub struct ReplyPoll { - reply: ReplyRaw, -} - -#[cfg(feature = "abi-7-11")] -impl Reply for ReplyPoll { - fn new(unique: u64, sender: S) -> ReplyPoll { - ReplyPoll { - reply: Reply::new(unique, sender), - } - } -} - -#[cfg(feature = "abi-7-11")] -impl ReplyPoll { - /// Reply to a request with the given poll result - pub fn poll(self, revents: u32) { - self.reply.send_ll(&ll::Response::new_poll(revents)) - } - - /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - self.reply.error(err); - } -} - -/// -/// Directory reply -/// -#[derive(Debug)] -pub struct ReplyDirectory { - reply: ReplyRaw, - data: DirEntList, -} - -impl ReplyDirectory { - /// Creates a new ReplyDirectory with a specified buffer size. - pub fn new(unique: u64, sender: S, size: usize) -> ReplyDirectory { - ReplyDirectory { - reply: Reply::new(unique, sender), - data: DirEntList::new(size), - } - } - - /// Add an entry to the directory reply buffer. Returns true if the buffer is full. - /// A transparent offset value can be provided for each entry. The kernel uses these - /// value to request the next entries in further readdir calls - #[must_use] - pub fn add>(&mut self, ino: u64, offset: i64, kind: FileType, name: T) -> bool { - let name = name.as_ref(); - self.data.push(&DirEntry::new( - INodeNo(ino), - DirEntOffset(offset), - kind, - name, - )) - } - - /// Reply to a request with the filled directory buffer - pub fn ok(self) { - self.reply.send_ll(&self.data.into()); - } - - /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - self.reply.error(err); - } -} - -/// -/// DirectoryPlus reply -/// -#[derive(Debug)] -pub struct ReplyDirectoryPlus { - reply: ReplyRaw, - buf: DirEntPlusList, -} - -impl ReplyDirectoryPlus { - /// Creates a new ReplyDirectory with a specified buffer size. - pub fn new(unique: u64, sender: S, size: usize) -> ReplyDirectoryPlus { - ReplyDirectoryPlus { - reply: Reply::new(unique, sender), - buf: DirEntPlusList::new(size), - } - } - - /// Add an entry to the directory reply buffer. Returns true if the buffer is full. - /// A transparent offset value can be provided for each entry. The kernel uses these - /// value to request the next entries in further readdir calls - pub fn add>( - &mut self, - ino: u64, - offset: i64, - name: T, - ttl: &Duration, - attr: &FileAttr, - generation: u64, - ) -> bool { - let name = name.as_ref(); - self.buf.push(&DirEntryPlus::new( - INodeNo(ino), - Generation(generation), - DirEntOffset(offset), - name, - *ttl, - attr.into(), - *ttl, - )) - } - - /// Reply to a request with the filled directory buffer - pub fn ok(self) { - self.reply.send_ll(&self.buf.into()); - } - - /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - self.reply.error(err); - } -} - -/// -/// Xattr reply -/// -#[derive(Debug)] -pub struct ReplyXattr { - reply: ReplyRaw, -} - -impl Reply for ReplyXattr { - fn new(unique: u64, sender: S) -> ReplyXattr { - ReplyXattr { - reply: Reply::new(unique, sender), - } - } -} - -impl ReplyXattr { - /// Reply to a request with the size of the xattr. - pub fn size(self, size: u32) { - self.reply.send_ll(&ll::Response::new_xattr_size(size)) - } - - /// Reply to a request with the data in the xattr. - pub fn data(self, data: &[u8]) { - self.reply.send_ll(&ll::Response::new_data(data)) - } - - /// Reply to a request with the given error code. - pub fn error(self, err: c_int) { - self.reply.error(err); - } -} - -/// -/// Lseek Reply -/// -#[derive(Debug)] -pub struct ReplyLseek { - reply: ReplyRaw, -} - -impl Reply for ReplyLseek { - fn new(unique: u64, sender: S) -> ReplyLseek { - ReplyLseek { - reply: Reply::new(unique, sender), - } - } -} - -impl ReplyLseek { - /// Reply to a request with seeked offset - pub fn offset(self, offset: i64) { - self.reply.send_ll(&ll::Response::new_lseek(offset)) - } - - /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - self.reply.error(err); - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::{FileAttr, FileType}; - use std::io::IoSlice; - use std::sync::mpsc::{sync_channel, SyncSender}; - use std::thread; - use std::time::{Duration, UNIX_EPOCH}; - use zerocopy::{Immutable, IntoBytes}; - - #[derive(Debug, IntoBytes, Immutable)] - #[repr(C)] - struct Data { - a: u8, - b: u8, - c: u16, - } - - #[test] - fn serialize_empty() { - assert!(().as_bytes().is_empty()); - } - - #[test] - fn serialize_slice() { - let data: [u8; 4] = [0x12, 0x34, 0x56, 0x78]; - assert_eq!(data.as_bytes(), [0x12, 0x34, 0x56, 0x78]); - } - - #[test] - fn serialize_struct() { - let data = Data { - a: 0x12, - b: 0x34, - c: 0x5678, - }; - assert_eq!(data.as_bytes(), [0x12, 0x34, 0x78, 0x56]); - } - - struct AssertSender { - expected: Vec, - } - - impl super::ReplySender for AssertSender { - fn send(&self, data: &[IoSlice<'_>]) -> std::io::Result<()> { - let mut v = vec![]; - for x in data { - v.extend_from_slice(x) - } - assert_eq!(self.expected, v); - Ok(()) - } - } - - #[test] - fn reply_raw() { - let data = Data { - a: 0x12, - b: 0x34, - c: 0x5678, - }; - let sender = AssertSender { - expected: vec![ - 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, 0x12, 0x34, 0x78, 0x56, - ], - }; - let reply: ReplyRaw = Reply::new(0xdeadbeef, sender); - reply.send_ll(&ll::Response::new_data(data.as_bytes())); - } - - #[test] - fn reply_error() { - let sender = AssertSender { - expected: vec![ - 0x10, 0x00, 0x00, 0x00, 0xbe, 0xff, 0xff, 0xff, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, - ], - }; - let reply: ReplyRaw = Reply::new(0xdeadbeef, sender); - reply.error(66); - } - - #[test] - fn reply_empty() { - let sender = AssertSender { - expected: vec![ - 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, - ], - }; - let reply: ReplyEmpty = Reply::new(0xdeadbeef, sender); - reply.ok(); - } - - #[test] - fn reply_data() { - let sender = AssertSender { - expected: vec![ - 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, 0xde, 0xad, 0xbe, 0xef, - ], - }; - let reply: ReplyData = Reply::new(0xdeadbeef, sender); - reply.data(&[0xde, 0xad, 0xbe, 0xef]); - } - - #[test] - fn reply_entry() { - let mut expected = if cfg!(target_os = "macos") { - vec![ - 0x98, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xaa, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x65, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x65, 0x87, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x43, 0x00, 0x00, 0x21, 0x43, 0x00, 0x00, - 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x78, 0x56, 0x00, 0x00, 0x78, 0x56, 0x00, 0x00, 0x78, 0x56, 0x00, 0x00, 0x78, 0x56, - 0x00, 0x00, 0xa4, 0x81, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, - 0x77, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, - ] - } else { - vec![ - 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xaa, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x65, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x65, 0x87, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x43, 0x00, 0x00, 0x21, 0x43, 0x00, 0x00, - 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x56, 0x00, 0x00, 0x78, 0x56, 0x00, 0x00, - 0x78, 0x56, 0x00, 0x00, 0xa4, 0x81, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x66, 0x00, - 0x00, 0x00, 0x77, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, - ] - }; - - if cfg!(feature = "abi-7-9") { - expected.extend(vec![0xbb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); - } - expected[0] = (expected.len()) as u8; - - let sender = AssertSender { expected }; - let reply: ReplyEntry = Reply::new(0xdeadbeef, sender); - let time = UNIX_EPOCH + Duration::new(0x1234, 0x5678); - let ttl = Duration::new(0x8765, 0x4321); - let attr = FileAttr { - ino: 0x11, - size: 0x22, - blocks: 0x33, - atime: time, - mtime: time, - ctime: time, - crtime: time, - kind: FileType::RegularFile, - perm: 0o644, - nlink: 0x55, - uid: 0x66, - gid: 0x77, - rdev: 0x88, - flags: 0x99, - blksize: 0xbb, - }; - reply.entry(&ttl, &attr, 0xaa); - } - - #[test] - fn reply_attr() { - let mut expected = if cfg!(target_os = "macos") { - vec![ - 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, 0x65, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x43, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x78, 0x56, 0x00, 0x00, 0x78, 0x56, 0x00, 0x00, 0x78, 0x56, - 0x00, 0x00, 0x78, 0x56, 0x00, 0x00, 0xa4, 0x81, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, - 0x66, 0x00, 0x00, 0x00, 0x77, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x99, 0x00, - 0x00, 0x00, - ] - } else { - vec![ - 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, 0x65, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x43, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x56, 0x00, 0x00, - 0x78, 0x56, 0x00, 0x00, 0x78, 0x56, 0x00, 0x00, 0xa4, 0x81, 0x00, 0x00, 0x55, 0x00, - 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, 0x77, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, - ] - }; - - if cfg!(feature = "abi-7-9") { - expected.extend_from_slice(&[0xbb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); - } - expected[0] = expected.len() as u8; - - let sender = AssertSender { expected }; - let reply: ReplyAttr = Reply::new(0xdeadbeef, sender); - let time = UNIX_EPOCH + Duration::new(0x1234, 0x5678); - let ttl = Duration::new(0x8765, 0x4321); - let attr = FileAttr { - ino: 0x11, - size: 0x22, - blocks: 0x33, - atime: time, - mtime: time, - ctime: time, - crtime: time, - kind: FileType::RegularFile, - perm: 0o644, - nlink: 0x55, - uid: 0x66, - gid: 0x77, - rdev: 0x88, - flags: 0x99, - blksize: 0xbb, - }; - reply.attr(&ttl, &attr); - } - - #[test] - #[cfg(target_os = "macos")] - fn reply_xtimes() { - let sender = AssertSender { - expected: vec![ - 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x78, 0x56, 0x00, 0x00, 0x78, 0x56, 0x00, 0x00, - ], - }; - let reply: ReplyXTimes = Reply::new(0xdeadbeef, sender); - let time = UNIX_EPOCH + Duration::new(0x1234, 0x5678); - reply.xtimes(time, time); - } - - #[test] - fn reply_open() { - let sender = AssertSender { - expected: vec![ - 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, 0x22, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - ], - }; - let reply: ReplyOpen = Reply::new(0xdeadbeef, sender); - reply.opened(0x1122, 0x33); - } - - #[test] - fn reply_write() { - let sender = AssertSender { - expected: vec![ - 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, 0x22, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ], - }; - let reply: ReplyWrite = Reply::new(0xdeadbeef, sender); - reply.written(0x1122); - } - - #[test] - fn reply_statfs() { - let sender = AssertSender { - expected: vec![ - 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x66, 0x00, 0x00, 0x00, 0x77, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ], - }; - let reply: ReplyStatfs = Reply::new(0xdeadbeef, sender); - reply.statfs(0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88); - } - - #[test] - fn reply_create() { - let mut expected = if cfg!(target_os = "macos") { - vec![ - 0xa8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xaa, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x65, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x65, 0x87, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x43, 0x00, 0x00, 0x21, 0x43, 0x00, 0x00, - 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x78, 0x56, 0x00, 0x00, 0x78, 0x56, 0x00, 0x00, 0x78, 0x56, 0x00, 0x00, 0x78, 0x56, - 0x00, 0x00, 0xa4, 0x81, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, - 0x77, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, 0xbb, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xcc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ] - } else { - vec![ - 0x98, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xaa, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x65, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x65, 0x87, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x43, 0x00, 0x00, 0x21, 0x43, 0x00, 0x00, - 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x56, 0x00, 0x00, 0x78, 0x56, 0x00, 0x00, - 0x78, 0x56, 0x00, 0x00, 0xa4, 0x81, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x66, 0x00, - 0x00, 0x00, 0x77, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0xbb, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xcc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ] - }; - - if cfg!(feature = "abi-7-9") { - let insert_at = expected.len() - 16; - expected.splice( - insert_at..insert_at, - vec![0xdd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], - ); - } - expected[0] = (expected.len()) as u8; - - let sender = AssertSender { expected }; - let reply: ReplyCreate = Reply::new(0xdeadbeef, sender); - let time = UNIX_EPOCH + Duration::new(0x1234, 0x5678); - let ttl = Duration::new(0x8765, 0x4321); - let attr = FileAttr { - ino: 0x11, - size: 0x22, - blocks: 0x33, - atime: time, - mtime: time, - ctime: time, - crtime: time, - kind: FileType::RegularFile, - perm: 0o644, - nlink: 0x55, - uid: 0x66, - gid: 0x77, - rdev: 0x88, - flags: 0x99, - blksize: 0xdd, - }; - reply.created(&ttl, &attr, 0xaa, 0xbb, 0xcc); - } - - #[test] - fn reply_lock() { - let sender = AssertSender { - expected: vec![ - 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, - ], - }; - let reply: ReplyLock = Reply::new(0xdeadbeef, sender); - reply.locked(0x11, 0x22, 0x33, 0x44); - } - - #[test] - fn reply_bmap() { - let sender = AssertSender { - expected: vec![ - 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ], - }; - let reply: ReplyBmap = Reply::new(0xdeadbeef, sender); - reply.bmap(0x1234); - } - - #[test] - fn reply_directory() { - let sender = AssertSender { - expected: vec![ - 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, - 0x00, 0x00, 0xbb, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x68, 0x65, - 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x00, 0xdd, 0xcc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00, - 0x00, 0x00, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x72, 0x73, - ], - }; - let mut reply = ReplyDirectory::new(0xdeadbeef, sender, 4096); - assert!(!reply.add(0xaabb, 1, FileType::Directory, "hello")); - assert!(!reply.add(0xccdd, 2, FileType::RegularFile, "world.rs")); - reply.ok(); - } - - #[test] - fn reply_xattr_size() { - let sender = AssertSender { - expected: vec![ - 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xEF, 0xBE, 0xAD, 0xDE, 0x00, 0x00, - 0x00, 0x00, 0x78, 0x56, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, - ], - }; - let reply = ReplyXattr::new(0xdeadbeef, sender); - reply.size(0x12345678); - } - - #[test] - fn reply_xattr_data() { - let sender = AssertSender { - expected: vec![ - 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xEF, 0xBE, 0xAD, 0xDE, 0x00, 0x00, - 0x00, 0x00, 0x11, 0x22, 0x33, 0x44, - ], - }; - let reply = ReplyXattr::new(0xdeadbeef, sender); - reply.data(&[0x11, 0x22, 0x33, 0x44]); - } - - impl super::ReplySender for SyncSender<()> { - fn send(&self, _: &[IoSlice<'_>]) -> std::io::Result<()> { - self.send(()).unwrap(); - Ok(()) - } - } - - #[test] - fn async_reply() { - let (tx, rx) = sync_channel::<()>(1); - let reply: ReplyEmpty = Reply::new(0xdeadbeef, tx); - thread::spawn(move || { - reply.ok(); - }); - rx.recv().unwrap(); - } -} diff --git a/vendor/fuser/src/request.rs b/vendor/fuser/src/request.rs deleted file mode 100644 index e25a4c646..000000000 --- a/vendor/fuser/src/request.rs +++ /dev/null @@ -1,673 +0,0 @@ -//! Filesystem operation request -//! -//! A request represents information about a filesystem operation the kernel driver wants us to -//! perform. -//! -//! TODO: This module is meant to go away soon in favor of `ll::Request`. - -use crate::ll::{fuse_abi as abi, Errno, Response}; -use log::{debug, error, warn}; -use std::convert::TryFrom; -#[cfg(feature = "abi-7-28")] -use std::convert::TryInto; -use std::path::Path; - -use crate::channel::ChannelSender; -use crate::ll::Request as _; -#[cfg(feature = "abi-7-21")] -use crate::reply::ReplyDirectoryPlus; -use crate::reply::{Reply, ReplyDirectory, ReplySender}; -use crate::session::{Session, SessionACL}; -use crate::Filesystem; -#[cfg(feature = "abi-7-11")] -use crate::PollHandle; -use crate::{ll, KernelConfig}; - -/// Request data structure -#[derive(Debug)] -pub struct Request<'a> { - /// Channel sender for sending the reply - ch: ChannelSender, - /// Request raw data - #[allow(unused)] - data: &'a [u8], - /// Parsed request - request: ll::AnyRequest<'a>, -} - -impl<'a> Request<'a> { - /// Create a new request from the given data - pub(crate) fn new(ch: ChannelSender, data: &'a [u8]) -> Option> { - let request = match ll::AnyRequest::try_from(data) { - Ok(request) => request, - Err(err) => { - error!("{}", err); - return None; - } - }; - - Some(Self { ch, data, request }) - } - - /// Dispatch request to the given filesystem. - /// This calls the appropriate filesystem operation method for the - /// request and sends back the returned reply to the kernel - pub(crate) fn dispatch(&self, se: &mut Session) { - debug!("{}", self.request); - let unique = self.request.unique(); - - let res = match self.dispatch_req(se) { - Ok(Some(resp)) => resp, - Ok(None) => return, - Err(errno) => self.request.reply_err(errno), - } - .with_iovec(unique, |iov| self.ch.send(iov)); - - if let Err(err) = res { - warn!("Request {:?}: Failed to send reply: {}", unique, err) - } - } - - fn dispatch_req( - &self, - se: &mut Session, - ) -> Result>, Errno> { - let op = self.request.operation().map_err(|_| Errno::ENOSYS)?; - // Implement allow_root & access check for auto_unmount - if (se.allowed == SessionACL::RootAndOwner - && self.request.uid() != se.session_owner - && self.request.uid() != 0) - || (se.allowed == SessionACL::Owner && self.request.uid() != se.session_owner) - { - #[cfg(feature = "abi-7-21")] - { - match op { - // Only allow operations that the kernel may issue without a uid set - ll::Operation::Init(_) - | ll::Operation::Destroy(_) - | ll::Operation::Read(_) - | ll::Operation::ReadDir(_) - | ll::Operation::ReadDirPlus(_) - | ll::Operation::BatchForget(_) - | ll::Operation::Forget(_) - | ll::Operation::Write(_) - | ll::Operation::FSync(_) - | ll::Operation::FSyncDir(_) - | ll::Operation::Release(_) - | ll::Operation::ReleaseDir(_) => {} - _ => { - return Err(Errno::EACCES); - } - } - } - #[cfg(all(feature = "abi-7-16", not(feature = "abi-7-21")))] - { - match op { - // Only allow operations that the kernel may issue without a uid set - ll::Operation::Init(_) - | ll::Operation::Destroy(_) - | ll::Operation::Read(_) - | ll::Operation::ReadDir(_) - | ll::Operation::BatchForget(_) - | ll::Operation::Forget(_) - | ll::Operation::Write(_) - | ll::Operation::FSync(_) - | ll::Operation::FSyncDir(_) - | ll::Operation::Release(_) - | ll::Operation::ReleaseDir(_) => {} - _ => { - return Err(Errno::EACCES); - } - } - } - #[cfg(not(feature = "abi-7-16"))] - { - match op { - // Only allow operations that the kernel may issue without a uid set - ll::Operation::Init(_) - | ll::Operation::Destroy(_) - | ll::Operation::Read(_) - | ll::Operation::ReadDir(_) - | ll::Operation::Forget(_) - | ll::Operation::Write(_) - | ll::Operation::FSync(_) - | ll::Operation::FSyncDir(_) - | ll::Operation::Release(_) - | ll::Operation::ReleaseDir(_) => {} - _ => { - return Err(Errno::EACCES); - } - } - } - } - match op { - // Filesystem initialization - ll::Operation::Init(x) => { - // We don't support ABI versions before 7.6 - let v = x.version(); - if v < ll::Version(7, 6) { - error!("Unsupported FUSE ABI version {}", v); - return Err(Errno::EPROTO); - } - // Remember ABI version supported by kernel - se.proto_major = v.major(); - se.proto_minor = v.minor(); - - let mut config = KernelConfig::new(x.capabilities(), x.max_readahead()); - // Call filesystem init method and give it a chance to return an error - se.filesystem - .init(self, &mut config) - .map_err(Errno::from_i32)?; - - // Reply with our desired version and settings. If the kernel supports a - // larger major version, it'll re-send a matching init message. If it - // supports only lower major versions, we replied with an error above. - debug!( - "INIT response: ABI {}.{}, flags {:#x}, max readahead {}, max write {}", - abi::FUSE_KERNEL_VERSION, - abi::FUSE_KERNEL_MINOR_VERSION, - x.capabilities() & config.requested, - config.max_readahead, - config.max_write - ); - se.initialized = true; - return Ok(Some(x.reply(&config))); - } - // Any operation is invalid before initialization - _ if !se.initialized => { - warn!("Ignoring FUSE operation before init: {}", self.request); - return Err(Errno::EIO); - } - // Filesystem destroyed - ll::Operation::Destroy(x) => { - se.filesystem.destroy(); - se.destroyed = true; - return Ok(Some(x.reply())); - } - // Any operation is invalid after destroy - _ if se.destroyed => { - warn!("Ignoring FUSE operation after destroy: {}", self.request); - return Err(Errno::EIO); - } - - ll::Operation::Interrupt(_) => { - // TODO: handle FUSE_INTERRUPT - return Err(Errno::ENOSYS); - } - - ll::Operation::Lookup(x) => { - se.filesystem.lookup( - self, - self.request.nodeid().into(), - x.name().as_ref(), - self.reply(), - ); - } - ll::Operation::Forget(x) => { - se.filesystem - .forget(self, self.request.nodeid().into(), x.nlookup()); // no reply - } - ll::Operation::GetAttr(_attr) => { - #[cfg(feature = "abi-7-9")] - se.filesystem.getattr( - self, - self.request.nodeid().into(), - _attr.file_handle().map(|fh| fh.into()), - self.reply(), - ); - - // Pre-abi-7-9 does not support providing a file handle. - #[cfg(not(feature = "abi-7-9"))] - se.filesystem - .getattr(self, self.request.nodeid().into(), None, self.reply()); - } - ll::Operation::SetAttr(x) => { - se.filesystem.setattr( - self, - self.request.nodeid().into(), - x.mode(), - x.uid(), - x.gid(), - x.size(), - x.atime(), - x.mtime(), - x.ctime(), - x.file_handle().map(|fh| fh.into()), - x.crtime(), - x.chgtime(), - x.bkuptime(), - x.flags(), - self.reply(), - ); - } - ll::Operation::ReadLink(_) => { - se.filesystem - .readlink(self, self.request.nodeid().into(), self.reply()); - } - ll::Operation::MkNod(x) => { - se.filesystem.mknod( - self, - self.request.nodeid().into(), - x.name().as_ref(), - x.mode(), - x.umask(), - x.rdev(), - self.reply(), - ); - } - ll::Operation::MkDir(x) => { - se.filesystem.mkdir( - self, - self.request.nodeid().into(), - x.name().as_ref(), - x.mode(), - x.umask(), - self.reply(), - ); - } - ll::Operation::Unlink(x) => { - se.filesystem.unlink( - self, - self.request.nodeid().into(), - x.name().as_ref(), - self.reply(), - ); - } - ll::Operation::RmDir(x) => { - se.filesystem.rmdir( - self, - self.request.nodeid().into(), - x.name().as_ref(), - self.reply(), - ); - } - ll::Operation::SymLink(x) => { - se.filesystem.symlink( - self, - self.request.nodeid().into(), - x.link_name().as_ref(), - Path::new(x.target()), - self.reply(), - ); - } - ll::Operation::Rename(x) => { - se.filesystem.rename( - self, - self.request.nodeid().into(), - x.src().name.as_ref(), - x.dest().dir.into(), - x.dest().name.as_ref(), - 0, - self.reply(), - ); - } - ll::Operation::Link(x) => { - se.filesystem.link( - self, - x.inode_no().into(), - self.request.nodeid().into(), - x.dest().name.as_ref(), - self.reply(), - ); - } - ll::Operation::Open(x) => { - se.filesystem - .open(self, self.request.nodeid().into(), x.flags(), self.reply()); - } - ll::Operation::Read(x) => { - se.filesystem.read( - self, - self.request.nodeid().into(), - x.file_handle().into(), - x.offset(), - x.size(), - x.flags(), - x.lock_owner().map(|l| l.into()), - self.reply(), - ); - } - ll::Operation::Write(x) => { - se.filesystem.write( - self, - self.request.nodeid().into(), - x.file_handle().into(), - x.offset(), - x.data(), - x.write_flags(), - x.flags(), - x.lock_owner().map(|l| l.into()), - self.reply(), - ); - } - ll::Operation::Flush(x) => { - se.filesystem.flush( - self, - self.request.nodeid().into(), - x.file_handle().into(), - x.lock_owner().into(), - self.reply(), - ); - } - ll::Operation::Release(x) => { - se.filesystem.release( - self, - self.request.nodeid().into(), - x.file_handle().into(), - x.flags(), - x.lock_owner().map(|x| x.into()), - x.flush(), - self.reply(), - ); - } - ll::Operation::FSync(x) => { - se.filesystem.fsync( - self, - self.request.nodeid().into(), - x.file_handle().into(), - x.fdatasync(), - self.reply(), - ); - } - ll::Operation::OpenDir(x) => { - se.filesystem - .opendir(self, self.request.nodeid().into(), x.flags(), self.reply()); - } - ll::Operation::ReadDir(x) => { - se.filesystem.readdir( - self, - self.request.nodeid().into(), - x.file_handle().into(), - x.offset(), - ReplyDirectory::new( - self.request.unique().into(), - self.ch.clone(), - x.size() as usize, - ), - ); - } - ll::Operation::ReleaseDir(x) => { - se.filesystem.releasedir( - self, - self.request.nodeid().into(), - x.file_handle().into(), - x.flags(), - self.reply(), - ); - } - ll::Operation::FSyncDir(x) => { - se.filesystem.fsyncdir( - self, - self.request.nodeid().into(), - x.file_handle().into(), - x.fdatasync(), - self.reply(), - ); - } - ll::Operation::StatFs(_) => { - se.filesystem - .statfs(self, self.request.nodeid().into(), self.reply()); - } - ll::Operation::SetXAttr(x) => { - se.filesystem.setxattr( - self, - self.request.nodeid().into(), - x.name(), - x.value(), - x.flags(), - x.position(), - self.reply(), - ); - } - ll::Operation::GetXAttr(x) => { - se.filesystem.getxattr( - self, - self.request.nodeid().into(), - x.name(), - x.size_u32(), - self.reply(), - ); - } - ll::Operation::ListXAttr(x) => { - se.filesystem - .listxattr(self, self.request.nodeid().into(), x.size(), self.reply()); - } - ll::Operation::RemoveXAttr(x) => { - se.filesystem.removexattr( - self, - self.request.nodeid().into(), - x.name(), - self.reply(), - ); - } - ll::Operation::Access(x) => { - se.filesystem - .access(self, self.request.nodeid().into(), x.mask(), self.reply()); - } - ll::Operation::Create(x) => { - se.filesystem.create( - self, - self.request.nodeid().into(), - x.name().as_ref(), - x.mode(), - x.umask(), - x.flags(), - self.reply(), - ); - } - ll::Operation::GetLk(x) => { - se.filesystem.getlk( - self, - self.request.nodeid().into(), - x.file_handle().into(), - x.lock_owner().into(), - x.lock().range.0, - x.lock().range.1, - x.lock().typ, - x.lock().pid, - self.reply(), - ); - } - ll::Operation::SetLk(x) => { - se.filesystem.setlk( - self, - self.request.nodeid().into(), - x.file_handle().into(), - x.lock_owner().into(), - x.lock().range.0, - x.lock().range.1, - x.lock().typ, - x.lock().pid, - false, - self.reply(), - ); - } - ll::Operation::SetLkW(x) => { - se.filesystem.setlk( - self, - self.request.nodeid().into(), - x.file_handle().into(), - x.lock_owner().into(), - x.lock().range.0, - x.lock().range.1, - x.lock().typ, - x.lock().pid, - true, - self.reply(), - ); - } - ll::Operation::BMap(x) => { - se.filesystem.bmap( - self, - self.request.nodeid().into(), - x.block_size(), - x.block(), - self.reply(), - ); - } - - #[cfg(feature = "abi-7-11")] - ll::Operation::IoCtl(x) => { - if x.unrestricted() { - return Err(Errno::ENOSYS); - } else { - se.filesystem.ioctl( - self, - self.request.nodeid().into(), - x.file_handle().into(), - x.flags(), - x.command(), - x.in_data(), - x.out_size(), - self.reply(), - ); - } - } - #[cfg(feature = "abi-7-11")] - ll::Operation::Poll(x) => { - let ph = PollHandle::new(se.ch.sender(), x.kernel_handle()); - - se.filesystem.poll( - self, - self.request.nodeid().into(), - x.file_handle().into(), - ph, - x.events(), - x.flags(), - self.reply(), - ); - } - #[cfg(feature = "abi-7-15")] - ll::Operation::NotifyReply(_) => { - // TODO: handle FUSE_NOTIFY_REPLY - return Err(Errno::ENOSYS); - } - #[cfg(feature = "abi-7-16")] - ll::Operation::BatchForget(x) => { - se.filesystem.batch_forget(self, x.nodes()); // no reply - } - #[cfg(feature = "abi-7-19")] - ll::Operation::FAllocate(x) => { - se.filesystem.fallocate( - self, - self.request.nodeid().into(), - x.file_handle().into(), - x.offset(), - x.len(), - x.mode(), - self.reply(), - ); - } - #[cfg(feature = "abi-7-21")] - ll::Operation::ReadDirPlus(x) => { - se.filesystem.readdirplus( - self, - self.request.nodeid().into(), - x.file_handle().into(), - x.offset(), - ReplyDirectoryPlus::new( - self.request.unique().into(), - self.ch.clone(), - x.size() as usize, - ), - ); - } - #[cfg(feature = "abi-7-23")] - ll::Operation::Rename2(x) => { - se.filesystem.rename( - self, - x.from().dir.into(), - x.from().name.as_ref(), - x.to().dir.into(), - x.to().name.as_ref(), - x.flags(), - self.reply(), - ); - } - #[cfg(feature = "abi-7-24")] - ll::Operation::Lseek(x) => { - se.filesystem.lseek( - self, - self.request.nodeid().into(), - x.file_handle().into(), - x.offset(), - x.whence(), - self.reply(), - ); - } - #[cfg(feature = "abi-7-28")] - ll::Operation::CopyFileRange(x) => { - let (i, o) = (x.src(), x.dest()); - se.filesystem.copy_file_range( - self, - i.inode.into(), - i.file_handle.into(), - i.offset, - o.inode.into(), - o.file_handle.into(), - o.offset, - x.len(), - x.flags().try_into().unwrap(), - self.reply(), - ); - } - #[cfg(target_os = "macos")] - ll::Operation::SetVolName(x) => { - se.filesystem.setvolname(self, x.name(), self.reply()); - } - #[cfg(target_os = "macos")] - ll::Operation::GetXTimes(x) => { - se.filesystem - .getxtimes(self, x.nodeid().into(), self.reply()); - } - #[cfg(target_os = "macos")] - ll::Operation::Exchange(x) => { - se.filesystem.exchange( - self, - x.from().dir.into(), - x.from().name.as_ref(), - x.to().dir.into(), - x.to().name.as_ref(), - x.options(), - self.reply(), - ); - } - - #[cfg(feature = "abi-7-12")] - ll::Operation::CuseInit(_) => { - // TODO: handle CUSE_INIT - return Err(Errno::ENOSYS); - } - } - Ok(None) - } - - /// Create a reply object for this request that can be passed to the filesystem - /// implementation and makes sure that a request is replied exactly once - fn reply(&self) -> T { - Reply::new(self.request.unique().into(), self.ch.clone()) - } - - /// Returns the unique identifier of this request - #[inline] - pub fn unique(&self) -> u64 { - self.request.unique().into() - } - - /// Returns the uid of this request - #[inline] - pub fn uid(&self) -> u32 { - self.request.uid() - } - - /// Returns the gid of this request - #[inline] - pub fn gid(&self) -> u32 { - self.request.gid() - } - - /// Returns the pid of this request - #[inline] - pub fn pid(&self) -> u32 { - self.request.pid() - } -} diff --git a/vendor/fuser/src/session.rs b/vendor/fuser/src/session.rs deleted file mode 100644 index c435cb6c7..000000000 --- a/vendor/fuser/src/session.rs +++ /dev/null @@ -1,304 +0,0 @@ -//! Filesystem session -//! -//! A session runs a filesystem implementation while it is being mounted to a specific mount -//! point. A session begins by mounting the filesystem and ends by unmounting it. While the -//! filesystem is mounted, the session loop receives, dispatches and replies to kernel requests -//! for filesystem operations under its mount point. - -use libc::{EAGAIN, EINTR, ENODEV, ENOENT}; -use log::{info, warn}; -use nix::unistd::geteuid; -use std::fmt; -use std::os::fd::{AsFd, BorrowedFd, OwnedFd}; -use std::path::{Path, PathBuf}; -use std::sync::{Arc, Mutex}; -use std::thread::{self, JoinHandle}; -use std::{io, ops::DerefMut}; - -use crate::ll::fuse_abi as abi; -use crate::request::Request; -use crate::Filesystem; -use crate::MountOption; -use crate::{channel::Channel, mnt::Mount}; -#[cfg(feature = "abi-7-11")] -use crate::{channel::ChannelSender, notify::Notifier}; - -/// The max size of write requests from the kernel. The absolute minimum is 4k, -/// FUSE recommends at least 128k, max 16M. The FUSE default is 16M on macOS -/// and 128k on other systems. -pub const MAX_WRITE_SIZE: usize = 16 * 1024 * 1024; - -/// Size of the buffer for reading a request from the kernel. Since the kernel may send -/// up to MAX_WRITE_SIZE bytes in a write request, we use that value plus some extra space. -const BUFFER_SIZE: usize = MAX_WRITE_SIZE + 4096; - -#[derive(Default, Debug, Eq, PartialEq)] -/// How requests should be filtered based on the calling UID. -pub enum SessionACL { - /// Allow requests from any user. Corresponds to the `allow_other` mount option. - All, - /// Allow requests from root. Corresponds to the `allow_root` mount option. - RootAndOwner, - /// Allow requests from the owning UID. This is FUSE's default mode of operation. - #[default] - Owner, -} - -/// The session data structure -#[derive(Debug)] -pub struct Session { - /// Filesystem operation implementations - pub(crate) filesystem: FS, - /// Communication channel to the kernel driver - pub(crate) ch: Channel, - /// Handle to the mount. Dropping this unmounts. - mount: Arc>>, - /// Whether to restrict access to owner, root + owner, or unrestricted - /// Used to implement allow_root and auto_unmount - pub(crate) allowed: SessionACL, - /// User that launched the fuser process - pub(crate) session_owner: u32, - /// FUSE protocol major version - pub(crate) proto_major: u32, - /// FUSE protocol minor version - pub(crate) proto_minor: u32, - /// True if the filesystem is initialized (init operation done) - pub(crate) initialized: bool, - /// True if the filesystem was destroyed (destroy operation done) - pub(crate) destroyed: bool, -} - -impl AsFd for Session { - fn as_fd(&self) -> BorrowedFd<'_> { - self.ch.as_fd() - } -} - -impl Session { - /// Create a new session by mounting the given filesystem to the given mountpoint - pub fn new>( - filesystem: FS, - mountpoint: P, - options: &[MountOption], - ) -> io::Result> { - let mountpoint = mountpoint.as_ref(); - info!("Mounting {}", mountpoint.display()); - // If AutoUnmount is requested, but not AllowRoot or AllowOther we enforce the ACL - // ourself and implicitly set AllowOther because fusermount needs allow_root or allow_other - // to handle the auto_unmount option - let (file, mount) = if options.contains(&MountOption::AutoUnmount) - && !(options.contains(&MountOption::AllowRoot) - || options.contains(&MountOption::AllowOther)) - { - warn!("Given auto_unmount without allow_root or allow_other; adding allow_other, with userspace permission handling"); - let mut modified_options = options.to_vec(); - modified_options.push(MountOption::AllowOther); - Mount::new(mountpoint, &modified_options)? - } else { - Mount::new(mountpoint, options)? - }; - - let ch = Channel::new(file); - let allowed = if options.contains(&MountOption::AllowRoot) { - SessionACL::RootAndOwner - } else if options.contains(&MountOption::AllowOther) { - SessionACL::All - } else { - SessionACL::Owner - }; - - Ok(Session { - filesystem, - ch, - mount: Arc::new(Mutex::new(Some((mountpoint.to_owned(), mount)))), - allowed, - session_owner: geteuid().as_raw(), - proto_major: 0, - proto_minor: 0, - initialized: false, - destroyed: false, - }) - } - - /// Wrap an existing /dev/fuse file descriptor. This doesn't mount the - /// filesystem anywhere; that must be done separately. - pub fn from_fd(filesystem: FS, fd: OwnedFd, acl: SessionACL) -> Self { - let ch = Channel::new(Arc::new(fd.into())); - Session { - filesystem, - ch, - mount: Arc::new(Mutex::new(None)), - allowed: acl, - session_owner: geteuid().as_raw(), - proto_major: 0, - proto_minor: 0, - initialized: false, - destroyed: false, - } - } - - /// Run the session loop that receives kernel requests and dispatches them to method - /// calls into the filesystem. This read-dispatch-loop is non-concurrent to prevent - /// having multiple buffers (which take up much memory), but the filesystem methods - /// may run concurrent by spawning threads. - pub fn run(&mut self) -> io::Result<()> { - // Buffer for receiving requests from the kernel. Only one is allocated and - // it is reused immediately after dispatching to conserve memory and allocations. - let mut buffer = vec![0; BUFFER_SIZE]; - let buf = aligned_sub_buf( - buffer.deref_mut(), - std::mem::align_of::(), - ); - loop { - // Read the next request from the given channel to kernel driver - // The kernel driver makes sure that we get exactly one request per read - match self.ch.receive(buf) { - Ok(size) => match Request::new(self.ch.sender(), &buf[..size]) { - // Dispatch request - Some(req) => req.dispatch(self), - // Quit loop on illegal request - None => break, - }, - Err(err) => match err.raw_os_error() { - // Operation interrupted. Accordingly to FUSE, this is safe to retry - Some(ENOENT) => continue, - // Interrupted system call, retry - Some(EINTR) => continue, - // Explicitly try again - Some(EAGAIN) => continue, - // Filesystem was unmounted, quit the loop - Some(ENODEV) => break, - // Unhandled error - _ => return Err(err), - }, - } - } - Ok(()) - } - - /// Unmount the filesystem - pub fn unmount(&mut self) { - drop(std::mem::take(&mut *self.mount.lock().unwrap())); - } - - /// Returns a thread-safe object that can be used to unmount the Filesystem - pub fn unmount_callable(&mut self) -> SessionUnmounter { - SessionUnmounter { - mount: self.mount.clone(), - } - } - - /// Returns an object that can be used to send notifications to the kernel - #[cfg(feature = "abi-7-11")] - pub fn notifier(&self) -> Notifier { - Notifier::new(self.ch.sender()) - } -} - -#[derive(Debug)] -/// A thread-safe object that can be used to unmount a Filesystem -pub struct SessionUnmounter { - mount: Arc>>, -} - -impl SessionUnmounter { - /// Unmount the filesystem - pub fn unmount(&mut self) -> io::Result<()> { - drop(std::mem::take(&mut *self.mount.lock().unwrap())); - Ok(()) - } -} - -fn aligned_sub_buf(buf: &mut [u8], alignment: usize) -> &mut [u8] { - let off = alignment - (buf.as_ptr() as usize) % alignment; - if off == alignment { - buf - } else { - &mut buf[off..] - } -} - -impl Session { - /// Run the session loop in a background thread - pub fn spawn(self) -> io::Result { - BackgroundSession::new(self) - } -} - -impl Drop for Session { - fn drop(&mut self) { - if !self.destroyed { - self.filesystem.destroy(); - self.destroyed = true; - } - - if let Some((mountpoint, _mount)) = std::mem::take(&mut *self.mount.lock().unwrap()) { - info!("unmounting session at {}", mountpoint.display()); - } - } -} - -/// The background session data structure -pub struct BackgroundSession { - /// Thread guard of the background session - pub guard: JoinHandle>, - /// Object for creating Notifiers for client use - #[cfg(feature = "abi-7-11")] - sender: ChannelSender, - /// Ensures the filesystem is unmounted when the session ends - _mount: Option, -} - -impl BackgroundSession { - /// Create a new background session for the given session by running its - /// session loop in a background thread. If the returned handle is dropped, - /// the filesystem is unmounted and the given session ends. - pub fn new(se: Session) -> io::Result { - #[cfg(feature = "abi-7-11")] - let sender = se.ch.sender(); - // Take the fuse_session, so that we can unmount it - let mount = std::mem::take(&mut *se.mount.lock().unwrap()).map(|(_, mount)| mount); - let guard = thread::spawn(move || { - let mut se = se; - se.run() - }); - Ok(BackgroundSession { - guard, - #[cfg(feature = "abi-7-11")] - sender, - _mount: mount, - }) - } - /// Unmount the filesystem and wait for the request thread to exit. - pub fn unmount(self) -> io::Result<()> { - let Self { - guard, - #[cfg(feature = "abi-7-11")] - sender: _, - _mount, - } = self; - drop(_mount); - guard - .join() - .map_err(|_| io::Error::new(io::ErrorKind::Other, "FUSE request thread panicked"))? - } - - /// Unmount the filesystem and join the background thread. - pub fn join(self) { - self.unmount().unwrap(); - } - - /// Returns an object that can be used to send notifications to the kernel - #[cfg(feature = "abi-7-11")] - pub fn notifier(&self) -> Notifier { - Notifier::new(self.sender.clone()) - } -} - -// replace with #[derive(Debug)] if Debug ever gets implemented for -// thread_scoped::JoinGuard -impl fmt::Debug for BackgroundSession { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - write!(f, "BackgroundSession {{ guard: JoinGuard<()> }}",) - } -} diff --git a/vendor/fuser/tests/integration_tests.rs b/vendor/fuser/tests/integration_tests.rs deleted file mode 100644 index 0bbc84517..000000000 --- a/vendor/fuser/tests/integration_tests.rs +++ /dev/null @@ -1,28 +0,0 @@ -// No integration tests for non-Linux targets, so turn off the module for now. -#![cfg(target_os = "linux")] - -use fuser::{Filesystem, Session}; -use std::rc::Rc; -use std::thread; -use std::time::Duration; -use tempfile::TempDir; - -#[test] -#[cfg(target_os = "linux")] -fn unmount_no_send() { - struct NoSendFS( - // Rc to make this !Send - #[allow(dead_code)] Rc<()>, - ); - - impl Filesystem for NoSendFS {} - - let tmpdir: TempDir = tempfile::tempdir().unwrap(); - let mut session = Session::new(NoSendFS(Rc::new(())), tmpdir.path(), &[]).unwrap(); - let mut unmounter = session.unmount_callable(); - thread::spawn(move || { - thread::sleep(Duration::from_secs(1)); - unmounter.unmount().unwrap(); - }); - session.run().unwrap(); -} diff --git a/vendor/fuser/xfstests.Dockerfile b/vendor/fuser/xfstests.Dockerfile deleted file mode 100644 index 1cafecc10..000000000 --- a/vendor/fuser/xfstests.Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -FROM ubuntu:20.04 - -ARG DEBIAN_FRONTEND=noninteractive - -RUN apt update && apt install -y git build-essential autoconf curl cmake libfuse-dev pkg-config fuse bc libtool \ - uuid-dev xfslibs-dev libattr1-dev libacl1-dev libaio-dev attr acl quota bsdmainutils dbench psmisc - -RUN adduser --disabled-password --gecos '' fsgqa - -RUN echo 'user_allow_other' >> /etc/fuse.conf - -ADD rust-toolchain /code/fuser/rust-toolchain - -RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain=$(cat /code/fuser/rust-toolchain) - -ENV PATH=/root/.cargo/bin:$PATH - -RUN mkdir -p /code && cd /code && git clone https://github.com/fleetfs/fuse-xfstests && cd fuse-xfstests \ - && git checkout c123d014fcca48cf340be78d6712eff80ee4e8d6 && make - -ADD . /code/fuser/ - -RUN cd /code/fuser && cargo build --release --examples --features=abi-7-31 && cp target/release/examples/simple /bin/fuser diff --git a/vendor/fuser/xfstests.sh b/vendor/fuser/xfstests.sh deleted file mode 100755 index dd894bf82..000000000 --- a/vendor/fuser/xfstests.sh +++ /dev/null @@ -1,142 +0,0 @@ -#!/usr/bin/env bash - -set -ex - -exit_handler() { - exit "$XFSTESTS_EXIT_STATUS" -} -trap exit_handler TERM -trap "kill 0" INT EXIT - -export RUST_BACKTRACE=1 - -TEST_DATA_DIR=$(mktemp --directory) -SCRATCH_DATA_DIR=$(mktemp --directory) -TEST_DIR=$(mktemp --directory) -SCRATCH_DIR=$(mktemp --directory) - -set +e -# Clear mount log file, since the tests append to it -echo "" > /code/logs/xfstests_mount.log -DIR=/var/tmp/fuse-xfstests/check-fuser -mkdir -p $DIR -cd /code/fuse-xfstests - -# requires OFD & POSIX locks. OFD locks are not supported by fuse -echo "generic/478" >> xfs_excludes.txt - -# TODO: requires supporting orphaned files, that have an open file handle, but no links -echo "generic/484" >> xfs_excludes.txt - -# Writes directly to scratch block dev -echo "generic/062" >> xfs_excludes.txt - -# TODO: looks like it requires character file support -echo "generic/078" >> xfs_excludes.txt - -# TODO: takes > 10min -echo "generic/069" >> xfs_excludes.txt - -# TODO: needs fallocate which is missing from Linux FUSE driver (https://github.com/libfuse/libfuse/issues/395) -echo "generic/263" >> xfs_excludes.txt - -# TODO: Passes, but takes ~30min -echo "generic/127" >> xfs_excludes.txt - -# TODO: requires more complete falloc support. Also fills up the entire hard disk... -echo "generic/103" >> xfs_excludes.txt - -# TODO: requires support for mknod on character files -echo "generic/184" >> xfs_excludes.txt -echo "generic/401" >> xfs_excludes.txt - -# TODO: requires fifo support -echo "generic/423" >> xfs_excludes.txt -echo "generic/434" >> xfs_excludes.txt - -# TODO: requires ulimit support for limiting file size -echo "generic/394" >> xfs_excludes.txt - -# requires BSD lock support, and checks /proc/locks. fuse locks don't seem to show up in /proc/locks -echo "generic/504" >> xfs_excludes.txt - -# TODO: requires support for system.posix_acl_access xattr sync'ing to file permissions -# Some information about it linked from here: https://stackoverflow.com/questions/29569408/documentation-of-posix-acl-access-and-friends -echo "generic/099" >> xfs_excludes.txt -echo "generic/105" >> xfs_excludes.txt -echo "generic/375" >> xfs_excludes.txt - -# TODO: requires support for mounting read-only -echo "generic/294" >> xfs_excludes.txt -echo "generic/306" >> xfs_excludes.txt -echo "generic/452" >> xfs_excludes.txt - -# TODO: requires atime support -echo "generic/003" >> xfs_excludes.txt -echo "generic/192" >> xfs_excludes.txt - -# TODO: Passes, but takes ~10min and writes > 20GB. Needs support for writing files with large holes, -# for this test to be fast -echo "generic/130" >> xfs_excludes.txt - -# TODO: uses namespaces and inodes don't seem to get mapped properly -# this test ends up trying to chmod "/" (the root inode) -echo "generic/317" >> xfs_excludes.txt - -# TODO: requires more complete ACL support -echo "generic/319" >> xfs_excludes.txt -echo "generic/444" >> xfs_excludes.txt - -# TODO: Seems to cause a host OOM (even from inside Docker), when run with 84, 87, 88, 100, and 109 -echo "generic/089" >> xfs_excludes.txt - -# TODO: very slow. Passes, but takes > 30min -echo "generic/074" >> xfs_excludes.txt - -# TODO: very slow. Ran for > 3hrs without completing -echo "generic/339" >> xfs_excludes.txt - -# TODO: Passes, but takes ~60min on CI -echo "generic/006" >> xfs_excludes.txt -echo "generic/011" >> xfs_excludes.txt -echo "generic/070" >> xfs_excludes.txt - -# TODO: very slow. Passes, but takes 20min -echo "generic/438" >> xfs_excludes.txt - -# TODO: seems to crash host -echo "generic/476" >> xfs_excludes.txt - -# TODO: writing to /proc/sys/vm/drop_caches is not allowed inside Docker -echo "generic/086" >> xfs_excludes.txt -echo "generic/391" >> xfs_excludes.txt -echo "generic/426" >> xfs_excludes.txt -echo "generic/467" >> xfs_excludes.txt -echo "generic/477" >> xfs_excludes.txt - -# TODO: permission failure invoking FIBMAP -echo "generic/519" >> xfs_excludes.txt - -# TODO: Tries to create 50k+ files, which OOMs -echo "generic/531" >> xfs_excludes.txt - -# Test requires mounting a loopback device -echo "generic/564" >> xfs_excludes.txt - - -FUSER_EXTRA_MOUNT_OPTIONS="" TEST_DEV="$TEST_DATA_DIR" TEST_DIR="$TEST_DIR" SCRATCH_DEV="$SCRATCH_DATA_DIR" SCRATCH_MNT="$SCRATCH_DIR" \ -./check-fuser -E xfs_excludes.txt "$@" \ -| tee /code/logs/xfstests.log - -export XFSTESTS_EXIT_STATUS=${PIPESTATUS[0]} - -if [ $XFSTESTS_EXIT_STATUS ] -then - cat /code/fuse-xfstests/results/generic/*.bad - cp /code/fuse-xfstests/results/generic/*.bad /code/logs/ -fi - -rm -rf ${TEST_DATA_DIR} -rm -rf ${TEST_DIR} -rm -rf ${SCRATCH_DATA_DIR} -rm -rf ${SCRATCH_DIR} diff --git a/vendor/krun-devices/.cargo-ok b/vendor/krun-devices/.cargo-ok deleted file mode 100644 index 5f8b79583..000000000 --- a/vendor/krun-devices/.cargo-ok +++ /dev/null @@ -1 +0,0 @@ -{"v":1} \ No newline at end of file diff --git a/vendor/krun-devices/.cargo_vcs_info.json b/vendor/krun-devices/.cargo_vcs_info.json deleted file mode 100644 index 63110c816..000000000 --- a/vendor/krun-devices/.cargo_vcs_info.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "git": { - "sha1": "654e4a6045858d5ced90efae106e91d0eca3469f" - }, - "path_in_vcs": "src/devices" -} \ No newline at end of file diff --git a/vendor/krun-devices/Cargo.lock b/vendor/krun-devices/Cargo.lock deleted file mode 100644 index c246a0d1a..000000000 --- a/vendor/krun-devices/Cargo.lock +++ /dev/null @@ -1,1168 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "annotate-snippets" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4" -dependencies = [ - "anstyle", - "unicode-width", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "bincode" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" -dependencies = [ - "bincode_derive", - "unty", -] - -[[package]] -name = "bincode_derive" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" -dependencies = [ - "virtue", -] - -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "annotate-snippets", - "bitflags 2.11.0", - "cexpr", - "clang-sys", - "itertools", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" - -[[package]] -name = "caps" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd1ddba47aba30b6a889298ad0109c3b8dcb0e8fc993b459daa7067d46f865e0" -dependencies = [ - "libc", -] - -[[package]] -name = "cc" -version = "1.2.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom 7.1.3", -] - -[[package]] -name = "cfg-expr" -version = "0.20.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6b04e07d8080154ed4ac03546d9a2b303cc2fe1901ba0b35b301516e289368" -dependencies = [ - "smallvec", - "target-lexicon", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "convert_case" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "cookie-factory" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys", -] - -[[package]] -name = "fastrand" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "imago" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae7cfee876c698a1a2ed9c705ab18f21acbed82110f19b51cc458de73426fe2c" -dependencies = [ - "async-trait", - "bincode", - "cfg-if", - "libc", - "miniz_oxide", - "nix", - "page_size", - "rustc_version", - "tokio", - "tracing", - "windows-sys", -] - -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "krun-arch" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e408be4923e881a3fb6536322d569a845e55fb0f5af4a9af3202ed97becbb192" -dependencies = [ - "krun-arch-gen", - "krun-smbios", - "krun-utils", - "kvm-bindings", - "kvm-ioctls", - "libc", - "vm-memory", - "vmm-sys-util", -] - -[[package]] -name = "krun-arch-gen" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e903977e89dbb2f77008307ce9953281f681a099e647b79e9220169278ce1970" - -[[package]] -name = "krun-devices" -version = "0.1.0-1.19.3" -dependencies = [ - "bitflags 1.3.2", - "caps", - "crossbeam-channel", - "imago", - "krun-arch", - "krun-display", - "krun-hvf", - "krun-input", - "krun-polly", - "krun-rutabaga-gfx", - "krun-utils", - "kvm-bindings", - "kvm-ioctls", - "libc", - "libloading", - "log", - "lru", - "nix", - "persisting-overlay-core", - "pipewire", - "rand", - "tempfile", - "thiserror 2.0.18", - "virtio-bindings", - "vm-fdt", - "vm-memory", - "zerocopy", -] - -[[package]] -name = "krun-display" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e13751337ed633505118ddea0182350eb2d0dbfffca299da641fe36c50e8e93" -dependencies = [ - "bindgen", - "bitflags 2.11.0", - "log", - "static_assertions", - "thiserror 2.0.18", -] - -[[package]] -name = "krun-hvf" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f7e78f0c5431195ca36aded1886024872699a6a56f0a600f619aeb7ef2161bc" -dependencies = [ - "crossbeam-channel", - "krun-arch", - "libloading", - "log", -] - -[[package]] -name = "krun-input" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f67de5755f01ea31499764a4a850613e21ec209ad207b8e93156491e53c2d3" -dependencies = [ - "bindgen", - "bitflags 2.11.0", - "libc", - "log", - "static_assertions", - "thiserror 2.0.18", -] - -[[package]] -name = "krun-polly" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d2c61890357072d5751ef813aea744b93a67bfc3b820f36061f9706f72af84d" -dependencies = [ - "krun-utils", - "libc", -] - -[[package]] -name = "krun-rutabaga-gfx" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cb903397f5798f49d0aa8e481e54a79ef163e0621f1020a363e798081210991" -dependencies = [ - "anyhow", - "cfg-if", - "libc", - "log", - "nix", - "pkg-config", - "remain", - "thiserror 1.0.69", - "vmm-sys-util", - "winapi", - "zerocopy", -] - -[[package]] -name = "krun-smbios" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01462ad735097a9a9650564e7f7ba082db720a41696f94ec9fb56ecaf072c744" -dependencies = [ - "vm-memory", -] - -[[package]] -name = "krun-utils" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8315551f3fd0c86191ff14318ac595a1e62d9c1e0690d30355d3a4e0ac741c87" -dependencies = [ - "bitflags 1.3.2", - "crossbeam-channel", - "kvm-bindings", - "libc", - "log", - "nix", - "vmm-sys-util", -] - -[[package]] -name = "kvm-bindings" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a537873e15e8daabb416667e606d9b0abc2a8fb9a45bd5853b888ae0ead82f9" -dependencies = [ - "vmm-sys-util", -] - -[[package]] -name = "kvm-ioctls" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8f7370330b4f57981e300fa39b02088f2f2a5c2d0f1f994e8090589619c56d" -dependencies = [ - "bitflags 2.11.0", - "kvm-bindings", - "libc", - "vmm-sys-util", -] - -[[package]] -name = "libc" -version = "0.2.183" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" - -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link", -] - -[[package]] -name = "libspa" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6b8cfa2a7656627b4c92c6b9ef929433acd673d5ab3708cda1b18478ac00df4" -dependencies = [ - "bitflags 2.11.0", - "cc", - "convert_case", - "cookie-factory", - "libc", - "libspa-sys", - "nix", - "nom 8.0.0", - "system-deps", -] - -[[package]] -name = "libspa-sys" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "901049455d2eb6decf9058235d745237952f4804bc584c5fcb41412e6adcc6e0" -dependencies = [ - "bindgen", - "cc", - "system-deps", -] - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "lru" -version = "0.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" -dependencies = [ - "hashbrown", -] - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", -] - -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags 2.11.0", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "nom" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "page_size" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "persisting-overlay-core" -version = "0.2.0" -dependencies = [ - "libc", - "log", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pipewire" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9688b89abf11d756499f7c6190711d6dbe5a3acdb30c8fbf001d6596d06a8d44" -dependencies = [ - "anyhow", - "bitflags 2.11.0", - "libc", - "libspa", - "libspa-sys", - "nix", - "once_cell", - "pipewire-sys", - "thiserror 2.0.18", -] - -[[package]] -name = "pipewire-sys" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb028afee0d6ca17020b090e3b8fa2d7de23305aef975c7e5192a5050246ea36" -dependencies = [ - "bindgen", - "libspa-sys", - "system-deps", -] - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" - -[[package]] -name = "remain" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7ef12e84481ab4006cb942f8682bba28ece7270743e649442027c5db87df126" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags 2.11.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys", -] - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_spanned" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" -dependencies = [ - "serde_core", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "system-deps" -version = "7.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7" -dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml", - "version-compare", -] - -[[package]] -name = "target-lexicon" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" - -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom", - "once_cell", - "rustix", - "windows-sys", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio" -version = "1.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" -dependencies = [ - "pin-project-lite", -] - -[[package]] -name = "toml" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "toml_writer", - "winnow", -] - -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow", -] - -[[package]] -name = "toml_writer" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "unty" -version = "0.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" - -[[package]] -name = "version-compare" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" - -[[package]] -name = "virtio-bindings" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091f1f09cfbf2a78563b562e7a949465cce1aef63b6065645188d995162f8868" - -[[package]] -name = "virtue" -version = "0.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" - -[[package]] -name = "vm-fdt" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e21282841a059bb62627ce8441c491f09603622cd5a21c43bfedc85a2952f23" - -[[package]] -name = "vm-memory" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f39348a049689cabd3377cdd9182bf526ec76a6f823b79903896452e9d7a7380" -dependencies = [ - "libc", - "thiserror 2.0.18", - "winapi", -] - -[[package]] -name = "vmm-sys-util" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d21f366bf22bfba3e868349978766a965cbe628c323d58e026be80b8357ab789" -dependencies = [ - "bitflags 1.3.2", - "libc", -] - -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "winnow" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" - -[[package]] -name = "zerocopy" -version = "0.8.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/vendor/krun-devices/Cargo.toml b/vendor/krun-devices/Cargo.toml deleted file mode 100644 index 3f868032f..000000000 --- a/vendor/krun-devices/Cargo.toml +++ /dev/null @@ -1,182 +0,0 @@ -# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO -# -# When uploading crates to the registry Cargo will automatically -# "normalize" Cargo.toml files for maximal compatibility -# with all versions of Cargo and also rewrite `path` dependencies -# to registry (e.g., crates.io) dependencies. -# -# If you are reading this file be aware that the original Cargo.toml -# will likely look very different (and much more reasonable). -# See Cargo.toml.orig for the original contents. - -[package] -edition = "2021" -name = "krun-devices" -version = "0.1.0-1.19.3" -authors = ["The libkrun Authors"] -build = false -autolib = false -autobins = false -autoexamples = false -autotests = false -autobenches = false -description = "Virtual device emulation for libkrun" -readme = false -license = "Apache-2.0" -repository = "https://github.com/containers/libkrun" - -[features] -amd-sev = [ - "blk", - "tee", -] -aws-nitro = [] -blk = [] -efi = [ - "blk", - "net", -] -gpu = [ - "rutabaga_gfx", - "thiserror", - "zerocopy", - "krun_display", -] -input = [ - "zerocopy", - "krun_input", -] -net = [] -snd = [ - "pw", - "thiserror", -] -tdx = [ - "blk", - "tee", -] -tee = [] -test_utils = [] -virgl_resource_map2 = [] - -[lib] -name = "krun_devices" -path = "src/lib.rs" - -[dependencies.arch] -version = "=0.1.0-1.19.3" -package = "krun-arch" - -[dependencies.bitflags] -version = "1.2.0" - -[dependencies.crossbeam-channel] -version = ">=0.5.15" - -[dependencies.imago] -version = "0.2.2" -features = ["sync-wrappers"] - -[dependencies.krun_display] -version = "0.1.0" -features = ["bindgen_clang_runtime"] -optional = true -package = "krun-display" - -[dependencies.krun_input] -version = "0.1.0" -features = ["bindgen_clang_runtime"] -optional = true -package = "krun-input" - -[dependencies.libc] -version = ">=0.2.39" - -[dependencies.persisting-overlay-core] -path = "../../crates/persisting-overlay-core" - -[dependencies.libloading] -version = "0.8" - -[dependencies.log] -version = "0.4.0" - -[dependencies.nix] -version = "0.30.1" -features = [ - "ioctl", - "net", - "poll", - "socket", - "fs", -] - -[dependencies.polly] -version = "=0.1.0-1.19.3" -package = "krun-polly" - -[dependencies.pw] -version = "0.9.2" -optional = true -package = "pipewire" - -[dependencies.rand] -version = "0.9.2" - -[dependencies.rutabaga_gfx] -version = "=0.1.0-1.19.3" -features = [ - "virgl_renderer", - "virgl_renderer_next", -] -optional = true -package = "krun-rutabaga-gfx" - -[dependencies.thiserror] -version = "2.0" -optional = true - -[dependencies.utils] -version = "=0.1.0-1.19.3" -package = "krun-utils" - -[dependencies.virtio-bindings] -version = "0.2.0" - -[dependencies.vm-memory] -version = "=0.17.1" -features = ["backend-mmap"] - -[dependencies.zerocopy] -version = "0.8.26" -features = ["derive"] -optional = true - -[target.'cfg(any(target_arch = "aarch64", target_arch = "riscv64"))'.dependencies.vm-fdt] -version = ">= 0.2.0" - -[target.'cfg(target_os = "linux")'.dependencies.caps] -version = "0.5.5" - -[target.'cfg(target_os = "linux")'.dependencies.kvm-bindings] -version = "0.12" -features = ["fam-wrappers"] - -[target.'cfg(target_os = "linux")'.dependencies.kvm-ioctls] -version = "0.22" - -[target.'cfg(target_os = "linux")'.dependencies.rutabaga_gfx] -version = "=0.1.0-1.19.3" -features = ["x"] -optional = true -package = "krun-rutabaga-gfx" - -[target.'cfg(target_os = "macos")'.dependencies.hvf] -version = "=0.1.0-1.19.3" -package = "krun-hvf" - -[target.'cfg(target_os = "macos")'.dependencies.lru] -version = ">=0.9" - -[dev-dependencies] -tempfile = "3" diff --git a/vendor/krun-devices/Cargo.toml.orig b/vendor/krun-devices/Cargo.toml.orig deleted file mode 100644 index f13793553..000000000 --- a/vendor/krun-devices/Cargo.toml.orig +++ /dev/null @@ -1,58 +0,0 @@ -[package] -name = "krun-devices" -version = "0.1.0-1.19.3" -authors = ["The libkrun Authors"] -edition = "2021" - -description = "Virtual device emulation for libkrun" -license = "Apache-2.0" -repository = "https://github.com/containers/libkrun" - -[features] -tee = [] -amd-sev = ["blk", "tee"] -tdx = ["blk", "tee"] -net = [] -blk = [] -efi = ["blk", "net"] -gpu = ["rutabaga_gfx", "thiserror", "zerocopy", "krun_display"] -snd = ["pw", "thiserror"] -input = ["zerocopy", "krun_input"] -virgl_resource_map2 = [] -aws-nitro = [] -test_utils = [] - -[dependencies] -bitflags = "1.2.0" -crossbeam-channel = ">=0.5.15" -libc = ">=0.2.39" -libloading = "0.8" -log = "0.4.0" -nix = { version = "0.30.1", features = ["ioctl", "net", "poll", "socket", "fs"] } -pw = { package = "pipewire", version = "0.9.2", optional = true } -rand = "0.9.2" -thiserror = { version = "2.0", optional = true } -virtio-bindings = "0.2.0" -vm-memory = { version = "=0.17.1", features = ["backend-mmap"] } -zerocopy = { version = "0.8.26", optional = true, features = ["derive"] } -krun_display = { package = "krun-display", version = "0.1.0", path = "../display", optional = true, features = ["bindgen_clang_runtime"] } -krun_input = { package = "krun-input", version = "0.1.0", path = "../input", features = ["bindgen_clang_runtime"], optional = true } - -arch = { package = "krun-arch", version = "=0.1.0-1.19.3", path = "../arch" } -utils = { package = "krun-utils", version = "=0.1.0-1.19.3", path = "../utils" } -polly = { package = "krun-polly", version = "=0.1.0-1.19.3", path = "../polly" } -rutabaga_gfx = { package = "krun-rutabaga-gfx", version = "=0.1.0-1.19.3", path = "../rutabaga_gfx", features = ["virgl_renderer", "virgl_renderer_next"], optional = true } -imago = { version = "0.2.2", features = ["sync-wrappers"] } - -[target.'cfg(target_os = "macos")'.dependencies] -hvf = { package = "krun-hvf", version = "=0.1.0-1.19.3", path = "../hvf" } -lru = ">=0.9" - -[target.'cfg(target_os = "linux")'.dependencies] -rutabaga_gfx = { package = "krun-rutabaga-gfx", version = "=0.1.0-1.19.3", path = "../rutabaga_gfx", features = ["x"], optional = true } -caps = "0.5.5" -kvm-bindings = { version = "0.12", features = ["fam-wrappers"] } -kvm-ioctls = "0.22" - -[target.'cfg(any(target_arch = "aarch64", target_arch = "riscv64"))'.dependencies] -vm-fdt = ">= 0.2.0" diff --git a/vendor/krun-devices/src/bus.rs b/vendor/krun-devices/src/bus.rs deleted file mode 100644 index e47e13ac3..000000000 --- a/vendor/krun-devices/src/bus.rs +++ /dev/null @@ -1,287 +0,0 @@ -// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -// -// Portions Copyright 2017 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the THIRD-PARTY file. - -//! Handles routing to devices in an address space. - -use std::cmp::{Ord, Ordering, PartialEq, PartialOrd}; -use std::collections::btree_map::BTreeMap; -use std::fmt; -use std::io; -use std::result; -use std::sync::{Arc, Mutex}; - -use crate::virtio::AsAny; - -/// Trait for devices that respond to reads or writes in an arbitrary address space. -/// -/// The device does not care where it exists in address space as each method is only given an offset -/// into its allocated portion of address space. -#[allow(unused_variables)] -pub trait BusDevice: AsAny + Send { - /// Reads at `offset` from this device - fn read(&mut self, vcpuid: u64, offset: u64, data: &mut [u8]) {} - /// Writes at `offset` into this device - fn write(&mut self, vcpuid: u64, offset: u64, data: &[u8]) {} - /// Triggers the `irq_mask` interrupt on this device - fn interrupt(&self, irq_mask: u32) -> io::Result<()> { - Ok(()) - } -} - -#[derive(Debug)] -pub enum Error { - /// The insertion failed because the new device overlapped with an old device. - Overlap, -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use self::Error::*; - - match *self { - Overlap => write!(f, "New device overlaps with an old device."), - } - } -} - -pub type Result = result::Result; - -#[derive(Debug, Copy, Clone)] -struct BusRange(u64, u64); - -impl Eq for BusRange {} - -impl PartialEq for BusRange { - fn eq(&self, other: &BusRange) -> bool { - self.0 == other.0 - } -} - -impl Ord for BusRange { - fn cmp(&self, other: &BusRange) -> Ordering { - self.0.cmp(&other.0) - } -} - -impl PartialOrd for BusRange { - fn partial_cmp(&self, other: &BusRange) -> Option { - Some(self.cmp(other)) - } -} - -/// A device container for routing reads and writes over some address space. -/// -/// This doesn't have any restrictions on what kind of device or address space this applies to. The -/// only restriction is that no two devices can overlap in this address space. -#[derive(Clone, Default)] -pub struct Bus { - devices: BTreeMap>>, -} - -impl Bus { - /// Constructs an a bus with an empty address space. - pub fn new() -> Bus { - Bus { - devices: BTreeMap::new(), - } - } - - fn first_before(&self, addr: u64) -> Option<(BusRange, &Mutex)> { - // for when we switch to rustc 1.17: self.devices.range(..addr).iter().rev().next() - for (range, dev) in self.devices.iter().rev() { - if range.0 <= addr { - return Some((*range, dev)); - } - } - None - } - - pub fn get_device(&self, addr: u64) -> Option<(u64, &Mutex)> { - if let Some((BusRange(start, len), dev)) = self.first_before(addr) { - let offset = addr - start; - if offset < len { - return Some((offset, dev)); - } - } - None - } - - /// Puts the given device at the given address space. - pub fn insert(&mut self, device: Arc>, base: u64, len: u64) -> Result<()> { - if len == 0 { - return Err(Error::Overlap); - } - - // Reject all cases where the new device's base is within an old device's range. - if self.get_device(base).is_some() { - return Err(Error::Overlap); - } - - // The above check will miss an overlap in which the new device's base address is before the - // range of another device. To catch that case, we search for a device with a range before - // the new device's range's end. If there is no existing device in that range that starts - // after the new device, then there will be no overlap. - if let Some((BusRange(start, _), _)) = self.first_before(base + len - 1) { - // Such a device only conflicts with the new device if it also starts after the new - // device because of our initial `get_device` check above. - if start >= base { - return Err(Error::Overlap); - } - } - - if self.devices.insert(BusRange(base, len), device).is_some() { - return Err(Error::Overlap); - } - - Ok(()) - } - - /// Reads data from the device that owns the range containing `addr` and puts it into `data`. - /// - /// Returns true on success, otherwise `data` is untouched. - pub fn read(&self, vcpuid: u64, addr: u64, data: &mut [u8]) -> bool { - if let Some((offset, dev)) = self.get_device(addr) { - // OK to unwrap as lock() failing is a serious error condition and should panic. - dev.lock() - .expect("Failed to acquire device lock") - .read(vcpuid, offset, data); - true - } else { - false - } - } - - /// Writes `data` to the device that owns the range containing `addr`. - /// - /// Returns true on success, otherwise `data` is untouched. - pub fn write(&self, vcpuid: u64, addr: u64, data: &[u8]) -> bool { - if let Some((offset, dev)) = self.get_device(addr) { - // OK to unwrap as lock() failing is a serious error condition and should panic. - dev.lock() - .expect("Failed to acquire device lock") - .write(vcpuid, offset, data); - true - } else { - false - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - struct DummyDevice; - impl BusDevice for DummyDevice {} - - struct ConstantDevice; - impl BusDevice for ConstantDevice { - fn read(&mut self, _vcpuid: u64, offset: u64, data: &mut [u8]) { - for (i, v) in data.iter_mut().enumerate() { - *v = (offset as u8) + (i as u8); - } - } - - fn write(&mut self, _vcpuid: u64, offset: u64, data: &[u8]) { - for (i, v) in data.iter().enumerate() { - assert_eq!(*v, (offset as u8) + (i as u8)) - } - } - } - - #[test] - fn bus_insert() { - let mut bus = Bus::new(); - let dummy = Arc::new(Mutex::new(DummyDevice)); - // Insert len should not be 0. - assert!(bus.insert(dummy.clone(), 0x10, 0).is_err()); - assert!(bus.insert(dummy.clone(), 0x10, 0x10).is_ok()); - - let result = bus.insert(dummy.clone(), 0x0f, 0x10); - // This overlaps the address space of the existing bus device at 0x10. - assert!(result.is_err()); - assert_eq!(format!("{result:?}"), "Err(Overlap)"); - - // This overlaps the address space of the existing bus device at 0x10. - assert!(bus.insert(dummy.clone(), 0x10, 0x10).is_err()); - // This overlaps the address space of the existing bus device at 0x10. - assert!(bus.insert(dummy.clone(), 0x10, 0x15).is_err()); - // This overlaps the address space of the existing bus device at 0x10. - assert!(bus.insert(dummy.clone(), 0x12, 0x15).is_err()); - // This overlaps the address space of the existing bus device at 0x10. - assert!(bus.insert(dummy.clone(), 0x12, 0x01).is_err()); - // This overlaps the address space of the existing bus device at 0x10. - assert!(bus.insert(dummy.clone(), 0x0, 0x20).is_err()); - assert!(bus.insert(dummy.clone(), 0x20, 0x05).is_ok()); - assert!(bus.insert(dummy.clone(), 0x25, 0x05).is_ok()); - assert!(bus.insert(dummy, 0x0, 0x10).is_ok()); - } - - #[test] - fn bus_read_write() { - let mut bus = Bus::new(); - let dummy = Arc::new(Mutex::new(DummyDevice)); - assert!(bus.insert(dummy, 0x10, 0x10).is_ok()); - assert!(bus.read(0, 0x10, &mut [0, 0, 0, 0])); - assert!(bus.write(0, 0x10, &[0, 0, 0, 0])); - assert!(bus.read(0, 0x11, &mut [0, 0, 0, 0])); - assert!(bus.write(0, 0x11, &[0, 0, 0, 0])); - assert!(bus.read(0, 0x16, &mut [0, 0, 0, 0])); - assert!(bus.write(0, 0x16, &[0, 0, 0, 0])); - assert!(!bus.read(0, 0x20, &mut [0, 0, 0, 0])); - assert!(!bus.write(0, 0x20, &[0, 0, 0, 0])); - assert!(!bus.read(0, 0x06, &mut [0, 0, 0, 0])); - assert!(!bus.write(0, 0x06, &[0, 0, 0, 0])); - } - - #[test] - fn bus_read_write_values() { - let mut bus = Bus::new(); - let dummy = Arc::new(Mutex::new(ConstantDevice)); - assert!(bus.insert(dummy, 0x10, 0x10).is_ok()); - - let mut values = [0, 1, 2, 3]; - assert!(bus.read(0, 0x10, &mut values)); - assert_eq!(values, [0, 1, 2, 3]); - assert!(bus.write(0, 0x10, &values)); - assert!(bus.read(0, 0x15, &mut values)); - assert_eq!(values, [5, 6, 7, 8]); - assert!(bus.write(0, 0x15, &values)); - } - - #[test] - fn busrange_cmp_and_clone() { - assert_eq!(BusRange(0x10, 2), BusRange(0x10, 3)); - assert_eq!(BusRange(0x10, 2), BusRange(0x10, 2)); - - assert!(BusRange(0x10, 2) < BusRange(0x12, 1)); - assert!(BusRange(0x10, 2) < BusRange(0x12, 3)); - - let bus_range = BusRange(0x10, 2); - assert_eq!(bus_range, BusRange(0x10, 2)); - - let mut bus = Bus::new(); - let mut data = [1, 2, 3, 4]; - assert!(bus - .insert(Arc::new(Mutex::new(DummyDevice)), 0x10, 0x10) - .is_ok()); - assert!(bus.write(0, 0x10, &data)); - let bus_clone = bus.clone(); - assert!(bus.read(0, 0x10, &mut data)); - assert_eq!(data, [1, 2, 3, 4]); - assert!(bus_clone.read(0, 0x10, &mut data)); - assert_eq!(data, [1, 2, 3, 4]); - } - - #[test] - fn test_display_error() { - assert_eq!( - format!("{}", Error::Overlap), - "New device overlaps with an old device." - ); - } -} diff --git a/vendor/krun-devices/src/fdt/aarch64.rs b/vendor/krun-devices/src/fdt/aarch64.rs deleted file mode 100644 index a3135472a..000000000 --- a/vendor/krun-devices/src/fdt/aarch64.rs +++ /dev/null @@ -1,444 +0,0 @@ -// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -// -// Portions Copyright 2017 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the THIRD-PARTY file. - -use std::collections::HashMap; -use std::fmt::Debug; -use std::{io, result}; - -use crate::legacy::gic::GICDevice; -use crate::legacy::IrqChip; -use crate::DeviceType; -use arch::aarch64::layout::{GTIMER_HYP, GTIMER_PHYS, GTIMER_SEC, GTIMER_VIRT}; -use arch::{ArchMemoryInfo, InitrdConfig}; -use vm_fdt::{Error as FdtError, FdtWriter}; -use vm_memory::{Address, Bytes, GuestAddress, GuestMemoryError, GuestMemoryMmap}; - -// This is a value for uniquely identifying the FDT node declaring the interrupt controller. -const GIC_PHANDLE: u32 = 1; -// This is a value for uniquely identifying the FDT node containing the clock definition. -const CLOCK_PHANDLE: u32 = 2; -// This is a value for uniquely identifying the FDT node containing the gpio controller. -const GPIO_PHANDLE: u32 = 4; -// Read the documentation specified when appending the root node to the FDT. -const ADDRESS_CELLS: u32 = 0x2; -const SIZE_CELLS: u32 = 0x2; - -// System restart -const KEY_RESTART: u32 = 0x198; - -// As per kvm tool and -// https://www.kernel.org/doc/Documentation/devicetree/bindings/interrupt-controller/arm%2Cgic.txt -// Look for "The 1st cell..." -const GIC_FDT_IRQ_TYPE_SPI: u32 = 0; -const GIC_FDT_IRQ_TYPE_PPI: u32 = 1; - -// From https://elixir.bootlin.com/linux/v4.9.62/source/include/dt-bindings/interrupt-controller/irq.h#L17 -const IRQ_TYPE_EDGE_RISING: u32 = 1; -const IRQ_TYPE_LEVEL_HI: u32 = 4; - -/// Trait for devices to be added to the Flattened Device Tree. -pub trait DeviceInfoForFDT { - /// Returns the address where this device will be loaded. - fn addr(&self) -> u64; - /// Returns the associated interrupt for this device. - fn irq(&self) -> u32; - /// Returns the amount of memory that needs to be reserved for this device. - fn length(&self) -> u64; -} - -/// Errors thrown while configuring the Flattened Device Tree for aarch64. -#[derive(Debug)] -pub enum Error { - /// Creating FDT failed. - CreateFDT(FdtError), - /// Failure in calling syscall for terminating this FDT. - FinishFDTReserveMap(io::Error), - /// Failure in writing FDT in memory. - WriteFDTToMemory(GuestMemoryError), -} -type Result = result::Result; - -impl From for Error { - fn from(item: FdtError) -> Self { - Error::CreateFDT(item) - } -} - -/// Creates the flattened device tree for this aarch64 microVM. -pub fn create_fdt( - guest_mem: &GuestMemoryMmap, - arch_memory_info: &ArchMemoryInfo, - vcpu_mpidr: Vec, - cmdline: &str, - device_info: &HashMap<(DeviceType, String), T>, - gic_device: &IrqChip, - initrd: &Option, -) -> Result> { - // Alocate stuff necessary for the holding the blob. - let mut fdt = FdtWriter::new()?; - - // For an explanation why these nodes were introduced in the blob take a look at - // https://github.com/torvalds/linux/blob/master/Documentation/devicetree/booting-without-of.txt#L845 - // Look for "Required nodes and properties". - - // Header or the root node as per above mentioned documentation. - let root_node = fdt.begin_node("root")?; - fdt.property_string("compatible", "linux,dummy-virt")?; - // For info on #address-cells and size-cells read "Note about cells and address representation" - // from the above mentioned txt file. - fdt.property_u32("#address-cells", ADDRESS_CELLS)?; - fdt.property_u32("#size-cells", SIZE_CELLS)?; - // This is not mandatory but we use it to point the root node to the node - // containing description of the interrupt controller for this VM. - fdt.property_u32("interrupt-parent", GIC_PHANDLE)?; - create_cpu_nodes(&mut fdt, &vcpu_mpidr)?; - create_memory_node(&mut fdt, guest_mem, arch_memory_info)?; - create_chosen_node(&mut fdt, cmdline, initrd, device_info)?; - create_gic_node(&mut fdt, gic_device)?; - create_timer_node(&mut fdt)?; - create_clock_node(&mut fdt)?; - create_psci_node(&mut fdt)?; - create_devices_node(&mut fdt, device_info)?; - - // End Header node. - fdt.end_node(root_node)?; - - // Allocate another buffer so we can format and then write fdt to guest. - let fdt_final = fdt.finish()?; - - // Write FDT to memory. - let fdt_address = GuestAddress(arch_memory_info.fdt_addr); - guest_mem - .write_slice(fdt_final.as_slice(), fdt_address) - .map_err(Error::WriteFDTToMemory)?; - Ok(fdt_final) -} - -// Auxiliary functions for writing u32/u64 numbers in big endian order. -fn to_be32(input: u32) -> [u8; 4] { - u32::to_be_bytes(input) -} - -fn to_be64(input: u64) -> [u8; 8] { - u64::to_be_bytes(input) -} - -// Helper functions for generating a properly formatted byte vector using 32-bit/64-bit cells. -fn generate_prop32(cells: &[u32]) -> Vec { - let mut ret: Vec = Vec::new(); - for &e in cells { - ret.extend(to_be32(e).iter()); - } - ret -} - -fn generate_prop64(cells: &[u64]) -> Vec { - let mut ret: Vec = Vec::new(); - for &e in cells { - ret.extend(to_be64(e).iter()); - } - ret -} - -// Following are the auxiliary function for creating the different nodes that we append to our FDT. -fn create_cpu_nodes(fdt: &mut FdtWriter, vcpu_mpidr: &[u64]) -> Result<()> { - // See https://github.com/torvalds/linux/blob/master/Documentation/devicetree/bindings/arm/cpus.yaml. - let cpu_node = fdt.begin_node("cpus")?; - // As per documentation, on ARM v8 64-bit systems value should be set to 2. - fdt.property_u32("#address-cells", 0x02)?; - fdt.property_u32("#size-cells", 0x0)?; - let num_cpus = vcpu_mpidr.len(); - - for (index, mpidr) in vcpu_mpidr.iter().enumerate() { - let cpu_name = format!("cpu@{index:x}"); - let cpu_name_node = fdt.begin_node(&cpu_name)?; - fdt.property_string("device_type", "cpu")?; - fdt.property_string("compatible", "arm,arm-v8")?; - if num_cpus > 1 { - // This is required on armv8 64-bit. See aforementioned documentation. - fdt.property_string("enable-method", "psci")?; - } - // Set the field to first 24 bits of the MPIDR - Multiprocessor Affinity Register. - // See http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0488c/BABHBJCI.html. - fdt.property_u64("reg", mpidr & 0x7FFFFF)?; - fdt.end_node(cpu_name_node)?; - } - fdt.end_node(cpu_node)?; - Ok(()) -} - -fn create_memory_node( - fdt: &mut FdtWriter, - _guest_mem: &GuestMemoryMmap, - arch_memory_info: &ArchMemoryInfo, -) -> Result<()> { - let mem_size = arch_memory_info.ram_last_addr - arch_memory_info.ram_start_addr; - // See https://github.com/torvalds/linux/blob/master/Documentation/devicetree/booting-without-of.txt#L960 - // for an explanation of this. - let mem_reg_prop = generate_prop64(&[arch_memory_info.ram_start_addr, mem_size]); - - let mem_node = fdt.begin_node("memory")?; - fdt.property_string("device_type", "memory")?; - fdt.property("reg", &mem_reg_prop)?; - fdt.end_node(mem_node)?; - Ok(()) -} - -fn create_chosen_node( - fdt: &mut FdtWriter, - cmdline: &str, - initrd: &Option, - dev_info: &HashMap<(DeviceType, String), T>, -) -> Result<()> { - let chosen_node = fdt.begin_node("chosen")?; - fdt.property_string("bootargs", cmdline)?; - - // If we have a legacy serial device, tell the guest this is the default console. - // Clever guests will still switch to a better console (like virtio-console) if - // it becomes available later, and this gives us a good fallback. - for ((device_type, _device_id), info) in dev_info { - if device_type == &DeviceType::Serial { - fdt.property_string("stdout-path", &format!("/uart@{:x}", info.addr()))?; - } - } - - if let Some(initrd_config) = initrd { - fdt.property_u64("linux,initrd-start", initrd_config.address.raw_value())?; - fdt.property_u64( - "linux,initrd-end", - initrd_config.address.raw_value() + initrd_config.size as u64, - )?; - } - - fdt.end_node(chosen_node)?; - - Ok(()) -} - -fn create_gic_node(fdt: &mut FdtWriter, gic_device: &IrqChip) -> Result<()> { - let gic_device = gic_device.lock().unwrap(); - let gic_reg_prop = generate_prop64(&gic_device.device_properties()); - - let intc_node = fdt.begin_node("intc")?; - fdt.property_string("compatible", &gic_device.fdt_compatibility())?; - fdt.property_null("interrupt-controller")?; - // "interrupt-cells" field specifies the number of cells needed to encode an - // interrupt source. The type shall be a and the value shall be 3 if no PPI affinity description - // is required. - fdt.property_u32("#interrupt-cells", 3)?; - fdt.property("reg", &gic_reg_prop)?; - fdt.property_u32("phandle", GIC_PHANDLE)?; - fdt.property_u32("#address-cells", 2)?; - fdt.property_u32("#size-cells", 2)?; - fdt.property_null("ranges")?; - let gic_intr = [ - GIC_FDT_IRQ_TYPE_PPI, - gic_device.fdt_maint_irq(), - IRQ_TYPE_LEVEL_HI, - ]; - let gic_intr_prop = generate_prop32(&gic_intr); - - fdt.property("interrupts", &gic_intr_prop)?; - fdt.end_node(intc_node)?; - - Ok(()) -} - -fn create_clock_node(fdt: &mut FdtWriter) -> Result<()> { - // The Advanced Peripheral Bus (APB) is part of the Advanced Microcontroller Bus Architecture - // (AMBA) protocol family. It defines a low-cost interface that is optimized for minimal power - // consumption and reduced interface complexity. - // PCLK is the clock source and this node defines exactly the clock for the APB. - let node = fdt.begin_node("apb-pclk")?; - fdt.property_string("compatible", "fixed-clock")?; - fdt.property_u32("#clock-cells", 0x0)?; - fdt.property_u32("clock-frequency", 24000000)?; - fdt.property_string("clock-output-names", "clk24mhz")?; - fdt.property_u32("phandle", CLOCK_PHANDLE)?; - fdt.end_node(node)?; - - Ok(()) -} - -fn create_timer_node(fdt: &mut FdtWriter) -> Result<()> { - // See - // https://github.com/torvalds/linux/blob/master/Documentation/devicetree/bindings/interrupt-controller/arch_timer.txt - // These are fixed interrupt numbers for the timer device. - let irqs = [GTIMER_SEC, GTIMER_HYP, GTIMER_VIRT, GTIMER_PHYS]; - let compatible = "arm,armv8-timer"; - - let mut timer_reg_cells: Vec = Vec::new(); - for &irq in irqs.iter() { - timer_reg_cells.push(GIC_FDT_IRQ_TYPE_PPI); - timer_reg_cells.push(irq); - timer_reg_cells.push(IRQ_TYPE_LEVEL_HI); - } - let timer_reg_prop = generate_prop32(timer_reg_cells.as_slice()); - - let node = fdt.begin_node("timer")?; - fdt.property_string("compatible", compatible)?; - fdt.property_null("always-on")?; - fdt.property("interrupts", &timer_reg_prop)?; - fdt.end_node(node)?; - - Ok(()) -} - -fn create_psci_node(fdt: &mut FdtWriter) -> Result<()> { - let compatible = "arm,psci-0.2"; - let node = fdt.begin_node("psci")?; - fdt.property_string("compatible", compatible)?; - // Two methods available: hvc and smc. - // As per documentation, PSCI calls between a guest and hypervisor may use the HVC conduit instead of SMC. - // So, since we are using kvm, we need to use hvc. - #[cfg(target_os = "linux")] - fdt.property_string("method", "hvc")?; - #[cfg(target_os = "macos")] - fdt.property_string("method", "smc")?; - fdt.end_node(node)?; - - Ok(()) -} - -fn create_virtio_node( - fdt: &mut FdtWriter, - dev_info: &T, -) -> Result<()> { - let device_reg_prop = generate_prop64(&[dev_info.addr(), dev_info.length()]); - #[cfg(target_os = "linux")] - let irq = generate_prop32(&[GIC_FDT_IRQ_TYPE_SPI, dev_info.irq(), IRQ_TYPE_EDGE_RISING]); - #[cfg(target_os = "macos")] - let irq = generate_prop32(&[ - GIC_FDT_IRQ_TYPE_SPI, - dev_info.irq() - 32, - IRQ_TYPE_EDGE_RISING, - ]); - - let virtio_node = fdt.begin_node(&format!("virtio_mmio@{:x}", dev_info.addr()))?; - fdt.property_string("compatible", "virtio,mmio")?; - fdt.property("reg", &device_reg_prop)?; - fdt.property("interrupts", &irq)?; - fdt.property_u32("interrupt-parent", GIC_PHANDLE)?; - fdt.end_node(virtio_node)?; - - Ok(()) -} - -fn create_serial_node( - fdt: &mut FdtWriter, - dev_info: &T, -) -> Result<()> { - let serial_reg_prop = generate_prop64(&[dev_info.addr(), dev_info.length()]); - #[cfg(target_os = "linux")] - let irq = generate_prop32(&[GIC_FDT_IRQ_TYPE_SPI, dev_info.irq(), IRQ_TYPE_EDGE_RISING]); - #[cfg(target_os = "macos")] - let irq = generate_prop32(&[ - GIC_FDT_IRQ_TYPE_SPI, - dev_info.irq() - 32, - IRQ_TYPE_EDGE_RISING, - ]); - - let node = fdt.begin_node(&format!("uart@{:x}", dev_info.addr()))?; - fdt.property_string("compatible", "arm,pl011")?; - fdt.property_string("status", "okay")?; - fdt.property("reg", &serial_reg_prop)?; - fdt.property_u32("clocks", CLOCK_PHANDLE)?; - fdt.property_string("clock-names", "apb_pclk")?; - fdt.property("interrupts", &irq)?; - fdt.end_node(node)?; - - Ok(()) -} - -fn create_rtc_node( - fdt: &mut FdtWriter, - dev_info: &T, -) -> Result<()> { - let compatible = b"arm,pl031\0arm,primecell\0"; - let rtc_reg_prop = generate_prop64(&[dev_info.addr(), dev_info.length()]); - #[cfg(target_os = "linux")] - let irq = generate_prop32(&[GIC_FDT_IRQ_TYPE_SPI, dev_info.irq(), IRQ_TYPE_LEVEL_HI]); - #[cfg(target_os = "macos")] - let irq = generate_prop32(&[GIC_FDT_IRQ_TYPE_SPI, dev_info.irq() - 32, IRQ_TYPE_LEVEL_HI]); - let rtc_node = fdt.begin_node(&format!("rtc@{:x}", dev_info.addr()))?; - fdt.property("compatible", compatible)?; - fdt.property("reg", &rtc_reg_prop)?; - fdt.property("interrupts", &irq)?; - fdt.property_u32("clocks", CLOCK_PHANDLE)?; - fdt.property_string("clock-names", "apb_pclk")?; - fdt.end_node(rtc_node)?; - - Ok(()) -} - -fn create_gpio_node( - fdt: &mut FdtWriter, - dev_info: &T, -) -> Result<()> { - let compatible = b"arm,pl061\0arm,primecell\0"; - - let gpio_reg_prop = generate_prop64(&[dev_info.addr(), dev_info.length()]); - let irq = generate_prop32(&[ - GIC_FDT_IRQ_TYPE_SPI, - dev_info.irq() - 32, - IRQ_TYPE_EDGE_RISING, - ]); - - let gpio_node = fdt.begin_node(&format!("pl061@{:x}", dev_info.addr()))?; - fdt.property("compatible", compatible)?; - - fdt.property("reg", &gpio_reg_prop)?; - fdt.property("interrupts", &irq)?; - fdt.property_null("gpio-controller")?; - fdt.property_u32("#gpio-cells", 2)?; - fdt.property_u32("clocks", CLOCK_PHANDLE)?; - fdt.property_string("clock-names", "apb_pclk")?; - fdt.property_u32("phandle", GPIO_PHANDLE)?; - fdt.end_node(gpio_node)?; - - // gpio-keys node - let gpio_keys_node = fdt.begin_node("gpio-keys")?; - fdt.property_string("compatible", "gpio-keys")?; - fdt.property_u32("#size-cells", 0)?; - fdt.property_u32("#address-cells", 1)?; - let gpio_keys_poweroff_node = fdt.begin_node("button@1")?; - fdt.property_string("label", "GPIO Key Poweroff")?; - fdt.property_u32("linux,code", KEY_RESTART)?; - let gpios = [GPIO_PHANDLE, 3, 0]; - fdt.property_array_u32("gpios", &gpios)?; - fdt.end_node(gpio_keys_poweroff_node)?; - fdt.end_node(gpio_keys_node)?; - - Ok(()) -} - -fn create_devices_node( - fdt: &mut FdtWriter, - dev_info: &HashMap<(DeviceType, String), T>, -) -> Result<()> { - // Create one temp Vec to store all virtio devices - let mut ordered_virtio_device: Vec<&T> = Vec::new(); - - for ((device_type, _device_id), info) in dev_info { - match device_type { - DeviceType::Gpio => create_gpio_node(fdt, info)?, - DeviceType::RTC => create_rtc_node(fdt, info)?, - DeviceType::Serial => create_serial_node(fdt, info)?, - DeviceType::Virtio(_) => { - ordered_virtio_device.push(info); - } - } - } - - // Sort out virtio devices by address from low to high and insert them into fdt table. - ordered_virtio_device.sort_by_key(|a| a.addr()); - for ordered_device_info in ordered_virtio_device.drain(..) { - create_virtio_node(fdt, ordered_device_info)?; - } - - Ok(()) -} diff --git a/vendor/krun-devices/src/fdt/mod.rs b/vendor/krun-devices/src/fdt/mod.rs deleted file mode 100644 index 43a81999d..000000000 --- a/vendor/krun-devices/src/fdt/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2025, Institute of Software, CAS. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -#[cfg(target_arch = "aarch64")] -pub mod aarch64; -#[cfg(target_arch = "aarch64")] -pub use aarch64::*; - -#[cfg(target_arch = "riscv64")] -pub mod riscv64; -#[cfg(target_arch = "riscv64")] -pub use riscv64::*; diff --git a/vendor/krun-devices/src/fdt/riscv64.rs b/vendor/krun-devices/src/fdt/riscv64.rs deleted file mode 100644 index 9d6a5a41b..000000000 --- a/vendor/krun-devices/src/fdt/riscv64.rs +++ /dev/null @@ -1,286 +0,0 @@ -// Copyright 2025 The libkrun Authors. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -use std::collections::HashMap; -use std::fmt::Debug; -use std::{io, result}; - -use crate::legacy::aia::AIADevice; -use crate::legacy::IrqChip; -use crate::DeviceType; -use arch::riscv64::get_fdt_addr; -use arch::riscv64::layout::IRQ_BASE; -use arch::{ArchMemoryInfo, InitrdConfig}; -use vm_fdt::{Error as FdtError, FdtWriter}; -use vm_memory::{Address, Bytes, GuestAddress, GuestMemoryError, GuestMemoryMmap}; - -const AIA_APLIC_PHANDLE: u32 = 1; -const AIA_IMSIC_PHANDLE: u32 = 2; -const CPU_INTC_BASE_PHANDLE: u32 = 3; -const CPU_BASE_PHANDLE: u32 = 256 + CPU_INTC_BASE_PHANDLE; -// Read the documentation specified when appending the root node to the FDT. -const ADDRESS_CELLS: u32 = 0x2; -const SIZE_CELLS: u32 = 0x2; - -// From https://elixir.bootlin.com/linux/v6.10/source/include/dt-bindings/interrupt-controller/irq.h#L14 -const _IRQ_TYPE_EDGE_RISING: u32 = 1; -const IRQ_TYPE_LEVEL_HI: u32 = 4; - -const S_MODE_EXT_IRQ: u32 = 9; - -/// Trait for devices to be added to the Flattened Device Tree. -pub trait DeviceInfoForFDT { - /// Returns the address where this device will be loaded. - fn addr(&self) -> u64; - /// Returns the associated interrupt for this device. - fn irq(&self) -> u32; - /// Returns the amount of memory that needs to be reserved for this device. - fn length(&self) -> u64; -} - -/// Errors thrown while configuring the Flattened Device Tree for aarch64. -#[derive(Debug)] -pub enum Error { - /// Creating FDT failed. - CreateFDT(FdtError), - /// Failure in calling syscall for terminating this FDT. - FinishFDTReserveMap(io::Error), - /// Failure in writing FDT in memory. - WriteFDTToMemory(GuestMemoryError), -} -type Result = result::Result; - -impl From for Error { - fn from(item: FdtError) -> Self { - Error::CreateFDT(item) - } -} - -/// Creates the flattened device tree for this riscv64 VM. -pub fn create_fdt( - guest_mem: &GuestMemoryMmap, - arch_memory_info: &ArchMemoryInfo, - num_vcpu: u32, - cmdline: &str, - device_info: &HashMap<(DeviceType, String), T>, - aia_device: &IrqChip, - initrd: &Option, -) -> Result> { - // Allocate stuff necessary for the holding the blob. - let mut fdt = FdtWriter::new()?; - - // For an explanation why these nodes were introduced in the blob take a look at - // https://github.com/torvalds/linux/blob/master/Documentation/devicetree/booting-without-of.txt#L845 - // Look for "Required nodes and properties". - - // Header or the root node as per above mentioned documentation. - let root_node = fdt.begin_node("root")?; - fdt.property_string("compatible", "linux,dummy-virt")?; - // For info on #address-cells and size-cells resort to Table 3.1 Root Node - // Properties - fdt.property_u32("#address-cells", ADDRESS_CELLS)?; - fdt.property_u32("#size-cells", SIZE_CELLS)?; - create_cpu_nodes(&mut fdt, num_vcpu)?; - create_memory_node(&mut fdt, guest_mem, arch_memory_info)?; - create_chosen_node(&mut fdt, cmdline, initrd)?; - create_aia_node(&mut fdt, aia_device)?; - create_devices_node(&mut fdt, device_info)?; - - // End Header node. - fdt.end_node(root_node)?; - - // Allocate another buffer so we can format and then write fdt to guest. - let fdt_final = fdt.finish()?; - - // Write FDT to memory. - let fdt_address = GuestAddress(get_fdt_addr(guest_mem)); - guest_mem - .write_slice(fdt_final.as_slice(), fdt_address) - .map_err(Error::WriteFDTToMemory)?; - Ok(fdt_final) -} - -// Following are the auxiliary function for creating the different nodes that we append to our FDT. -fn create_cpu_nodes(fdt: &mut FdtWriter, num_cpus: u32) -> Result<()> { - // See https://elixir.bootlin.com/linux/v6.10/source/Documentation/devicetree/bindings/riscv/cpus.yaml - let cpus = fdt.begin_node("cpus")?; - // As per documentation, on RISC-V 64-bit systems value should be set to 1. - fdt.property_u32("#address-cells", 0x01)?; - fdt.property_u32("#size-cells", 0x0)?; - fdt.property_u32("timebase-frequency", 0x989680)?; - - for cpu_index in 0..num_cpus { - let cpu = fdt.begin_node(&format!("cpu@{cpu_index:x}"))?; - fdt.property_string("device_type", "cpu")?; - fdt.property_string("compatible", "riscv")?; - fdt.property_string("mmu-type", "sv48")?; - fdt.property_string("riscv,isa", "rv64imafdc_smaia_ssaia")?; - fdt.property_string("status", "okay")?; - fdt.property_u32("reg", cpu_index)?; - fdt.property_u32("phandle", CPU_BASE_PHANDLE + cpu_index)?; - - // interrupt controller node - let intc_node = fdt.begin_node("interrupt-controller")?; - fdt.property_string("compatible", "riscv,cpu-intc")?; - fdt.property_u32("#interrupt-cells", 1u32)?; - fdt.property_null("interrupt-controller")?; - fdt.property_u32("phandle", CPU_INTC_BASE_PHANDLE + cpu_index)?; - fdt.end_node(intc_node)?; - - fdt.end_node(cpu)?; - } - fdt.end_node(cpus)?; - Ok(()) -} - -fn create_memory_node( - fdt: &mut FdtWriter, - _guest_mem: &GuestMemoryMmap, - arch_memory_info: &ArchMemoryInfo, -) -> Result<()> { - let mem_size = arch_memory_info.ram_last_addr - arch::riscv64::layout::DRAM_MEM_START; - // See https://github.com/torvalds/linux/blob/master/Documentation/devicetree/booting-without-of.txt#L960 - // for an explanation of this. - let mem_reg_prop = [arch::riscv64::layout::DRAM_MEM_START, mem_size]; - - let mem_node = fdt.begin_node("memory")?; - fdt.property_string("device_type", "memory")?; - fdt.property_array_u64("reg", &mem_reg_prop)?; - fdt.end_node(mem_node)?; - Ok(()) -} - -fn create_chosen_node( - fdt: &mut FdtWriter, - cmdline: &str, - initrd: &Option, -) -> Result<()> { - let chosen_node = fdt.begin_node("chosen")?; - fdt.property_string("bootargs", cmdline)?; - - if let Some(initrd_config) = initrd { - fdt.property_u64("linux,initrd-start", initrd_config.address.raw_value())?; - fdt.property_u64( - "linux,initrd-end", - initrd_config.address.raw_value() + initrd_config.size as u64, - )?; - } - - fdt.end_node(chosen_node)?; - - Ok(()) -} - -fn create_aia_node(fdt: &mut FdtWriter, aia_device: &IrqChip) -> Result<()> { - // IMSIC - if aia_device.lock().unwrap().msi_compatible() { - use arch::riscv64::layout::IMSIC_START; - let imsic_name = format!("imsics@{IMSIC_START:x}"); - let imsic_node = fdt.begin_node(&imsic_name)?; - - fdt.property_string( - "compatible", - aia_device.lock().unwrap().imsic_compatibility(), - )?; - let imsic_reg_prop = aia_device.lock().unwrap().imsic_properties(); - fdt.property_array_u32("reg", &imsic_reg_prop)?; - fdt.property_u32("#interrupt-cells", 0u32)?; - fdt.property_null("interrupt-controller")?; - fdt.property_null("msi-controller")?; - // TODO complete num-ids - fdt.property_u32("riscv,num-ids", 2047u32)?; - fdt.property_u32("phandle", AIA_IMSIC_PHANDLE)?; - - let mut irq_cells = Vec::new(); - let num_cpus = aia_device.lock().unwrap().vcpu_count(); - for i in 0..num_cpus { - irq_cells.push(CPU_INTC_BASE_PHANDLE + i); - irq_cells.push(S_MODE_EXT_IRQ); - } - fdt.property_array_u32("interrupts-extended", &irq_cells)?; - - fdt.end_node(imsic_node)?; - } - - // APLIC - use arch::riscv64::layout::APLIC_START; - let aplic_name = format!("aplic@{APLIC_START:x}"); - let aplic_node = fdt.begin_node(&aplic_name)?; - - fdt.property_string( - "compatible", - aia_device.lock().unwrap().aplic_compatibility(), - )?; - let reg_cells = aia_device.lock().unwrap().aplic_properties(); - fdt.property_array_u32("reg", ®_cells)?; - fdt.property_u32("#interrupt-cells", 2u32)?; - fdt.property_null("interrupt-controller")?; - fdt.property_u32("riscv,num-sources", 96u32)?; - fdt.property_u32("phandle", AIA_APLIC_PHANDLE)?; - fdt.property_u32("msi-parent", AIA_IMSIC_PHANDLE)?; - - fdt.end_node(aplic_node)?; - - Ok(()) -} - -fn create_virtio_node( - fdt: &mut FdtWriter, - dev_info: &T, -) -> Result<()> { - let device_reg_prop = [dev_info.addr(), dev_info.length()]; - #[cfg(target_os = "linux")] - let irq = [dev_info.irq() - IRQ_BASE, IRQ_TYPE_LEVEL_HI]; - - let virtio_node = fdt.begin_node(&format!("virtio_mmio@{:x}", dev_info.addr()))?; - fdt.property_string("compatible", "virtio,mmio")?; - fdt.property_array_u64("reg", &device_reg_prop)?; - fdt.property_array_u32("interrupts", &irq)?; - fdt.property_u32("interrupt-parent", AIA_APLIC_PHANDLE)?; - fdt.end_node(virtio_node)?; - - Ok(()) -} - -fn create_serial_node( - fdt: &mut FdtWriter, - dev_info: &T, -) -> Result<()> { - let serial_reg_prop = [dev_info.addr(), dev_info.length()]; - let irq = [dev_info.irq() - IRQ_BASE, IRQ_TYPE_LEVEL_HI]; - - let serial_node = fdt.begin_node(&format!("serial@{:x}", dev_info.addr()))?; - fdt.property_string("compatible", "ns16550a")?; - fdt.property_array_u64("reg", &serial_reg_prop)?; - fdt.property_u32("clock-frequency", 3686400)?; - fdt.property_u32("interrupt-parent", AIA_APLIC_PHANDLE)?; - fdt.property_array_u32("interrupts", &irq)?; - fdt.end_node(serial_node)?; - - Ok(()) -} - -fn create_devices_node( - fdt: &mut FdtWriter, - dev_info: &HashMap<(DeviceType, String), T>, -) -> Result<()> { - // Create one temp Vec to store all virtio devices - let mut ordered_virtio_device: Vec<&T> = Vec::new(); - - for ((device_type, _device_id), info) in dev_info { - match device_type { - DeviceType::Serial => create_serial_node(fdt, info)?, - DeviceType::Virtio(_) => { - ordered_virtio_device.push(info); - } - } - } - - // Sort out virtio devices by address from low to high and insert them into fdt table. - ordered_virtio_device.sort_by_key(|a| a.addr()); - for ordered_device_info in ordered_virtio_device.drain(..) { - create_virtio_node(fdt, ordered_device_info)?; - } - - Ok(()) -} diff --git a/vendor/krun-devices/src/legacy/aarch64/gpio.rs b/vendor/krun-devices/src/legacy/aarch64/gpio.rs deleted file mode 100644 index 7f9c32174..000000000 --- a/vendor/krun-devices/src/legacy/aarch64/gpio.rs +++ /dev/null @@ -1,258 +0,0 @@ -// Copyright 2021 Arm Limited (or its affiliates). All rights reserved. -// -// SPDX-License-Identifier: Apache-2.0 - -//! ARM PrimeCell General Purpose Input/Output(PL061) -//! -//! This module implements an ARM PrimeCell General Purpose Input/Output(PL061) to support gracefully poweroff microvm from external. -//! - -use std::fmt; -use std::os::fd::AsRawFd; -use std::result; - -use polly::event_manager::{EventManager, Subscriber}; -use utils::byte_order::{read_le_u32, write_le_u32}; -use utils::epoll::{EpollEvent, EventSet}; -use utils::eventfd::EventFd; - -use crate::bus::BusDevice; -use crate::legacy::IrqChip; - -const OFS_DATA: u64 = 0x400; // Data Register -const GPIODIR: u64 = 0x400; // Direction Register -const GPIOIS: u64 = 0x404; // Interrupt Sense Register -const GPIOIBE: u64 = 0x408; // Interrupt Both Edges Register -const GPIOIEV: u64 = 0x40c; // Interrupt Event Register -const GPIOIE: u64 = 0x410; // Interrupt Mask Register -const GPIORIE: u64 = 0x414; // Raw Interrupt Status Register -const GPIOMIS: u64 = 0x418; // Masked Interrupt Status Register -const GPIOIC: u64 = 0x41c; // Interrupt Clear Register -const GPIOAFSEL: u64 = 0x420; // Mode Control Select Register - // From 0x424 to 0xFDC => reserved space. - // From 0xFE0 to 0xFFC => Peripheral and PrimeCell Identification Registers which are Read Only registers. - // These registers can conceptually be treated as a 32-bit register, and PartNumber[11:0] is used to identify the peripheral. - // We are putting the expected values (look at 'Reset value' column from above mentioned document) in an array. -const GPIO_ID: [u8; 8] = [0x61, 0x10, 0x14, 0x00, 0x0d, 0xf0, 0x05, 0xb1]; -// ID Margins -const GPIO_ID_LOW: u64 = 0xfe0; -const GPIO_ID_HIGH: u64 = 0x1000; - -#[derive(Debug)] -pub enum Error { - BadWriteOffset(u64), -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Error::BadWriteOffset(offset) => write!(f, "Bad Write Offset: {offset}"), - } - } -} - -type Result = result::Result; - -/// A GPIO device following the PL061 specification. -pub struct Gpio { - // Data Register - data: u32, - // Direction Register - dir: u32, - // Interrupt Sense Register - isense: u32, - // Interrupt Both Edges Register - ibe: u32, - // Interrupt Event Register - iev: u32, - // Interrupt Mask Register - im: u32, - // Raw Interrupt Status Register - istate: u32, - // Mode Control Select Register - afsel: u32, - // GPIO irq_field - interrupt_evt: EventFd, - intc: Option, - irq_line: Option, - shutdown_efd: EventFd, -} - -impl Gpio { - /// Constructs an PL061 GPIO device. - pub fn new(shutdown_efd: EventFd, interrupt_evt: EventFd) -> Self { - Self { - data: 0, - dir: 0, - isense: 0, - ibe: 0, - iev: 0, - im: 0, - istate: 0, - afsel: 0, - interrupt_evt, - intc: None, - irq_line: None, - shutdown_efd, - } - } - - pub fn set_intc(&mut self, intc: IrqChip) { - self.intc = Some(intc); - } - - pub fn set_irq_line(&mut self, irq: u32) { - debug!("SET_IRQ_LINE (GPIO)={irq}"); - self.irq_line = Some(irq); - } - - fn handle_write(&mut self, offset: u64, val: u32) -> Result<()> { - if offset < OFS_DATA { - // In order to write to data register, the corresponding bits in the mask, resulting - // from the offsite[9:2], must be HIGH. otherwise the bit values remain unchanged. - let mask = (offset >> 2) as u32 & self.dir; - self.data = (self.data & !mask) | (val & mask); - } else { - match offset { - GPIODIR => { - /* Direction Register */ - self.dir = val & 0xff; - } - GPIOIS => { - /* Interrupt Sense Register */ - self.isense = val & 0xff; - } - GPIOIBE => { - /* Interrupt Both Edges Register */ - self.ibe = val & 0xff; - } - GPIOIEV => { - /* Interrupt Event Register */ - self.iev = val & 0xff; - } - GPIOIE => { - /* Interrupt Mask Register */ - self.im = val & 0xff; - } - GPIOIC => { - /* Interrupt Clear Register */ - self.istate &= !val; - } - GPIOAFSEL => { - /* Mode Control Select Register */ - self.afsel = val & 0xff; - } - o => { - return Err(Error::BadWriteOffset(o)); - } - } - } - Ok(()) - } - - pub fn trigger_restart_key(&mut self, press: bool) { - if press { - debug!("Generate a restart key press event"); - self.istate = 0x8; - self.data = 0x8; - } else { - debug!("Generate a restart key release event"); - self.istate = 0x8; - self.data = 0x0; - } - - self.trigger_gpio_interrupt(); - } - - fn trigger_gpio_interrupt(&self) { - if let Some(intc) = &self.intc { - if let Err(e) = intc - .lock() - .unwrap() - .set_irq(self.irq_line, Some(&self.interrupt_evt)) - { - warn!("Error signalling irq: {e:?}"); - } - } - } -} - -impl BusDevice for Gpio { - fn read(&mut self, _base: u64, offset: u64, data: &mut [u8]) { - let value; - let mut read_ok = true; - - if (GPIO_ID_LOW..GPIO_ID_HIGH).contains(&offset) { - let index = ((offset - GPIO_ID_LOW) >> 2) as usize; - value = u32::from(GPIO_ID[index]); - } else if offset < OFS_DATA { - value = self.data & ((offset >> 2) as u32); - if value != 0 { - // Now that the guest has read it, send a key release event. - self.trigger_restart_key(false); - } - } else { - value = match offset { - GPIODIR => self.dir, - GPIOIS => self.isense, - GPIOIBE => self.ibe, - GPIOIEV => self.iev, - GPIOIE => self.im, - GPIORIE => self.istate, - GPIOMIS => self.istate & self.im, - GPIOAFSEL => self.afsel, - _ => { - read_ok = false; - 0 - } - }; - } - - if read_ok && data.len() <= 4 { - write_le_u32(data, value); - } else { - warn!( - "Invalid GPIO PL061 read: offset {}, data length {}", - offset, - data.len() - ); - } - } - - fn write(&mut self, _base: u64, offset: u64, data: &[u8]) { - if data.len() <= 4 { - let value = read_le_u32(data); - if let Err(e) = self.handle_write(offset, value) { - warn!("Failed to write to GPIO PL061 device: {e}"); - } - } else { - warn!( - "Invalid GPIO PL061 write: offset {}, data length {}", - offset, - data.len() - ); - } - } -} - -impl Subscriber for Gpio { - fn process(&mut self, event: &EpollEvent, _event_manager: &mut EventManager) { - let source = event.fd(); - - match source { - _ if source == self.shutdown_efd.as_raw_fd() => { - _ = self.shutdown_efd.read(); - // Send a key press event. - self.trigger_restart_key(true); - } - _ => warn!("Unexpected gpio event received: {source:?}"), - } - } - - fn interest_list(&self) -> Vec { - vec![EpollEvent::new( - EventSet::IN, - self.shutdown_efd.as_raw_fd() as u64, - )] - } -} diff --git a/vendor/krun-devices/src/legacy/aarch64/mod.rs b/vendor/krun-devices/src/legacy/aarch64/mod.rs deleted file mode 100644 index df45be37b..000000000 --- a/vendor/krun-devices/src/legacy/aarch64/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod gpio; -pub mod serial; diff --git a/vendor/krun-devices/src/legacy/aarch64/serial.rs b/vendor/krun-devices/src/legacy/aarch64/serial.rs deleted file mode 100644 index 5d75f12d7..000000000 --- a/vendor/krun-devices/src/legacy/aarch64/serial.rs +++ /dev/null @@ -1,429 +0,0 @@ -// Copyright 2021 Arm Limited (or its affiliates). All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! ARM PrimeCell UART(PL011) -//! -//! This module implements an ARM PrimeCell UART(PL011). -//! - -use std::collections::VecDeque; -use std::fmt; -use std::{io, result}; - -use polly::event_manager::{EventManager, Subscriber}; -use utils::byte_order::{read_le_u32, write_le_u32}; -use utils::epoll::{EpollEvent, EventSet}; -use utils::eventfd::EventFd; - -use crate::bus::BusDevice; -use crate::legacy::{IrqChip, ReadableFd}; -use crate::Error as DeviceError; - -/* Registers */ -const UARTDR: u64 = 0; -const UARTRSR_UARTECR: u64 = 1; -const UARTFR: u64 = 6; -const UARTILPR: u64 = 8; -const UARTIBRD: u64 = 9; -const UARTFBRD: u64 = 10; -const UARTLCR_H: u64 = 11; -const UARTCR: u64 = 12; -const UARTIFLS: u64 = 13; -const UARTIMSC: u64 = 14; -const UARTRIS: u64 = 15; -const UARTMIS: u64 = 16; -const UARTICR: u64 = 17; -const UARTDMACR: u64 = 18; -const UARTDEBUG: u64 = 0x3c0; - -const PL011_INT_TX: u32 = 0x20; -const PL011_INT_RX: u32 = 0x10; - -const PL011_FLAG_RXFF: u32 = 0x40; -const PL011_FLAG_RXFE: u32 = 0x10; - -const PL011_ID: [u8; 8] = [0x11, 0x10, 0x14, 0x00, 0x0d, 0xf0, 0x05, 0xb1]; -// We are only interested in the margins. -const AMBA_ID_LOW: u64 = 0x3f8; -const AMBA_ID_HIGH: u64 = 0x401; - -#[derive(Debug)] -pub enum Error { - BadWriteOffset(u64), - DmaNotImplemented, - InterruptFailure(DeviceError), - WriteAllFailure(io::Error), - FlushFailure(io::Error), -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Error::BadWriteOffset(offset) => write!(f, "pl011_write: Bad Write Offset: {offset}"), - Error::DmaNotImplemented => write!(f, "pl011: DMA not implemented."), - Error::InterruptFailure(e) => write!(f, "Failed to trigger interrupt: {e:?}"), - Error::WriteAllFailure(e) => write!(f, "Failed to write: {e}"), - Error::FlushFailure(e) => write!(f, "Failed to flush: {e}"), - } - } -} - -type Result = result::Result; - -/// A PL011 device following the PL011 specification. -pub struct Serial { - interrupt_evt: EventFd, - flags: u32, - lcr: u32, - rsr: u32, - cr: u32, - dmacr: u32, - debug: u32, - int_enabled: u32, - int_level: u32, - read_fifo: VecDeque, - ilpr: u32, - ibrd: u32, - fbrd: u32, - ifl: u32, - read_count: u32, - read_trigger: u32, - out: Option>, - input: Option>, - intc: Option, - irq_line: Option, -} - -impl Serial { - /// Constructs an AMBA PL011 UART device. - pub fn new( - interrupt_evt: EventFd, - out: Option>, - input: Option>, - ) -> Self { - let ( - flags, - lcr, - rsr, - cr, - dmacr, - debug, - int_enabled, - int_level, - read_fifo, - ilpr, - ibrd, - fbrd, - ifl, - read_count, - read_trigger, - ) = ( - 0x90, - 0, - 0, - 0x300, - 0, - 0, - 0, - 0, - VecDeque::new(), - 0, - 0, - 0, - 0x12, - 0, - 1, - ); - - Self { - interrupt_evt, - flags, - lcr, - rsr, - cr, - dmacr, - debug, - int_enabled, - int_level, - read_fifo, - ilpr, - ibrd, - fbrd, - ifl, - read_count, - read_trigger, - out, - input, - intc: None, - irq_line: None, - } - } - - /// Constructs a Serial port ready for input and output. - pub fn new_in_out( - interrupt_evt: EventFd, - input: Box, - out: Box, - ) -> Serial { - Self::new(interrupt_evt, Some(out), Some(input)) - } - - /// Constructs a Serial port ready for output but with no input. - pub fn new_out(interrupt_evt: EventFd, out: Box) -> Serial { - Self::new(interrupt_evt, Some(out), None) - } - - /// Constructs a Serial port with no connected input or output. - pub fn new_sink(interrupt_evt: EventFd) -> Serial { - Self::new(interrupt_evt, None, None) - } - - pub fn set_intc(&mut self, intc: IrqChip) { - self.intc = Some(intc); - } - - pub fn set_irq_line(&mut self, irq: u32) { - debug!("SET_IRQ_LINE (SERIAL)={irq}"); - self.irq_line = Some(irq); - } - - /// Provides a reference to the interrupt event fd. - pub fn interrupt_evt(&self) -> &EventFd { - &self.interrupt_evt - } - - /// Queues raw bytes for the guest to read and signals the interrupt - pub fn queue_input_bytes(&mut self, c: &[u8]) -> result::Result<(), DeviceError> { - self.read_fifo.extend(c); - self.read_count += c.len() as u32; - self.flags &= !PL011_FLAG_RXFE; - - if ((self.lcr & 0x10) == 0) || (self.read_count == 16) { - self.flags |= PL011_FLAG_RXFF; - } - - if self.read_count >= self.read_trigger { - self.int_level |= PL011_INT_RX; - self.trigger_interrupt()?; - } - - Ok(()) - } - - pub fn flush_output(&mut self) -> result::Result<(), io::Error> { - if let Some(out) = self.out.as_mut() { - out.flush()?; - } - Ok(()) - } - - fn pl011_get_baudrate(&self) -> u32 { - if self.fbrd == 0 { - return 0; - } - - let clk = 24_000_000; // We set the APB_PLCK to 24M in device tree - (clk / ((self.ibrd << 6) + self.fbrd)) << 2 - } - - fn pl011_trace_baudrate_change(&self) { - debug!( - "=== New baudrate: {:#?} (clk: {:#?}Hz, ibrd: {:#?}, fbrd: {:#?}) ===", - self.pl011_get_baudrate(), - 24_000_000, // We set the APB_PLCK to 24M in device tree - self.ibrd, - self.fbrd - ); - } - - fn pl011_set_read_trigger(&mut self) { - self.read_trigger = 1; - } - - fn handle_write(&mut self, offset: u64, val: u32) -> Result<()> { - match offset >> 2 { - UARTDR => { - self.int_level |= PL011_INT_TX; - if let Some(out) = self.out.as_mut() { - out.write_all(&[val.to_le_bytes()[0]]) - .map_err(Error::WriteAllFailure)?; - out.flush().map_err(Error::FlushFailure)?; - } - } - UARTRSR_UARTECR => { - self.rsr = 0; - } - UARTFR => { /* Writes to Flag register are ignored.*/ } - UARTILPR => { - self.ilpr = val; - } - UARTIBRD => { - self.ibrd = val; - self.pl011_trace_baudrate_change(); - } - UARTFBRD => { - self.fbrd = val; - self.pl011_trace_baudrate_change(); - } - UARTLCR_H => { - /* Reset the FIFO state on FIFO enable or disable */ - if ((self.lcr ^ val) & 0x10) != 0 { - self.read_count = 0; - } - self.lcr = val; - self.pl011_set_read_trigger(); - } - UARTCR => { - self.cr = val; - } - UARTIFLS => { - self.ifl = val; - self.pl011_set_read_trigger(); - } - UARTIMSC => { - self.int_enabled = val; - self.trigger_interrupt().map_err(Error::InterruptFailure)?; - } - UARTICR => { - self.int_level &= !val; - self.trigger_interrupt().map_err(Error::InterruptFailure)?; - } - UARTDMACR => { - self.dmacr = val; - if (val & 3) != 0 { - return Err(Error::DmaNotImplemented); - } - } - UARTDEBUG => { - self.debug = val; - //self.handle_debug(); - } - off => { - debug!("PL011: Bad write offset, offset: {off}"); - return Err(Error::BadWriteOffset(off)); - } - } - Ok(()) - } - - fn trigger_interrupt(&mut self) -> result::Result<(), DeviceError> { - if let Some(intc) = &self.intc { - intc.lock() - .unwrap() - .set_irq(self.irq_line, Some(&self.interrupt_evt))?; - } - Ok(()) - } -} - -impl BusDevice for Serial { - fn read(&mut self, _base: u64, offset: u64, data: &mut [u8]) { - debug!("read: offset={offset:x}"); - let mut read_ok = true; - let v = if (AMBA_ID_LOW..AMBA_ID_HIGH).contains(&(offset >> 2)) { - let index = ((offset - 0xfe0) >> 2) as usize; - u32::from(PL011_ID[index]) - } else { - match offset >> 2 { - UARTDR => { - self.flags &= !PL011_FLAG_RXFF; - let c: u32 = self.read_fifo.pop_front().unwrap_or_default().into(); - if self.read_count > 0 { - self.read_count -= 1; - } - if self.read_count == 0 { - self.flags |= PL011_FLAG_RXFE; - } - if self.read_count == (self.read_trigger - 1) { - self.int_level &= !PL011_INT_RX; - } - self.rsr = c >> 8; - c - } - UARTRSR_UARTECR => self.rsr, - UARTFR => self.flags, - UARTILPR => self.ilpr, - UARTIBRD => self.ibrd, - UARTFBRD => self.fbrd, - UARTLCR_H => self.lcr, - UARTCR => self.cr, - UARTIFLS => self.ifl, - UARTIMSC => self.int_enabled, - UARTRIS => self.int_level, - UARTMIS => self.int_level & self.int_enabled, - UARTDMACR => self.dmacr, - UARTDEBUG => self.debug, - _ => { - read_ok = false; - 0 - } - } - }; - - if read_ok && data.len() <= 4 { - write_le_u32(data, v); - } else { - warn!( - "Invalid PL011 read: offset {}, data length {}", - offset, - data.len() - ); - } - } - - fn write(&mut self, _base: u64, offset: u64, data: &[u8]) { - debug!("write: offset={offset:x}, data={data:?}"); - if data.len() <= 4 { - let v = read_le_u32(data); - if let Err(e) = self.handle_write(offset, v) { - warn!("Failed to write to PL011 device: {e}"); - } - } else { - warn!( - "Invalid PL011 write: offset {}, data length {}", - offset, - data.len() - ); - } - } -} - -impl Subscriber for Serial { - /// Handle a read event (EPOLLIN) on the serial input fd. - fn process(&mut self, event: &EpollEvent, _: &mut EventManager) { - let source = event.fd(); - let event_set = event.event_set(); - - // TODO: also check for errors. Pending high level discussions on how we want - // to handle errors in devices. - let supported_events = EventSet::IN; - if !supported_events.contains(event_set) { - warn!("Received unknown event: {event_set:?} from source: {source:?}"); - return; - } - - if let Some(input) = self.input.as_mut() { - if input.as_raw_fd() == source { - let mut out = [0u8; 32]; - match input.read(&mut out[..]) { - Ok(count) => { - self.queue_input_bytes(&out[..count]) - .unwrap_or_else(|e| warn!("Serial error on input: {e:?}")); - } - Err(e) => { - warn!("error while reading stdin: {e:?}"); - } - } - } - } - } - - /// Initial registration of pollable objects. - /// If serial input is present, register the serial input FD as readable. - fn interest_list(&self) -> Vec { - match &self.input { - Some(input) => vec![EpollEvent::new(EventSet::IN, input.as_raw_fd() as u64)], - None => vec![], - } - } -} diff --git a/vendor/krun-devices/src/legacy/aia.rs b/vendor/krun-devices/src/legacy/aia.rs deleted file mode 100644 index a5aabdcdc..000000000 --- a/vendor/krun-devices/src/legacy/aia.rs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2025 The libkrun Authors. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -pub trait AIADevice { - /// Returns the compatibility property of APLIC - fn aplic_compatibility(&self) -> &str; - - /// Returns an array with APLIC device properties - fn aplic_properties(&self) -> [u32; 4]; - - /// Returns the compatibility property of IMSIC - fn imsic_compatibility(&self) -> &str; - - /// Returns an array with IMSIC device properties - fn imsic_properties(&self) -> [u32; 4]; - - /// Returns the number of vCPUs this AIA handles - fn vcpu_count(&self) -> u32; - - /// Returns whether the AIA device is MSI compatible or not - fn msi_compatible(&self) -> bool; -} diff --git a/vendor/krun-devices/src/legacy/gic.rs b/vendor/krun-devices/src/legacy/gic.rs deleted file mode 100644 index 28235dd21..000000000 --- a/vendor/krun-devices/src/legacy/gic.rs +++ /dev/null @@ -1,16 +0,0 @@ -pub trait GICDevice { - /// Returns an array with GIC device properties - fn device_properties(&self) -> Vec; - - /// Returns the number of vCPUs this GIC handles - fn vcpu_count(&self) -> u64; - - /// Returns the fdt compatibility property of the device - fn fdt_compatibility(&self) -> String; - - /// Returns the maint_irq fdt property of the device - fn fdt_maint_irq(&self) -> u32; - - /// Returns the GIC version of the device - fn version(&self) -> u32; -} diff --git a/vendor/krun-devices/src/legacy/gicv3.rs b/vendor/krun-devices/src/legacy/gicv3.rs deleted file mode 100644 index df5bffbd9..000000000 --- a/vendor/krun-devices/src/legacy/gicv3.rs +++ /dev/null @@ -1,524 +0,0 @@ -use std::convert::TryInto; -use std::io; -use std::sync::Arc; - -use crate::bus::BusDevice; -use crate::legacy::gic::GICDevice; -use crate::legacy::irqchip::IrqChipT; -use crate::legacy::VcpuList; -use crate::Error as DeviceError; - -use utils::eventfd::EventFd; - -const IRQ_NUM: u32 = 288; -const MAXIRQ: u32 = 1020; -const BITMAP_SZ: usize = (MAXIRQ as usize).div_ceil(32); - -const GIC_INTERNAL: u32 = 32; - -const GICD_CTLR: u64 = 0x0000; -const GICD_TYPER: u64 = 0x0004; -const GICD_IIDR: u64 = 0x0008; -const GICD_STATUSR: u64 = 0x0010; -const GICD_IGROUPR: u64 = 0x0080; -const GICD_ISENABLER: u64 = 0x0100; -const GICD_ICENABLER: u64 = 0x0180; -const GICD_ISPENDR: u64 = 0x0200; -const GICD_ICPENDR: u64 = 0x0280; -const GICD_ISACTIVER: u64 = 0x0300; -const GICD_ICACTIVER: u64 = 0x0380; -const GICD_IPRIORITYR: u64 = 0x0400; -const GICD_ITARGETSR: u64 = 0x0800; -const GICD_ICFGR: u64 = 0x0C00; -const GICD_SGIR: u64 = 0x0F00; -const GICD_IROUTER: u64 = 0x6000; -const GICD_IDREGS: u64 = 0xFFD0; - -/* GICD_CTLR fields */ -const GICD_CTLR_EN_GRP0: u32 = 1 << 0; -const GICD_CTLR_EN_GRP1NS: u32 = 1 << 1; /* GICv3 5.3.20 */ -/* Bit 4 is ARE if the system doesn't support TrustZone, ARE_S otherwise */ -const GICD_CTLR_ARE: u32 = 1 << 4; -const GICD_CTLR_DS: u32 = 1 << 6; - -/* - * Redistributor registers, offsets from RD_base - */ -const GICR_CTLR: u64 = 0x0000; -const GICR_TYPER: u64 = 0x0008; -const GICR_WAKER: u64 = 0x0014; -const GICR_IDREGS: u64 = 0xFFD0; - -const GICR_WAKER_PROCESSOR_SLEEP: u32 = 1 << 1; -const GICR_WAKER_CHILDREN_ASLEEP: u32 = 1 << 2; - -/* - * Redistributor frame offsets from RD_base - */ -const GICR_SGI_OFFSET: u64 = 0x10000; - -/* SGI and PPI Redistributor registers, offsets from RD_base */ -const GICR_IGROUPR0: u64 = GICR_SGI_OFFSET + 0x0080; -const GICR_ISENABLER0: u64 = GICR_SGI_OFFSET + 0x0100; -const GICR_ICENABLER0: u64 = GICR_SGI_OFFSET + 0x0180; -const GICR_ICACTIVER0: u64 = GICR_SGI_OFFSET + 0x0380; -const GICR_IPRIORITYR: u64 = GICR_SGI_OFFSET + 0x0400; -const GICR_ICFGR1: u64 = GICR_SGI_OFFSET + 0x0C04; - -/* Distributor register fields */ -// GICD_TYPER (https://developer.arm.com/documentation/ddi0601/2020-12/External-Registers/GICD-TYPER--Interrupt-Controller-Type-Register?lang=en) -const GICD_TYPER_RSS_SHIFT: u64 = 26; -const GICD_TYPER_NO1N_SHIFT: u64 = 25; -const GICD_TYPER_A3V_SHIFT: u64 = 24; -const GICD_TYPER_ID_BITS_SHIFT: u64 = 19; -const GICD_TYPER_LPIS_SHIFT: u64 = 17; -const GICD_TYPER_IT_LINES_NUMBER_SHIFT: u64 = 0; - -/* Redistributor register fields */ -// GICR_TYPER (https://developer.arm.com/documentation/ddi0601/2020-12/External-Registers/GICR-TYPER--Redistributor-Type-Register?lang=en) -const GICR_TYPER_AFFINITY_VALUE: u64 = 32; -const GICR_TYPER_COMMON_LPI_AFF_SHIFT: u64 = 24; -const GICR_TYPER_PROCESSOR_NUMBER_SHIFT: u64 = 8; -const GICR_TYPER_LAST_SHIFT: u64 = 4; - -/* CoreSight PIDR0 values for ARM GICv3 implementations */ -const GICV3_PIDR0_DIST: u8 = 0x92; -const GICV3_PIDR0_REDIST: u8 = 0x93; - -// Device tree specific constants -const GICV3_BASE_SIZE: u64 = 0x0001_0000; -const GICV3_MAINT_IRQ: u32 = 8; - -#[derive(Clone)] -pub struct GicV3 { - dist_addr: u64, - dist_size: u64, - redist_size: u64, - redists_addr: u64, - redists_size: u64, - - gicd_ctlr: u32, - vcpu_list: Arc, - revision: u8, - edge_trigger: [u32; BITMAP_SZ], - gicr_waker: u32, - gicd_irouter: [u64; MAXIRQ as usize], - - /// GIC device properties, to be used for setting up the fdt entry - properties: [u64; 4], -} - -impl GicV3 { - /// Get the address of the GICv3 distributor. - pub fn get_dist_addr(&self) -> u64 { - self.dist_addr - } - - /// Get the size of the GIC_v3 distributor. - pub const fn get_dist_size(&self) -> u64 { - self.dist_size - } - - pub fn get_redists_addr(&self) -> u64 { - self.redists_addr - } - - pub fn get_redists_size(&self) -> u64 { - self.redists_size - } - - pub const fn get_redist_size(&self) -> u64 { - self.redist_size - } - - pub fn new(vcpu_list: Arc) -> Self { - let vcpu_count = vcpu_list.get_cpu_count(); - let dist_size = GICV3_BASE_SIZE; - let dist_addr = arch::MMIO_MEM_START - 3 * dist_size; - let redist_size = 2 * dist_size; - let redists_size = redist_size * vcpu_count; - let redists_addr = dist_addr - redists_size; - - Self { - dist_addr, - dist_size, - redist_size, - redists_addr, - redists_size, - - gicd_ctlr: GICD_CTLR_DS | GICD_CTLR_ARE, - vcpu_list, - revision: 3, - edge_trigger: [0; BITMAP_SZ], - gicr_waker: GICR_WAKER_PROCESSOR_SLEEP | GICR_WAKER_CHILDREN_ASLEEP, - gicd_irouter: [0; MAXIRQ as usize], - - properties: [dist_addr, dist_size, redists_addr, redists_size], - } - } - - fn handle_dist_read32(&self, _vcpuid: u64, offset: u64, data: &mut [u8]) { - let mut val: u32 = 0; - match offset { - GICD_CTLR => val = self.gicd_ctlr, - GICD_TYPER => { - let itlinesnumber = (IRQ_NUM / 32) - 1; - val = (1 << GICD_TYPER_RSS_SHIFT) - | (1 << GICD_TYPER_NO1N_SHIFT) - | (1 << GICD_TYPER_A3V_SHIFT) - | (1 << GICD_TYPER_LPIS_SHIFT) - | (0xf << GICD_TYPER_ID_BITS_SHIFT) - | (itlinesnumber << GICD_TYPER_IT_LINES_NUMBER_SHIFT); - } - GICD_IIDR => val = 0x43b, - GICD_STATUSR => {} - _ if (GICD_IGROUPR..GICD_IGROUPR + 0x7f).contains(&offset) => {} - _ if (GICD_ISENABLER..GICD_ISENABLER + 0x7f).contains(&offset) => {} - _ if (GICD_ICENABLER..GICD_ICENABLER + 0x7f).contains(&offset) => {} - _ if (GICD_ISPENDR..GICD_ISPENDR + 0x7f).contains(&offset) => {} - _ if (GICD_ICPENDR..GICD_ICPENDR + 0x7f).contains(&offset) => {} - _ if (GICD_ISACTIVER..GICD_ISACTIVER + 0x7f).contains(&offset) => {} - _ if (GICD_ICACTIVER..GICD_ICACTIVER + 0x7f).contains(&offset) => {} - _ if (GICD_IPRIORITYR..GICD_IPRIORITYR + 0x3ff).contains(&offset) => {} - _ if (GICD_ITARGETSR..GICD_ITARGETSR + 0x3ff).contains(&offset) => { - panic!("[GICv3] only affinity routing is implemented"); - } - _ if (GICD_ICFGR..GICD_ICFGR + 0xff).contains(&offset) => { - let irq = ((offset - GICD_ICFGR) * 4) as u32; - if !(GIC_INTERNAL..IRQ_NUM).contains(&irq) { - val = 0; - } else { - let mut value = self.edge_trigger[((irq & !0x1f) / 32) as usize]; - value = extract32(value, if (irq & 0x1f) != 0 { 16 } else { 0 }, 16); - value = half_shuffle32(value) << 1; - val = value; - } - } - _ if (GICD_IDREGS..GICD_IDREGS + 0x2f).contains(&offset) => { - /* Return the value of the CoreSight ID register at the specified - * offset from the first ID register (as found in the distributor - * and redistributor register banks). - * These values indicate an ARM implementation of a GICv3 or v4. - */ - let gicd_ids: [u8; 12] = [ - 0x44, 0x00, 0x00, 0x00, 0x92, 0xB4, 0x0B, 0x00, 0x0D, 0xF0, 0x05, 0xB1, - ]; - let mut id: u32; - let regoffset = (offset - GICD_IDREGS) / 4; - - if regoffset == 4 { - id = GICV3_PIDR0_DIST as u32; - } else { - id = gicd_ids[regoffset as usize] as u32; - if regoffset == 6 { - /* PIDR2 bits [7:4] are the GIC architecture revision */ - id |= (self.revision as u32) << 4; - } - } - - val = id; - } - GICD_SGIR => {} - 0xc => { - // invalid guest read on Qemu - } - _ => panic!("Unknown GIC DIST read32 offset=0x{offset:x}"), - } - for (i, b) in val.to_le_bytes().iter().enumerate() { - data[i] = *b; - } - debug!("[GICv3] -> read32 DIST offset={offset} val={val}"); - } - - fn handle_dist_write32(&mut self, _vcpuid: u64, offset: u64, data: &[u8]) { - debug!( - "[GICv3] write32 DIST offset={} val={}", - offset, - u32::from_le_bytes(data.try_into().unwrap()) - ); - - let val: u32 = u32::from_le_bytes(data.try_into().unwrap()); - match offset { - GICD_CTLR => { - let mask = GICD_CTLR_EN_GRP0 | GICD_CTLR_EN_GRP1NS; - self.gicd_ctlr = (self.gicd_ctlr & !mask) | (val & mask); - } - _ if (GICD_IGROUPR..GICD_IGROUPR + 0x7f).contains(&offset) => {} - _ if (GICD_ISENABLER..GICD_ISENABLER + 0x7f).contains(&offset) => {} - _ if (GICD_ICENABLER..GICD_ICENABLER + 0x7f).contains(&offset) => {} - _ if (GICD_ISPENDR..GICD_ISPENDR + 0x7f).contains(&offset) => {} - _ if (GICD_ICPENDR..GICD_ICPENDR + 0x7f).contains(&offset) => {} - _ if (GICD_ISACTIVER..GICD_ISACTIVER + 0x7f).contains(&offset) => {} - _ if (GICD_ICACTIVER..GICD_ICACTIVER + 0x7f).contains(&offset) => {} - _ if (GICD_IPRIORITYR..GICD_IPRIORITYR + 0x3ff).contains(&offset) => {} - _ if (GICD_ITARGETSR..GICD_ITARGETSR + 0x3ff).contains(&offset) => { - panic!("[GICv3] only affinity routing is implemented"); - } - _ if (GICD_ICFGR..GICD_ICFGR + 0xff).contains(&offset) => { - /* Here only the odd bits are used; even bits are RES0 */ - let irq = ((offset - GICD_ICFGR) * 4) as u32; - let mut mask: u32; - - if !(GIC_INTERNAL..IRQ_NUM).contains(&irq) { - return; - } - - /* Since our edge_trigger bitmap is one bit per irq, our input - * 32-bits will compress down into 16 bits which we need - * to write into the bitmap. - */ - let mut value = half_unshuffle32(val >> 1); - mask = 0xFFFFFFFFu32; - if (irq as u64) & 0x1fu64 != 0u64 { - value <<= 16; - mask &= 0xffff0000u32; - } else { - mask &= 0xffff; - } - let idx = (irq & !0x1f) / 32; - let oldval = self.edge_trigger[idx as usize]; - value = (oldval & !mask) | (value & mask); - self.edge_trigger[idx as usize] = value; - } - _ => panic!("Unknown GIC DIST write32 offset=0x{offset:x}"), - } - } - - fn handle_dist_write64(&mut self, _vcpuid: u64, offset: u64, data: &[u8]) { - let val = u64::from_le_bytes(data.try_into().unwrap()); - debug!("[GICv3] write64 DIST offset=0x{offset:x} value=0x{val:x}"); - match offset { - _ if (GICD_IROUTER..GICD_IROUTER + 0x1fdf).contains(&offset) => { - let intid = ((offset - GICD_IROUTER) / 8) as usize; - self.gicd_irouter[intid] = val; - } - _ => panic!("Unknown GIC DIST write64 offset=0x{offset:x}"), - } - } - - fn handle_redist_read32(&self, _vcpuid: u64, offset: u64, data: &mut [u8]) { - let mut val: u32 = 0; - match offset { - GICR_CTLR => { - val = 2; - } - GICD_IIDR => { - val = 0x43b; - } - GICD_STATUSR => {} - GICR_WAKER => { - val = self.gicr_waker; - } - _ if (GICR_IPRIORITYR..GICR_IPRIORITYR + 0x3ff).contains(&offset) => {} - _ if (GICR_IDREGS..GICR_IDREGS + 0x2f).contains(&offset) => { - /* Return the value of the CoreSight ID register at the specified - * offset from the first ID register (as found in the distributor - * and redistributor register banks). - * These values indicate an ARM implementation of a GICv3 or v4. - */ - let gicd_ids: [u8; 12] = [ - 0x44, 0x00, 0x00, 0x00, 0x92, 0xB4, 0x0B, 0x00, 0x0D, 0xF0, 0x05, 0xB1, - ]; - let mut id: u32; - let regoffset = (offset - GICR_IDREGS) / 4; - - if regoffset == 4 { - id = GICV3_PIDR0_REDIST as u32; - } else { - id = gicd_ids[regoffset as usize] as u32; - if regoffset == 6 { - /* PIDR2 bits [7:4] are the GIC architecture revision */ - id |= (self.revision as u32) << 4; - } - } - - val = id; - } - GICR_ICFGR1 => {} - _ => panic!("Unknown GIC REDIST read32 offset=0x{offset:x}"), - } - for (i, b) in val.to_le_bytes().iter().enumerate() { - data[i] = *b; - } - - debug!("[GICv3] -> read32 REDIST offset={offset} val={val}"); - } - - fn handle_redist_read64(&self, vcpuid: u64, offset: u64, data: &mut [u8]) { - let val = match offset { - GICR_TYPER => { - let mut typer = (vcpuid << GICR_TYPER_AFFINITY_VALUE) - | (1 << GICR_TYPER_COMMON_LPI_AFF_SHIFT) - | (vcpuid << GICR_TYPER_PROCESSOR_NUMBER_SHIFT); - // Assume we have one redistributor range, set last bit for last CPU. - if vcpuid == self.vcpu_list.get_cpu_count() - 1 { - typer |= 1u64 << GICR_TYPER_LAST_SHIFT; - } - typer - } - _ => panic!("Unknown GIC REDIST read64 offset=0x{offset:x}"), - }; - for (i, b) in val.to_le_bytes().iter().enumerate() { - data[i] = *b; - } - - debug!("[GICv3] -> read64 REDIST offset={offset} val={val}"); - } - - fn handle_redist_write32(&mut self, _vcpuid: u64, offset: u64, data: &[u8]) { - debug!( - "[GICv3] write32 REDIST offset={} val={}", - offset, - u32::from_le_bytes(data.try_into().unwrap()) - ); - - let mut val: u32 = u32::from_le_bytes(data.try_into().unwrap()); - match offset { - GICR_WAKER => { - val &= GICR_WAKER_PROCESSOR_SLEEP; - if (val & GICR_WAKER_PROCESSOR_SLEEP) != 0 { - val |= GICR_WAKER_CHILDREN_ASLEEP; - } - self.gicr_waker = val; - } - GICR_IGROUPR0 | GICR_ISENABLER0 | GICR_ICENABLER0 | GICR_ICACTIVER0 => {} - _ if (GICR_IPRIORITYR..GICR_IPRIORITYR + 0x1f).contains(&offset) => {} - _ => panic!("Unknown GIC REDIST write32 offset=0x{offset:x}"), - } - } -} - -impl IrqChipT for GicV3 { - fn get_mmio_addr(&self) -> u64 { - self.redists_addr - } - - fn get_mmio_size(&self) -> u64 { - self.dist_size + self.redists_size - } - - fn set_irq( - &self, - irq_line: Option, - _interrupt_evt: Option<&EventFd>, - ) -> Result<(), DeviceError> { - if let Some(irq_line) = irq_line { - assert!(irq_line < MAXIRQ, "[GICv3] intid out of range"); - // TODO(p1-0tr): extract full MPID, but for now Aff0 will do - let mpid = self.gicd_irouter[irq_line as usize] & 0xff; - self.vcpu_list.set_irq_common(mpid, irq_line); - Ok(()) - } else { - Err(DeviceError::FailedSignalingUsedQueue(io::Error::new( - io::ErrorKind::InvalidData, - "IRQ not line configured", - ))) - } - } -} - -impl BusDevice for GicV3 { - fn read(&mut self, vcpuid: u64, offset: u64, data: &mut [u8]) { - if offset >= self.redists_size { - let offset = offset - self.redists_size; - match data.len() { - 1 => panic!("GIC DIST read8 vcpuid={vcpuid} offset=0x{offset:x}"), - 2 => panic!("GIC DIST read16 vcpuid={vcpuid} offset=0x{offset:x}"), - 4 => self.handle_dist_read32(vcpuid, offset, data), - 8 => panic!("GIC DIST read64 vcpuid={vcpuid} offset=0x{offset:x}"), - _ => panic!("GIC DIST unsupported read size"), - } - } else { - let vcpuid = offset / self.redist_size; - let offset = offset % self.redist_size; - - match data.len() { - 1 => panic!("GIC REDIST read8 vcpuid={vcpuid} offset=0x{offset:x}"), - 2 => panic!("GIC REDIST read16 vcpuid={vcpuid} offset=0x{offset:x}"), - 4 => self.handle_redist_read32(vcpuid, offset, data), - 8 => self.handle_redist_read64(vcpuid, offset, data), - _ => panic!("GIC REDIST unsupported read size"), - } - } - } - - fn write(&mut self, vcpuid: u64, offset: u64, data: &[u8]) { - if offset >= self.redists_size { - let offset = offset - self.redists_size; - match data.len() { - 1 => panic!("GIC DIST write8 vcpuid={vcpuid} offset=0x{offset:x}, data={data:?}"), - 2 => panic!("GIC DIST write16 vcpuid={vcpuid} offset=0x{offset:x}, data={data:?}"), - 4 => self.handle_dist_write32(vcpuid, offset, data), - 8 => self.handle_dist_write64(vcpuid, offset, data), - _ => panic!("GIC DIST unsupported read size"), - } - } else { - let vcpuid = offset / self.redist_size; - let offset = offset % self.redist_size; - - match data.len() { - 1 => panic!("GIC REDIST write8 vcpuid={vcpuid} offset=0x{offset:x}, data={data:?}"), - 2 => { - panic!("GIC REDIST write16 vcpuid={vcpuid} offset=0x{offset:x}, data={data:?}") - } - 4 => self.handle_redist_write32(vcpuid, offset, data), - 8 => { - panic!("GIC REDIST write64 vcpuid={vcpuid} offset=0x{offset:x}, data={data:?}") - } - _ => panic!("GIC REDIST unsupported write size"), - } - } - } -} - -impl GICDevice for GicV3 { - fn device_properties(&self) -> Vec { - self.properties.to_vec() - } - - fn vcpu_count(&self) -> u64 { - self.vcpu_list.get_cpu_count() - } - - fn fdt_compatibility(&self) -> String { - "arm,gic-v3".to_string() - } - - fn fdt_maint_irq(&self) -> u32 { - GICV3_MAINT_IRQ - } - - fn version(&self) -> u32 { - 0 - } -} - -fn half_shuffle32(val: u32) -> u32 { - /* This algorithm is from _Hacker's Delight_ section 7-2 "Shuffling Bits". - * It ignores any bits set in the top half of the input. - */ - let mut x = val; - x = ((x & 0xFF00) << 8) | (x & 0x00FF); - x = ((x << 4) | x) & 0x0F0F0F0F; - x = ((x << 2) | x) & 0x33333333; - x = ((x << 1) | x) & 0x55555555; - x -} - -fn half_unshuffle32(val: u32) -> u32 { - /* This algorithm is from _Hacker's Delight_ section 7-2 "Shuffling Bits". - * where it is called an inverse half shuffle. - */ - let mut x = val; - x &= 0x55555555; - x = ((x >> 1) | x) & 0x33333333; - x = ((x >> 2) | x) & 0x0F0F0F0F; - x = ((x >> 4) | x) & 0x00FF00FF; - x = ((x >> 8) | x) & 0x0000FFFF; - x -} - -fn extract32(value: u32, start: u32, length: u32) -> u32 { - assert!(length <= 32 - start); - (value >> start) & ((!0u32) >> (32 - length)) -} diff --git a/vendor/krun-devices/src/legacy/hvfgicv3.rs b/vendor/krun-devices/src/legacy/hvfgicv3.rs deleted file mode 100644 index c831bba15..000000000 --- a/vendor/krun-devices/src/legacy/hvfgicv3.rs +++ /dev/null @@ -1,183 +0,0 @@ -// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -use std::io; -use std::sync::LazyLock; - -use crate::bus::BusDevice; -use crate::legacy::gic::GICDevice; -use crate::legacy::irqchip::IrqChipT; -use crate::Error as DeviceError; - -use hvf::bindings::{hv_gic_config_t, hv_ipa_t, hv_return_t, HV_SUCCESS}; -use hvf::Error; -use utils::eventfd::EventFd; - -// Device trees specific constants -const ARCH_GIC_V3_MAINT_IRQ: u32 = 9; - -pub struct HvfGicBindings { - hv_gic_create: - libloading::Symbol<'static, unsafe extern "C" fn(hv_gic_config_t) -> hv_return_t>, - hv_gic_config_create: libloading::Symbol<'static, unsafe extern "C" fn() -> hv_gic_config_t>, - hv_gic_config_set_distributor_base: - libloading::Symbol<'static, unsafe extern "C" fn(hv_gic_config_t, hv_ipa_t) -> hv_return_t>, - hv_gic_config_set_redistributor_base: - libloading::Symbol<'static, unsafe extern "C" fn(hv_gic_config_t, hv_ipa_t) -> hv_return_t>, - hv_gic_get_distributor_size: - libloading::Symbol<'static, unsafe extern "C" fn(*mut usize) -> hv_return_t>, - hv_gic_get_redistributor_size: - libloading::Symbol<'static, unsafe extern "C" fn(*mut usize) -> hv_return_t>, - hv_gic_set_spi: libloading::Symbol<'static, unsafe extern "C" fn(u32, bool) -> hv_return_t>, -} - -pub struct HvfGicV3 { - bindings: HvfGicBindings, - - /// GIC device properties, to be used for setting up the fdt entry - properties: [u64; 4], - - /// Number of CPUs handled by the device - vcpu_count: u64, -} - -static HVF: LazyLock = LazyLock::new(|| unsafe { - libloading::Library::new( - "/System/Library/Frameworks/Hypervisor.framework/Versions/A/Hypervisor", - ) - .unwrap() -}); - -impl HvfGicV3 { - pub fn new(vcpu_count: u64) -> Result { - let bindings = unsafe { - HvfGicBindings { - hv_gic_create: HVF.get(b"hv_gic_create").map_err(Error::FindSymbol)?, - hv_gic_config_create: HVF - .get(b"hv_gic_config_create") - .map_err(Error::FindSymbol)?, - hv_gic_config_set_distributor_base: HVF - .get(b"hv_gic_config_set_distributor_base") - .map_err(Error::FindSymbol)?, - hv_gic_config_set_redistributor_base: HVF - .get(b"hv_gic_config_set_redistributor_base") - .map_err(Error::FindSymbol)?, - hv_gic_get_distributor_size: HVF - .get(b"hv_gic_get_distributor_size") - .map_err(Error::FindSymbol)?, - hv_gic_get_redistributor_size: HVF - .get(b"hv_gic_get_redistributor_size") - .map_err(Error::FindSymbol)?, - hv_gic_set_spi: HVF.get(b"hv_gic_set_spi").map_err(Error::FindSymbol)?, - } - }; - - let mut dist_size: usize = 0; - let ret = unsafe { (bindings.hv_gic_get_distributor_size)(&mut dist_size) }; - if ret != HV_SUCCESS { - return Err(Error::VmCreate); - } - let dist_size = dist_size as u64; - - let mut redist_size: usize = 0; - let ret = unsafe { (bindings.hv_gic_get_redistributor_size)(&mut redist_size) }; - if ret != HV_SUCCESS { - return Err(Error::VmCreate); - } - - let redists_size = redist_size as u64 * vcpu_count; - let dist_addr = arch::MMIO_MEM_START - dist_size - redists_size; - let redists_addr = arch::MMIO_MEM_START - redists_size; - - let gic_config = unsafe { (bindings.hv_gic_config_create)() }; - let ret = unsafe { (bindings.hv_gic_config_set_distributor_base)(gic_config, dist_addr) }; - if ret != HV_SUCCESS { - return Err(Error::VmCreate); - } - - let ret = unsafe { - (bindings.hv_gic_config_set_redistributor_base)( - gic_config, - arch::MMIO_MEM_START - redists_size, - ) - }; - if ret != HV_SUCCESS { - return Err(Error::VmCreate); - } - - let ret = unsafe { (bindings.hv_gic_create)(gic_config) }; - if ret != HV_SUCCESS { - return Err(Error::VmCreate); - } - - Ok(Self { - bindings, - properties: [dist_addr, dist_size, redists_addr, redists_size], - vcpu_count, - }) - } -} - -impl IrqChipT for HvfGicV3 { - fn get_mmio_addr(&self) -> u64 { - 0 - } - - fn get_mmio_size(&self) -> u64 { - 0 - } - - fn set_irq( - &self, - irq_line: Option, - _interrupt_evt: Option<&EventFd>, - ) -> Result<(), DeviceError> { - if let Some(irq_line) = irq_line { - let ret = unsafe { (self.bindings.hv_gic_set_spi)(irq_line, true) }; - if ret != HV_SUCCESS { - Err(DeviceError::FailedSignalingUsedQueue( - std::io::Error::other("HVF returned error when setting SPI"), - )) - } else { - Ok(()) - } - } else { - Err(DeviceError::FailedSignalingUsedQueue(io::Error::new( - io::ErrorKind::InvalidData, - "IRQ not line configured", - ))) - } - } -} - -impl BusDevice for HvfGicV3 { - fn read(&mut self, _vcpuid: u64, _offset: u64, _data: &mut [u8]) { - unreachable!("MMIO operations are managed in-kernel"); - } - - fn write(&mut self, _vcpuid: u64, _offset: u64, _data: &[u8]) { - unreachable!("MMIO operations are managed in-kernel"); - } -} - -impl GICDevice for HvfGicV3 { - fn device_properties(&self) -> Vec { - self.properties.to_vec() - } - - fn vcpu_count(&self) -> u64 { - self.vcpu_count - } - - fn fdt_compatibility(&self) -> String { - "arm,gic-v3".to_string() - } - - fn fdt_maint_irq(&self) -> u32 { - ARCH_GIC_V3_MAINT_IRQ - } - - fn version(&self) -> u32 { - 7 - } -} diff --git a/vendor/krun-devices/src/legacy/i8042.rs b/vendor/krun-devices/src/legacy/i8042.rs deleted file mode 100644 index 669dc1fec..000000000 --- a/vendor/krun-devices/src/legacy/i8042.rs +++ /dev/null @@ -1,488 +0,0 @@ -// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -// -// Portions Copyright 2017 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the THIRD-PARTY file. - -use std::fmt; -use std::num::Wrapping; -use std::{io, result}; -use utils::eventfd::EventFd; - -use crate::bus::BusDevice; - -#[derive(Debug)] -pub enum Error { - CloneCpuResetEvt(io::Error), - KbdInterruptDisabled, - KbdInterruptFailure(io::Error), - InternalBufferFull, -} -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Error::CloneCpuResetEvt(io_err) => { - write!(f, "Could not clone CPU reset eventfd: {io_err}.") - } - Error::KbdInterruptDisabled => { - write!(f, "Keyboard interrupt disabled by guest driver.",) - } - Error::KbdInterruptFailure(io_err) => { - write!(f, "Could not trigger keyboard interrupt: {io_err}.") - } - Error::InternalBufferFull => write!(f, "i8042 internal buffer full."), - } - } -} - -type Result = result::Result; - -/// Offset of the status port (port 0x64) -const OFS_STATUS: u64 = 4; - -/// Offset of the data port (port 0x60) -const OFS_DATA: u64 = 0; - -/// i8042 commands -/// These values are written by the guest driver to port 0x64. -const CMD_READ_CTR: u8 = 0x20; // Read control register -const CMD_WRITE_CTR: u8 = 0x60; // Write control register -const CMD_READ_OUTP: u8 = 0xD0; // Read output port -const CMD_WRITE_OUTP: u8 = 0xD1; // Write output port -const CMD_RESET_CPU: u8 = 0xFE; // Reset CPU - -/// i8042 status register bits -const SB_OUT_DATA_AVAIL: u8 = 0x0001; // Data available at port 0x60 -const SB_I8042_CMD_DATA: u8 = 0x0008; // i8042 expecting command parameter at port 0x60 -const SB_KBD_ENABLED: u8 = 0x0010; // 1 = kbd enabled, 0 = kbd locked - -/// i8042 control register bits -const CB_KBD_INT: u8 = 0x0001; // kbd interrupt enabled -const CB_POST_OK: u8 = 0x0004; // POST ok (should always be 1) - -/// Key scan codes -const KEY_CTRL: u16 = 0x0014; -const KEY_ALT: u16 = 0x0011; -const KEY_DEL: u16 = 0xE071; - -/// Internal i8042 buffer size, in bytes -const BUF_SIZE: usize = 16; - -/// A i8042 PS/2 controller that emulates just enough to shutdown the machine. -pub struct I8042Device { - /// CPU reset eventfd. We will set this event when the guest issues CMD_RESET_CPU. - reset_evt: EventFd, - - /// Keyboard interrupt event (IRQ 1). - kbd_interrupt_evt: EventFd, - - /// The i8042 status register. - status: u8, - - /// The i8042 control register. - control: u8, - - /// The i8042 output port. - outp: u8, - - /// The last command sent to port 0x64. - cmd: u8, - - /// The internal i8042 data buffer. - buf: [u8; BUF_SIZE], - bhead: Wrapping, - btail: Wrapping, -} - -impl I8042Device { - /// Constructs an i8042 device that will signal the given event when the guest requests it. - pub fn new(reset_evt: EventFd, kbd_interrupt_evt: EventFd) -> I8042Device { - I8042Device { - reset_evt, - kbd_interrupt_evt, - control: CB_POST_OK | CB_KBD_INT, - cmd: 0, - outp: 0, - status: SB_KBD_ENABLED, - buf: [0; BUF_SIZE], - bhead: Wrapping(0), - btail: Wrapping(0), - } - } - - /// Returns a clone of the CPU reset event fd - pub fn get_reset_evt_clone(&self) -> Result { - self.reset_evt.try_clone().map_err(Error::CloneCpuResetEvt) - } - - pub fn trigger_kbd_interrupt(&self) -> Result<()> { - if (self.control & CB_KBD_INT) == 0 { - warn!("Failed to trigger i8042 kbd interrupt (disabled by guest OS)"); - return Err(Error::KbdInterruptDisabled); - } - self.kbd_interrupt_evt - .write(1) - .map_err(Error::KbdInterruptFailure) - } - - pub fn trigger_key(&mut self, key: u16) -> Result<()> { - if key & 0xff00 != 0 { - // Check if there is enough room in the buffer, before pushing an extended (2-byte) key. - if BUF_SIZE - self.buf_len() < 2 { - return Err(Error::InternalBufferFull); - } - self.push_byte((key >> 8) as u8)?; - } - self.push_byte((key & 0xff) as u8)?; - - match self.trigger_kbd_interrupt() { - Ok(_) | Err(Error::KbdInterruptDisabled) => Ok(()), - Err(e) => Err(e), - } - } - - #[inline] - pub fn trigger_ctrl_alt_del(&mut self) -> Result<()> { - // The CTRL+ALT+DEL sequence is 4 bytes in total (1 extended key + 2 normal keys). - // Fail if we don't have room for the whole sequence. - if BUF_SIZE - self.buf_len() < 4 { - return Err(Error::InternalBufferFull); - } - self.trigger_key(KEY_CTRL)?; - self.trigger_key(KEY_ALT)?; - self.trigger_key(KEY_DEL)?; - Ok(()) - } - - #[inline] - fn push_byte(&mut self, byte: u8) -> Result<()> { - self.status |= SB_OUT_DATA_AVAIL; - if self.buf_len() == BUF_SIZE { - return Err(Error::InternalBufferFull); - } - self.buf[self.btail.0 % BUF_SIZE] = byte; - self.btail += Wrapping(1usize); - Ok(()) - } - - #[inline] - fn pop_byte(&mut self) -> Option { - if self.buf_len() == 0 { - return None; - } - let res = self.buf[self.bhead.0 % BUF_SIZE]; - self.bhead += Wrapping(1usize); - if self.buf_len() == 0 { - self.status &= !SB_OUT_DATA_AVAIL; - } - Some(res) - } - - #[inline] - fn flush_buf(&mut self) { - self.bhead = Wrapping(0usize); - self.btail = Wrapping(0usize); - self.status &= !SB_OUT_DATA_AVAIL; - } - - #[inline] - fn buf_len(&self) -> usize { - (self.btail - self.bhead).0 - } -} - -impl BusDevice for I8042Device { - fn read(&mut self, _vcpuid: u64, offset: u64, data: &mut [u8]) { - // All our ports are byte-wide. We don't know how to handle any wider data. - if data.len() != 1 { - return; - } - - match offset { - OFS_STATUS => data[0] = self.status, - OFS_DATA => { - // The guest wants to read a byte from port 0x60. For the 8042, that means the top - // byte in the internal buffer. If the buffer is empty, the guest will get a 0. - data[0] = self.pop_byte().unwrap_or(0); - - // Check if we still have data in the internal buffer. If so, we need to trigger - // another interrupt, to let the guest know they need to issue another read from - // port 0x60. - if (self.status & SB_OUT_DATA_AVAIL) != 0 { - if let Err(Error::KbdInterruptFailure(err)) = self.trigger_kbd_interrupt() { - warn!("Failed to trigger i8042 kbd interrupt {err:?}"); - } - } - } - _ => {} - } - } - - fn write(&mut self, _vcpuid: u64, offset: u64, data: &[u8]) { - // All our ports are byte-wide. We don't know how to handle any wider data. - if data.len() != 1 { - return; - } - - match offset { - OFS_STATUS if data[0] == CMD_RESET_CPU => { - // The guest wants to assert the CPU reset line. We handle that by triggering - // our exit event fd. Meaning Firecracker will be exiting as soon as the VMM - // thread wakes up to handle this event. - if let Err(e) = self.reset_evt.write(1) { - error!("Failed to trigger i8042 reset event: {e:?}"); - } - } - OFS_STATUS if data[0] == CMD_READ_CTR => { - // The guest wants to read the control register. - // Let's make sure only the control register will be available for reading from - // the data port, for the next inb(0x60). - self.flush_buf(); - let control = self.control; - // Buffer is empty, push() will always succeed. - self.push_byte(control).unwrap(); - } - OFS_STATUS if data[0] == CMD_WRITE_CTR => { - // The guest wants to write the control register. This is a two-step command: - // 1. port 0x64 < CMD_WRITE_CTR - // 2. port 0x60 < - // Make sure we'll be expecting the control reg value on port 0x60 for the next - // write. - self.flush_buf(); - self.status |= SB_I8042_CMD_DATA; - self.cmd = data[0]; - } - OFS_STATUS if data[0] == CMD_READ_OUTP => { - // The guest wants to read the output port (for lack of a better name - this is - // just another register on the 8042, that happens to also have its bits connected - // to some output pins of the 8042). - self.flush_buf(); - let outp = self.outp; - // Buffer is empty, push() will always succeed. - self.push_byte(outp).unwrap(); - } - OFS_STATUS if data[0] == CMD_WRITE_OUTP => { - // Similar to writing the control register, this is a two-step command. - // I.e. write CMD_WRITE_OUTP at port 0x64, then write the actual out port value - // to port 0x60. - self.status |= SB_I8042_CMD_DATA; - self.cmd = data[0]; - } - OFS_DATA if (self.status & SB_I8042_CMD_DATA) != 0 => { - // The guest is writing to port 0x60. This byte can either be: - // 1. the payload byte of a CMD_WRITE_CTR or CMD_WRITE_OUTP command, in which case - // the status reg bit SB_I8042_CMD_DATA will be set, or - // 2. a direct command sent to the keyboard - // This match arm handles the first option (when the SB_I8042_CMD_DATA bit is set). - match self.cmd { - CMD_WRITE_CTR => self.control = data[0], - CMD_WRITE_OUTP => self.outp = data[0], - _ => (), - } - self.status &= !SB_I8042_CMD_DATA; - } - OFS_DATA => { - // The guest is sending a command straight to the keyboard (so this byte is not - // addressed to the 8042, but to the keyboard). Since we're emulating a pretty - // dumb keyboard, we can get away with blindly ack-in anything (byte 0xFA). - // Something along the lines of "Yeah, uhm-uhm, yeah, okay, honey, that's great." - self.flush_buf(); - // Buffer is empty, push() will always succeed. - self.push_byte(0xFA).unwrap(); - if let Err(Error::KbdInterruptFailure(err)) = self.trigger_kbd_interrupt() { - warn!("Failed to trigger i8042 kbd interrupt {err:?}"); - } - } - _ => {} - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - impl PartialEq for Error { - fn eq(&self, other: &Error) -> bool { - self.to_string() == other.to_string() - } - } - - #[test] - fn test_i8042_read_write_and_event() { - let mut i8042 = I8042Device::new( - EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap(), - EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap(), - ); - let reset_evt = i8042.get_reset_evt_clone().unwrap(); - - // Check if reading in a 2-length array doesn't have side effects. - let mut data = [1, 2]; - i8042.read(0, 0, &mut data); - assert_eq!(data, [1, 2]); - i8042.read(0, 1, &mut data); - assert_eq!(data, [1, 2]); - - // Check if reset works. - // Write 1 to the reset event fd, so that read doesn't block in case the event fd - // counter doesn't change (for 0 it blocks). - assert!(reset_evt.write(1).is_ok()); - let mut data = [CMD_RESET_CPU]; - i8042.write(0, OFS_STATUS, &data); - assert_eq!(reset_evt.read().unwrap(), 2); - - // Check if reading with offset 1 doesn't have side effects. - i8042.read(0, 1, &mut data); - assert_eq!(data[0], CMD_RESET_CPU); - } - - #[test] - fn test_i8042_commands() { - let mut i8042 = I8042Device::new( - EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap(), - EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap(), - ); - let mut data = [1]; - - // Test reading/writing the control register. - data[0] = CMD_WRITE_CTR; - i8042.write(0, OFS_STATUS, &data); - assert_ne!(i8042.status & SB_I8042_CMD_DATA, 0); - data[0] = 0x52; - i8042.write(0, OFS_DATA, &data); - data[0] = CMD_READ_CTR; - i8042.write(0, OFS_STATUS, &data); - assert_ne!(i8042.status & SB_OUT_DATA_AVAIL, 0); - i8042.read(0, OFS_DATA, &mut data); - assert_eq!(data[0], 0x52); - - // Test reading/writing the output port. - data[0] = CMD_WRITE_OUTP; - i8042.write(0, OFS_STATUS, &data); - assert_ne!(i8042.status & SB_I8042_CMD_DATA, 0); - data[0] = 0x52; - i8042.write(0, OFS_DATA, &data); - data[0] = CMD_READ_OUTP; - i8042.write(0, OFS_STATUS, &data); - assert_ne!(i8042.status & SB_OUT_DATA_AVAIL, 0); - i8042.read(0, OFS_DATA, &mut data); - assert_eq!(data[0], 0x52); - - // Test kbd commands. - data[0] = 0x52; - i8042.write(0, OFS_DATA, &data); - assert_ne!(i8042.status & SB_OUT_DATA_AVAIL, 0); - i8042.read(0, OFS_DATA, &mut data); - assert_eq!(data[0], 0xFA); - } - - #[test] - fn test_i8042_buffer() { - let mut i8042 = I8042Device::new( - EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap(), - EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap(), - ); - - // Test push/pop. - i8042.push_byte(52).unwrap(); - assert_ne!(i8042.status & SB_OUT_DATA_AVAIL, 0); - assert_eq!(i8042.pop_byte().unwrap(), 52); - assert_eq!(i8042.status & SB_OUT_DATA_AVAIL, 0); - - // Test empty buffer pop. - assert!(i8042.pop_byte().is_none()); - - // Test buffer full. - for i in 0..BUF_SIZE { - i8042.push_byte(i as u8).unwrap(); - assert_eq!(i8042.buf_len(), i + 1); - } - assert_eq!(i8042.push_byte(0).unwrap_err(), Error::InternalBufferFull); - } - - #[test] - fn test_i8042_kbd() { - let mut i8042 = I8042Device::new( - EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap(), - EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap(), - ); - - fn expect_key(i8042: &mut I8042Device, key: u16) { - let mut data = [1]; - - // The interrupt line should be on. - i8042.trigger_kbd_interrupt().unwrap(); - assert!(i8042.kbd_interrupt_evt.read().unwrap() > 1); - - // The "data available" flag should be on. - i8042.read(0, OFS_STATUS, &mut data); - - let mut key_byte: u8; - if key & 0xFF00 != 0 { - // For extended keys, we should be able to read the MSB first. - key_byte = ((key & 0xFF00) >> 8) as u8; - i8042.read(0, OFS_DATA, &mut data); - assert_eq!(data[0], key_byte); - - // And then do the same for the LSB. - - // The interrupt line should be on. - i8042.trigger_kbd_interrupt().unwrap(); - assert!(i8042.kbd_interrupt_evt.read().unwrap() > 1); - // The "data available" flag should be on. - i8042.read(0, OFS_STATUS, &mut data); - } - key_byte = (key & 0xFF) as u8; - i8042.read(0, OFS_DATA, &mut data); - assert_eq!(data[0], key_byte); - } - - // Test key trigger. - i8042.trigger_key(KEY_CTRL).unwrap(); - expect_key(&mut i8042, KEY_CTRL); - - // Test extended key trigger. - i8042.trigger_key(KEY_DEL).unwrap(); - expect_key(&mut i8042, KEY_DEL); - - // Test CTRL+ALT+DEL trigger. - i8042.trigger_ctrl_alt_del().unwrap(); - expect_key(&mut i8042, KEY_CTRL); - expect_key(&mut i8042, KEY_ALT); - expect_key(&mut i8042, KEY_DEL); - - // Almost fill up the buffer, so we can test trigger failures. - for _i in 0..BUF_SIZE - 1 { - i8042.push_byte(1).unwrap(); - } - - // Test extended key trigger failure. - assert_eq!(i8042.buf_len(), BUF_SIZE - 1); - assert_eq!( - i8042.trigger_key(KEY_DEL).unwrap_err(), - Error::InternalBufferFull - ); - - // Test ctrl+alt+del trigger failure. - i8042.pop_byte().unwrap(); - i8042.pop_byte().unwrap(); - assert_eq!(i8042.buf_len(), BUF_SIZE - 3); - assert_eq!( - i8042.trigger_ctrl_alt_del().unwrap_err(), - Error::InternalBufferFull - ); - - // Test kbd interrupt disable. - let mut data = [1]; - data[0] = CMD_WRITE_CTR; - i8042.write(0, OFS_STATUS, &data); - data[0] = i8042.control & !CB_KBD_INT; - i8042.write(0, OFS_DATA, &data); - i8042.trigger_key(KEY_CTRL).unwrap(); - assert_eq!( - i8042.trigger_kbd_interrupt().unwrap_err(), - Error::KbdInterruptDisabled - ) - } -} diff --git a/vendor/krun-devices/src/legacy/ioapic.rs b/vendor/krun-devices/src/legacy/ioapic.rs deleted file mode 100644 index af752c906..000000000 --- a/vendor/krun-devices/src/legacy/ioapic.rs +++ /dev/null @@ -1,472 +0,0 @@ -use crossbeam_channel::unbounded; -#[cfg(not(feature = "tdx"))] -use kvm_bindings::{kvm_enable_cap, KVM_CAP_SPLIT_IRQCHIP}; -use kvm_bindings::{ - kvm_irq_routing_entry, kvm_irq_routing_entry__bindgen_ty_1, kvm_irq_routing_msi, KvmIrqRouting, - KVM_IRQ_ROUTING_MSI, -}; - -use kvm_ioctls::{Error, VmFd}; - -use utils::eventfd::EventFd; -use utils::worker_message::WorkerMessage; - -use crate::bus::BusDevice; -use crate::legacy::irqchip::IrqChipT; -use crate::Error as DeviceError; - -const IOAPIC_BASE: u32 = 0xfec0_0000; -const APIC_DEFAULT_ADDRESS: u32 = 0xfee0_0000; -const IOAPIC_NUM_PINS: usize = 24; - -const IO_REG_SEL: u64 = 0x00; -const IO_WIN: u64 = 0x10; -const IO_EOI: u64 = 0x40; - -const IO_APIC_ID: u8 = 0x00; -const IO_APIC_VER: u8 = 0x01; -const IO_APIC_ARB: u8 = 0x02; - -const IOAPIC_LVT_DELIV_MODE_SHIFT: u64 = 8; -const IOAPIC_LVT_DEST_MODE_SHIFT: u64 = 11; -const IOAPIC_LVT_DELIV_STATUS_SHIFT: u64 = 12; -const IOAPIC_LVT_REMOTE_IRR_SHIFT: u64 = 14; -const IOAPIC_LVT_TRIGGER_MODE_SHIFT: u64 = 15; -const IOAPIC_LVT_MASKED_SHIFT: u64 = 16; -const IOAPIC_LVT_DEST_IDX_SHIFT: u64 = 48; - -const IOAPIC_VER_ENTRIES_SHIFT: u64 = 16; -const IOAPIC_ID_SHIFT: u64 = 24; - -const MSI_DATA_VECTOR_SHIFT: u64 = 0; -const MSI_ADDR_DEST_MODE_SHIFT: u64 = 2; -const MSI_ADDR_DEST_IDX_SHIFT: u64 = 4; -const MSI_DATA_DELIVERY_MODE_SHIFT: u64 = 8; -const MSI_DATA_TRIGGER_SHIFT: u64 = 15; - -const IOAPIC_LVT_REMOTE_IRR: u64 = 1 << IOAPIC_LVT_REMOTE_IRR_SHIFT; -const IOAPIC_LVT_TRIGGER_MODE: u64 = 1 << IOAPIC_LVT_TRIGGER_MODE_SHIFT; -const IOAPIC_LVT_DELIV_STATUS: u64 = 1 << IOAPIC_LVT_DELIV_STATUS_SHIFT; - -const IOAPIC_RO_BITS: u64 = IOAPIC_LVT_REMOTE_IRR | IOAPIC_LVT_DELIV_STATUS; -const IOAPIC_RW_BITS: u64 = !IOAPIC_RO_BITS; - -const IOAPIC_DM_MASK: u64 = 0x7; -const IOAPIC_ID_MASK: u64 = 0xf; -const IOAPIC_VECTOR_MASK: u64 = 0xff; - -const IOAPIC_DM_EXTINT: u64 = 0x7; -const IOAPIC_REG_REDTBL_BASE: u64 = 0x10; - -const IOAPIC_TRIGGER_EDGE: u64 = 0; - -/// 63:56 Destination Field (RW) -/// 55:17 Reserved -/// 16 Interrupt Mask (RW) -/// 15 Trigger Mode (RW) -/// 14 Remote IRR (RO) -/// 13 Interrupt Input Pin Polarity (INTPOL) (RW) -/// 12 Delivery Status (DELIVS) (RO) -/// 11 Destination Mode (DESTMOD) (RW) -/// 10:8 Delivery Mode (DELMOD) (RW) -/// 7:0 Interrupt Vector (INTVEC) (RW) -type RedirectionTableEntry = u64; - -#[derive(Debug, Default)] -pub struct IoApicEntryInfo { - masked: u8, - trig_mode: u8, - _dest_idx: u16, - _dest_mode: u8, - _delivery_mode: u8, - _vector: u8, - - addr: u32, - data: u32, -} - -#[derive(Default)] -struct MsiMessage { - address: u64, - data: u64, -} - -#[derive(Debug)] -pub struct IoApic { - id: u8, - ioregsel: u8, - irr: u32, - ioredtbl: [u64; IOAPIC_NUM_PINS], - version: u8, - irq_eoi: [i32; IOAPIC_NUM_PINS], - irq_routes: Vec, - irq_sender: crossbeam_channel::Sender, -} - -impl IoApic { - pub fn new( - vm: &VmFd, - _irq_sender: crossbeam_channel::Sender, - ) -> Result { - #[cfg(not(feature = "tdx"))] - { - let mut cap = kvm_enable_cap { - cap: KVM_CAP_SPLIT_IRQCHIP, - ..Default::default() - }; - cap.args[0] = 24; - vm.enable_cap(&cap)?; - } - - let mut ioapic = Self { - id: 0, - ioregsel: 0, - irr: 0, - ioredtbl: [1 << IOAPIC_LVT_MASKED_SHIFT; IOAPIC_NUM_PINS], - version: 0x20, - irq_eoi: [0; IOAPIC_NUM_PINS], - irq_routes: Vec::with_capacity(IOAPIC_NUM_PINS), - irq_sender: _irq_sender, - }; - - (0..IOAPIC_NUM_PINS).for_each(|i| ioapic.add_msi_route(i)); - - let mut routing = KvmIrqRouting::new(ioapic.irq_routes.len()).unwrap(); - let routing_entires = routing.as_mut_slice(); - routing_entires.copy_from_slice(ioapic.irq_routes.as_slice()); - vm.set_gsi_routing(&routing)?; - - Ok(ioapic) - } - - fn add_msi_route(&mut self, virq: usize) { - let msg = MsiMessage::default(); - let kroute = kvm_irq_routing_entry { - gsi: virq as u32, - type_: KVM_IRQ_ROUTING_MSI, - flags: 0, - u: kvm_irq_routing_entry__bindgen_ty_1 { - msi: kvm_irq_routing_msi { - address_lo: msg.address as u32, - address_hi: (msg.address >> 32) as u32, - data: msg.data as u32, - ..Default::default() - }, - }, - ..Default::default() - }; - - // 4095 is the max irq number for kvm (MAX_IRQ_ROUTES - 1) - if self.irq_routes.len() < 4095 { - self.irq_routes.push(kroute); - } else { - error!("ioapic: not enough space for irq"); - } - } - - fn fix_edge_remote_irr(&mut self, index: usize) { - if self.ioredtbl[index] & IOAPIC_LVT_TRIGGER_MODE == IOAPIC_TRIGGER_EDGE { - self.ioredtbl[index] &= !IOAPIC_LVT_REMOTE_IRR; - } - } - - fn parse_entry(&self, entry: &RedirectionTableEntry) -> IoApicEntryInfo { - let vector = (entry & IOAPIC_VECTOR_MASK) as u8; - let dest_idx = ((entry >> IOAPIC_LVT_DEST_IDX_SHIFT) & 0xffff) as u16; - let delivery_mode = ((entry >> IOAPIC_LVT_DELIV_MODE_SHIFT) & IOAPIC_DM_MASK) as u8; - let trig_mode = ((entry >> IOAPIC_LVT_TRIGGER_MODE_SHIFT) & 1) as u8; - let dest_mode = ((entry >> IOAPIC_LVT_DEST_MODE_SHIFT) & 1) as u8; - - if delivery_mode as u64 == IOAPIC_DM_EXTINT { - panic!("ioapic: libkrun does not have PIC support"); - } - - IoApicEntryInfo { - masked: ((entry >> IOAPIC_LVT_MASKED_SHIFT) & 1) as u8, - trig_mode, - _dest_idx: dest_idx, - _dest_mode: dest_mode, - _delivery_mode: delivery_mode, - _vector: vector, - - addr: ((APIC_DEFAULT_ADDRESS as u64) - | ((dest_idx as u64) << MSI_ADDR_DEST_IDX_SHIFT) - | ((dest_mode as u64) << MSI_ADDR_DEST_MODE_SHIFT)) as u32, - data: (((vector as u64) << MSI_DATA_VECTOR_SHIFT) - | ((trig_mode as u64) << MSI_DATA_TRIGGER_SHIFT) - | ((delivery_mode as u64) << MSI_DATA_DELIVERY_MODE_SHIFT)) - as u32, - } - } - - fn update_msi_route(&mut self, virq: usize, msg: &MsiMessage) { - let kroute = kvm_irq_routing_entry { - gsi: virq as u32, - type_: KVM_IRQ_ROUTING_MSI, - flags: 0, - u: kvm_irq_routing_entry__bindgen_ty_1 { - msi: kvm_irq_routing_msi { - address_lo: msg.address as u32, - address_hi: (msg.address >> 32) as u32, - data: msg.data as u32, - ..Default::default() - }, - }, - ..Default::default() - }; - - for entry in self.irq_routes.iter_mut() { - if entry.gsi == kroute.gsi { - *entry = kroute; - } - } - } - - fn update_routes(&mut self) { - for i in 0..IOAPIC_NUM_PINS { - let info = self.parse_entry(&self.ioredtbl[i]); - - if info.masked == 0 { - let msg = MsiMessage { - address: info.addr as u64, - data: info.data as u64, - }; - - self.update_msi_route(i, &msg); - } - } - - let (response_sender, response_receiver) = unbounded(); - self.irq_sender - .send(WorkerMessage::GsiRoute( - response_sender.clone(), - self.irq_routes.clone(), - )) - .unwrap(); - if !response_receiver.recv().unwrap() { - error!("unable to set GSI Routes for IO APIC"); - } - } - - fn service(&mut self) { - for i in 0..IOAPIC_NUM_PINS { - let mask = 1 << i; - - if self.irr & mask > 0 { - let mut coalesce = 0; - - let entry = self.ioredtbl[i]; - let info = self.parse_entry(&entry); - if info.masked == 0 { - if info.trig_mode as u64 == IOAPIC_TRIGGER_EDGE { - self.irr &= !mask; - } else { - coalesce = self.ioredtbl[i] & IOAPIC_LVT_REMOTE_IRR; - self.ioredtbl[i] |= IOAPIC_LVT_REMOTE_IRR; - } - - if coalesce > 0 { - continue; - } - - let (response_sender, response_receiver) = unbounded(); - if info.trig_mode as u64 == IOAPIC_TRIGGER_EDGE { - self.irq_sender - .send(WorkerMessage::IrqLine( - response_sender.clone(), - i as u32, - true, - )) - .unwrap(); - if !response_receiver.recv().unwrap() { - error!( - "unable to set IRQ LINE for IRQ {} with active set to {}", - i, true - ); - } - - self.irq_sender - .send(WorkerMessage::IrqLine( - response_sender.clone(), - i as u32, - false, - )) - .unwrap(); - if !response_receiver.recv().unwrap() { - error!( - "unable to set IRQ LINE for IRQ {} with active set to {}", - i, false - ); - } - } else { - self.irq_sender - .send(WorkerMessage::IrqLine( - response_sender.clone(), - i as u32, - true, - )) - .unwrap(); - if !response_receiver.recv().unwrap() { - error!( - "unable to set IRQ LINE for IRQ {} with active set to {}", - i, true - ); - } - } - } - } - } - } -} - -impl IrqChipT for IoApic { - fn get_mmio_addr(&self) -> u64 { - IOAPIC_BASE as u64 - } - - fn get_mmio_size(&self) -> u64 { - 0x1000 - } - - fn set_irq( - &self, - _irq_line: Option, - interrupt_evt: Option<&EventFd>, - ) -> Result<(), DeviceError> { - if let Some(interrupt_evt) = interrupt_evt { - if let Err(e) = interrupt_evt.write(1) { - error!("Failed to signal used queue: {e:?}"); - return Err(DeviceError::FailedSignalingUsedQueue(e)); - } - } else { - error!("EventFd not set up for irq line"); - return Err(DeviceError::FailedSignalingUsedQueue(std::io::Error::new( - std::io::ErrorKind::NotFound, - "EventFd not set up for irq line", - ))); - } - Ok(()) - } -} - -impl BusDevice for IoApic { - fn read(&mut self, _vcpuid: u64, offset: u64, data: &mut [u8]) { - let val = match offset { - IO_REG_SEL => { - debug!("ioapic: read: ioregsel"); - self.ioregsel as u32 - } - IO_WIN => { - // the data needs to be 32-bits in size - if data.len() != 4 { - error!("ioapic: bad read size {}", data.len()); - return; - } - - match self.ioregsel { - IO_APIC_ID | IO_APIC_ARB => { - debug!("ioapic: read: IOAPIC ID"); - ((self.id as u64) << IOAPIC_ID_SHIFT) as u32 - } - IO_APIC_VER => { - debug!("ioapic: read: IOAPIC version"); - self.version as u32 - | ((IOAPIC_NUM_PINS as u32 - 1) << IOAPIC_VER_ENTRIES_SHIFT) - } - _ => { - let index = (self.ioregsel as u64 - IOAPIC_REG_REDTBL_BASE) >> 1; - debug!("ioapic: read: ioredtbl register {index}"); - let mut val = 0u32; - - // we can only read from this register in 32-bit chunks. - // Therefore, we need to check if we are reading the - // upper 32 bits or the lower - if index < IOAPIC_NUM_PINS as u64 { - if self.ioregsel & 1 > 0 { - // read upper 32 bits - val = (self.ioredtbl[index as usize] >> 32) as u32; - } else { - // read lower 32 bits - val = (self.ioredtbl[index as usize] & 0xffff_ffffu64) as u32; - } - } - val - } - } - } - _ => unreachable!(), - }; - - // turn the value into native endian byte order and put that value into `data` - let out_arr = val.to_ne_bytes(); - for i in 0..4 { - if i < data.len() { - data[i] = out_arr[i]; - } - } - } - - fn write(&mut self, _vcpuid: u64, offset: u64, data: &[u8]) { - // data needs to be 32-bits in size - if data.len() != 4 { - error!("ioapic: bad write size {}", data.len()); - return; - } - - // convert data into a u32 int with native endianness - let arr = [data[0], data[1], data[2], data[3]]; - let val = u32::from_ne_bytes(arr); - match offset { - IO_REG_SEL => { - debug!("ioapic: write: ioregsel"); - self.ioregsel = val as u8 - } - IO_WIN => { - match self.ioregsel { - IO_APIC_ID => { - debug!("ioapic: write: IOAPIC ID"); - self.id = ((val >> IOAPIC_ID_SHIFT) & (IOAPIC_ID_MASK as u32)) as u8 - } - // NOTE: these are read-only registers, so they should never be written to - IO_APIC_VER | IO_APIC_ARB => debug!("ioapic: write: IOAPIC VERSION"), - _ => { - if self.ioregsel < (IO_WIN as u8) { - debug!("invalid write; ignore"); - return; - } - - let index = (self.ioregsel as u64 - IOAPIC_REG_REDTBL_BASE) >> 1; - debug!("ioapic: write: ioredtbl register {index}"); - if index >= IOAPIC_NUM_PINS as u64 { - warn!("ioapic: write: virq out of pin range {index}"); - return; - } - - let ro_bits = self.ioredtbl[index as usize] & IOAPIC_RO_BITS; - // check if we are writing to the upper 32-bits of the - // register or the lower 32-bits - if self.ioregsel & 1 > 0 { - self.ioredtbl[index as usize] &= 0xffff_ffff; - self.ioredtbl[index as usize] |= (val as u64) << 32; - } else { - self.ioredtbl[index as usize] &= !0xffff_ffff; - self.ioredtbl[index as usize] |= val as u64; - } - - // restore RO bits - self.ioredtbl[index as usize] &= IOAPIC_RW_BITS; - self.ioredtbl[index as usize] |= ro_bits; - self.irq_eoi[index as usize] = 0; - - // if the trigger mode is EDGE, clear IRR bit - self.fix_edge_remote_irr(index as usize); - self.update_routes(); - self.service(); - } - } - } - IO_EOI => todo!(), - _ => unreachable!(), - } - } -} diff --git a/vendor/krun-devices/src/legacy/irqchip.rs b/vendor/krun-devices/src/legacy/irqchip.rs deleted file mode 100644 index ed33bd2a8..000000000 --- a/vendor/krun-devices/src/legacy/irqchip.rs +++ /dev/null @@ -1,227 +0,0 @@ -use std::sync::{Arc, Mutex}; - -use crate::bus::BusDevice; -#[cfg(target_arch = "riscv64")] -use crate::legacy::aia::AIADevice; -#[cfg(target_arch = "aarch64")] -use crate::legacy::gic::GICDevice; -use crate::Error as DeviceError; - -use utils::eventfd::EventFd; - -pub type IrqChip = Arc>; - -pub struct IrqChipDevice { - inner: Box, -} - -impl IrqChipDevice { - pub fn new(irqchip: Box) -> Self { - Self { inner: irqchip } - } - - pub fn get_mmio_addr(&self) -> u64 { - self.inner.get_mmio_addr() - } - - pub fn get_mmio_size(&self) -> u64 { - self.inner.get_mmio_size() - } - - pub fn set_irq( - &self, - irq_line: Option, - interrupt_evt: Option<&EventFd>, - ) -> Result<(), DeviceError> { - self.inner.set_irq(irq_line, interrupt_evt) - } -} - -impl BusDevice for IrqChipDevice { - fn read(&mut self, vcpuid: u64, offset: u64, data: &mut [u8]) { - self.inner.read(vcpuid, offset, data) - } - - fn write(&mut self, vcpuid: u64, offset: u64, data: &[u8]) { - self.inner.write(vcpuid, offset, data) - } -} - -#[cfg(target_arch = "aarch64")] -impl GICDevice for IrqChipDevice { - /// Returns an array with GIC device properties - fn device_properties(&self) -> Vec { - self.inner.device_properties().clone() - } - - /// Returns the number of vCPUs this GIC handles - fn vcpu_count(&self) -> u64 { - self.inner.vcpu_count() - } - - /// Returns the fdt compatibility property of the device - fn fdt_compatibility(&self) -> String { - self.inner.fdt_compatibility().clone() - } - - /// Returns the maint_irq fdt property of the device - fn fdt_maint_irq(&self) -> u32 { - self.inner.fdt_maint_irq() - } - - /// Returns the GIC version of the device - fn version(&self) -> u32 { - self.inner.version() - } -} - -#[cfg(target_arch = "riscv64")] -impl AIADevice for IrqChipDevice { - fn aplic_compatibility(&self) -> &str { - "riscv,aplic" - } - - fn aplic_properties(&self) -> [u32; 4] { - [ - 0, - arch::riscv64::layout::APLIC_START as u32, - 0, - kvm_bindings::KVM_DEV_RISCV_APLIC_SIZE, - ] - } - - fn imsic_compatibility(&self) -> &str { - "riscv,imsics" - } - - fn imsic_properties(&self) -> [u32; 4] { - [ - 0, - arch::riscv64::layout::IMSIC_START as u32, - 0, - kvm_bindings::KVM_DEV_RISCV_IMSIC_SIZE * self.vcpu_count(), - ] - } - - fn vcpu_count(&self) -> u32 { - self.inner.vcpu_count() - } - - fn msi_compatible(&self) -> bool { - true - } -} - -#[cfg(target_arch = "x86_64")] -pub trait IrqChipT: BusDevice { - fn get_mmio_addr(&self) -> u64; - fn get_mmio_size(&self) -> u64; - fn set_irq( - &self, - irq_line: Option, - interrupt_evt: Option<&EventFd>, - ) -> Result<(), DeviceError>; -} - -#[cfg(target_arch = "aarch64")] -pub trait IrqChipT: BusDevice + GICDevice { - fn get_mmio_addr(&self) -> u64; - fn get_mmio_size(&self) -> u64; - fn set_irq( - &self, - irq_line: Option, - interrupt_evt: Option<&EventFd>, - ) -> Result<(), DeviceError>; -} - -#[cfg(target_arch = "riscv64")] -pub trait IrqChipT: BusDevice + AIADevice { - fn get_mmio_addr(&self) -> u64; - fn get_mmio_size(&self) -> u64; - fn set_irq( - &self, - irq_line: Option, - interrupt_evt: Option<&EventFd>, - ) -> Result<(), DeviceError>; -} - -#[cfg(any(test, feature = "test_utils"))] -pub mod test_utils { - use super::*; - - #[derive(Clone, Default, Debug)] - pub struct DummyIrqChip {} - - impl DummyIrqChip { - pub fn new() -> Self { - Default::default() - } - } - - impl Into for DummyIrqChip { - fn into(self) -> IrqChip { - Arc::new(Mutex::new(IrqChipDevice::new( - Box::new(DummyIrqChip::new()), - ))) - } - } - - #[cfg(target_arch = "aarch64")] - impl GICDevice for DummyIrqChip { - fn device_properties(&self) -> Vec { - vec![] - } - fn vcpu_count(&self) -> u64 { - 0 - } - fn fdt_compatibility(&self) -> String { - "vendor,dummy-gic".into() - } - fn fdt_maint_irq(&self) -> u32 { - 0 - } - fn version(&self) -> u32 { - 0 - } - } - - #[cfg(target_arch = "riscv64")] - impl AIADevice for DummyIrqChip { - fn aplic_compatibility(&self) -> &str { - "riscv,aplic" - } - fn aplic_properties(&self) -> [u32; 4] { - [0, 0, 0, 0] - } - fn imsic_compatibility(&self) -> &str { - "riscv,imsics" - } - fn imsic_properties(&self) -> [u32; 4] { - [0, 0, 0, 0] - } - fn vcpu_count(&self) -> u32 { - 0 - } - fn msi_compatible(&self) -> bool { - false - } - } - - impl BusDevice for DummyIrqChip {} - - impl IrqChipT for DummyIrqChip { - fn get_mmio_addr(&self) -> u64 { - 0 - } - fn get_mmio_size(&self) -> u64 { - 0 - } - fn set_irq( - &self, - _irq_line: Option, - _interrupt_evt: Option<&EventFd>, - ) -> Result<(), DeviceError> { - Ok(()) - } - } -} diff --git a/vendor/krun-devices/src/legacy/kvmaia.rs b/vendor/krun-devices/src/legacy/kvmaia.rs deleted file mode 100644 index 17763dfb9..000000000 --- a/vendor/krun-devices/src/legacy/kvmaia.rs +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright 2025 The libkrun Authors. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -use std::io; - -use crate::bus::BusDevice; -use crate::legacy::aia::AIADevice; -use crate::legacy::irqchip::IrqChipT; -use crate::Error as DeviceError; - -use kvm_ioctls::{DeviceFd, VmFd}; -use utils::eventfd::EventFd; - -pub struct KvmAia { - _device_fd: DeviceFd, - - /// Number of CPUs handled by the device - vcpu_count: u32, -} - -impl KvmAia { - pub fn new(vm: &VmFd, vcpu_count: u32) -> Result { - // Create a KVM AIA device - let mut aia_device = kvm_bindings::kvm_create_device { - type_: kvm_bindings::kvm_device_type_KVM_DEV_TYPE_RISCV_AIA, - fd: 0, - flags: 0, - }; - let device_fd = vm.create_device(&mut aia_device).unwrap(); - - // Setting up the number of wired interrupt sources - let nr_irqs: u32 = arch::riscv64::layout::IRQ_MAX - arch::riscv64::layout::IRQ_BASE; - let nr_irqs_ptr = &nr_irqs as *const u32; - let attr = kvm_bindings::kvm_device_attr { - group: kvm_bindings::KVM_DEV_RISCV_AIA_GRP_CONFIG, - attr: u64::from(kvm_bindings::KVM_DEV_RISCV_AIA_CONFIG_SRCS), - addr: nr_irqs_ptr as u64, - flags: 0, - }; - device_fd.set_device_attr(&attr).unwrap(); - - // Setting up hart_bits - let max_hart_index = vcpu_count as u64 - 1; - let hart_bits = std::cmp::max(64 - max_hart_index.leading_zeros(), 1); - let hart_bits_ptr = &hart_bits as *const u32; - let attr = kvm_bindings::kvm_device_attr { - group: kvm_bindings::KVM_DEV_RISCV_AIA_GRP_CONFIG, - attr: u64::from(kvm_bindings::KVM_DEV_RISCV_AIA_CONFIG_HART_BITS), - addr: hart_bits_ptr as u64, - flags: 0, - }; - device_fd.set_device_attr(&attr).unwrap(); - - // Designate addresses of APLIC and IMSICS - - // Setting up RISC-V APLIC - let aplic_addr = arch::riscv64::layout::APLIC_START; - let aplic_addr_ptr = &aplic_addr as *const u64; - let attr = kvm_bindings::kvm_device_attr { - group: kvm_bindings::KVM_DEV_RISCV_AIA_GRP_ADDR, - attr: u64::from(kvm_bindings::KVM_DEV_RISCV_AIA_ADDR_APLIC), - addr: aplic_addr_ptr as u64, - flags: 0, - }; - device_fd.set_device_attr(&attr).unwrap(); - - // Setting up RISC-V IMSICs - for cpu_index in 0..vcpu_count { - let cpu_imsic_addr = arch::riscv64::layout::IMSIC_START - + (cpu_index * kvm_bindings::KVM_DEV_RISCV_IMSIC_SIZE) as u64; - let cpu_imsic_addr_ptr = &cpu_imsic_addr as *const u64; - let attr = kvm_bindings::kvm_device_attr { - group: kvm_bindings::KVM_DEV_RISCV_AIA_GRP_ADDR, - attr: cpu_index as u64 + 1, - addr: cpu_imsic_addr_ptr as u64, - flags: 0, - }; - device_fd.set_device_attr(&attr).unwrap(); - } - - // Finalizing the AIA device - let attr = kvm_bindings::kvm_device_attr { - group: kvm_bindings::KVM_DEV_RISCV_AIA_GRP_CTRL, - attr: u64::from(kvm_bindings::KVM_DEV_RISCV_AIA_CTRL_INIT), - addr: 0, - flags: 0, - }; - device_fd.set_device_attr(&attr).unwrap(); - - Ok(Self { - _device_fd: device_fd, - vcpu_count, - }) - } -} - -impl IrqChipT for KvmAia { - fn get_mmio_addr(&self) -> u64 { - 0 - } - - fn get_mmio_size(&self) -> u64 { - 0 - } - - fn set_irq( - &self, - _irq_line: Option, - interrupt_evt: Option<&EventFd>, - ) -> Result<(), DeviceError> { - if let Some(interrupt_evt) = interrupt_evt { - if let Err(e) = interrupt_evt.write(1) { - error!("Failed to signal used queue: {e:?}"); - return Err(DeviceError::FailedSignalingUsedQueue(e)); - } - } else { - error!("EventFd not set up for irq line"); - return Err(DeviceError::FailedSignalingUsedQueue(io::Error::new( - io::ErrorKind::NotFound, - "EventFd not set up for irq line".to_string(), - ))); - } - Ok(()) - } -} - -impl BusDevice for KvmAia { - fn read(&mut self, _vcpuid: u64, _offset: u64, _data: &mut [u8]) { - unreachable!("MMIO operations are managed in-kernel"); - } - - fn write(&mut self, _vcpuid: u64, _offset: u64, _data: &[u8]) { - unreachable!("MMIO operations are managed in-kernel"); - } -} - -impl AIADevice for KvmAia { - fn aplic_compatibility(&self) -> &str { - "riscv,aplic" - } - - fn aplic_properties(&self) -> [u32; 4] { - [ - 0, - arch::riscv64::layout::APLIC_START as u32, - 0, - kvm_bindings::KVM_DEV_RISCV_APLIC_SIZE, - ] - } - - fn imsic_compatibility(&self) -> &str { - "riscv,imsics" - } - - fn imsic_properties(&self) -> [u32; 4] { - [ - 0, - arch::riscv64::layout::IMSIC_START as u32, - 0, - kvm_bindings::KVM_DEV_RISCV_IMSIC_SIZE * self.vcpu_count, - ] - } - - fn vcpu_count(&self) -> u32 { - self.vcpu_count - } - - fn msi_compatible(&self) -> bool { - true - } -} diff --git a/vendor/krun-devices/src/legacy/kvmgicv2.rs b/vendor/krun-devices/src/legacy/kvmgicv2.rs deleted file mode 100644 index fcf764f8e..000000000 --- a/vendor/krun-devices/src/legacy/kvmgicv2.rs +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright 2025 The libkrun Authors. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -use std::io; - -use crate::bus::BusDevice; -use crate::legacy::gic::GICDevice; -use crate::legacy::irqchip::IrqChipT; -use crate::Error as DeviceError; - -use kvm_ioctls::{DeviceFd, VmFd}; -use utils::eventfd::EventFd; - -const KVM_VGIC_V2_DIST_SIZE: u64 = 0x1000; -const KVM_VGIC_V2_CPU_SIZE: u64 = 0x2000; - -// Device trees specific constants -const ARCH_GIC_V2_MAINT_IRQ: u32 = 8; - -pub struct KvmGicV2 { - _device_fd: DeviceFd, - - /// GIC device properties, to be used for setting up the fdt entry - properties: [u64; 4], - - /// Number of CPUs handled by the device - vcpu_count: u64, -} - -impl KvmGicV2 { - pub fn new(vm: &VmFd, vcpu_count: u64) -> Self { - let dist_size = KVM_VGIC_V2_DIST_SIZE; - let dist_addr = arch::MMIO_MEM_START - dist_size; - let cpu_size = KVM_VGIC_V2_CPU_SIZE; - let cpu_addr = dist_addr - cpu_size; - - let mut gic_device = kvm_bindings::kvm_create_device { - type_: kvm_bindings::kvm_device_type_KVM_DEV_TYPE_ARM_VGIC_V2, - fd: 0, - flags: 0, - }; - let device_fd = vm.create_device(&mut gic_device).unwrap(); - - let attr = kvm_bindings::kvm_device_attr { - group: kvm_bindings::KVM_DEV_ARM_VGIC_GRP_ADDR, - attr: u64::from(kvm_bindings::KVM_VGIC_V2_ADDR_TYPE_DIST), - addr: &dist_addr as *const u64 as u64, - flags: 0, - }; - device_fd.set_device_attr(&attr).unwrap(); - - let attr = kvm_bindings::kvm_device_attr { - group: kvm_bindings::KVM_DEV_ARM_VGIC_GRP_ADDR, - attr: u64::from(kvm_bindings::KVM_VGIC_V2_ADDR_TYPE_CPU), - addr: &cpu_addr as *const u64 as u64, - flags: 0, - }; - device_fd.set_device_attr(&attr).unwrap(); - - let nr_irqs: u32 = arch::aarch64::layout::IRQ_MAX - arch::aarch64::layout::IRQ_BASE + 1; - let nr_irqs_ptr = &nr_irqs as *const u32; - let attr = kvm_bindings::kvm_device_attr { - group: kvm_bindings::KVM_DEV_ARM_VGIC_GRP_NR_IRQS, - attr: 0, - addr: nr_irqs_ptr as u64, - flags: 0, - }; - device_fd.set_device_attr(&attr).unwrap(); - - let attr = kvm_bindings::kvm_device_attr { - group: kvm_bindings::KVM_DEV_ARM_VGIC_GRP_CTRL, - attr: u64::from(kvm_bindings::KVM_DEV_ARM_VGIC_CTRL_INIT), - addr: 0, - flags: 0, - }; - device_fd.set_device_attr(&attr).unwrap(); - - Self { - _device_fd: device_fd, - properties: [dist_addr, dist_size, cpu_addr, cpu_size], - vcpu_count, - } - } -} - -impl IrqChipT for KvmGicV2 { - fn get_mmio_addr(&self) -> u64 { - 0 - } - - fn get_mmio_size(&self) -> u64 { - 0 - } - - fn set_irq( - &self, - _irq_line: Option, - interrupt_evt: Option<&EventFd>, - ) -> Result<(), DeviceError> { - if let Some(interrupt_evt) = interrupt_evt { - if let Err(e) = interrupt_evt.write(1) { - error!("Failed to signal used queue: {e:?}"); - return Err(DeviceError::FailedSignalingUsedQueue(e)); - } - } else { - error!("EventFd not set up for irq line"); - return Err(DeviceError::FailedSignalingUsedQueue(io::Error::new( - io::ErrorKind::NotFound, - "EventFd not set up for irq line".to_string(), - ))); - } - Ok(()) - } -} - -impl BusDevice for KvmGicV2 { - fn read(&mut self, _vcpuid: u64, _offset: u64, _data: &mut [u8]) { - unreachable!("MMIO operations are managed in-kernel"); - } - - fn write(&mut self, _vcpuid: u64, _offset: u64, _data: &[u8]) { - unreachable!("MMIO operations are managed in-kernel"); - } -} - -impl GICDevice for KvmGicV2 { - fn device_properties(&self) -> Vec { - self.properties.to_vec() - } - - fn vcpu_count(&self) -> u64 { - self.vcpu_count - } - - fn fdt_compatibility(&self) -> String { - "arm,gic-400".to_string() - } - - fn fdt_maint_irq(&self) -> u32 { - ARCH_GIC_V2_MAINT_IRQ - } - - fn version(&self) -> u32 { - kvm_bindings::kvm_device_type_KVM_DEV_TYPE_ARM_VGIC_V2 - } -} diff --git a/vendor/krun-devices/src/legacy/kvmgicv3.rs b/vendor/krun-devices/src/legacy/kvmgicv3.rs deleted file mode 100644 index a25bbb330..000000000 --- a/vendor/krun-devices/src/legacy/kvmgicv3.rs +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -use std::io; - -use crate::bus::BusDevice; -use crate::legacy::gic::GICDevice; -use crate::legacy::irqchip::IrqChipT; -use crate::Error as DeviceError; - -use kvm_ioctls::{DeviceFd, Error, VmFd}; -use utils::eventfd::EventFd; - -const KVM_VGIC_V3_BASE_SIZE: u64 = 0x0001_0000; - -// Device trees specific constants -const ARCH_GIC_V3_MAINT_IRQ: u32 = 9; - -pub struct KvmGicV3 { - _device_fd: DeviceFd, - - /// GIC device properties, to be used for setting up the fdt entry - properties: [u64; 4], - - /// Number of CPUs handled by the device - vcpu_count: u64, -} - -impl KvmGicV3 { - pub fn new(vm: &VmFd, vcpu_count: u64) -> Result { - let dist_size = KVM_VGIC_V3_BASE_SIZE; - let dist_addr = arch::MMIO_MEM_START - dist_size; - let redist_size = 2 * dist_size; - let redists_size = redist_size * vcpu_count; - let redists_addr = dist_addr - redists_size; - - let mut gic_device = kvm_bindings::kvm_create_device { - type_: kvm_bindings::kvm_device_type_KVM_DEV_TYPE_ARM_VGIC_V3, - fd: 0, - flags: 0, - }; - let device_fd = vm.create_device(&mut gic_device)?; - - let attr = kvm_bindings::kvm_device_attr { - group: kvm_bindings::KVM_DEV_ARM_VGIC_GRP_ADDR, - attr: u64::from(kvm_bindings::KVM_VGIC_V3_ADDR_TYPE_DIST), - addr: &dist_addr as *const u64 as u64, - flags: 0, - }; - device_fd.set_device_attr(&attr)?; - - let attr = kvm_bindings::kvm_device_attr { - group: kvm_bindings::KVM_DEV_ARM_VGIC_GRP_ADDR, - attr: u64::from(kvm_bindings::KVM_VGIC_V3_ADDR_TYPE_REDIST), - addr: &redists_addr as *const u64 as u64, - flags: 0, - }; - device_fd.set_device_attr(&attr)?; - - let nr_irqs: u32 = arch::aarch64::layout::IRQ_MAX - arch::aarch64::layout::IRQ_BASE + 1; - let nr_irqs_ptr = &nr_irqs as *const u32; - let attr = kvm_bindings::kvm_device_attr { - group: kvm_bindings::KVM_DEV_ARM_VGIC_GRP_NR_IRQS, - attr: 0, - addr: nr_irqs_ptr as u64, - flags: 0, - }; - device_fd.set_device_attr(&attr)?; - - let attr = kvm_bindings::kvm_device_attr { - group: kvm_bindings::KVM_DEV_ARM_VGIC_GRP_CTRL, - attr: u64::from(kvm_bindings::KVM_DEV_ARM_VGIC_CTRL_INIT), - addr: 0, - flags: 0, - }; - device_fd.set_device_attr(&attr)?; - - Ok(Self { - _device_fd: device_fd, - properties: [dist_addr, dist_size, redists_addr, redists_size], - vcpu_count, - }) - } -} - -impl IrqChipT for KvmGicV3 { - fn get_mmio_addr(&self) -> u64 { - 0 - } - - fn get_mmio_size(&self) -> u64 { - 0 - } - - fn set_irq( - &self, - _irq_line: Option, - interrupt_evt: Option<&EventFd>, - ) -> Result<(), DeviceError> { - if let Some(interrupt_evt) = interrupt_evt { - if let Err(e) = interrupt_evt.write(1) { - error!("Failed to signal used queue: {e:?}"); - return Err(DeviceError::FailedSignalingUsedQueue(e)); - } - } else { - error!("EventFd not set up for irq line"); - return Err(DeviceError::FailedSignalingUsedQueue(io::Error::new( - io::ErrorKind::NotFound, - "EventFd not set up for irq line".to_string(), - ))); - } - Ok(()) - } -} - -impl BusDevice for KvmGicV3 { - fn read(&mut self, _vcpuid: u64, _offset: u64, _data: &mut [u8]) { - unreachable!("MMIO operations are managed in-kernel"); - } - - fn write(&mut self, _vcpuid: u64, _offset: u64, _data: &[u8]) { - unreachable!("MMIO operations are managed in-kernel"); - } -} - -impl GICDevice for KvmGicV3 { - fn device_properties(&self) -> Vec { - self.properties.to_vec() - } - - fn vcpu_count(&self) -> u64 { - self.vcpu_count - } - - fn fdt_compatibility(&self) -> String { - "arm,gic-v3".to_string() - } - - fn fdt_maint_irq(&self) -> u32 { - ARCH_GIC_V3_MAINT_IRQ - } - - fn version(&self) -> u32 { - kvm_bindings::kvm_device_type_KVM_DEV_TYPE_ARM_VGIC_V3 - } -} diff --git a/vendor/krun-devices/src/legacy/kvmioapic.rs b/vendor/krun-devices/src/legacy/kvmioapic.rs deleted file mode 100644 index 57b63e7e9..000000000 --- a/vendor/krun-devices/src/legacy/kvmioapic.rs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -use std::io; - -use crate::bus::BusDevice; -use crate::legacy::irqchip::IrqChipT; -use crate::Error as DeviceError; - -use kvm_bindings::{kvm_pit_config, KVM_PIT_SPEAKER_DUMMY}; -use kvm_ioctls::{Error, VmFd}; -use utils::eventfd::EventFd; - -pub struct KvmIoapic {} - -impl KvmIoapic { - pub fn new(vm: &VmFd) -> Result { - vm.create_irq_chip()?; - let pit_config = kvm_pit_config { - // We need to enable the emulation of a dummy speaker port stub so that writing to port - // 0x61 (i.e. KVM_SPEAKER_BASE_ADDRESS) does not trigger an exit to user space. - flags: KVM_PIT_SPEAKER_DUMMY, - ..Default::default() - }; - vm.create_pit2(pit_config)?; - - Ok(Self {}) - } -} - -impl IrqChipT for KvmIoapic { - fn get_mmio_addr(&self) -> u64 { - 0 - } - - fn get_mmio_size(&self) -> u64 { - 0 - } - - fn set_irq( - &self, - _irq_line: Option, - interrupt_evt: Option<&EventFd>, - ) -> Result<(), DeviceError> { - if let Some(interrupt_evt) = interrupt_evt { - if let Err(e) = interrupt_evt.write(1) { - error!("Failed to signal used queue: {e:?}"); - return Err(DeviceError::FailedSignalingUsedQueue(e)); - } - } else { - error!("EventFd not set up for irq line"); - return Err(DeviceError::FailedSignalingUsedQueue(io::Error::new( - io::ErrorKind::NotFound, - "EventFd not set up for irq line", - ))); - } - Ok(()) - } -} - -impl BusDevice for KvmIoapic { - fn read(&mut self, _vcpuid: u64, _offset: u64, _data: &mut [u8]) { - unreachable!("MMIO operations are managed in-kernel"); - } - - fn write(&mut self, _vcpuid: u64, _offset: u64, _data: &[u8]) { - unreachable!("MMIO operations are managed in-kernel"); - } -} diff --git a/vendor/krun-devices/src/legacy/mod.rs b/vendor/krun-devices/src/legacy/mod.rs deleted file mode 100644 index 9fc38ee0f..000000000 --- a/vendor/krun-devices/src/legacy/mod.rs +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -// -// Portions Copyright 2017 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the THIRD-PARTY file. - -pub mod aia; -pub mod gic; -#[cfg(target_os = "macos")] -mod gicv3; -#[cfg(all(target_os = "macos", target_arch = "aarch64"))] -mod hvfgicv3; -#[cfg(target_arch = "x86_64")] -mod i8042; -#[cfg(all(target_os = "linux", target_arch = "x86_64"))] -mod ioapic; -mod irqchip; -#[cfg(all(target_os = "linux", target_arch = "riscv64"))] -mod kvmaia; -#[cfg(all(target_os = "linux", target_arch = "aarch64"))] -mod kvmgicv2; -#[cfg(all(target_os = "linux", target_arch = "aarch64"))] -mod kvmgicv3; -#[cfg(all(target_os = "linux", target_arch = "x86_64"))] -mod kvmioapic; -#[cfg(target_arch = "aarch64")] -mod rtc_pl031; -#[cfg(target_os = "macos")] -mod vcpu; -#[cfg(target_arch = "x86_64")] -mod x86_64; -#[cfg(target_arch = "x86_64")] -use x86_64::cmos; -#[cfg(target_arch = "x86_64")] -use x86_64::serial; -#[cfg(target_arch = "aarch64")] -mod aarch64; -#[cfg(target_arch = "aarch64")] -use aarch64::gpio; -#[cfg(target_arch = "aarch64")] -use aarch64::serial; -#[cfg(target_arch = "riscv64")] -mod riscv64; -#[cfg(target_arch = "riscv64")] -use riscv64::serial; - -#[cfg(target_arch = "x86_64")] -pub use self::cmos::Cmos; -#[cfg(target_os = "macos")] -pub use self::gicv3::GicV3; -#[cfg(target_arch = "aarch64")] -pub use self::gpio::Gpio; -#[cfg(all(target_os = "macos", target_arch = "aarch64"))] -pub use self::hvfgicv3::HvfGicV3; -#[cfg(target_arch = "x86_64")] -pub use self::i8042::{Error as I8042DeviceError, I8042Device}; -#[cfg(all(target_os = "linux", target_arch = "x86_64"))] -pub use self::ioapic::IoApic; -#[cfg(any(test, feature = "test_utils"))] -pub use self::irqchip::test_utils::DummyIrqChip; -pub use self::irqchip::{IrqChip, IrqChipDevice, IrqChipT}; -#[cfg(all(target_os = "linux", target_arch = "riscv64"))] -pub use self::kvmaia::KvmAia; -#[cfg(all(target_os = "linux", target_arch = "aarch64"))] -pub use self::kvmgicv2::KvmGicV2; -#[cfg(all(target_os = "linux", target_arch = "aarch64"))] -pub use self::kvmgicv3::KvmGicV3; -#[cfg(all(target_os = "linux", target_arch = "x86_64"))] -pub use self::kvmioapic::KvmIoapic; -#[cfg(target_arch = "aarch64")] -pub use self::rtc_pl031::RTC; -pub use self::serial::Serial; -#[cfg(target_os = "macos")] -pub use self::vcpu::VcpuList; - -// Cannot use multiple types as bounds for a trait object, so we define our own trait -// which is a composition of the desired bounds. In this case, io::Read and AsRawFd. -// Run `rustc --explain E0225` for more details. -/// Trait that composes the `std::io::Read` and `std::os::unix::io::AsRawFd` traits. -pub trait ReadableFd: std::io::Read + std::os::fd::AsRawFd {} - -impl ReadableFd for std::fs::File {} - -#[cfg(target_os = "linux")] -#[derive(Clone)] -pub struct GicV3 {} - -#[cfg(target_os = "linux")] -impl GicV3 { - pub fn set_irq(&self, _irq: u32) {} -} diff --git a/vendor/krun-devices/src/legacy/riscv64/mod.rs b/vendor/krun-devices/src/legacy/riscv64/mod.rs deleted file mode 100644 index 945a2bbc6..000000000 --- a/vendor/krun-devices/src/legacy/riscv64/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright 2025 The libkrun Authors. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -pub mod serial; diff --git a/vendor/krun-devices/src/legacy/riscv64/serial.rs b/vendor/krun-devices/src/legacy/riscv64/serial.rs deleted file mode 100644 index 72aa3942e..000000000 --- a/vendor/krun-devices/src/legacy/riscv64/serial.rs +++ /dev/null @@ -1,320 +0,0 @@ -// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -// -// Portions Copyright 2017 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the THIRD-PARTY file. - -use std::collections::VecDeque; -use std::io; - -use polly::event_manager::{EventManager, Subscriber}; -use utils::epoll::{EpollEvent, EventSet}; -use utils::eventfd::EventFd; - -use crate::bus::BusDevice; -use crate::legacy::{IrqChip, ReadableFd}; - -const LOOP_SIZE: usize = 0x40; - -const DATA: u8 = 0; -const IER: u8 = 1; -const IIR: u8 = 2; -const LCR: u8 = 3; -const MCR: u8 = 4; -const LSR: u8 = 5; -const MSR: u8 = 6; -const SCR: u8 = 7; - -const DLAB_LOW: u8 = 0; -const DLAB_HIGH: u8 = 1; - -const IER_RECV_BIT: u8 = 0x1; -const IER_THR_BIT: u8 = 0x2; -const IER_FIFO_BITS: u8 = 0x0f; - -const IIR_FIFO_BITS: u8 = 0xc0; -const IIR_NONE_BIT: u8 = 0x1; -const IIR_THR_BIT: u8 = 0x2; -const IIR_RECV_BIT: u8 = 0x4; - -const LCR_DLAB_BIT: u8 = 0x80; - -const LSR_DATA_BIT: u8 = 0x1; -const LSR_EMPTY_BIT: u8 = 0x20; -const LSR_IDLE_BIT: u8 = 0x40; - -const MCR_LOOP_BIT: u8 = 0x10; - -const DEFAULT_INTERRUPT_IDENTIFICATION: u8 = IIR_NONE_BIT; // no pending interrupt -const DEFAULT_LINE_STATUS: u8 = LSR_EMPTY_BIT | LSR_IDLE_BIT; // THR empty and line is idle -const DEFAULT_LINE_CONTROL: u8 = 0x3; // 8-bits per character -const DEFAULT_MODEM_CONTROL: u8 = 0x8; // Auxiliary output 2 -const DEFAULT_MODEM_STATUS: u8 = 0x20 | 0x10 | 0x80; // data ready, clear to send, carrier detect -const DEFAULT_BAUD_DIVISOR: u16 = 12; // 9600 bps - -/// Emulates serial COM ports commonly seen on x86 I/O ports 0x3f8/0x2f8/0x3e8/0x2e8. -/// -/// This can optionally write the guest's output to a Write trait object. To send input to the -/// guest, use `raw_input`. -pub struct Serial { - interrupt_enable: u8, - interrupt_identification: u8, - interrupt_evt: EventFd, - intc: Option, - irq_line: Option, - line_control: u8, - line_status: u8, - modem_control: u8, - modem_status: u8, - scratch: u8, - baud_divisor: u16, - in_buffer: VecDeque, - out: Option>, - input: Option>, -} - -impl Serial { - pub fn new( - interrupt_evt: EventFd, - out: Option>, - input: Option>, - ) -> Serial { - Serial { - interrupt_enable: 0, - interrupt_identification: DEFAULT_INTERRUPT_IDENTIFICATION, - interrupt_evt, - intc: None, - irq_line: None, - line_control: DEFAULT_LINE_CONTROL, - line_status: DEFAULT_LINE_STATUS, - modem_control: DEFAULT_MODEM_CONTROL, - modem_status: DEFAULT_MODEM_STATUS, - scratch: 0, - baud_divisor: DEFAULT_BAUD_DIVISOR, - in_buffer: VecDeque::new(), - out, - input, - } - } - - /// Constructs a Serial port ready for input and output. - pub fn new_in_out( - interrupt_evt: EventFd, - input: Box, - out: Box, - ) -> Serial { - Self::new(interrupt_evt, Some(out), Some(input)) - } - - /// Constructs a Serial port ready for output but with no input. - pub fn new_out(interrupt_evt: EventFd, out: Box) -> Serial { - Self::new(interrupt_evt, Some(out), None) - } - - /// Constructs a Serial port with no connected input or output. - pub fn new_sink(interrupt_evt: EventFd) -> Serial { - Self::new(interrupt_evt, None, None) - } - - /// Provides a reference to the interrupt event fd. - pub fn interrupt_evt(&self) -> &EventFd { - &self.interrupt_evt - } - - pub fn set_intc(&mut self, intc: IrqChip) { - self.intc = Some(intc); - } - - pub fn set_irq_line(&mut self, irq: u32) { - debug!("SET_IRQ_LINE (SERIAL)={irq}"); - self.irq_line = Some(irq); - } - - fn is_dlab_set(&self) -> bool { - (self.line_control & LCR_DLAB_BIT) != 0 - } - - fn is_recv_intr_enabled(&self) -> bool { - (self.interrupt_enable & IER_RECV_BIT) != 0 - } - - fn is_thr_intr_enabled(&self) -> bool { - (self.interrupt_enable & IER_THR_BIT) != 0 - } - - fn is_loop(&self) -> bool { - (self.modem_control & MCR_LOOP_BIT) != 0 - } - - fn add_intr_bit(&mut self, bit: u8) { - self.interrupt_identification &= !IIR_NONE_BIT; - self.interrupt_identification |= bit; - } - - fn del_intr_bit(&mut self, bit: u8) { - self.interrupt_identification &= !bit; - if self.interrupt_identification == 0x0 { - self.interrupt_identification = IIR_NONE_BIT; - } - } - - fn thr_empty(&mut self) -> io::Result<()> { - if self.is_thr_intr_enabled() { - self.add_intr_bit(IIR_THR_BIT); - self.trigger_interrupt()? - } - Ok(()) - } - - fn recv_data(&mut self) -> io::Result<()> { - if self.is_recv_intr_enabled() { - self.add_intr_bit(IIR_RECV_BIT); - self.trigger_interrupt()? - } - self.line_status |= LSR_DATA_BIT; - Ok(()) - } - - fn trigger_interrupt(&mut self) -> io::Result<()> { - if let Some(intc) = &self.intc { - return intc - .lock() - .unwrap() - .set_irq(self.irq_line, Some(&self.interrupt_evt)) - .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("{e:?}"))); - } - - self.interrupt_evt.write(1) - } - - fn iir_reset(&mut self) { - self.interrupt_identification = DEFAULT_INTERRUPT_IDENTIFICATION; - } - - // Handles a write request from the driver. - fn handle_write(&mut self, offset: u8, value: u8) -> io::Result<()> { - match offset { - DLAB_LOW if self.is_dlab_set() => { - self.baud_divisor = (self.baud_divisor & 0xff00) | u16::from(value) - } - DLAB_HIGH if self.is_dlab_set() => { - self.baud_divisor = (self.baud_divisor & 0x00ff) | (u16::from(value) << 8) - } - DATA => { - if self.is_loop() { - if self.in_buffer.len() < LOOP_SIZE { - self.in_buffer.push_back(value); - self.recv_data()?; - } - } else { - if let Some(out) = self.out.as_mut() { - out.write_all(&[value])?; - out.flush()?; - } - self.thr_empty()?; - } - } - IER => self.interrupt_enable = value & IER_FIFO_BITS, - LCR => self.line_control = value, - MCR => self.modem_control = value, - SCR => self.scratch = value, - _ => {} - } - Ok(()) - } - - // Handles a read request from the driver. - fn handle_read(&mut self, offset: u8) -> u8 { - match offset { - DLAB_LOW if self.is_dlab_set() => self.baud_divisor as u8, - DLAB_HIGH if self.is_dlab_set() => (self.baud_divisor >> 8) as u8, - DATA => { - self.del_intr_bit(IIR_RECV_BIT); - if self.in_buffer.len() <= 1 { - self.line_status &= !LSR_DATA_BIT; - } - self.in_buffer.pop_front().unwrap_or_default() - } - IER => self.interrupt_enable, - IIR => { - let v = self.interrupt_identification | IIR_FIFO_BITS; - self.iir_reset(); - v - } - LCR => self.line_control, - MCR => self.modem_control, - LSR => self.line_status, - MSR => self.modem_status, - SCR => self.scratch, - _ => 0, - } - } - - fn raw_input(&mut self, data: &[u8]) -> io::Result<()> { - if !self.is_loop() { - self.in_buffer.extend(data); - self.recv_data()?; - } - Ok(()) - } -} - -impl BusDevice for Serial { - fn read(&mut self, _vcpuid: u64, offset: u64, data: &mut [u8]) { - if data.len() != 1 { - return; - } - - data[0] = self.handle_read(offset as u8); - } - - fn write(&mut self, _vcpuid: u64, offset: u64, data: &[u8]) { - if data.len() != 1 { - return; - } - if let Err(e) = self.handle_write(offset as u8, data[0]) { - error!("Failed the write to serial: {e}"); - } - } -} - -impl Subscriber for Serial { - /// Handle a read event (EPOLLIN) on the serial input fd. - fn process(&mut self, event: &EpollEvent, _: &mut EventManager) { - let source = event.fd(); - let event_set = event.event_set(); - - // TODO: also check for errors. Pending high level discussions on how we want - // to handle errors in devices. - let supported_events = EventSet::IN; - if !supported_events.contains(event_set) { - warn!("Received unknown event: {event_set:?} from source: {source:?}"); - return; - } - - if let Some(input) = self.input.as_mut() { - if input.as_raw_fd() == source { - let mut out = [0u8; 32]; - match input.read(&mut out[..]) { - Ok(count) => { - self.raw_input(&out[..count]) - .unwrap_or_else(|e| warn!("Serial error on input: {e}")); - } - Err(e) => { - warn!("error while reading stdin: {e:?}"); - } - } - } - } - } - - /// Initial registration of pollable objects. - /// If serial input is present, register the serial input FD as readable. - fn interest_list(&self) -> Vec { - match &self.input { - Some(input) => vec![EpollEvent::new(EventSet::IN, input.as_raw_fd() as u64)], - None => vec![], - } - } -} diff --git a/vendor/krun-devices/src/legacy/rtc_pl031.rs b/vendor/krun-devices/src/legacy/rtc_pl031.rs deleted file mode 100644 index 4960e596d..000000000 --- a/vendor/krun-devices/src/legacy/rtc_pl031.rs +++ /dev/null @@ -1,241 +0,0 @@ -// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! ARM PL031 Real Time Clock -//! -//! This module implements a PL031 Real Time Clock (RTC) that provides to provides long time base counter. -//! This is achieved by generating an interrupt signal after counting for a programmed number of cycles of -//! a real-time clock input. -//! - -use std::fmt; -use std::time::Instant; -use std::{io, result}; - -use crate::BusDevice; -use utils::byte_order; -use utils::eventfd::EventFd; -//use bus::Error; - -// As you can see in https://static.docs.arm.com/ddi0224/c/real_time_clock_pl031_r1p3_technical_reference_manual_DDI0224C.pdf -// at section 3.2 Summary of RTC registers, the total size occupied by this device is 0x000 -> 0xFFC + 4 = 0x1000. -// From 0x0 to 0x1C we have following registers: -const RTCDR: u64 = 0x0; // Data Register. -const RTCMR: u64 = 0x4; // Match Register. -const RTCLR: u64 = 0x8; // Load Regiser. -const RTCCR: u64 = 0xc; // Control Register. -const RTCIMSC: u64 = 0x10; // Interrupt Mask Set or Clear Register. -const RTCRIS: u64 = 0x14; // Raw Interrupt Status. -const RTCMIS: u64 = 0x18; // Masked Interrupt Status. -const RTCICR: u64 = 0x1c; // Interrupt Clear Register. - // From 0x020 to 0xFDC => reserved space. - // From 0xFE0 to 0x1000 => Peripheral and PrimeCell Identification Registers which are Read Only registers. - // AMBA standard devices have CIDs (Cell IDs) and PIDs (Peripheral IDs). The linux kernel will look for these in order to assert the identity - // of these devices (i.e look at the `amba_device_try_add` function). - // We are putting the expected values (look at 'Reset value' column from above mentioned document) in an array. -const PL031_ID: [u8; 8] = [0x31, 0x10, 0x14, 0x00, 0x0d, 0xf0, 0x05, 0xb1]; -// We are only interested in the margins. -const AMBA_ID_LOW: u64 = 0xFE0; -const AMBA_ID_HIGH: u64 = 0x1000; - -#[derive(Debug)] -pub enum Error { - BadWriteOffset(u64), - InterruptFailure(io::Error), -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Error::BadWriteOffset(offset) => write!(f, "Bad Write Offset: {offset}"), - Error::InterruptFailure(e) => write!(f, "Failed to trigger interrupt: {e}"), - } - } -} -type Result = result::Result; - -/// A RTC device following the PL031 specification.. -pub struct RTC { - previous_now: Instant, - tick_offset: i64, - // This is used for implementing the RTC alarm. However, in Firecracker we do not need it. - match_value: u32, - // Writes to this register load an update value into the RTC. - load: u32, - imsc: u32, - ris: u32, - interrupt_evt: EventFd, -} - -impl RTC { - /// Constructs an AMBA PL031 RTC device. - pub fn new(interrupt_evt: EventFd) -> RTC { - RTC { - // This is used only for duration measuring purposes. - previous_now: Instant::now(), - tick_offset: utils::time::get_time(utils::time::ClockType::Real) as i64, - match_value: 0, - load: 0, - imsc: 0, - ris: 0, - interrupt_evt, - } - } - - fn trigger_interrupt(&mut self) -> Result<()> { - self.interrupt_evt.write(1).map_err(Error::InterruptFailure) - } - - fn get_time(&self) -> u32 { - let ts = (self.tick_offset as i128) - + (Instant::now().duration_since(self.previous_now).as_nanos() as i128); - (ts / utils::time::NANOS_PER_SECOND as i128) as u32 - } - - fn handle_write(&mut self, offset: u64, val: u32) -> Result<()> { - match offset { - RTCMR => { - // The MR register is used for implementing the RTC alarm. A real time clock alarm is - // a feature that can be used to allow a computer to 'wake up' after shut down to execute - // tasks every day or on a certain day. It can sometimes be found in the 'Power Management' - // section of a motherboard's BIOS setup. This is functionality that extends beyond - // Firecracker intended use. However, we increment a metric just in case. - self.match_value = val; - } - RTCLR => { - self.load = val; - self.previous_now = Instant::now(); - // If the unwrap fails, then the internal value of the clock has been corrupted and - // we want to terminate the execution of the process. - self.tick_offset = utils::time::seconds_to_nanoseconds(i64::from(val)).unwrap(); - } - RTCIMSC => { - self.imsc = val & 1; - self.trigger_interrupt()?; - } - RTCICR => { - // As per above mentioned doc, the interrupt is cleared by writing any data value to - // the Interrupt Clear Register. - self.ris = 0; - self.trigger_interrupt()?; - } - RTCCR => (), // ignore attempts to turn off the timer. - o => { - return Err(Error::BadWriteOffset(o)); - } - } - Ok(()) - } -} - -impl BusDevice for RTC { - fn read(&mut self, _vcpuid: u64, offset: u64, data: &mut [u8]) { - let mut read_ok = true; - - let v = if (AMBA_ID_LOW..AMBA_ID_HIGH).contains(&offset) { - let index = ((offset - AMBA_ID_LOW) >> 2) as usize; - u32::from(PL031_ID[index]) - } else { - match offset { - RTCDR => self.get_time(), - RTCMR => { - // Even though we are not implementing RTC alarm we return the last value - self.match_value - } - RTCLR => self.load, - RTCCR => 1, // RTC is always enabled. - RTCIMSC => self.imsc, - RTCRIS => self.ris, - RTCMIS => self.ris & self.imsc, - _ => { - read_ok = false; - 0 - } - } - }; - if read_ok && data.len() <= 4 { - byte_order::write_le_u32(data, v); - } else { - warn!( - "Invalid RTC PL031 read: offset {}, data length {}", - offset, - data.len() - ); - } - } - - fn write(&mut self, _vcpuid: u64, offset: u64, data: &[u8]) { - if data.len() <= 4 { - let v = byte_order::read_le_u32(data); - if let Err(e) = self.handle_write(offset, v) { - warn!("Failed to write to RTC PL031 device: {e}"); - } - } else { - warn!( - "Invalid RTC PL031 write: offset {}, data length {}", - offset, - data.len() - ); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_rtc_read_write_and_event() { - let mut rtc = RTC::new(EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap()); - let mut data = [0; 4]; - - // Read and write to the MR register. - byte_order::write_le_u32(&mut data, 123); - rtc.write(0, RTCMR, &mut data); - rtc.read(0, RTCMR, &mut data); - let v = byte_order::read_le_u32(&data[..]); - assert_eq!(v, 123); - - // Read and write to the LR register. - let v = utils::time::get_time(utils::time::ClockType::Real); - byte_order::write_le_u32(&mut data, (v / utils::time::NANOS_PER_SECOND) as u32); - let previous_now_before = rtc.previous_now; - rtc.write(0, RTCLR, &mut data); - - assert!(rtc.previous_now > previous_now_before); - - rtc.read(0, RTCLR, &mut data); - let v_read = byte_order::read_le_u32(&data[..]); - assert_eq!((v / utils::time::NANOS_PER_SECOND) as u32, v_read); - - // Read and write to IMSC register. - // Test with non zero value. - let non_zero = 1; - byte_order::write_le_u32(&mut data, non_zero); - rtc.write(0, RTCIMSC, &mut data); - // The interrupt line should be on. - assert!(rtc.interrupt_evt.read().unwrap() == 1); - rtc.read(0, RTCIMSC, &mut data); - let v = byte_order::read_le_u32(&data[..]); - assert_eq!(non_zero & 1, v); - - // Now test with 0. - byte_order::write_le_u32(&mut data, 0); - rtc.write(0, RTCIMSC, &mut data); - rtc.read(0, RTCIMSC, &mut data); - let v = byte_order::read_le_u32(&data[..]); - assert_eq!(0, v); - - // Attempts to turn off the RTC should not go through. - byte_order::write_le_u32(&mut data, 0); - rtc.write(0, RTCCR, &mut data); - rtc.read(0, RTCCR, &mut data); - let v = byte_order::read_le_u32(&data[..]); - assert_eq!(v, 1); - - let mut data = [0; 4]; - rtc.read(0, AMBA_ID_LOW, &mut data); - let index = AMBA_ID_LOW + 3; - assert_eq!(data[0], PL031_ID[((index - AMBA_ID_LOW) >> 2) as usize]); - } -} diff --git a/vendor/krun-devices/src/legacy/vcpu.rs b/vendor/krun-devices/src/legacy/vcpu.rs deleted file mode 100644 index a6d5a3dbb..000000000 --- a/vendor/krun-devices/src/legacy/vcpu.rs +++ /dev/null @@ -1,264 +0,0 @@ -use crossbeam_channel::Sender; -use std::collections::VecDeque; -use std::sync::Mutex; - -use arch::aarch64::layout::VTIMER_IRQ; -use arch::aarch64::sysreg::*; -use hvf::bindings::{ - hv_sys_reg_t_HV_SYS_REG_CNTHCTL_EL2, hv_sys_reg_t_HV_SYS_REG_MDCCINT_EL1, hv_vcpu_get_sys_reg, - hv_vcpu_set_sys_reg, HV_SUCCESS, -}; -use hvf::{vcpu_request_exit, Vcpus}; - -// See https://developer.arm.com/documentation/ddi0595/2020-12/AArch64-Registers/ICC-IAR0-EL1--Interrupt-Controller-Interrupt-Acknowledge-Register-0 -const GIC_INTID_SPURIOUS: u32 = 1023; - -enum VcpuStatus { - Running, - Waiting, -} - -struct PerCPUInterruptControllerState { - vcpuid: u64, - status: VcpuStatus, - pending_irqs: VecDeque, - wfe_sender: Option>, -} - -impl PerCPUInterruptControllerState { - fn set_irq_common(&mut self, irq: u32) { - debug!( - "[GICv3] SET_IRQ_COMMON vcpuid={}, irq_line={}", - self.vcpuid, irq - ); - self.pending_irqs.push_back(irq); - - match self.status { - VcpuStatus::Waiting => { - self.wfe_sender - .as_mut() - .unwrap() - .send(self.vcpuid as u32) - .unwrap(); - self.status = VcpuStatus::Running; - } - VcpuStatus::Running => { - vcpu_request_exit(self.vcpuid).unwrap(); - } - } - } - - fn should_wait(&mut self) -> bool { - if self.pending_irqs.is_empty() { - self.status = VcpuStatus::Waiting; - return true; - } - false - } - - fn has_pending_irq(&self) -> bool { - !self.pending_irqs.is_empty() - } - - fn get_pending_irq(&mut self) -> u32 { - self.pending_irqs.pop_front().unwrap_or(GIC_INTID_SPURIOUS) - } -} - -pub struct VcpuList { - cpu_count: u64, - vcpus: Vec>, -} - -impl VcpuList { - pub fn new(cpu_count: u64) -> Self { - let mut vcpus = Vec::with_capacity(cpu_count as usize); - for vcpuid in 0..cpu_count { - vcpus.push(Mutex::new(PerCPUInterruptControllerState { - vcpuid, - status: VcpuStatus::Running, - pending_irqs: VecDeque::new(), - wfe_sender: None, - })); - } - - Self { cpu_count, vcpus } - } - - pub fn get_cpu_count(&self) -> u64 { - self.cpu_count - } - - pub fn set_irq_common(&self, vcpuid: u64, irq: u32) { - assert!(vcpuid < self.cpu_count); - self.vcpus[vcpuid as usize] - .lock() - .unwrap() - .set_irq_common(irq); - } - - pub fn set_sgi_irq(&self, vcpuid: u64, irq: u32) { - assert!(vcpuid < self.cpu_count); - assert!(irq < 16); - self.vcpus[vcpuid as usize] - .lock() - .unwrap() - .set_irq_common(irq); - } - - pub fn register(&self, vcpuid: u64, wfe_sender: Sender) { - assert!(vcpuid < self.cpu_count); - self.vcpus[vcpuid as usize].lock().unwrap().wfe_sender = Some(wfe_sender); - } -} - -impl Vcpus for VcpuList { - fn set_vtimer_irq(&self, vcpuid: u64) { - assert!(vcpuid < self.cpu_count); - self.vcpus[vcpuid as usize] - .lock() - .unwrap() - .set_irq_common(VTIMER_IRQ); - } - - fn should_wait(&self, vcpuid: u64) -> bool { - assert!(vcpuid < self.cpu_count); - self.vcpus[vcpuid as usize].lock().unwrap().should_wait() - } - - fn has_pending_irq(&self, vcpuid: u64) -> bool { - assert!(vcpuid < self.cpu_count); - self.vcpus[vcpuid as usize] - .lock() - .unwrap() - .has_pending_irq() - } - - fn get_pending_irq(&self, vcpuid: u64) -> u32 { - assert!(vcpuid < self.cpu_count); - self.vcpus[vcpuid as usize] - .lock() - .unwrap() - .get_pending_irq() - } - - fn handle_sysreg_read(&self, vcpuid: u64, reg: u32) -> Option { - assert!(vcpuid < self.cpu_count); - - if is_id_sysreg(reg) { - return Some(0); - } - - match reg { - SYSREG_ICC_IAR1_EL1 => Some( - self.vcpus[vcpuid as usize] - .lock() - .unwrap() - .get_pending_irq() as u64, - ), - SYSREG_ICC_PMR_EL1 => Some(0), - SYSREG_ICC_CTLR_EL1 => Some( - (1 << ICC_CTLR_EL1_RSS_SHIFT) - | (1 << ICC_CTLR_EL1_A3V_SHIFT) - | (1 << ICC_CTLR_EL1_ID_BITS_SHIFT) - | (4 << ICC_CTLR_EL1_PRI_BITS_SHIFT), - ), - SYSREG_CNTHCTL_EL2 => { - let val: u64 = 0; - let ret = unsafe { - hv_vcpu_get_sys_reg( - vcpuid, - hv_sys_reg_t_HV_SYS_REG_CNTHCTL_EL2, - &val as *const _ as *mut _, - ) - }; - if ret == HV_SUCCESS { - Some(val) - } else { - None - } - } - SYSREG_MDCCINT_EL1 => { - let val: u64 = 0; - let ret = unsafe { - hv_vcpu_get_sys_reg( - vcpuid, - hv_sys_reg_t_HV_SYS_REG_MDCCINT_EL1, - &val as *const _ as *mut _, - ) - }; - if ret == HV_SUCCESS { - Some(val) - } else { - None - } - } - _ => None, - } - } - - fn handle_sysreg_write(&self, vcpuid: u64, reg: u32, val: u64) -> bool { - assert!(vcpuid < self.cpu_count); - - if is_id_sysreg(reg) { - return true; - } - - match reg { - SYSREG_ICC_SGI1R_EL1 => { - let target_list = val & 0xffff; - let intid = ((val >> 24) & 0xf) as u32; - let irm = (val & (1 << 40)) >> 40; - let is_broadcast = irm == 1; - let aff3aff2aff1 = val & ((0xff << 48) | (0xff << 32) | (0xff << 16)); - let rs = (val & (0xf << 44)) >> 44; - - debug!("vCPU {vcpuid} GenerateSoftwareInterrupt={intid} (0x{val:x})"); - - // A flat core hierarchy should be good enough, but if we ever start using - // Aff[123] MPIDR fields (currently MPID is configured via DT), GICv3 support - // will need to be added. - assert_eq!( - aff3aff2aff1, 0, - "[GICv3] only flat core hierarchy supported for now" - ); - - assert!( - !is_broadcast, - "[GICv3] SGI broadcast is not implemented yet" - ); - - // for each core in target list - for target_id in 0u64..=15u64 { - if (target_list >> target_id) & 1 == 1 { - self.set_sgi_irq(rs * 16 + target_id, intid) - } - } - - true - } - SYSREG_CNTHCTL_EL2 => { - let ret = unsafe { - hv_vcpu_set_sys_reg(vcpuid, hv_sys_reg_t_HV_SYS_REG_CNTHCTL_EL2, val) - }; - ret == HV_SUCCESS - } - SYSREG_MDCCINT_EL1 => { - let ret = unsafe { - hv_vcpu_set_sys_reg(vcpuid, hv_sys_reg_t_HV_SYS_REG_MDCCINT_EL1, val) - }; - ret == HV_SUCCESS - } - SYSREG_ICC_EOIR1_EL1 - | SYSREG_ICC_IGRPEN1_EL1 - | SYSREG_ICC_PMR_EL1 - | SYSREG_ICC_BPR1_EL1 - | SYSREG_ICC_CTLR_EL1 - | SYSREG_ICC_AP1R0_EL1 - | SYSREG_LORC_EL1 - | SYSREG_OSLAR_EL1 - | SYSREG_OSDLR_EL1 => true, - _ => false, - } - } -} diff --git a/vendor/krun-devices/src/legacy/x86_64/cmos.rs b/vendor/krun-devices/src/legacy/x86_64/cmos.rs deleted file mode 100644 index 52223c9d7..000000000 --- a/vendor/krun-devices/src/legacy/x86_64/cmos.rs +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2025 Red Hat, Inc. -// SPDX-License-Identifier: Apache-2.0 - -use std::cmp::min; - -use crate::bus::BusDevice; - -const INDEX_MASK: u8 = 0x7f; -const INDEX_OFFSET: u64 = 0x0; -const DATA_OFFSET: u64 = 0x1; -const DATA_LEN: usize = 128; - -pub struct Cmos { - index: u8, - data: [u8; DATA_LEN], -} - -impl Cmos { - pub fn new(mem_below_4g: u64, mem_above_4g: u64) -> Cmos { - debug!("cmos: mem_below_4g={mem_below_4g} mem_above_4g={mem_above_4g}"); - - let mut data = [0u8; DATA_LEN]; - - // Extended memory from 16 MB to 4 GB in units of 64 KB - let ext_mem = min( - 0xFFFF, - mem_below_4g.saturating_sub(16 * 1024 * 1024) / (64 * 1024), - ); - data[0x34] = ext_mem as u8; - data[0x35] = (ext_mem >> 8) as u8; - - // High memory (> 4GB) in units of 64 KB - let high_mem = min(0xFFFFFF, mem_above_4g / (64 * 1024)); - data[0x5b] = high_mem as u8; - data[0x5c] = (high_mem >> 8) as u8; - data[0x5d] = (high_mem >> 16) as u8; - - Cmos { index: 0, data } - } -} - -impl BusDevice for Cmos { - fn read(&mut self, _vcpuid: u64, offset: u64, data: &mut [u8]) { - if data.len() != 1 { - error!("cmos: unsupported read length"); - return; - } - - data[0] = match offset { - INDEX_OFFSET => { - debug!("cmos: read index offset"); - self.index - } - DATA_OFFSET => { - debug!("cmos: read data offset from index={:x}", self.index); - self.data[(self.index & INDEX_MASK) as usize] - } - _ => { - debug!("cmos: unsupported read offset"); - 0 - } - }; - } - - fn write(&mut self, _vcpuid: u64, offset: u64, data: &[u8]) { - if data.len() != 1 { - error!("cmos: unsupported write length"); - return; - } - - match offset { - INDEX_OFFSET => { - debug!("cmos: update index"); - self.index = data[0] & INDEX_MASK; - } - _ => debug!("cmos: ignoring unsupported write to CMOS"), - } - } -} diff --git a/vendor/krun-devices/src/legacy/x86_64/mod.rs b/vendor/krun-devices/src/legacy/x86_64/mod.rs deleted file mode 100644 index 6e3f15a91..000000000 --- a/vendor/krun-devices/src/legacy/x86_64/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod cmos; -pub mod serial; diff --git a/vendor/krun-devices/src/legacy/x86_64/serial.rs b/vendor/krun-devices/src/legacy/x86_64/serial.rs deleted file mode 100644 index 9ac6dccc2..000000000 --- a/vendor/krun-devices/src/legacy/x86_64/serial.rs +++ /dev/null @@ -1,563 +0,0 @@ -// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -// -// Portions Copyright 2017 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the THIRD-PARTY file. - -use std::collections::VecDeque; -use std::io; - -use polly::event_manager::{EventManager, Subscriber}; -use utils::epoll::{EpollEvent, EventSet}; -use utils::eventfd::EventFd; - -use crate::bus::BusDevice; -use crate::legacy::ReadableFd; - -const LOOP_SIZE: usize = 0x40; - -const DATA: u8 = 0; -const IER: u8 = 1; -const IIR: u8 = 2; -const LCR: u8 = 3; -const MCR: u8 = 4; -const LSR: u8 = 5; -const MSR: u8 = 6; -const SCR: u8 = 7; - -const DLAB_LOW: u8 = 0; -const DLAB_HIGH: u8 = 1; - -const IER_RECV_BIT: u8 = 0x1; -const IER_THR_BIT: u8 = 0x2; -const IER_FIFO_BITS: u8 = 0x0f; - -const IIR_FIFO_BITS: u8 = 0xc0; -const IIR_NONE_BIT: u8 = 0x1; -const IIR_THR_BIT: u8 = 0x2; -const IIR_RECV_BIT: u8 = 0x4; - -const LCR_DLAB_BIT: u8 = 0x80; - -const LSR_DATA_BIT: u8 = 0x1; -const LSR_EMPTY_BIT: u8 = 0x20; -const LSR_IDLE_BIT: u8 = 0x40; - -const MCR_LOOP_BIT: u8 = 0x10; - -const DEFAULT_INTERRUPT_IDENTIFICATION: u8 = IIR_NONE_BIT; // no pending interrupt -const DEFAULT_LINE_STATUS: u8 = LSR_EMPTY_BIT | LSR_IDLE_BIT; // THR empty and line is idle -const DEFAULT_LINE_CONTROL: u8 = 0x3; // 8-bits per character -const DEFAULT_MODEM_CONTROL: u8 = 0x8; // Auxiliary output 2 -const DEFAULT_MODEM_STATUS: u8 = 0x20 | 0x10 | 0x80; // data ready, clear to send, carrier detect -const DEFAULT_BAUD_DIVISOR: u16 = 12; // 9600 bps - -/// Emulates serial COM ports commonly seen on x86 I/O ports 0x3f8/0x2f8/0x3e8/0x2e8. -/// -/// This can optionally write the guest's output to a Write trait object. To send input to the -/// guest, use `raw_input`. -pub struct Serial { - interrupt_enable: u8, - interrupt_identification: u8, - interrupt_evt: EventFd, - line_control: u8, - line_status: u8, - modem_control: u8, - modem_status: u8, - scratch: u8, - baud_divisor: u16, - in_buffer: VecDeque, - out: Option>, - input: Option>, -} - -impl Serial { - pub fn new( - interrupt_evt: EventFd, - out: Option>, - input: Option>, - ) -> Serial { - Serial { - interrupt_enable: 0, - interrupt_identification: DEFAULT_INTERRUPT_IDENTIFICATION, - interrupt_evt, - line_control: DEFAULT_LINE_CONTROL, - line_status: DEFAULT_LINE_STATUS, - modem_control: DEFAULT_MODEM_CONTROL, - modem_status: DEFAULT_MODEM_STATUS, - scratch: 0, - baud_divisor: DEFAULT_BAUD_DIVISOR, - in_buffer: VecDeque::new(), - out, - input, - } - } - - /// Constructs a Serial port ready for input and output. - pub fn new_in_out( - interrupt_evt: EventFd, - input: Box, - out: Box, - ) -> Serial { - Self::new(interrupt_evt, Some(out), Some(input)) - } - - /// Constructs a Serial port ready for output but with no input. - pub fn new_out(interrupt_evt: EventFd, out: Box) -> Serial { - Self::new(interrupt_evt, Some(out), None) - } - - /// Constructs a Serial port with no connected input or output. - pub fn new_sink(interrupt_evt: EventFd) -> Serial { - Self::new(interrupt_evt, None, None) - } - - /// Provides a reference to the interrupt event fd. - pub fn interrupt_evt(&self) -> &EventFd { - &self.interrupt_evt - } - - fn is_dlab_set(&self) -> bool { - (self.line_control & LCR_DLAB_BIT) != 0 - } - - fn is_recv_intr_enabled(&self) -> bool { - (self.interrupt_enable & IER_RECV_BIT) != 0 - } - - fn is_thr_intr_enabled(&self) -> bool { - (self.interrupt_enable & IER_THR_BIT) != 0 - } - - fn is_loop(&self) -> bool { - (self.modem_control & MCR_LOOP_BIT) != 0 - } - - fn add_intr_bit(&mut self, bit: u8) { - self.interrupt_identification &= !IIR_NONE_BIT; - self.interrupt_identification |= bit; - } - - fn del_intr_bit(&mut self, bit: u8) { - self.interrupt_identification &= !bit; - if self.interrupt_identification == 0x0 { - self.interrupt_identification = IIR_NONE_BIT; - } - } - - fn thr_empty(&mut self) -> io::Result<()> { - if self.is_thr_intr_enabled() { - self.add_intr_bit(IIR_THR_BIT); - self.trigger_interrupt()? - } - Ok(()) - } - - fn recv_data(&mut self) -> io::Result<()> { - if self.is_recv_intr_enabled() { - self.add_intr_bit(IIR_RECV_BIT); - self.trigger_interrupt()? - } - self.line_status |= LSR_DATA_BIT; - Ok(()) - } - - fn trigger_interrupt(&mut self) -> io::Result<()> { - self.interrupt_evt.write(1) - } - - fn iir_reset(&mut self) { - self.interrupt_identification = DEFAULT_INTERRUPT_IDENTIFICATION; - } - - // Handles a write request from the driver. - fn handle_write(&mut self, offset: u8, value: u8) -> io::Result<()> { - match offset { - DLAB_LOW if self.is_dlab_set() => { - self.baud_divisor = (self.baud_divisor & 0xff00) | u16::from(value) - } - DLAB_HIGH if self.is_dlab_set() => { - self.baud_divisor = (self.baud_divisor & 0x00ff) | (u16::from(value) << 8) - } - DATA => { - if self.is_loop() { - if self.in_buffer.len() < LOOP_SIZE { - self.in_buffer.push_back(value); - self.recv_data()?; - } - } else { - if let Some(out) = self.out.as_mut() { - out.write_all(&[value])?; - out.flush()?; - } - self.thr_empty()?; - } - } - IER => self.interrupt_enable = value & IER_FIFO_BITS, - LCR => self.line_control = value, - MCR => self.modem_control = value, - SCR => self.scratch = value, - _ => {} - } - Ok(()) - } - - // Handles a read request from the driver. - fn handle_read(&mut self, offset: u8) -> u8 { - match offset { - DLAB_LOW if self.is_dlab_set() => self.baud_divisor as u8, - DLAB_HIGH if self.is_dlab_set() => (self.baud_divisor >> 8) as u8, - DATA => { - self.del_intr_bit(IIR_RECV_BIT); - if self.in_buffer.len() <= 1 { - self.line_status &= !LSR_DATA_BIT; - } - self.in_buffer.pop_front().unwrap_or_default() - } - IER => self.interrupt_enable, - IIR => { - let v = self.interrupt_identification | IIR_FIFO_BITS; - self.iir_reset(); - v - } - LCR => self.line_control, - MCR => self.modem_control, - LSR => self.line_status, - MSR => self.modem_status, - SCR => self.scratch, - _ => 0, - } - } - - fn raw_input(&mut self, data: &[u8]) -> io::Result<()> { - if !self.is_loop() { - self.in_buffer.extend(data); - self.recv_data()?; - } - Ok(()) - } -} - -impl BusDevice for Serial { - fn read(&mut self, _vcpuid: u64, offset: u64, data: &mut [u8]) { - if data.len() != 1 { - return; - } - - data[0] = self.handle_read(offset as u8); - } - - fn write(&mut self, _vcpuid: u64, offset: u64, data: &[u8]) { - if data.len() != 1 { - return; - } - if let Err(e) = self.handle_write(offset as u8, data[0]) { - error!("Failed the write to serial: {e}"); - } - } -} - -impl Subscriber for Serial { - /// Handle a read event (EPOLLIN) on the serial input fd. - fn process(&mut self, event: &EpollEvent, _: &mut EventManager) { - let source = event.fd(); - let event_set = event.event_set(); - - // TODO: also check for errors. Pending high level discussions on how we want - // to handle errors in devices. - let supported_events = EventSet::IN; - if !supported_events.contains(event_set) { - warn!("Received unknown event: {event_set:?} from source: {source:?}"); - return; - } - - if let Some(input) = self.input.as_mut() { - if input.as_raw_fd() == source { - let mut out = [0u8; 32]; - match input.read(&mut out[..]) { - Ok(count) => { - self.raw_input(&out[..count]) - .unwrap_or_else(|e| warn!("Serial error on input: {e}")); - } - Err(e) => { - warn!("error while reading stdin: {e:?}"); - } - } - } - } - } - - /// Initial registration of pollable objects. - /// If serial input is present, register the serial input FD as readable. - fn interest_list(&self) -> Vec { - match &self.input { - Some(input) => vec![EpollEvent::new(EventSet::IN, input.as_raw_fd() as u64)], - None => vec![], - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::io; - use std::io::Write; - use std::os::unix::io::{AsRawFd, RawFd}; - use std::sync::{Arc, Mutex}; - - use polly::event_manager::EventManager; - - struct SharedBufferInternal { - read_buf: Vec, - write_buf: Vec, - evfd: EventFd, - } - - #[derive(Clone)] - struct SharedBuffer { - internal: Arc>, - } - - impl SharedBuffer { - fn new() -> SharedBuffer { - SharedBuffer { - internal: Arc::new(Mutex::new(SharedBufferInternal { - read_buf: Vec::new(), - write_buf: Vec::new(), - evfd: EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap(), - })), - } - } - } - impl io::Write for SharedBuffer { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.internal.lock().unwrap().write_buf.write(buf) - } - fn flush(&mut self) -> io::Result<()> { - self.internal.lock().unwrap().write_buf.flush() - } - } - impl io::Read for SharedBuffer { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - self.internal.lock().unwrap().read_buf.as_slice().read(buf) - } - } - impl AsRawFd for SharedBuffer { - fn as_raw_fd(&self) -> RawFd { - self.internal.lock().unwrap().evfd.as_raw_fd() - } - } - impl ReadableFd for SharedBuffer {} - - static RAW_INPUT_BUF: [u8; 3] = [b'a', b'b', b'c']; - - #[test] - fn test_event_handling_no_in() { - let mut event_manager = EventManager::new().unwrap(); - - let intr_evt = EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap(); - let serial_out = SharedBuffer::new(); - - let mut serial = Serial::new_out(intr_evt, Box::new(serial_out)); - // A serial without in does not have any events in the list. - assert!(serial.interest_list().is_empty()); - // Even though there is no in, process should not panic. Call it to validate this. - let epoll_event = EpollEvent::new(EventSet::IN, 0); - serial.process(&epoll_event, &mut event_manager); - } - - #[test] - fn test_event_handling_with_in() { - let mut event_manager = EventManager::new().unwrap(); - - let intr_evt = EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap(); - let serial_in_out = SharedBuffer::new(); - - let mut serial = Serial::new_in_out( - intr_evt.try_clone().unwrap(), - Box::new(serial_in_out.clone()), - Box::new(serial_in_out), - ); - // Check that the interest list contains the EPOLL_IN event. - assert_eq!(serial.interest_list().len(), 1); - - // Process an invalid event type does not panic. - let invalid_event = EpollEvent::new(EventSet::OUT, intr_evt.as_raw_fd() as u64); - serial.process(&invalid_event, &mut event_manager); - - // Process an event with a `RawFd` that does not correspond to `intr_evt` does not panic. - let invalid_event = EpollEvent::new(EventSet::IN, 0); - serial.process(&invalid_event, &mut event_manager); - } - - #[test] - fn test_serial_output() { - let intr_evt = EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap(); - let serial_out = SharedBuffer::new(); - - let mut serial = Serial::new_out(intr_evt, Box::new(serial_out.clone())); - - // Invalid write of multiple chars at once. - serial.write(0, u64::from(DATA), &[b'x', b'y']); - // Valid one char at a time writes. - RAW_INPUT_BUF - .iter() - .for_each(|&c| serial.write(0, u64::from(DATA), &[c])); - assert_eq!( - serial_out.internal.lock().unwrap().write_buf.as_slice(), - &RAW_INPUT_BUF - ); - } - - #[test] - fn test_serial_raw_input() { - let intr_evt = EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap(); - let serial_out = SharedBuffer::new(); - - let mut serial = Serial::new_out(intr_evt.try_clone().unwrap(), Box::new(serial_out)); - - // Write 1 to the interrupt event fd, so that read doesn't block in case the event fd - // counter doesn't change (for 0 it blocks). - assert!(intr_evt.write(1).is_ok()); - serial.write(0, u64::from(IER), &[IER_RECV_BIT]); - serial.raw_input(&RAW_INPUT_BUF).unwrap(); - - // Verify the serial raised an interrupt. - assert_eq!(intr_evt.read().unwrap(), 2); - - // Check if reading in a 2-length array doesn't have side effects. - let mut data = [0u8, 0u8]; - serial.read(0, u64::from(DATA), &mut data[..]); - assert_eq!(data, [0u8, 0u8]); - - let mut data = [0u8]; - serial.read(0, u64::from(LSR), &mut data[..]); - assert_ne!(data[0] & LSR_DATA_BIT, 0); - - // Verify reading the previously inputted buffer. - RAW_INPUT_BUF.iter().for_each(|&c| { - serial.read(0, u64::from(DATA), &mut data[..]); - assert_eq!(data[0], c); - }); - - // Check if reading from the largest u8 offset returns 0. - serial.read(0, 0xff, &mut data[..]); - assert_eq!(data[0], 0); - } - - #[test] - fn test_serial_input() { - let intr_evt = EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap(); - let serial_in_out = SharedBuffer::new(); - - let mut serial = Serial::new_in_out( - intr_evt.try_clone().unwrap(), - Box::new(serial_in_out.clone()), - Box::new(serial_in_out.clone()), - ); - - // Write 1 to the interrupt event fd, so that read doesn't block in case the event fd - // counter doesn't change (for 0 it blocks). - assert!(intr_evt.write(1).is_ok()); - serial.write(0, u64::from(IER), &[IER_RECV_BIT]); - - // Prepare the input buffer. - { - let mut guard = serial_in_out.internal.lock().unwrap(); - guard.read_buf.write_all(&RAW_INPUT_BUF).unwrap(); - guard.evfd.write(1).unwrap(); - } - - let mut evmgr = EventManager::new().unwrap(); - let serial_wrap = Arc::new(Mutex::new(serial)); - evmgr.add_subscriber(serial_wrap.clone()).unwrap(); - - // Run the event handler which should drive serial input. - // There should be one event reported (which should have also handled serial input). - assert_eq!(evmgr.run_with_timeout(50).unwrap(), 1); - - // Verify the serial raised an interrupt. - assert_eq!(intr_evt.read().unwrap(), 2); - - let mut serial = serial_wrap.lock().unwrap(); - let mut data = [0u8]; - serial.read(0, u64::from(LSR), &mut data[..]); - assert_ne!(data[0] & LSR_DATA_BIT, 0); - - // Verify reading the previously inputted buffer. - RAW_INPUT_BUF.iter().for_each(|&c| { - serial.read(0, u64::from(DATA), &mut data[..]); - assert_eq!(data[0], c); - }); - } - - #[test] - fn test_serial_thr() { - let intr_evt = EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap(); - let mut serial = Serial::new_sink(intr_evt.try_clone().unwrap()); - - // write 1 to the interrupt event fd, so that read doesn't block in case the event fd - // counter doesn't change (for 0 it blocks) - assert!(intr_evt.write(1).is_ok()); - serial.write(0, u64::from(IER), &[IER_THR_BIT]); - serial.write(0, u64::from(DATA), &[b'a']); - - assert_eq!(intr_evt.read().unwrap(), 2); - let mut data = [0u8]; - serial.read(0, u64::from(IER), &mut data[..]); - assert_eq!(data[0] & IER_FIFO_BITS, IER_THR_BIT); - serial.read(0, u64::from(IIR), &mut data[..]); - assert_ne!(data[0] & IIR_THR_BIT, 0); - } - - #[test] - fn test_serial_dlab() { - let mut serial = Serial::new_sink(EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap()); - - serial.write(0, u64::from(LCR), &[LCR_DLAB_BIT]); - serial.write(0, u64::from(DLAB_LOW), &[0x12_u8]); - serial.write(0, u64::from(DLAB_HIGH), &[0x34_u8]); - - let mut data = [0u8]; - serial.read(0, u64::from(LCR), &mut data[..]); - assert_eq!(data[0], { LCR_DLAB_BIT }); - serial.read(0, u64::from(DLAB_LOW), &mut data[..]); - assert_eq!(data[0], 0x12); - serial.read(0, u64::from(DLAB_HIGH), &mut data[..]); - assert_eq!(data[0], 0x34); - } - - #[test] - fn test_serial_modem() { - let mut serial = Serial::new_sink(EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap()); - - serial.write(0, u64::from(MCR), &[MCR_LOOP_BIT]); - serial.write(0, u64::from(DATA), &[b'a']); - serial.write(0, u64::from(DATA), &[b'b']); - serial.write(0, u64::from(DATA), &[b'c']); - - let mut data = [0u8]; - serial.read(0, u64::from(MSR), &mut data[..]); - assert_eq!(data[0], { DEFAULT_MODEM_STATUS }); - serial.read(0, u64::from(MCR), &mut data[..]); - assert_eq!(data[0], { MCR_LOOP_BIT }); - serial.read(0, u64::from(DATA), &mut data[..]); - assert_eq!(data[0], b'a'); - serial.read(0, u64::from(DATA), &mut data[..]); - assert_eq!(data[0], b'b'); - serial.read(0, u64::from(DATA), &mut data[..]); - assert_eq!(data[0], b'c'); - } - - #[test] - fn test_serial_scratch() { - let mut serial = Serial::new_sink(EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap()); - - serial.write(0, u64::from(SCR), &[0x12_u8]); - - let mut data = [0u8]; - serial.read(0, u64::from(SCR), &mut data[..]); - assert_eq!(data[0], 0x12_u8); - } -} diff --git a/vendor/krun-devices/src/lib.rs b/vendor/krun-devices/src/lib.rs deleted file mode 100644 index 5df65dcf5..000000000 --- a/vendor/krun-devices/src/lib.rs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -// -// Portions Copyright 2017 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the THIRD-PARTY file. - -//! Emulates virtual and hardware devices. - -#[macro_use] -extern crate log; - -use std::fmt; -use std::io; - -mod bus; -#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))] -pub mod fdt; -pub mod legacy; -pub mod virtio; - -pub use self::bus::{Bus, BusDevice, Error as BusError}; - -#[derive(Debug)] -pub enum Error { - FailedReadingQueue { - event_type: &'static str, - underlying: io::Error, - }, - FailedReadTap, - FailedSignalingUsedQueue(io::Error), - PayloadExpected, - IoError(io::Error), - NoAvailBuffers, - SpuriousEvent, -} - -/// Types of devices that can get attached to this platform. -#[derive(Clone, Debug, PartialEq, Eq, Hash, Copy)] -pub enum DeviceType { - /// Device Type: Virtio. - Virtio(u32), - /// Device Type: GPIO (PL061). - #[cfg(target_arch = "aarch64")] - Gpio, - /// Device Type: Serial. - #[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))] - Serial, - /// Device Type: RTC. - #[cfg(target_arch = "aarch64")] - RTC, -} - -impl fmt::Display for DeviceType { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{self:?}") - } -} diff --git a/vendor/krun-devices/src/virtio/balloon/device.rs b/vendor/krun-devices/src/virtio/balloon/device.rs deleted file mode 100644 index 85b2812c1..000000000 --- a/vendor/krun-devices/src/virtio/balloon/device.rs +++ /dev/null @@ -1,195 +0,0 @@ -use std::cmp; -use std::convert::TryInto; -use std::io::Write; - -use utils::eventfd::EventFd; -use vm_memory::{ByteValued, GuestMemory, GuestMemoryMmap}; - -use super::super::{ - ActivateError, ActivateResult, BalloonError, DeviceQueue, DeviceState, QueueConfig, - VirtioDevice, -}; -use super::{defs, defs::uapi}; -use crate::virtio::InterruptTransport; - -// Inflate queue. -pub(crate) const IFQ_INDEX: usize = 0; -// Deflate queue. -pub(crate) const DFQ_INDEX: usize = 1; -// Stats queue. -pub(crate) const STQ_INDEX: usize = 2; -// Page-hinting queue. -pub(crate) const PHQ_INDEX: usize = 3; -// Free page reporting queue. -pub(crate) const FRQ_INDEX: usize = 4; - -// Supported features. -pub(crate) const AVAIL_FEATURES: u64 = (1 << uapi::VIRTIO_F_VERSION_1 as u64) - | (1 << uapi::VIRTIO_BALLOON_F_STATS_VQ as u64) - | (1 << uapi::VIRTIO_BALLOON_F_FREE_PAGE_HINT as u64) - | (1 << uapi::VIRTIO_BALLOON_F_REPORTING as u64); - -#[derive(Copy, Clone, Debug, Default)] -#[repr(C, packed)] -pub struct VirtioBalloonConfig { - /* Number of pages host wants Guest to give up. */ - num_pages: u32, - /* Number of pages we've actually got in balloon. */ - actual: u32, - /* Free page report command id, readonly by guest */ - free_page_report_cmd_id: u32, - /* Stores PAGE_POISON if page poisoning is in use */ - poison_val: u32, -} - -// Safe because it only has data and has no implicit padding. -unsafe impl ByteValued for VirtioBalloonConfig {} - -pub struct Balloon { - pub(crate) queues: Option>, - pub(crate) avail_features: u64, - pub(crate) acked_features: u64, - pub(crate) activate_evt: EventFd, - pub(crate) device_state: DeviceState, - config: VirtioBalloonConfig, -} - -impl Balloon { - pub fn new() -> super::Result { - Ok(Balloon { - queues: None, - avail_features: AVAIL_FEATURES, - acked_features: 0, - activate_evt: EventFd::new(utils::eventfd::EFD_NONBLOCK) - .map_err(BalloonError::EventFd)?, - device_state: DeviceState::Inactive, - config: VirtioBalloonConfig::default(), - }) - } - - pub fn id(&self) -> &str { - defs::BALLOON_DEV_ID - } - - pub fn process_frq(&mut self) -> bool { - debug!("balloon: process_frq()"); - let mem = match self.device_state { - DeviceState::Activated(ref mem, _) => mem, - // This should never happen, it's been already validated in the event handler. - DeviceState::Inactive => unreachable!(), - }; - - let queues = self - .queues - .as_mut() - .expect("queues should exist when activated"); - let mut have_used = false; - - while let Some(head) = queues[FRQ_INDEX].queue.pop(mem) { - let index = head.index; - for desc in head.into_iter() { - let host_addr = mem.get_host_address(desc.addr).unwrap(); - debug!( - "balloon: should release guest_addr={:?} host_addr={:p} len={}", - desc.addr, host_addr, desc.len - ); - #[cfg(target_os = "linux")] - let advice = libc::MADV_DONTNEED; - #[cfg(target_os = "macos")] - let advice = libc::MADV_FREE; - unsafe { - libc::madvise( - host_addr as *mut libc::c_void, - desc.len.try_into().unwrap(), - advice, - ) - }; - } - - have_used = true; - if let Err(e) = queues[FRQ_INDEX].queue.add_used(mem, index, 0) { - error!("failed to add used elements to the queue: {e:?}"); - } - } - - have_used - } -} - -impl VirtioDevice for Balloon { - fn avail_features(&self) -> u64 { - self.avail_features - } - - fn acked_features(&self) -> u64 { - self.acked_features - } - - fn set_acked_features(&mut self, acked_features: u64) { - self.acked_features = acked_features - } - - fn device_type(&self) -> u32 { - uapi::VIRTIO_ID_BALLOON - } - - fn device_name(&self) -> &str { - "balloon" - } - - fn queue_config(&self) -> &[QueueConfig] { - &defs::QUEUE_CONFIG - } - - fn read_config(&self, offset: u64, mut data: &mut [u8]) { - let config_slice = self.config.as_slice(); - let config_len = config_slice.len() as u64; - if offset >= config_len { - error!("Failed to read config space"); - return; - } - if let Some(end) = offset.checked_add(data.len() as u64) { - // This write can't fail, offset and end are checked against config_len. - data.write_all(&config_slice[offset as usize..cmp::min(end, config_len) as usize]) - .unwrap(); - } - } - - fn write_config(&mut self, offset: u64, data: &[u8]) { - warn!( - "balloon: guest driver attempted to write device config (offset={:x}, len={:x})", - offset, - data.len() - ); - } - - fn activate( - &mut self, - mem: GuestMemoryMmap, - interrupt: InterruptTransport, - queues: Vec, - ) -> ActivateResult { - if queues.len() != defs::NUM_QUEUES { - error!( - "Cannot perform activate. Expected {} queue(s), got {}", - defs::NUM_QUEUES, - queues.len() - ); - return Err(ActivateError::BadActivate); - } - - if self.activate_evt.write(1).is_err() { - error!("Cannot write to activate_evt",); - return Err(ActivateError::BadActivate); - } - - self.queues = Some(queues); - self.device_state = DeviceState::Activated(mem, interrupt); - - Ok(()) - } - - fn is_activated(&self) -> bool { - self.device_state.is_activated() - } -} diff --git a/vendor/krun-devices/src/virtio/balloon/event_handler.rs b/vendor/krun-devices/src/virtio/balloon/event_handler.rs deleted file mode 100644 index 2558d6e14..000000000 --- a/vendor/krun-devices/src/virtio/balloon/event_handler.rs +++ /dev/null @@ -1,189 +0,0 @@ -use std::os::unix::io::AsRawFd; - -use polly::event_manager::{EventManager, Subscriber}; -use utils::epoll::{EpollEvent, EventSet}; - -use super::device::{Balloon, DFQ_INDEX, FRQ_INDEX, IFQ_INDEX, PHQ_INDEX, STQ_INDEX}; -use crate::virtio::device::VirtioDevice; - -impl Balloon { - fn queue_event(&self, idx: usize) -> &std::sync::Arc { - &self.queues.as_ref().expect("queues should exist")[idx].event - } - - pub(crate) fn handle_ifq_event(&mut self, event: &EpollEvent) { - error!("balloon: unsupported inflate queue event"); - - let event_set = event.event_set(); - if event_set != EventSet::IN { - warn!("balloon: inflate unexpected event {event_set:?}"); - return; - } - - if let Err(e) = self.queue_event(IFQ_INDEX).read() { - error!("Failed to read balloon inflate queue event: {e:?}"); - } - } - - pub(crate) fn handle_dfq_event(&mut self, event: &EpollEvent) { - error!("balloon: unsupported deflate queue event"); - - let event_set = event.event_set(); - if event_set != EventSet::IN { - warn!("balloon: deflate unexpected event {event_set:?}"); - return; - } - - if let Err(e) = self.queue_event(DFQ_INDEX).read() { - error!("Failed to read balloon inflate queue event: {e:?}"); - } - } - - pub(crate) fn handle_stq_event(&mut self, event: &EpollEvent) { - debug!("balloon: stats queue event (ignored)"); - - let event_set = event.event_set(); - if event_set != EventSet::IN { - warn!("balloon: stats unexpected event {event_set:?}"); - return; - } - - if let Err(e) = self.queue_event(STQ_INDEX).read() { - error!("Failed to read balloon stats queue event: {e:?}"); - } - } - - pub(crate) fn handle_phq_event(&mut self, event: &EpollEvent) { - error!("balloon: unsupported page-hinting queue event"); - - let event_set = event.event_set(); - if event_set != EventSet::IN { - warn!("balloon: page-hinting unexpected event {event_set:?}"); - return; - } - - if let Err(e) = self.queue_event(PHQ_INDEX).read() { - error!("Failed to read balloon page-hinting queue event: {e:?}"); - } - } - - pub(crate) fn handle_frq_event(&mut self, event: &EpollEvent) { - debug!("balloon: free-page reporting queue event"); - - let event_set = event.event_set(); - if event_set != EventSet::IN { - warn!("balloon: free-page reporting unexpected event {event_set:?}"); - return; - } - - if let Err(e) = self.queue_event(FRQ_INDEX).read() { - error!("Failed to read balloon free-page reporting queue event: {e:?}"); - } else if self.process_frq() { - self.device_state.signal_used_queue(); - } - } - - fn handle_activate_event(&self, event_manager: &mut EventManager) { - debug!("balloon: activate event"); - if let Err(e) = self.activate_evt.read() { - error!("Failed to consume balloon activate event: {e:?}"); - } - - // The subscriber must exist as we previously registered activate_evt via - // `interest_list()`. - let self_subscriber = event_manager - .subscriber(self.activate_evt.as_raw_fd()) - .unwrap(); - - event_manager - .register( - self.queue_event(IFQ_INDEX).as_raw_fd(), - EpollEvent::new(EventSet::IN, self.queue_event(IFQ_INDEX).as_raw_fd() as u64), - self_subscriber.clone(), - ) - .unwrap_or_else(|e| { - error!("Failed to register balloon ifq with event manager: {e:?}"); - }); - - event_manager - .register( - self.queue_event(DFQ_INDEX).as_raw_fd(), - EpollEvent::new(EventSet::IN, self.queue_event(DFQ_INDEX).as_raw_fd() as u64), - self_subscriber.clone(), - ) - .unwrap_or_else(|e| { - error!("Failed to register balloon dfq with event manager: {e:?}"); - }); - - event_manager - .register( - self.queue_event(STQ_INDEX).as_raw_fd(), - EpollEvent::new(EventSet::IN, self.queue_event(STQ_INDEX).as_raw_fd() as u64), - self_subscriber.clone(), - ) - .unwrap_or_else(|e| { - error!("Failed to register balloon stq with event manager: {e:?}"); - }); - - event_manager - .register( - self.queue_event(PHQ_INDEX).as_raw_fd(), - EpollEvent::new(EventSet::IN, self.queue_event(PHQ_INDEX).as_raw_fd() as u64), - self_subscriber.clone(), - ) - .unwrap_or_else(|e| { - error!("Failed to register balloon dfq with event manager: {e:?}"); - }); - - event_manager - .register( - self.queue_event(FRQ_INDEX).as_raw_fd(), - EpollEvent::new(EventSet::IN, self.queue_event(FRQ_INDEX).as_raw_fd() as u64), - self_subscriber.clone(), - ) - .unwrap_or_else(|e| { - error!("Failed to register balloon frq with event manager: {e:?}"); - }); - - event_manager - .unregister(self.activate_evt.as_raw_fd()) - .unwrap_or_else(|e| { - error!("Failed to unregister balloon activate evt: {e:?}"); - }) - } -} - -impl Subscriber for Balloon { - fn process(&mut self, event: &EpollEvent, event_manager: &mut EventManager) { - let source = event.fd(); - let ifq = self.queue_event(IFQ_INDEX).as_raw_fd(); - let dfq = self.queue_event(DFQ_INDEX).as_raw_fd(); - let stq = self.queue_event(STQ_INDEX).as_raw_fd(); - let phq = self.queue_event(PHQ_INDEX).as_raw_fd(); - let frq = self.queue_event(FRQ_INDEX).as_raw_fd(); - let activate_evt = self.activate_evt.as_raw_fd(); - - if self.is_activated() { - match source { - _ if source == ifq => self.handle_ifq_event(event), - _ if source == dfq => self.handle_dfq_event(event), - _ if source == stq => self.handle_stq_event(event), - _ if source == phq => self.handle_phq_event(event), - _ if source == frq => self.handle_frq_event(event), - _ if source == activate_evt => { - self.handle_activate_event(event_manager); - } - _ => warn!("Unexpected balloon event received: {source:?}"), - } - } else { - warn!("balloon: The device is not yet activated. Spurious event received: {source:?}"); - } - } - - fn interest_list(&self) -> Vec { - vec![EpollEvent::new( - EventSet::IN, - self.activate_evt.as_raw_fd() as u64, - )] - } -} diff --git a/vendor/krun-devices/src/virtio/balloon/mod.rs b/vendor/krun-devices/src/virtio/balloon/mod.rs deleted file mode 100644 index 6859c79b8..000000000 --- a/vendor/krun-devices/src/virtio/balloon/mod.rs +++ /dev/null @@ -1,30 +0,0 @@ -mod device; -mod event_handler; - -pub use self::defs::uapi::VIRTIO_ID_BALLOON as TYPE_BALLOON; -pub use self::device::Balloon; - -mod defs { - use super::super::QueueConfig; - - pub const BALLOON_DEV_ID: &str = "virtio_balloon"; - pub const NUM_QUEUES: usize = 5; - pub const QUEUE_SIZE: u16 = 256; - pub static QUEUE_CONFIG: [QueueConfig; NUM_QUEUES] = [QueueConfig::new(QUEUE_SIZE); NUM_QUEUES]; - - pub mod uapi { - pub const VIRTIO_F_VERSION_1: u32 = 32; - pub const VIRTIO_ID_BALLOON: u32 = 5; - pub const VIRTIO_BALLOON_F_STATS_VQ: u32 = 1; - pub const VIRTIO_BALLOON_F_FREE_PAGE_HINT: u32 = 3; - pub const VIRTIO_BALLOON_F_REPORTING: u32 = 5; - } -} - -#[derive(Debug)] -pub enum BalloonError { - /// Failed to create event fd. - EventFd(std::io::Error), -} - -type Result = std::result::Result; diff --git a/vendor/krun-devices/src/virtio/bindings.rs b/vendor/krun-devices/src/virtio/bindings.rs deleted file mode 100644 index f25a248f3..000000000 --- a/vendor/krun-devices/src/virtio/bindings.rs +++ /dev/null @@ -1,219 +0,0 @@ -#![allow(clippy::missing_safety_doc)] -use libc; - -pub const LINUX_EACCES: libc::c_int = 13; -pub const LINUX_ENODATA: libc::c_int = 61; -pub const LINUX_ENOSYS: libc::c_int = 38; -pub const LINUX_ENOTEMPTY: libc::c_int = 39; - -pub const LINUX_O_APPEND: libc::c_int = 1024; -pub const LINUX_O_CLOEXEC: libc::c_int = 0x80000; -pub const LINUX_O_DIRECT: libc::c_int = 0x4000; -pub const LINUX_O_DIRECTORY: libc::c_int = 0x10000; -pub const LINUX_O_LARGEFILE: libc::c_int = 0; -pub const LINUX_O_NOFOLLOW: libc::c_int = 0x20000; -pub const LINUX_O_CREAT: libc::c_int = 64; -pub const LINUX_O_EXCL: libc::c_int = 128; -pub const LINUX_O_NOCTTY: libc::c_int = 256; -pub const LINUX_O_NONBLOCK: libc::c_int = 2048; -pub const LINUX_O_SYNC: libc::c_int = 1052672; -pub const LINUX_O_TRUNC: libc::c_int = 512; -pub const LINUX_O_RSYNC: libc::c_int = 1052672; -pub const LINUX_O_DSYNC: libc::c_int = 4096; -pub const LINUX_O_ASYNC: libc::c_int = 0x2000; - -pub const LINUX_RENAME_NOREPLACE: libc::c_int = 1 << 0; -pub const LINUX_RENAME_EXCHANGE: libc::c_int = 1 << 1; -pub const LINUX_RENAME_WHITEOUT: libc::c_int = 1 << 2; - -pub const LINUX_XATTR_CREATE: libc::c_int = 1; -pub const LINUX_XATTR_REPLACE: libc::c_int = 2; - -pub const LINUX_FALLOC_FL_ALLOCATE_RANGE: libc::c_int = 0; -pub const LINUX_FALLOC_FL_KEEP_SIZE: libc::c_int = 1; -pub const LINUX_FALLOC_FL_PUNCH_HOLE: libc::c_int = 2; - -#[cfg(target_os = "macos")] -pub type stat64 = libc::stat; -#[cfg(target_os = "linux")] -pub use libc::stat64; - -#[cfg(target_os = "macos")] -pub type off64_t = libc::off_t; -#[cfg(target_os = "linux")] -pub use libc::off64_t; - -#[cfg(target_os = "macos")] -pub type statvfs64 = libc::statvfs; -#[cfg(target_os = "linux")] -pub use libc::statvfs64; - -#[cfg(target_os = "macos")] -pub type ino64_t = libc::ino_t; -#[cfg(target_os = "linux")] -pub use libc::ino64_t; - -#[cfg(target_os = "linux")] -pub unsafe fn pread64( - fd: libc::c_int, - buf: *mut libc::c_void, - count: libc::size_t, - offset: off64_t, -) -> libc::ssize_t { - libc::pread64(fd, buf, count, offset) -} -#[cfg(target_os = "macos")] -pub unsafe fn pread64( - fd: libc::c_int, - buf: *mut libc::c_void, - count: libc::size_t, - offset: off64_t, -) -> libc::ssize_t { - libc::pread(fd, buf, count, offset) -} - -#[cfg(target_os = "linux")] -pub unsafe fn preadv64( - fd: libc::c_int, - iov: *const libc::iovec, - iovcnt: libc::c_int, - offset: off64_t, -) -> libc::ssize_t { - libc::preadv64(fd, iov, iovcnt, offset) -} -#[cfg(target_os = "macos")] -pub unsafe fn preadv64( - fd: libc::c_int, - iov: *const libc::iovec, - iovcnt: libc::c_int, - offset: off64_t, -) -> libc::ssize_t { - libc::preadv(fd, iov, iovcnt, offset) -} - -#[cfg(target_os = "linux")] -pub unsafe fn pwrite64( - fd: libc::c_int, - buf: *const libc::c_void, - count: libc::size_t, - offset: off64_t, -) -> libc::ssize_t { - libc::pwrite64(fd, buf, count, offset) -} -#[cfg(target_os = "macos")] -pub unsafe fn pwrite64( - fd: libc::c_int, - buf: *const libc::c_void, - count: libc::size_t, - offset: off64_t, -) -> libc::ssize_t { - libc::pwrite(fd, buf, count, offset) -} - -#[cfg(target_os = "linux")] -pub unsafe fn pwritev64( - fd: libc::c_int, - iov: *const libc::iovec, - iovcnt: libc::c_int, - offset: off64_t, -) -> libc::ssize_t { - libc::pwritev64(fd, iov, iovcnt, offset) -} -#[cfg(target_os = "macos")] -pub unsafe fn pwritev64( - fd: libc::c_int, - iov: *const libc::iovec, - iovcnt: libc::c_int, - offset: off64_t, -) -> libc::ssize_t { - libc::pwritev(fd, iov, iovcnt, offset) -} - -#[cfg(target_os = "linux")] -pub unsafe fn fstatat64( - dirfd: libc::c_int, - pathname: *const libc::c_char, - buf: *mut stat64, - flags: libc::c_int, -) -> libc::c_int { - libc::fstatat64(dirfd, pathname, buf, flags) -} -#[cfg(target_os = "macos")] -pub unsafe fn fstatat64( - dirfd: libc::c_int, - pathname: *const libc::c_char, - buf: *mut stat64, - flags: libc::c_int, -) -> libc::c_int { - libc::fstatat(dirfd, pathname, buf, flags) -} - -#[cfg(target_os = "linux")] -pub unsafe fn fallocate64( - fd: libc::c_int, - mode: libc::c_int, - offset: off64_t, - len: off64_t, -) -> libc::c_int { - libc::fallocate64(fd, mode, offset, len) -} -#[cfg(target_os = "macos")] -pub unsafe fn fallocate64( - _fd: libc::c_int, - _mode: libc::c_int, - _offset: off64_t, - _len: off64_t, -) -> libc::c_int { - -LINUX_ENOSYS -} - -#[cfg(target_os = "linux")] -pub unsafe fn ftruncate64(fd: libc::c_int, length: off64_t) -> libc::c_int { - libc::ftruncate64(fd, length) -} -#[cfg(target_os = "macos")] -pub unsafe fn ftruncate64(fd: libc::c_int, length: off64_t) -> libc::c_int { - libc::ftruncate(fd, length) -} - -#[cfg(target_os = "linux")] -pub unsafe fn lseek64(fd: libc::c_int, offset: off64_t, whence: libc::c_int) -> off64_t { - libc::lseek64(fd, offset, whence) -} -#[cfg(target_os = "macos")] -pub unsafe fn lseek64(fd: libc::c_int, offset: off64_t, whence: libc::c_int) -> off64_t { - libc::lseek(fd, offset, whence) -} - -#[cfg(target_os = "macos")] -pub unsafe fn statvfs64(path: *const libc::c_char, buf: *mut statvfs64) -> libc::c_int { - libc::statvfs(path, buf) -} - -#[cfg(target_os = "linux")] -pub unsafe fn fstatvfs64(fd: libc::c_int, buf: *mut statvfs64) -> libc::c_int { - libc::fstatvfs64(fd, buf) -} -#[cfg(target_os = "macos")] -pub unsafe fn fstatvfs64(fd: libc::c_int, buf: *mut statvfs64) -> libc::c_int { - libc::fstatvfs(fd, buf) -} - -#[cfg(target_os = "linux")] -pub unsafe fn mknodat( - dirfd: libc::c_int, - pathname: *const libc::c_char, - mode: libc::mode_t, - dev: libc::dev_t, -) -> libc::c_int { - libc::mknodat(dirfd, pathname, mode, dev) -} -#[cfg(target_os = "macos")] -pub unsafe fn mknodat( - _dirfd: libc::c_int, - _pathname: *const libc::c_char, - _mode: libc::mode_t, - _dev: u64, -) -> libc::c_int { - -LINUX_ENOSYS -} diff --git a/vendor/krun-devices/src/virtio/block/device.rs b/vendor/krun-devices/src/virtio/block/device.rs deleted file mode 100644 index c943b10e0..000000000 --- a/vendor/krun-devices/src/virtio/block/device.rs +++ /dev/null @@ -1,444 +0,0 @@ -// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -// -// Portions Copyright 2017 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the THIRD-PARTY file. - -use std::cmp; -use std::convert::From; -use std::fs::{File, OpenOptions}; -use std::io::{self, Write}; -#[cfg(target_os = "linux")] -use std::os::linux::fs::MetadataExt; -#[cfg(target_os = "macos")] -use std::os::macos::fs::MetadataExt; -use std::path::PathBuf; -use std::result; -use std::sync::{Arc, Mutex}; -use std::thread::JoinHandle; - -use imago::{ - file::File as ImagoFile, qcow2::Qcow2, raw::Raw, vmdk::Vmdk, DynStorage, FormatDriverBuilder, - PermissiveImplicitOpenGate, Storage, StorageOpenOptions, SyncFormatAccess, -}; -use log::{error, warn}; -use utils::eventfd::{EventFd, EFD_NONBLOCK}; -use virtio_bindings::{ - virtio_blk::*, virtio_config::VIRTIO_F_VERSION_1, virtio_ring::VIRTIO_RING_F_EVENT_IDX, -}; -use vm_memory::{ByteValued, GuestMemoryMmap}; - -use super::worker::BlockWorker; -use super::{ - super::{ActivateResult, DeviceQueue, DeviceState, QueueConfig, VirtioDevice, TYPE_BLOCK}, - Error, NUM_QUEUES, QUEUE_CONFIG, SECTOR_SHIFT, SECTOR_SIZE, -}; - -use crate::virtio::{ - block::{ImageType, SyncMode}, - ActivateError, InterruptTransport, -}; - -/// Configuration options for disk caching. -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -pub enum CacheType { - /// Flushing mechanic will be advertised to the guest driver, but - /// the operation will be a noop. - #[default] - Unsafe, - /// Flushing mechanic will be advertised to the guest driver and - /// flush requests coming from the guest will be performed using - /// `fsync`. - Writeback, -} - -impl CacheType { - /// Picks the appropriate cache type based on disk image or device path. - /// Special files like `/dev/rdisk*` on macOS do not support flush/sync. - pub fn auto(_path: &str) -> CacheType { - #[cfg(target_os = "macos")] - if _path.starts_with("/dev/rdisk") { - return CacheType::Unsafe; - } - CacheType::Writeback - } -} - -/// Helper object for setting up all `Block` fields derived from its backing file. -pub(crate) struct DiskProperties { - cache_type: CacheType, - pub(crate) file: Arc>>>, - nsectors: u64, - image_id: Vec, -} - -impl DiskProperties { - pub fn new( - disk_image: Arc>>>, - disk_image_id: Vec, - cache_type: CacheType, - ) -> io::Result { - let disk_size = disk_image.lock().unwrap().size(); - - // We only support disk size, which uses the first two words of the configuration space. - // If the image is not a multiple of the sector size, the tail bits are not exposed. - if !disk_size.is_multiple_of(SECTOR_SIZE) { - warn!( - "Disk size {disk_size} is not a multiple of sector size {SECTOR_SIZE}; \ - the remainder will not be visible to the guest." - ); - } - - Ok(Self { - cache_type, - nsectors: disk_size >> SECTOR_SHIFT, - image_id: disk_image_id, - file: disk_image, - }) - } - - pub fn nsectors(&self) -> u64 { - self.nsectors - } - - pub fn image_id(&self) -> &[u8] { - &self.image_id - } - - fn build_device_id(disk_file: &File) -> result::Result { - let blk_metadata = disk_file.metadata().map_err(Error::GetFileMetadata)?; - // This is how kvmtool does it. - let device_id = format!( - "{}{}{}", - blk_metadata.st_dev(), - blk_metadata.st_rdev(), - blk_metadata.st_ino() - ); - Ok(device_id) - } - - fn build_disk_image_id(disk_file: &File) -> Vec { - let mut default_id = vec![0; VIRTIO_BLK_ID_BYTES as usize]; - match Self::build_device_id(disk_file) { - Err(_) => { - warn!("Could not generate device id. We'll use a default."); - } - Ok(m) => { - // The kernel only knows to read a maximum of VIRTIO_BLK_ID_BYTES. - // This will also zero out any leftover bytes. - let disk_id = m.as_bytes(); - let bytes_to_copy = cmp::min(disk_id.len(), VIRTIO_BLK_ID_BYTES as usize); - default_id[..bytes_to_copy].clone_from_slice(&disk_id[..bytes_to_copy]) - } - } - default_id - } - - pub fn cache_type(&self) -> CacheType { - self.cache_type - } -} - -impl Drop for DiskProperties { - fn drop(&mut self) { - match self.cache_type { - CacheType::Writeback => { - // flush() first to force any cached data out. - if self.file.lock().unwrap().flush().is_err() { - error!("Failed to flush block data on drop."); - } - // Sync data out to physical media on host. - if self.file.lock().unwrap().sync().is_err() { - error!("Failed to sync block data on drop.") - } - } - CacheType::Unsafe => { - // This is a noop. - } - }; - } -} - -#[derive(Copy, Clone, Debug, Default)] -#[repr(C, packed)] -struct VirtioBlkGeometry { - cylinders: u16, - heads: u8, - sectors: u8, -} - -#[derive(Copy, Clone, Debug, Default)] -#[repr(C, packed)] -struct VirtioBlkTopology { - physical_block_exp: u8, - alignment_offset: u8, - min_io_size: u16, - opt_io_size: u32, -} - -#[derive(Copy, Clone, Debug, Default)] -#[repr(C, packed)] -struct VirtioBlkConfig { - capacity: u64, - size_max: u32, - seg_max: u32, - geometry: VirtioBlkGeometry, - blk_size: u32, - topology: VirtioBlkTopology, - writeback: u8, - unused0: u8, - num_queues: u16, - max_discard_sectors: u32, - max_discard_seg: u32, - discard_sector_alignment: u32, - max_write_zeroes_sectors: u32, - max_write_zeroes_seg: u32, - write_zeroes_may_unmap: u8, -} - -// Safe because it only has data and has no implicit padding. -unsafe impl ByteValued for VirtioBlkConfig {} - -/// Virtio device for exposing block level read/write operations on a host file. -pub struct Block { - // Host file and properties. - disk: Option, - cache_type: CacheType, - disk_image: Arc>>>, - disk_image_id: Vec, - worker_thread: Option>, - worker_stopfd: EventFd, - - // Virtio fields. - pub(crate) avail_features: u64, - pub(crate) acked_features: u64, - config: VirtioBlkConfig, - - // Transport related fields. - pub(crate) device_state: DeviceState, - - // Implementation specific fields. - pub(crate) id: String, - pub(crate) partuuid: Option, -} - -impl Block { - /// Create a new virtio block device that operates on the given file. - /// - /// The given file must be seekable and sizable. - #[allow(clippy::too_many_arguments)] - pub fn new( - id: String, - partuuid: Option, - cache_type: CacheType, - disk_image_path: String, - disk_image_format: ImageType, - is_disk_read_only: bool, - direct_io: bool, - sync_mode: SyncMode, - ) -> io::Result { - let disk_image = OpenOptions::new() - .read(true) - .write(!is_disk_read_only) - .open(PathBuf::from(&disk_image_path))?; - - let disk_image_id = DiskProperties::build_disk_image_id(&disk_image); - - let file_opts = StorageOpenOptions::new() - .write(!is_disk_read_only) - .filename(disk_image_path) - .direct(direct_io); - - #[cfg(target_os = "macos")] - let file_opts = file_opts.relaxed_sync(sync_mode == SyncMode::Relaxed); - let file = ImagoFile::open_sync(file_opts)?; - let discard_alignment = file.discard_align(); - - let disk_image = match disk_image_format { - ImageType::Qcow2 => { - let mut qcow2 = - Qcow2::, Arc>>::open_image_sync( - Box::new(file), - !is_disk_read_only, - )?; - qcow2.open_implicit_dependencies_sync()?; - SyncFormatAccess::new(qcow2)? - } - ImageType::Raw => { - let raw = Raw::>::open_image_sync( - Box::new(file), - !is_disk_read_only, - )?; - SyncFormatAccess::new(raw)? - } - ImageType::Vmdk => { - let vmdk = Vmdk::, Arc>>::builder( - Box::new(file), - ) - .open_sync(PermissiveImplicitOpenGate::default())?; - SyncFormatAccess::new(vmdk)? - } - }; - - let disk_image = Arc::new(Mutex::new(disk_image)); - - let disk_properties = - DiskProperties::new(disk_image.clone(), disk_image_id.clone(), cache_type)?; - - let mut avail_features = (1u64 << VIRTIO_F_VERSION_1) - | (1u64 << VIRTIO_BLK_F_SEG_MAX) - | (1u64 << VIRTIO_BLK_F_DISCARD) - | (1u64 << VIRTIO_BLK_F_WRITE_ZEROES) - | (1u64 << VIRTIO_RING_F_EVENT_IDX); - - if sync_mode != SyncMode::None { - avail_features |= 1u64 << VIRTIO_BLK_F_FLUSH; - } - - if is_disk_read_only { - avail_features |= 1u64 << VIRTIO_BLK_F_RO; - }; - - let config = VirtioBlkConfig { - capacity: disk_properties.nsectors(), - size_max: 0, - // QUEUE_SIZE - 2 - seg_max: 254, - max_discard_sectors: u32::MAX, - max_discard_seg: 1, - discard_sector_alignment: discard_alignment as u32 / 512, - max_write_zeroes_sectors: u32::MAX, - max_write_zeroes_seg: 1, - write_zeroes_may_unmap: 1, - ..Default::default() - }; - - Ok(Block { - id, - partuuid, - config, - disk: Some(disk_properties), - cache_type, - disk_image, - disk_image_id, - avail_features, - acked_features: 0u64, - device_state: DeviceState::Inactive, - worker_thread: None, - worker_stopfd: EventFd::new(EFD_NONBLOCK)?, - }) - } - - /// Provides the ID of this block device. - pub fn id(&self) -> &String { - &self.id - } - - /// Provides the PARTUUID of this block device. - pub fn partuuid(&self) -> Option<&String> { - self.partuuid.as_ref() - } - - /// Specifies if this block device is read only. - pub fn is_read_only(&self) -> bool { - self.avail_features & (1u64 << VIRTIO_BLK_F_RO) != 0 - } -} - -impl VirtioDevice for Block { - fn device_type(&self) -> u32 { - TYPE_BLOCK - } - - fn device_name(&self) -> &str { - "block" - } - - fn queue_config(&self) -> &[QueueConfig] { - &QUEUE_CONFIG - } - - fn avail_features(&self) -> u64 { - self.avail_features - } - - fn acked_features(&self) -> u64 { - self.acked_features - } - - fn set_acked_features(&mut self, acked_features: u64) { - self.acked_features = acked_features; - } - - fn read_config(&self, offset: u64, mut data: &mut [u8]) { - let config_slice = self.config.as_slice(); - let config_len = config_slice.len() as u64; - if offset >= config_len { - error!("Failed to read config space"); - return; - } - if let Some(end) = offset.checked_add(data.len() as u64) { - // This write can't fail, offset and end are checked against config_len. - data.write_all(&config_slice[offset as usize..cmp::min(end, config_len) as usize]) - .unwrap(); - } - } - - fn write_config(&mut self, _offset: u64, _data: &[u8]) { - error!("Guest attempted to write config"); - } - - fn is_activated(&self) -> bool { - self.device_state.is_activated() - } - - fn activate( - &mut self, - mem: GuestMemoryMmap, - interrupt: InterruptTransport, - queues: Vec, - ) -> ActivateResult { - if self.worker_thread.is_some() { - panic!("virtio_blk: worker thread already exists"); - } - - let [blk_q]: [_; NUM_QUEUES] = queues.try_into().map_err(|_| { - error!("Cannot perform activate. Expected {} queue(s)", NUM_QUEUES); - ActivateError::BadActivate - })?; - - let disk = match self.disk.take() { - Some(d) => d, - None => DiskProperties::new( - Arc::clone(&self.disk_image), - self.disk_image_id.clone(), - self.cache_type, - ) - .map_err(|_| ActivateError::BadActivate)?, - }; - - let worker = BlockWorker::new( - blk_q, - interrupt.clone(), - mem.clone(), - disk, - self.worker_stopfd.try_clone().unwrap(), - ); - self.worker_thread = Some(worker.run()); - - self.device_state = DeviceState::Activated(mem, interrupt); - Ok(()) - } - - fn reset(&mut self) -> bool { - if let Some(worker) = self.worker_thread.take() { - let _ = self.worker_stopfd.write(1); - if let Err(e) = worker.join() { - error!("error waiting for worker thread: {e:?}"); - } - } - self.device_state = DeviceState::Inactive; - true - } -} diff --git a/vendor/krun-devices/src/virtio/block/event_handler.rs b/vendor/krun-devices/src/virtio/block/event_handler.rs deleted file mode 100644 index e69de29bb..000000000 diff --git a/vendor/krun-devices/src/virtio/block/mod.rs b/vendor/krun-devices/src/virtio/block/mod.rs deleted file mode 100644 index c842bf271..000000000 --- a/vendor/krun-devices/src/virtio/block/mod.rs +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -pub mod device; -mod worker; - -pub use self::device::{Block, CacheType}; - -use vm_memory::GuestMemoryError; - -use super::QueueConfig; - -pub const CONFIG_SPACE_SIZE: usize = 8; -pub const SECTOR_SHIFT: u8 = 9; -pub const SECTOR_SIZE: u64 = (0x01_u64) << SECTOR_SHIFT; -const QUEUE_SIZE: u16 = 256; -pub const NUM_QUEUES: usize = 1; -pub static QUEUE_CONFIG: [QueueConfig; NUM_QUEUES] = [QueueConfig::new(QUEUE_SIZE)]; - -#[derive(Debug)] -pub enum Error { - /// Guest gave us too few descriptors in a descriptor chain. - DescriptorChainTooShort, - /// Guest gave us a descriptor that was too short to use. - DescriptorLengthTooSmall, - /// Getting a block's metadata fails for any reason. - GetFileMetadata(std::io::Error), - /// Guest gave us bad memory addresses. - GuestMemory(GuestMemoryError), - /// The requested operation would cause a seek beyond disk end. - InvalidOffset, - /// Guest gave us a read only descriptor that protocol says to write to. - UnexpectedReadOnlyDescriptor, - /// Guest gave us a write only descriptor that protocol says to read from. - UnexpectedWriteOnlyDescriptor, -} - -/// Supported disk image formats -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum ImageType { - Raw, - Qcow2, - Vmdk, -} - -impl TryFrom for ImageType { - type Error = (); - - fn try_from(disk_format: u32) -> Result { - match disk_format { - 0 => Ok(ImageType::Raw), - 1 => Ok(ImageType::Qcow2), - 2 => Ok(ImageType::Vmdk), - _ => { - // Do not continue if the user cannot specify a valid disk format - Err(()) - } - } - } -} - -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub enum SyncMode { - None, - Relaxed, - #[default] - Full, -} - -impl TryFrom for SyncMode { - type Error = (); - - fn try_from(sync_mode: u32) -> Result { - match sync_mode { - 0 => Ok(SyncMode::None), - 1 => Ok(SyncMode::Relaxed), - 2 => Ok(SyncMode::Full), - _ => { - // Do not continue if the user cannot specify a valid sync mode - Err(()) - } - } - } -} diff --git a/vendor/krun-devices/src/virtio/block/test_utils.rs b/vendor/krun-devices/src/virtio/block/test_utils.rs deleted file mode 100644 index af2eb16df..000000000 --- a/vendor/krun-devices/src/virtio/block/test_utils.rs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -use std::os::unix::io::AsRawFd; - -use crate::virtio::{Block, CacheType, Queue}; -use polly::event_manager::{EventManager, Subscriber}; -use utils::epoll::{EpollEvent, EventSet}; -use utils::tempfile::TempFile; - -/// Create a default Block instance to be used in tests. -pub fn default_block() -> Block { - // Create backing file. - let f = TempFile::new().unwrap(); - f.as_file().set_len(0x1000).unwrap(); - - default_block_with_path(f.as_path().to_str().unwrap().to_string()) -} - -/// Create a default Block instance using file at the specified path to be used in tests. -pub fn default_block_with_path(path: String) -> Block { - let id = "test".to_string(); - // The default block device is read-write and non-root. - Block::new(id, None, CacheType::Unsafe, path, false, false).unwrap() -} - -pub fn invoke_handler_for_queue_event(b: &mut Block) { - // Trigger the queue event. - b.queue_evts[0].write(1).unwrap(); - // Handle event. - b.process( - &EpollEvent::new(EventSet::IN, b.queue_evts[0].as_raw_fd() as u64), - &mut EventManager::new().unwrap(), - ); - // Validate the queue operation finished successfully. - assert_eq!(b.interrupt_evt.read().unwrap(), 1); -} - -pub fn set_queue(blk: &mut Block, idx: usize, q: Queue) { - blk.queues[idx] = q; -} diff --git a/vendor/krun-devices/src/virtio/block/worker.rs b/vendor/krun-devices/src/virtio/block/worker.rs deleted file mode 100644 index ad91fc918..000000000 --- a/vendor/krun-devices/src/virtio/block/worker.rs +++ /dev/null @@ -1,308 +0,0 @@ -use crate::virtio::descriptor_utils::{Reader, Writer}; - -use super::super::DeviceQueue; -use super::device::{CacheType, DiskProperties}; - -use crate::virtio::InterruptTransport; -use std::io::{self, Write}; -use std::os::fd::AsRawFd; -use std::result; -use std::thread; -use utils::epoll::{ControlOperation, Epoll, EpollEvent, EventSet}; -use utils::eventfd::EventFd; -use virtio_bindings::virtio_blk::*; -use vm_memory::{ByteValued, GuestMemoryMmap}; - -#[allow(dead_code)] -#[derive(Debug)] -pub enum RequestError { - Discarding(io::Error), - DiscardingToZero(io::Error), - FlushingToDisk(io::Error), - InvalidDataLength, - ReadingFromDescriptor(io::Error), - WritingToDescriptor(io::Error), - WritingZeroes(io::Error), - UnknownRequest, -} - -/// The request header represents the mandatory fields of each block device request. -/// -/// A request header contains the following fields: -/// * request_type: an u32 value mapping to a read, write or flush operation. -/// * reserved: 32 bits are reserved for future extensions of the Virtio Spec. -/// * sector: an u64 value representing the offset where a read/write is to occur. -/// -/// The header simplifies reading the request from memory as all request follow -/// the same memory layout. -#[derive(Copy, Clone, Default)] -#[repr(C)] -pub struct RequestHeader { - request_type: u32, - _reserved: u32, - sector: u64, -} -// Safe because RequestHeader only contains plain data. -unsafe impl ByteValued for RequestHeader {} - -#[derive(Copy, Clone, Default)] -#[repr(C)] -pub struct DiscardWriteData { - sector: u64, - num_sectors: u32, - flags: u32, -} -// Safe because DiscardWriteData only contains plain data. -unsafe impl ByteValued for DiscardWriteData {} - -pub struct BlockWorker { - device_queue: DeviceQueue, - interrupt: InterruptTransport, - mem: GuestMemoryMmap, - disk: DiskProperties, - stop_fd: EventFd, -} - -impl BlockWorker { - pub fn new( - device_queue: DeviceQueue, - interrupt: InterruptTransport, - mem: GuestMemoryMmap, - disk: DiskProperties, - stop_fd: EventFd, - ) -> Self { - Self { - device_queue, - interrupt, - mem, - disk, - stop_fd, - } - } - - pub fn run(self) -> thread::JoinHandle<()> { - thread::Builder::new() - .name("block worker".into()) - .spawn(|| self.work()) - .unwrap() - } - - fn work(mut self) { - let virtq_ev_fd = self.device_queue.event.as_raw_fd(); - let stop_ev_fd = self.stop_fd.as_raw_fd(); - - let epoll = Epoll::new().unwrap(); - - let _ = epoll.ctl( - ControlOperation::Add, - virtq_ev_fd, - &EpollEvent::new(EventSet::IN, virtq_ev_fd as u64), - ); - - let _ = epoll.ctl( - ControlOperation::Add, - stop_ev_fd, - &EpollEvent::new(EventSet::IN, stop_ev_fd as u64), - ); - - loop { - let mut epoll_events = vec![EpollEvent::new(EventSet::empty(), 0); 32]; - match epoll.wait(epoll_events.len(), -1, epoll_events.as_mut_slice()) { - Ok(ev_cnt) => { - for event in &epoll_events[0..ev_cnt] { - let source = event.fd(); - let event_set = event.event_set(); - match event_set { - EventSet::IN if source == virtq_ev_fd => { - self.process_queue_event(); - } - EventSet::IN if source == stop_ev_fd => { - debug!("stopping worker thread"); - let _ = self.stop_fd.read(); - return; - } - _ => { - log::warn!( - "Received unknown event: {event_set:?} from fd: {source:?}" - ); - } - } - } - } - Err(e) => { - debug!("failed to consume muxer epoll event: {e}"); - } - } - } - } - - fn process_queue_event(&mut self) { - if let Err(e) = self.device_queue.event.read() { - error!("Failed to get queue event: {e:?}"); - } else { - self.process_virtio_queues(); - } - } - - /// Process device virtio queue(s). - fn process_virtio_queues(&mut self) { - let mem = self.mem.clone(); - loop { - self.device_queue.queue.disable_notification(&mem).unwrap(); - - self.process_queue(&mem); - - if !self.device_queue.queue.enable_notification(&mem).unwrap() { - break; - } - } - } - - fn process_queue(&mut self, mem: &GuestMemoryMmap) { - while let Some(head) = self.device_queue.queue.pop(mem) { - let mut reader = match Reader::new(mem, head.clone()) { - Ok(r) => r, - Err(e) => { - error!("invalid descriptor chain: {e:?}"); - continue; - } - }; - let mut writer = match Writer::new(mem, head.clone()) { - Ok(r) => r, - Err(e) => { - error!("invalid descriptor chain: {e:?}"); - continue; - } - }; - let request_header: RequestHeader = match reader.read_obj() { - Ok(h) => h, - Err(e) => { - error!("invalid request header: {e:?}"); - continue; - } - }; - - let (status, len): (u8, usize) = - match self.process_request(request_header, &mut reader, &mut writer) { - Ok(l) => (VIRTIO_BLK_S_OK.try_into().unwrap(), l), - Err(e) => { - error!("error processing request: {e:?}"); - (VIRTIO_BLK_S_IOERR.try_into().unwrap(), 0) - } - }; - - if let Err(e) = writer.write_obj(status) { - error!("Failed to write virtio block status: {e:?}") - } - - if let Err(e) = self - .device_queue - .queue - .add_used(mem, head.index, len as u32) - { - error!("failed to add used elements to the queue: {e:?}"); - } - - if self.device_queue.queue.needs_notification(mem).unwrap() { - if let Err(e) = self.interrupt.try_signal_used_queue() { - error!("error signalling queue: {e:?}"); - } - } - } - } - - fn process_request( - &mut self, - request_header: RequestHeader, - reader: &mut Reader, - writer: &mut Writer, - ) -> result::Result { - match request_header.request_type { - VIRTIO_BLK_T_IN => { - let data_len = writer.available_bytes() - 1; - if !data_len.is_multiple_of(512) { - Err(RequestError::InvalidDataLength) - } else { - writer - .write_from_at(&self.disk, data_len, request_header.sector * 512) - .map_err(RequestError::WritingToDescriptor) - } - } - VIRTIO_BLK_T_OUT => { - let data_len = reader.available_bytes(); - if !data_len.is_multiple_of(512) { - Err(RequestError::InvalidDataLength) - } else { - reader - .read_to_at(&self.disk, data_len, request_header.sector * 512) - .map_err(RequestError::ReadingFromDescriptor) - } - } - VIRTIO_BLK_T_FLUSH => match self.disk.cache_type() { - CacheType::Writeback => { - let diskfile = self.disk.file.lock().unwrap(); - diskfile.flush().map_err(RequestError::FlushingToDisk)?; - diskfile.sync().map_err(RequestError::FlushingToDisk)?; - Ok(0) - } - CacheType::Unsafe => Ok(0), - }, - VIRTIO_BLK_T_GET_ID => { - let data_len = writer.available_bytes(); - let disk_id = self.disk.image_id(); - if data_len < disk_id.len() { - Err(RequestError::InvalidDataLength) - } else { - writer - .write_all(disk_id) - .map_err(RequestError::WritingToDescriptor)?; - Ok(disk_id.len()) - } - } - VIRTIO_BLK_T_DISCARD => { - let discard_write_data: DiscardWriteData = reader - .read_obj() - .map_err(RequestError::ReadingFromDescriptor)?; - self.disk - .file - .lock() - .unwrap() - .discard_to_any( - discard_write_data.sector * 512, - discard_write_data.num_sectors as u64 * 512, - ) - .map_err(RequestError::Discarding)?; - Ok(0) - } - VIRTIO_BLK_T_WRITE_ZEROES => { - let discard_write_data: DiscardWriteData = reader - .read_obj() - .map_err(RequestError::ReadingFromDescriptor)?; - let unmap = (discard_write_data.flags & VIRTIO_BLK_WRITE_ZEROES_FLAG_UNMAP) != 0; - if unmap { - self.disk - .file - .lock() - .unwrap() - .discard_to_zero( - discard_write_data.sector * 512, - discard_write_data.num_sectors as u64 * 512, - ) - .map_err(RequestError::DiscardingToZero)?; - } else { - self.disk - .file - .lock() - .unwrap() - .write_zeroes( - discard_write_data.sector * 512, - discard_write_data.num_sectors as u64 * 512, - ) - .map_err(RequestError::WritingZeroes)?; - } - Ok(0) - } - _ => Err(RequestError::UnknownRequest), - } - } -} diff --git a/vendor/krun-devices/src/virtio/console/console_control.rs b/vendor/krun-devices/src/virtio/console/console_control.rs deleted file mode 100644 index 1716a0313..000000000 --- a/vendor/krun-devices/src/virtio/console/console_control.rs +++ /dev/null @@ -1,152 +0,0 @@ -use std::collections::VecDeque; -use std::ops::Deref; -use std::sync::{Arc, Mutex}; - -use utils::eventfd::EventFd; -use utils::eventfd::EFD_NONBLOCK; -use vm_memory::{ByteValued, GuestMemoryMmap}; - -use crate::virtio::console::defs::control_event::{ - VIRTIO_CONSOLE_CONSOLE_PORT, VIRTIO_CONSOLE_PORT_ADD, VIRTIO_CONSOLE_PORT_NAME, - VIRTIO_CONSOLE_PORT_OPEN, VIRTIO_CONSOLE_RESIZE, -}; - -#[derive(Copy, Clone, Debug, Default)] -#[repr(C, packed(4))] -pub struct VirtioConsoleControl { - /// Port number - pub id: u32, - /// The kind of control event - pub event: u16, - /// Extra information for the event - pub value: u16, -} - -// Safe because it only has data and has no implicit padding. -// But NOTE that this relies on CPU being little endian, to have correct semantics -unsafe impl ByteValued for VirtioConsoleControl {} - -#[derive(Copy, Clone, Debug, Default)] -#[repr(C, packed)] -pub struct VirtioConsoleResize { - // NOTE: the order of these fields in older Linux kernels and in the spec were swapped. - // Linux changed it in commit 5326ab737a47 to match the specs. - pub cols: u16, - pub rows: u16, -} - -// Safe because it only has data and has no implicit padding. -// but NOTE, that we rely on CPU being little endian, for the values to be correct -unsafe impl ByteValued for VirtioConsoleResize {} - -pub enum Payload { - ConsoleControl(VirtioConsoleControl), - Bytes(Vec), -} - -impl Deref for Payload { - type Target = [u8]; - - fn deref(&self) -> &Self::Target { - match self { - Payload::ConsoleControl(b) => b.as_slice(), - Payload::Bytes(b) => b.as_slice(), - } - } -} - -// Utility for sending commands into control rx queue -pub struct ConsoleControl { - queue: Mutex>, - queue_evt: EventFd, -} - -impl ConsoleControl { - pub fn new() -> Arc { - Arc::new(Self { - queue: Default::default(), - queue_evt: EventFd::new(EFD_NONBLOCK).unwrap(), - }) - } - - pub fn mark_console_port(&self, _mem: &GuestMemoryMmap, port_id: u32) { - self.push_msg(VirtioConsoleControl { - id: port_id, - event: VIRTIO_CONSOLE_CONSOLE_PORT, - value: 1, - }) - } - - pub fn console_resize(&self, port_id: u32, new_size: VirtioConsoleResize) { - let mut buf = Vec::new(); - buf.extend( - VirtioConsoleControl { - id: port_id, - event: VIRTIO_CONSOLE_RESIZE, - value: 0, - } - .as_slice(), - ); - buf.extend(new_size.as_slice()); - self.push_vec(buf) - } - - /// Adds another port with the specified port_id - pub fn port_add(&self, port_id: u32) { - self.push_msg(VirtioConsoleControl { - id: port_id, - event: VIRTIO_CONSOLE_PORT_ADD, - value: 0, - }) - } - - pub fn port_open(&self, port_id: u32, open: bool) { - self.push_msg(VirtioConsoleControl { - id: port_id, - event: VIRTIO_CONSOLE_PORT_OPEN, - value: open as u16, - }) - } - - pub fn port_name(&self, port_id: u32, name: &str) { - let mut buf: Vec = Vec::new(); - - buf.extend_from_slice( - VirtioConsoleControl { - id: port_id, - event: VIRTIO_CONSOLE_PORT_NAME, - value: 1, // Unspecified/unused in the spec, lets use the same value as QEMU. - } - .as_slice(), - ); - - // The spec says the name shouldn't be NUL terminated. - buf.extend(name.as_bytes()); - self.push_vec(buf) - } - - pub fn queue_pop(&self) -> Option { - let mut queue = self.queue.lock().expect("Poisoned lock"); - queue.pop_front() - } - - pub fn queue_evt(&self) -> &EventFd { - &self.queue_evt - } - - fn push_msg(&self, msg: VirtioConsoleControl) { - let mut queue = self.queue.lock().expect("Poisoned lock"); - queue.push_back(Payload::ConsoleControl(msg)); - if let Err(e) = self.queue_evt.write(1) { - log::trace!("ConsoleControl failed to write to notify {e}") - } - } - - fn push_vec(&self, buf: Vec) { - let mut queue = self.queue.lock().expect("Poisoned lock"); - queue.push_back(Payload::Bytes(buf)); - if let Err(e) = self.queue_evt.write(1) { - log::trace!("ConsoleControl failed to write to notify {e}") - } - } -} diff --git a/vendor/krun-devices/src/virtio/console/device.rs b/vendor/krun-devices/src/virtio/console/device.rs deleted file mode 100644 index b1022a38e..000000000 --- a/vendor/krun-devices/src/virtio/console/device.rs +++ /dev/null @@ -1,369 +0,0 @@ -use std::cmp; -use std::io::Write; -use std::iter::zip; -use std::mem::{size_of, size_of_val}; -use std::os::unix::io::{AsRawFd, RawFd}; -use std::sync::Arc; - -use utils::eventfd::EventFd; -use vm_memory::{ByteValued, Bytes, GuestMemoryMmap}; - -use super::super::{ - ActivateError, ActivateResult, DeviceQueue, DeviceState, QueueConfig, VirtioDevice, -}; -use super::{defs, defs::control_event, defs::uapi}; -use crate::virtio::console::console_control::{ - ConsoleControl, VirtioConsoleControl, VirtioConsoleResize, -}; -use crate::virtio::console::defs::QUEUE_SIZE; -use crate::virtio::console::port::Port; -use crate::virtio::console::port_queue_mapping::{ - num_queues, port_id_to_queue_idx, QueueDirection, -}; -use crate::virtio::{InterruptTransport, PortDescription, VmmExitObserver}; - -pub(crate) const CONTROL_RXQ_INDEX: usize = 2; -pub(crate) const CONTROL_TXQ_INDEX: usize = 3; - -pub(crate) const AVAIL_FEATURES: u64 = (1 << uapi::VIRTIO_CONSOLE_F_SIZE as u64) - | (1 << uapi::VIRTIO_CONSOLE_F_MULTIPORT as u64) - | (1 << uapi::VIRTIO_F_VERSION_1 as u64); - -#[derive(Copy, Clone, Debug, Default)] -#[repr(C, packed)] -pub struct VirtioConsoleConfig { - cols: u16, - rows: u16, - max_nr_ports: u32, - emerg_wr: u32, -} - -// Safe because it only has data and has no implicit padding. -unsafe impl ByteValued for VirtioConsoleConfig {} - -impl VirtioConsoleConfig { - pub fn new(cols: u16, rows: u16, max_nr_ports: u32) -> Self { - VirtioConsoleConfig { - cols, - rows, - max_nr_ports, - emerg_wr: 0u32, - } - } -} - -pub struct Console { - pub(crate) device_state: DeviceState, - pub(crate) control: Arc, - pub(crate) ports: Vec, - - queue_config: Vec, - // Queues are stored as Option so individual queues can be taken when ports start. - pub(crate) queues: Vec>, - // TODO: move the queue event handling to the correct threads! - pub(crate) queue_events: Vec>, - - pub(crate) avail_features: u64, - pub(crate) acked_features: u64, - - pub(crate) activate_evt: EventFd, - pub(crate) sigwinch_evt: EventFd, - - config: VirtioConsoleConfig, -} - -impl Console { - pub fn new(ports: Vec) -> super::Result { - assert!(!ports.is_empty(), "Expected at least 1 port"); - - let num_queues = num_queues(ports.len()); - let queue_config: Vec = (0..num_queues) - .map(|_| QueueConfig::new(QUEUE_SIZE)) - .collect(); - - let ports: Vec = zip(0u32.., ports) - .map(|(port_id, description)| Port::new(port_id, description)) - .collect(); - - let (cols, rows) = ports[0] - .terminal() - .map(|t| t.get_win_size()) - .unwrap_or((0, 0)); - let config = VirtioConsoleConfig::new(cols, rows, ports.len() as u32); - - Ok(Console { - control: ConsoleControl::new(), - ports, - queue_config, - queues: Vec::new(), - queue_events: Vec::new(), - avail_features: AVAIL_FEATURES, - acked_features: 0, - activate_evt: EventFd::new(utils::eventfd::EFD_NONBLOCK) - .map_err(super::ConsoleError::EventFd)?, - sigwinch_evt: EventFd::new(utils::eventfd::EFD_NONBLOCK) - .map_err(super::ConsoleError::EventFd)?, - device_state: DeviceState::Inactive, - config, - }) - } - - pub fn id(&self) -> &str { - defs::CONSOLE_DEV_ID - } - - pub fn get_sigwinch_fd(&self) -> RawFd { - self.sigwinch_evt.as_raw_fd() - } - - pub fn update_console_size(&mut self, port_id: u32, cols: u16, rows: u16) { - log::debug!("update_console_size {port_id}: {cols} {rows}"); - self.control - .console_resize(port_id, VirtioConsoleResize { rows, cols }); - } - - pub(crate) fn process_control_rx(&mut self) -> bool { - log::trace!("process_control_rx"); - let DeviceState::Activated(ref mem, _) = self.device_state else { - unreachable!() - }; - let mut raise_irq = false; - - let control_rx = self.queues[CONTROL_RXQ_INDEX] - .as_mut() - .expect("control rx queue should exist"); - - while let Some(head) = control_rx.queue.pop(mem) { - if let Some(buf) = self.control.queue_pop() { - match mem.write(&buf, head.addr) { - Ok(n) => { - if n != buf.len() { - log::error!("process_control_rx: partial write"); - } - raise_irq = true; - log::trace!("process_control_rx wrote {n}"); - if let Err(e) = control_rx.queue.add_used(mem, head.index, n as u32) { - error!("failed to add used elements to the queue: {e:?}"); - } - } - Err(e) => { - log::error!("process_control_rx failed to write: {e}"); - } - } - } else { - control_rx.queue.undo_pop(); - break; - } - } - raise_irq - } - - pub(crate) fn process_control_tx(&mut self) -> bool { - log::trace!("process_control_tx"); - let DeviceState::Activated(ref mem, ref interrupt) = self.device_state else { - unreachable!() - }; - - let control_tx = self.queues[CONTROL_TXQ_INDEX] - .as_mut() - .expect("control tx queue should exist"); - let mut raise_irq = false; - - let mut ports_to_start = Vec::new(); - - while let Some(head) = control_tx.queue.pop(mem) { - raise_irq = true; - - let cmd: VirtioConsoleControl = match mem.read_obj(head.addr) { - Ok(cmd) => cmd, - Err(e) => { - log::error!( - "Failed to read VirtioConsoleControl struct: {e:?}, struct len = {len}, head.len = {head_len}", - len = size_of::(), - head_len = head.len, - ); - continue; - } - }; - if let Err(e) = control_tx - .queue - .add_used(mem, head.index, size_of_val(&cmd) as u32) - { - error!("failed to add used elements to the queue: {e:?}"); - } - - log::trace!("VirtioConsoleControl cmd: {cmd:?}"); - match cmd.event { - control_event::VIRTIO_CONSOLE_DEVICE_READY => { - log::debug!( - "Device is ready: initialization {}", - if cmd.value == 1 { "ok" } else { "failed" } - ); - for port_id in 0..self.ports.len() { - self.control.port_add(port_id as u32); - } - } - control_event::VIRTIO_CONSOLE_PORT_READY => { - if cmd.value != 1 { - log::error!("Port initialization failed: {cmd:?}"); - continue; - } - - if let Some(term) = self.ports[cmd.id as usize].terminal() { - self.control.mark_console_port(mem, cmd.id); - self.control.port_open(cmd.id, true); - let (cols, rows) = term.get_win_size(); - self.control - .console_resize(cmd.id, VirtioConsoleResize { cols, rows }); - } else { - // We start with all ports open, this makes sense for now, - // because underlying file descriptors STDIN, STDOUT, STDERR are always open too - self.control.port_open(cmd.id, true) - } - - let name = self.ports[cmd.id as usize].name(); - log::trace!("Port ready {id}: {name}", id = cmd.id); - if !name.is_empty() { - self.control.port_name(cmd.id, name) - } - } - control_event::VIRTIO_CONSOLE_PORT_OPEN => { - let opened = match cmd.value { - 0 => false, - 1 => true, - _ => { - log::error!( - "Invalid value ({}) for VIRTIO_CONSOLE_PORT_OPEN on port {}", - cmd.value, - cmd.id - ); - continue; - } - }; - - if !opened { - log::debug!("Guest closed port {}", cmd.id); - continue; - } - - ports_to_start.push(cmd.id as usize); - } - _ => log::warn!("Unknown console control event {:x}", cmd.event), - } - } - - for port_id in ports_to_start { - log::trace!("Starting port io for port {port_id}"); - let rx_idx = port_id_to_queue_idx(QueueDirection::Rx, port_id); - let tx_idx = port_id_to_queue_idx(QueueDirection::Tx, port_id); - - // Take ownership of port queues - they are moved to the port. - let rx_queue = self.queues[rx_idx] - .take() - .expect("port rx queue should exist") - .queue; - let tx_queue = self.queues[tx_idx] - .take() - .expect("port tx queue should exist") - .queue; - - self.ports[port_id].start( - mem.clone(), - rx_queue, - tx_queue, - interrupt.clone(), - self.control.clone(), - ); - } - - raise_irq - } -} - -impl VirtioDevice for Console { - fn avail_features(&self) -> u64 { - self.avail_features - } - - fn acked_features(&self) -> u64 { - self.acked_features - } - - fn set_acked_features(&mut self, acked_features: u64) { - self.acked_features = acked_features - } - - fn device_type(&self) -> u32 { - uapi::VIRTIO_ID_CONSOLE - } - - fn device_name(&self) -> &str { - "console" - } - - fn queue_config(&self) -> &[QueueConfig] { - &self.queue_config - } - - fn read_config(&self, offset: u64, mut data: &mut [u8]) { - let config_slice = self.config.as_slice(); - let config_len = config_slice.len() as u64; - if offset >= config_len { - error!("Failed to read config space"); - return; - } - if let Some(end) = offset.checked_add(data.len() as u64) { - // This write can't fail, offset and end are checked against config_len. - data.write_all(&config_slice[offset as usize..cmp::min(end, config_len) as usize]) - .unwrap(); - } - } - - fn write_config(&mut self, offset: u64, data: &[u8]) { - warn!( - "console: guest driver attempted to write device config (offset={:x}, len={:x})", - offset, - data.len() - ); - } - - fn activate( - &mut self, - mem: GuestMemoryMmap, - interrupt: InterruptTransport, - queues: Vec, - ) -> ActivateResult { - if self.activate_evt.write(1).is_err() { - error!("Cannot write to activate_evt"); - return Err(ActivateError::BadActivate); - } - - self.queue_events = queues.iter().map(|dq| dq.event.clone()).collect(); - self.queues = queues.into_iter().map(Some).collect(); - self.device_state = DeviceState::Activated(mem, interrupt); - - Ok(()) - } - - fn is_activated(&self) -> bool { - self.device_state.is_activated() - } - - fn reset(&mut self) -> bool { - // Shutdown ports and clear queues. - for port in &mut self.ports { - port.shutdown(); - } - self.queues.clear(); - self.queue_events.clear(); - self.device_state = DeviceState::Inactive; - true - } -} - -impl VmmExitObserver for Console { - fn on_vmm_exit(&mut self) { - self.reset(); - log::trace!("Console on_vmm_exit finished"); - } -} diff --git a/vendor/krun-devices/src/virtio/console/event_handler.rs b/vendor/krun-devices/src/virtio/console/event_handler.rs deleted file mode 100644 index 1dd9712b9..000000000 --- a/vendor/krun-devices/src/virtio/console/event_handler.rs +++ /dev/null @@ -1,169 +0,0 @@ -use std::os::unix::io::AsRawFd; - -use polly::event_manager::{EventManager, Subscriber}; -use utils::epoll::{EpollEvent, EventSet}; - -use super::device::Console; -use crate::virtio::console::device::{CONTROL_RXQ_INDEX, CONTROL_TXQ_INDEX}; -use crate::virtio::console::port_queue_mapping::{queue_idx_to_port_id, QueueDirection}; -use crate::virtio::device::VirtioDevice; - -impl Console { - pub(crate) fn read_queue_event(&self, queue_index: usize, event: &EpollEvent) -> bool { - log::trace!("Event on queue {queue_index}: {:?}", event.event_set()); - - let event_set = event.event_set(); - if event_set != EventSet::IN { - warn!("Unexpected event from queue index {queue_index}: {event_set:?}"); - return false; - } - - if let Err(e) = self.queue_events[queue_index].read() { - error!("Failed to read event from queue index {queue_index}: {e:?}"); - return false; - } - - true - } - - fn notify_port_queue_event(&mut self, queue_index: usize) { - let (direction, port_id) = queue_idx_to_port_id(queue_index); - match direction { - QueueDirection::Rx => { - log::trace!("Notify rx (queue event)"); - self.ports[port_id].notify_rx() - } - QueueDirection::Tx => { - log::trace!("Notify tx (queue event)"); - self.ports[port_id].notify_tx() - } - } - } - - fn handle_activate_event(&self, event_manager: &mut EventManager) { - debug!("console: activate event"); - if let Err(e) = self.activate_evt.read() { - error!("Failed to consume console activate event: {e:?}"); - } - - // The subscriber must exist as we previously registered activate_evt via - // `interest_list()`. - let self_subscriber = event_manager - .subscriber(self.activate_evt.as_raw_fd()) - .unwrap(); - - for queue_index in 0..self.queues.len() { - event_manager - .register( - self.queue_events[queue_index].as_raw_fd(), - EpollEvent::new( - EventSet::IN, - self.queue_events[queue_index].as_raw_fd() as u64, - ), - self_subscriber.clone(), - ) - .unwrap_or_else(|e| { - error!( - "Failed to register queue index {queue_index} with event manager: {e:?}" - ); - }); - } - - event_manager - .unregister(self.activate_evt.as_raw_fd()) - .unwrap_or_else(|e| { - error!("Failed to unregister fs activate evt: {e:?}"); - }) - } - - fn handle_sigwinch_event(&mut self, event: &EpollEvent) { - debug!("console: SIGWINCH event"); - - let event_set = event.event_set(); - if event_set != EventSet::IN { - warn!("console: sigwinch unexpected event {event_set:?}"); - } - - if let Err(e) = self.sigwinch_evt.read() { - error!("Failed to read the sigwinch event: {e:?}"); - } - - for i in 0..self.ports.len() { - if let Some(term) = self.ports[i].terminal() { - let (cols, rows) = term.get_win_size(); - self.update_console_size(i as u32, cols, rows); - } - } - } - - fn read_control_queue_event(&mut self, event: &EpollEvent) { - let event_set = event.event_set(); - if event_set != EventSet::IN { - warn!("Unexpected event {event_set:?}"); - } - - if let Err(e) = self.control.queue_evt().read() { - error!("Failed to read the ConsoleControl event: {e:?}"); - } - } -} - -impl Subscriber for Console { - fn process(&mut self, event: &EpollEvent, event_manager: &mut EventManager) { - let source = event.fd(); - - let activate_evt = self.activate_evt.as_raw_fd(); - let sigwinch_evt = self.sigwinch_evt.as_raw_fd(); - - if self.is_activated() { - // interest_list() registers sigwinch_evt and control.queue_evt() with - // epoll at creation time, but queue_events is only populated later in - // activate(). If a spurious event arrives before activation, indexing - // into the empty queue_events would panic — so these must stay inside - // the is_activated() guard. - let control_rxq = self.queue_events[CONTROL_RXQ_INDEX].as_raw_fd(); - let control_txq = self.queue_events[CONTROL_TXQ_INDEX].as_raw_fd(); - let control_rxq_control = self.control.queue_evt().as_raw_fd(); - - let mut raise_irq = false; - - if source == control_txq { - raise_irq |= - self.read_queue_event(CONTROL_TXQ_INDEX, event) && self.process_control_tx() - } else if source == control_rxq_control { - self.read_control_queue_event(event); - raise_irq |= self.process_control_rx(); - } else if source == control_rxq { - raise_irq |= self.read_queue_event(CONTROL_RXQ_INDEX, event) - } - /* Guest signaled input/output on port */ - else if let Some(queue_index) = self - .queue_events - .iter() - .position(|fd| fd.as_raw_fd() == source) - { - raise_irq |= self.read_queue_event(queue_index, event); - self.notify_port_queue_event(queue_index); - } else if source == activate_evt { - self.handle_activate_event(event_manager); - } else if source == sigwinch_evt { - self.handle_sigwinch_event(event); - } else { - log::warn!("Unexpected console event received: {source:?}") - } - if raise_irq { - self.device_state.signal_used_queue(); - } - } else { - warn!("console: The device is not yet activated. Spurious event received: {source:?}"); - } - } - - fn interest_list(&self) -> Vec { - vec![ - EpollEvent::new(EventSet::IN, self.activate_evt.as_raw_fd() as u64), - EpollEvent::new(EventSet::IN, self.sigwinch_evt.as_raw_fd() as u64), - EpollEvent::new(EventSet::IN, self.control.queue_evt().as_raw_fd() as u64), - ] - } -} diff --git a/vendor/krun-devices/src/virtio/console/mod.rs b/vendor/krun-devices/src/virtio/console/mod.rs deleted file mode 100644 index 33e47994c..000000000 --- a/vendor/krun-devices/src/virtio/console/mod.rs +++ /dev/null @@ -1,49 +0,0 @@ -mod console_control; -mod device; -mod event_handler; -mod port; -pub mod port_io; -mod port_queue_mapping; -mod process_rx; -mod process_tx; - -pub use self::defs::uapi::VIRTIO_ID_CONSOLE as TYPE_CONSOLE; -pub use self::device::Console; -pub use self::port::PortDescription; - -mod defs { - pub const CONSOLE_DEV_ID: &str = "virtio_console"; - pub const QUEUE_SIZE: u16 = 32; - - pub mod uapi { - /// The device conforms to the virtio spec version 1.0. - pub const VIRTIO_CONSOLE_F_SIZE: u32 = 0; - pub const VIRTIO_CONSOLE_F_MULTIPORT: u32 = 1; - pub const VIRTIO_F_VERSION_1: u32 = 32; - pub const VIRTIO_ID_CONSOLE: u32 = 3; - } - - #[allow(dead_code)] - pub mod control_event { - pub const VIRTIO_CONSOLE_DEVICE_READY: u16 = 0; - // Also known as VIRTIO_CONSOLE_DEVICE_ADD in spec, but kernel uses this (more descriptive) name - pub const VIRTIO_CONSOLE_PORT_ADD: u16 = 1; - /// Also known as VIRTIO_CONSOLE_DEVICE_REMOVE in spec, but kernel uses this (more descriptive) name - pub const VIRTIO_CONSOLE_PORT_REMOVE: u16 = 2; - pub const VIRTIO_CONSOLE_PORT_READY: u16 = 3; - pub const VIRTIO_CONSOLE_CONSOLE_PORT: u16 = 4; - pub const VIRTIO_CONSOLE_RESIZE: u16 = 5; - pub const VIRTIO_CONSOLE_PORT_OPEN: u16 = 6; - pub const VIRTIO_CONSOLE_PORT_NAME: u16 = 7; - } -} - -#[derive(Debug)] -pub enum ConsoleError { - /// Failed to create event fd. - EventFd(std::io::Error), - /// Failed to create SIGWINCH pipe. - SigwinchPipe(std::io::Error), -} - -type Result = std::result::Result; diff --git a/vendor/krun-devices/src/virtio/console/port.rs b/vendor/krun-devices/src/virtio/console/port.rs deleted file mode 100644 index caae5bc7f..000000000 --- a/vendor/krun-devices/src/virtio/console/port.rs +++ /dev/null @@ -1,202 +0,0 @@ -use std::borrow::Cow; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; -use std::thread::JoinHandle; -use std::{mem, thread}; - -use vm_memory::GuestMemoryMmap; - -use crate::virtio::console::console_control::ConsoleControl; -use crate::virtio::console::port_io::{PortInput, PortOutput}; -use crate::virtio::console::process_rx::process_rx; -use crate::virtio::console::process_tx::process_tx; -use crate::virtio::port_io::PortTerminalProperties; -use crate::virtio::{InterruptTransport, Queue}; - -pub struct PortDescription { - pub name: Cow<'static, str>, - pub input: Option>, - pub output: Option>, - pub terminal: Option>, -} - -impl PortDescription { - pub fn console( - input: Option>, - output: Option>, - terminal: Box, - ) -> Self { - Self { - name: "".into(), - input, - output, - terminal: Some(terminal), - } - } - - pub fn output_pipe( - name: impl Into>, - output: Box, - ) -> Self { - Self { - name: name.into(), - input: None, - output: Some(output), - terminal: None, - } - } - - pub fn input_pipe( - name: impl Into>, - input: Box, - ) -> Self { - Self { - name: name.into(), - input: Some(input), - output: None, - terminal: None, - } - } -} - -enum PortState { - Inactive, - Active { - stopfd: utils::eventfd::EventFd, - stop: Arc, - rx_thread: Option>, - tx_thread: Option>, - }, -} - -pub(crate) struct Port { - port_id: u32, - /// Empty if no name given - name: Cow<'static, str>, - state: PortState, - input: Option>>>, - output: Option>>>, - terminal: Option>, -} - -impl Port { - pub(crate) fn new(port_id: u32, description: PortDescription) -> Self { - Self { - port_id, - name: description.name, - state: PortState::Inactive, - input: description.input.map(|input| Arc::new(Mutex::new(input))), - output: description - .output - .map(|output| Arc::new(Mutex::new(output))), - terminal: description.terminal, - } - } - - pub fn name(&self) -> &str { - &self.name - } - - pub fn terminal(&self) -> Option<&dyn PortTerminalProperties> { - self.terminal.as_deref() - } - - pub fn notify_rx(&self) { - if let PortState::Active { - rx_thread: Some(handle), - .. - } = &self.state - { - handle.thread().unpark() - } - } - - pub fn notify_tx(&self) { - if let PortState::Active { - tx_thread: Some(handle), - .. - } = &self.state - { - handle.thread().unpark() - } - } - - pub fn start( - &mut self, - mem: GuestMemoryMmap, - rx_queue: Queue, - tx_queue: Queue, - interrupt: InterruptTransport, - control: Arc, - ) { - if let PortState::Active { .. } = &mut self.state { - self.shutdown(); - }; - - let input = self.input.as_ref().cloned(); - let output = self.output.as_ref().cloned(); - - let stopfd = utils::eventfd::EventFd::new(utils::eventfd::EFD_NONBLOCK) - .expect("Failed to create EventFd for interrupt_evt"); - let stop = Arc::new(AtomicBool::new(false)); - - let rx_thread = input.map(|input| { - let mem = mem.clone(); - let interrupt = interrupt.clone(); - let port_id = self.port_id; - let stopfd = stopfd.try_clone().unwrap(); - let stop = stop.clone(); - thread::Builder::new() - .name("console port".into()) - .spawn(move || { - process_rx( - mem, rx_queue, interrupt, input, control, port_id, stopfd, stop, - ) - }) - .unwrap() - }); - - let tx_thread = output.map(|output| { - let stop = stop.clone(); - thread::spawn(move || process_tx(mem, tx_queue, interrupt, output, stop)) - }); - - self.state = PortState::Active { - stopfd, - stop, - rx_thread, - tx_thread, - } - } - - pub fn shutdown(&mut self) { - if let PortState::Active { - stopfd, - stop, - tx_thread, - rx_thread, - } = &mut self.state - { - stop.store(true, Ordering::Release); - if let Some(tx_thread) = mem::take(tx_thread) { - tx_thread.thread().unpark(); - if let Err(e) = tx_thread.join() { - log::error!( - "Failed to flush tx for port {port_id}, thread panicked: {e:?}", - port_id = self.port_id - ) - } - } - stopfd.write(1).unwrap(); - if let Some(rx_thread) = mem::take(rx_thread) { - rx_thread.thread().unpark(); - if let Err(e) = rx_thread.join() { - log::error!( - "Failed to flush tx for port {port_id}, thread panicked: {e:?}", - port_id = self.port_id - ) - } - } - }; - } -} diff --git a/vendor/krun-devices/src/virtio/console/port_io.rs b/vendor/krun-devices/src/virtio/console/port_io.rs deleted file mode 100644 index be515ab81..000000000 --- a/vendor/krun-devices/src/virtio/console/port_io.rs +++ /dev/null @@ -1,334 +0,0 @@ -use libc::{ - fcntl, F_GETFL, F_SETFL, O_NONBLOCK, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO, TIOCGWINSZ, -}; -use log::Level; -use nix::errno::Errno; -use nix::ioctl_read_bad; -use nix::poll::{poll, PollFd, PollFlags, PollTimeout}; -use nix::unistd::{dup, isatty}; -use std::fs::File; -use std::io::{self, ErrorKind}; -use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd, RawFd}; -use utils::eventfd::EventFd; -use utils::eventfd::EFD_NONBLOCK; -use vm_memory::bitmap::Bitmap; -use vm_memory::{VolatileMemoryError, VolatileSlice, WriteVolatile}; - -pub trait PortInput { - fn read_volatile(&mut self, buf: &mut VolatileSlice) -> Result; - - fn wait_until_readable(&self, stopfd: Option<&EventFd>); -} - -pub trait PortOutput { - fn write_volatile(&mut self, buf: &VolatileSlice) -> Result; - - fn wait_until_writable(&self); -} - -/// Terminal properties associated with this port -pub trait PortTerminalProperties: Send + Sync { - fn get_win_size(&self) -> (u16, u16); -} - -pub fn stdin() -> Result, nix::Error> { - let fd = dup_raw_fd_into_owned(STDIN_FILENO)?; - make_non_blocking(&fd)?; - Ok(Box::new(PortInputFd(fd))) -} - -pub fn input_to_raw_fd_dup(fd: RawFd) -> Result, nix::Error> { - let fd = dup_raw_fd_into_owned(fd)?; - make_non_blocking(&fd)?; - Ok(Box::new(PortInputFd(fd))) -} - -pub fn stdout() -> Result, nix::Error> { - output_to_raw_fd_dup(STDOUT_FILENO) -} - -pub fn stderr() -> Result, nix::Error> { - output_to_raw_fd_dup(STDERR_FILENO) -} - -pub fn term_fd( - term_fd: RawFd, -) -> Result, nix::Error> { - let fd = dup_raw_fd_into_owned(term_fd)?; - assert!( - isatty(&fd).is_ok_and(|v| v), - "Expected fd {fd:?}, to be a tty, to query the window size!" - ); - Ok(Box::new(PortTerminalPropertiesFd(fd))) -} - -pub fn term_fixed_size(width: u16, height: u16) -> Box { - Box::new(PortTerminalPropertiesFixed((width, height))) -} - -pub fn input_empty() -> Result, nix::Error> { - Ok(Box::new(PortInputEmpty {})) -} - -pub fn output_file(file: File) -> Result, nix::Error> { - output_to_raw_fd_dup(file.as_raw_fd()) -} - -pub fn output_to_raw_fd_dup(fd: RawFd) -> Result, nix::Error> { - let fd = dup_raw_fd_into_owned(fd)?; - make_non_blocking(&fd)?; - Ok(Box::new(PortOutputFd(fd))) -} - -pub fn output_to_log_as_err() -> Box { - Box::new(PortOutputLog::new()) -} - -struct PortInputFd(OwnedFd); - -impl AsRawFd for PortInputFd { - fn as_raw_fd(&self) -> RawFd { - self.0.as_raw_fd() - } -} - -impl PortInput for PortInputFd { - fn read_volatile(&mut self, buf: &mut VolatileSlice) -> io::Result { - // This source code is copied from vm-memory, except it fixes an issue, where - // the original code would does not handle handle EWOULDBLOCK - - let fd = self.as_raw_fd(); - let guard = buf.ptr_guard_mut(); - - let dst = guard.as_ptr().cast::(); - - // SAFETY: We got a valid file descriptor from `AsRawFd`. The memory pointed to by `dst` is - // valid for writes of length `buf.len() by the invariants upheld by the constructor - // of `VolatileSlice`. - let bytes_read = unsafe { libc::read(fd, dst, buf.len()) }; - - if bytes_read < 0 { - let err = std::io::Error::last_os_error(); - if err.kind() != ErrorKind::WouldBlock { - // We don't know if a partial read might have happened, so mark everything as dirty - buf.bitmap().mark_dirty(0, buf.len()); - } - - Err(err) - } else { - let bytes_read = bytes_read.try_into().unwrap(); - buf.bitmap().mark_dirty(0, bytes_read); - Ok(bytes_read) - } - } - - fn wait_until_readable(&self, stopfd: Option<&EventFd>) { - let mut poll_fds = Vec::new(); - poll_fds.push(PollFd::new(self.0.as_fd(), PollFlags::POLLIN)); - if let Some(stopfd) = stopfd { - // SAFETY: we trust stopfd won't go away to avoid a dup call here. - let borrowed_fd = unsafe { BorrowedFd::borrow_raw(stopfd.as_raw_fd()) }; - poll_fds.push(PollFd::new(borrowed_fd, PollFlags::POLLIN)); - } - poll(&mut poll_fds, PollTimeout::NONE).expect("Failed to poll"); - } -} - -struct PortOutputFd(OwnedFd); - -impl AsRawFd for PortOutputFd { - fn as_raw_fd(&self) -> RawFd { - self.0.as_raw_fd() - } -} - -impl PortOutput for PortOutputFd { - fn write_volatile(&mut self, buf: &VolatileSlice) -> Result { - self.0.write_volatile(buf).map_err(|e| match e { - VolatileMemoryError::IOError(e) => e, - e => { - log::error!("Unsuported error from write_volatile: {e:?}"); - io::Error::other(e) - } - }) - } - - fn wait_until_writable(&self) { - let mut poll_fds = [PollFd::new(self.0.as_fd(), PollFlags::POLLOUT)]; - poll(&mut poll_fds, PollTimeout::NONE).expect("Failed to poll"); - } -} - -fn dup_raw_fd_into_owned(raw_fd: RawFd) -> Result { - // SAFETY: if raw_fd is invalid the `dup` call below will fail - let borrowed_fd = unsafe { BorrowedFd::borrow_raw(raw_fd) }; - let fd = dup(borrowed_fd)?; - Ok(fd) -} - -fn make_non_blocking(as_rw_fd: &impl AsRawFd) -> Result<(), nix::Error> { - let fd = as_rw_fd.as_raw_fd(); - unsafe { - let flags = fcntl(fd, F_GETFL, 0); - if flags < 0 { - return Err(Errno::last()); - } - - if fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0 { - return Err(Errno::last()); - } - } - Ok(()) -} - -// Utility to relay log from the VM (the kernel boot log and messages from init) -// to the rust log -#[derive(Default)] -pub struct PortOutputLog { - buf: Vec, -} - -impl PortOutputLog { - const FORCE_FLUSH_TRESHOLD: usize = 512; - const LOG_TARGET: &'static str = "init_or_kernel"; - - fn new() -> Self { - Self::default() - } - - fn force_flush(&mut self) { - log::log!(target: PortOutputLog::LOG_TARGET, Level::Error, "[missing newline]{}", String::from_utf8_lossy(&self.buf)); - self.buf.clear(); - } -} - -impl PortOutput for PortOutputLog { - fn write_volatile(&mut self, buf: &VolatileSlice) -> Result { - self.buf.write_volatile(buf).map_err(io::Error::other)?; - - let mut start = 0; - for (i, ch) in self.buf.iter().cloned().enumerate() { - if ch == b'\n' { - log::log!(target: PortOutputLog::LOG_TARGET, Level::Error, "{}", String::from_utf8_lossy(&self.buf[start..i])); - start = i + 1; - } - } - self.buf.drain(0..start); - // Make sure to not grow the internal buffer forever! - if self.buf.len() > PortOutputLog::FORCE_FLUSH_TRESHOLD { - self.force_flush() - } - Ok(buf.len()) - } - - fn wait_until_writable(&self) {} -} - -pub struct PortInputSigInt { - sigint_evt: EventFd, -} - -impl PortInputSigInt { - pub fn new() -> Self { - PortInputSigInt { - sigint_evt: EventFd::new(EFD_NONBLOCK) - .expect("Failed to create EventFd for SIGINT signaling"), - } - } - - pub fn sigint_evt(&self) -> &EventFd { - &self.sigint_evt - } -} - -impl Default for PortInputSigInt { - fn default() -> Self { - Self::new() - } -} - -impl PortInput for PortInputSigInt { - fn read_volatile(&mut self, buf: &mut VolatileSlice) -> Result { - self.sigint_evt.read()?; - log::trace!("SIGINT received"); - buf.copy_from(&[3u8]); //ASCII 'ETX' -> generates SIGINIT in a terminal - Ok(1) - } - - fn wait_until_readable(&self, stopfd: Option<&EventFd>) { - let mut poll_fds = Vec::with_capacity(2); - // SAFETY: we trust sigint_evt won't go away to avoid a dup call here. - let sigint_bfd = unsafe { BorrowedFd::borrow_raw(self.sigint_evt.as_raw_fd()) }; - poll_fds.push(PollFd::new(sigint_bfd, PollFlags::POLLIN)); - if let Some(stopfd) = stopfd { - // SAFETY: we trust stopfd won't go away to avoid a dup call here. - let stop_bfd = unsafe { BorrowedFd::borrow_raw(stopfd.as_raw_fd()) }; - poll_fds.push(PollFd::new(stop_bfd, PollFlags::POLLIN)); - } - - poll(&mut poll_fds, PollTimeout::NONE).expect("Failed to poll"); - } -} - -pub struct PortInputEmpty {} - -impl PortInputEmpty { - pub fn new() -> Self { - PortInputEmpty {} - } -} - -impl Default for PortInputEmpty { - fn default() -> Self { - Self::new() - } -} - -impl PortInput for PortInputEmpty { - fn read_volatile(&mut self, _buf: &mut VolatileSlice) -> Result { - Ok(0) - } - - fn wait_until_readable(&self, stopfd: Option<&EventFd>) { - if let Some(stopfd) = stopfd { - // SAFETY: we trust stopfd won't go away to avoid a dup call here. - let borrowed_fd = unsafe { BorrowedFd::borrow_raw(stopfd.as_raw_fd()) }; - let mut poll_fds = [PollFd::new(borrowed_fd, PollFlags::POLLIN)]; - poll(&mut poll_fds, PollTimeout::NONE).expect("Failed to poll"); - } else { - std::thread::sleep(std::time::Duration::MAX); - } - } -} - -struct PortTerminalPropertiesFixed((u16, u16)); - -impl PortTerminalProperties for PortTerminalPropertiesFixed { - fn get_win_size(&self) -> (u16, u16) { - self.0 - } -} - -struct PortTerminalPropertiesFd(OwnedFd); - -impl PortTerminalProperties for PortTerminalPropertiesFd { - fn get_win_size(&self) -> (u16, u16) { - let mut ws: WS = WS::default(); - - if let Err(err) = unsafe { tiocgwinsz(self.0.as_raw_fd(), &mut ws) } { - error!("Couldn't get terminal dimensions: {err}"); - return (0, 0); - } - (ws.cols, ws.rows) - } -} - -#[repr(C)] -#[derive(Default)] -struct WS { - rows: u16, - cols: u16, - xpixel: u16, - ypixel: u16, -} -ioctl_read_bad!(tiocgwinsz, TIOCGWINSZ, WS); diff --git a/vendor/krun-devices/src/virtio/console/port_queue_mapping.rs b/vendor/krun-devices/src/virtio/console/port_queue_mapping.rs deleted file mode 100644 index 49462454e..000000000 --- a/vendor/krun-devices/src/virtio/console/port_queue_mapping.rs +++ /dev/null @@ -1,74 +0,0 @@ -#[derive(Debug, Eq, PartialEq)] -pub(crate) enum QueueDirection { - Rx, - Tx, -} - -#[must_use] -pub(crate) fn port_id_to_queue_idx(queue_direction: QueueDirection, port_id: usize) -> usize { - match queue_direction { - QueueDirection::Rx if port_id == 0 => 0, - QueueDirection::Rx => 2 + 2 * port_id, - QueueDirection::Tx if port_id == 0 => 1, - QueueDirection::Tx => 2 + 2 * port_id + 1, - } -} - -#[must_use] -pub(crate) fn queue_idx_to_port_id(queue_index: usize) -> (QueueDirection, usize) { - let port_id = match queue_index { - 0 | 1 => 0, - 2 | 3 => { - panic!("Invalid argument: {queue_index} is not a valid receiveq nor transmitq index!") - } - _ => queue_index / 2 - 1, - }; - - let direction = if queue_index.is_multiple_of(2) { - QueueDirection::Rx - } else { - QueueDirection::Tx - }; - - (direction, port_id) -} - -pub(crate) fn num_queues(num_ports: usize) -> usize { - // 2 control queues and then an rx and tx queue for each port - 2 + 2 * num_ports -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_port_id_to_queue_idx() { - assert_eq!(port_id_to_queue_idx(QueueDirection::Rx, 0), 0); - assert_eq!(port_id_to_queue_idx(QueueDirection::Tx, 0), 1); - assert_eq!(port_id_to_queue_idx(QueueDirection::Rx, 1), 4); - assert_eq!(port_id_to_queue_idx(QueueDirection::Tx, 1), 5); - } - - #[test] - fn test_queue_idx_to_port_id_ok() { - assert_eq!(queue_idx_to_port_id(0), (QueueDirection::Rx, 0)); - assert_eq!(queue_idx_to_port_id(1), (QueueDirection::Tx, 0)); - assert_eq!(queue_idx_to_port_id(4), (QueueDirection::Rx, 1)); - assert_eq!(queue_idx_to_port_id(5), (QueueDirection::Tx, 1)); - assert_eq!(queue_idx_to_port_id(6), (QueueDirection::Rx, 2)); - assert_eq!(queue_idx_to_port_id(7), (QueueDirection::Tx, 2)); - } - - #[test] - #[should_panic] - fn test_queue_idx_to_port_id_panic_rx_control() { - let _ = queue_idx_to_port_id(2); - } - - #[test] - #[should_panic] - fn test_queue_idx_to_port_id_panic_tx_control() { - let _ = queue_idx_to_port_id(3); - } -} diff --git a/vendor/krun-devices/src/virtio/console/process_rx.rs b/vendor/krun-devices/src/virtio/console/process_rx.rs deleted file mode 100644 index dd0498c0f..000000000 --- a/vendor/krun-devices/src/virtio/console/process_rx.rs +++ /dev/null @@ -1,116 +0,0 @@ -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; -use std::{io, thread}; - -use vm_memory::{GuestMemory, GuestMemoryError, GuestMemoryMmap, GuestMemoryRegion}; - -use crate::virtio::console::console_control::ConsoleControl; -use crate::virtio::console::port_io::PortInput; -use crate::virtio::{DescriptorChain, InterruptTransport, Queue}; - -#[allow(clippy::too_many_arguments)] -pub(crate) fn process_rx( - mem: GuestMemoryMmap, - mut queue: Queue, - interrupt: InterruptTransport, - input: Arc>>, - control: Arc, - port_id: u32, - stopfd: utils::eventfd::EventFd, - stop: Arc, -) { - let mem = &mem; - let mut eof = false; - - let mut input = input.lock().unwrap(); - loop { - let Some(head) = pop_head_blocking(&mut queue, mem, &interrupt, &stop) else { - return; - }; - - let head_index = head.index; - let mut bytes_read = 0; - for chain in head.into_iter().writable() { - match read_to_desc(chain, input.as_mut(), &mut eof) { - Ok(0) => { - break; - } - Ok(len) => { - bytes_read += len; - } - Err(e) => { - log::error!("Failed to read: {e:?}") - } - } - } - - if bytes_read != 0 { - log::trace!("Rx {bytes_read} bytes queue len{}", queue.len(mem)); - if let Err(e) = queue.add_used(mem, head_index, bytes_read as u32) { - error!("failed to add used elements to the queue: {e:?}"); - } - } - - // We signal_used_queue only when we get WouldBlock or EOF - if eof { - interrupt.signal_used_queue(); - log::trace!("signaling EOF on port {port_id}"); - control.port_open(port_id, false); - return; - } else if bytes_read == 0 { - queue.undo_pop(); - interrupt.signal_used_queue(); - input.wait_until_readable(Some(&stopfd)); - } - - if stop.load(Ordering::Acquire) { - return; - } - } -} - -fn pop_head_blocking<'mem>( - queue: &mut Queue, - mem: &'mem GuestMemoryMmap, - interrupt: &InterruptTransport, - stop: &AtomicBool, -) -> Option> { - loop { - match queue.pop(mem) { - Some(descriptor) => break Some(descriptor), - None => { - interrupt.signal_used_queue(); - if stop.load(Ordering::Acquire) { - break None; - } - thread::park(); - log::trace!("rx unparked, queue len {}", queue.len(mem)) - } - } - } -} - -fn read_to_desc( - desc: DescriptorChain, - input: &mut (dyn PortInput + Send), - eof: &mut bool, -) -> Result { - // TODO: Switch to using `get_slices()` with the next vm-memory - // bump. - #[allow(deprecated)] - desc.mem - .try_access(desc.len as usize, desc.addr, |_, len, addr, region| { - let mut target = region.get_slice(addr, len).unwrap(); - match input.read_volatile(&mut target) { - Ok(n) => { - if n == 0 { - *eof = true - } - Ok(n) - } - // We can't return an error otherwise we would not know how many bytes were processed before WouldBlock - Err(e) if e.kind() == io::ErrorKind::WouldBlock => Ok(0), - Err(e) => Err(GuestMemoryError::IOError(e)), - } - }) -} diff --git a/vendor/krun-devices/src/virtio/console/process_tx.rs b/vendor/krun-devices/src/virtio/console/process_tx.rs deleted file mode 100644 index d1c7f691d..000000000 --- a/vendor/krun-devices/src/virtio/console/process_tx.rs +++ /dev/null @@ -1,107 +0,0 @@ -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; -use std::{io, thread}; - -use vm_memory::{GuestMemory, GuestMemoryError, GuestMemoryMmap, GuestMemoryRegion}; - -use crate::virtio::console::port_io::PortOutput; -use crate::virtio::{DescriptorChain, InterruptTransport, Queue}; - -pub(crate) fn process_tx( - mem: GuestMemoryMmap, - mut queue: Queue, - interrupt: InterruptTransport, - output: Arc>>, - stop: Arc, -) { - loop { - let Some(head) = pop_head_blocking(&mut queue, &mem, &interrupt, &stop) else { - return; - }; - - let head_index = head.index; - let mut bytes_written = 0; - - for desc in head.into_iter().readable() { - let desc_len = desc.len as usize; - match write_desc_to_output(desc, output.lock().unwrap().as_mut(), &interrupt) { - Ok(0) => { - break; - } - Ok(n) => { - assert_eq!(n, desc_len); - bytes_written += n; - } - Err(e) => { - log::error!("Failed to write output: {e}"); - if matches!(e, GuestMemoryError::IOError(e) if e.kind() == io::ErrorKind::BrokenPipe) - { - // Errors could conceivably be spurious. Broken - // pipe is not and there is no point in attempting - // to write more. - return; - } - } - } - } - - if bytes_written == 0 { - log::trace!("Tx Add used {bytes_written}"); - queue.undo_pop(); - } else { - log::trace!("Tx add used {bytes_written}"); - if let Err(e) = queue.add_used(&mem, head_index, bytes_written as u32) { - error!("failed to add used elements to the queue: {e:?}"); - } - } - } -} - -fn pop_head_blocking<'mem>( - queue: &mut Queue, - mem: &'mem GuestMemoryMmap, - interrupt: &InterruptTransport, - stop: &AtomicBool, -) -> Option> { - loop { - match queue.pop(mem) { - Some(descriptor) => break Some(descriptor), - None => { - interrupt.signal_used_queue(); - if stop.load(Ordering::Acquire) { - break None; - } - thread::park(); - log::trace!("tx unparked, queue len {}", queue.len(mem)) - } - } - } -} - -fn write_desc_to_output( - desc: DescriptorChain, - output: &mut (dyn PortOutput + Send), - interrupt: &InterruptTransport, -) -> Result { - // TODO: Switch to using `get_slices()` with the next vm-memory - // bump. - #[allow(deprecated)] - desc.mem - .try_access(desc.len as usize, desc.addr, |_, len, addr, region| { - let src = region.get_slice(addr, len).unwrap(); - loop { - log::trace!("Tx {src:?}, write_volatile {len} bytes"); - match output.write_volatile(&src) { - // try_access seem to handle partial write for us (we will be invoked again with an offset) - Ok(n) => break Ok(n), - // We can't return an error otherwise we would not know how many bytes were processed before WouldBlock - Err(e) if e.kind() == io::ErrorKind::WouldBlock => { - log::trace!("Tx wait for output (would block)"); - interrupt.signal_used_queue(); - output.wait_until_writable(); - } - Err(e) => break Err(GuestMemoryError::IOError(e)), - } - } - }) -} diff --git a/vendor/krun-devices/src/virtio/descriptor_utils.rs b/vendor/krun-devices/src/virtio/descriptor_utils.rs deleted file mode 100644 index 1a40642f9..000000000 --- a/vendor/krun-devices/src/virtio/descriptor_utils.rs +++ /dev/null @@ -1,984 +0,0 @@ -// Copyright 2019 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -use std::cmp; -use std::collections::VecDeque; -use std::fmt::{self, Display}; -use std::io::{self, Read, Write}; -use std::mem::{size_of, MaybeUninit}; -use std::ops::Deref; -use std::ptr::copy_nonoverlapping; -use std::result; - -use crate::virtio::queue::DescriptorChain; -use vm_memory::{ - Address, ByteValued, Bytes, GuestAddress, GuestMemory, GuestMemoryError, GuestMemoryMmap, - GuestMemoryRegion, Le16, Le32, Le64, VolatileMemory, VolatileMemoryError, VolatileSlice, -}; - -use super::file_traits::{FileReadWriteAtVolatile, FileReadWriteVolatile}; - -#[derive(Debug)] -pub enum Error { - DescriptorChainOverflow, - FindMemoryRegion, - GuestMemoryError(GuestMemoryError), - InvalidChain, - IoError(io::Error), - SplitOutOfBounds(usize), - VolatileMemoryError(VolatileMemoryError), -} - -impl Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use self::Error::*; - - match self { - DescriptorChainOverflow => write!( - f, - "the combined length of all the buffers in a `DescriptorChain` would overflow" - ), - FindMemoryRegion => write!(f, "no memory region for this address range"), - GuestMemoryError(e) => write!(f, "descriptor guest memory error: {e}"), - InvalidChain => write!(f, "invalid descriptor chain"), - IoError(e) => write!(f, "descriptor I/O error: {e}"), - SplitOutOfBounds(off) => write!(f, "`DescriptorChain` split is out of bounds: {off}"), - VolatileMemoryError(e) => write!(f, "volatile memory error: {e}"), - } - } -} - -pub type Result = result::Result; - -impl std::error::Error for Error {} - -#[derive(Clone)] -struct DescriptorChainConsumer<'a> { - buffers: VecDeque>, - bytes_consumed: usize, -} - -impl<'a> DescriptorChainConsumer<'a> { - fn available_bytes(&self) -> usize { - // This is guaranteed not to overflow because the total length of the chain - // is checked during all creations of `DescriptorChainConsumer` (see - // `Reader::new()` and `Writer::new()`). - self.buffers - .iter() - .fold(0usize, |count, vs| count + vs.len()) - } - - fn bytes_consumed(&self) -> usize { - self.bytes_consumed - } - - /// Consumes at most `count` bytes from the `DescriptorChain`. Callers must provide a function - /// that takes a `&[VolatileSlice]` and returns the total number of bytes consumed. This - /// function guarantees that the combined length of all the slices in the `&[VolatileSlice]` is - /// less than or equal to `count`. - /// - /// # Errors - /// - /// If the provided function returns any error then no bytes are consumed from the buffer and - /// the error is returned to the caller. - fn consume(&mut self, count: usize, f: F) -> io::Result - where - F: FnOnce(&[VolatileSlice]) -> io::Result, - { - let mut buflen = 0; - let mut bufs = Vec::with_capacity(self.buffers.len()); - for &vs in &self.buffers { - if buflen >= count { - break; - } - - bufs.push(vs); - - let rem = count - buflen; - if rem < vs.len() { - buflen += rem; - } else { - buflen += vs.len(); - } - } - - if bufs.is_empty() { - return Ok(0); - } - - let bytes_consumed = f(&bufs)?; - - // This can happen if a driver tricks a device into reading/writing more data than - // fits in a `usize`. - let total_bytes_consumed = - self.bytes_consumed - .checked_add(bytes_consumed) - .ok_or_else(|| { - io::Error::new(io::ErrorKind::InvalidData, Error::DescriptorChainOverflow) - })?; - - let mut rem = bytes_consumed; - while let Some(vs) = self.buffers.pop_front() { - if rem < vs.len() { - // Split the slice and push the remainder back into the buffer list. Safe because we - // know that `rem` is not out of bounds due to the check and we checked the bounds - // on `vs` when we added it to the buffer list. - self.buffers.push_front(vs.offset(rem).unwrap()); - break; - } - - // No need for checked math because we know that `vs.size() <= rem`. - rem -= vs.len(); - } - - self.bytes_consumed = total_bytes_consumed; - - Ok(bytes_consumed) - } - - fn split_at(&mut self, offset: usize) -> Result> { - let mut rem = offset; - let pos = self.buffers.iter().position(|vs| { - if rem < vs.len() { - true - } else { - rem -= vs.len(); - false - } - }); - - if let Some(at) = pos { - let mut other = self.buffers.split_off(at); - - if rem > 0 { - // There must be at least one element in `other` because we checked - // its `size` value in the call to `position` above. - let front = other.pop_front().expect("empty VecDeque after split"); - self.buffers - .push_back(front.offset(rem).map_err(Error::VolatileMemoryError)?); - other.push_front(front.offset(rem).map_err(Error::VolatileMemoryError)?); - } - - Ok(DescriptorChainConsumer { - buffers: other, - bytes_consumed: 0, - }) - } else if rem == 0 { - Ok(DescriptorChainConsumer { - buffers: VecDeque::new(), - bytes_consumed: 0, - }) - } else { - Err(Error::SplitOutOfBounds(offset)) - } - } -} - -/// Provides high-level interface over the sequence of memory regions -/// defined by readable descriptors in the descriptor chain. -/// -/// Note that virtio spec requires driver to place any device-writable -/// descriptors after any device-readable descriptors (2.6.4.2 in Virtio Spec v1.1). -/// Reader will skip iterating over descriptor chain when first writable -/// descriptor is encountered. -#[derive(Clone)] -pub struct Reader<'a> { - buffer: DescriptorChainConsumer<'a>, -} - -impl<'a> Reader<'a> { - /// Construct a new Reader wrapper over `desc_chain`. - pub fn new(mem: &'a GuestMemoryMmap, chain: DescriptorChain<'a>) -> Result> { - let mut total_len: usize = 0; - let buffers = chain - .into_iter() - .readable() - .map(|desc| { - // Verify that summing the descriptor sizes does not overflow. - // This can happen if a driver tricks a device into reading more data than - // fits in a `usize`. - total_len = total_len - .checked_add(desc.len as usize) - .ok_or(Error::DescriptorChainOverflow)?; - - let region = mem.find_region(desc.addr).ok_or(Error::FindMemoryRegion)?; - let offset = desc - .addr - .checked_sub(region.start_addr().raw_value()) - .unwrap(); - region - .deref() - .get_slice(offset.raw_value() as usize, desc.len as usize) - .map_err(Error::VolatileMemoryError) - }) - .collect::>>>()?; - Ok(Reader { - buffer: DescriptorChainConsumer { - buffers, - bytes_consumed: 0, - }, - }) - } - - /// Reads an object from the descriptor chain buffer. - pub fn read_obj(&mut self) -> io::Result { - let mut obj = MaybeUninit::::uninit(); - - // Safe because `MaybeUninit` guarantees that the pointer is valid for - // `size_of::()` bytes. - let buf = unsafe { - ::std::slice::from_raw_parts_mut(obj.as_mut_ptr() as *mut u8, size_of::()) - }; - - self.read_exact(buf)?; - - // Safe because any type that implements `ByteValued` can be considered initialized - // even if it is filled with random data. - Ok(unsafe { obj.assume_init() }) - } - - /// Reads data from the descriptor chain buffer into a file descriptor. - /// Returns the number of bytes read from the descriptor chain buffer. - /// The number of bytes read can be less than `count` if there isn't - /// enough data in the descriptor chain buffer. - pub fn read_to( - &mut self, - mut dst: F, - count: usize, - ) -> io::Result { - self.buffer - .consume(count, |bufs| dst.write_vectored_volatile(bufs)) - } - - /// Reads data from the descriptor chain buffer into a File at offset `off`. - /// Returns the number of bytes read from the descriptor chain buffer. - /// The number of bytes read can be less than `count` if there isn't - /// enough data in the descriptor chain buffer. - pub fn read_to_at( - &mut self, - dst: F, - count: usize, - off: u64, - ) -> io::Result { - self.buffer - .consume(count, |bufs| dst.write_vectored_at_volatile(bufs, off)) - } - - pub fn read_exact_to( - &mut self, - mut dst: F, - mut count: usize, - ) -> io::Result<()> { - while count > 0 { - match self.read_to(&mut dst, count) { - Ok(0) => { - return Err(io::Error::new( - io::ErrorKind::UnexpectedEof, - "failed to fill whole buffer", - )) - } - Ok(n) => count -= n, - Err(ref e) if e.kind() == io::ErrorKind::Interrupted => {} - Err(e) => return Err(e), - } - } - - Ok(()) - } - - /// Returns number of bytes available for reading. May return an error if the combined - /// lengths of all the buffers in the DescriptorChain would cause an integer overflow. - pub fn available_bytes(&self) -> usize { - self.buffer.available_bytes() - } - - /// Returns number of bytes already read from the descriptor chain buffer. - pub fn bytes_read(&self) -> usize { - self.buffer.bytes_consumed() - } - - /// Splits this `Reader` into two at the given offset in the `DescriptorChain` buffer. - /// After the split, `self` will be able to read up to `offset` bytes while the returned - /// `Reader` can read up to `available_bytes() - offset` bytes. Returns an error if - /// `offset > self.available_bytes()`. - pub fn split_at(&mut self, offset: usize) -> Result> { - self.buffer.split_at(offset).map(|buffer| Reader { buffer }) - } -} - -impl io::Read for Reader<'_> { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - self.buffer.consume(buf.len(), |bufs| { - let mut rem = buf; - let mut total = 0; - for vs in bufs { - let copy_len = cmp::min(rem.len(), vs.len()); - - // Safe because we have already verified that `vs` points to valid memory. - unsafe { - copy_nonoverlapping(vs.ptr_guard().as_ptr(), rem.as_mut_ptr(), copy_len); - } - rem = &mut rem[copy_len..]; - total += copy_len; - } - Ok(total) - }) - } -} - -/// Provides high-level interface over the sequence of memory regions -/// defined by writable descriptors in the descriptor chain. -/// -/// Note that virtio spec requires driver to place any device-writable -/// descriptors after any device-readable descriptors (2.6.4.2 in Virtio Spec v1.1). -/// Writer will start iterating the descriptors from the first writable one and will -/// assume that all following descriptors are writable. -#[derive(Clone)] -pub struct Writer<'a> { - buffer: DescriptorChainConsumer<'a>, -} - -impl<'a> Writer<'a> { - /// Construct a new Writer wrapper over `desc_chain`. - pub fn new(mem: &'a GuestMemoryMmap, chain: DescriptorChain<'a>) -> Result> { - let mut total_len: usize = 0; - let buffers = chain - .into_iter() - .writable() - .map(|desc| { - // Verify that summing the descriptor sizes does not overflow. - // This can happen if a driver tricks a device into writing more data than - // fits in a `usize`. - total_len = total_len - .checked_add(desc.len as usize) - .ok_or(Error::DescriptorChainOverflow)?; - - let region = mem.find_region(desc.addr).ok_or(Error::FindMemoryRegion)?; - let offset = desc - .addr - .checked_sub(region.start_addr().raw_value()) - .unwrap(); - region - .deref() - .get_slice(offset.raw_value() as usize, desc.len as usize) - .map_err(Error::VolatileMemoryError) - }) - .collect::>>>()?; - - Ok(Writer { - buffer: DescriptorChainConsumer { - buffers, - bytes_consumed: 0, - }, - }) - } - - /// Writes an object to the descriptor chain buffer. - pub fn write_obj(&mut self, val: T) -> io::Result<()> { - self.write_all(val.as_slice()) - } - - /// Returns number of bytes available for writing. May return an error if the combined - /// lengths of all the buffers in the DescriptorChain would cause an overflow. - pub fn available_bytes(&self) -> usize { - self.buffer.available_bytes() - } - - /// Writes data to the descriptor chain buffer from a file descriptor. - /// Returns the number of bytes written to the descriptor chain buffer. - /// The number of bytes written can be less than `count` if - /// there isn't enough data in the descriptor chain buffer. - pub fn write_from( - &mut self, - mut src: F, - count: usize, - ) -> io::Result { - self.buffer - .consume(count, |bufs| src.read_vectored_volatile(bufs)) - } - - /// Writes data to the descriptor chain buffer from a File at offset `off`. - /// Returns the number of bytes written to the descriptor chain buffer. - /// The number of bytes written can be less than `count` if - /// there isn't enough data in the descriptor chain buffer. - pub fn write_from_at( - &mut self, - src: F, - count: usize, - off: u64, - ) -> io::Result { - self.buffer - .consume(count, |bufs| src.read_vectored_at_volatile(bufs, off)) - } - - pub fn write_all_from( - &mut self, - mut src: F, - mut count: usize, - ) -> io::Result<()> { - while count > 0 { - match self.write_from(&mut src, count) { - Ok(0) => { - return Err(io::Error::new( - io::ErrorKind::WriteZero, - "failed to write whole buffer", - )) - } - Ok(n) => count -= n, - Err(ref e) if e.kind() == io::ErrorKind::Interrupted => {} - Err(e) => return Err(e), - } - } - - Ok(()) - } - - /// Returns number of bytes already written to the descriptor chain buffer. - pub fn bytes_written(&self) -> usize { - self.buffer.bytes_consumed() - } - - /// Splits this `Writer` into two at the given offset in the `DescriptorChain` buffer. - /// After the split, `self` will be able to write up to `offset` bytes while the returned - /// `Writer` can write up to `available_bytes() - offset` bytes. Returns an error if - /// `offset > self.available_bytes()`. - pub fn split_at(&mut self, offset: usize) -> Result> { - self.buffer.split_at(offset).map(|buffer| Writer { buffer }) - } -} - -impl io::Write for Writer<'_> { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.buffer.consume(buf.len(), |bufs| { - let mut rem = buf; - let mut total = 0; - for vs in bufs { - let copy_len = cmp::min(rem.len(), vs.len()); - - // Safe because we have already verified that `vs` points to valid memory. - unsafe { - copy_nonoverlapping(rem.as_ptr(), vs.ptr_guard_mut().as_ptr(), copy_len); - } - rem = &rem[copy_len..]; - total += copy_len; - } - Ok(total) - }) - } - - fn flush(&mut self) -> io::Result<()> { - // Nothing to flush since the writes go straight into the buffer. - Ok(()) - } -} - -const VIRTQ_DESC_F_NEXT: u16 = 0x1; -const VIRTQ_DESC_F_WRITE: u16 = 0x2; - -#[derive(Copy, Clone, PartialEq, Eq)] -pub enum DescriptorType { - Readable, - Writable, -} - -#[derive(Copy, Clone, Debug, Default)] -#[repr(C)] -struct virtq_desc { - addr: Le64, - len: Le32, - flags: Le16, - next: Le16, -} - -// Safe because it only has data and has no implicit padding. -unsafe impl ByteValued for virtq_desc {} - -/// Test utility function to create a descriptor chain in guest memory. -pub fn create_descriptor_chain( - memory: &GuestMemoryMmap, - descriptor_array_addr: GuestAddress, - mut buffers_start_addr: GuestAddress, - descriptors: Vec<(DescriptorType, u32)>, - spaces_between_regions: u32, -) -> Result> { - let descriptors_len = descriptors.len(); - for (index, (type_, size)) in descriptors.into_iter().enumerate() { - let mut flags = 0; - if let DescriptorType::Writable = type_ { - flags |= VIRTQ_DESC_F_WRITE; - } - if index + 1 < descriptors_len { - flags |= VIRTQ_DESC_F_NEXT; - } - - let index = index as u16; - let desc = virtq_desc { - addr: buffers_start_addr.raw_value().into(), - len: size.into(), - flags: flags.into(), - next: (index + 1).into(), - }; - - let offset = size + spaces_between_regions; - buffers_start_addr = buffers_start_addr - .checked_add(u64::from(offset)) - .ok_or(Error::InvalidChain)?; - - let _ = memory.write_obj( - desc, - descriptor_array_addr - .checked_add(u64::from(index) * std::mem::size_of::() as u64) - .ok_or(Error::InvalidChain)?, - ); - } - - DescriptorChain::checked_new(memory, descriptor_array_addr, 0x100, 0).ok_or(Error::InvalidChain) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn reader_test_simple_chain() { - use DescriptorType::*; - - let memory_start_addr = GuestAddress(0x0); - let memory = GuestMemoryMmap::from_ranges(&[(memory_start_addr, 0x10000)]).unwrap(); - - let chain = create_descriptor_chain( - &memory, - GuestAddress(0x0), - GuestAddress(0x100), - vec![ - (Readable, 8), - (Readable, 16), - (Readable, 18), - (Readable, 64), - ], - 0, - ) - .expect("create_descriptor_chain failed"); - let mut reader = Reader::new(&memory, chain).expect("failed to create Reader"); - assert_eq!(reader.available_bytes(), 106); - assert_eq!(reader.bytes_read(), 0); - - let mut buffer = [0_u8; 64]; - if let Err(_) = reader.read_exact(&mut buffer) { - panic!("read_exact should not fail here"); - } - - assert_eq!(reader.available_bytes(), 42); - assert_eq!(reader.bytes_read(), 64); - - match reader.read(&mut buffer) { - Err(_) => panic!("read should not fail here"), - Ok(length) => assert_eq!(length, 42), - } - - assert_eq!(reader.available_bytes(), 0); - assert_eq!(reader.bytes_read(), 106); - } - - #[test] - fn writer_test_simple_chain() { - use DescriptorType::*; - - let memory_start_addr = GuestAddress(0x0); - let memory = GuestMemoryMmap::from_ranges(&[(memory_start_addr, 0x10000)]).unwrap(); - - let chain = create_descriptor_chain( - &memory, - GuestAddress(0x0), - GuestAddress(0x100), - vec![ - (Writable, 8), - (Writable, 16), - (Writable, 18), - (Writable, 64), - ], - 0, - ) - .expect("create_descriptor_chain failed"); - let mut writer = Writer::new(&memory, chain).expect("failed to create Writer"); - assert_eq!(writer.available_bytes(), 106); - assert_eq!(writer.bytes_written(), 0); - - let mut buffer = [0_u8; 64]; - if let Err(_) = writer.write_all(&mut buffer) { - panic!("write_all should not fail here"); - } - - assert_eq!(writer.available_bytes(), 42); - assert_eq!(writer.bytes_written(), 64); - - match writer.write(&mut buffer) { - Err(_) => panic!("write should not fail here"), - Ok(length) => assert_eq!(length, 42), - } - - assert_eq!(writer.available_bytes(), 0); - assert_eq!(writer.bytes_written(), 106); - } - - #[test] - fn reader_test_incompatible_chain() { - use DescriptorType::*; - - let memory_start_addr = GuestAddress(0x0); - let memory = GuestMemoryMmap::from_ranges(&[(memory_start_addr, 0x10000)]).unwrap(); - - let chain = create_descriptor_chain( - &memory, - GuestAddress(0x0), - GuestAddress(0x100), - vec![(Writable, 8)], - 0, - ) - .expect("create_descriptor_chain failed"); - let mut reader = Reader::new(&memory, chain).expect("failed to create Reader"); - assert_eq!(reader.available_bytes(), 0); - assert_eq!(reader.bytes_read(), 0); - - assert!(reader.read_obj::().is_err()); - - assert_eq!(reader.available_bytes(), 0); - assert_eq!(reader.bytes_read(), 0); - } - - #[test] - fn writer_test_incompatible_chain() { - use DescriptorType::*; - - let memory_start_addr = GuestAddress(0x0); - let memory = GuestMemoryMmap::from_ranges(&[(memory_start_addr, 0x10000)]).unwrap(); - - let chain = create_descriptor_chain( - &memory, - GuestAddress(0x0), - GuestAddress(0x100), - vec![(Readable, 8)], - 0, - ) - .expect("create_descriptor_chain failed"); - let mut writer = Writer::new(&memory, chain).expect("failed to create Writer"); - assert_eq!(writer.available_bytes(), 0); - assert_eq!(writer.bytes_written(), 0); - - assert!(writer.write_obj(0u8).is_err()); - - assert_eq!(writer.available_bytes(), 0); - assert_eq!(writer.bytes_written(), 0); - } - - #[test] - fn reader_writer_shared_chain() { - use DescriptorType::*; - - let memory_start_addr = GuestAddress(0x0); - let memory = GuestMemoryMmap::from_ranges(&[(memory_start_addr, 0x10000)]).unwrap(); - - let chain = create_descriptor_chain( - &memory, - GuestAddress(0x0), - GuestAddress(0x100), - vec![ - (Readable, 16), - (Readable, 16), - (Readable, 96), - (Writable, 64), - (Writable, 1), - (Writable, 3), - ], - 0, - ) - .expect("create_descriptor_chain failed"); - let mut reader = Reader::new(&memory, chain.clone()).expect("failed to create Reader"); - let mut writer = Writer::new(&memory, chain).expect("failed to create Writer"); - - assert_eq!(reader.bytes_read(), 0); - assert_eq!(writer.bytes_written(), 0); - - let mut buffer = Vec::with_capacity(200); - - assert_eq!( - reader - .read_to_end(&mut buffer) - .expect("read should not fail here"), - 128 - ); - - // The writable descriptors are only 68 bytes long. - writer - .write_all(&buffer[..68]) - .expect("write should not fail here"); - - assert_eq!(reader.available_bytes(), 0); - assert_eq!(reader.bytes_read(), 128); - assert_eq!(writer.available_bytes(), 0); - assert_eq!(writer.bytes_written(), 68); - } - - #[test] - fn reader_writer_shattered_object() { - use DescriptorType::*; - - let memory_start_addr = GuestAddress(0x0); - let memory = GuestMemoryMmap::from_ranges(&[(memory_start_addr, 0x10000)]).unwrap(); - - let secret: Le32 = 0x12345678.into(); - - // Create a descriptor chain with memory regions that are properly separated. - let chain_writer = create_descriptor_chain( - &memory, - GuestAddress(0x0), - GuestAddress(0x100), - vec![(Writable, 1), (Writable, 1), (Writable, 1), (Writable, 1)], - 123, - ) - .expect("create_descriptor_chain failed"); - let mut writer = Writer::new(&memory, chain_writer).expect("failed to create Writer"); - if let Err(_) = writer.write_obj(secret) { - panic!("write_obj should not fail here"); - } - - // Now create new descriptor chain pointing to the same memory and try to read it. - let chain_reader = create_descriptor_chain( - &memory, - GuestAddress(0x0), - GuestAddress(0x100), - vec![(Readable, 1), (Readable, 1), (Readable, 1), (Readable, 1)], - 123, - ) - .expect("create_descriptor_chain failed"); - let mut reader = Reader::new(&memory, chain_reader).expect("failed to create Reader"); - match reader.read_obj::() { - Err(_) => panic!("read_obj should not fail here"), - Ok(read_secret) => assert_eq!(read_secret, secret), - } - } - - #[test] - fn reader_unexpected_eof() { - use DescriptorType::*; - - let memory_start_addr = GuestAddress(0x0); - let memory = GuestMemoryMmap::from_ranges(&[(memory_start_addr, 0x10000)]).unwrap(); - - let chain = create_descriptor_chain( - &memory, - GuestAddress(0x0), - GuestAddress(0x100), - vec![(Readable, 256), (Readable, 256)], - 0, - ) - .expect("create_descriptor_chain failed"); - - let mut reader = Reader::new(&memory, chain).expect("failed to create Reader"); - - let mut buf = Vec::with_capacity(1024); - buf.resize(1024, 0); - - assert_eq!( - reader - .read_exact(&mut buf[..]) - .expect_err("read more bytes than available") - .kind(), - io::ErrorKind::UnexpectedEof - ); - } - - #[test] - fn split_border() { - use DescriptorType::*; - - let memory_start_addr = GuestAddress(0x0); - let memory = GuestMemoryMmap::from_ranges(&[(memory_start_addr, 0x10000)]).unwrap(); - - let chain = create_descriptor_chain( - &memory, - GuestAddress(0x0), - GuestAddress(0x100), - vec![ - (Readable, 16), - (Readable, 16), - (Readable, 96), - (Writable, 64), - (Writable, 1), - (Writable, 3), - ], - 0, - ) - .expect("create_descriptor_chain failed"); - let mut reader = Reader::new(&memory, chain).expect("failed to create Reader"); - - let other = reader.split_at(32).expect("failed to split Reader"); - assert_eq!(reader.available_bytes(), 32); - assert_eq!(other.available_bytes(), 96); - } - - #[test] - fn split_middle() { - use DescriptorType::*; - - let memory_start_addr = GuestAddress(0x0); - let memory = GuestMemoryMmap::from_ranges(&[(memory_start_addr, 0x10000)]).unwrap(); - - let chain = create_descriptor_chain( - &memory, - GuestAddress(0x0), - GuestAddress(0x100), - vec![ - (Readable, 16), - (Readable, 16), - (Readable, 96), - (Writable, 64), - (Writable, 1), - (Writable, 3), - ], - 0, - ) - .expect("create_descriptor_chain failed"); - let mut reader = Reader::new(&memory, chain).expect("failed to create Reader"); - - let other = reader.split_at(24).expect("failed to split Reader"); - assert_eq!(reader.available_bytes(), 24); - assert_eq!(other.available_bytes(), 104); - } - - #[test] - fn split_end() { - use DescriptorType::*; - - let memory_start_addr = GuestAddress(0x0); - let memory = GuestMemoryMmap::from_ranges(&[(memory_start_addr, 0x10000)]).unwrap(); - - let chain = create_descriptor_chain( - &memory, - GuestAddress(0x0), - GuestAddress(0x100), - vec![ - (Readable, 16), - (Readable, 16), - (Readable, 96), - (Writable, 64), - (Writable, 1), - (Writable, 3), - ], - 0, - ) - .expect("create_descriptor_chain failed"); - let mut reader = Reader::new(&memory, chain).expect("failed to create Reader"); - - let other = reader.split_at(128).expect("failed to split Reader"); - assert_eq!(reader.available_bytes(), 128); - assert_eq!(other.available_bytes(), 0); - } - - #[test] - fn split_beginning() { - use DescriptorType::*; - - let memory_start_addr = GuestAddress(0x0); - let memory = GuestMemoryMmap::from_ranges(&[(memory_start_addr, 0x10000)]).unwrap(); - - let chain = create_descriptor_chain( - &memory, - GuestAddress(0x0), - GuestAddress(0x100), - vec![ - (Readable, 16), - (Readable, 16), - (Readable, 96), - (Writable, 64), - (Writable, 1), - (Writable, 3), - ], - 0, - ) - .expect("create_descriptor_chain failed"); - let mut reader = Reader::new(&memory, chain).expect("failed to create Reader"); - - let other = reader.split_at(0).expect("failed to split Reader"); - assert_eq!(reader.available_bytes(), 0); - assert_eq!(other.available_bytes(), 128); - } - - #[test] - fn split_outofbounds() { - use DescriptorType::*; - - let memory_start_addr = GuestAddress(0x0); - let memory = GuestMemoryMmap::from_ranges(&[(memory_start_addr, 0x10000)]).unwrap(); - - let chain = create_descriptor_chain( - &memory, - GuestAddress(0x0), - GuestAddress(0x100), - vec![ - (Readable, 16), - (Readable, 16), - (Readable, 96), - (Writable, 64), - (Writable, 1), - (Writable, 3), - ], - 0, - ) - .expect("create_descriptor_chain failed"); - let mut reader = Reader::new(&memory, chain).expect("failed to create Reader"); - - if let Ok(_) = reader.split_at(256) { - panic!("successfully split Reader with out of bounds offset"); - } - } - - #[test] - fn read_full() { - use DescriptorType::*; - - let memory_start_addr = GuestAddress(0x0); - let memory = GuestMemoryMmap::from_ranges(&[(memory_start_addr, 0x10000)]).unwrap(); - - let chain = create_descriptor_chain( - &memory, - GuestAddress(0x0), - GuestAddress(0x100), - vec![(Readable, 16), (Readable, 16), (Readable, 16)], - 0, - ) - .expect("create_descriptor_chain failed"); - let mut reader = Reader::new(&memory, chain).expect("failed to create Reader"); - - let mut buf = [0u8; 64]; - assert_eq!( - reader.read(&mut buf[..]).expect("failed to read to buffer"), - 48 - ); - } - - #[test] - fn write_full() { - use DescriptorType::*; - - let memory_start_addr = GuestAddress(0x0); - let memory = GuestMemoryMmap::from_ranges(&[(memory_start_addr, 0x10000)]).unwrap(); - - let chain = create_descriptor_chain( - &memory, - GuestAddress(0x0), - GuestAddress(0x100), - vec![(Writable, 16), (Writable, 16), (Writable, 16)], - 0, - ) - .expect("create_descriptor_chain failed"); - let mut writer = Writer::new(&memory, chain).expect("failed to create Writer"); - - let buf = [0xdeu8; 64]; - assert_eq!( - writer.write(&buf[..]).expect("failed to write from buffer"), - 48 - ); - } -} diff --git a/vendor/krun-devices/src/virtio/device.rs b/vendor/krun-devices/src/virtio/device.rs deleted file mode 100644 index bb2c669d6..000000000 --- a/vendor/krun-devices/src/virtio/device.rs +++ /dev/null @@ -1,184 +0,0 @@ -// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -// -// Portions Copyright 2017 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the THIRD-PARTY file. - -use std::sync::Arc; - -use super::{ActivateResult, InterruptTransport, Queue}; -use crate::virtio::AsAny; -use utils::eventfd::EventFd; -use vm_memory::GuestMemoryMmap; - -/// Configuration for a single virtqueue. -/// This is used by devices to declare their queue requirements, -/// and by the transport to construct the actual queues. -#[derive(Clone, Copy, Debug)] -pub struct QueueConfig { - /// Maximum size of the queue. - pub size: u16, -} - -impl QueueConfig { - pub const fn new(size: u16) -> Self { - Self { size } - } -} - -/// A virtqueue combined with its notification eventfd. -/// This is passed to devices during activation. -pub struct DeviceQueue { - pub queue: Queue, - pub event: Arc, -} - -impl DeviceQueue { - pub fn new(queue: Queue, event: Arc) -> Self { - Self { queue, event } - } -} - -/// Enum that indicates if a VirtioDevice is inactive or has been activated -/// and memory attached to it. -pub enum DeviceState { - Inactive, - Activated(GuestMemoryMmap, InterruptTransport), -} - -impl DeviceState { - pub fn signal_used_queue(&self) { - match self { - Self::Inactive => { - warn!("DeviceState::signal_used_queue() called, but device is not activated") - } - Self::Activated(_, ref interrupt) => interrupt.signal_used_queue(), - } - } -} - -impl DeviceState { - pub fn is_activated(&self) -> bool { - matches!(self, DeviceState::Activated(..)) - } -} - -#[derive(Clone)] -pub struct VirtioShmRegion { - pub host_addr: u64, - pub guest_addr: u64, - pub size: usize, -} - -/// Trait for virtio devices to be driven by a virtio transport. -/// -/// The lifecycle of a virtio device is to be moved to a virtio transport, which will then query the -/// device. The transport constructs queues based on queue_config() and passes them to the device -/// during activation, transferring ownership. After reset, the transport recreates queues -/// from queue_config() for the next negotiation cycle. -pub trait VirtioDevice: AsAny + Send { - /// Get the available features offered by device. - fn avail_features(&self) -> u64; - - /// Get acknowledged features of the driver. - fn acked_features(&self) -> u64; - - /// Set acknowledged features of the driver. - /// This function must maintain the following invariant: - /// - self.avail_features() & self.acked_features() = self.get_acked_features() - fn set_acked_features(&mut self, acked_features: u64); - - /// The virtio device type. - fn device_type(&self) -> u32; - - /// Device name used for logging information about the device at the transport layer - fn device_name(&self) -> &str; - - /// Returns the queue configuration for this device. - /// The transport uses this to construct the queues during initialization and after reset. - fn queue_config(&self) -> &[QueueConfig]; - - /// The set of feature bits shifted by `page * 32`. - fn avail_features_by_page(&self, page: u32) -> u32 { - let avail_features = self.avail_features(); - match page { - // Get the lower 32-bits of the features bitfield. - 0 => avail_features as u32, - // Get the upper 32-bits of the features bitfield. - 1 => (avail_features >> 32) as u32, - _ => { - warn!("Received request for unknown features page."); - 0u32 - } - } - } - - /// Acknowledges that this set of features should be enabled. - fn ack_features_by_page(&mut self, page: u32, value: u32) { - let mut v = match page { - 0 => u64::from(value), - 1 => u64::from(value) << 32, - _ => { - warn!("Cannot acknowledge unknown features page: {page}"); - 0u64 - } - }; - - // Check if the guest is ACK'ing a feature that we didn't claim to have. - let avail_features = self.avail_features(); - let unrequested_features = v & !avail_features; - if unrequested_features != 0 { - warn!("Received acknowledge request for unknown feature: {v:x}"); - // Don't count these features as acked. - v &= !unrequested_features; - } - self.set_acked_features(self.acked_features() | v); - } - - /// Reads this device configuration space at `offset`. - fn read_config(&self, offset: u64, data: &mut [u8]); - - /// Writes to this device configuration space at `offset`. - fn write_config(&mut self, offset: u64, data: &[u8]); - - /// Performs the formal activation for a device, which can be verified also with `is_activated`. - /// Ownership of the queues is transferred to the device. - fn activate( - &mut self, - mem: GuestMemoryMmap, - interrupt: InterruptTransport, - queues: Vec, - ) -> ActivateResult; - - /// Checks if the resources of this device are activated. - fn is_activated(&self) -> bool; - - /// Optionally deactivates this device. The device should drop its queues. - /// After reset, the transport will recreate queues from queue_config(). - fn reset(&mut self) -> bool { - false - } - - /// Get base and size of the SHM region - fn shm_region(&self) -> Option<&VirtioShmRegion> { - None - } -} - -pub trait VmmExitObserver: Send { - /// Callback to finish processing or cleanup the device resources - fn on_vmm_exit(&mut self) {} -} - -impl VmmExitObserver for F { - fn on_vmm_exit(&mut self) { - self() - } -} - -impl std::fmt::Debug for dyn VirtioDevice { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "VirtioDevice type {}", self.device_type()) - } -} diff --git a/vendor/krun-devices/src/virtio/file_traits.rs b/vendor/krun-devices/src/virtio/file_traits.rs deleted file mode 100644 index bef037dd0..000000000 --- a/vendor/krun-devices/src/virtio/file_traits.rs +++ /dev/null @@ -1,482 +0,0 @@ -// Copyright 2018 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -use std::fs::File; -use std::io::{Error, ErrorKind, Result}; -use std::os::unix::io::AsRawFd; - -#[cfg(feature = "blk")] -use imago::io_buffers::{IoVector, IoVectorMut}; -#[cfg(feature = "blk")] -use std::io::{IoSlice, IoSliceMut}; -use vm_memory::VolatileSlice; - -use libc::{c_int, c_void, read, readv, size_t, write, writev}; - -use super::bindings::{off64_t, pread64, preadv64, pwrite64, pwritev64}; -#[cfg(feature = "blk")] -use super::block::device::DiskProperties; - -/// A trait for setting the size of a file. -/// This is equivalent to File's `set_len` method, but -/// wrapped in a trait so that it can be implemented for -/// other types. -pub trait FileSetLen { - // Set the size of this file. - // This is the moral equivalent of `ftruncate()`. - fn set_len(&self, _len: u64) -> Result<()>; -} - -impl FileSetLen for File { - fn set_len(&self, len: u64) -> Result<()> { - File::set_len(self, len) - } -} - -/// A trait similar to `Read` and `Write`, but uses volatile memory as buffers. -pub trait FileReadWriteVolatile { - /// Read bytes from this file into the given slice, returning the number of bytes read on - /// success. - fn read_volatile(&mut self, slice: VolatileSlice) -> Result; - - /// Like `read_volatile`, except it reads to a slice of buffers. Data is copied to fill each - /// buffer in order, with the final buffer written to possibly being only partially filled. This - /// method must behave as a single call to `read_volatile` with the buffers concatenated would. - /// The default implementation calls `read_volatile` with either the first nonempty buffer - /// provided, or returns `Ok(0)` if none exists. - fn read_vectored_volatile(&mut self, bufs: &[VolatileSlice]) -> Result { - bufs.iter() - .find(|b| !b.is_empty()) - .map(|&b| self.read_volatile(b)) - .unwrap_or(Ok(0)) - } - - /// Reads bytes from this into the given slice until all bytes in the slice are written, or an - /// error is returned. - fn read_exact_volatile(&mut self, mut slice: VolatileSlice) -> Result<()> { - while !slice.is_empty() { - let bytes_read = self.read_volatile(slice)?; - if bytes_read == 0 { - return Err(Error::from(ErrorKind::UnexpectedEof)); - } - // Will panic if read_volatile read more bytes than we gave it, which would be worthy of - // a panic. - slice = slice.offset(bytes_read).unwrap(); - } - Ok(()) - } - - /// Write bytes from the slice to the given file, returning the number of bytes written on - /// success. - fn write_volatile(&mut self, slice: VolatileSlice) -> Result; - - /// Like `write_volatile`, except that it writes from a slice of buffers. Data is copied from - /// each buffer in order, with the final buffer read from possibly being only partially - /// consumed. This method must behave as a call to `write_volatile` with the buffers - /// concatenated would. The default implementation calls `write_volatile` with either the first - /// nonempty buffer provided, or returns `Ok(0)` if none exists. - fn write_vectored_volatile(&mut self, bufs: &[VolatileSlice]) -> Result { - bufs.iter() - .find(|b| !b.is_empty()) - .map(|&b| self.write_volatile(b)) - .unwrap_or(Ok(0)) - } - - /// Write bytes from the slice to the given file until all the bytes from the slice have been - /// written, or an error is returned. - fn write_all_volatile(&mut self, mut slice: VolatileSlice) -> Result<()> { - while !slice.is_empty() { - let bytes_written = self.write_volatile(slice)?; - if bytes_written == 0 { - return Err(Error::from(ErrorKind::WriteZero)); - } - // Will panic if read_volatile read more bytes than we gave it, which would be worthy of - // a panic. - slice = slice.offset(bytes_written).unwrap(); - } - Ok(()) - } -} - -impl FileReadWriteVolatile for &mut T { - fn read_volatile(&mut self, slice: VolatileSlice) -> Result { - (**self).read_volatile(slice) - } - - fn read_vectored_volatile(&mut self, bufs: &[VolatileSlice]) -> Result { - (**self).read_vectored_volatile(bufs) - } - - fn read_exact_volatile(&mut self, slice: VolatileSlice) -> Result<()> { - (**self).read_exact_volatile(slice) - } - - fn write_volatile(&mut self, slice: VolatileSlice) -> Result { - (**self).write_volatile(slice) - } - - fn write_vectored_volatile(&mut self, bufs: &[VolatileSlice]) -> Result { - (**self).write_vectored_volatile(bufs) - } - - fn write_all_volatile(&mut self, slice: VolatileSlice) -> Result<()> { - (**self).write_all_volatile(slice) - } -} - -/// A trait similar to the unix `ReadExt` and `WriteExt` traits, but for volatile memory. -pub trait FileReadWriteAtVolatile { - /// Reads bytes from this file at `offset` into the given slice, returning the number of bytes - /// read on success. - fn read_at_volatile(&self, slice: VolatileSlice, offset: u64) -> Result; - - /// Like `read_at_volatile`, except it reads to a slice of buffers. Data is copied to fill each - /// buffer in order, with the final buffer written to possibly being only partially filled. This - /// method must behave as a single call to `read_at_volatile` with the buffers concatenated - /// would. The default implementation calls `read_at_volatile` with either the first nonempty - /// buffer provided, or returns `Ok(0)` if none exists. - fn read_vectored_at_volatile(&self, bufs: &[VolatileSlice], offset: u64) -> Result { - if let Some(&slice) = bufs.first() { - self.read_at_volatile(slice, offset) - } else { - Ok(0) - } - } - - /// Reads bytes from this file at `offset` into the given slice until all bytes in the slice are - /// read, or an error is returned. - fn read_exact_at_volatile(&self, mut slice: VolatileSlice, mut offset: u64) -> Result<()> { - while !slice.is_empty() { - match self.read_at_volatile(slice, offset) { - Ok(0) => return Err(Error::from(ErrorKind::UnexpectedEof)), - Ok(n) => { - slice = slice.offset(n).unwrap(); - offset = offset.checked_add(n as u64).unwrap(); - } - Err(ref e) if e.kind() == ErrorKind::Interrupted => {} - Err(e) => return Err(e), - } - } - Ok(()) - } - - /// Writes bytes from this file at `offset` into the given slice, returning the number of bytes - /// written on success. - fn write_at_volatile(&self, slice: VolatileSlice, offset: u64) -> Result; - - /// Like `write_at_at_volatile`, except that it writes from a slice of buffers. Data is copied - /// from each buffer in order, with the final buffer read from possibly being only partially - /// consumed. This method must behave as a call to `write_at_volatile` with the buffers - /// concatenated would. The default implementation calls `write_at_volatile` with either the - /// first nonempty buffer provided, or returns `Ok(0)` if none exists. - fn write_vectored_at_volatile(&self, bufs: &[VolatileSlice], offset: u64) -> Result { - if let Some(&slice) = bufs.first() { - self.write_at_volatile(slice, offset) - } else { - Ok(0) - } - } - - /// Writes bytes from this file at `offset` into the given slice until all bytes in the slice - /// are written, or an error is returned. - fn write_all_at_volatile(&self, mut slice: VolatileSlice, mut offset: u64) -> Result<()> { - while !slice.is_empty() { - match self.write_at_volatile(slice, offset) { - Ok(0) => return Err(Error::from(ErrorKind::WriteZero)), - Ok(n) => { - slice = slice.offset(n).unwrap(); - offset = offset.checked_add(n as u64).unwrap(); - } - Err(ref e) if e.kind() == ErrorKind::Interrupted => {} - Err(e) => return Err(e), - } - } - Ok(()) - } -} - -impl FileReadWriteAtVolatile for &T { - fn read_at_volatile(&self, slice: VolatileSlice, offset: u64) -> Result { - (**self).read_at_volatile(slice, offset) - } - - fn read_vectored_at_volatile(&self, bufs: &[VolatileSlice], offset: u64) -> Result { - (**self).read_vectored_at_volatile(bufs, offset) - } - - fn read_exact_at_volatile(&self, slice: VolatileSlice, offset: u64) -> Result<()> { - (**self).read_exact_at_volatile(slice, offset) - } - - fn write_at_volatile(&self, slice: VolatileSlice, offset: u64) -> Result { - (**self).write_at_volatile(slice, offset) - } - - fn write_vectored_at_volatile(&self, bufs: &[VolatileSlice], offset: u64) -> Result { - (**self).write_vectored_at_volatile(bufs, offset) - } - - fn write_all_at_volatile(&self, slice: VolatileSlice, offset: u64) -> Result<()> { - (**self).write_all_at_volatile(slice, offset) - } -} - -macro_rules! volatile_impl { - ($ty:ty) => { - impl FileReadWriteVolatile for $ty { - fn read_volatile(&mut self, slice: VolatileSlice) -> Result { - // Safe because only bytes inside the slice are accessed and the kernel is expected - // to handle arbitrary memory for I/O. - let ret = unsafe { - read( - self.as_raw_fd(), - slice.ptr_guard_mut().as_ptr() as *mut c_void, - slice.len(), - ) - }; - if ret >= 0 { - Ok(ret as usize) - } else { - Err(Error::last_os_error()) - } - } - - fn read_vectored_volatile(&mut self, bufs: &[VolatileSlice]) -> Result { - let iovecs: Vec = bufs - .iter() - .map(|s| libc::iovec { - iov_base: s.ptr_guard_mut().as_ptr() as *mut c_void, - iov_len: s.len() as size_t, - }) - .collect(); - - if iovecs.is_empty() { - return Ok(0); - } - - // Safe because only bytes inside the buffers are accessed and the kernel is - // expected to handle arbitrary memory for I/O. - let ret = unsafe { readv(self.as_raw_fd(), &iovecs[0], iovecs.len() as c_int) }; - if ret >= 0 { - Ok(ret as usize) - } else { - Err(Error::last_os_error()) - } - } - - fn write_volatile(&mut self, slice: VolatileSlice) -> Result { - // Safe because only bytes inside the slice are accessed and the kernel is expected - // to handle arbitrary memory for I/O. - let ret = unsafe { - write( - self.as_raw_fd(), - slice.ptr_guard().as_ptr() as *const c_void, - slice.len(), - ) - }; - if ret >= 0 { - Ok(ret as usize) - } else { - Err(Error::last_os_error()) - } - } - - fn write_vectored_volatile(&mut self, bufs: &[VolatileSlice]) -> Result { - let iovecs: Vec = bufs - .iter() - .map(|s| libc::iovec { - iov_base: s.ptr_guard_mut().as_ptr() as *mut c_void, - iov_len: s.len() as size_t, - }) - .collect(); - - if iovecs.is_empty() { - return Ok(0); - } - - // Safe because only bytes inside the buffers are accessed and the kernel is - // expected to handle arbitrary memory for I/O. - let ret = unsafe { writev(self.as_raw_fd(), &iovecs[0], iovecs.len() as c_int) }; - if ret >= 0 { - Ok(ret as usize) - } else { - Err(Error::last_os_error()) - } - } - } - - impl FileReadWriteAtVolatile for $ty { - fn read_at_volatile(&self, slice: VolatileSlice, offset: u64) -> Result { - // Safe because only bytes inside the slice are accessed and the kernel is expected - // to handle arbitrary memory for I/O. - let ret = unsafe { - pread64( - self.as_raw_fd(), - slice.ptr_guard_mut().as_ptr() as *mut c_void, - slice.len(), - offset as off64_t, - ) - }; - - if ret >= 0 { - Ok(ret as usize) - } else { - Err(Error::last_os_error()) - } - } - - fn read_vectored_at_volatile( - &self, - bufs: &[VolatileSlice], - offset: u64, - ) -> Result { - let iovecs: Vec = bufs - .iter() - .map(|s| libc::iovec { - iov_base: s.ptr_guard_mut().as_ptr() as *mut c_void, - iov_len: s.len() as size_t, - }) - .collect(); - - if iovecs.is_empty() { - return Ok(0); - } - - // Safe because only bytes inside the buffers are accessed and the kernel is - // expected to handle arbitrary memory for I/O. - let ret = unsafe { - preadv64( - self.as_raw_fd(), - &iovecs[0], - iovecs.len() as c_int, - offset as off64_t, - ) - }; - if ret >= 0 { - Ok(ret as usize) - } else { - Err(Error::last_os_error()) - } - } - - fn write_at_volatile(&self, slice: VolatileSlice, offset: u64) -> Result { - // Safe because only bytes inside the slice are accessed and the kernel is expected - // to handle arbitrary memory for I/O. - let ret = unsafe { - pwrite64( - self.as_raw_fd(), - slice.ptr_guard().as_ptr() as *const c_void, - slice.len(), - offset as off64_t, - ) - }; - - if ret >= 0 { - Ok(ret as usize) - } else { - Err(Error::last_os_error()) - } - } - - fn write_vectored_at_volatile( - &self, - bufs: &[VolatileSlice], - offset: u64, - ) -> Result { - let iovecs: Vec = bufs - .iter() - .map(|s| libc::iovec { - iov_base: s.ptr_guard_mut().as_ptr() as *mut c_void, - iov_len: s.len() as size_t, - }) - .collect(); - - if iovecs.is_empty() { - return Ok(0); - } - - // Safe because only bytes inside the buffers are accessed and the kernel is - // expected to handle arbitrary memory for I/O. - let ret = unsafe { - pwritev64( - self.as_raw_fd(), - &iovecs[0], - iovecs.len() as c_int, - offset as off64_t, - ) - }; - if ret >= 0 { - Ok(ret as usize) - } else { - Err(Error::last_os_error()) - } - } - } - }; -} - -volatile_impl!(File); - -#[cfg(feature = "blk")] -impl FileReadWriteAtVolatile for DiskProperties { - fn read_at_volatile(&self, slice: VolatileSlice, offset: u64) -> Result { - self.read_vectored_at_volatile(&[slice], offset) - } - - fn read_vectored_at_volatile(&self, bufs: &[VolatileSlice], offset: u64) -> Result { - if bufs.is_empty() { - return Ok(0); - } - - let guards: Vec<_> = bufs.iter().map(|s| s.ptr_guard_mut()).collect(); - let slices: Vec<_> = guards - .iter() - .map(|g| { - let slice = if g.len() == 0 { - &mut [] - } else { - unsafe { std::slice::from_raw_parts_mut(g.as_ptr(), g.len()) } - }; - IoSliceMut::new(slice) - }) - .collect(); - let iovec = IoVectorMut::from(slices); - let full_length = iovec - .len() - .try_into() - .map_err(|e| Error::new(ErrorKind::InvalidData, e))?; - self.file.lock().unwrap().readv(iovec, offset)?; - Ok(full_length) - } - - fn write_at_volatile(&self, slice: VolatileSlice, offset: u64) -> Result { - self.write_vectored_at_volatile(&[slice], offset) - } - - fn write_vectored_at_volatile(&self, bufs: &[VolatileSlice], offset: u64) -> Result { - if bufs.is_empty() { - return Ok(0); - } - - let guards: Vec<_> = bufs.iter().map(|s| s.ptr_guard()).collect(); - let slices: Vec<_> = guards - .iter() - .map(|g| { - let slice = if g.len() == 0 { - &[] - } else { - unsafe { std::slice::from_raw_parts(g.as_ptr(), g.len()) } - }; - IoSlice::new(slice) - }) - .collect(); - let iovec = IoVector::from(slices); - let full_length = iovec - .len() - .try_into() - .map_err(|e| Error::new(ErrorKind::InvalidData, e))?; - self.file.lock().unwrap().writev(iovec, offset)?; - Ok(full_length) - } -} diff --git a/vendor/krun-devices/src/virtio/fs/augment_fs.rs b/vendor/krun-devices/src/virtio/fs/augment_fs.rs deleted file mode 100644 index b3d2349cc..000000000 --- a/vendor/krun-devices/src/virtio/fs/augment_fs.rs +++ /dev/null @@ -1,745 +0,0 @@ -// Virtual inode overlay for virtiofs. -// -// `AugmentFs` wraps an inner `FileSystem` implementation and intercepts -// FUSE operations for virtual inodes — synthetic read-only files that exist -// only in memory. All other operations are delegated to the inner filesystem. -// -// Virtual inodes are injected into the root directory (parent = ROOT_ID) and -// are currently only accessible via lookup (they do not appear in readdir). -// -// One-shot files can only be looked up once — the name is removed from the -// directory on first lookup so subsequent lookups return ENOENT. - -#[cfg(target_os = "macos")] -use crossbeam_channel::Sender; -use std::collections::HashMap; -use std::ffi::{CStr, CString}; -use std::io; -use std::mem; -use std::sync::atomic::{AtomicI32, Ordering}; -use std::sync::{Arc, RwLock}; -use std::time::Duration; - -#[cfg(target_os = "macos")] -use utils::worker_message::WorkerMessage; - -use super::filesystem::{ - Context, DirEntry, Entry, Extensions, FileSystem, FsOptions, GetxattrReply, ListxattrReply, - OpenOptions, SetattrValid, ZeroCopyReader, ZeroCopyWriter, -}; -use super::fuse; -use super::inode_alloc::InodeAllocator; -use super::virtual_entry::{VirtualDirEntry, VirtualEntry, VirtualEntryContent, VIRTUAL_BLKSIZE}; -use crate::virtio::bindings; -use crate::virtio::linux_errno; - -type Inode = u64; -type Handle = u64; - -/// Sentinel handle returned for all virtual file opens. This works because -/// virtual file operations dispatch on inode, not handle — there is no -/// per-open state. If per-fd state is ever needed (e.g. writable virtual -/// files), this must be replaced with a real handle allocator. -const VIRTUAL_HANDLE: Handle = 0; - -/// Persistent virtual entries never change. -const VIRTUAL_TIMEOUT: Duration = Duration::MAX; - -/// Overlay that injects virtual inodes into an inner `FileSystem`. -pub struct AugmentFs { - inner: T, - /// Maps (parent_inode, name) → child inode number. One-shot entries - /// are removed on first lookup so the file can only be opened once. - name_to_inode: RwLock>, - /// Maps virtual inode number → (mode, inode data). One-shot entries are - /// removed from this map on release. - inodes: RwLock>, -} - -impl> AugmentFs { - /// Create a new overlay. - /// - /// `entries` are registered as virtual inodes in the root directory. - /// Inode numbers are obtained from `inode_alloc`, the same allocator - /// used by the inner filesystem. - pub fn new(inner: T, inode_alloc: &InodeAllocator, entries: Vec) -> Self { - let mut name_to_inode = HashMap::new(); - let mut inodes = HashMap::new(); - - Self::register_entries( - fuse::ROOT_ID, - entries, - inode_alloc, - &mut name_to_inode, - &mut inodes, - ); - - Self { - inner, - name_to_inode: RwLock::new(name_to_inode), - inodes: RwLock::new(inodes), - } - } - - fn register_entries( - parent: Inode, - entries: Vec, - inode_alloc: &InodeAllocator, - name_to_inode: &mut HashMap<(Inode, CString), Inode>, - inodes: &mut HashMap, - ) { - for entry in entries { - let ino = inode_alloc.next(); - name_to_inode.insert((parent, entry.name), ino); - - // Recurse into directory children before moving the node. - if let VirtualEntryContent::Dir { children } = entry.entry.content { - Self::register_entries(ino, children, inode_alloc, name_to_inode, inodes); - inodes.insert( - ino, - VirtualEntry { - mode: entry.entry.mode, - one_shot: entry.entry.one_shot, - content: VirtualEntryContent::Dir { - children: Vec::new(), - }, - }, - ); - } else { - inodes.insert(ino, entry.entry); - } - } - } - - fn is_virtual(&self, inode: Inode) -> bool { - self.inodes.read().unwrap().contains_key(&inode) - } - - fn virtual_stat(ino: Inode, vnode: &VirtualEntry) -> (bindings::stat64, Duration) { - let mut st: bindings::stat64 = unsafe { mem::zeroed() }; - st.st_ino = ino; - st.st_mode = vnode.st_mode() as _; - st.st_blksize = VIRTUAL_BLKSIZE as _; - let timeout = if vnode.one_shot { - Duration::ZERO - } else { - VIRTUAL_TIMEOUT - }; - match &vnode.content { - VirtualEntryContent::File { data, .. } => { - st.st_size = data.len() as i64; - st.st_nlink = 1; - st.st_blocks = ((data.len() as i64) + 511) / 512; - } - VirtualEntryContent::Dir { .. } => { - st.st_nlink = 2; - } - } - (st, timeout) - } -} - -impl> FileSystem for AugmentFs { - type Inode = Inode; - type Handle = Handle; - - fn init(&self, capable: FsOptions) -> io::Result { - self.inner.init(capable) - } - - fn destroy(&self) { - self.inner.destroy() - } - - fn lookup(&self, ctx: Context, parent: Inode, name: &CStr) -> io::Result { - let key = (parent, CString::from(name)); - let inode = self.name_to_inode.read().unwrap().get(&key).copied(); - if let Some(inode) = inode { - let inodes = self.inodes.read().unwrap(); - if let Some(vnode) = inodes.get(&inode) { - let one_shot = vnode.one_shot; - let (st, timeout) = Self::virtual_stat(inode, vnode); - - if one_shot { - drop(inodes); - self.name_to_inode.write().unwrap().remove(&key); - } - - return Ok(Entry { - inode, - generation: 0, - attr: st, - attr_flags: 0, - attr_timeout: timeout, - entry_timeout: timeout, - }); - } - } - self.inner.lookup(ctx, parent, name) - } - - fn forget(&self, ctx: Context, inode: Inode, count: u64) { - if !self.is_virtual(inode) { - self.inner.forget(ctx, inode, count) - } - } - - fn batch_forget(&self, ctx: Context, mut requests: Vec<(Inode, u64)>) { - requests.retain(|(ino, _)| !self.is_virtual(*ino)); - self.inner.batch_forget(ctx, requests); - } - - fn getattr( - &self, - ctx: Context, - inode: Inode, - handle: Option, - ) -> io::Result<(bindings::stat64, Duration)> { - { - let inodes = self.inodes.read().unwrap(); - if let Some(vnode) = inodes.get(&inode) { - return Ok(Self::virtual_stat(inode, vnode)); - } - } - self.inner.getattr(ctx, inode, handle) - } - - fn setattr( - &self, - ctx: Context, - inode: Inode, - attr: bindings::stat64, - handle: Option, - valid: SetattrValid, - ) -> io::Result<(bindings::stat64, Duration)> { - if self.is_virtual(inode) { - return Err(linux_errno::eperm()); - } - self.inner.setattr(ctx, inode, attr, handle, valid) - } - - fn readlink(&self, ctx: Context, inode: Inode) -> io::Result> { - if self.is_virtual(inode) { - return Err(linux_errno::einval()); - } - self.inner.readlink(ctx, inode) - } - - fn symlink( - &self, - ctx: Context, - linkname: &CStr, - parent: Inode, - name: &CStr, - extensions: Extensions, - ) -> io::Result { - self.inner.symlink(ctx, linkname, parent, name, extensions) - } - - fn mknod( - &self, - ctx: Context, - inode: Inode, - name: &CStr, - mode: u32, - rdev: u32, - umask: u32, - extensions: Extensions, - ) -> io::Result { - self.inner - .mknod(ctx, inode, name, mode, rdev, umask, extensions) - } - - fn mkdir( - &self, - ctx: Context, - parent: Inode, - name: &CStr, - mode: u32, - umask: u32, - extensions: Extensions, - ) -> io::Result { - let key = (parent, CString::from(name)); - if self.name_to_inode.read().unwrap().contains_key(&key) { - return Err(linux_errno::eexist()); - } - self.inner.mkdir(ctx, parent, name, mode, umask, extensions) - } - - fn unlink(&self, ctx: Context, parent: Inode, name: &CStr) -> io::Result<()> { - self.inner.unlink(ctx, parent, name) - } - - fn rmdir(&self, ctx: Context, parent: Inode, name: &CStr) -> io::Result<()> { - self.inner.rmdir(ctx, parent, name) - } - - fn rename( - &self, - ctx: Context, - olddir: Inode, - oldname: &CStr, - newdir: Inode, - newname: &CStr, - flags: u32, - ) -> io::Result<()> { - self.inner - .rename(ctx, olddir, oldname, newdir, newname, flags) - } - - fn link( - &self, - ctx: Context, - inode: Inode, - newparent: Inode, - newname: &CStr, - ) -> io::Result { - if self.is_virtual(inode) { - return Err(linux_errno::eperm()); - } - self.inner.link(ctx, inode, newparent, newname) - } - - fn open( - &self, - ctx: Context, - inode: Inode, - kill_priv: bool, - flags: u32, - ) -> io::Result<(Option, OpenOptions)> { - { - let inodes = self.inodes.read().unwrap(); - if let Some(vnode) = inodes.get(&inode) { - if vnode.is_dir() { - return Err(linux_errno::eisdir()); - } - if (flags as i32 & libc::O_ACCMODE) != libc::O_RDONLY { - return Err(linux_errno::eacces()); - } - return Ok((Some(VIRTUAL_HANDLE), OpenOptions::empty())); - } - } - self.inner.open(ctx, inode, kill_priv, flags) - } - - fn create( - &self, - ctx: Context, - parent: Inode, - name: &CStr, - mode: u32, - kill_priv: bool, - flags: u32, - umask: u32, - extensions: Extensions, - ) -> io::Result<(Entry, Option, OpenOptions)> { - self.inner - .create(ctx, parent, name, mode, kill_priv, flags, umask, extensions) - } - - fn read( - &self, - ctx: Context, - inode: Inode, - handle: Handle, - mut w: W, - size: u32, - offset: u64, - lock_owner: Option, - flags: u32, - ) -> io::Result { - { - let inodes = self.inodes.read().unwrap(); - if let Some(vnode) = inodes.get(&inode) { - let data = vnode.data().ok_or_else(linux_errno::eisdir)?; - let off: usize = offset.try_into().map_err(|_| linux_errno::einval())?; - if off >= data.len() { - return Ok(0); - } - let remaining = data.len() - off; - let len = remaining.min(size as usize); - return w.write(&data[off..(off + len)]); - } - } - self.inner - .read(ctx, inode, handle, w, size, offset, lock_owner, flags) - } - - fn write( - &self, - ctx: Context, - inode: Inode, - handle: Handle, - r: R, - size: u32, - offset: u64, - lock_owner: Option, - delayed_write: bool, - kill_priv: bool, - flags: u32, - ) -> io::Result { - if self.is_virtual(inode) { - return Err(linux_errno::eperm()); - } - self.inner.write( - ctx, - inode, - handle, - r, - size, - offset, - lock_owner, - delayed_write, - kill_priv, - flags, - ) - } - - fn flush(&self, ctx: Context, inode: Inode, handle: Handle, lock_owner: u64) -> io::Result<()> { - if self.is_virtual(inode) { - return Ok(()); - } - self.inner.flush(ctx, inode, handle, lock_owner) - } - - fn fsync(&self, ctx: Context, inode: Inode, datasync: bool, handle: Handle) -> io::Result<()> { - if self.is_virtual(inode) { - return Ok(()); - } - self.inner.fsync(ctx, inode, datasync, handle) - } - - fn fallocate( - &self, - ctx: Context, - inode: Inode, - handle: Handle, - mode: u32, - offset: u64, - length: u64, - ) -> io::Result<()> { - if self.is_virtual(inode) { - return Err(linux_errno::eperm()); - } - self.inner - .fallocate(ctx, inode, handle, mode, offset, length) - } - - fn release( - &self, - ctx: Context, - inode: Inode, - flags: u32, - handle: Handle, - flush: bool, - flock_release: bool, - lock_owner: Option, - ) -> io::Result<()> { - { - let mut inodes = self.inodes.write().unwrap(); - if let Some(vnode) = inodes.get(&inode) { - if vnode.one_shot { - inodes.remove(&inode); - } - return Ok(()); - } - } - self.inner - .release(ctx, inode, flags, handle, flush, flock_release, lock_owner) - } - - fn statfs(&self, ctx: Context, inode: Inode) -> io::Result { - self.inner.statfs(ctx, inode) - } - - fn getxattr( - &self, - ctx: Context, - inode: Inode, - name: &CStr, - size: u32, - ) -> io::Result { - if self.is_virtual(inode) { - return Err(linux_errno::enodata()); - } - self.inner.getxattr(ctx, inode, name, size) - } - - fn listxattr(&self, ctx: Context, inode: Inode, size: u32) -> io::Result { - if self.is_virtual(inode) { - if size == 0 { - return Ok(ListxattrReply::Count(0)); - } - return Ok(ListxattrReply::Names(Vec::new())); - } - self.inner.listxattr(ctx, inode, size) - } - - fn setxattr( - &self, - ctx: Context, - inode: Inode, - name: &CStr, - value: &[u8], - flags: u32, - ) -> io::Result<()> { - if self.is_virtual(inode) { - return Err(linux_errno::eperm()); - } - self.inner.setxattr(ctx, inode, name, value, flags) - } - - fn removexattr(&self, ctx: Context, inode: Inode, name: &CStr) -> io::Result<()> { - if self.is_virtual(inode) { - return Err(linux_errno::eperm()); - } - self.inner.removexattr(ctx, inode, name) - } - - fn opendir( - &self, - ctx: Context, - inode: Inode, - flags: u32, - ) -> io::Result<(Option, OpenOptions)> { - self.inner.opendir(ctx, inode, flags) - } - - fn readdir( - &self, - ctx: Context, - inode: Inode, - handle: Handle, - size: u32, - offset: u64, - add_entry: F, - ) -> io::Result<()> - where - F: FnMut(DirEntry) -> io::Result, - { - self.inner - .readdir(ctx, inode, handle, size, offset, add_entry) - } - - fn readdirplus( - &self, - ctx: Context, - inode: Inode, - handle: Handle, - size: u32, - offset: u64, - add_entry: F, - ) -> io::Result<()> - where - F: FnMut(DirEntry, Entry) -> io::Result, - { - self.inner - .readdirplus(ctx, inode, handle, size, offset, add_entry) - } - - fn fsyncdir( - &self, - ctx: Context, - inode: Inode, - datasync: bool, - handle: Handle, - ) -> io::Result<()> { - self.inner.fsyncdir(ctx, inode, datasync, handle) - } - - fn releasedir(&self, ctx: Context, inode: Inode, flags: u32, handle: Handle) -> io::Result<()> { - self.inner.releasedir(ctx, inode, flags, handle) - } - - fn access(&self, ctx: Context, inode: Inode, mask: u32) -> io::Result<()> { - if self.is_virtual(inode) { - if mask & (libc::W_OK as u32) != 0 { - return Err(linux_errno::eacces()); - } - return Ok(()); - } - self.inner.access(ctx, inode, mask) - } - - fn lseek( - &self, - ctx: Context, - inode: Inode, - _handle: Handle, - offset: u64, - whence: u32, - ) -> io::Result { - { - let inodes = self.inodes.read().unwrap(); - if let Some(vnode) = inodes.get(&inode) { - let size = vnode.data().ok_or_else(linux_errno::eisdir)?.len() as u64; - // FUSE lseek is only called for SEEK_DATA/SEEK_HOLE. - return match whence as i32 { - libc::SEEK_DATA => { - if offset < size { - Ok(offset) - } else { - Err(linux_errno::enxio()) - } - } - libc::SEEK_HOLE => { - if offset < size { - Ok(size) - } else { - Err(linux_errno::enxio()) - } - } - _ => Err(linux_errno::einval()), - }; - } - } - self.inner.lseek(ctx, inode, _handle, offset, whence) - } - - fn copyfilerange( - &self, - ctx: Context, - inode_in: Inode, - handle_in: Handle, - offset_in: u64, - inode_out: Inode, - handle_out: Handle, - offset_out: u64, - len: u64, - flags: u64, - ) -> io::Result { - // Virtual inodes don't have real file descriptors, so copy_file_range - // cannot work. Return EXDEV to tell the kernel to fall back to - // read+write. - if self.is_virtual(inode_in) || self.is_virtual(inode_out) { - return Err(linux_errno::exdev()); - } - self.inner.copyfilerange( - ctx, inode_in, handle_in, offset_in, inode_out, handle_out, offset_out, len, flags, - ) - } - - fn setupmapping( - &self, - ctx: Context, - inode: Inode, - handle: Handle, - foffset: u64, - len: u64, - flags: u64, - moffset: u64, - host_shm_base: u64, - shm_size: u64, - #[cfg(target_os = "macos")] map_sender: &Option>, - ) -> io::Result<()> { - { - let inodes = self.inodes.read().unwrap(); - if let Some(vnode) = inodes.get(&inode) { - let data = vnode.data().ok_or_else(linux_errno::eisdir)?; - #[cfg(target_os = "linux")] - { - if (moffset + len) > shm_size { - return Err(linux_errno::einval()); - } - - let addr = host_shm_base + moffset; - let ret = unsafe { - libc::mmap( - addr as *mut libc::c_void, - len as usize, - libc::PROT_READ | libc::PROT_WRITE, - libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | libc::MAP_FIXED, - -1, - 0, - ) - }; - if std::ptr::eq(ret, libc::MAP_FAILED) { - return Err(io::Error::last_os_error()); - } - - let foff = foffset as usize; - if foff < data.len() { - let available = data.len() - foff; - let to_copy = (len as usize).min(available); - unsafe { - libc::memcpy( - addr as *mut libc::c_void, - data.as_ptr().add(foff) as *const _, - to_copy, - ) - }; - } - - return Ok(()); - } - - // TODO: implement DAX for virtual files on macOS. - // Needs a shared memory region manager (see setupmapping - // in macos/passthrough.rs for the real-file DAX path). - #[cfg(target_os = "macos")] - { - let _ = data; - return Err(linux_errno::enosys()); - } - } - } - self.inner.setupmapping( - ctx, - inode, - handle, - foffset, - len, - flags, - moffset, - host_shm_base, - shm_size, - #[cfg(target_os = "macos")] - map_sender, - ) - } - - fn removemapping( - &self, - ctx: Context, - requests: Vec, - host_shm_base: u64, - shm_size: u64, - #[cfg(target_os = "macos")] map_sender: &Option>, - ) -> io::Result<()> { - self.inner.removemapping( - ctx, - requests, - host_shm_base, - shm_size, - #[cfg(target_os = "macos")] - map_sender, - ) - } - - fn ioctl( - &self, - ctx: Context, - inode: Inode, - handle: Handle, - flags: u32, - cmd: u32, - arg: u64, - in_size: u32, - out_size: u32, - exit_code: &Arc, - ) -> io::Result> { - // We can't use nix::request_code_none here since it's system-dependent - // and we need the value from Linux. - const VIRTIO_IOC_EXIT_CODE_REQ: u32 = 0x7602; - - match cmd { - VIRTIO_IOC_EXIT_CODE_REQ => { - exit_code.store(arg as i32, Ordering::SeqCst); - Ok(Vec::new()) - } - _ => self.inner.ioctl( - ctx, inode, handle, flags, cmd, arg, in_size, out_size, exit_code, - ), - } - } -} diff --git a/vendor/krun-devices/src/virtio/fs/device.rs b/vendor/krun-devices/src/virtio/fs/device.rs deleted file mode 100644 index ba8b0ac32..000000000 --- a/vendor/krun-devices/src/virtio/fs/device.rs +++ /dev/null @@ -1,257 +0,0 @@ -#[cfg(target_os = "macos")] -use crossbeam_channel::Sender; -use std::cmp; -use std::io::Write; -use std::sync::atomic::{AtomicI32, AtomicU64, Ordering}; -use std::sync::Arc; -use std::thread::JoinHandle; -use std::time::Duration; - -use utils::eventfd::{EventFd, EFD_NONBLOCK}; -#[cfg(target_os = "macos")] -use utils::worker_message::WorkerMessage; -use virtio_bindings::{virtio_config::VIRTIO_F_VERSION_1, virtio_ring::VIRTIO_RING_F_EVENT_IDX}; -use vm_memory::{ByteValued, GuestMemoryMmap}; - -use super::super::{ - ActivateError, ActivateResult, DeviceQueue, DeviceState, FsError, QueueConfig, VirtioDevice, - VirtioShmRegion, -}; -use super::overlay::Config as OverlayConfig; -use super::passthrough; -use super::virtual_entry::VirtualDirEntry; -use super::worker::FsWorker; -use super::ExportTable; -use super::{defs, defs::uapi}; -use crate::virtio::passthrough::PermissionSemantics; -use crate::virtio::InterruptTransport; - -#[derive(Copy, Clone)] -#[repr(C, packed)] -struct VirtioFsConfig { - tag: [u8; 36], - num_request_queues: u32, -} - -impl Default for VirtioFsConfig { - fn default() -> Self { - VirtioFsConfig { - tag: [0; 36], - num_request_queues: 0, - } - } -} - -unsafe impl ByteValued for VirtioFsConfig {} - -pub struct Fs { - avail_features: u64, - acked_features: u64, - device_state: DeviceState, - config: VirtioFsConfig, - allow_idmap: bool, - shm_region: Option, - passthrough_cfg: Option, - overlay_cfg: Option, - read_only: bool, - virtual_entries: Vec, - worker_thread: Option>, - worker_stopfd: EventFd, - exit_code: Arc, - #[cfg(target_os = "macos")] - map_sender: Option>, -} - -impl Fs { - pub fn new( - fs_id: String, - semantics: PermissionSemantics, - shared_dir: Option, - exit_code: Arc, - read_only: bool, - virtual_entries: Vec, - overlay_cfg: Option, - ) -> super::Result { - let avail_features = (1u64 << VIRTIO_F_VERSION_1) | (1u64 << VIRTIO_RING_F_EVENT_IDX); - - let tag = fs_id.into_bytes(); - let mut config = VirtioFsConfig::default(); - config.tag[..tag.len()].copy_from_slice(tag.as_slice()); - config.num_request_queues = 1; - - let attr_timeout = if matches!(semantics, PermissionSemantics::LinuxSimplified) { - // As uid/gid are context-dependent, attributes can't be cached. - Duration::from_secs(0) - } else { - // The value defined as default in virtio-fs. - Duration::from_secs(5) - }; - - let fs_cfg = shared_dir.map(|root_dir| passthrough::Config { - root_dir, - semantics, - attr_timeout, - ..Default::default() - }); - - let allow_idmap = matches!(semantics, PermissionSemantics::LinuxComplete); - - Ok(Fs { - avail_features, - acked_features: 0, - device_state: DeviceState::Inactive, - config, - allow_idmap, - shm_region: None, - passthrough_cfg: fs_cfg, - overlay_cfg, - read_only, - virtual_entries, - worker_thread: None, - worker_stopfd: EventFd::new(EFD_NONBLOCK).map_err(FsError::EventFd)?, - exit_code, - #[cfg(target_os = "macos")] - map_sender: None, - }) - } - - pub fn id(&self) -> &str { - defs::FS_DEV_ID - } - - pub fn set_shm_region(&mut self, shm_region: VirtioShmRegion) { - self.shm_region = Some(shm_region); - } - - pub fn set_export_table(&mut self, export_table: ExportTable) -> u64 { - static FS_UNIQUE_ID: AtomicU64 = AtomicU64::new(0); - - let Some(cfg) = self.passthrough_cfg.as_mut() else { - // NullFs-backed devices have no passthrough config and don't - // participate in cross-domain fd export. Consume (and waste) an - // fsid so numbering stays dense, but don't store the table. - return FS_UNIQUE_ID.fetch_add(1, Ordering::Relaxed); - }; - cfg.export_fsid = FS_UNIQUE_ID.fetch_add(1, Ordering::Relaxed); - cfg.export_table = Some(export_table); - - cfg.export_fsid - } - - #[cfg(target_os = "macos")] - pub fn set_map_sender(&mut self, map_sender: Sender) { - self.map_sender = Some(map_sender); - } -} - -impl VirtioDevice for Fs { - fn avail_features(&self) -> u64 { - self.avail_features - } - - fn acked_features(&self) -> u64 { - self.acked_features - } - - fn set_acked_features(&mut self, acked_features: u64) { - self.acked_features = acked_features - } - - fn device_type(&self) -> u32 { - uapi::VIRTIO_ID_FS - } - - fn device_name(&self) -> &str { - "fs" - } - - fn queue_config(&self) -> &[QueueConfig] { - &defs::QUEUE_CONFIG - } - - fn read_config(&self, offset: u64, mut data: &mut [u8]) { - let config_slice = self.config.as_slice(); - let config_len = config_slice.len() as u64; - if offset >= config_len { - error!("Failed to read config space"); - return; - } - if let Some(end) = offset.checked_add(data.len() as u64) { - // This write can't fail, offset and end are checked against config_len. - data.write_all(&config_slice[offset as usize..cmp::min(end, config_len) as usize]) - .unwrap(); - } - } - - fn write_config(&mut self, offset: u64, data: &[u8]) { - warn!( - "fs: guest driver attempted to write device config (offset={:x}, len={:x})", - offset, - data.len() - ); - } - - fn activate( - &mut self, - mem: GuestMemoryMmap, - interrupt: InterruptTransport, - queues: Vec, - ) -> ActivateResult { - if self.worker_thread.is_some() { - panic!("virtio_fs: worker thread already exists"); - } - - // Extract queues and eventfds from DeviceQueues. - let mut worker_queues = Vec::with_capacity(queues.len()); - let mut queue_evts = Vec::with_capacity(queues.len()); - for dq in queues { - worker_queues.push(dq.queue); - queue_evts.push(dq.event); - } - - let virtual_entries = self.virtual_entries.clone(); - let worker = FsWorker::new( - worker_queues, - queue_evts, - interrupt.clone(), - mem.clone(), - self.allow_idmap, - self.shm_region.clone(), - self.passthrough_cfg.clone(), - self.overlay_cfg.clone(), - self.read_only, - virtual_entries, - self.worker_stopfd.try_clone().unwrap(), - self.exit_code.clone(), - #[cfg(target_os = "macos")] - self.map_sender.clone(), - ) - .map_err(|e| { - error!("virtio_fs: failed to create worker: {}", e); - ActivateError::BadActivate - })?; - self.worker_thread = Some(worker.run()); - - self.device_state = DeviceState::Activated(mem, interrupt); - Ok(()) - } - - fn is_activated(&self) -> bool { - self.device_state.is_activated() - } - - fn shm_region(&self) -> Option<&VirtioShmRegion> { - self.shm_region.as_ref() - } - - fn reset(&mut self) -> bool { - if let Some(worker) = self.worker_thread.take() { - let _ = self.worker_stopfd.write(1); - if let Err(e) = worker.join() { - error!("error waiting for worker thread: {e:?}"); - } - } - self.device_state = DeviceState::Inactive; - true - } -} diff --git a/vendor/krun-devices/src/virtio/fs/filesystem.rs b/vendor/krun-devices/src/virtio/fs/filesystem.rs deleted file mode 100644 index ef286e312..000000000 --- a/vendor/krun-devices/src/virtio/fs/filesystem.rs +++ /dev/null @@ -1,1206 +0,0 @@ -// Copyright 2019 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#[cfg(target_os = "macos")] -use crossbeam_channel::Sender; -#[cfg(target_os = "macos")] -use utils::worker_message::WorkerMessage; - -use std::collections::BTreeMap; -use std::convert::TryInto; -use std::ffi::{CStr, CString}; -use std::fs::File; -use std::io; -use std::mem; -use std::sync::atomic::AtomicI32; -use std::sync::{Arc, Mutex}; -use std::time::Duration; - -use super::bindings; -use super::fuse; - -pub use super::fuse::FsOptions; -pub use fuse::OpenOptions; -pub use fuse::RemovemappingOne; -pub use fuse::SetattrValid; - -/// Information about a path in the filesystem. -pub struct Entry { - /// An `Inode` that uniquely identifies this path. During `lookup`, setting this to `0` means a - /// negative entry. Returning `ENOENT` also means a negative entry but setting this to `0` - /// allows the kernel to cache the negative result for `entry_timeout`. The value should be - /// produced by converting a `FileSystem::Inode` into a `u64`. - pub inode: u64, - - /// The generation number for this `Entry`. Typically used for network file systems. An `inode` - /// / `generation` pair must be unique over the lifetime of the file system (rather than just - /// the lifetime of the mount). In other words, if a `FileSystem` implementation re-uses an - /// `Inode` after it has been deleted then it must assign a new, previously unused generation - /// number to the `Inode` at the same time. - pub generation: u64, - - /// Inode attributes. Even if `attr_timeout` is zero, `attr` must be correct. For example, for - /// `open()`, FUSE uses `attr.st_size` from `lookup()` to determine how many bytes to request. - /// If this value is not correct, incorrect data will be returned. - pub attr: bindings::stat64, - - /// Flags for `fuse::Attr.flags`. - pub attr_flags: u32, - - /// How long the values in `attr` should be considered valid. If the attributes of the `Entry` - /// are only modified by the FUSE client, then this should be set to a very large value. - pub attr_timeout: Duration, - - /// How long the name associated with this `Entry` should be considered valid. If directory - /// entries are only changed or deleted by the FUSE client, then this should be set to a very - /// large value. - pub entry_timeout: Duration, -} - -impl From for fuse::EntryOut { - fn from(entry: Entry) -> fuse::EntryOut { - fuse::EntryOut { - nodeid: entry.inode, - generation: entry.generation, - entry_valid: entry.entry_timeout.as_secs(), - attr_valid: entry.attr_timeout.as_secs(), - entry_valid_nsec: entry.entry_timeout.subsec_nanos(), - attr_valid_nsec: entry.attr_timeout.subsec_nanos(), - attr: fuse::Attr::with_flags(entry.attr, entry.attr_flags), - } - } -} - -/// Represents information about an entry in a directory. -pub struct DirEntry<'a> { - /// The inode number for this entry. This does NOT have to be the same as the `Inode` for this - /// directory entry. However, it must be the same as the `attr.st_ino` field of the `Entry` that - /// would be returned by a `lookup` request in the parent directory for `name`. - pub ino: bindings::ino64_t, - - /// Any non-zero value that the kernel can use to identify the current point in the directory - /// entry stream. It does not need to be the actual physical position. A value of `0` is - /// reserved to mean "from the beginning" and should never be used. The `offset` value of the - /// first entry in a stream should point to the beginning of the second entry and so on. - pub offset: u64, - - /// The type of this directory entry. Valid values are any of the `libc::DT_*` constants. - pub type_: u32, - - /// The name of this directory entry. There are no requirements for the contents of this field - /// and any sequence of bytes is considered valid. - pub name: &'a [u8], -} - -/// A reply to a `getxattr` method call. -pub enum GetxattrReply { - /// The value of the requested extended attribute. This can be arbitrary textual or binary data - /// and does not need to be nul-terminated. - Value(Vec), - - /// The size of the buffer needed to hold the value of the requested extended attribute. Should - /// be returned when the `size` parameter is 0. Callers should note that it is still possible - /// for the size of the value to change in between `getxattr` calls and should not assume that a - /// subsequent call to `getxattr` with the returned count will always succeed. - Count(u32), -} - -/// A reply to a `listxattr` method call. -pub enum ListxattrReply { - /// A buffer containing a nul-separated list of the names of all the extended attributes - /// associated with this `Inode`. This list of names may be unordered and includes a namespace - /// prefix. There may be several disjoint namespaces associated with a single `Inode`. - Names(Vec), - - /// This size of the buffer needed to hold the full list of extended attribute names associated - /// with this `Inode`. Should be returned when the `size` parameter is 0. Callers should note - /// that it is still possible for the set of extended attributes to change between `listxattr` - /// calls and so should not assume that a subsequent call to `listxattr` with the returned count - /// will always succeed. - Count(u32), -} - -/// A trait for directly copying data from the fuse transport into a `File` without first storing it -/// in an intermediate buffer. -pub trait ZeroCopyReader { - /// Copies at most `count` bytes from `self` directly into `f` at offset `off` without storing - /// it in any intermediate buffers. If the return value is `Ok(n)` then it must be guaranteed - /// that `0 <= n <= count`. If `n` is `0`, then it can indicate one of 3 possibilities: - /// - /// 1. There is no more data left in `self`. - /// 2. There is no more space in `f`. - /// 3. `count` was `0`. - /// - /// # Errors - /// - /// If any error is returned then the implementation must guarantee that no bytes were copied - /// from `self`. If the underlying write to `f` returns `0` then the implementation must return - /// an error of the kind `io::ErrorKind::WriteZero`. - fn read_to(&mut self, f: &File, count: usize, off: u64) -> io::Result; - - /// Copies exactly `count` bytes of data from `self` into `f` at offset `off`. `off + count` - /// must be less than `u64::MAX`. - /// - /// # Errors - /// - /// If an error is returned then the number of bytes copied from `self` is unspecified but it - /// will never be more than `count`. - fn read_exact_to(&mut self, f: &mut File, mut count: usize, mut off: u64) -> io::Result<()> { - let c = count - .try_into() - .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?; - if off.checked_add(c).is_none() { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "`off` + `count` must be less than u64::MAX", - )); - } - - while count > 0 { - match self.read_to(f, count, off) { - Ok(0) => { - return Err(io::Error::new( - io::ErrorKind::WriteZero, - "failed to fill whole buffer", - )) - } - Ok(n) => { - count -= n; - off += n as u64; - } - Err(ref e) if e.kind() == io::ErrorKind::Interrupted => {} - Err(e) => return Err(e), - } - } - - Ok(()) - } - - /// Copies all remaining bytes from `self` into `f` at offset `off`. Equivalent to repeatedly - /// calling `read_to` until it returns either `Ok(0)` or a non-`ErrorKind::Interrupted` error. - /// - /// # Errors - /// - /// If an error is returned then the number of bytes copied from `self` is unspecified. - fn copy_to_end(&mut self, f: &mut File, mut off: u64) -> io::Result { - let mut out = 0; - loop { - match self.read_to(f, usize::MAX, off) { - Ok(0) => return Ok(out), - Ok(n) => { - off = off.saturating_add(n as u64); - out += n; - } - Err(ref e) if e.kind() == io::ErrorKind::Interrupted => {} - Err(e) => return Err(e), - } - } - } -} - -impl ZeroCopyReader for &mut R { - fn read_to(&mut self, f: &File, count: usize, off: u64) -> io::Result { - (**self).read_to(f, count, off) - } - fn read_exact_to(&mut self, f: &mut File, count: usize, off: u64) -> io::Result<()> { - (**self).read_exact_to(f, count, off) - } - fn copy_to_end(&mut self, f: &mut File, off: u64) -> io::Result { - (**self).copy_to_end(f, off) - } -} - -/// A trait for directly copying data from a `File` into the fuse transport without first storing -/// it in an intermediate buffer. -pub trait ZeroCopyWriter { - /// Copies at most `count` bytes from `f` at offset `off` directly into `self` without storing - /// it in any intermediate buffers. If the return value is `Ok(n)` then it must be guaranteed - /// that `0 <= n <= count`. If `n` is `0`, then it can indicate one of 3 possibilities: - /// - /// 1. There is no more data left in `f`. - /// 2. There is no more space in `self`. - /// 3. `count` was `0`. - /// - /// # Errors - /// - /// If any error is returned then the implementation must guarantee that no bytes were copied - /// from `f`. If the underlying read from `f` returns `0` then the implementation must return an - /// error of the kind `io::ErrorKind::UnexpectedEof`. - fn write_from(&mut self, f: &File, count: usize, off: u64) -> io::Result; - - /// Copies exactly `count` bytes of data from `f` at offset `off` into `self`. `off + count` - /// must be less than `u64::MAX`. - /// - /// # Errors - /// - /// If an error is returned then the number of bytes copied from `self` is unspecified but it - /// well never be more than `count`. - fn write_all_from(&mut self, f: &mut File, mut count: usize, mut off: u64) -> io::Result<()> { - let c = count - .try_into() - .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?; - if off.checked_add(c).is_none() { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "`off` + `count` must be less than u64::MAX", - )); - } - - while count > 0 { - match self.write_from(f, count, off) { - Ok(0) => { - return Err(io::Error::new( - io::ErrorKind::UnexpectedEof, - "failed to write whole buffer", - )) - } - Ok(n) => { - // No need for checked math here because we verified that `off + count` will not - // overflow and `n` must be <= `count`. - count -= n; - off += n as u64; - } - Err(ref e) if e.kind() == io::ErrorKind::Interrupted => {} - Err(e) => return Err(e), - } - } - - Ok(()) - } - - /// Copies all remaining bytes from `f` at offset `off` into `self`. Equivalent to repeatedly - /// calling `write_from` until it returns either `Ok(0)` or a non-`ErrorKind::Interrupted` - /// error. - /// - /// # Errors - /// - /// If an error is returned then the number of bytes copied from `f` is unspecified. - fn copy_to_end(&mut self, f: &mut File, mut off: u64) -> io::Result { - let mut out = 0; - loop { - match self.write_from(f, usize::MAX, off) { - Ok(0) => return Ok(out), - Ok(n) => { - off = off.saturating_add(n as u64); - out += n; - } - Err(ref e) if e.kind() == io::ErrorKind::Interrupted => {} - Err(e) => return Err(e), - } - } - } -} - -impl ZeroCopyWriter for &mut W { - fn write_from(&mut self, f: &File, count: usize, off: u64) -> io::Result { - (**self).write_from(f, count, off) - } - fn write_all_from(&mut self, f: &mut File, count: usize, off: u64) -> io::Result<()> { - (**self).write_all_from(f, count, off) - } - fn copy_to_end(&mut self, f: &mut File, off: u64) -> io::Result { - (**self).copy_to_end(f, off) - } -} - -/// Additional context associated with requests. -#[derive(Clone, Copy, Debug)] -pub struct Context { - /// The user ID of the calling process. - pub uid: libc::uid_t, - - /// The group ID of the calling process. - pub gid: libc::gid_t, - - /// The thread group ID of the calling process. - pub pid: libc::pid_t, -} - -impl From for Context { - fn from(source: fuse::InHeader) -> Self { - Context { - uid: source.uid, - gid: source.gid, - pid: source.pid as i32, - } - } -} - -/// Request extensions -#[derive(Clone, Default, Debug)] -pub struct Extensions { - pub secctx: Option, - pub sup_gid: Option, -} - -/// Additional security context associated with requests. -#[derive(Clone, Debug, Default)] -pub struct SecContext { - /// Name of security context - pub name: CString, - - /// Actual security context - pub secctx: Vec, -} - -pub type ExportTable = Arc>>; - -/// The main trait that connects a file system with a transport. -#[allow(unused_variables)] -pub trait FileSystem { - /// Represents a location in the filesystem tree and can be used to perform operations that act - /// on the metadata of a file/directory (e.g., `getattr` and `setattr`). Can also be used as the - /// starting point for looking up paths in the filesystem tree. An `Inode` may support operating - /// directly on the content of the path that to which it points. `FileSystem` implementations - /// that support this should set the `FsOptions::ZERO_MESSAGE_OPEN` option in the return value - /// of the `init` function. On linux based systems, an `Inode` is equivalent to opening a file - /// or directory with the `libc::O_PATH` flag. - /// - /// # Lookup Count - /// - /// The `FileSystem` implementation is required to keep a "lookup count" for every `Inode`. - /// Every time an `Entry` is returned by a `FileSystem` trait method, this lookup count should - /// increase by 1. The lookup count for an `Inode` decreases when the kernel sends a `forget` - /// request. `Inode`s with a non-zero lookup count may receive requests from the kernel even - /// after calls to `unlink`, `rmdir` or (when overwriting an existing file) `rename`. - /// `FileSystem` implementations must handle such requests properly and it is recommended to - /// defer removal of the `Inode` until the lookup count reaches zero. Calls to `unlink`, `rmdir` - /// or `rename` will be followed closely by `forget` unless the file or directory is open, in - /// which case the kernel issues `forget` only after the `release` or `releasedir` calls. - /// - /// Note that if a file system will be exported over NFS the `Inode`'s lifetime must extend even - /// beyond `forget`. See the `generation` field in `Entry`. - type Inode: From + Into; - - /// Represents a file or directory that is open for reading/writing. - type Handle: From + Into; - - /// Initialize the file system. - /// - /// This method is called when a connection to the FUSE kernel module is first established. The - /// `capable` parameter indicates the features that are supported by the kernel module. The - /// implementation should return the options that it supports. Any options set in the returned - /// `FsOptions` that are not also set in `capable` are silently dropped. - fn init(&self, capable: FsOptions) -> io::Result { - Ok(FsOptions::empty()) - } - - /// Clean up the file system. - /// - /// Called when the filesystem exits. All open `Handle`s should be closed and the lookup count - /// for all open `Inode`s implicitly goes to zero. At this point the connection to the FUSE - /// kernel module may already be gone so implementations should not rely on being able to - /// communicate with the kernel. - fn destroy(&self) {} - - /// Look up a directory entry by name and get its attributes. - /// - /// If this call is successful then the lookup count of the `Inode` associated with the returned - /// `Entry` must be increased by 1. - fn lookup(&self, ctx: Context, parent: Self::Inode, name: &CStr) -> io::Result { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// Forget about an inode. - /// - /// Called when the kernel removes an inode from its internal caches. `count` indicates the - /// amount by which the lookup count for the inode should be decreased. If reducing the lookup - /// count by `count` causes it to go to zero, then the implementation may delete the `Inode`. - fn forget(&self, ctx: Context, inode: Self::Inode, count: u64) {} - - /// Forget about multiple inodes. - /// - /// `requests` is a vector of `(inode, count)` pairs. See the documentation for `forget` for - /// more information. - fn batch_forget(&self, ctx: Context, requests: Vec<(Self::Inode, u64)>) { - for (inode, count) in requests { - self.forget(ctx, inode, count) - } - } - - /// Get attributes for a file / directory. - /// - /// If `handle` is not `None`, then it contains the handle previously returned by the - /// implementation after a call to `open` or `opendir`. However, implementations should still - /// take care to verify the handle if they do not trust the client (e.g., virtio-fs). - /// - /// If writeback caching is enabled (`FsOptions::WRITEBACK_CACHE`), then the kernel module - /// likely has a better idea of the length of the file than the file system (for - /// example, if there was a write that extended the size of the file but has not yet been - /// flushed). In this case, the `st_size` field of the returned struct is ignored. - /// - /// The returned `Duration` indicates how long the returned attributes should be considered - /// valid by the client. If the attributes are only changed via the FUSE kernel module (i.e., - /// the kernel module has exclusive access), then this should be a very large value. - fn getattr( - &self, - ctx: Context, - inode: Self::Inode, - handle: Option, - ) -> io::Result<(bindings::stat64, Duration)> { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// Set attributes for a file / directory. - /// - /// If `handle` is not `None`, then it contains the handle previously returned by the - /// implementation after a call to `open` or `opendir`. However, implementations should still - /// take care to verify the handle if they do not trust the client (e.g., virtio-fs). - /// - /// The `valid` parameter indicates the fields of `attr` that may be considered valid and should - /// be set by the file system. The content of all other fields in `attr` is undefined. - /// - /// If the `FsOptions::HANDLE_KILLPRIV` was set during `init`, then the implementation is - /// expected to reset the setuid and setgid bits if the file size or owner is being changed. - /// - /// This method returns the new attributes after making the modifications requested by the - /// client. The returned `Duration` indicates how long the returned attributes should be - /// considered valid by the client. If the attributes are only changed via the FUSE kernel - /// module (i.e., the kernel module has exclusive access), then this should be a very large - /// value. - fn setattr( - &self, - ctx: Context, - inode: Self::Inode, - attr: bindings::stat64, - handle: Option, - valid: SetattrValid, - ) -> io::Result<(bindings::stat64, Duration)> { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// Read a symbolic link. - fn readlink(&self, ctx: Context, inode: Self::Inode) -> io::Result> { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// Create a symbolic link. - /// - /// The file system must create a symbolic link named `name` in the directory represented by - /// `parent`, which contains the string `linkname`. Returns an `Entry` for the newly created - /// symlink. - /// - /// If this call is successful then the lookup count of the `Inode` associated with the returned - /// `Entry` must be increased by 1. - fn symlink( - &self, - ctx: Context, - linkname: &CStr, - parent: Self::Inode, - name: &CStr, - extensions: Extensions, - ) -> io::Result { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// Create a file node. - /// - /// Create a regular file, character device, block device, fifo, or socket node named `name` in - /// the directory represented by `inode`. Valid values for `mode` and `rdev` are the same as - /// those accepted by the `mknod(2)` system call. Returns an `Entry` for the newly created node. - /// - /// When the `FsOptions::DONT_MASK` feature is set, the file system is responsible for setting - /// the permissions of the created node to `mode & !umask`. - /// - /// If this call is successful then the lookup count of the `Inode` associated with the returned - /// `Entry` must be increased by 1. - #[allow(clippy::too_many_arguments)] - fn mknod( - &self, - ctx: Context, - inode: Self::Inode, - name: &CStr, - mode: u32, - rdev: u32, - umask: u32, - extensions: Extensions, - ) -> io::Result { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// Create a directory. - /// - /// When the `FsOptions::DONT_MASK` feature is set, the file system is responsible for setting - /// the permissions of the created directory to `mode & !umask`. Returns an `Entry` for the - /// newly created directory. - /// - /// If this call is successful then the lookup count of the `Inode` associated with the returned - /// `Entry` must be increased by 1. - fn mkdir( - &self, - ctx: Context, - parent: Self::Inode, - name: &CStr, - mode: u32, - umask: u32, - extensions: Extensions, - ) -> io::Result { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// Remove a file. - /// - /// If the file's inode lookup count is non-zero, then the file system is expected to delay - /// removal of the inode until the lookup count goes to zero. See the documentation of the - /// `forget` function for more information. - fn unlink(&self, ctx: Context, parent: Self::Inode, name: &CStr) -> io::Result<()> { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// Remove a directory. - /// - /// If the directory's inode lookup count is non-zero, then the file system is expected to delay - /// removal of the inode until the lookup count goes to zero. See the documentation of the - /// `forget` function for more information. - fn rmdir(&self, ctx: Context, parent: Self::Inode, name: &CStr) -> io::Result<()> { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// Rename a file / directory. - /// - /// If the destination exists, it should be atomically replaced. If the destination's inode - /// lookup count is non-zero, then the file system is expected to delay removal of the inode - /// until the lookup count goes to zero. See the documentation of the `forget` function for more - /// information. - /// - /// `flags` may be `libc::RENAME_EXCHANGE` or `libc::RENAME_NOREPLACE`. If - /// `libc::RENAME_NOREPLACE` is specified, the implementation must not overwrite `newname` if it - /// exists and must return an error instead. If `libc::RENAME_EXCHANGE` is specified, the - /// implementation must atomically exchange the two files, i.e., both must exist and neither may - /// be deleted. - fn rename( - &self, - ctx: Context, - olddir: Self::Inode, - oldname: &CStr, - newdir: Self::Inode, - newname: &CStr, - flags: u32, - ) -> io::Result<()> { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// Create a hard link. - /// - /// Create a hard link from `inode` to `newname` in the directory represented by `newparent`. - /// - /// If this call is successful then the lookup count of the `Inode` associated with the returned - /// `Entry` must be increased by 1. - fn link( - &self, - ctx: Context, - inode: Self::Inode, - newparent: Self::Inode, - newname: &CStr, - ) -> io::Result { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// Open a file. - /// - /// Open the file associated with `inode` for reading / writing. All values accepted by the - /// `open(2)` system call are valid values for `flags` and must be handled by the file system. - /// However, there are some additional rules: - /// - /// * Creation flags (`libc::O_CREAT`, `libc::O_EXCL`, `libc::O_NOCTTY`) will be filtered out - /// and handled by the kernel. - /// - /// * The file system should check the access modes (`libc::O_RDONLY`, `libc::O_WRONLY`, - /// `libc::O_RDWR`) to determine if the operation is permitted. If the file system was mounted - /// with the `-o default_permissions` mount option, then this check will also be carried out - /// by the kernel before sending the open request. - /// - /// * When writeback caching is enabled (`FsOptions::WRITEBACK_CACHE`) the kernel may send read - /// requests even for files opened with `libc::O_WRONLY`. The file system should be prepared - /// to handle this. - /// - /// * When writeback caching is enabled, the kernel will handle the `libc::O_APPEND` flag. - /// However, this will not work reliably unless the kernel has exclusive access to the file. - /// In this case the file system may either ignore the `libc::O_APPEND` flag or return an - /// error to indicate that reliable `libc::O_APPEND` handling is not available. - /// - /// * When writeback caching is disabled, the file system is expected to properly handle - /// `libc::O_APPEND` and ensure that each write is appended to the end of the file. - /// - /// The file system may choose to return a `Handle` to refer to the newly opened file. The - /// kernel will then use this `Handle` for all operations on the content of the file (`read`, - /// `write`, `flush`, `release`, `fsync`). If the file system does not return a - /// `Handle` then the kernel will use the `Inode` for the file to operate on its contents. In - /// this case the file system may wish to enable the `FsOptions::ZERO_MESSAGE_OPEN` feature if - /// it is supported by the kernel (see below). - /// - /// The returned `OpenOptions` allow the file system to change the way the opened file is - /// handled by the kernel. See the documentation of `OpenOptions` for more information. - /// - /// If `kill_priv` is true then it indicates that the file system is expected to clear the - /// setuid and setgid bits. - /// - /// If the `FsOptions::ZERO_MESSAGE_OPEN` feature is enabled by both the file system - /// implementation and the kernel, then the file system may return an error of `ENOSYS`. This - /// will be interpreted by the kernel as success and future calls to `open` and `release` will - /// be handled by the kernel without being passed on to the file system. - fn open( - &self, - ctx: Context, - inode: Self::Inode, - kill_priv: bool, - flags: u32, - ) -> io::Result<(Option, OpenOptions)> { - // Matches the behavior of libfuse. - Ok((None, OpenOptions::empty())) - } - - /// Create and open a file. - /// - /// If the file does not already exist, the file system should create it with the specified - /// `mode`. When the `FsOptions::DONT_MASK` feature is set, the file system is responsible for - /// setting the permissions of the created file to `mode & !umask`. - /// - /// If the file system returns an `ENOSYS` error, then the kernel will treat this method as - /// unimplemented and all future calls to `create` will be handled by calling the `mknod` and - /// `open` methods instead. - /// - /// If `kill_priv` is true then it indicates that the file system is expected to clear the - /// setuid and setgid bits. - /// - /// See the documentation for the `open` method for more information about opening the file. In - /// addition to the optional `Handle` and the `OpenOptions`, the file system must also return an - /// `Entry` for the file. This increases the lookup count for the `Inode` associated with the - /// file by 1. - #[allow(clippy::too_many_arguments)] - fn create( - &self, - ctx: Context, - parent: Self::Inode, - name: &CStr, - mode: u32, - kill_priv: bool, - flags: u32, - umask: u32, - extensions: Extensions, - ) -> io::Result<(Entry, Option, OpenOptions)> { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// Read data from a file. - /// - /// Returns `size` bytes of data starting from offset `off` from the file associated with - /// `inode` or `handle`. - /// - /// `flags` contains the flags used to open the file. Similarly, `handle` is the `Handle` - /// returned by the file system from the `open` method, if any. If the file system - /// implementation did not return a `Handle` from `open` then the contents of `handle` are - /// undefined. - /// - /// This method should return exactly the number of bytes requested by the kernel, except in the - /// case of error or EOF. Otherwise, the kernel will substitute the rest of the data with - /// zeroes. An exception to this rule is if the file was opened with the "direct I/O" option - /// (`libc::O_DIRECT`), in which case the kernel will forward the return code from this method - /// to the userspace application that made the system call. - #[allow(clippy::too_many_arguments)] - fn read( - &self, - ctx: Context, - inode: Self::Inode, - handle: Self::Handle, - w: W, - size: u32, - offset: u64, - lock_owner: Option, - flags: u32, - ) -> io::Result { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// Write data to a file. - /// - /// Writes `size` bytes of data starting from offset `off` to the file associated with `inode` - /// or `handle`. - /// - /// `flags` contains the flags used to open the file. Similarly, `handle` is the `Handle` - /// returned by the file system from the `open` method, if any. If the file system - /// implementation did not return a `Handle` from `open` then the contents of `handle` are - /// undefined. - /// - /// If `delayed_write` is true then it indicates that this is a write for buffered data. - /// - /// If `kill_priv` is true then it indicates that the file system is expected to clear the - /// setuid and setgid bits. - /// - /// This method should return exactly the number of bytes requested by the kernel, except in the - /// case of error. An exception to this rule is if the file was opened with the "direct I/O" - /// option (`libc::O_DIRECT`), in which case the kernel will forward the return code from this - /// method to the userspace application that made the system call. - #[allow(clippy::too_many_arguments)] - fn write( - &self, - ctx: Context, - inode: Self::Inode, - handle: Self::Handle, - r: R, - size: u32, - offset: u64, - lock_owner: Option, - delayed_write: bool, - kill_priv: bool, - flags: u32, - ) -> io::Result { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// Flush the contents of a file. - /// - /// This method is called on every `close()` of a file descriptor. Since it is possible to - /// duplicate file descriptors there may be many `flush` calls for one call to `open`. - /// - /// File systems should not make any assumptions about when `flush` will be - /// called or even if it will be called at all. - /// - /// `handle` is the `Handle` returned by the file system from the `open` method, if any. If the - /// file system did not return a `Handle` from `open` then the contents of `handle` are - /// undefined. - /// - /// Unlike `fsync`, the file system is not required to flush pending writes. One reason to flush - /// data is if the file system wants to return write errors during close. However, this is not - /// portable because POSIX does not require `close` to wait for delayed I/O to complete. - /// - /// If the `FsOptions::POSIX_LOCKS` feature is enabled, then the file system must remove all - /// locks belonging to `lock_owner`. - /// - /// If this method returns an `ENOSYS` error then the kernel will treat it as success and all - /// subsequent calls to `flush` will be handled by the kernel without being forwarded to the - /// file system. - fn flush( - &self, - ctx: Context, - inode: Self::Inode, - handle: Self::Handle, - lock_owner: u64, - ) -> io::Result<()> { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// Synchronize file contents. - /// - /// File systems must ensure that the file contents have been flushed to disk before returning - /// from this method. If `datasync` is true then only the file data (but not the metadata) needs - /// to be flushed. - /// - /// `handle` is the `Handle` returned by the file system from the `open` method, if any. If the - /// file system did not return a `Handle` from `open` then the contents of - /// `handle` are undefined. - /// - /// If this method returns an `ENOSYS` error then the kernel will treat it as success and all - /// subsequent calls to `fsync` will be handled by the kernel without being forwarded to the - /// file system. - fn fsync( - &self, - ctx: Context, - inode: Self::Inode, - datasync: bool, - handle: Self::Handle, - ) -> io::Result<()> { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// Allocate requested space for file data. - /// - /// If this function returns success, then the file sytem must guarantee that it is possible to - /// write up to `length` bytes of data starting at `offset` without failing due to a lack of - /// free space on the disk. - /// - /// `handle` is the `Handle` returned by the file system from the `open` method, if any. If the - /// file system did not return a `Handle` from `open` then the contents of `handle` are - /// undefined. - /// - /// If this method returns an `ENOSYS` error then the kernel will treat that as a permanent - /// failure: all future calls to `fallocate` will fail with `EOPNOTSUPP` without being forwarded - /// to the file system. - fn fallocate( - &self, - ctx: Context, - inode: Self::Inode, - handle: Self::Handle, - mode: u32, - offset: u64, - length: u64, - ) -> io::Result<()> { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// Release an open file. - /// - /// This method is called when there are no more references to an open file: all file - /// descriptors are closed and all memory mappings are unmapped. - /// - /// For every `open` call there will be exactly one `release` call (unless the file system is - /// force-unmounted). - /// - /// The file system may reply with an error, but error values are not returned to the `close()` - /// or `munmap()` which triggered the release. - /// - /// `handle` is the `Handle` returned by the file system from the `open` method, if any. If the - /// file system did not return a `Handle` from `open` then the contents of - /// `handle` are undefined. - /// - /// If `flush` is `true` then the contents of the file should also be flushed to disk. - #[allow(clippy::too_many_arguments)] - fn release( - &self, - ctx: Context, - inode: Self::Inode, - flags: u32, - handle: Self::Handle, - flush: bool, - flock_release: bool, - lock_owner: Option, - ) -> io::Result<()> { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// Get information about the file system. - fn statfs(&self, ctx: Context, inode: Self::Inode) -> io::Result { - // Safe because we are zero-initializing a struct with only POD fields. - let mut st: bindings::statvfs64 = unsafe { mem::zeroed() }; - - // This matches the behavior of libfuse as it returns these values if the - // filesystem doesn't implement this method. - st.f_namemax = 255; - st.f_bsize = 512; - - Ok(st) - } - - /// Set an extended attribute. - /// - /// If this method fails with an `ENOSYS` error, then the kernel will treat that as a permanent - /// failure. The kernel will return `EOPNOTSUPP` for all future calls to `setxattr` without - /// forwarding them to the file system. - /// - /// Valid values for flags are the same as those accepted by the `setxattr(2)` system call and - /// have the same behavior. - fn setxattr( - &self, - ctx: Context, - inode: Self::Inode, - name: &CStr, - value: &[u8], - flags: u32, - ) -> io::Result<()> { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// Get an extended attribute. - /// - /// If `size` is 0, then the file system should respond with `GetxattrReply::Count` and the - /// number of bytes needed to hold the value. If `size` is large enough to hold the value, then - /// the file system should reply with `GetxattrReply::Value` and the value of the extended - /// attribute. If `size` is not 0 but is also not large enough to hold the value, then the file - /// system should reply with an `ERANGE` error. - /// - /// If this method fails with an `ENOSYS` error, then the kernel will treat that as a permanent - /// failure. The kernel will return `EOPNOTSUPP` for all future calls to `getxattr` without - /// forwarding them to the file system. - fn getxattr( - &self, - ctx: Context, - inode: Self::Inode, - name: &CStr, - size: u32, - ) -> io::Result { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// List extended attribute names. - /// - /// If `size` is 0, then the file system should respond with `ListxattrReply::Count` and the - /// number of bytes needed to hold a `\0` byte separated list of the names of all the extended - /// attributes. If `size` is large enough to hold the `\0` byte separated list of the attribute - /// names, then the file system should reply with `ListxattrReply::Names` and the list. If - /// `size` is not 0 but is also not large enough to hold the list, then the file system should - /// reply with an `ERANGE` error. - /// - /// If this method fails with an `ENOSYS` error, then the kernel will treat that as a permanent - /// failure. The kernel will return `EOPNOTSUPP` for all future calls to `listxattr` without - /// forwarding them to the file system. - fn listxattr(&self, ctx: Context, inode: Self::Inode, size: u32) -> io::Result { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// Remove an extended attribute. - /// - /// If this method fails with an `ENOSYS` error, then the kernel will treat that as a permanent - /// failure. The kernel will return `EOPNOTSUPP` for all future calls to `removexattr` without - /// forwarding them to the file system. - fn removexattr(&self, ctx: Context, inode: Self::Inode, name: &CStr) -> io::Result<()> { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// Open a directory for reading. - /// - /// The file system may choose to return a `Handle` to refer to the newly opened directory. The - /// kernel will then use this `Handle` for all operations on the content of the directory - /// (`readdir`, `readdirplus`, `fsyncdir`, `releasedir`). If the file system does not return a - /// `Handle` then the kernel will use the `Inode` for the directory to operate on its contents. - /// In this case the file system may wish to enable the `FsOptions::ZERO_MESSAGE_OPENDIR` - /// feature if it is supported by the kernel (see below). - /// - /// The returned `OpenOptions` allow the file system to change the way the opened directory is - /// handled by the kernel. See the documentation of `OpenOptions` for more information. - /// - /// If the `FsOptions::ZERO_MESSAGE_OPENDIR` feature is enabled by both the file system - /// implementation and the kernel, then the file system may return an error of `ENOSYS`. This - /// will be interpreted by the kernel as success and future calls to `opendir` and `releasedir` - /// will be handled by the kernel without being passed on to the file system. - fn opendir( - &self, - ctx: Context, - inode: Self::Inode, - flags: u32, - ) -> io::Result<(Option, OpenOptions)> { - // Matches the behavior of libfuse. - Ok((None, OpenOptions::empty())) - } - - /// Read a directory. - /// - /// `handle` is the `Handle` returned by the file system from the `opendir` method, if any. If - /// the file system did not return a `Handle` from `opendir` then the contents of `handle` are - /// undefined. - /// - /// `size` indicates the maximum number of bytes that should be returned by this method. - /// - /// If `offset` is non-zero then it corresponds to one of the `offset` values from a `DirEntry` - /// that was previously returned by a call to `readdir` for the same handle. In this case the - /// file system should skip over the entries before the position defined by the `offset` value. - /// If entries were added or removed while the `Handle` is open then the file system may still - /// include removed entries or skip newly created entries. However, adding or removing entries - /// should never cause the file system to skip over unrelated entries or include an entry more - /// than once. This means that `offset` cannot be a simple index and must include sufficient - /// information to uniquely determine the next entry in the list even when the set of entries is - /// being changed. - /// - /// The file system may return entries for the current directory (".") and parent directory - /// ("..") but is not required to do so. If the file system does not return these entries, then - /// they are implicitly added by the kernel. - /// - /// The lookup count for `Inode`s associated with the returned directory entries is **NOT** - /// affected by this method. - /// - // TODO(chirantan): Change method signature to return `Iterator` rather than using an - // `FnMut` for adding entries. - fn readdir( - &self, - ctx: Context, - inode: Self::Inode, - handle: Self::Handle, - size: u32, - offset: u64, - add_entry: F, - ) -> io::Result<()> - where - F: FnMut(DirEntry) -> io::Result, - { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// Read a directory with entry attributes. - /// - /// Like `readdir` but also includes the attributes for each directory entry. - /// - /// `handle` is the `Handle` returned by the file system from the `opendir` method, if any. If - /// the file system did not return a `Handle` from `opendir` then the contents of `handle` are - /// undefined. - /// - /// `size` indicates the maximum number of bytes that should be returned by this method. - /// - /// Unlike `readdir`, the lookup count for `Inode`s associated with the returned directory - /// entries **IS** affected by this method (since it returns an `Entry` for each `DirEntry`). - /// The count for each `Inode` should be increased by 1. - /// - /// File systems that implement this method should enable the `FsOptions::DO_READDIRPLUS` - /// feature when supported by the kernel. The kernel will not call this method unless that - /// feature is enabled. - /// - /// Additionally, file systems that implement both `readdir` and `readdirplus` should enable the - /// `FsOptions::READDIRPLUS_AUTO` feature to allow the kernel to issue both `readdir` and - /// `readdirplus` requests, depending on how much information is expected to be required. - /// - /// TODO(chirantan): Change method signature to return `Iterator<(DirEntry, Entry)>` rather than - /// using an `FnMut` for adding entries. - fn readdirplus( - &self, - ctx: Context, - inode: Self::Inode, - handle: Self::Handle, - size: u32, - offset: u64, - add_entry: F, - ) -> io::Result<()> - where - F: FnMut(DirEntry, Entry) -> io::Result, - { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// Synchronize the contents of a directory. - /// - /// File systems must ensure that the directory contents have been flushed to disk before - /// returning from this method. If `datasync` is true then only the directory data (but not the - /// metadata) needs to be flushed. - /// - /// `handle` is the `Handle` returned by the file system from the `opendir` method, if any. If - /// the file system did not return a `Handle` from `opendir` then the contents of - /// `handle` are undefined. - /// - /// If this method returns an `ENOSYS` error then the kernel will treat it as success and all - /// subsequent calls to `fsyncdir` will be handled by the kernel without being forwarded to the - /// file system. - fn fsyncdir( - &self, - ctx: Context, - inode: Self::Inode, - datasync: bool, - handle: Self::Handle, - ) -> io::Result<()> { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// Release an open directory. - /// - /// For every `opendir` call there will be exactly one `releasedir` call (unless the file system - /// is force-unmounted). - /// - /// `handle` is the `Handle` returned by the file system from the `opendir` method, if any. If - /// the file system did not return a `Handle` from `opendir` then the contents of `handle` are - /// undefined. - /// - /// `flags` contains used the flags used to open the directory in `opendir`. - fn releasedir( - &self, - ctx: Context, - inode: Self::Inode, - flags: u32, - handle: Self::Handle, - ) -> io::Result<()> { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// Check file access permissions. - /// - /// This method is called when a userspace process in the client makes an `access()` or - /// `chdir()` system call. If the file system was mounted with the `-o default_permissions` - /// mount option, then the kernel will perform these checks itself and this method will not be - /// called. - /// - /// If this method returns an `ENOSYS` error, then the kernel will treat it as a permanent - /// success: all future calls to `access` will return success without being forwarded to the - /// file system. - fn access(&self, ctx: Context, inode: Self::Inode, mask: u32) -> io::Result<()> { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// Reposition read/write file offset. - fn lseek( - &self, - ctx: Context, - inode: Self::Inode, - handle: Self::Handle, - offset: u64, - whence: u32, - ) -> io::Result { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - #[allow(clippy::too_many_arguments)] - fn copyfilerange( - &self, - ctx: Context, - inode_in: Self::Inode, - handle_in: Self::Handle, - offset_in: u64, - inode_out: Self::Inode, - handle_out: Self::Handle, - offset_out: u64, - len: u64, - flags: u64, - ) -> io::Result { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// Setup a mapping so that guest can access files in DAX style. - #[allow(clippy::too_many_arguments)] - fn setupmapping( - &self, - _ctx: Context, - inode: Self::Inode, - handle: Self::Handle, - foffset: u64, - len: u64, - flags: u64, - moffset: u64, - host_shm_base: u64, - shm_size: u64, - #[cfg(target_os = "macos")] map_sender: &Option>, - ) -> io::Result<()> { - Err(io::Error::from_raw_os_error(libc::ENOSYS)) - } - - fn removemapping( - &self, - _ctx: Context, - requests: Vec, - host_shm_base: u64, - shm_size: u64, - #[cfg(target_os = "macos")] map_sender: &Option>, - ) -> io::Result<()> { - Err(io::Error::from_raw_os_error(libc::ENOSYS)) - } - - #[allow(clippy::too_many_arguments)] - fn ioctl( - &self, - ctx: Context, - inode: Self::Inode, - handle: Self::Handle, - flags: u32, - cmd: u32, - arg: u64, - in_size: u32, - out_size: u32, - exit_code: &Arc, - ) -> io::Result> { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// TODO: support this - fn getlk(&self) -> io::Result<()> { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// TODO: support this - fn setlk(&self) -> io::Result<()> { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// TODO: support this - fn setlkw(&self) -> io::Result<()> { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// TODO: support this - fn bmap(&self) -> io::Result<()> { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// TODO: support this - fn poll(&self) -> io::Result<()> { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } - - /// TODO: support this - fn notify_reply(&self) -> io::Result<()> { - Err(io::Error::from_raw_os_error(bindings::LINUX_ENOSYS)) - } -} diff --git a/vendor/krun-devices/src/virtio/fs/fuse.rs b/vendor/krun-devices/src/virtio/fs/fuse.rs deleted file mode 100644 index 442cdad98..000000000 --- a/vendor/krun-devices/src/virtio/fs/fuse.rs +++ /dev/null @@ -1,1379 +0,0 @@ -// Copyright 2019 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -use std::mem; - -use super::bindings; -use bitflags::bitflags; -use vm_memory::ByteValued; - -/// Version number of this interface. -pub const KERNEL_VERSION: u32 = 7; - -/// Minor version number of this interface. -pub const KERNEL_MINOR_VERSION: u32 = 27; - -/// The ID of the inode corresponding to the root directory of the file system. -pub const ROOT_ID: u64 = 1; - -// Bitmasks for `fuse_setattr_in.valid`. -const FATTR_MODE: u32 = 1; -const FATTR_UID: u32 = 2; -const FATTR_GID: u32 = 4; -const FATTR_SIZE: u32 = 8; -const FATTR_ATIME: u32 = 16; -const FATTR_MTIME: u32 = 32; -pub const FATTR_FH: u32 = 64; -const FATTR_ATIME_NOW: u32 = 128; -const FATTR_MTIME_NOW: u32 = 256; -pub const FATTR_LOCKOWNER: u32 = 512; -const FATTR_CTIME: u32 = 1024; -const FATTR_KILL_SUIDGID: u32 = 2048; - -bitflags! { - pub struct SetattrValid: u32 { - const MODE = FATTR_MODE; - const UID = FATTR_UID; - const GID = FATTR_GID; - const SIZE = FATTR_SIZE; - const ATIME = FATTR_ATIME; - const MTIME = FATTR_MTIME; - const ATIME_NOW = FATTR_ATIME_NOW; - const MTIME_NOW = FATTR_MTIME_NOW; - const CTIME = FATTR_CTIME; - const KILL_SUIDGID = FATTR_KILL_SUIDGID; - } -} - -// Flags returned by the OPEN request. - -/// Bypass page cache for this open file. -const FOPEN_DIRECT_IO: u32 = 1; - -/// Don't invalidate the data cache on open. -const FOPEN_KEEP_CACHE: u32 = 2; - -/// The file is not seekable. -const FOPEN_NONSEEKABLE: u32 = 4; - -/// Allow caching this directory. -const FOPEN_CACHE_DIR: u32 = 8; - -bitflags! { - /// Options controlling the behavior of files opened by the server in response - /// to an open or create request. - pub struct OpenOptions: u32 { - const DIRECT_IO = FOPEN_DIRECT_IO; - const KEEP_CACHE = FOPEN_KEEP_CACHE; - const NONSEEKABLE = FOPEN_NONSEEKABLE; - const CACHE_DIR = FOPEN_CACHE_DIR; - } -} - -// INIT request/reply flags. -/// Asynchronous read requests. -const ASYNC_READ: u64 = 1 << 0; - -/// Remote locking for POSIX file locks. -const POSIX_LOCKS: u64 = 1 << 1; - -/// Kernel sends file handle for fstat, etc... (not yet supported). -const FILE_OPS: u64 = 1 << 2; - -/// Handles the O_TRUNC open flag in the filesystem. -const ATOMIC_O_TRUNC: u64 = 1 << 3; - -/// FileSystem handles lookups of "." and "..". -const EXPORT_SUPPORT: u64 = 1 << 4; - -/// FileSystem can handle write size larger than 4kB. -const BIG_WRITES: u64 = 1 << 5; - -/// Don't apply umask to file mode on create operations. -const DONT_MASK: u64 = 1 << 6; - -/// Kernel supports splice write on the device. -const SPLICE_WRITE: u64 = 1 << 7; - -/// Kernel supports splice move on the device. -const SPLICE_MOVE: u64 = 1 << 8; - -/// Kernel supports splice read on the device. -const SPLICE_READ: u64 = 1 << 9; - -/// Remote locking for BSD style file locks. -const FLOCK_LOCKS: u64 = 1 << 10; - -/// Kernel supports ioctl on directories. -const HAS_IOCTL_DIR: u64 = 1 << 11; - -/// Automatically invalidate cached pages. -const AUTO_INVAL_DATA: u64 = 1 << 12; - -/// Do READDIRPLUS (READDIR+LOOKUP in one). -const DO_READDIRPLUS: u64 = 1 << 13; - -/// Adaptive readdirplus. -const READDIRPLUS_AUTO: u64 = 1 << 14; - -/// Asynchronous direct I/O submission. -const ASYNC_DIO: u64 = 1 << 15; - -/// Use writeback cache for buffered writes. -const WRITEBACK_CACHE: u64 = 1 << 16; - -/// Kernel supports zero-message opens. -const NO_OPEN_SUPPORT: u64 = 1 << 17; - -/// Allow parallel lookups and readdir. -const PARALLEL_DIROPS: u64 = 1 << 18; - -/// Fs handles killing suid/sgid/cap on write/chown/trunc. -const HANDLE_KILLPRIV: u64 = 1 << 19; - -/// FileSystem supports posix acls. -const POSIX_ACL: u64 = 1 << 20; - -/// Reading the device after abort returns ECONNABORTED. -const ABORT_ERROR: u64 = 1 << 21; - -/// Init_out.max_pages contains the max number of req pages. -const MAX_PAGES: u64 = 1 << 22; - -/// Cache READLINK responses -const CACHE_SYMLINKS: u64 = 1 << 23; - -/// Kernel supports zero-message opendir -const NO_OPENDIR_SUPPORT: u64 = 1 << 24; - -/// Only invalidate cached pages on explicit request -const EXPLICIT_INVAL_DATA: u64 = 1 << 25; - -/// init_out.map_alignment contains log2(byte alignment) for -/// foffset and moffset fields in struct fuse_setupmapping_out and -/// fuse_removemapping_one -#[allow(dead_code)] -const MAP_ALIGNMENT: u64 = 1 << 26; - -/// Kernel supports auto-mounting directory submounts -const SUBMOUNTS: u64 = 1 << 27; - -/// Fs handles killing suid/sgid/cap on write/chown/trunc (v2). -const HANDLE_KILLPRIV_V2: u64 = 1 << 28; - -/// Server supports extended struct SetxattrIn -const SETXATTR_EXT: u64 = 1 << 29; - -/// Extended fuse_init_in request -const INIT_EXT: u64 = 1 << 30; - -/// Reserved. Do not use. -const INIT_RESERVED: u64 = 1 << 31; - -/// Add security context to create, mkdir, symlink, and mknod -const SECURITY_CTX: u64 = 1 << 32; - -/// Use per inode DAX -const HAS_INODE_DAX: u64 = 1 << 33; - -/// Add supplementary groups info to create, mkdir, symlink -/// and mknod (single group that matches parent) -const CREATE_SUPP_GROUP: u64 = 1 << 34; - -/// We need this for idmapped mounts support -const ALLOW_IDMAP: u64 = 1 << 40; - -bitflags! { - /// A bitfield passed in as a parameter to and returned from the `init` method of the - /// `FileSystem` trait. - pub struct FsOptions: u64 { - /// Indicates that the filesystem supports asynchronous read requests. - /// - /// If this capability is not requested/available, the kernel will ensure that there is at - /// most one pending read request per file-handle at any time, and will attempt to order - /// read requests by increasing offset. - /// - /// This feature is enabled by default when supported by the kernel. - const ASYNC_READ = ASYNC_READ; - - /// Indicates that the filesystem supports "remote" locking. - /// - /// This feature is not enabled by default and should only be set if the filesystem - /// implements the `getlk` and `setlk` methods of the `FileSystem` trait. - const POSIX_LOCKS = POSIX_LOCKS; - - /// Kernel sends file handle for fstat, etc... (not yet supported). - const FILE_OPS = FILE_OPS; - - /// Indicates that the filesystem supports the `O_TRUNC` open flag. If disabled, and an - /// application specifies `O_TRUNC`, fuse first calls `setattr` to truncate the file and - /// then calls `open` with `O_TRUNC` filtered out. - /// - /// This feature is enabled by default when supported by the kernel. - const ATOMIC_O_TRUNC = ATOMIC_O_TRUNC; - - /// Indicates that the filesystem supports lookups of "." and "..". - /// - /// This feature is disabled by default. - const EXPORT_SUPPORT = EXPORT_SUPPORT; - - /// FileSystem can handle write size larger than 4kB. - const BIG_WRITES = BIG_WRITES; - - /// Indicates that the kernel should not apply the umask to the file mode on create - /// operations. - /// - /// This feature is disabled by default. - const DONT_MASK = DONT_MASK; - - /// Indicates that the server should try to use `splice(2)` when writing to the fuse device. - /// This may improve performance. - /// - /// This feature is not currently supported. - const SPLICE_WRITE = SPLICE_WRITE; - - /// Indicates that the server should try to move pages instead of copying when writing to / - /// reading from the fuse device. This may improve performance. - /// - /// This feature is not currently supported. - const SPLICE_MOVE = SPLICE_MOVE; - - /// Indicates that the server should try to use `splice(2)` when reading from the fuse - /// device. This may improve performance. - /// - /// This feature is not currently supported. - const SPLICE_READ = SPLICE_READ; - - /// If set, then calls to `flock` will be emulated using POSIX locks and must - /// then be handled by the filesystem's `setlock()` handler. - /// - /// If not set, `flock` calls will be handled by the FUSE kernel module internally (so any - /// access that does not go through the kernel cannot be taken into account). - /// - /// This feature is disabled by default. - const FLOCK_LOCKS = FLOCK_LOCKS; - - /// Indicates that the filesystem supports ioctl's on directories. - /// - /// This feature is enabled by default when supported by the kernel. - const HAS_IOCTL_DIR = HAS_IOCTL_DIR; - - /// Traditionally, while a file is open the FUSE kernel module only asks the filesystem for - /// an update of the file's attributes when a client attempts to read beyond EOF. This is - /// unsuitable for e.g. network filesystems, where the file contents may change without the - /// kernel knowing about it. - /// - /// If this flag is set, FUSE will check the validity of the attributes on every read. If - /// the attributes are no longer valid (i.e., if the *attribute* timeout has expired) then - /// FUSE will first send another `getattr` request. If the new mtime differs from the - /// previous value, any cached file *contents* will be invalidated as well. - /// - /// This flag should always be set when available. If all file changes go through the - /// kernel, *attribute* validity should be set to a very large number to avoid unnecessary - /// `getattr()` calls. - /// - /// This feature is enabled by default when supported by the kernel. - const AUTO_INVAL_DATA = AUTO_INVAL_DATA; - - /// Indicates that the filesystem supports readdirplus. - /// - /// The feature is not enabled by default and should only be set if the filesystem - /// implements the `readdirplus` method of the `FileSystem` trait. - const DO_READDIRPLUS = DO_READDIRPLUS; - - /// Indicates that the filesystem supports adaptive readdirplus. - /// - /// If `DO_READDIRPLUS` is not set, this flag has no effect. - /// - /// If `DO_READDIRPLUS` is set and this flag is not set, the kernel will always issue - /// `readdirplus()` requests to retrieve directory contents. - /// - /// If `DO_READDIRPLUS` is set and this flag is set, the kernel will issue both `readdir()` - /// and `readdirplus()` requests, depending on how much information is expected to be - /// required. - /// - /// This feature is not enabled by default and should only be set if the file system - /// implements both the `readdir` and `readdirplus` methods of the `FileSystem` trait. - const READDIRPLUS_AUTO = READDIRPLUS_AUTO; - - /// Indicates that the filesystem supports asynchronous direct I/O submission. - /// - /// If this capability is not requested/available, the kernel will ensure that there is at - /// most one pending read and one pending write request per direct I/O file-handle at any - /// time. - /// - /// This feature is enabled by default when supported by the kernel. - const ASYNC_DIO = ASYNC_DIO; - - /// Indicates that writeback caching should be enabled. This means that individual write - /// request may be buffered and merged in the kernel before they are sent to the file - /// system. - /// - /// This feature is disabled by default. - const WRITEBACK_CACHE = WRITEBACK_CACHE; - - /// Indicates support for zero-message opens. If this flag is set in the `capable` parameter - /// of the `init` trait method, then the file system may return `ENOSYS` from the open() handler - /// to indicate success. Further attempts to open files will be handled in the kernel. (If - /// this flag is not set, returning ENOSYS will be treated as an error and signaled to the - /// caller). - /// - /// Setting (or not setting) the field in the `FsOptions` returned from the `init` method - /// has no effect. - const ZERO_MESSAGE_OPEN = NO_OPEN_SUPPORT; - - /// Indicates support for parallel directory operations. If this flag is unset, the FUSE - /// kernel module will ensure that lookup() and readdir() requests are never issued - /// concurrently for the same directory. - /// - /// This feature is enabled by default when supported by the kernel. - const PARALLEL_DIROPS = PARALLEL_DIROPS; - - /// Indicates that the file system is responsible for unsetting setuid and setgid bits when a - /// file is written, truncated, or its owner is changed. - /// - /// This feature is enabled by default when supported by the kernel. - const HANDLE_KILLPRIV = HANDLE_KILLPRIV; - - /// Indicates support for POSIX ACLs. - /// - /// If this feature is enabled, the kernel will cache and have responsibility for enforcing - /// ACLs. ACL will be stored as xattrs and passed to userspace, which is responsible for - /// updating the ACLs in the filesystem, keeping the file mode in sync with the ACL, and - /// ensuring inheritance of default ACLs when new filesystem nodes are created. Note that - /// this requires that the file system is able to parse and interpret the xattr - /// representation of ACLs. - /// - /// Enabling this feature implicitly turns on the `default_permissions` mount option (even - /// if it was not passed to mount(2)). - /// - /// This feature is disabled by default. - const POSIX_ACL = POSIX_ACL; - - /// Indicates that if the connection is gone because of sysfs abort, reading from the device - /// will return -ECONNABORTED. - /// - /// This feature is not currently supported. - const ABORT_ERROR = ABORT_ERROR; - - /// Indicates support for negotiating the maximum number of pages supported. - /// - /// If this feature is enabled, we can tell the kernel the maximum number of pages that we - /// support to transfer in a single request. - /// - /// This feature is enabled by default if supported by the kernel. - const MAX_PAGES = MAX_PAGES; - - /// Indicates that the kernel supports caching READLINK responses. - /// - /// This feature is not currently supported. - const CACHE_SYMLINKS = CACHE_SYMLINKS; - - /// Indicates support for zero-message opens. If this flag is set in the `capable` parameter - /// of the `init` trait method, then the file system may return `ENOSYS` from the opendir() handler - /// to indicate success. Further attempts to open directories will be handled in the kernel. (If - /// this flag is not set, returning ENOSYS will be treated as an error and signaled to the - /// caller). - /// - /// Setting (or not setting) the field in the `FsOptions` returned from the `init` method - /// has no effect. - const ZERO_MESSAGE_OPENDIR = NO_OPENDIR_SUPPORT; - - /// Indicates support for explicit data invalidation. If this feature is enabled, the - /// server is fully responsible for data cache invalidation, and the kernel won't - /// invalidate files data cache on size change and only truncate that cache to new size - /// in case the size decreased. - /// - /// This feature is not currently supported. - const EXPLICIT_INVAL_DATA = EXPLICIT_INVAL_DATA; - - /// Indicates that the kernel supports the FUSE_ATTR_SUBMOUNT flag. - /// - /// Setting (or not setting) this flag in the `FsOptions` returned from the `init` method - /// has no effect. - const SUBMOUNTS = SUBMOUNTS; - - /// Indicates that the filesystem is responsible for clearing - /// security.capability xattr and clearing setuid and setgid bits. Following - /// are the rules. - /// - clear "security.capability" on write, truncate and chown unconditionally - /// - clear suid/sgid if following is true. Note, sgid is cleared only if - /// group executable bit is set. - /// o setattr has FATTR_SIZE and FATTR_KILL_SUIDGID set. - /// o setattr has FATTR_UID or FATTR_GID - /// o open has O_TRUNC and FUSE_OPEN_KILL_SUIDGID - /// o create has O_TRUNC and FUSE_OPEN_KILL_SUIDGID flag set. - /// o write has FUSE_WRITE_KILL_SUIDGID - /// - /// This feature is enabled by default if supported by the kernel. - const HANDLE_KILLPRIV_V2 = HANDLE_KILLPRIV_V2; - - /// Server supports extended struct SetxattrIn - const SETXATTR_EXT = SETXATTR_EXT; - - /// Indicates that fuse_init_in structure has been extended and - /// expect extended struct coming in from kernel. - const INIT_EXT = INIT_EXT; - - /// This bit is reserved. Don't use it. - const INIT_RESERVED = INIT_RESERVED; - - /// Indicates that kernel is capable of sending a security - /// context at file creation time (create, mkdir, symlink - /// and mknod). This is expected to be a SELinux security - /// context as of now. - const SECURITY_CTX = SECURITY_CTX; - - /// Indicates that kernel is capable of understanding - /// per inode dax flag sent in response to getattr - /// request. This will allow server to enable to - /// enable dax on selective files. - const HAS_INODE_DAX = HAS_INODE_DAX; - - /// Add supplementary groups info to create, mkdir, symlink - /// and mknod (single group that matches parent). - const CREATE_SUPP_GROUP = CREATE_SUPP_GROUP; - - /// Indicates if idmapped mounts are allowed for virtiofs. - const ALLOW_IDMAP = ALLOW_IDMAP; - } -} - -// Release flags. -pub const RELEASE_FLUSH: u32 = 1; -pub const RELEASE_FLOCK_UNLOCK: u32 = 2; - -// Getattr flags. -pub const GETATTR_FH: u32 = 1; - -// Lock flags. -pub const LK_FLOCK: u32 = 1; - -// Write flags. - -/// Delayed write from page cache, file handle is guessed. -pub const WRITE_CACHE: u32 = 1; - -/// `lock_owner` field is valid. -pub const WRITE_LOCKOWNER: u32 = 2; - -/// Kill suid and sgid bits -pub const WRITE_KILL_PRIV: u32 = 4; - -// Read flags. -pub const READ_LOCKOWNER: u32 = 2; - -// Ioctl flags. - -/// 32bit compat ioctl on 64bit machine -const IOCTL_COMPAT: u32 = 1; - -/// Not restricted to well-formed ioctls, retry allowed -const IOCTL_UNRESTRICTED: u32 = 2; - -/// Retry with new iovecs -const IOCTL_RETRY: u32 = 4; - -/// 32bit ioctl -const IOCTL_32BIT: u32 = 8; - -/// Is a directory -const IOCTL_DIR: u32 = 16; - -/// x32 compat ioctl on 64bit machine (64bit time_t) -const IOCTL_COMPAT_X32: u32 = 32; - -/// Maximum of in_iovecs + out_iovecs -const IOCTL_MAX_IOV: u32 = 256; - -bitflags! { - pub struct IoctlFlags: u32 { - /// 32bit compat ioctl on 64bit machine - const IOCTL_COMPAT = IOCTL_COMPAT; - - /// Not restricted to well-formed ioctls, retry allowed - const IOCTL_UNRESTRICTED = IOCTL_UNRESTRICTED; - - /// Retry with new iovecs - const IOCTL_RETRY = IOCTL_RETRY; - - /// 32bit ioctl - const IOCTL_32BIT = IOCTL_32BIT; - - /// Is a directory - const IOCTL_DIR = IOCTL_DIR; - - /// x32 compat ioctl on 64bit machine (64bit time_t) - const IOCTL_COMPAT_X32 = IOCTL_COMPAT_X32; - - /// Maximum of in_iovecs + out_iovecs - const IOCTL_MAX_IOV = IOCTL_MAX_IOV; - } -} - -/// Request poll notify. -pub const POLL_SCHEDULE_NOTIFY: u32 = 1; - -/// The read buffer is required to be at least 8k, but may be much larger. -pub const FUSE_MIN_READ_BUFFER: u32 = 8192; - -pub const FUSE_COMPAT_ENTRY_OUT_SIZE: u32 = 120; -pub const FUSE_COMPAT_ATTR_OUT_SIZE: u32 = 96; -pub const FUSE_COMPAT_MKNOD_IN_SIZE: u32 = 8; -pub const FUSE_COMPAT_WRITE_IN_SIZE: u32 = 24; -pub const FUSE_COMPAT_STATFS_SIZE: u32 = 48; -pub const FUSE_COMPAT_INIT_OUT_SIZE: u32 = 8; -pub const FUSE_COMPAT_22_INIT_OUT_SIZE: u32 = 24; - -// Attr.flags flags. - -/// Object is a submount root -pub const ATTR_SUBMOUNT: u32 = 1; - -/// Kill suid and sgid if executable -pub const OPEN_KILL_SUIDGID: u32 = 1; - -// Message definitions follow. It is safe to implement ByteValued for all of these -// because they are POD types. - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct Attr { - pub ino: u64, - pub size: u64, - pub blocks: u64, - pub atime: u64, - pub mtime: u64, - pub ctime: u64, - pub atimensec: u32, - pub mtimensec: u32, - pub ctimensec: u32, - pub mode: u32, - pub nlink: u32, - pub uid: u32, - pub gid: u32, - pub rdev: u32, - pub blksize: u32, - pub flags: u32, -} -unsafe impl ByteValued for Attr {} - -impl From for Attr { - fn from(st: bindings::stat64) -> Attr { - Attr::with_flags(st, 0) - } -} - -impl Attr { - pub fn with_flags(st: bindings::stat64, flags: u32) -> Attr { - Attr { - ino: st.st_ino, - size: st.st_size as u64, - blocks: st.st_blocks as u64, - atime: st.st_atime as u64, - mtime: st.st_mtime as u64, - ctime: st.st_ctime as u64, - atimensec: st.st_atime_nsec as u32, - mtimensec: st.st_mtime_nsec as u32, - ctimensec: st.st_ctime_nsec as u32, - #[cfg(target_os = "linux")] - mode: st.st_mode, - #[cfg(target_os = "macos")] - mode: st.st_mode as u32, - #[cfg(all(target_os = "linux", target_arch = "x86_64"))] - nlink: st.st_nlink as u32, - #[cfg(all( - target_os = "linux", - any(target_arch = "aarch64", target_arch = "riscv64") - ))] - nlink: st.st_nlink, - #[cfg(target_os = "macos")] - nlink: st.st_nlink as u32, - uid: st.st_uid, - gid: st.st_gid, - rdev: st.st_rdev as u32, - blksize: st.st_blksize as u32, - flags, - } - } -} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct Kstatfs { - pub blocks: u64, - pub bfree: u64, - pub bavail: u64, - pub files: u64, - pub ffree: u64, - pub bsize: u32, - pub namelen: u32, - pub frsize: u32, - pub padding: u32, - pub spare: [u32; 6], -} -unsafe impl ByteValued for Kstatfs {} - -#[cfg(target_os = "linux")] -impl From for Kstatfs { - fn from(st: bindings::statvfs64) -> Self { - Kstatfs { - blocks: st.f_blocks, - bfree: st.f_bfree, - bavail: st.f_bavail, - files: st.f_files, - ffree: st.f_ffree, - bsize: st.f_bsize as u32, - namelen: st.f_namemax as u32, - frsize: st.f_frsize as u32, - ..Default::default() - } - } -} -#[cfg(target_os = "macos")] -impl From for Kstatfs { - fn from(st: bindings::statvfs64) -> Self { - Kstatfs { - blocks: st.f_blocks as u64, - bfree: st.f_bfree as u64, - bavail: st.f_bavail as u64, - files: st.f_files as u64, - ffree: st.f_ffree as u64, - bsize: st.f_bsize as u32, - namelen: st.f_namemax as u32, - frsize: st.f_frsize as u32, - ..Default::default() - } - } -} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct FileLock { - pub start: u64, - pub end: u64, - pub type_: u32, - pub pid: u32, /* tgid */ -} -unsafe impl ByteValued for FileLock {} - -#[repr(u32)] -#[derive(Debug, Copy, Clone)] -pub enum Opcode { - Lookup = 1, - Forget = 2, /* No Reply */ - Getattr = 3, - Setattr = 4, - Readlink = 5, - Symlink = 6, - Mknod = 8, - Mkdir = 9, - Unlink = 10, - Rmdir = 11, - Rename = 12, - Link = 13, - Open = 14, - Read = 15, - Write = 16, - Statfs = 17, - Release = 18, - Fsync = 20, - Setxattr = 21, - Getxattr = 22, - Listxattr = 23, - Removexattr = 24, - Flush = 25, - Init = 26, - Opendir = 27, - Readdir = 28, - Releasedir = 29, - Fsyncdir = 30, - Getlk = 31, - Setlk = 32, - Setlkw = 33, - Access = 34, - Create = 35, - Interrupt = 36, - Bmap = 37, - Destroy = 38, - Ioctl = 39, - Poll = 40, - NotifyReply = 41, - BatchForget = 42, - Fallocate = 43, - Readdirplus = 44, - Rename2 = 45, - Lseek = 46, - CopyFileRange = 47, - SetupMapping = 48, - RemoveMapping = 49, -} - -#[repr(u32)] -#[derive(Debug, Copy, Clone)] -pub enum NotifyOpcode { - Poll = 1, - InvalInode = 2, - InvalEntry = 3, - Store = 4, - Retrieve = 5, - Delete = 6, - CodeMax = 7, -} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct EntryOut { - pub nodeid: u64, /* Inode ID */ - pub generation: u64, /* Inode generation: nodeid:gen must be unique for the fs's lifetime */ - pub entry_valid: u64, /* Cache timeout for the name */ - pub attr_valid: u64, /* Cache timeout for the attributes */ - pub entry_valid_nsec: u32, - pub attr_valid_nsec: u32, - pub attr: Attr, -} -unsafe impl ByteValued for EntryOut {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct ForgetIn { - pub nlookup: u64, -} -unsafe impl ByteValued for ForgetIn {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct ForgetOne { - pub nodeid: u64, - pub nlookup: u64, -} -unsafe impl ByteValued for ForgetOne {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct BatchForgetIn { - pub count: u32, - pub dummy: u32, -} -unsafe impl ByteValued for BatchForgetIn {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct GetattrIn { - pub flags: u32, - pub dummy: u32, - pub fh: u64, -} -unsafe impl ByteValued for GetattrIn {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct AttrOut { - pub attr_valid: u64, /* Cache timeout for the attributes */ - pub attr_valid_nsec: u32, - pub dummy: u32, - pub attr: Attr, -} -unsafe impl ByteValued for AttrOut {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct MknodIn { - pub mode: u32, - pub rdev: u32, - pub umask: u32, - pub padding: u32, -} -unsafe impl ByteValued for MknodIn {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct MkdirIn { - pub mode: u32, - pub umask: u32, -} -unsafe impl ByteValued for MkdirIn {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct RenameIn { - pub newdir: u64, -} -unsafe impl ByteValued for RenameIn {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct Rename2In { - pub newdir: u64, - pub flags: u32, - pub padding: u32, -} -unsafe impl ByteValued for Rename2In {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct LinkIn { - pub oldnodeid: u64, -} -unsafe impl ByteValued for LinkIn {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct SetattrIn { - pub valid: u32, - pub padding: u32, - pub fh: u64, - pub size: u64, - pub lock_owner: u64, - pub atime: u64, - pub mtime: u64, - pub ctime: u64, - pub atimensec: u32, - pub mtimensec: u32, - pub ctimensec: u32, - pub mode: u32, - pub unused4: u32, - pub uid: u32, - pub gid: u32, - pub unused5: u32, -} -unsafe impl ByteValued for SetattrIn {} - -impl From for bindings::stat64 { - #[allow(clippy::useless_conversion)] - fn from(sai: SetattrIn) -> bindings::stat64 { - let mut out: bindings::stat64 = unsafe { mem::zeroed() }; - // We need this conversion on macOS. - out.st_mode = sai.mode.try_into().unwrap(); - out.st_uid = sai.uid; - out.st_gid = sai.gid; - out.st_size = sai.size as i64; - out.st_atime = sai.atime as i64; - out.st_mtime = sai.mtime as i64; - out.st_ctime = sai.ctime as i64; - out.st_atime_nsec = sai.atimensec.into(); - out.st_mtime_nsec = sai.mtimensec.into(); - out.st_ctime_nsec = sai.ctimensec.into(); - - out - } -} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct OpenIn { - pub flags: u32, - pub open_flags: u32, -} -unsafe impl ByteValued for OpenIn {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct CreateIn { - pub flags: u32, - pub mode: u32, - pub umask: u32, - pub open_flags: u32, -} -unsafe impl ByteValued for CreateIn {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct OpenOut { - pub fh: u64, - pub open_flags: u32, - pub padding: u32, -} -unsafe impl ByteValued for OpenOut {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct ReleaseIn { - pub fh: u64, - pub flags: u32, - pub release_flags: u32, - pub lock_owner: u64, -} -unsafe impl ByteValued for ReleaseIn {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct FlushIn { - pub fh: u64, - pub unused: u32, - pub padding: u32, - pub lock_owner: u64, -} -unsafe impl ByteValued for FlushIn {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct ReadIn { - pub fh: u64, - pub offset: u64, - pub size: u32, - pub read_flags: u32, - pub lock_owner: u64, - pub flags: u32, - pub padding: u32, -} -unsafe impl ByteValued for ReadIn {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct WriteIn { - pub fh: u64, - pub offset: u64, - pub size: u32, - pub write_flags: u32, - pub lock_owner: u64, - pub flags: u32, - pub padding: u32, -} -unsafe impl ByteValued for WriteIn {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct WriteOut { - pub size: u32, - pub padding: u32, -} -unsafe impl ByteValued for WriteOut {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct StatfsOut { - pub st: Kstatfs, -} -unsafe impl ByteValued for StatfsOut {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct FsyncIn { - pub fh: u64, - pub fsync_flags: u32, - pub padding: u32, -} -unsafe impl ByteValued for FsyncIn {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct SetxattrIn { - pub size: u32, - pub flags: u32, -} -unsafe impl ByteValued for SetxattrIn {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct GetxattrIn { - pub size: u32, - pub padding: u32, -} -unsafe impl ByteValued for GetxattrIn {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct GetxattrOut { - pub size: u32, - pub padding: u32, -} -unsafe impl ByteValued for GetxattrOut {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct LkIn { - pub fh: u64, - pub owner: u64, - pub lk: FileLock, - pub lk_flags: u32, - pub padding: u32, -} -unsafe impl ByteValued for LkIn {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct LkOut { - pub lk: FileLock, -} -unsafe impl ByteValued for LkOut {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct AccessIn { - pub mask: u32, - pub padding: u32, -} -unsafe impl ByteValued for AccessIn {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct InitInCompat { - pub major: u32, - pub minor: u32, - pub max_readahead: u32, - pub flags: u32, -} -unsafe impl ByteValued for InitInCompat {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct InitInExt { - pub flags2: u32, - pub unused: [u32; 11], -} -unsafe impl ByteValued for InitInExt {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct InitOut { - pub major: u32, - pub minor: u32, - pub max_readahead: u32, - pub flags: u32, - pub max_background: u16, - pub congestion_threshold: u16, - pub max_write: u32, - pub time_gran: u32, - pub max_pages: u16, - pub map_alignment: u16, - pub flags2: u32, - pub unused: [u32; 7], -} -unsafe impl ByteValued for InitOut {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct InterruptIn { - pub unique: u64, -} -unsafe impl ByteValued for InterruptIn {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct BmapIn { - pub block: u64, - pub blocksize: u32, - pub padding: u32, -} -unsafe impl ByteValued for BmapIn {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct BmapOut { - pub block: u64, -} -unsafe impl ByteValued for BmapOut {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct IoctlIn { - pub fh: u64, - pub flags: u32, - pub cmd: u32, - pub arg: u64, - pub in_size: u32, - pub out_size: u32, -} -unsafe impl ByteValued for IoctlIn {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct IoctlIovec { - pub base: u64, - pub len: u64, -} -unsafe impl ByteValued for IoctlIovec {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct IoctlOut { - pub result: i32, - pub flags: u32, - pub in_iovs: u32, - pub out_iovs: u32, -} -unsafe impl ByteValued for IoctlOut {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct PollIn { - pub fh: u64, - pub kh: u64, - pub flags: u32, - pub events: u32, -} -unsafe impl ByteValued for PollIn {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct PollOut { - pub revents: u32, - pub padding: u32, -} -unsafe impl ByteValued for PollOut {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct NotifyPollWakeupOut { - pub kh: u64, -} -unsafe impl ByteValued for NotifyPollWakeupOut {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct FallocateIn { - pub fh: u64, - pub offset: u64, - pub length: u64, - pub mode: u32, - pub padding: u32, -} -unsafe impl ByteValued for FallocateIn {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct InHeader { - pub len: u32, - pub opcode: u32, - pub unique: u64, - pub nodeid: u64, - pub uid: u32, - pub gid: u32, - pub pid: u32, - pub padding: u32, -} -unsafe impl ByteValued for InHeader {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct OutHeader { - pub len: u32, - pub error: i32, - pub unique: u64, -} -unsafe impl ByteValued for OutHeader {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct Dirent { - pub ino: u64, - pub off: u64, - pub namelen: u32, - pub type_: u32, - // char name[]; -} -unsafe impl ByteValued for Dirent {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct Direntplus { - pub entry_out: EntryOut, - pub dirent: Dirent, -} -unsafe impl ByteValued for Direntplus {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct NotifyInvalInodeOut { - pub ino: u64, - pub off: i64, - pub len: i64, -} -unsafe impl ByteValued for NotifyInvalInodeOut {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct NotifyInvalEntryOut { - pub parent: u64, - pub namelen: u32, - pub padding: u32, -} -unsafe impl ByteValued for NotifyInvalEntryOut {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct NotifyDeleteOut { - pub parent: u64, - pub child: u64, - pub namelen: u32, - pub padding: u32, -} -unsafe impl ByteValued for NotifyDeleteOut {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct NotifyStoreOut { - pub nodeid: u64, - pub offset: u64, - pub size: u32, - pub padding: u32, -} -unsafe impl ByteValued for NotifyStoreOut {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct Notify_Retrieve_Out { - pub notify_unique: u64, - pub nodeid: u64, - pub offset: u64, - pub size: u32, - pub padding: u32, -} -unsafe impl ByteValued for Notify_Retrieve_Out {} - -/* Matches the size of fuse_write_in */ -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct NotifyRetrieveIn { - pub dummy1: u64, - pub offset: u64, - pub size: u32, - pub dummy2: u32, - pub dummy3: u64, - pub dummy4: u64, -} -unsafe impl ByteValued for NotifyRetrieveIn {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct LseekIn { - pub fh: u64, - pub offset: u64, - pub whence: u32, - pub padding: u32, -} -unsafe impl ByteValued for LseekIn {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct LseekOut { - pub offset: u64, -} -unsafe impl ByteValued for LseekOut {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct CopyfilerangeIn { - pub fh_in: u64, - pub off_in: u64, - pub nodeid_out: u64, - pub fh_out: u64, - pub off_out: u64, - pub len: u64, - pub flags: u64, -} -unsafe impl ByteValued for CopyfilerangeIn {} - -bitflags! { - pub struct SetupmappingFlags: u64 { - const WRITE = 0x1; - const READ = 0x2; - } -} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct SetupmappingIn { - pub fh: u64, - pub foffset: u64, - pub len: u64, - pub flags: u64, - pub moffset: u64, -} - -unsafe impl ByteValued for SetupmappingIn {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct RemovemappingIn { - pub count: u32, -} - -unsafe impl ByteValued for RemovemappingIn {} - -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct RemovemappingOne { - pub moffset: u64, - pub len: u64, -} - -unsafe impl ByteValued for RemovemappingOne {} - -/// Extension header -/// `size`: total size of this extension including this header -/// `ext_type`: type of extension -/// This is made compatible with `SecctxHeader` by using type values > `FUSE_MAX_NR_SECCTX` -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct ExtHeader { - pub size: u32, - pub ext_type: u32, -} - -/// Extension types -/// Types `0..MAX_NR_SECCTX` are reserved for `SecCtx` extension for backward compatibility. -const MAX_NR_SECCTX: u32 = 31; // Maximum value of `SecctxHeader::nr_secctx` -const EXT_SUP_GROUPS: u32 = 32; - -unsafe impl ByteValued for ExtHeader {} - -/// Extension type -#[derive(Debug, Copy, Clone)] -pub enum ExtType { - /// Security contexts - SecCtx(u32), - /// `Supplementary groups - SupGroups, -} - -impl TryFrom for ExtType { - type Error = (); - - fn try_from(value: u32) -> Result { - match value { - v if v <= MAX_NR_SECCTX => Ok(Self::SecCtx(value)), - v if v == EXT_SUP_GROUPS => Ok(Self::SupGroups), - _ => Err(()), - } - } -} - -/// For each security context, send `Secctx` with size of security context -/// `Secctx` will be followed by security context name and this in turn -/// will be followed by actual context label. -/// `Secctx`, name, context -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct Secctx { - pub size: u32, - pub padding: u32, -} - -unsafe impl ByteValued for Secctx {} - -/// Contains the information about how many `Secctx` structures are being -/// sent and what's the total size of all security contexts (including -/// size of `SecctxHeader`). -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct SecctxHeader { - pub size: u32, - pub nr_secctx: u32, -} - -unsafe impl ByteValued for SecctxHeader {} - -/// Supplementary groups extension -/// `nr_groups`: number of supplementary groups -/// `groups`: flexible array of group IDs -#[repr(C)] -#[derive(Debug, Default, Copy, Clone)] -pub struct SuppGroups { - pub nr_groups: u32, - // uint32_t groups[]; -} - -unsafe impl ByteValued for SuppGroups {} diff --git a/vendor/krun-devices/src/virtio/fs/inode_alloc.rs b/vendor/krun-devices/src/virtio/fs/inode_alloc.rs deleted file mode 100644 index 1919b1406..000000000 --- a/vendor/krun-devices/src/virtio/fs/inode_alloc.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::sync::atomic::{AtomicU64, Ordering}; - -use super::fuse; - -/// Allocates unique FUSE inode numbers. -/// -/// FUSE inode numbers are opaque identifiers with two reserved values: -/// - `0` — invalid / negative-entry cache sentinel (never allocated) -/// - `1` (`ROOT_ID`) — the root directory of the filesystem -/// -/// All other numbers are allocated sequentially starting from `ROOT_ID + 1`. -/// The allocator is `Send + Sync` and safe to share across threads. -pub struct InodeAllocator { - next: AtomicU64, -} - -impl InodeAllocator { - pub fn new() -> Self { - Self { - next: AtomicU64::new(fuse::ROOT_ID + 1), - } - } - - /// Allocate the next inode number. Each call returns a unique value. - pub fn next(&self) -> u64 { - self.next.fetch_add(1, Ordering::Relaxed) - } -} diff --git a/vendor/krun-devices/src/virtio/fs/linux/fs_utils.rs b/vendor/krun-devices/src/virtio/fs/linux/fs_utils.rs deleted file mode 100644 index 2a15d20d6..000000000 --- a/vendor/krun-devices/src/virtio/fs/linux/fs_utils.rs +++ /dev/null @@ -1,9 +0,0 @@ -use std::io; - -pub fn ebadf() -> io::Error { - io::Error::from_raw_os_error(libc::EBADF) -} - -pub fn einval() -> io::Error { - io::Error::from_raw_os_error(libc::EINVAL) -} diff --git a/vendor/krun-devices/src/virtio/fs/linux/mod.rs b/vendor/krun-devices/src/virtio/fs/linux/mod.rs deleted file mode 100644 index b8edbc7f9..000000000 --- a/vendor/krun-devices/src/virtio/fs/linux/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod fs_utils; -pub mod passthrough; diff --git a/vendor/krun-devices/src/virtio/fs/linux/passthrough.rs b/vendor/krun-devices/src/virtio/fs/linux/passthrough.rs deleted file mode 100644 index 009121cff..000000000 --- a/vendor/krun-devices/src/virtio/fs/linux/passthrough.rs +++ /dev/null @@ -1,2304 +0,0 @@ -// Copyright 2019 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -use std::collections::btree_map; -use std::collections::BTreeMap; -use std::convert::TryInto; -use std::ffi::{CStr, CString}; -use std::fs::File; -use std::io; -use std::mem::{self, size_of, MaybeUninit}; -use std::os::unix::io::{AsRawFd, FromRawFd, RawFd}; -use std::str::FromStr; -use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU64, Ordering}; -use std::sync::{Arc, RwLock}; -use std::time::Duration; - -use caps::{has_cap, CapSet, Capability}; -use nix::request_code_read; - -use vm_memory::ByteValued; - -use super::super::filesystem::{ - Context, DirEntry, Entry, ExportTable, Extensions, FileSystem, FsOptions, GetxattrReply, - ListxattrReply, OpenOptions, SetattrValid, ZeroCopyReader, ZeroCopyWriter, -}; -use super::super::fuse; -use super::super::inode_alloc::InodeAllocator; -use super::super::multikey::MultikeyBTreeMap; - -const CURRENT_DIR_CSTR: &[u8] = b".\0"; -const PARENT_DIR_CSTR: &[u8] = b"..\0"; -const EMPTY_CSTR: &[u8] = b"\0"; -const PROC_CSTR: &[u8] = b"/proc/self/fd\0"; - -type Inode = u64; -type Handle = u64; - -#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq)] -struct InodeAltKey { - ino: libc::ino64_t, - dev: libc::dev_t, - mnt_id: u64, -} - -struct InodeData { - inode: Inode, - // Most of these aren't actually files but ¯\_(ツ)_/¯. - file: File, - dev: u64, - mnt_id: u64, - refcount: AtomicU64, -} - -struct HandleData { - inode: Inode, - file: RwLock, - exported: AtomicBool, -} - -#[repr(C, packed)] -#[derive(Clone, Copy, Debug, Default)] -struct LinuxDirent64 { - d_ino: libc::ino64_t, - d_off: libc::off64_t, - d_reclen: libc::c_ushort, - d_ty: libc::c_uchar, -} -unsafe impl ByteValued for LinuxDirent64 {} - -macro_rules! scoped_cred { - ($name:ident, $ty:ty, $syscall_nr:expr_2021, $get_current:expr_2021) => { - #[derive(Debug)] - struct $name { - old: $ty, - } - - impl $name { - // Changes the effective uid/gid of the current thread to `val`. Changes - // the thread's credentials back to the previous value when the returned - // struct is dropped. - fn new(val: $ty) -> io::Result> { - // We want credential changes to be per-thread because otherwise - // we might interfere with operations being carried out on other - // threads with different uids/gids. However, posix requires that - // all threads in a process share the same credentials. To do this - // libc uses signals to ensure that when one thread changes its - // credentials the other threads do the same thing. - // - // So instead we invoke the syscall directly in order to get around - // this limitation. Another option is to use the setfsuid and - // setfsgid systems calls. However since those calls have no way to - // return an error, it's preferable to do this instead. - - // Remember the current effective id so Drop can restore it. - // Restoring a hardcoded 0 instead is wrong when the server - // runs as an unprivileged user granted CAP_SETUID/CAP_SETGID: - // the first Drop parks the thread at euid 0, the next switch - // to a non-zero uid then clears the thread's effective - // capability set (see capabilities(7), "Effect of user ID - // changes on capabilities"), and the restore after that fails - // with EPERM -- leaving the worker thread stuck with the guest - // uid's credentials for every subsequent request. - let old = unsafe { $get_current() } as $ty; - - // This call is safe because it doesn't modify any memory and we - // check the return value. - let res = unsafe { libc::syscall($syscall_nr, -1, val, -1) }; - if res == 0 { - Ok(Some($name { old })) - } else { - Err(io::Error::last_os_error()) - } - } - } - - impl Drop for $name { - fn drop(&mut self) { - let res = unsafe { libc::syscall($syscall_nr, -1, self.old, -1) }; - if res < 0 { - error!( - "failed to restore credentials: {}", - io::Error::last_os_error(), - ); - } - } - } - }; -} -scoped_cred!(ScopedUid, libc::uid_t, libc::SYS_setresuid, libc::geteuid); -scoped_cred!(ScopedGid, libc::gid_t, libc::SYS_setresgid, libc::getegid); - -#[must_use] -pub struct ScopedCaps { - cap: Capability, -} - -impl ScopedCaps { - fn new(cap: Capability) -> io::Result> { - if has_cap(None, CapSet::Effective, cap).map_err(|e| { - error!("couldn't check {cap:?} capability: {e}"); - einval() - })? { - caps::drop(None, CapSet::Effective, cap).map_err(|e| { - error!("couldn't drop {cap:?} capability: {e}"); - einval() - })?; - Ok(Some(Self { cap })) - } else { - Ok(None) - } - } -} - -impl Drop for ScopedCaps { - fn drop(&mut self) { - caps::raise(None, CapSet::Effective, self.cap) - .unwrap_or_else(|e| panic!("couldn't restore {:?} capability: {e}", self.cap)); - } -} - -pub fn drop_effective_cap(cap: Capability) -> io::Result> { - ScopedCaps::new(cap) -} - -fn ebadf() -> io::Error { - io::Error::from_raw_os_error(libc::EBADF) -} - -fn einval() -> io::Error { - io::Error::from_raw_os_error(libc::EINVAL) -} - -fn stat(f: &File) -> io::Result { - let mut st = MaybeUninit::::zeroed(); - - // Safe because this is a constant value and a valid C string. - let pathname = unsafe { CStr::from_bytes_with_nul_unchecked(EMPTY_CSTR) }; - - // Safe because the kernel will only write data in `st` and we check the return - // value. - let res = unsafe { - libc::fstatat64( - f.as_raw_fd(), - pathname.as_ptr(), - st.as_mut_ptr(), - libc::AT_EMPTY_PATH | libc::AT_SYMLINK_NOFOLLOW, - ) - }; - if res >= 0 { - // Safe because the kernel guarantees that the struct is now fully initialized. - Ok(unsafe { st.assume_init() }) - } else { - Err(io::Error::last_os_error()) - } -} - -fn statx(f: &File) -> io::Result<(libc::stat64, u64)> { - let mut stx = MaybeUninit::::zeroed(); - - // Safe because this is a constant value and a valid C string. - let pathname = unsafe { CStr::from_bytes_with_nul_unchecked(EMPTY_CSTR) }; - - // Safe because the kernel will only write data in `st` and we check the return - // value. - let res = unsafe { - libc::statx( - f.as_raw_fd(), - pathname.as_ptr(), - libc::AT_EMPTY_PATH | libc::AT_SYMLINK_NOFOLLOW, - libc::STATX_BASIC_STATS | libc::STATX_MNT_ID, - stx.as_mut_ptr(), - ) - }; - if res >= 0 { - // Safe because the kernel guarantees that the struct is now fully initialized. - let stx = unsafe { stx.assume_init() }; - - // Unfortunately, we cannot use an initializer to create the stat64 object, - // because it may contain padding and reserved fields (depending on the - // architecture), and it does not implement the Default trait. - // So we take a zeroed struct and set what we can. (Zero in all fields is - // wrong, but safe.) - let mut st = unsafe { MaybeUninit::::zeroed().assume_init() }; - - st.st_dev = libc::makedev(stx.stx_dev_major, stx.stx_dev_minor); - st.st_ino = stx.stx_ino; - st.st_mode = stx.stx_mode as _; - st.st_nlink = stx.stx_nlink as _; - st.st_uid = stx.stx_uid; - st.st_gid = stx.stx_gid; - st.st_rdev = libc::makedev(stx.stx_rdev_major, stx.stx_rdev_minor); - st.st_size = stx.stx_size as _; - st.st_blksize = stx.stx_blksize as _; - st.st_blocks = stx.stx_blocks as _; - st.st_atime = stx.stx_atime.tv_sec; - st.st_atime_nsec = stx.stx_atime.tv_nsec as _; - st.st_mtime = stx.stx_mtime.tv_sec; - st.st_mtime_nsec = stx.stx_mtime.tv_nsec as _; - st.st_ctime = stx.stx_ctime.tv_sec; - st.st_ctime_nsec = stx.stx_ctime.tv_nsec as _; - Ok((st, stx.stx_mnt_id)) - } else { - Err(io::Error::last_os_error()) - } -} - -/// The caching policy that the file system should report to the FUSE client. By default the FUSE -/// protocol uses close-to-open consistency. This means that any cached contents of the file are -/// invalidated the next time that file is opened. -#[derive(Default, Debug, Clone)] -pub enum CachePolicy { - /// The client should never cache file data and all I/O should be directly forwarded to the - /// server. This policy must be selected when file contents may change without the knowledge of - /// the FUSE client (i.e., the file system does not have exclusive access to the directory). - Never, - - /// The client is free to choose when and how to cache file data. This is the default policy and - /// uses close-to-open consistency as described in the enum documentation. - #[default] - Auto, - - /// The client should always cache file data. This means that the FUSE client will not - /// invalidate any cached data that was returned by the file system the last time the file was - /// opened. This policy should only be selected when the file system has exclusive access to the - /// directory. - Always, -} - -impl FromStr for CachePolicy { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - match s { - "never" | "Never" | "NEVER" => Ok(CachePolicy::Never), - "auto" | "Auto" | "AUTO" => Ok(CachePolicy::Auto), - "always" | "Always" | "ALWAYS" => Ok(CachePolicy::Always), - _ => Err("invalid cache policy"), - } - } -} - -/// The permission semantics to be emulated by this file system personality. -#[derive(Debug, Default, Clone, Copy)] -pub enum PermissionSemantics { - /// Be as close as possible to the common semantics of Linux file systems. - #[default] - LinuxComplete, - - /// As `LinuxComplete`, with the following simplifications: - /// - Extended attributes are not supported. - /// - Idmaps are not supported. - /// - Ownership bits are ignored, always returning the uid/gid from the process - /// requesting the operation within the guest (obtained from `Context`). - /// - Permissions bits are stored in the host, not as extended attributes. - LinuxSimplified, -} - -impl TryFrom for PermissionSemantics { - type Error = (); - - fn try_from(semantics: u32) -> Result { - match semantics { - 0 => Ok(PermissionSemantics::LinuxComplete), - 1 => Ok(PermissionSemantics::LinuxSimplified), - _ => Err(()), - } - } -} - -/// Options that configure the behavior of the file system. -#[derive(Debug, Clone)] -pub struct Config { - /// How long the FUSE client should consider directory entries to be valid. If the contents of a - /// directory can only be modified by the FUSE client (i.e., the file system has exclusive - /// access), then this should be a large value. - /// - /// The default value for this option is 5 seconds. - pub entry_timeout: Duration, - - /// How long the FUSE client should consider file and directory attributes to be valid. If the - /// attributes of a file or directory can only be modified by the FUSE client (i.e., the file - /// system has exclusive access), then this should be set to a large value. - /// - /// The default value for this option is 5 seconds. - pub attr_timeout: Duration, - - /// The caching policy the file system should use. See the documentation of `CachePolicy` for - /// more details. - pub cache_policy: CachePolicy, - - /// Whether the file system should enabled writeback caching. This can improve performance as it - /// allows the FUSE client to cache and coalesce multiple writes before sending them to the file - /// system. However, enabling this option can increase the risk of data corruption if the file - /// contents can change without the knowledge of the FUSE client (i.e., the server does **NOT** - /// have exclusive access). Additionally, the file system should have read access to all files - /// in the directory it is serving as the FUSE client may send read requests even for files - /// opened with `O_WRONLY`. - /// - /// Therefore callers should only enable this option when they can guarantee that: 1) the file - /// system has exclusive access to the directory and 2) the file system has read permissions for - /// all files in that directory. - /// - /// The default value for this option is `false`. - pub writeback: bool, - - /// The path of the root directory. - /// - /// The default is `/`. - pub root_dir: String, - - /// Whether the file system should support Extended Attributes (xattr). Enabling this feature may - /// have a significant impact on performance, especially on write parallelism. This is the result - /// of FUSE attempting to remove the special file privileges after each write request. - /// - /// The default value for this options is `false`. - pub xattr: bool, - - /// Optional file descriptor for /proc/self/fd. Callers can obtain a file descriptor and pass it - /// here, so there's no need to open it in PassthroughFs::new(). This is specially useful for - /// sandboxing. - /// - /// The default is `None`. - pub proc_sfd_rawfd: Option, - - /// ID of this filesystem to uniquely identify exports. - pub export_fsid: u64, - - /// Table of exported FDs to share with other subsystems. - pub export_table: Option, - - /// The permission semantics to be emulated. See the documentation for `PermissionSemantics` for - /// more details. - pub semantics: PermissionSemantics, -} - -impl Default for Config { - fn default() -> Self { - Config { - entry_timeout: Duration::from_secs(5), - attr_timeout: Duration::from_secs(5), - cache_policy: Default::default(), - writeback: false, - root_dir: String::from("/"), - xattr: true, - proc_sfd_rawfd: None, - export_fsid: 0, - export_table: None, - semantics: PermissionSemantics::LinuxComplete, - } - } -} - -/// A file system that simply "passes through" all requests it receives to the underlying file -/// system. To keep the implementation simple it servers the contents of its root directory. Users -/// that wish to serve only a specific directory should set up the environment so that that -/// directory ends up as the root of the file system process. One way to accomplish this is via a -/// combination of mount namespaces and the pivot_root system call. -pub struct PassthroughFs { - // File descriptors for various points in the file system tree. These fds are always opened with - // the `O_PATH` option so they cannot be used for reading or writing any data. See the - // documentation of the `O_PATH` flag in `open(2)` for more details on what one can and cannot - // do with an fd opened with this flag. - inodes: RwLock>>, - inode_alloc: Arc, - - // File descriptors for open files and directories. Unlike the fds in `inodes`, these _can_ be - // used for reading and writing data. - handles: RwLock>>, - next_handle: AtomicU64, - - // File descriptor pointing to the `/proc/self/fd` directory. This is used to convert an fd from - // `inodes` into one that can go into `handles`. This is accomplished by reading the - // `/proc/self/fd/{}` symlink. We keep an open fd here in case the file system tree that we are - // meant to be serving doesn't have access to `/proc/self/fd`. - proc_self_fd: File, - - // Whether writeback caching is enabled for this directory. This will only be true when - // `cfg.writeback` is true and `init` was called with `FsOptions::WRITEBACK_CACHE`. - writeback: AtomicBool, - announce_submounts: AtomicBool, - my_uid: Option, - my_gid: Option, - cap_fowner: bool, - - cfg: Config, -} - -/// Some operations can only be performed on opened FDs without O_PATH, or on symlink paths. -/// This enum encodes a fallback to handle those symlinks separately. -enum FileOrLink { - File(File), - Link(CString), -} - -impl PassthroughFs { - pub fn new(cfg: Config, inode_alloc: Arc) -> io::Result { - let fd = if let Some(fd) = cfg.proc_sfd_rawfd { - fd - } else { - // Safe because this is a constant value and a valid C string. - let proc_cstr = unsafe { CStr::from_bytes_with_nul_unchecked(PROC_CSTR) }; - - // Safe because this doesn't modify any memory and we check the return value. - let fd = unsafe { - libc::openat( - libc::AT_FDCWD, - proc_cstr.as_ptr(), - libc::O_PATH | libc::O_NOFOLLOW | libc::O_CLOEXEC, - ) - }; - if fd < 0 { - return Err(io::Error::last_os_error()); - } - - fd - }; - - let my_uid = if has_cap(None, CapSet::Effective, Capability::CAP_SETUID).unwrap_or_default() - { - None - } else { - // SAFETY: This syscall is always safe to call and always succeeds. - Some(unsafe { libc::getuid() }) - }; - - let my_gid = if has_cap(None, CapSet::Effective, Capability::CAP_SETGID).unwrap_or_default() - { - None - } else { - // SAFETY: This syscall is always safe to call and always succeeds. - Some(unsafe { libc::getgid() }) - }; - - let cap_fowner = - has_cap(None, CapSet::Effective, Capability::CAP_FOWNER).unwrap_or_default(); - - // Safe because we just opened this fd or it was provided by our caller. - let proc_self_fd = unsafe { File::from_raw_fd(fd) }; - - Ok(PassthroughFs { - inodes: RwLock::new(MultikeyBTreeMap::new()), - inode_alloc, - - handles: RwLock::new(BTreeMap::new()), - next_handle: AtomicU64::new(1), - - proc_self_fd, - - writeback: AtomicBool::new(false), - announce_submounts: AtomicBool::new(false), - my_uid, - my_gid, - cap_fowner, - cfg, - }) - } - - fn open_inode(&self, inode: Inode, mut flags: i32) -> io::Result { - let data = self - .inodes - .read() - .unwrap() - .get(&inode) - .cloned() - .ok_or_else(ebadf)?; - - let pathname = CString::new(format!("{}", data.file.as_raw_fd())) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; - - // When writeback caching is enabled, the kernel may send read requests even if the - // userspace program opened the file write-only. So we need to ensure that we have opened - // the file for reading as well as writing. - let writeback = self.writeback.load(Ordering::Relaxed); - if writeback && flags & libc::O_ACCMODE == libc::O_WRONLY { - flags &= !libc::O_ACCMODE; - flags |= libc::O_RDWR; - } - - // When writeback caching is enabled the kernel is responsible for handling `O_APPEND`. - // However, this breaks atomicity as the file may have changed on disk, invalidating the - // cached copy of the data in the kernel and the offset that the kernel thinks is the end of - // the file. Just allow this for now as it is the user's responsibility to enable writeback - // caching only for directories that are not shared. It also means that we need to clear the - // `O_APPEND` flag. - if writeback && flags & libc::O_APPEND != 0 { - flags &= !libc::O_APPEND; - } - - // Safe because this doesn't modify any memory and we check the return value. We don't - // really check `flags` because if the kernel can't handle poorly specified flags then we - // have much bigger problems. Also, clear the `O_NOFOLLOW` flag if it is set since we need - // to follow the `/proc/self/fd` symlink to get the file. - let fd = unsafe { - libc::openat( - self.proc_self_fd.as_raw_fd(), - pathname.as_ptr(), - (flags | libc::O_CLOEXEC) & (!libc::O_NOFOLLOW), - ) - }; - if fd < 0 { - return Err(io::Error::last_os_error()); - } - - // Safe because we just opened this fd. - Ok(unsafe { File::from_raw_fd(fd) }) - } - - fn open_inode_or_path(&self, inode: Inode, flags: i32) -> io::Result { - match self.open_inode(inode, flags) { - Ok(a) => Ok(FileOrLink::File(a)), - Err(e) => { - if e.raw_os_error() == Some(libc::ELOOP) { - let data = self - .inodes - .read() - .unwrap() - .get(&inode) - .cloned() - .ok_or_else(ebadf)?; - - let pathname = CString::new(format!("/proc/self/fd/{}", data.file.as_raw_fd())) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; - Ok(FileOrLink::Link(pathname)) - } else { - Err(e) - } - } - } - } - - fn do_readdir( - &self, - inode: Inode, - handle: Handle, - size: u32, - offset: u64, - mut add_entry: F, - ) -> io::Result<()> - where - F: FnMut(DirEntry) -> io::Result, - { - if size == 0 { - return Ok(()); - } - - let data = self - .handles - .read() - .unwrap() - .get(&handle) - .filter(|hd| hd.inode == inode) - .cloned() - .ok_or_else(ebadf)?; - - let mut buf = vec![0; size as usize]; - - { - // Since we are going to work with the kernel offset, we have to acquire the file lock - // for both the `lseek64` and `getdents64` syscalls to ensure that no other thread - // changes the kernel offset while we are using it. - let dir = data.file.write().unwrap(); - - // Safe because this doesn't modify any memory and we check the return value. - let res = - unsafe { libc::lseek64(dir.as_raw_fd(), offset as libc::off64_t, libc::SEEK_SET) }; - if res < 0 { - return Err(io::Error::last_os_error()); - } - - // Safe because the kernel guarantees that it will only write to `buf` and we check the - // return value. - let res = unsafe { - libc::syscall( - libc::SYS_getdents64, - dir.as_raw_fd(), - buf.as_mut_ptr() as *mut LinuxDirent64, - size as libc::c_int, - ) - }; - if res < 0 { - return Err(io::Error::last_os_error()); - } - buf.resize(res as usize, 0); - - // Explicitly drop the lock so that it's not held while we fill in the fuse buffer. - mem::drop(dir); - } - - let mut rem = &buf[..]; - while !rem.is_empty() { - // We only use debug asserts here because these values are coming from the kernel and we - // trust them implicitly. - debug_assert!( - rem.len() >= size_of::(), - "not enough space left in `rem`" - ); - - let (front, back) = rem.split_at(size_of::()); - - let dirent64 = - LinuxDirent64::from_slice(front).expect("unable to get LinuxDirent64 from slice"); - - let namelen = dirent64.d_reclen as usize - size_of::(); - debug_assert!(namelen <= back.len(), "back is smaller than `namelen`"); - - let name = &back[..namelen]; - let term = name - .iter() - .position(|&a| a == 0) - .expect("LinuxDirent64 name not NUL-terminated"); - let name = &name[..term]; - let res = if name.starts_with(CURRENT_DIR_CSTR) || name.starts_with(PARENT_DIR_CSTR) { - // We don't want to report the "." and ".." entries. However, returning `Ok(0)` will - // break the loop so return `Ok` with a non-zero value instead. - Ok(1) - } else { - add_entry(DirEntry { - ino: dirent64.d_ino, - offset: dirent64.d_off as u64, - type_: u32::from(dirent64.d_ty), - name, - }) - }; - - debug_assert!( - rem.len() >= dirent64.d_reclen as usize, - "rem is smaller than `d_reclen`" - ); - - match res { - Ok(0) => break, - Ok(_) => rem = &rem[dirent64.d_reclen as usize..], - Err(e) => return Err(e), - } - } - - Ok(()) - } - - fn do_open( - &self, - inode: Inode, - kill_priv: bool, - mut flags: u32, - ) -> io::Result<(Option, OpenOptions)> { - debug!("do_open: {inode:?}"); - if !self.cap_fowner { - // O_NOATIME can only be used with CAP_FOWNER or if we are the file - // owner. Not worth checking the latter, just drop it if we don't - // have the cap. This makes overlayfs mounts with virtiofs lower dirs - // work. - flags &= !(libc::O_NOATIME as u32); - } - - let file = { - let _killpriv_guard = if kill_priv { - drop_effective_cap(Capability::CAP_FSETID)? - } else { - None - }; - RwLock::new(self.open_inode(inode, flags as i32)?) - }; - - let handle = self.next_handle.fetch_add(1, Ordering::Relaxed); - let data = HandleData { - inode, - file, - exported: Default::default(), - }; - - self.handles.write().unwrap().insert(handle, Arc::new(data)); - - let mut opts = OpenOptions::empty(); - match self.cfg.cache_policy { - // We only set the direct I/O option on files. - CachePolicy::Never => opts.set( - OpenOptions::DIRECT_IO, - flags & (libc::O_DIRECTORY as u32) == 0, - ), - CachePolicy::Always => { - if flags & (libc::O_DIRECTORY as u32) == 0 { - opts |= OpenOptions::KEEP_CACHE; - } else { - opts |= OpenOptions::CACHE_DIR; - } - } - _ => {} - }; - - Ok((Some(handle), opts)) - } - - fn do_release(&self, inode: Inode, handle: Handle) -> io::Result<()> { - let mut handles = self.handles.write().unwrap(); - - if let btree_map::Entry::Occupied(e) = handles.entry(handle) { - if e.get().inode == inode { - if e.get().exported.load(Ordering::Relaxed) { - self.cfg - .export_table - .as_ref() - .unwrap() - .lock() - .unwrap() - .remove(&(self.cfg.export_fsid, handle)); - } - - // We don't need to close the file here because that will happen automatically when - // the last `Arc` is dropped. - e.remove(); - return Ok(()); - } - } - - Err(ebadf()) - } - - fn do_getattr(&self, inode: Inode) -> io::Result<(libc::stat64, Duration)> { - let data = self - .inodes - .read() - .unwrap() - .get(&inode) - .cloned() - .ok_or_else(ebadf)?; - - let st = stat(&data.file)?; - - Ok((st, self.cfg.attr_timeout)) - } - - fn do_unlink(&self, parent: Inode, name: &CStr, flags: libc::c_int) -> io::Result<()> { - let data = self - .inodes - .read() - .unwrap() - .get(&parent) - .cloned() - .ok_or_else(ebadf)?; - - // Safe because this doesn't modify any memory and we check the return value. - let res = unsafe { libc::unlinkat(data.file.as_raw_fd(), name.as_ptr(), flags) }; - if res == 0 { - Ok(()) - } else { - Err(io::Error::last_os_error()) - } - } - - fn set_creds( - &self, - uid: libc::uid_t, - gid: libc::gid_t, - ) -> io::Result<(Option, Option)> { - // Change the gid first, since once we change the uid we lose the capability to change the gid. - let scoped_gid = if gid == 0 || self.my_gid == Some(gid) { - // Always allow "root" accesses even if we don't have root powers. - // This means guest processes running as root can use /tmp (though - // the files will not be actually owned by root), which is desirable. - None - } else if self.my_gid.is_some() { - // Reject writes as any other gid if we do not have setgid - // privileges. - return Err(io::Error::from_raw_os_error(libc::EPERM)); - } else { - ScopedGid::new(gid)? - }; - - // Same logic as above, for uid. - let scoped_uid = if uid == 0 || self.my_uid == Some(uid) { - None - } else if self.my_uid.is_some() { - return Err(io::Error::from_raw_os_error(libc::EPERM)); - } else { - ScopedUid::new(uid)? - }; - - Ok((scoped_uid, scoped_gid)) - } -} - -fn forget_one( - inodes: &mut MultikeyBTreeMap>, - inode: Inode, - count: u64, -) { - if let Some(data) = inodes.get(&inode) { - // Acquiring the write lock on the inode map prevents new lookups from incrementing the - // refcount but there is the possibility that a previous lookup already acquired a - // reference to the inode data and is in the process of updating the refcount so we need - // to loop here until we can decrement successfully. - loop { - let refcount = data.refcount.load(Ordering::Relaxed); - - // Saturating sub because it doesn't make sense for a refcount to go below zero and - // we don't want misbehaving clients to cause integer overflow. - let new_count = refcount.saturating_sub(count); - - // Synchronizes with the acquire load in `lookup`. - if data - .refcount - .compare_exchange(refcount, new_count, Ordering::Release, Ordering::Relaxed) - .unwrap() - == refcount - { - if new_count == 0 { - // We just removed the last refcount for this inode. There's no need for an - // acquire fence here because we hold a write lock on the inode map and any - // thread that is waiting to do a forget on the same inode will have to wait - // until we release the lock. So there's is no other release store for us to - // synchronize with before deleting the entry. - inodes.remove(&inode); - } - break; - } - } - } -} - -impl FileSystem for PassthroughFs { - type Inode = Inode; - type Handle = Handle; - - fn init(&self, capable: FsOptions) -> io::Result { - let root = CString::new(self.cfg.root_dir.as_str()).expect("CString::new failed"); - - // Safe because this doesn't modify any memory and we check the return value. - // We use `O_PATH` because we just want this for traversing the directory tree - // and not for actually reading the contents. - let fd = unsafe { - libc::openat( - libc::AT_FDCWD, - root.as_ptr(), - libc::O_PATH | libc::O_NOFOLLOW | libc::O_CLOEXEC, - ) - }; - if fd < 0 { - return Err(io::Error::last_os_error()); - } - - // Safe because we just opened this fd above. - let f = unsafe { File::from_raw_fd(fd) }; - - let (st, mnt_id) = statx(&f)?; - - // Safe because this doesn't modify any memory and there is no need to check the return - // value because this system call always succeeds. We need to clear the umask here because - // we want the client to be able to set all the bits in the mode. - unsafe { libc::umask(0o000) }; - - let mut inodes = self.inodes.write().unwrap(); - - // Not sure why the root inode gets a refcount of 2 but that's what libfuse does. - inodes.insert( - fuse::ROOT_ID, - InodeAltKey { - ino: st.st_ino, - dev: st.st_dev, - mnt_id, - }, - Arc::new(InodeData { - inode: fuse::ROOT_ID, - file: f, - dev: st.st_dev, - mnt_id, - refcount: AtomicU64::new(2), - }), - ); - - let mut opts = FsOptions::DO_READDIRPLUS | FsOptions::READDIRPLUS_AUTO; - if self.cfg.writeback && capable.contains(FsOptions::WRITEBACK_CACHE) { - opts |= FsOptions::WRITEBACK_CACHE; - self.writeback.store(true, Ordering::Relaxed); - } - - if capable.contains(FsOptions::SUBMOUNTS) { - opts |= FsOptions::SUBMOUNTS; - self.announce_submounts.store(true, Ordering::Relaxed); - } - - Ok(opts) - } - - fn destroy(&self) { - self.handles.write().unwrap().clear(); - self.inodes.write().unwrap().clear(); - } - - fn statfs(&self, _ctx: Context, inode: Inode) -> io::Result { - let data = self - .inodes - .read() - .unwrap() - .get(&inode) - .cloned() - .ok_or_else(ebadf)?; - - let mut out = MaybeUninit::::zeroed(); - - // Safe because this will only modify `out` and we check the return value. - let res = unsafe { libc::fstatvfs64(data.file.as_raw_fd(), out.as_mut_ptr()) }; - if res == 0 { - // Safe because the kernel guarantees that `out` has been initialized. - Ok(unsafe { out.assume_init() }) - } else { - Err(io::Error::last_os_error()) - } - } - - fn lookup(&self, _ctx: Context, parent: Inode, name: &CStr) -> io::Result { - let p = self - .inodes - .read() - .unwrap() - .get(&parent) - .cloned() - .ok_or_else(ebadf)?; - - // Safe because this doesn't modify any memory and we check the return value. - let fd = unsafe { - libc::openat( - p.file.as_raw_fd(), - name.as_ptr(), - libc::O_PATH | libc::O_NOFOLLOW | libc::O_CLOEXEC, - ) - }; - if fd < 0 { - return Err(io::Error::last_os_error()); - } - - // Safe because we just opened this fd. - let f = unsafe { File::from_raw_fd(fd) }; - - let (st, mnt_id) = statx(&f)?; - - let mut attr_flags: u32 = 0; - - if st.st_mode & libc::S_IFMT == libc::S_IFDIR - && self.announce_submounts.load(Ordering::Relaxed) - && (st.st_dev != p.dev || mnt_id != p.mnt_id) - { - attr_flags |= fuse::ATTR_SUBMOUNT; - } - - let altkey = InodeAltKey { - ino: st.st_ino, - dev: st.st_dev, - mnt_id, - }; - let data = self.inodes.read().unwrap().get_alt(&altkey).cloned(); - - let inode = if let Some(data) = data { - // Matches with the release store in `forget`. - data.refcount.fetch_add(1, Ordering::Acquire); - data.inode - } else { - // There is a possible race here where 2 threads end up adding the same file - // into the inode list. However, since each of those will get a unique Inode - // value and unique file descriptors this shouldn't be that much of a problem. - let inode = self.inode_alloc.next(); - self.inodes.write().unwrap().insert( - inode, - InodeAltKey { - ino: st.st_ino, - dev: st.st_dev, - mnt_id, - }, - Arc::new(InodeData { - inode, - file: f, - dev: st.st_dev, - mnt_id, - refcount: AtomicU64::new(1), - }), - ); - - inode - }; - - debug!("lookup: {}, inode: {:?}", name.to_str().unwrap(), inode); - - Ok(Entry { - inode, - generation: 0, - attr: st, - attr_flags, - attr_timeout: self.cfg.attr_timeout, - entry_timeout: self.cfg.entry_timeout, - }) - } - - fn forget(&self, _ctx: Context, inode: Inode, count: u64) { - let mut inodes = self.inodes.write().unwrap(); - - forget_one(&mut inodes, inode, count) - } - - fn batch_forget(&self, _ctx: Context, requests: Vec<(Inode, u64)>) { - let mut inodes = self.inodes.write().unwrap(); - - for (inode, count) in requests { - forget_one(&mut inodes, inode, count) - } - } - - fn opendir( - &self, - _ctx: Context, - inode: Inode, - flags: u32, - ) -> io::Result<(Option, OpenOptions)> { - self.do_open(inode, false, flags | (libc::O_DIRECTORY as u32)) - } - - fn releasedir( - &self, - _ctx: Context, - inode: Inode, - _flags: u32, - handle: Handle, - ) -> io::Result<()> { - self.do_release(inode, handle) - } - - fn mkdir( - &self, - ctx: Context, - parent: Inode, - name: &CStr, - mode: u32, - umask: u32, - extensions: Extensions, - ) -> io::Result { - if extensions.secctx.is_some() { - unimplemented!("SECURITY_CTX is not supported and should not be used by the guest"); - } - - let (_uid, _gid) = self.set_creds(ctx.uid, ctx.gid)?; - let data = self - .inodes - .read() - .unwrap() - .get(&parent) - .cloned() - .ok_or_else(ebadf)?; - - // Safe because this doesn't modify any memory and we check the return value. - let res = unsafe { libc::mkdirat(data.file.as_raw_fd(), name.as_ptr(), mode & !umask) }; - if res == 0 { - self.lookup(ctx, parent, name) - } else { - Err(io::Error::last_os_error()) - } - } - - fn rmdir(&self, _ctx: Context, parent: Inode, name: &CStr) -> io::Result<()> { - self.do_unlink(parent, name, libc::AT_REMOVEDIR) - } - - fn readdir( - &self, - _ctx: Context, - inode: Inode, - handle: Handle, - size: u32, - offset: u64, - add_entry: F, - ) -> io::Result<()> - where - F: FnMut(DirEntry) -> io::Result, - { - self.do_readdir(inode, handle, size, offset, add_entry) - } - - fn readdirplus( - &self, - ctx: Context, - inode: Inode, - handle: Handle, - size: u32, - offset: u64, - mut add_entry: F, - ) -> io::Result<()> - where - F: FnMut(DirEntry, Entry) -> io::Result, - { - self.do_readdir(inode, handle, size, offset, |dir_entry| { - // Safe because the kernel guarantees that the buffer is nul-terminated. Additionally, - // the kernel will pad the name with '\0' bytes up to 8-byte alignment and there's no - // way for us to know exactly how many padding bytes there are. This would cause - // `CStr::from_bytes_with_nul` to return an error because it would think there are - // interior '\0' bytes. We trust the kernel to provide us with properly formatted data - // so we'll just skip the checks here. - let name = unsafe { CStr::from_bytes_with_nul_unchecked(dir_entry.name) }; - let entry = self.lookup(ctx, inode, name)?; - - add_entry(dir_entry, entry) - }) - } - - fn open( - &self, - _ctx: Context, - inode: Inode, - kill_priv: bool, - flags: u32, - ) -> io::Result<(Option, OpenOptions)> { - self.do_open(inode, kill_priv, flags) - } - - fn release( - &self, - _ctx: Context, - inode: Inode, - _flags: u32, - handle: Handle, - _flush: bool, - _flock_release: bool, - _lock_owner: Option, - ) -> io::Result<()> { - self.do_release(inode, handle) - } - - fn create( - &self, - ctx: Context, - parent: Inode, - name: &CStr, - mode: u32, - kill_priv: bool, - flags: u32, - umask: u32, - extensions: Extensions, - ) -> io::Result<(Entry, Option, OpenOptions)> { - if extensions.secctx.is_some() { - unimplemented!("SECURITY_CTX is not supported and should not be used by the guest"); - } - - let (_uid, _gid) = self.set_creds(ctx.uid, ctx.gid)?; - let _killpriv_guard = if kill_priv { - drop_effective_cap(Capability::CAP_FSETID)? - } else { - None - }; - - let data = self - .inodes - .read() - .unwrap() - .get(&parent) - .cloned() - .ok_or_else(ebadf)?; - - // Safe because this doesn't modify any memory and we check the return value. We don't - // really check `flags` because if the kernel can't handle poorly specified flags then we - // have much bigger problems. - let fd = unsafe { - libc::openat( - data.file.as_raw_fd(), - name.as_ptr(), - flags as i32 | libc::O_CREAT | libc::O_CLOEXEC | libc::O_NOFOLLOW, - mode & !(umask & 0o777), - ) - }; - if fd < 0 { - return Err(io::Error::last_os_error()); - } - - // Safe because we just opened this fd. - let file = RwLock::new(unsafe { File::from_raw_fd(fd) }); - - let entry = self.lookup(ctx, parent, name)?; - - let handle = self.next_handle.fetch_add(1, Ordering::Relaxed); - let data = HandleData { - inode: entry.inode, - file, - exported: Default::default(), - }; - - self.handles.write().unwrap().insert(handle, Arc::new(data)); - - let mut opts = OpenOptions::empty(); - match self.cfg.cache_policy { - CachePolicy::Never => opts |= OpenOptions::DIRECT_IO, - CachePolicy::Always => opts |= OpenOptions::KEEP_CACHE, - _ => {} - }; - - Ok((entry, Some(handle), opts)) - } - - fn unlink(&self, _ctx: Context, parent: Inode, name: &CStr) -> io::Result<()> { - self.do_unlink(parent, name, 0) - } - - fn read( - &self, - _ctx: Context, - inode: Inode, - handle: Handle, - mut w: W, - size: u32, - offset: u64, - _lock_owner: Option, - _flags: u32, - ) -> io::Result { - debug!("read: {inode:?}"); - let data = self - .handles - .read() - .unwrap() - .get(&handle) - .filter(|hd| hd.inode == inode) - .cloned() - .ok_or_else(ebadf)?; - - // This is safe because write_from uses preadv64, so the underlying file descriptor - // offset is not affected by this operation. - let f = data.file.read().unwrap(); - w.write_from(&f, size as usize, offset) - } - - fn write( - &self, - _ctx: Context, - inode: Inode, - handle: Handle, - mut r: R, - size: u32, - offset: u64, - _lock_owner: Option, - _delayed_write: bool, - kill_priv: bool, - _flags: u32, - ) -> io::Result { - let _killpriv_guard = if kill_priv { - // We need to drop FSETID during a write so that the kernel will remove setuid - // or setgid bits from the file if it was written to by someone other than the - // owner. - drop_effective_cap(Capability::CAP_FSETID)? - } else { - None - }; - - let data = self - .handles - .read() - .unwrap() - .get(&handle) - .filter(|hd| hd.inode == inode) - .cloned() - .ok_or_else(ebadf)?; - - // This is safe because read_to uses pwritev64, so the underlying file descriptor - // offset is not affected by this operation. - let f = data.file.read().unwrap(); - r.read_to(&f, size as usize, offset) - } - - fn getattr( - &self, - _ctx: Context, - inode: Inode, - _handle: Option, - ) -> io::Result<(libc::stat64, Duration)> { - self.do_getattr(inode) - } - - fn setattr( - &self, - _ctx: Context, - inode: Inode, - attr: libc::stat64, - handle: Option, - valid: SetattrValid, - ) -> io::Result<(libc::stat64, Duration)> { - let inode_data = self - .inodes - .read() - .unwrap() - .get(&inode) - .cloned() - .ok_or_else(ebadf)?; - - enum Data { - Handle(RawFd), - ProcPath(CString), - } - - // If we have a handle then use it otherwise get a new fd from the inode. - let data = if let Some(handle) = handle { - let hd = self - .handles - .read() - .unwrap() - .get(&handle) - .filter(|hd| hd.inode == inode) - .cloned() - .ok_or_else(ebadf)?; - - let fd = hd.file.write().unwrap().as_raw_fd(); - Data::Handle(fd) - } else { - let pathname = CString::new(format!("{}", inode_data.file.as_raw_fd())) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; - Data::ProcPath(pathname) - }; - - if valid.contains(SetattrValid::MODE) { - // Safe because this doesn't modify any memory and we check the return value. - let res = unsafe { - match data { - Data::Handle(fd) => libc::fchmod(fd, attr.st_mode), - Data::ProcPath(ref p) => { - libc::fchmodat(self.proc_self_fd.as_raw_fd(), p.as_ptr(), attr.st_mode, 0) - } - } - }; - if res < 0 { - return Err(io::Error::last_os_error()); - } - } - - if valid.intersects(SetattrValid::UID | SetattrValid::GID) { - let uid = if valid.contains(SetattrValid::UID) { - attr.st_uid - } else { - // Cannot use -1 here because these are unsigned values. - u32::MAX - }; - let gid = if valid.contains(SetattrValid::GID) { - attr.st_gid - } else { - // Cannot use -1 here because these are unsigned values. - u32::MAX - }; - - // Safe because this is a constant value and a valid C string. - let empty = unsafe { CStr::from_bytes_with_nul_unchecked(EMPTY_CSTR) }; - - // Safe because this doesn't modify any memory and we check the return value. - let res = unsafe { - libc::fchownat( - inode_data.file.as_raw_fd(), - empty.as_ptr(), - uid, - gid, - libc::AT_EMPTY_PATH | libc::AT_SYMLINK_NOFOLLOW, - ) - }; - if res < 0 { - return Err(io::Error::last_os_error()); - } - } - - if valid.contains(SetattrValid::SIZE) { - // Safe because this doesn't modify any memory and we check the return value. - let res = match data { - Data::Handle(fd) => unsafe { libc::ftruncate(fd, attr.st_size) }, - _ => { - // There is no `ftruncateat` so we need to get a new fd and truncate it. - let f = self.open_inode(inode, libc::O_NONBLOCK | libc::O_RDWR)?; - unsafe { libc::ftruncate(f.as_raw_fd(), attr.st_size) } - } - }; - if res < 0 { - return Err(io::Error::last_os_error()); - } - } - - if valid.intersects(SetattrValid::ATIME | SetattrValid::MTIME) { - let mut tvs = [ - libc::timespec { - tv_sec: 0, - tv_nsec: libc::UTIME_OMIT, - }, - libc::timespec { - tv_sec: 0, - tv_nsec: libc::UTIME_OMIT, - }, - ]; - - if valid.contains(SetattrValid::ATIME_NOW) { - tvs[0].tv_nsec = libc::UTIME_NOW; - } else if valid.contains(SetattrValid::ATIME) { - tvs[0].tv_sec = attr.st_atime; - tvs[0].tv_nsec = attr.st_atime_nsec; - } - - if valid.contains(SetattrValid::MTIME_NOW) { - tvs[1].tv_nsec = libc::UTIME_NOW; - } else if valid.contains(SetattrValid::MTIME) { - tvs[1].tv_sec = attr.st_mtime; - tvs[1].tv_nsec = attr.st_mtime_nsec; - } - - // Safe because this doesn't modify any memory and we check the return value. - let res = match data { - Data::Handle(fd) => unsafe { libc::futimens(fd, tvs.as_ptr()) }, - Data::ProcPath(ref p) => unsafe { - libc::utimensat(self.proc_self_fd.as_raw_fd(), p.as_ptr(), tvs.as_ptr(), 0) - }, - }; - if res < 0 { - return Err(io::Error::last_os_error()); - } - } - - self.do_getattr(inode) - } - - fn rename( - &self, - _ctx: Context, - olddir: Inode, - oldname: &CStr, - newdir: Inode, - newname: &CStr, - flags: u32, - ) -> io::Result<()> { - let old_inode = self - .inodes - .read() - .unwrap() - .get(&olddir) - .cloned() - .ok_or_else(ebadf)?; - let new_inode = self - .inodes - .read() - .unwrap() - .get(&newdir) - .cloned() - .ok_or_else(ebadf)?; - - // Safe because this doesn't modify any memory and we check the return value. - // TODO: Switch to libc::renameat2 once https://github.com/rust-lang/libc/pull/1508 lands - // and we have glibc 2.28. - let res = unsafe { - libc::syscall( - libc::SYS_renameat2, - old_inode.file.as_raw_fd(), - oldname.as_ptr(), - new_inode.file.as_raw_fd(), - newname.as_ptr(), - flags, - ) - }; - if res == 0 { - Ok(()) - } else { - Err(io::Error::last_os_error()) - } - } - - fn mknod( - &self, - ctx: Context, - parent: Inode, - name: &CStr, - mode: u32, - rdev: u32, - umask: u32, - extensions: Extensions, - ) -> io::Result { - if extensions.secctx.is_some() { - unimplemented!("SECURITY_CTX is not supported and should not be used by the guest"); - } - - let (_uid, _gid) = self.set_creds(ctx.uid, ctx.gid)?; - let data = self - .inodes - .read() - .unwrap() - .get(&parent) - .cloned() - .ok_or_else(ebadf)?; - - // Safe because this doesn't modify any memory and we check the return value. - let res = unsafe { - libc::mknodat( - data.file.as_raw_fd(), - name.as_ptr(), - (mode & !umask) as libc::mode_t, - u64::from(rdev), - ) - }; - - if res < 0 { - Err(io::Error::last_os_error()) - } else { - self.lookup(ctx, parent, name) - } - } - - fn link( - &self, - ctx: Context, - inode: Inode, - newparent: Inode, - newname: &CStr, - ) -> io::Result { - let data = self - .inodes - .read() - .unwrap() - .get(&inode) - .cloned() - .ok_or_else(ebadf)?; - let new_inode = self - .inodes - .read() - .unwrap() - .get(&newparent) - .cloned() - .ok_or_else(ebadf)?; - - let procname = CString::new(format!("{}", data.file.as_raw_fd())) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; - - // Safe because this doesn't modify any memory and we check the return value. - let res = unsafe { - libc::linkat( - self.proc_self_fd.as_raw_fd(), - procname.as_ptr(), - new_inode.file.as_raw_fd(), - newname.as_ptr(), - libc::AT_SYMLINK_FOLLOW, - ) - }; - if res == 0 { - self.lookup(ctx, newparent, newname) - } else { - Err(io::Error::last_os_error()) - } - } - - fn symlink( - &self, - ctx: Context, - linkname: &CStr, - parent: Inode, - name: &CStr, - extensions: Extensions, - ) -> io::Result { - // Set security context on symlink. - if extensions.secctx.is_some() { - unimplemented!("SECURITY_CTX is not supported and should not be used by the guest"); - } - - let (_uid, _gid) = self.set_creds(ctx.uid, ctx.gid)?; - let data = self - .inodes - .read() - .unwrap() - .get(&parent) - .cloned() - .ok_or_else(ebadf)?; - - // Safe because this doesn't modify any memory and we check the return value. - let res = - unsafe { libc::symlinkat(linkname.as_ptr(), data.file.as_raw_fd(), name.as_ptr()) }; - if res == 0 { - self.lookup(ctx, parent, name) - } else { - Err(io::Error::last_os_error()) - } - } - - fn readlink(&self, _ctx: Context, inode: Inode) -> io::Result> { - let data = self - .inodes - .read() - .unwrap() - .get(&inode) - .cloned() - .ok_or_else(ebadf)?; - - let mut buf = vec![0; libc::PATH_MAX as usize]; - - // Safe because this is a constant value and a valid C string. - let empty = unsafe { CStr::from_bytes_with_nul_unchecked(EMPTY_CSTR) }; - - // Safe because this will only modify the contents of `buf` and we check the return value. - let res = unsafe { - libc::readlinkat( - data.file.as_raw_fd(), - empty.as_ptr(), - buf.as_mut_ptr() as *mut libc::c_char, - buf.len(), - ) - }; - if res < 0 { - return Err(io::Error::last_os_error()); - } - - buf.resize(res as usize, 0); - Ok(buf) - } - - fn flush( - &self, - _ctx: Context, - inode: Inode, - handle: Handle, - _lock_owner: u64, - ) -> io::Result<()> { - let data = self - .handles - .read() - .unwrap() - .get(&handle) - .filter(|hd| hd.inode == inode) - .cloned() - .ok_or_else(ebadf)?; - - // Since this method is called whenever an fd is closed in the client, we can emulate that - // behavior by doing the same thing (dup-ing the fd and then immediately closing it). Safe - // because this doesn't modify any memory and we check the return values. - unsafe { - let newfd = libc::dup(data.file.write().unwrap().as_raw_fd()); - if newfd < 0 { - return Err(io::Error::last_os_error()); - } - - if libc::close(newfd) < 0 { - Err(io::Error::last_os_error()) - } else { - Ok(()) - } - } - } - - fn fsync(&self, _ctx: Context, inode: Inode, datasync: bool, handle: Handle) -> io::Result<()> { - let data = self - .handles - .read() - .unwrap() - .get(&handle) - .filter(|hd| hd.inode == inode) - .cloned() - .ok_or_else(ebadf)?; - - let fd = data.file.write().unwrap().as_raw_fd(); - - // Safe because this doesn't modify any memory and we check the return value. - let res = unsafe { - if datasync { - libc::fdatasync(fd) - } else { - libc::fsync(fd) - } - }; - - if res == 0 { - Ok(()) - } else { - Err(io::Error::last_os_error()) - } - } - - fn fsyncdir( - &self, - ctx: Context, - inode: Inode, - datasync: bool, - handle: Handle, - ) -> io::Result<()> { - self.fsync(ctx, inode, datasync, handle) - } - - fn access(&self, ctx: Context, inode: Inode, mask: u32) -> io::Result<()> { - let data = self - .inodes - .read() - .unwrap() - .get(&inode) - .cloned() - .ok_or_else(ebadf)?; - - let st = stat(&data.file)?; - let mode = mask as i32 & (libc::R_OK | libc::W_OK | libc::X_OK); - - if mode == libc::F_OK { - // The file exists since we were able to call `stat(2)` on it. - return Ok(()); - } - - // We use ctx.uid/ctx.gid for these checks, but when idmapped mounts - // support is enabled on the guest side, it means that "default_permissions" - // flag is set on virtiofs mount and FUSE_ACCESS request should never be - // sent to the userspace. Please, refer to the kernel commit - // ("fs/fuse: warn if fuse_access is called when idmapped mounts are allowed"). - // In case when idmapped mounts are not enabled we are good to rely on ctx.uid/ctx.gid values. - - if (mode & libc::R_OK) != 0 - && ctx.uid != 0 - && (st.st_uid != ctx.uid || st.st_mode & 0o400 == 0) - && (st.st_gid != ctx.gid || st.st_mode & 0o040 == 0) - && st.st_mode & 0o004 == 0 - { - return Err(io::Error::from_raw_os_error(libc::EACCES)); - } - - if (mode & libc::W_OK) != 0 - && ctx.uid != 0 - && (st.st_uid != ctx.uid || st.st_mode & 0o200 == 0) - && (st.st_gid != ctx.gid || st.st_mode & 0o020 == 0) - && st.st_mode & 0o002 == 0 - { - return Err(io::Error::from_raw_os_error(libc::EACCES)); - } - - // root can only execute something if it is executable by one of the owner, the group, or - // everyone. - if (mode & libc::X_OK) != 0 - && (ctx.uid != 0 || st.st_mode & 0o111 == 0) - && (st.st_uid != ctx.uid || st.st_mode & 0o100 == 0) - && (st.st_gid != ctx.gid || st.st_mode & 0o010 == 0) - && st.st_mode & 0o001 == 0 - { - return Err(io::Error::from_raw_os_error(libc::EACCES)); - } - - Ok(()) - } - - fn setxattr( - &self, - _ctx: Context, - inode: Inode, - name: &CStr, - value: &[u8], - flags: u32, - ) -> io::Result<()> { - if !self.cfg.xattr { - return Err(io::Error::from_raw_os_error(libc::ENOSYS)); - } - - // The f{set,get,remove,list}xattr functions don't work on an fd opened with `O_PATH` so we - // need to get a new fd. This doesn't work for symlinks, so we use the l* family of - // functions in that case. - let res = match self.open_inode_or_path(inode, libc::O_RDONLY | libc::O_NONBLOCK)? { - FileOrLink::File(file) => { - // Safe because this doesn't modify any memory and we check the return value. - unsafe { - libc::fsetxattr( - file.as_raw_fd(), - name.as_ptr(), - value.as_ptr() as *const libc::c_void, - value.len(), - flags as libc::c_int, - ) - } - } - FileOrLink::Link(link) => { - // Safe because this doesn't modify any memory and we check the return value. - unsafe { - libc::lsetxattr( - link.as_ptr(), - name.as_ptr(), - value.as_ptr() as *const libc::c_void, - value.len(), - flags as libc::c_int, - ) - } - } - }; - - if res == 0 { - Ok(()) - } else { - Err(io::Error::last_os_error()) - } - } - - fn getxattr( - &self, - _ctx: Context, - inode: Inode, - name: &CStr, - size: u32, - ) -> io::Result { - if !self.cfg.xattr { - return Err(io::Error::from_raw_os_error(libc::ENOSYS)); - } - - let mut buf = vec![0; size as usize]; - - // The f{set,get,remove,list}xattr functions don't work on an fd opened with `O_PATH` so we - // need to get a new fd. This doesn't work for symlinks, so we use the l* family of - // functions in that case. - let res = match self.open_inode_or_path(inode, libc::O_RDONLY | libc::O_NONBLOCK)? { - FileOrLink::File(file) => { - // Safe because this will only modify the contents of `buf`. - unsafe { - libc::fgetxattr( - file.as_raw_fd(), - name.as_ptr(), - buf.as_mut_ptr() as *mut libc::c_void, - size as libc::size_t, - ) - } - } - FileOrLink::Link(link) => { - // Safe because this will only modify the contents of `buf`. - unsafe { - libc::lgetxattr( - link.as_ptr(), - name.as_ptr(), - buf.as_mut_ptr() as *mut libc::c_void, - size as libc::size_t, - ) - } - } - }; - - if res < 0 { - return Err(io::Error::last_os_error()); - } - - if size == 0 { - Ok(GetxattrReply::Count(res as u32)) - } else { - buf.resize(res as usize, 0); - Ok(GetxattrReply::Value(buf)) - } - } - - fn listxattr(&self, _ctx: Context, inode: Inode, size: u32) -> io::Result { - if !self.cfg.xattr { - return Err(io::Error::from_raw_os_error(libc::ENOSYS)); - } - - let mut buf = vec![0; size as usize]; - - // The f{set,get,remove,list}xattr functions don't work on an fd opened with `O_PATH` so we - // need to get a new fd. This doesn't work for symlinks, so we use the l* family of - // functions in that case. - let res = match self.open_inode_or_path(inode, libc::O_RDONLY | libc::O_NONBLOCK)? { - FileOrLink::File(file) => { - // Safe because this will only modify the contents of `buf`. - unsafe { - libc::flistxattr( - file.as_raw_fd(), - buf.as_mut_ptr() as *mut libc::c_char, - size as libc::size_t, - ) - } - } - FileOrLink::Link(link) => unsafe { - libc::llistxattr( - link.as_ptr(), - buf.as_mut_ptr() as *mut libc::c_char, - size as libc::size_t, - ) - }, - }; - if res < 0 { - return Err(io::Error::last_os_error()); - } - - if size == 0 { - Ok(ListxattrReply::Count(res as u32)) - } else { - buf.resize(res as usize, 0); - Ok(ListxattrReply::Names(buf)) - } - } - - fn removexattr(&self, _ctx: Context, inode: Inode, name: &CStr) -> io::Result<()> { - if !self.cfg.xattr { - return Err(io::Error::from_raw_os_error(libc::ENOSYS)); - } - - // The f{set,get,remove,list}xattr functions don't work on an fd opened with `O_PATH` so we - // need to get a new fd. This doesn't work for symlinks, so we use the l* family of - // functions in that case. - let res = match self.open_inode_or_path(inode, libc::O_RDONLY | libc::O_NONBLOCK)? { - FileOrLink::File(file) => { - // Safe because this doesn't modify any memory and we check the return value. - unsafe { libc::fremovexattr(file.as_raw_fd(), name.as_ptr()) } - } - FileOrLink::Link(link) => { - // Safe because this doesn't modify any memory and we check the return value. - unsafe { libc::lremovexattr(link.as_ptr(), name.as_ptr()) } - } - }; - - if res == 0 { - Ok(()) - } else { - Err(io::Error::last_os_error()) - } - } - - fn fallocate( - &self, - _ctx: Context, - inode: Inode, - handle: Handle, - mode: u32, - offset: u64, - length: u64, - ) -> io::Result<()> { - let data = self - .handles - .read() - .unwrap() - .get(&handle) - .filter(|hd| hd.inode == inode) - .cloned() - .ok_or_else(ebadf)?; - - let fd = data.file.write().unwrap().as_raw_fd(); - // Safe because this doesn't modify any memory and we check the return value. - let res = unsafe { - libc::fallocate64( - fd, - mode as libc::c_int, - offset as libc::off64_t, - length as libc::off64_t, - ) - }; - if res == 0 { - Ok(()) - } else { - Err(io::Error::last_os_error()) - } - } - - fn lseek( - &self, - _ctx: Context, - inode: Inode, - handle: Handle, - offset: u64, - whence: u32, - ) -> io::Result { - let data = self - .handles - .read() - .unwrap() - .get(&handle) - .filter(|hd| hd.inode == inode) - .cloned() - .ok_or_else(ebadf)?; - - let fd = data.file.write().unwrap().as_raw_fd(); - - // Safe because this doesn't modify any memory and we check the return value. - let res = unsafe { libc::lseek(fd, offset as libc::off64_t, whence as libc::c_int) }; - if res < 0 { - Err(io::Error::last_os_error()) - } else { - Ok(res as u64) - } - } - - fn copyfilerange( - &self, - _ctx: Context, - inode_in: Inode, - handle_in: Handle, - offset_in: u64, - inode_out: Inode, - handle_out: Handle, - offset_out: u64, - len: u64, - flags: u64, - ) -> io::Result { - let data_in = self - .handles - .read() - .unwrap() - .get(&handle_in) - .filter(|hd| hd.inode == inode_in) - .cloned() - .ok_or_else(ebadf)?; - - // Take just a read lock as we're not going to alter the file descriptor offset. - let fd_in = data_in.file.read().unwrap().as_raw_fd(); - - let data_out = self - .handles - .read() - .unwrap() - .get(&handle_out) - .filter(|hd| hd.inode == inode_out) - .cloned() - .ok_or_else(ebadf)?; - - // Take just a read lock as we're not going to alter the file descriptor offset. - let fd_out = data_out.file.read().unwrap().as_raw_fd(); - - // Safe because this will only modify `offset_in` and `offset_out` and we check - // the return value. - let res = unsafe { - libc::copy_file_range( - fd_in, - &mut (offset_in as i64) as &mut _ as *mut _, - fd_out, - &mut (offset_out as i64) as &mut _ as *mut _, - len.try_into().unwrap(), - flags.try_into().unwrap(), - ) - }; - if res < 0 { - Err(io::Error::last_os_error()) - } else { - Ok(res as usize) - } - } - - fn setupmapping( - &self, - _ctx: Context, - inode: Inode, - _handle: Handle, - foffset: u64, - len: u64, - flags: u64, - moffset: u64, - host_shm_base: u64, - shm_size: u64, - ) -> io::Result<()> { - let open_flags = if (flags & fuse::SetupmappingFlags::WRITE.bits()) != 0 { - libc::O_RDWR - } else { - libc::O_RDONLY - }; - - let prot_flags = if (flags & fuse::SetupmappingFlags::WRITE.bits()) != 0 { - libc::PROT_READ | libc::PROT_WRITE - } else { - libc::PROT_READ - }; - - if (moffset + len) > shm_size { - return Err(einval()); - } - - let addr = host_shm_base + moffset; - - debug!("setupmapping: ino {inode:?} addr={addr:x} len={len}"); - - let file = self.open_inode(inode, open_flags)?; - let fd = file.as_raw_fd(); - - let ret = unsafe { - libc::mmap( - addr as *mut libc::c_void, - len as usize, - prot_flags, - libc::MAP_SHARED | libc::MAP_FIXED, - fd, - foffset as libc::off_t, - ) - }; - if std::ptr::eq(ret, libc::MAP_FAILED) { - return Err(io::Error::last_os_error()); - } - - Ok(()) - } - - fn removemapping( - &self, - _ctx: Context, - requests: Vec, - host_shm_base: u64, - shm_size: u64, - ) -> io::Result<()> { - for req in requests { - let addr = host_shm_base + req.moffset; - if (req.moffset + req.len) > shm_size { - return Err(einval()); - } - debug!("removemapping: addr={:x} len={:?}", addr, req.len); - let ret = unsafe { - libc::mmap( - addr as *mut libc::c_void, - req.len as usize, - libc::PROT_NONE, - libc::MAP_ANONYMOUS | libc::MAP_PRIVATE | libc::MAP_FIXED, - -1, - 0_i64, - ) - }; - if std::ptr::eq(ret, libc::MAP_FAILED) { - return Err(io::Error::last_os_error()); - } - } - - Ok(()) - } - - fn ioctl( - &self, - _ctx: Context, - inode: Self::Inode, - handle: Self::Handle, - _flags: u32, - cmd: u32, - _arg: u64, - _in_size: u32, - out_size: u32, - _exit_code: &Arc, - ) -> io::Result> { - const VIRTIO_IOC_MAGIC: u8 = b'v'; - - const VIRTIO_IOC_TYPE_EXPORT_FD: u8 = 1; - const VIRTIO_IOC_EXPORT_FD_SIZE: usize = 2 * mem::size_of::(); - const VIRTIO_IOC_EXPORT_FD_REQ: u32 = request_code_read!( - VIRTIO_IOC_MAGIC, - VIRTIO_IOC_TYPE_EXPORT_FD, - VIRTIO_IOC_EXPORT_FD_SIZE - ) as u32; - - match cmd { - VIRTIO_IOC_EXPORT_FD_REQ => { - if out_size as usize != VIRTIO_IOC_EXPORT_FD_SIZE { - return Err(einval()); - } - - let mut exports = self - .cfg - .export_table - .as_ref() - .ok_or(io::Error::from_raw_os_error(libc::EOPNOTSUPP))? - .lock() - .unwrap(); - - let handles = self.handles.read().unwrap(); - let data = handles - .get(&handle) - .filter(|hd| hd.inode == inode) - .ok_or_else(ebadf)?; - - data.exported.store(true, Ordering::Relaxed); - - let fd = data.file.read().unwrap().try_clone()?; - - exports.insert((self.cfg.export_fsid, handle), fd); - - let mut ret: Vec<_> = self.cfg.export_fsid.to_ne_bytes().into(); - ret.extend_from_slice(&handle.to_ne_bytes()); - Ok(ret) - } - _ => Err(io::Error::from_raw_os_error(libc::EOPNOTSUPP)), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - // A non-zero uid the server can be made to run as, plus a guest - // uid distinct from it and from root. Both must be mappable when - // the test runs inside a user namespace (e.g. `buildah unshare`), - // so keep them inside the conventional 65536-wide subid range. - const SERVER_UID: libc::uid_t = 1; - const SERVER_GID: libc::gid_t = 1; - const GUEST_UID: libc::uid_t = 1000; - - /// Regression test for the `scoped_cred` credential restore. - /// - /// The fs server switches the worker thread's effective uid per - /// request (`ScopedUid`) and restores it on drop. When the server - /// runs as a non-root user that holds CAP_SETUID — e.g. an - /// unprivileged daemon granted the capability via systemd's - /// `AmbientCapabilities=` — restoring to a hardcoded euid 0 (the - /// old behavior) wedges the thread: the next switch from euid 0 to - /// a non-zero uid clears its effective capability set, the restore - /// after that fails EPERM, and the thread is stranded at the guest - /// uid, failing every later request (including ones for guest - /// root). Restoring the *previous* euid keeps switching between - /// non-zero uids only, which never clears the capabilities. - /// - /// This drives the real `ScopedUid` through repeated switches and - /// asserts the thread is left back at the server uid. Credential - /// changes are per-thread (raw syscalls, by design), so it runs on - /// a dedicated thread and leaves the rest of the test binary - /// untouched. It needs CAP_SETUID and the ability to move to a - /// non-zero uid; without them (a plain `cargo test` as a normal - /// user) it skips rather than fails. - #[test] - fn scoped_uid_restores_server_uid_without_wedging() { - let outcome = std::thread::spawn(scoped_uid_no_wedge_body) - .join() - .expect("worker thread panicked"); - match outcome { - Ok(()) => {} - Err(skip) => eprintln!("SKIP scoped_uid_restores_server_uid_without_wedging: {skip}"), - } - } - - /// Establish the non-root-with-CAP_SETUID substrate on the current - /// thread, then exercise `ScopedUid`. `Err(reason)` means the - /// substrate could not be set up and the test should skip; a real - /// regression (the thread left wedged) panics via `assert` so the - /// test fails. - fn scoped_uid_no_wedge_body() -> Result<(), String> { - if !has_cap(None, CapSet::Effective, Capability::CAP_SETUID).map_err(|e| e.to_string())? { - return Err("missing CAP_SETUID (run as root or under a userns with caps)".into()); - } - - // Keep the permitted caps across the upcoming uid change. - if unsafe { libc::prctl(libc::PR_SET_KEEPCAPS, 1, 0, 0, 0) } != 0 { - return Err(format!("PR_SET_KEEPCAPS: {}", io::Error::last_os_error())); - } - // Per-thread setresgid/setresuid via raw syscalls, mirroring how - // `scoped_cred` changes only the calling thread's credentials. - // gid first — once euid is non-zero the right to setgid may be - // gone. A failure here is a substrate problem (e.g. the target - // uid is not mapped into the namespace), so it skips. - if unsafe { libc::syscall(libc::SYS_setresgid, SERVER_GID, SERVER_GID, SERVER_GID) } != 0 { - return Err(format!("setresgid: {}", io::Error::last_os_error())); - } - if unsafe { libc::syscall(libc::SYS_setresuid, SERVER_UID, SERVER_UID, SERVER_UID) } != 0 { - return Err(format!("setresuid: {}", io::Error::last_os_error())); - } - // The euid 0 -> non-zero move cleared the effective set; raise - // back the caps the server relies on from the retained permitted - // set. This is the state an AmbientCapabilities= daemon boots in. - for cap in [Capability::CAP_SETUID, Capability::CAP_SETGID] { - caps::raise(None, CapSet::Effective, cap).map_err(|e| e.to_string())?; - } - assert_eq!( - unsafe { libc::geteuid() }, - SERVER_UID, - "precondition: thread should now run as the server uid" - ); - - // From here the substrate is in place: anything wrong below is a - // real regression and must fail (assert), not skip. - // - // The server's per-request switch: a handful as a non-zero guest - // uid. Two already suffice (the first restore parks euid at the - // old hardcoded 0, the second switch then clears the caps), but - // run more so a wedge is unmistakable. - for i in 0..4 { - let scoped = ScopedUid::new(GUEST_UID) - .expect("switching to the guest uid should succeed while caps are held"); - assert_eq!( - unsafe { libc::geteuid() }, - GUEST_UID, - "switch #{i} should land on the guest uid" - ); - drop(scoped); - } - - assert_eq!( - unsafe { libc::geteuid() }, - SERVER_UID, - "worker thread wedged after repeated switches: euid was not restored to \ - the server uid (the scoped_cred Drop regressed to restoring euid 0)" - ); - Ok(()) - } -} diff --git a/vendor/krun-devices/src/virtio/fs/macos/fs_utils.rs b/vendor/krun-devices/src/virtio/fs/macos/fs_utils.rs deleted file mode 100644 index 1a141d382..000000000 --- a/vendor/krun-devices/src/virtio/fs/macos/fs_utils.rs +++ /dev/null @@ -1,11 +0,0 @@ -use std::io; - -use super::super::super::linux_errno::linux_error; - -pub fn ebadf() -> io::Error { - linux_error(io::Error::from_raw_os_error(libc::EBADF)) -} - -pub fn einval() -> io::Error { - linux_error(io::Error::from_raw_os_error(libc::EINVAL)) -} diff --git a/vendor/krun-devices/src/virtio/fs/macos/mod.rs b/vendor/krun-devices/src/virtio/fs/macos/mod.rs deleted file mode 100644 index b8edbc7f9..000000000 --- a/vendor/krun-devices/src/virtio/fs/macos/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod fs_utils; -pub mod passthrough; diff --git a/vendor/krun-devices/src/virtio/fs/macos/passthrough.rs b/vendor/krun-devices/src/virtio/fs/macos/passthrough.rs deleted file mode 100644 index af712cbbe..000000000 --- a/vendor/krun-devices/src/virtio/fs/macos/passthrough.rs +++ /dev/null @@ -1,2763 +0,0 @@ -// Copyright 2019 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -use std::collections::btree_map; -use std::collections::BTreeMap; -use std::collections::HashMap; -use std::ffi::{CStr, CString}; -use std::fs::File; -use std::io; -use std::mem::MaybeUninit; -use std::os::unix::io::{AsRawFd, FromRawFd, RawFd}; -use std::os::unix::net::UnixListener; -use std::path::Path; -use std::ptr::null_mut; -use std::str::FromStr; -use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering}; -use std::sync::{Arc, Mutex, RwLock}; -use std::time::Duration; - -use crossbeam_channel::{unbounded, Sender}; -use nix::errno::Errno; -use utils::worker_message::WorkerMessage; - -use crate::virtio::fs::filesystem::SecContext; - -use super::super::super::linux_errno::{linux_error, LINUX_ERANGE}; -use super::super::bindings; -use super::super::filesystem::{ - Context, DirEntry, Entry, ExportTable, Extensions, FileSystem, FsOptions, GetxattrReply, - ListxattrReply, OpenOptions, SetattrValid, ZeroCopyReader, ZeroCopyWriter, -}; -use super::super::fuse; -use super::super::inode_alloc::InodeAllocator; -use super::super::multikey::MultikeyBTreeMap; - -const XATTR_KEY: &[u8] = b"user.containers.override_stat\0"; -const SECURITY_CAPABILITY: &[u8] = b"security.capability\0"; - -const UID_MAX: u32 = u32::MAX - 1; - -type Inode = u64; -type Handle = u64; - -#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq)] -struct InodeAltKey { - ino: u64, - dev: i32, -} - -struct InodeData { - inode: Inode, - ino: u64, - dev: i32, - refcount: AtomicU64, - unlinked_fd: AtomicI64, -} - -enum InodeHandle { - Fd(RawFd), - Path(CString), -} - -struct CachedDirEntry { - ino: bindings::ino64_t, - name: Box<[u8]>, - type_: u8, -} - -struct DirStream { - entries: Vec, - ready: bool, -} - -impl DirStream { - fn new() -> Self { - Self { - entries: Vec::new(), - ready: false, - } - } - - fn get_entry<'a>(&'a self, offset: u64) -> Option> { - self.entries.get(offset as usize).map(|e| DirEntry { - ino: e.ino, - // offset points to the next entry, not the current one - offset: offset + 1, - type_: u32::from(e.type_), - name: &e.name, - }) - } - - fn fill_from_fd(&mut self, fd: RawFd) -> io::Result<()> { - // fdopendir() takes ownership of the fd, so we need to obtain a new one - // to be donated. - let newfd = unsafe { libc::dup(fd) }; - if newfd < 0 { - return Err(linux_error(io::Error::last_os_error())); - } - let dir = unsafe { libc::fdopendir(newfd) }; - if dir.is_null() { - let err = io::Error::last_os_error(); - let _ = unsafe { libc::close(newfd) }; - return Err(linux_error(err)); - } - - loop { - // To detect if error happened in readdir we should clear errno - // before the call and then verify it after - Errno::clear(); - let dentry = unsafe { libc::readdir(dir) }; - if dentry.is_null() { - let errno = Errno::last_raw(); - if errno != 0 { - let err = io::Error::from_raw_os_error(errno); - let _ = unsafe { libc::closedir(dir) }; - // Error happened in readdir, but we keep the entries we - // already read to handle the partial read. - return Err(linux_error(err)); - } - break; - } - // SAFETY: dentry is not null. - // We trust macOS to return correct number of bytes for the name - // length. The lifetime of a slice does not escape the unsafe block - // as we copy the data into box right away. - let name = unsafe { - let name_len = usize::from((*dentry).d_namlen); - let name_ptr = (*dentry).d_name.as_ptr().cast(); - let name = std::slice::from_raw_parts(name_ptr, name_len); - - if name == b"." || name == b".." { - continue; - } - Box::<[u8]>::from(name) - }; - - // SAFETY: dentry is not null. - let ino = unsafe { (*dentry).d_ino }; - // SAFETY: dentry is not null. The entry types use the same - // exact constants (`libc::DT_*`) on macOS, Linux, and FUSE. - let type_ = unsafe { (*dentry).d_type }; - - self.entries.push(CachedDirEntry { ino, name, type_ }); - } - - unsafe { libc::closedir(dir) }; - Ok(()) - } -} - -struct HandleData { - inode: Inode, - file: RwLock, - dirstream: Mutex, -} - -fn ebadf() -> io::Error { - linux_error(io::Error::from_raw_os_error(libc::EBADF)) -} - -fn einval() -> io::Error { - linux_error(io::Error::from_raw_os_error(libc::EINVAL)) -} - -fn item_to_value(item: &[u8], radix: u32) -> Option { - match std::str::from_utf8(item) { - Ok(val) => match u32::from_str_radix(val, radix) { - Ok(i) => Some(i), - Err(e) => { - debug!("invalid value: {radix} err={e}"); - None - } - }, - Err(_) => None, - } -} - -fn get_xattr_common(buf: &[u8]) -> io::Result<(Option, Option, Option)> { - let mut items = buf.split(|c| *c == b':'); - - let uid = match items.next() { - Some(item) => item_to_value(item, 10), - None => None, - }; - let gid = match items.next() { - Some(item) => item_to_value(item, 10), - None => None, - }; - let mode = match items.next() { - Some(item) => item_to_value(item, 8), - None => None, - }; - - Ok((uid, gid, mode)) -} - -fn get_xattr_fstat( - fd: RawFd, - st: bindings::stat64, -) -> io::Result<(Option, Option, Option)> { - let mut buf: Vec = vec![0; 32]; - let options = if (st.st_mode & libc::S_IFMT) == libc::S_IFLNK { - libc::XATTR_NOFOLLOW - } else { - 0 - }; - let res = unsafe { - libc::fgetxattr( - fd, - XATTR_KEY.as_ptr() as *const i8, - buf.as_mut_ptr() as *mut libc::c_void, - buf.len(), - 0, - options, - ) - }; - if res < 0 { - debug!("fget_xattr error: {res}"); - return Ok((None, None, None)); - } - - buf.resize(res as usize, 0); - - get_xattr_common(&buf) -} - -fn get_xattr_lstat( - path: &CString, - st: bindings::stat64, -) -> io::Result<(Option, Option, Option)> { - let mut buf: Vec = vec![0; 32]; - let options = if (st.st_mode & libc::S_IFMT) == libc::S_IFLNK { - libc::XATTR_NOFOLLOW - } else { - 0 - }; - let res = unsafe { - libc::getxattr( - path.as_ptr(), - XATTR_KEY.as_ptr() as *const i8, - buf.as_mut_ptr() as *mut libc::c_void, - buf.len(), - 0, - options, - ) - }; - if res < 0 { - debug!("fget_xattr error: {res}"); - return Ok((None, None, None)); - } - - buf.resize(res as usize, 0); - - get_xattr_common(&buf) -} - -fn is_valid_owner(owner: Option<(u32, u32)>) -> bool { - if let Some(owner) = owner { - if owner.0 < UID_MAX && owner.1 < UID_MAX { - return true; - } - } - - false -} - -// We won't need this once expressions like "if let ... &&" are allowed. -#[allow(clippy::unnecessary_unwrap)] -fn set_xattr_stat( - ctx: &Context, - file: &InodeHandle, - st: Option, - owner: Option<(u32, u32)>, - mode: Option, -) -> io::Result<()> { - let st = st.unwrap_or(istat(ctx, PermissionSemantics::LinuxComplete, file, true)?); - let options = if (st.st_mode & libc::S_IFMT) == libc::S_IFLNK { - libc::XATTR_NOFOLLOW - } else { - 0 - }; - - let buf = if is_valid_owner(owner) && mode.is_some() { - let owner = owner.unwrap(); - let mode = mode.unwrap(); - format!("{}:{}:0{:o}", owner.0, owner.1, mode) - } else { - let (orig_uid, orig_gid, orig_mode) = match file { - InodeHandle::Fd(fd) => get_xattr_fstat(*fd, st)?, - InodeHandle::Path(ref c_path) => get_xattr_lstat(c_path, st)?, - }; - - let (uid, gid) = match owner { - Some(o) => { - let uid = if o.0 < UID_MAX { Some(o.0) } else { orig_uid }; - let gid = if o.1 < UID_MAX { Some(o.1) } else { orig_gid }; - (uid, gid) - } - None => (orig_uid, orig_gid), - }; - - let mut buf = String::new(); - if let Some(uid) = uid { - buf.push_str(&format!("{uid}")); - } else { - buf.push('x'); - } - if let Some(gid) = gid { - buf.push_str(&format!(":{gid}:")); - } else { - buf.push_str(":x:"); - } - if let Some(mode) = mode { - buf.push_str(&format!("0{:o}", mode)); - } else if let Some(orig_mode) = orig_mode { - buf.push_str(&format!("0{:o}", orig_mode)); - } else { - buf.push('x'); - } - buf - }; - - let res = match file { - InodeHandle::Path(path) => unsafe { - libc::setxattr( - path.as_ptr(), - XATTR_KEY.as_ptr() as *const i8, - buf.as_ptr() as *mut libc::c_void, - buf.len() as libc::size_t, - 0, - options, - ) - }, - InodeHandle::Fd(fd) => unsafe { - libc::fsetxattr( - *fd, - XATTR_KEY.as_ptr() as *const i8, - buf.as_ptr() as *mut libc::c_void, - buf.len() as libc::size_t, - 0, - options, - ) - }, - }; - - if res < 0 { - Err(linux_error(io::Error::last_os_error())) - } else { - Ok(()) - } -} - -fn set_host_stat( - file: &InodeHandle, - _owner: Option<(u32, u32)>, - mode: Option, -) -> io::Result<()> { - // We're only using set_host_stat for LinuxSimplified semantics, and in this - // mode we ignore the host's owner bits, so don't attempt to write them here. - - if let Some(mode) = mode { - let res = match file { - InodeHandle::Path(path) => unsafe { libc::chmod(path.as_ptr(), mode as u16) }, - InodeHandle::Fd(fd) => unsafe { libc::fchmod(*fd, mode as u16) }, - }; - - if res < 0 { - return Err(linux_error(io::Error::last_os_error())); - } - } - - Ok(()) -} - -fn set_stat( - ctx: &Context, - semantics: PermissionSemantics, - file: &InodeHandle, - st: Option, - owner: Option<(u32, u32)>, - mode: Option, -) -> io::Result<()> { - match semantics { - PermissionSemantics::LinuxComplete => set_xattr_stat(ctx, file, st, owner, mode), - PermissionSemantics::LinuxSimplified => set_host_stat(file, owner, mode), - } -} - -fn stat_xattr_common( - st: &mut bindings::stat64, - uid: Option, - gid: Option, - mode: Option, -) -> io::Result { - if let Some(uid) = uid { - st.st_uid = uid; - } - if let Some(gid) = gid { - st.st_gid = gid; - } - if let Some(mode) = mode { - if mode as u16 & libc::S_IFMT == 0 { - st.st_mode = (st.st_mode & libc::S_IFMT) | mode as u16; - } else { - st.st_mode = mode as u16; - } - } - - Ok(*st) -} - -fn fstat( - ctx: &Context, - semantics: PermissionSemantics, - fd: RawFd, - host: bool, -) -> io::Result { - let mut st = MaybeUninit::::zeroed(); - - // Safe because the kernel will only write data in `st` and we check the return - // value. - let res = unsafe { libc::fstat(fd, st.as_mut_ptr()) }; - if res >= 0 { - // Safe because the kernel guarantees that the struct is now fully initialized. - let mut st = unsafe { st.assume_init() }; - if !host { - match semantics { - PermissionSemantics::LinuxComplete => { - let (uid, gid, mode) = get_xattr_fstat(fd, st)?; - stat_xattr_common(&mut st, uid, gid, mode) - } - PermissionSemantics::LinuxSimplified => { - st.st_uid = ctx.uid; - st.st_gid = ctx.gid; - Ok(st) - } - } - } else { - Ok(st) - } - } else { - Err(linux_error(io::Error::last_os_error())) - } -} - -fn lstat( - ctx: &Context, - semantics: PermissionSemantics, - c_path: &CString, - host: bool, -) -> io::Result { - let mut st = MaybeUninit::::zeroed(); - - // Safe because the kernel will only write data in `st` and we check the return - // value. - let res = unsafe { libc::lstat(c_path.as_ptr(), st.as_mut_ptr()) }; - if res >= 0 { - // Safe because the kernel guarantees that the struct is now fully initialized. - let mut st = unsafe { st.assume_init() }; - if !host { - match semantics { - PermissionSemantics::LinuxComplete => { - let (uid, gid, mode) = get_xattr_lstat(c_path, st)?; - stat_xattr_common(&mut st, uid, gid, mode) - } - PermissionSemantics::LinuxSimplified => { - st.st_uid = ctx.uid; - st.st_gid = ctx.gid; - Ok(st) - } - } - } else { - Ok(st) - } - } else { - Err(linux_error(io::Error::last_os_error())) - } -} - -fn istat( - ctx: &Context, - semantics: PermissionSemantics, - ihandle: &InodeHandle, - host: bool, -) -> io::Result { - match ihandle { - InodeHandle::Fd(fd) => fstat(ctx, semantics, *fd, host), - InodeHandle::Path(ref c_path) => lstat(ctx, semantics, c_path, host), - } -} - -/// The caching policy that the file system should report to the FUSE client. By default the FUSE -/// protocol uses close-to-open consistency. This means that any cached contents of the file are -/// invalidated the next time that file is opened. -#[derive(Debug, Default, Clone)] -pub enum CachePolicy { - /// The client should never cache file data and all I/O should be directly forwarded to the - /// server. This policy must be selected when file contents may change without the knowledge of - /// the FUSE client (i.e., the file system does not have exclusive access to the directory). - Never, - - /// The client is free to choose when and how to cache file data. This is the default policy and - /// uses close-to-open consistency as described in the enum documentation. - #[default] - Auto, - - /// The client should always cache file data. This means that the FUSE client will not - /// invalidate any cached data that was returned by the file system the last time the file was - /// opened. This policy should only be selected when the file system has exclusive access to the - /// directory. - Always, -} - -impl FromStr for CachePolicy { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - match s { - "never" | "Never" | "NEVER" => Ok(CachePolicy::Never), - "auto" | "Auto" | "AUTO" => Ok(CachePolicy::Auto), - "always" | "Always" | "ALWAYS" => Ok(CachePolicy::Always), - _ => Err("invalid cache policy"), - } - } -} - -/// The permission semantics to be emulated by this file system personality. -#[derive(Debug, Default, Clone, Copy)] -pub enum PermissionSemantics { - /// Be as close as possible to the common semantics of Linux file systems. - #[default] - LinuxComplete, - - /// As `LinuxComplete`, with the following simplifications: - /// - Extended attributes are not supported. - /// - Idmaps are not supported. - /// - Ownership bits are ignored, always returning the uid/gid from the process - /// requesting the operation within the guest (obtained from `Context`). - /// - Permissions bits are stored in the host, not as extended attributes. - LinuxSimplified, -} - -impl TryFrom for PermissionSemantics { - type Error = (); - - fn try_from(semantics: u32) -> Result { - match semantics { - 0 => Ok(PermissionSemantics::LinuxComplete), - 1 => Ok(PermissionSemantics::LinuxSimplified), - _ => Err(()), - } - } -} - -/// Options that configure the behavior of the file system. -#[derive(Debug, Clone)] -pub struct Config { - /// How long the FUSE client should consider directory entries to be valid. If the contents of a - /// directory can only be modified by the FUSE client (i.e., the file system has exclusive - /// access), then this should be a large value. - /// - /// The default value for this option is 5 seconds. - pub entry_timeout: Duration, - - /// How long the FUSE client should consider file and directory attributes to be valid. If the - /// attributes of a file or directory can only be modified by the FUSE client (i.e., the file - /// system has exclusive access), then this should be set to a large value. - /// - /// The default value for this option is 5 seconds. - pub attr_timeout: Duration, - - /// The caching policy the file system should use. See the documentation of `CachePolicy` for - /// more details. - pub cache_policy: CachePolicy, - - /// Whether the file system should enabled writeback caching. This can improve performance as it - /// allows the FUSE client to cache and coalesce multiple writes before sending them to the file - /// system. However, enabling this option can increase the risk of data corruption if the file - /// contents can change without the knowledge of the FUSE client (i.e., the server does **NOT** - /// have exclusive access). Additionally, the file system should have read access to all files - /// in the directory it is serving as the FUSE client may send read requests even for files - /// opened with `O_WRONLY`. - /// - /// Therefore callers should only enable this option when they can guarantee that: 1) the file - /// system has exclusive access to the directory and 2) the file system has read permissions for - /// all files in that directory. - /// - /// The default value for this option is `false`. - pub writeback: bool, - - /// The path of the root directory. - /// - /// The default is `/`. - pub root_dir: String, - - /// Whether the file system should support Extended Attributes (xattr). Enabling this feature may - /// have a significant impact on performance, especially on write parallelism. This is the result - /// of FUSE attempting to remove the special file privileges after each write request. - /// - /// The default value for this options is `false`. - pub xattr: bool, - - /// Optional file descriptor for /proc/self/fd. Callers can obtain a file descriptor and pass it - /// here, so there's no need to open it in PassthroughFs::new(). This is specially useful for - /// sandboxing. - /// - /// The default is `None`. - pub proc_sfd_rawfd: Option, - - /// ID of this filesystem to uniquely identify exports. Not supported for macos. - pub export_fsid: u64, - - /// Table of exported FDs to share with other subsystems. Not supported for macos. - pub export_table: Option, - - /// The permission semantics to be emulated. See the documentation for `PermissionSemantics` for - /// more details. - pub semantics: PermissionSemantics, -} - -impl Default for Config { - fn default() -> Self { - Config { - entry_timeout: Duration::from_secs(5), - attr_timeout: Duration::from_secs(5), - cache_policy: Default::default(), - writeback: false, - root_dir: String::from("/"), - xattr: true, - proc_sfd_rawfd: None, - export_fsid: 0, - export_table: None, - semantics: PermissionSemantics::LinuxComplete, - } - } -} - -/// A file system that simply "passes through" all requests it receives to the underlying file -/// system. To keep the implementation simple it servers the contents of its root directory. Users -/// that wish to serve only a specific directory should set up the environment so that that -/// directory ends up as the root of the file system process. One way to accomplish this is via a -/// combination of mount namespaces and the pivot_root system call. -pub struct PassthroughFs { - inodes: RwLock>>, - inode_alloc: Arc, - - handles: RwLock>>, - next_handle: AtomicU64, - - map_windows: Mutex>, - - // Whether writeback caching is enabled for this directory. This will only be true when - // `cfg.writeback` is true and `init` was called with `FsOptions::WRITEBACK_CACHE`. - writeback: AtomicBool, - announce_submounts: AtomicBool, - cfg: Config, -} - -impl PassthroughFs { - pub fn new(cfg: Config, inode_alloc: Arc) -> io::Result { - let root = CString::new(cfg.root_dir.as_str()).expect("CString::new failed"); - - // Safe because this doesn't modify any memory and we check the return value. - let fd = unsafe { - libc::openat( - libc::AT_FDCWD, - root.as_ptr(), - libc::O_NOFOLLOW | libc::O_CLOEXEC, - ) - }; - if fd < 0 { - return Err(linux_error(io::Error::last_os_error())); - } - - unsafe { libc::close(fd) }; - - Ok(PassthroughFs { - inodes: RwLock::new(MultikeyBTreeMap::new()), - inode_alloc, - - handles: RwLock::new(BTreeMap::new()), - next_handle: AtomicU64::new(1), - - map_windows: Mutex::new(HashMap::new()), - - writeback: AtomicBool::new(false), - announce_submounts: AtomicBool::new(false), - cfg, - }) - } - - fn inode_to_handle(&self, inode: Inode, supports_fd: bool) -> io::Result { - debug!("inode_to_handle: inode={inode}"); - let data = self - .inodes - .read() - .unwrap() - .get(&inode) - .cloned() - .ok_or_else(ebadf)?; - - let cstr = - CString::new(format!("/.vol/{}/{}", data.dev, data.ino)).map_err(|_| einval())?; - debug!("inode_to_handle: path={}", cstr.to_string_lossy()); - - if supports_fd { - let unlinked_fd = data.unlinked_fd.load(Ordering::Acquire); - if unlinked_fd >= 0 { - return Ok(InodeHandle::Fd(unlinked_fd as RawFd)); - } - } - - Ok(InodeHandle::Path(cstr)) - } - - fn name_to_path(&self, parent: Inode, name: &CStr) -> io::Result { - debug!( - "name_to_path: parent={} name={}", - parent, - name.to_string_lossy() - ); - let data = self - .inodes - .read() - .unwrap() - .get(&parent) - .cloned() - .ok_or_else(ebadf)?; - - let cstr = CString::new(format!( - "/.vol/{}/{}/{}", - data.dev, - data.ino, - name.to_string_lossy() - )) - .map_err(|_| einval())?; - debug!("name_to_path: path={}", cstr.to_string_lossy()); - Ok(cstr) - } - - fn open_inode(&self, inode: Inode, mut flags: i32) -> io::Result { - // When writeback caching is enabled, the kernel may send read requests even if the - // userspace program opened the file write-only. So we need to ensure that we have opened - // the file for reading as well as writing. - let writeback = self.writeback.load(Ordering::Relaxed); - if writeback && flags & libc::O_ACCMODE == libc::O_WRONLY { - flags &= !libc::O_ACCMODE; - flags |= libc::O_RDWR; - } - - // When writeback caching is enabled the kernel is responsible for handling `O_APPEND`. - // However, this breaks atomicity as the file may have changed on disk, invalidating the - // cached copy of the data in the kernel and the offset that the kernel thinks is the end of - // the file. Just allow this for now as it is the user's responsibility to enable writeback - // caching only for directories that are not shared. It also means that we need to clear the - // `O_APPEND` flag. - if writeback && flags & libc::O_APPEND != 0 { - flags &= !libc::O_APPEND; - } - - let ihandle = self.inode_to_handle(inode, true)?; - let fd = match ihandle { - InodeHandle::Path(c_path) => unsafe { - libc::open( - c_path.as_ptr(), - (flags | libc::O_CLOEXEC) & (!libc::O_NOFOLLOW) & (!libc::O_EXLOCK), - ) - }, - // Check if we have recently unlinked the inode and kept open a file descriptor to it. - InodeHandle::Fd(fd) => unsafe { libc::dup(fd) }, - }; - if fd < 0 { - return Err(linux_error(io::Error::last_os_error())); - } - - // Safe because we just opened this fd. - Ok(unsafe { File::from_raw_fd(fd) }) - } - - fn do_readdir( - &self, - inode: Inode, - handle: Handle, - size: u32, - mut offset: u64, - mut add_entry: F, - ) -> io::Result<()> - where - F: FnMut(DirEntry) -> io::Result, - { - if size == 0 { - return Ok(()); - } - - let data = self - .handles - .read() - .unwrap() - .get(&handle) - .filter(|hd| hd.inode == inode) - .cloned() - .ok_or_else(ebadf)?; - - let mut ds = data.dirstream.lock().unwrap(); - - // We use offset == 0 as an indicator of this being either a fresh directory - // stream or a stream that has been rewound. If that's the case, make sure - // the cache will be refreshed. - if offset == 0 && ds.ready { - let fd = data.file.write().unwrap().as_raw_fd(); - unsafe { libc::lseek(fd, 0, libc::SEEK_SET) }; - ds.entries.clear(); - ds.ready = false; - } - - if !ds.ready { - // Fill the cache on first call - if let Err(e) = ds.fill_from_fd(data.file.write().unwrap().as_raw_fd()) { - if ds.entries.is_empty() { - return Err(e); - } - // If we got some valid entries before error happened, - // treat this partial read as success and just log - // the error. - warn!("virtio-fs: error in readdir {}: {:?}", inode, e); - } - ds.ready = true; - } - - while let Some(entry) = ds.get_entry(offset) { - offset += 1; - - let name = entry.name; - match add_entry(entry) { - Ok(size) => { - if size == 0 { - break; - } - } - Err(e) => { - warn!( - "virtio-fs: error adding entry {}: {:?}", - String::from_utf8_lossy(name), - e - ); - break; - } - } - } - - Ok(()) - } - - fn do_open( - &self, - ctx: &Context, - inode: Inode, - kill_priv: bool, - flags: u32, - ) -> io::Result<(Option, OpenOptions)> { - let flags = self.parse_open_flags(flags as i32); - - let file = RwLock::new(self.open_inode(inode, flags)?); - - // If O_TRUNC and kill_priv (OPEN_KILL_SUIDGID), clear security.capability and suid/sgid - if (flags & libc::O_TRUNC) != 0 && kill_priv { - let fd = file.read().unwrap().as_raw_fd(); - let ihandle = InodeHandle::Fd(fd); - - remove_security_capability(&ihandle); - - if let Ok(st) = fstat(ctx, self.cfg.semantics, fd, false) { - let new_mode = clear_suid_sgid(st.st_mode as u32); - if new_mode != st.st_mode as u32 { - if let Err(err) = set_stat( - ctx, - self.cfg.semantics, - &ihandle, - Some(st), - None, - Some(new_mode), - ) { - error!("Couldn't clear suid/sgid for inode {inode}: {err}"); - } - } - } - } - - let handle = self.next_handle.fetch_add(1, Ordering::Relaxed); - let data = HandleData { - inode, - file, - dirstream: Mutex::new(DirStream::new()), - }; - - self.handles.write().unwrap().insert(handle, Arc::new(data)); - - let mut opts = OpenOptions::empty(); - match self.cfg.cache_policy { - // We only set the direct I/O option on files. - CachePolicy::Never => opts.set(OpenOptions::DIRECT_IO, flags & libc::O_DIRECTORY == 0), - CachePolicy::Always => { - if flags & libc::O_DIRECTORY == 0 { - opts |= OpenOptions::KEEP_CACHE; - } else { - opts |= OpenOptions::CACHE_DIR; - } - } - _ => {} - }; - - Ok((Some(handle), opts)) - } - - fn do_release(&self, inode: Inode, handle: Handle) -> io::Result<()> { - let mut handles = self.handles.write().unwrap(); - - if let btree_map::Entry::Occupied(e) = handles.entry(handle) { - if e.get().inode == inode { - // We don't need to close the file here because that will happen automatically when - // the last `Arc` is dropped. - e.remove(); - return Ok(()); - } - } - - Err(ebadf()) - } - - fn do_getattr(&self, ctx: &Context, inode: Inode) -> io::Result<(bindings::stat64, Duration)> { - let ihandle = self.inode_to_handle(inode, true)?; - let st = match ihandle { - InodeHandle::Path(c_path) => lstat(ctx, self.cfg.semantics, &c_path, false)?, - InodeHandle::Fd(fd) => fstat(ctx, self.cfg.semantics, fd, false)?, - }; - - Ok((st, self.cfg.attr_timeout)) - } - - fn grab_unlinked_fd(&self, parent_fd: RawFd, name: &CStr) -> io::Result { - let fd = - unsafe { libc::openat(parent_fd, name.as_ptr(), libc::O_NOFOLLOW | libc::O_CLOEXEC) }; - if fd < 0 { - return Err(io::Error::last_os_error()); - } - Ok(fd) - } - - fn store_unlinked_fd(&self, ctx: &Context, unlinked_fd: RawFd) -> io::Result { - let st = fstat(ctx, self.cfg.semantics, unlinked_fd, true)?; - let altkey = InodeAltKey { - ino: st.st_ino, - dev: st.st_dev, - }; - // Hold the read lock across the swap: dropping it earlier would let a - // concurrent `forget` remove this inode (closing its then-`-1` - // `unlinked_fd`) between our lookup and swap, leaking the fd we store. - let inodes = self.inodes.read().unwrap(); - if let Some(data) = inodes.get_alt(&altkey) { - // Swap rather than store so that if this inode already had a - // preserved fd (e.g. another hard link was unlinked/overwritten - // earlier), we recover and close it instead of leaking it. - let old_fd = data.unlinked_fd.swap(unlinked_fd as i64, Ordering::AcqRel); - if old_fd >= 0 { - unsafe { libc::close(old_fd as RawFd) }; - } - // The tracked inode now owns `unlinked_fd` (closed in `forget_one`). - Ok(true) - } else { - // No tracked inode for this (dev, ino): the caller keeps ownership - // of `unlinked_fd` and must close it to avoid a leak. - Ok(false) - } - } - - fn do_unlink( - &self, - ctx: Context, - parent: Inode, - name: &CStr, - flags: libc::c_int, - ) -> io::Result<()> { - let ihandle = self.inode_to_handle(parent, true)?; - - let (fd, close_fd) = match ihandle { - InodeHandle::Path(c_path) => unsafe { - ( - libc::open(c_path.as_ptr(), libc::O_NOFOLLOW | libc::O_CLOEXEC), - true, - ) - }, - InodeHandle::Fd(fd) => (fd, false), - }; - if fd < 0 { - return Err(io::Error::last_os_error()); - } - - // After unlinking this inode, we can't keep relying on getting a "/.vol/..." path - // to operate on it. Before unlinking the inode, grab a file descriptor so we can - // still operate on it. This one will be closed on "forget_one". - let unlinked_fd = match self.grab_unlinked_fd(fd, name) { - Ok(fd) => Some(fd), - Err(err) => { - warn!( - "Couldn't grab a file descriptor for file \"{}\": {err}", - name.to_string_lossy() - ); - None - } - }; - - // Safe because this doesn't modify any memory and we check the return value. - let res = unsafe { libc::unlinkat(fd, name.as_ptr(), flags) }; - let err = io::Error::last_os_error(); - - if close_fd { - unsafe { libc::close(fd) }; - } - - if res == 0 { - if let Some(unlinked_fd) = unlinked_fd { - match self.store_unlinked_fd(&ctx, unlinked_fd) { - // The tracked inode took ownership of the fd. - Ok(true) => {} - // No tracked inode: we still own the fd and must close it. - Ok(false) => unsafe { - libc::close(unlinked_fd); - }, - Err(err) => { - unsafe { libc::close(unlinked_fd) }; - warn!("Couldn't store unlinked fd \"{}\": {err}", unlinked_fd); - } - } - } - Ok(()) - } else { - if let Some(unlinked_fd) = unlinked_fd { - unsafe { libc::close(unlinked_fd) }; - } - Err(linux_error(err)) - } - } - - #[allow(clippy::too_many_arguments)] - fn mknod_complete( - &self, - ctx: Context, - parent: Inode, - name: &CStr, - mode: u32, - _rdev: u32, - umask: u32, - extensions: Extensions, - ) -> io::Result { - let c_path = self.name_to_path(parent, name)?; - - let fd = unsafe { - libc::open( - c_path.as_ptr(), - libc::O_CREAT | libc::O_CLOEXEC | libc::O_NOFOLLOW, - 0o600, - ) - }; - if fd < 0 { - Err(linux_error(io::Error::last_os_error())) - } else { - let ihandle = InodeHandle::Fd(fd); - - // Set security context - if let Some(secctx) = extensions.secctx { - set_secctx(&ihandle, secctx, false)? - }; - - // For mknod, we're forced to store the mode as xattr even in - // simplified mode, since macOS doesn't allow unprivileged users - // to create special files (such as sockets or fifos) using mknod. - if let Err(e) = set_xattr_stat( - &ctx, - &ihandle, - None, - Some((ctx.uid, ctx.gid)), - Some(mode & !umask), - ) { - unsafe { libc::close(fd) }; - return Err(e); - } - - unsafe { libc::close(fd) }; - self.lookup(ctx, parent, name) - } - } - - #[allow(clippy::too_many_arguments)] - fn mknod_simplified( - &self, - ctx: Context, - parent: Inode, - name: &CStr, - mode: u32, - rdev: u32, - umask: u32, - extensions: Extensions, - ) -> io::Result { - let c_path = self.name_to_path(parent, name)?; - - // macOS doesn't allow us to create UNIX sockets using macOS, so we - // have to resort to actually creating the socket ourselves and - // dropping it. - if (mode as u16 & libc::S_IFMT) == libc::S_IFSOCK { - let path = c_path.to_str().map_err(|_| einval())?; - let listener = UnixListener::bind(Path::new(path)).map_err(|_| einval())?; - // Explicitly drop the listener to make it clear we aren't going - // to use it. UnixListener's Drop doesn't remove the socket it - // created, so we can reuse for the guest. - drop(listener); - } else { - let res = unsafe { libc::mknod(c_path.as_ptr(), (mode & !umask) as u16, rdev as i32) }; - if res < 0 { - return Err(linux_error(io::Error::last_os_error())); - } - } - - // Set security context - if let Some(secctx) = extensions.secctx { - let ihandle = InodeHandle::Path(c_path); - set_secctx(&ihandle, secctx, false)? - }; - self.lookup(ctx, parent, name) - } - - fn parse_open_flags(&self, flags: i32) -> i32 { - let mut mflags: i32 = flags & 0b11; - - if (flags & bindings::LINUX_O_NONBLOCK) != 0 { - mflags |= libc::O_NONBLOCK; - } - if (flags & bindings::LINUX_O_APPEND) != 0 { - mflags |= libc::O_APPEND; - } - if (flags & bindings::LINUX_O_CREAT) != 0 { - mflags |= libc::O_CREAT; - } - if (flags & bindings::LINUX_O_TRUNC) != 0 { - mflags |= libc::O_TRUNC; - } - if (flags & bindings::LINUX_O_EXCL) != 0 { - mflags |= libc::O_EXCL; - } - if (flags & bindings::LINUX_O_NOFOLLOW) != 0 { - mflags |= libc::O_NOFOLLOW; - } - if (flags & bindings::LINUX_O_CLOEXEC) != 0 { - mflags |= libc::O_CLOEXEC; - } - - mflags - } -} - -fn set_secctx(file: &InodeHandle, secctx: SecContext, symlink: bool) -> io::Result<()> { - let options = if symlink { libc::XATTR_NOFOLLOW } else { 0 }; - let ret = match file { - InodeHandle::Path(path) => unsafe { - libc::setxattr( - path.as_ptr(), - secctx.name.as_ptr(), - secctx.secctx.as_ptr() as *const libc::c_void, - secctx.secctx.len(), - 0, - options, - ) - }, - InodeHandle::Fd(fd) => unsafe { - libc::fsetxattr( - *fd, - secctx.name.as_ptr(), - secctx.secctx.as_ptr() as *const libc::c_void, - secctx.secctx.len(), - 0, - options, - ) - }, - }; - - if ret != 0 { - Err(io::Error::last_os_error()) - } else { - Ok(()) - } -} - -/// Remove the security.capability extended attribute -fn remove_security_capability(file: &InodeHandle) { - let ret = match file { - InodeHandle::Path(path) => unsafe { - libc::removexattr(path.as_ptr(), SECURITY_CAPABILITY.as_ptr() as *const i8, 0) - }, - InodeHandle::Fd(fd) => unsafe { - libc::fremovexattr(*fd, SECURITY_CAPABILITY.as_ptr() as *const i8, 0) - }, - }; - - // ENODATA means the attribute didn't exist, which is fine - if ret != 0 && io::Error::last_os_error().raw_os_error() != Some(libc::ENODATA) { - warn!("Error removing security.capability from file"); - } -} - -/// Clear suid/sgid bits from mode. -/// sgid is cleared only if group executable bit is set. -fn clear_suid_sgid(mode: u32) -> u32 { - let mut new_mode = mode; - - // Clear suid bit - new_mode &= !libc::S_ISUID as u32; - - // Clear sgid bit only if group executable bit is set - if (mode & libc::S_IXGRP as u32) != 0 { - new_mode &= !libc::S_ISGID as u32; - } - - new_mode -} - -fn forget_one( - inodes: &mut MultikeyBTreeMap>, - inode: Inode, - count: u64, -) { - if let Some(data) = inodes.get(&inode) { - // Acquiring the write lock on the inode map prevents new lookups from incrementing the - // refcount but there is the possibility that a previous lookup already acquired a - // reference to the inode data and is in the process of updating the refcount so we need - // to loop here until we can decrement successfully. - loop { - let refcount = data.refcount.load(Ordering::Relaxed); - - // Saturating sub because it doesn't make sense for a refcount to go below zero and - // we don't want misbehaving clients to cause integer overflow. - let new_count = refcount.saturating_sub(count); - - // Synchronizes with the acquire load in `lookup`. - if data - .refcount - .compare_exchange(refcount, new_count, Ordering::Release, Ordering::Relaxed) - .unwrap() - == refcount - { - if new_count == 0 { - // If we have unlinked this inode, we have opened a file descriptor to be - // able to operate on it without a path. Close it now. - let fd = data.unlinked_fd.load(Ordering::Acquire); - if fd >= 0 { - unsafe { libc::close(fd as RawFd) }; - } - // We just removed the last refcount for this inode. There's no need for an - // acquire fence here because we hold a write lock on the inode map and any - // thread that is waiting to do a forget on the same inode will have to wait - // until we release the lock. So there's is no other release store for us to - // synchronize with before deleting the entry. - inodes.remove(&inode); - } - break; - } - } - } -} - -impl FileSystem for PassthroughFs { - type Inode = Inode; - type Handle = Handle; - - fn init(&self, capable: FsOptions) -> io::Result { - let root = CString::new(self.cfg.root_dir.as_str()).expect("CString::new failed"); - - // Safe because this doesn't modify any memory and we check the return value. - // We use `O_PATH` because we just want this for traversing the directory tree - // and not for actually reading the contents. - let fd = unsafe { - libc::openat( - libc::AT_FDCWD, - root.as_ptr(), - libc::O_NOFOLLOW | libc::O_CLOEXEC, - ) - }; - if fd < 0 { - return Err(io::Error::last_os_error()); - } - - // Safe because we just opened this fd above. - let f = unsafe { File::from_raw_fd(fd) }; - - // Build a fake Context for fstat, it won't be using it anyways - // as it'll be only looking at the host's bits. - let ctx = Context { - uid: 0, - gid: 0, - pid: 0, - }; - let st = fstat(&ctx, self.cfg.semantics, f.as_raw_fd(), true)?; - - // Safe because this doesn't modify any memory and there is no need to check the return - // value because this system call always succeeds. We need to clear the umask here because - // we want the client to be able to set all the bits in the mode. - unsafe { libc::umask(0o000) }; - - let mut inodes = self.inodes.write().unwrap(); - - // Not sure why the root inode gets a refcount of 2 but that's what libfuse does. - inodes.insert( - fuse::ROOT_ID, - InodeAltKey { - ino: st.st_ino, - dev: st.st_dev, - }, - Arc::new(InodeData { - inode: fuse::ROOT_ID, - ino: st.st_ino, - dev: st.st_dev, - refcount: AtomicU64::new(2), - unlinked_fd: AtomicI64::new(-1), - }), - ); - - let mut opts = FsOptions::empty(); - if self.cfg.writeback && capable.contains(FsOptions::WRITEBACK_CACHE) { - opts |= FsOptions::WRITEBACK_CACHE; - self.writeback.store(true, Ordering::Relaxed); - } - - if capable.contains(FsOptions::SUBMOUNTS) { - opts |= FsOptions::SUBMOUNTS; - self.announce_submounts.store(true, Ordering::Relaxed); - } - - Ok(opts) - } - - fn destroy(&self) { - self.handles.write().unwrap().clear(); - self.inodes.write().unwrap().clear(); - } - - fn statfs(&self, _ctx: Context, inode: Inode) -> io::Result { - let mut out = MaybeUninit::::zeroed(); - - let res = match self.inode_to_handle(inode, true)? { - InodeHandle::Path(c_path) => unsafe { - bindings::statvfs64(c_path.as_ptr(), out.as_mut_ptr()) - }, - InodeHandle::Fd(fd) => unsafe { bindings::fstatvfs64(fd, out.as_mut_ptr()) }, - }; - if res == 0 { - // Safe because the kernel guarantees that `out` has been initialized. - Ok(unsafe { out.assume_init() }) - } else { - Err(linux_error(io::Error::last_os_error())) - } - } - - fn lookup(&self, ctx: Context, parent: Inode, name: &CStr) -> io::Result { - let parent_data = self - .inodes - .read() - .unwrap() - .get(&parent) - .cloned() - .ok_or_else(ebadf)?; - - let c_path = self.name_to_path(parent, name)?; - let st = lstat(&ctx, self.cfg.semantics, &c_path, false)?; - - debug!( - "lookup: inode={} path={}", - st.st_ino, - c_path.to_str().unwrap() - ); - - let mut attr_flags: u32 = 0; - - if st.st_mode & libc::S_IFMT == libc::S_IFDIR - && self.announce_submounts.load(Ordering::Relaxed) - && (st.st_dev != parent_data.dev) - { - attr_flags |= fuse::ATTR_SUBMOUNT; - } - - let altkey = InodeAltKey { - ino: st.st_ino, - dev: st.st_dev, - }; - let data = self.inodes.read().unwrap().get_alt(&altkey).cloned(); - - let inode = if let Some(data) = data { - // Matches with the release store in `forget`. - data.refcount.fetch_add(1, Ordering::Acquire); - data.inode - } else { - // There is a possible race here where 2 threads end up adding the same file - // into the inode list. However, since each of those will get a unique Inode - // value and unique file descriptors this shouldn't be that much of a problem. - let inode = self.inode_alloc.next(); - self.inodes.write().unwrap().insert( - inode, - InodeAltKey { - ino: st.st_ino, - dev: st.st_dev, - }, - Arc::new(InodeData { - inode, - ino: st.st_ino, - dev: st.st_dev, - refcount: AtomicU64::new(1), - unlinked_fd: AtomicI64::new(-1), - }), - ); - - inode - }; - - Ok(Entry { - inode, - generation: 0, - attr: st, - attr_flags, - attr_timeout: self.cfg.attr_timeout, - entry_timeout: self.cfg.entry_timeout, - }) - } - - fn forget(&self, _ctx: Context, inode: Inode, count: u64) { - let mut inodes = self.inodes.write().unwrap(); - - forget_one(&mut inodes, inode, count) - } - - fn batch_forget(&self, _ctx: Context, requests: Vec<(Inode, u64)>) { - let mut inodes = self.inodes.write().unwrap(); - - for (inode, count) in requests { - forget_one(&mut inodes, inode, count) - } - } - - fn opendir( - &self, - ctx: Context, - inode: Inode, - flags: u32, - ) -> io::Result<(Option, OpenOptions)> { - self.do_open(&ctx, inode, false, flags | libc::O_DIRECTORY as u32) - } - - fn releasedir( - &self, - _ctx: Context, - inode: Inode, - _flags: u32, - handle: Handle, - ) -> io::Result<()> { - self.do_release(inode, handle) - } - - fn mkdir( - &self, - ctx: Context, - parent: Inode, - name: &CStr, - mode: u32, - umask: u32, - extensions: Extensions, - ) -> io::Result { - let c_path = self.name_to_path(parent, name)?; - - let (host_mode, complete) = match self.cfg.semantics { - PermissionSemantics::LinuxComplete => (0o700, true), - PermissionSemantics::LinuxSimplified => ((mode & !umask) as u16, false), - }; - - // Safe because this doesn't modify any memory and we check the return value. - let res = unsafe { libc::mkdir(c_path.as_ptr(), host_mode) }; - if res == 0 { - let ihandle = InodeHandle::Path(c_path); - // Set security context - if let Some(secctx) = extensions.secctx { - set_secctx(&ihandle, secctx, false)? - }; - - if complete { - set_stat( - &ctx, - self.cfg.semantics, - &ihandle, - None, - Some((ctx.uid, ctx.gid)), - Some(mode & !umask), - )?; - } - self.lookup(ctx, parent, name) - } else { - Err(linux_error(io::Error::last_os_error())) - } - } - - fn rmdir(&self, ctx: Context, parent: Inode, name: &CStr) -> io::Result<()> { - self.do_unlink(ctx, parent, name, libc::AT_REMOVEDIR) - } - - fn readdir( - &self, - _ctx: Context, - inode: Inode, - handle: Handle, - size: u32, - offset: u64, - add_entry: F, - ) -> io::Result<()> - where - F: FnMut(DirEntry) -> io::Result, - { - self.do_readdir(inode, handle, size, offset, add_entry) - } - - fn readdirplus( - &self, - ctx: Context, - inode: Inode, - handle: Handle, - size: u32, - offset: u64, - mut add_entry: F, - ) -> io::Result<()> - where - F: FnMut(DirEntry, Entry) -> io::Result, - { - self.do_readdir(inode, handle, size, offset, |dir_entry| { - // Safe because the kernel guarantees that the buffer is nul-terminated. Additionally, - // the kernel will pad the name with '\0' bytes up to 8-byte alignment and there's no - // way for us to know exactly how many padding bytes there are. This would cause - // `CStr::from_bytes_with_nul` to return an error because it would think there are - // interior '\0' bytes. We trust the kernel to provide us with properly formatted data - // so we'll just skip the checks here. - let name = unsafe { CStr::from_bytes_with_nul_unchecked(dir_entry.name) }; - let entry = self.lookup(ctx, inode, name)?; - - add_entry(dir_entry, entry) - }) - } - - fn open( - &self, - ctx: Context, - inode: Inode, - kill_priv: bool, - flags: u32, - ) -> io::Result<(Option, OpenOptions)> { - self.do_open(&ctx, inode, kill_priv, flags) - } - - fn release( - &self, - _ctx: Context, - inode: Inode, - _flags: u32, - handle: Handle, - _flush: bool, - _flock_release: bool, - _lock_owner: Option, - ) -> io::Result<()> { - self.do_release(inode, handle) - } - - fn create( - &self, - ctx: Context, - parent: Inode, - name: &CStr, - mode: u32, - kill_priv: bool, - flags: u32, - umask: u32, - extensions: Extensions, - ) -> io::Result<(Entry, Option, OpenOptions)> { - let c_path = self.name_to_path(parent, name)?; - - let flags = self.parse_open_flags(flags as i32); - let (host_mode, complete) = match self.cfg.semantics { - PermissionSemantics::LinuxComplete => { - let mode = if (flags & libc::O_DIRECTORY) != 0 { - 0o700 - } else { - 0o600 - }; - (mode, true) - } - PermissionSemantics::LinuxSimplified => (mode & !(umask & 0o777), false), - }; - - // Safe because this doesn't modify any memory and we check the return value. We don't - // really check `flags` because if the kernel can't handle poorly specified flags then we - // have much bigger problems. - let fd = unsafe { - libc::open( - c_path.as_ptr(), - flags | libc::O_CREAT | libc::O_CLOEXEC | libc::O_NOFOLLOW, - host_mode, - ) - }; - if fd < 0 { - return Err(linux_error(io::Error::last_os_error())); - } - let ihandle = InodeHandle::Fd(fd); - - if complete { - if let Err(e) = set_stat( - &ctx, - self.cfg.semantics, - &ihandle, - None, - Some((ctx.uid, ctx.gid)), - Some(libc::S_IFREG as u32 | (mode & !(umask & 0o777))), - ) { - unsafe { libc::close(fd) }; - return Err(e); - } - } - - // Set security context - if let Some(secctx) = extensions.secctx { - set_secctx(&ihandle, secctx, false)? - }; - - // If O_TRUNC and kill_priv (OPEN_KILL_SUIDGID), clear security.capability. - // We don't need to clear suid/sgid here because we've just updated them - // unconditionally above. - if (flags & libc::O_TRUNC) != 0 && kill_priv { - remove_security_capability(&ihandle); - } - - // Safe because we just opened this fd. - let file = RwLock::new(unsafe { File::from_raw_fd(fd) }); - - let entry = self.lookup(ctx, parent, name)?; - - let handle = self.next_handle.fetch_add(1, Ordering::Relaxed); - let data = HandleData { - inode: entry.inode, - file, - dirstream: Mutex::new(DirStream::new()), - }; - - self.handles.write().unwrap().insert(handle, Arc::new(data)); - - let mut opts = OpenOptions::empty(); - match self.cfg.cache_policy { - CachePolicy::Never => opts |= OpenOptions::DIRECT_IO, - CachePolicy::Always => opts |= OpenOptions::KEEP_CACHE, - _ => {} - }; - - Ok((entry, Some(handle), opts)) - } - - fn unlink(&self, ctx: Context, parent: Inode, name: &CStr) -> io::Result<()> { - self.do_unlink(ctx, parent, name, 0) - } - - fn read( - &self, - _ctx: Context, - inode: Inode, - handle: Handle, - mut w: W, - size: u32, - offset: u64, - _lock_owner: Option, - _flags: u32, - ) -> io::Result { - debug!("read: {inode:?}"); - let data = self - .handles - .read() - .unwrap() - .get(&handle) - .filter(|hd| hd.inode == inode) - .cloned() - .ok_or_else(ebadf)?; - - // This is safe because write_from uses preadv64, so the underlying file descriptor - // offset is not affected by this operation. - let f = data.file.read().unwrap(); - w.write_from(&f, size as usize, offset) - } - - fn write( - &self, - ctx: Context, - inode: Inode, - handle: Handle, - mut r: R, - size: u32, - offset: u64, - _lock_owner: Option, - _delayed_write: bool, - kill_priv: bool, - _flags: u32, - ) -> io::Result { - let data = self - .handles - .read() - .unwrap() - .get(&handle) - .filter(|hd| hd.inode == inode) - .cloned() - .ok_or_else(ebadf)?; - - // This is safe because read_to uses pwritev64, so the underlying file descriptor - // offset is not affected by this operation. - let f = data.file.read().unwrap(); - let result = r.read_to(&f, size as usize, offset); - - // If write succeeded and kill_priv is set, clear security.capability and suid/sgid - if result.is_ok() && kill_priv { - let fd = f.as_raw_fd(); - let ihandle = InodeHandle::Fd(fd); - - remove_security_capability(&ihandle); - - if let Ok(st) = fstat(&ctx, self.cfg.semantics, fd, false) { - let new_mode = clear_suid_sgid(st.st_mode as u32); - if new_mode != st.st_mode as u32 { - // Update mode in xattr - if let Err(err) = set_stat( - &ctx, - self.cfg.semantics, - &ihandle, - Some(st), - None, - Some(new_mode), - ) { - error!("Couldn't clear suid/sgid for inode {inode}: {err}"); - } - } - } - } - - result - } - - fn getattr( - &self, - ctx: Context, - inode: Inode, - _handle: Option, - ) -> io::Result<(bindings::stat64, Duration)> { - self.do_getattr(&ctx, inode) - } - - fn setattr( - &self, - ctx: Context, - inode: Inode, - attr: bindings::stat64, - handle: Option, - valid: SetattrValid, - ) -> io::Result<(bindings::stat64, Duration)> { - // If we have a handle then use it otherwise get a new fd from the inode. - let ihandle = if let Some(handle) = handle { - let hd = self - .handles - .read() - .unwrap() - .get(&handle) - .filter(|hd| hd.inode == inode) - .cloned() - .ok_or_else(ebadf)?; - - let fd = hd.file.write().unwrap().as_raw_fd(); - InodeHandle::Fd(fd) - } else { - self.inode_to_handle(inode, true)? - }; - - if valid.contains(SetattrValid::MODE) { - set_stat( - &ctx, - self.cfg.semantics, - &ihandle, - None, - None, - Some(attr.st_mode as u32), - )? - } - - if valid.intersects(SetattrValid::UID | SetattrValid::GID) { - let uid = if valid.contains(SetattrValid::UID) { - attr.st_uid - } else { - // Cannot use -1 here because these are unsigned values. - u32::MAX - }; - let gid = if valid.contains(SetattrValid::GID) { - attr.st_gid - } else { - // Cannot use -1 here because these are unsigned values. - u32::MAX - }; - - remove_security_capability(&ihandle); - let st = istat(&ctx, self.cfg.semantics, &ihandle, false)?; - - // Clear suid/sgid if UID or GID is being changed - let new_mode = clear_suid_sgid(st.st_mode as u32); - let new_mode = if new_mode != st.st_mode as u32 { - Some(new_mode) - } else { - None - }; - set_stat( - &ctx, - self.cfg.semantics, - &ihandle, - Some(st), - Some((uid, gid)), - new_mode, - )?; - } - - if valid.contains(SetattrValid::SIZE) { - // Safe because this doesn't modify any memory and we check the return value. - match ihandle { - InodeHandle::Fd(fd) => { - let res = unsafe { libc::ftruncate(fd, attr.st_size) }; - if res < 0 { - return Err(linux_error(io::Error::last_os_error())); - } - - // Clear security.capability on truncate unconditionally - remove_security_capability(&ihandle); - let st = fstat(&ctx, self.cfg.semantics, fd, false)?; - let new_mode = clear_suid_sgid(st.st_mode as u32); - if new_mode != st.st_mode as u32 { - set_stat( - &ctx, - self.cfg.semantics, - &ihandle, - Some(st), - None, - Some(new_mode), - )?; - } - } - InodeHandle::Path(_) => { - // There is no `ftruncateat` so we need to get a new fd and truncate it. - let f = self.open_inode(inode, libc::O_NONBLOCK | libc::O_RDWR)?; - let res = unsafe { libc::ftruncate(f.as_raw_fd(), attr.st_size) }; - if res < 0 { - return Err(linux_error(io::Error::last_os_error())); - } - - // Clear security.capability on truncate unconditionally - // - // Do this here even if it means duplicating the code above to be able to - // reuse the FD we just opened, thus reducing the number of syscalls. - let ihandle = InodeHandle::Fd(f.as_raw_fd()); - remove_security_capability(&ihandle); - let st = istat(&ctx, self.cfg.semantics, &ihandle, false)?; - let new_mode = clear_suid_sgid(st.st_mode as u32); - if new_mode != st.st_mode as u32 { - set_stat( - &ctx, - self.cfg.semantics, - &ihandle, - Some(st), - None, - Some(new_mode), - )?; - } - } - }; - } - - if valid.intersects(SetattrValid::ATIME | SetattrValid::MTIME) { - let mut tvs = [ - libc::timespec { - tv_sec: 0, - tv_nsec: libc::UTIME_OMIT, - }, - libc::timespec { - tv_sec: 0, - tv_nsec: libc::UTIME_OMIT, - }, - ]; - - if valid.contains(SetattrValid::ATIME_NOW) { - tvs[0].tv_nsec = libc::UTIME_NOW; - } else if valid.contains(SetattrValid::ATIME) { - tvs[0].tv_sec = attr.st_atime; - tvs[0].tv_nsec = attr.st_atime_nsec; - } - - if valid.contains(SetattrValid::MTIME_NOW) { - tvs[1].tv_nsec = libc::UTIME_NOW; - } else if valid.contains(SetattrValid::MTIME) { - tvs[1].tv_sec = attr.st_mtime; - tvs[1].tv_nsec = attr.st_mtime_nsec; - } - - // Safe because this doesn't modify any memory and we check the return value. - let res = match ihandle { - InodeHandle::Fd(fd) => unsafe { libc::futimens(fd, tvs.as_ptr()) }, - InodeHandle::Path(ref c_path) => unsafe { - let fd = libc::open(c_path.as_ptr(), libc::O_SYMLINK | libc::O_CLOEXEC); - let res = libc::futimens(fd, tvs.as_ptr()); - libc::close(fd); - res - }, - }; - if res < 0 { - return Err(io::Error::last_os_error()); - } - } - - self.do_getattr(&ctx, inode) - } - - fn rename( - &self, - ctx: Context, - olddir: Inode, - oldname: &CStr, - newdir: Inode, - newname: &CStr, - flags: u32, - ) -> io::Result<()> { - let mut mflags: u32 = 0; - if ((flags as i32) & bindings::LINUX_RENAME_NOREPLACE) != 0 { - mflags |= libc::RENAME_EXCL; - } - if ((flags as i32) & bindings::LINUX_RENAME_EXCHANGE) != 0 { - mflags |= libc::RENAME_SWAP; - } - - if ((flags as i32) & bindings::LINUX_RENAME_WHITEOUT) != 0 - && ((flags as i32) & bindings::LINUX_RENAME_EXCHANGE) != 0 - { - return Err(linux_error(io::Error::from_raw_os_error(libc::EINVAL))); - } - - let old_cpath = self.name_to_path(olddir, oldname)?; - let new_cpath = self.name_to_path(newdir, newname)?; - - // macOS addresses inodes by their volfs path ("/.vol/{dev}/{ino}"), - // which only resolves while the inode still has a directory entry. A - // rename that REPLACES an existing target drops that target's last - // link, so any inode the guest still holds open there would afterwards - // resolve to a dangling volfs path and fail path-based ops - // (getattr/open/setattr/...) with ENOENT (e.g. apt/dpkg's atomic - // rewrite of /var/lib/dpkg/status, surfaced as - // "close (2: No such file or directory)"). `do_unlink` already guards - // the unlink case by stashing an fd to the doomed inode in - // `InodeData.unlinked_fd`; mirror that for the overwritten target. Grab - // it *before* the rename, while its entry still exists. RENAME_SWAP - // keeps both inodes linked and RENAME_EXCL never overwrites, so skip - // those; best-effort otherwise (a non-overwriting rename finds nothing). - let doomed_fd = if (flags as i32) - & (bindings::LINUX_RENAME_EXCHANGE | bindings::LINUX_RENAME_NOREPLACE) - == 0 - { - match self.inode_to_handle(newdir, true) { - Ok(InodeHandle::Path(newdir_cpath)) => { - let newdir_fd = unsafe { - libc::open(newdir_cpath.as_ptr(), libc::O_NOFOLLOW | libc::O_CLOEXEC) - }; - if newdir_fd < 0 { - None - } else { - let grabbed = self.grab_unlinked_fd(newdir_fd, newname).ok(); - unsafe { libc::close(newdir_fd) }; - grabbed - } - } - Ok(InodeHandle::Fd(newdir_fd)) => self.grab_unlinked_fd(newdir_fd, newname).ok(), - Err(_) => None, - } - } else { - None - }; - - let res = unsafe { libc::renamex_np(old_cpath.as_ptr(), new_cpath.as_ptr(), mflags) }; - if res == 0 { - // If the rename overwrote a tracked inode, hand its preserved fd to - // the inode store so later ops resolve by fd, not the vanished path. - // `store_unlinked_fd` takes ownership only when that inode is - // tracked; close the fd ourselves otherwise so it is never leaked. - if let Some(fd) = doomed_fd { - match self.store_unlinked_fd(&ctx, fd) { - Ok(true) => {} - Ok(false) | Err(_) => unsafe { - libc::close(fd); - }, - } - } - - if ((flags as i32) & bindings::LINUX_RENAME_WHITEOUT) != 0 { - let (host_mode, complete) = match self.cfg.semantics { - PermissionSemantics::LinuxComplete => (0o600, true), - PermissionSemantics::LinuxSimplified => { - (((libc::S_IFCHR | 0o600) as u32), false) - } - }; - let fd = unsafe { - libc::open( - old_cpath.as_ptr(), - libc::O_CREAT | libc::O_CLOEXEC | libc::O_NOFOLLOW, - host_mode, - ) - }; - if fd > 0 { - if complete { - if let Err(e) = set_stat( - &ctx, - self.cfg.semantics, - &InodeHandle::Fd(fd), - None, - None, - Some((libc::S_IFCHR | 0o600) as u32), - ) { - unsafe { libc::close(fd) }; - return Err(e); - } - } - - unsafe { libc::close(fd) }; - } - } - - let entry = self.lookup(ctx, newdir, newname)?; - self.forget(ctx, entry.inode, 1); - - Ok(()) - } else { - if let Some(fd) = doomed_fd { - // The rename failed; nothing was overwritten. Drop the fd. - unsafe { libc::close(fd) }; - } - Err(linux_error(io::Error::last_os_error())) - } - } - - fn mknod( - &self, - ctx: Context, - parent: Inode, - name: &CStr, - mode: u32, - rdev: u32, - umask: u32, - extensions: Extensions, - ) -> io::Result { - match self.cfg.semantics { - PermissionSemantics::LinuxComplete => { - self.mknod_complete(ctx, parent, name, mode, rdev, umask, extensions) - } - PermissionSemantics::LinuxSimplified => { - self.mknod_simplified(ctx, parent, name, mode, rdev, umask, extensions) - } - } - } - - fn link( - &self, - ctx: Context, - inode: Inode, - newparent: Inode, - newname: &CStr, - ) -> io::Result { - let orig_c_path = match self.inode_to_handle(inode, false)? { - InodeHandle::Path(c_path) => c_path, - InodeHandle::Fd(_) => return Err(ebadf()), - }; - let link_c_path = self.name_to_path(newparent, newname)?; - - // Safe because this doesn't modify any memory and we check the return value. - let res = unsafe { libc::link(orig_c_path.as_ptr(), link_c_path.as_ptr()) }; - if res == 0 { - self.lookup(ctx, newparent, newname) - } else { - Err(linux_error(io::Error::last_os_error())) - } - } - - fn symlink( - &self, - ctx: Context, - linkname: &CStr, - parent: Inode, - name: &CStr, - extensions: Extensions, - ) -> io::Result { - let c_path = self.name_to_path(parent, name)?; - - // Safe because this doesn't modify any memory and we check the return value. - let res = unsafe { libc::symlink(linkname.as_ptr(), c_path.as_ptr()) }; - if res == 0 { - let ihandle = InodeHandle::Path(c_path); - - // Set security context - if let Some(secctx) = extensions.secctx { - set_secctx(&ihandle, secctx, true)? - }; - - let mut entry = self.lookup(ctx, parent, name)?; - if matches!(self.cfg.semantics, PermissionSemantics::LinuxComplete) { - let mode = libc::S_IFLNK | 0o777; - set_stat( - &ctx, - self.cfg.semantics, - &ihandle, - None, - Some((ctx.uid, ctx.gid)), - Some(mode as u32), - )?; - entry.attr.st_uid = ctx.uid; - entry.attr.st_gid = ctx.gid; - entry.attr.st_mode = mode; - } - Ok(entry) - } else { - Err(linux_error(io::Error::last_os_error())) - } - } - - fn readlink(&self, _ctx: Context, inode: Inode) -> io::Result> { - let mut buf = vec![0; libc::PATH_MAX as usize]; - - let res = match self.inode_to_handle(inode, true)? { - InodeHandle::Path(c_path) => unsafe { - libc::readlink( - c_path.as_ptr(), - buf.as_mut_ptr() as *mut libc::c_char, - buf.len(), - ) - }, - InodeHandle::Fd(fd) => unsafe { - libc::freadlink(fd, buf.as_mut_ptr() as *mut libc::c_char, buf.len()) as isize - }, - }; - if res < 0 { - return Err(linux_error(io::Error::last_os_error())); - } - - buf.resize(res as usize, 0); - Ok(buf) - } - - fn flush( - &self, - _ctx: Context, - inode: Inode, - handle: Handle, - _lock_owner: u64, - ) -> io::Result<()> { - let data = self - .handles - .read() - .unwrap() - .get(&handle) - .filter(|hd| hd.inode == inode) - .cloned() - .ok_or_else(ebadf)?; - - // Since this method is called whenever an fd is closed in the client, we can emulate that - // behavior by doing the same thing (dup-ing the fd and then immediately closing it). Safe - // because this doesn't modify any memory and we check the return values. - unsafe { - let newfd = libc::dup(data.file.write().unwrap().as_raw_fd()); - if newfd < 0 { - return Err(linux_error(io::Error::last_os_error())); - } - - if libc::close(newfd) < 0 { - Err(linux_error(io::Error::last_os_error())) - } else { - Ok(()) - } - } - } - - fn fsync( - &self, - _ctx: Context, - inode: Inode, - _datasync: bool, - handle: Handle, - ) -> io::Result<()> { - let data = self - .handles - .read() - .unwrap() - .get(&handle) - .filter(|hd| hd.inode == inode) - .cloned() - .ok_or_else(ebadf)?; - - let fd = data.file.write().unwrap().as_raw_fd(); - - // Safe because this doesn't modify any memory and we check the return value. - let res = unsafe { libc::fsync(fd) }; - - if res == 0 { - Ok(()) - } else { - Err(linux_error(io::Error::last_os_error())) - } - } - - fn fsyncdir( - &self, - ctx: Context, - inode: Inode, - datasync: bool, - handle: Handle, - ) -> io::Result<()> { - self.fsync(ctx, inode, datasync, handle) - } - - fn access(&self, ctx: Context, inode: Inode, mask: u32) -> io::Result<()> { - let st = match self.inode_to_handle(inode, true)? { - InodeHandle::Path(c_path) => lstat(&ctx, self.cfg.semantics, &c_path, false)?, - InodeHandle::Fd(fd) => fstat(&ctx, self.cfg.semantics, fd, false)?, - }; - - let mode = mask as i32 & (libc::R_OK | libc::W_OK | libc::X_OK); - - if mode == libc::F_OK { - // The file exists since we were able to call `stat(2)` on it. - return Ok(()); - } - - // We use ctx.uid/ctx.gid for these checks, but when idmapped mounts - // support is enabled on the guest side, it means that "default_permissions" - // flag is set on virtiofs mount and FUSE_ACCESS request should never be - // sent to the userspace. Please, refer to the kernel commit - // ("fs/fuse: warn if fuse_access is called when idmapped mounts are allowed"). - // In case when idmapped mounts are not enabled we are good to rely on ctx.uid/ctx.gid values. - - if (mode & libc::R_OK) != 0 - && ctx.uid != 0 - && (st.st_uid != ctx.uid || st.st_mode & 0o400 == 0) - && (st.st_gid != ctx.gid || st.st_mode & 0o040 == 0) - && st.st_mode & 0o004 == 0 - { - return Err(linux_error(io::Error::from_raw_os_error(libc::EACCES))); - } - - if (mode & libc::W_OK) != 0 - && ctx.uid != 0 - && (st.st_uid != ctx.uid || st.st_mode & 0o200 == 0) - && (st.st_gid != ctx.gid || st.st_mode & 0o020 == 0) - && st.st_mode & 0o002 == 0 - { - return Err(linux_error(io::Error::from_raw_os_error(libc::EACCES))); - } - - // root can only execute something if it is executable by one of the owner, the group, or - // everyone. - if (mode & libc::X_OK) != 0 - && (ctx.uid != 0 || st.st_mode & 0o111 == 0) - && (st.st_uid != ctx.uid || st.st_mode & 0o100 == 0) - && (st.st_gid != ctx.gid || st.st_mode & 0o010 == 0) - && st.st_mode & 0o001 == 0 - { - return Err(linux_error(io::Error::from_raw_os_error(libc::EACCES))); - } - - Ok(()) - } - - fn setxattr( - &self, - _ctx: Context, - inode: Inode, - name: &CStr, - value: &[u8], - flags: u32, - ) -> io::Result<()> { - debug!("setxattr: inode={inode} name={name:?} value={value:?}"); - - if !self.cfg.xattr { - return Err(linux_error(io::Error::from_raw_os_error(libc::ENOSYS))); - } - - if name.to_bytes() == XATTR_KEY { - return Err(linux_error(io::Error::from_raw_os_error(libc::EACCES))); - } - - let mut mflags: i32 = 0; - if (flags as i32) & bindings::LINUX_XATTR_CREATE != 0 { - mflags |= libc::XATTR_CREATE; - } - if (flags as i32) & bindings::LINUX_XATTR_REPLACE != 0 { - mflags |= libc::XATTR_REPLACE; - } - - // Safe because this doesn't modify any memory and we check the return value. - let res = match self.inode_to_handle(inode, true)? { - InodeHandle::Path(c_path) => unsafe { - libc::setxattr( - c_path.as_ptr(), - name.as_ptr(), - value.as_ptr() as *const libc::c_void, - value.len(), - 0, - mflags as libc::c_int, - ) - }, - InodeHandle::Fd(fd) => unsafe { - libc::fsetxattr( - fd, - name.as_ptr(), - value.as_ptr() as *const libc::c_void, - value.len(), - 0, - mflags as libc::c_int, - ) - }, - }; - - if res == 0 { - Ok(()) - } else { - Err(linux_error(io::Error::last_os_error())) - } - } - - fn getxattr( - &self, - _ctx: Context, - inode: Inode, - name: &CStr, - size: u32, - ) -> io::Result { - debug!("getxattr: inode={inode} name={name:?}, size={size}"); - - if !self.cfg.xattr { - return Err(linux_error(io::Error::from_raw_os_error(libc::ENOSYS))); - } - - if name.to_bytes() == XATTR_KEY { - return Err(linux_error(io::Error::from_raw_os_error(libc::EACCES))); - } - - let mut buf = vec![0; size as usize]; - - // Safe because this will only modify the contents of `buf` - let res = match self.inode_to_handle(inode, true)? { - InodeHandle::Path(c_path) => unsafe { - if size == 0 { - libc::getxattr( - c_path.as_ptr(), - name.as_ptr(), - std::ptr::null_mut(), - size as libc::size_t, - 0, - 0, - ) - } else { - libc::getxattr( - c_path.as_ptr(), - name.as_ptr(), - buf.as_mut_ptr() as *mut libc::c_void, - size as libc::size_t, - 0, - 0, - ) - } - }, - InodeHandle::Fd(fd) => unsafe { - if size == 0 { - libc::fgetxattr( - fd, - name.as_ptr(), - std::ptr::null_mut(), - size as libc::size_t, - 0, - 0, - ) - } else { - libc::fgetxattr( - fd, - name.as_ptr(), - buf.as_mut_ptr() as *mut libc::c_void, - size as libc::size_t, - 0, - 0, - ) - } - }, - }; - if res < 0 { - return Err(linux_error(io::Error::last_os_error())); - } - - if size == 0 { - Ok(GetxattrReply::Count(res as u32)) - } else { - buf.resize(res as usize, 0); - Ok(GetxattrReply::Value(buf)) - } - } - - fn listxattr(&self, _ctx: Context, inode: Inode, size: u32) -> io::Result { - if !self.cfg.xattr { - return Err(linux_error(io::Error::from_raw_os_error(libc::ENOSYS))); - } - - let mut buf = vec![0; 512_usize]; - - // Safe because this will only modify the contents of `buf`. - let res = match self.inode_to_handle(inode, true)? { - InodeHandle::Path(c_path) => unsafe { - libc::listxattr( - c_path.as_ptr(), - buf.as_mut_ptr() as *mut libc::c_char, - 512, - 0, - ) - }, - InodeHandle::Fd(fd) => unsafe { - libc::flistxattr(fd, buf.as_mut_ptr() as *mut libc::c_char, 512, 0) - }, - }; - if res < 0 { - return Err(linux_error(io::Error::last_os_error())); - } - - buf.truncate(res as usize); - - if size == 0 { - let mut clean_size = res as usize; - - for attr in buf.split(|c| *c == 0) { - if attr.starts_with(&XATTR_KEY[..XATTR_KEY.len() - 1]) { - clean_size -= XATTR_KEY.len(); - } - } - - Ok(ListxattrReply::Count(clean_size as u32)) - } else { - let mut clean_buf = Vec::new(); - - for attr in buf.split(|c| *c == 0) { - if attr.is_empty() || attr.starts_with(&XATTR_KEY[..XATTR_KEY.len() - 1]) { - continue; - } - - clean_buf.extend_from_slice(attr); - clean_buf.push(0); - } - - clean_buf.shrink_to_fit(); - - if clean_buf.len() > size as usize { - Err(io::Error::from_raw_os_error(LINUX_ERANGE)) - } else { - Ok(ListxattrReply::Names(clean_buf)) - } - } - } - - fn removexattr(&self, _ctx: Context, inode: Inode, name: &CStr) -> io::Result<()> { - if !self.cfg.xattr { - return Err(linux_error(io::Error::from_raw_os_error(libc::ENOSYS))); - } - - if name.to_bytes() == XATTR_KEY { - return Err(linux_error(io::Error::from_raw_os_error( - bindings::LINUX_EACCES, - ))); - } - - // Safe because this doesn't modify any memory and we check the return value. - let res = match self.inode_to_handle(inode, true)? { - InodeHandle::Path(c_path) => unsafe { - libc::removexattr(c_path.as_ptr(), name.as_ptr(), 0) - }, - InodeHandle::Fd(fd) => unsafe { libc::fremovexattr(fd, name.as_ptr(), 0) }, - }; - if res == 0 { - Ok(()) - } else { - Err(linux_error(io::Error::last_os_error())) - } - } - - fn fallocate( - &self, - ctx: Context, - inode: Inode, - handle: Handle, - mode: u32, - offset: u64, - length: u64, - ) -> io::Result<()> { - let data = self - .handles - .read() - .unwrap() - .get(&handle) - .filter(|hd| hd.inode == inode) - .cloned() - .ok_or_else(ebadf)?; - - let fd = data.file.write().unwrap().as_raw_fd(); - - const SUPPORTED_FLAGS: i32 = bindings::LINUX_FALLOC_FL_ALLOCATE_RANGE - | bindings::LINUX_FALLOC_FL_KEEP_SIZE - | bindings::LINUX_FALLOC_FL_PUNCH_HOLE; - - if mode as i32 & !SUPPORTED_FLAGS != 0 { - return Err(linux_error(io::Error::from_raw_os_error(libc::EOPNOTSUPP))); - } - - let keep_size = mode & bindings::LINUX_FALLOC_FL_KEEP_SIZE as u32 != 0; - let mode = mode & !bindings::LINUX_FALLOC_FL_KEEP_SIZE as u32; - - match mode as i32 { - bindings::LINUX_FALLOC_FL_ALLOCATE_RANGE => { - // The closest thing we have on macOS to posix_fallocate is F_PREALLOCATE, - // but this one doesn't allow us to allocate arbitrary ranges, only allocate - // blocks to the file's end. - // - // The best thing we can do here is extend the file to (offset + length). - // This doesn't adhere to the same semantics, but should work fine (albeit - // less performant) for most guest applications. - let st = fstat(&ctx, self.cfg.semantics, fd, true)?; - let new_length = (offset + length) as i64; - - if keep_size { - // Check the number of allocated blocks instead of the file size. - let disk_size = st.st_blocks * 512_i64; - if disk_size >= new_length { - return Ok(()); - } - let mut fs = libc::fstore_t { - fst_flags: libc::F_ALLOCATEALL, - fst_posmode: libc::F_PEOFPOSMODE, - fst_offset: 0, - fst_length: new_length - disk_size, - fst_bytesalloc: 0, - }; - - let res = unsafe { libc::fcntl(fd, libc::F_PREALLOCATE, &mut fs as *mut _) }; - if res < 0 { - return Err(linux_error(io::Error::last_os_error())); - } - } else { - if st.st_size >= new_length { - return Ok(()); - } - let res = unsafe { libc::ftruncate(fd, new_length) }; - if res < 0 { - return Err(linux_error(io::Error::last_os_error())); - } - } - } - bindings::LINUX_FALLOC_FL_PUNCH_HOLE => { - if !keep_size { - // Linux forbids the use of PUNCH_HOLE without KEEP_SIZE. - return Err(linux_error(io::Error::from_raw_os_error(libc::EINVAL))); - } - - let mut hole = libc::fpunchhole_t { - fp_offset: offset as i64, - fp_flags: 0, - reserved: 0, - fp_length: length as i64, - }; - - let res = unsafe { libc::fcntl(fd, libc::F_PUNCHHOLE, &mut hole as *mut _) }; - if res < 0 { - return Err(linux_error(io::Error::last_os_error())); - } - } - _ => unreachable!(), - } - - Ok(()) - } - - fn lseek( - &self, - _ctx: Context, - inode: Inode, - handle: Handle, - offset: u64, - whence: u32, - ) -> io::Result { - let data = self - .handles - .read() - .unwrap() - .get(&handle) - .filter(|hd| hd.inode == inode) - .cloned() - .ok_or_else(ebadf)?; - - // SEEK_DATA and SEEK_HOLE have slightly different semantics - // in Linux vs. macOS, which means we can't support them. - let mwhence = if whence == 3 { - // SEEK_DATA - return Ok(offset); - } else if whence == 4 { - // SEEK_HOLE - libc::SEEK_END - } else { - whence as i32 - }; - - let fd = data.file.write().unwrap().as_raw_fd(); - - // Safe because this doesn't modify any memory and we check the return value. - let res = unsafe { libc::lseek(fd, offset as bindings::off64_t, mwhence as libc::c_int) }; - if res < 0 { - Err(linux_error(io::Error::last_os_error())) - } else { - Ok(res as u64) - } - } - - fn setupmapping( - &self, - _ctx: Context, - inode: Inode, - _handle: Handle, - foffset: u64, - len: u64, - flags: u64, - moffset: u64, - guest_shm_base: u64, - shm_size: u64, - map_sender: &Option>, - ) -> io::Result<()> { - if map_sender.is_none() { - return Err(linux_error(io::Error::from_raw_os_error(libc::ENOSYS))); - } - - let open_flags = if (flags & fuse::SetupmappingFlags::WRITE.bits()) != 0 { - libc::O_RDWR - } else { - libc::O_RDONLY - }; - - let prot_flags = if (flags & fuse::SetupmappingFlags::WRITE.bits()) != 0 { - libc::PROT_READ | libc::PROT_WRITE - } else { - libc::PROT_READ - }; - - if (moffset + len) > shm_size { - return Err(linux_error(io::Error::from_raw_os_error(libc::EINVAL))); - } - - let guest_addr = guest_shm_base + moffset; - - debug!("setupmapping: ino {inode:?} guest_addr={guest_addr:x} len={len}"); - - let file = self.open_inode(inode, open_flags)?; - let fd = file.as_raw_fd(); - - let host_addr = unsafe { - libc::mmap( - null_mut(), - len as usize, - prot_flags, - libc::MAP_SHARED, - fd, - foffset as libc::off_t, - ) - }; - if host_addr == libc::MAP_FAILED { - return Err(linux_error(io::Error::last_os_error())); - } - - drop(file); - - // We've checked that map_sender is something above. - let sender = map_sender.as_ref().unwrap(); - let (reply_sender, reply_receiver) = unbounded(); - sender - .send(WorkerMessage::GpuAddMapping( - reply_sender, - host_addr as u64, - guest_addr, - len, - )) - .unwrap(); - if !reply_receiver.recv().unwrap() { - error!("Error requesting HVF the addition of a DAX window"); - unsafe { libc::munmap(host_addr, len as usize) }; - return Err(linux_error(io::Error::from_raw_os_error(libc::EINVAL))); - } - - self.map_windows - .lock() - .unwrap() - .insert(guest_addr, host_addr as u64); - - Ok(()) - } - - fn removemapping( - &self, - _ctx: Context, - requests: Vec, - guest_shm_base: u64, - shm_size: u64, - map_sender: &Option>, - ) -> io::Result<()> { - if map_sender.is_none() { - return Err(linux_error(io::Error::from_raw_os_error(libc::ENOSYS))); - } - - for req in requests { - let guest_addr = guest_shm_base + req.moffset; - if (req.moffset + req.len) > shm_size { - return Err(linux_error(io::Error::from_raw_os_error(libc::EINVAL))); - } - let host_addr = match self.map_windows.lock().unwrap().remove(&guest_addr) { - Some(a) => a, - None => return Err(linux_error(io::Error::from_raw_os_error(libc::EINVAL))), - }; - debug!( - "removemapping: guest_addr={:x} len={:?}", - guest_addr, req.len - ); - - let sender = map_sender.as_ref().unwrap(); - let (reply_sender, reply_receiver) = unbounded(); - sender - .send(WorkerMessage::GpuRemoveMapping( - reply_sender, - guest_addr, - req.len, - )) - .unwrap(); - if !reply_receiver.recv().unwrap() { - error!("Error requesting HVF the removal of a DAX window"); - return Err(linux_error(io::Error::from_raw_os_error(libc::EINVAL))); - } - - let ret = unsafe { libc::munmap(host_addr as *mut libc::c_void, req.len as usize) }; - if ret == -1 { - error!("Error unmapping DAX window"); - return Err(linux_error(io::Error::last_os_error())); - } - } - - Ok(()) - } -} diff --git a/vendor/krun-devices/src/virtio/fs/mod.rs b/vendor/krun-devices/src/virtio/fs/mod.rs deleted file mode 100644 index a79878c2f..000000000 --- a/vendor/krun-devices/src/virtio/fs/mod.rs +++ /dev/null @@ -1,83 +0,0 @@ -mod augment_fs; -mod device; -#[allow(dead_code)] -mod filesystem; -pub mod fuse; -mod inode_alloc; -#[allow(dead_code)] -mod multikey; -mod null_fs; -mod overlay; -mod read_only; -mod server; -pub mod virtual_entry; -mod worker; - -#[cfg(target_os = "linux")] -pub mod linux; -#[cfg(target_os = "linux")] -pub use linux::fs_utils; -#[cfg(target_os = "linux")] -pub use linux::passthrough; -#[cfg(target_os = "macos")] -pub mod macos; -#[cfg(target_os = "macos")] -pub use macos::fs_utils; -#[cfg(target_os = "macos")] -pub use macos::passthrough; - -use super::bindings; -use super::descriptor_utils; - -pub use self::defs::uapi::VIRTIO_ID_FS as TYPE_FS; -pub use self::device::Fs; -pub use self::filesystem::ExportTable; -pub use self::overlay::Config as OverlayConfig; - -mod defs { - use super::super::QueueConfig; - - pub const FS_DEV_ID: &str = "virtio_fs"; - pub const NUM_QUEUES: usize = 2; - pub const QUEUE_SIZE: u16 = 1024; - pub static QUEUE_CONFIG: [QueueConfig; NUM_QUEUES] = [QueueConfig::new(QUEUE_SIZE); NUM_QUEUES]; - // High priority queue. - pub const HPQ_INDEX: usize = 0; - // Request queue. - pub const REQ_INDEX: usize = 1; - - pub mod uapi { - pub const VIRTIO_ID_FS: u32 = 26; - } -} - -use std::ffi::{FromBytesWithNulError, FromVecWithNulError}; -use std::io; - -use descriptor_utils::Error as DescriptorError; - -#[derive(Debug)] -pub enum FsError { - /// Failed to decode protocol messages. - DecodeMessage(io::Error), - /// Failed to encode protocol messages. - EncodeMessage(io::Error), - /// Failed to create event fd. - EventFd(std::io::Error), - /// The guest failed to send a require extensions. - MissingExtension, - /// One or more parameters are missing. - MissingParameter, - /// A C string parameter is invalid. - InvalidCString(FromBytesWithNulError), - InvalidCString2(FromVecWithNulError), - /// The `len` field of the header is too small. - InvalidHeaderLength, - /// The `size` field of the `SetxattrIn` message does not match the length - /// of the decoded value. - InvalidXattrSize((u32, usize)), - QueueReader(DescriptorError), - QueueWriter(DescriptorError), -} - -type Result = std::result::Result; diff --git a/vendor/krun-devices/src/virtio/fs/multikey.rs b/vendor/krun-devices/src/virtio/fs/multikey.rs deleted file mode 100644 index 8dc35a447..000000000 --- a/vendor/krun-devices/src/virtio/fs/multikey.rs +++ /dev/null @@ -1,274 +0,0 @@ -// Copyright 2019 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -use std::borrow::Borrow; -use std::collections::BTreeMap; - -/// A BTreeMap that supports 2 types of keys per value. All the usual restrictions and warnings for -/// `std::collections::BTreeMap` also apply to this struct. Additionally, there is a 1:1 -/// relationship between the 2 key types. In other words, for each `K1` in the map, there is exactly -/// one `K2` in the map and vice versa. -#[derive(Default)] -pub struct MultikeyBTreeMap -where - K1: Ord, - K2: Ord, -{ - // We need to keep a copy of the second key in the main map so that we can remove entries using - // just the main key. Otherwise we would require the caller to provide both keys when calling - // `remove`. - main: BTreeMap, - alt: BTreeMap, -} - -impl MultikeyBTreeMap -where - K1: Clone + Ord, - K2: Clone + Ord, -{ - /// Create a new empty MultikeyBTreeMap. - pub fn new() -> Self { - MultikeyBTreeMap { - main: BTreeMap::default(), - alt: BTreeMap::default(), - } - } - - /// Returns a reference to the value corresponding to the key. - /// - /// The key may be any borrowed form of `K1``, but the ordering on the borrowed form must match - /// the ordering on `K1`. - pub fn get(&self, key: &Q) -> Option<&V> - where - K1: Borrow, - Q: Ord + ?Sized, - { - self.main.get(key).map(|(_, v)| v) - } - - /// Returns a reference to the value corresponding to the alternate key. - /// - /// The key may be any borrowed form of the `K2``, but the ordering on the borrowed form must - /// match the ordering on `K2`. - /// - /// Note that this method performs 2 lookups: one to get the main key and another to get the - /// value associated with that key. For best performance callers should prefer the `get` method - /// over this method whenever possible as `get` only needs to perform one lookup. - pub fn get_alt(&self, key: &Q2) -> Option<&V> - where - K2: Borrow, - Q2: Ord + ?Sized, - { - if let Some(k) = self.alt.get(key) { - self.get(k) - } else { - None - } - } - - /// Inserts a new entry into the map with the given keys and value. - /// - /// Returns `None` if the map did not have an entry with `k1` or `k2` present. If exactly one - /// key was present, then the value associated with that key is updated, the other key is - /// removed, and the old value is returned. If **both** keys were present then the value - /// associated with the main key is updated, the value associated with the alternate key is - /// removed, and the old value associated with the main key is returned. - pub fn insert(&mut self, k1: K1, k2: K2, v: V) -> Option { - let oldval = if let Some(oldkey) = self.alt.insert(k2.clone(), k1.clone()) { - self.main.remove(&oldkey) - } else { - None - }; - self.main - .insert(k1, (k2.clone(), v)) - .or(oldval) - .map(|(oldk2, v)| { - if oldk2 != k2 { - self.alt.remove(&oldk2); - } - v - }) - } - - /// Remove a key from the map, returning the value associated with that key if it was previously - /// in the map. - /// - /// The key may be any borrowed form of `K1``, but the ordering on the borrowed form must match - /// the ordering on `K1`. - pub fn remove(&mut self, key: &Q) -> Option - where - K1: Borrow, - Q: Ord + ?Sized, - { - self.main.remove(key).map(|(k2, v)| { - self.alt.remove(&k2); - v - }) - } - - /// Clears the map, removing all values. - pub fn clear(&mut self) { - self.alt.clear(); - self.main.clear() - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn get() { - let mut m = MultikeyBTreeMap::::new(); - - let k1 = 0xc6c8_f5e0_b13e_ed40; - let k2 = 0x1a04_ce4b_8329_14fe; - let val = 0xf4e3_c360; - assert!(m.insert(k1, k2, val).is_none()); - - assert_eq!(*m.get(&k1).expect("failed to look up main key"), val); - assert_eq!(*m.get_alt(&k2).expect("failed to look up alt key"), val); - } - - #[test] - fn update_main_key() { - let mut m = MultikeyBTreeMap::::new(); - - let k1 = 0xc6c8_f5e0_b13e_ed40; - let k2 = 0x1a04_ce4b_8329_14fe; - let val = 0xf4e3_c360; - assert!(m.insert(k1, k2, val).is_none()); - - let new_k1 = 0x3add_f8f8_c7c5_df5e; - let val2 = 0x7389_f8a7; - assert_eq!( - m.insert(new_k1, k2, val2) - .expect("failed to update main key"), - val - ); - - assert!(m.get(&k1).is_none()); - assert_eq!(*m.get(&new_k1).expect("failed to look up main key"), val2); - assert_eq!(*m.get_alt(&k2).expect("failed to look up alt key"), val2); - } - - #[test] - fn update_alt_key() { - let mut m = MultikeyBTreeMap::::new(); - - let k1 = 0xc6c8_f5e0_b13e_ed40; - let k2 = 0x1a04_ce4b_8329_14fe; - let val = 0xf4e3_c360; - assert!(m.insert(k1, k2, val).is_none()); - - let new_k2 = 0x6825_a60b_61ac_b333; - let val2 = 0xbb14_8f2c; - assert_eq!( - m.insert(k1, new_k2, val2) - .expect("failed to update alt key"), - val - ); - - assert!(m.get_alt(&k2).is_none()); - assert_eq!(*m.get(&k1).expect("failed to look up main key"), val2); - assert_eq!( - *m.get_alt(&new_k2).expect("failed to look up alt key"), - val2 - ); - } - - #[test] - fn update_value() { - let mut m = MultikeyBTreeMap::::new(); - - let k1 = 0xc6c8_f5e0_b13e_ed40; - let k2 = 0x1a04_ce4b_8329_14fe; - let val = 0xf4e3_c360; - assert!(m.insert(k1, k2, val).is_none()); - - let val2 = 0xe42d_79ba; - assert_eq!( - m.insert(k1, k2, val2).expect("failed to update alt key"), - val - ); - - assert_eq!(*m.get(&k1).expect("failed to look up main key"), val2); - assert_eq!(*m.get_alt(&k2).expect("failed to look up alt key"), val2); - } - - #[test] - fn update_both_keys_main() { - let mut m = MultikeyBTreeMap::::new(); - - let k1 = 0xc6c8_f5e0_b13e_ed40; - let k2 = 0x1a04_ce4b_8329_14fe; - let val = 0xf4e3_c360; - assert!(m.insert(k1, k2, val).is_none()); - - let new_k1 = 0xc980_587a_24b3_ae30; - let new_k2 = 0x2773_c5ee_8239_45a2; - let val2 = 0x31f4_33f9; - assert!(m.insert(new_k1, new_k2, val2).is_none()); - - let val3 = 0x8da1_9cf7; - assert_eq!( - m.insert(k1, new_k2, val3) - .expect("failed to update main key"), - val - ); - - // Both new_k1 and k2 should now be gone from the map. - assert!(m.get(&new_k1).is_none()); - assert!(m.get_alt(&k2).is_none()); - - assert_eq!(*m.get(&k1).expect("failed to look up main key"), val3); - assert_eq!( - *m.get_alt(&new_k2).expect("failed to look up alt key"), - val3 - ); - } - - #[test] - fn update_both_keys_alt() { - let mut m = MultikeyBTreeMap::::new(); - - let k1 = 0xc6c8_f5e0_b13e_ed40; - let k2 = 0x1a04_ce4b_8329_14fe; - let val = 0xf4e3_c360; - assert!(m.insert(k1, k2, val).is_none()); - - let new_k1 = 0xc980_587a_24b3_ae30; - let new_k2 = 0x2773_c5ee_8239_45a2; - let val2 = 0x31f4_33f9; - assert!(m.insert(new_k1, new_k2, val2).is_none()); - - let val3 = 0x8da1_9cf7; - assert_eq!( - m.insert(new_k1, k2, val3) - .expect("failed to update main key"), - val2 - ); - - // Both k1 and new_k2 should now be gone from the map. - assert!(m.get(&k1).is_none()); - assert!(m.get_alt(&new_k2).is_none()); - - assert_eq!(*m.get(&new_k1).expect("failed to look up main key"), val3); - assert_eq!(*m.get_alt(&k2).expect("failed to look up alt key"), val3); - } - - #[test] - fn remove() { - let mut m = MultikeyBTreeMap::::new(); - - let k1 = 0xc6c8_f5e0_b13e_ed40; - let k2 = 0x1a04_ce4b_8329_14fe; - let val = 0xf4e3_c360; - assert!(m.insert(k1, k2, val).is_none()); - - assert_eq!(m.remove(&k1).expect("failed to remove entry"), val); - assert!(m.get(&k1).is_none()); - assert!(m.get_alt(&k2).is_none()); - } -} diff --git a/vendor/krun-devices/src/virtio/fs/null_fs.rs b/vendor/krun-devices/src/virtio/fs/null_fs.rs deleted file mode 100644 index 4bb4b6360..000000000 --- a/vendor/krun-devices/src/virtio/fs/null_fs.rs +++ /dev/null @@ -1,50 +0,0 @@ -// A minimal filesystem that serves an empty root directory. -// -// Used with AugmentFs to provide a virtual-only filesystem (e.g. for -// booting from a block device where the virtiofs root only needs init.krun). - -use std::ffi::CStr; -use std::io; -use std::mem; -use std::time::Duration; - -use super::filesystem::{Context, Entry, FileSystem, FsOptions}; -use super::fuse; -use super::virtual_entry::VIRTUAL_BLKSIZE; -use crate::virtio::bindings; - -/// An empty filesystem with just a root directory and nothing in it. -pub struct NullFs; - -type Inode = u64; -type Handle = u64; - -impl FileSystem for NullFs { - type Inode = Inode; - type Handle = Handle; - - fn init(&self, _capable: FsOptions) -> io::Result { - Ok(FsOptions::empty()) - } - - fn lookup(&self, _ctx: Context, _parent: Inode, _name: &CStr) -> io::Result { - Err(io::Error::from_raw_os_error(libc::ENOENT)) - } - - fn getattr( - &self, - _ctx: Context, - inode: Inode, - _handle: Option, - ) -> io::Result<(bindings::stat64, Duration)> { - if inode == fuse::ROOT_ID { - let mut st: bindings::stat64 = unsafe { mem::zeroed() }; - st.st_ino = fuse::ROOT_ID; - st.st_mode = libc::S_IFDIR | 0o755; - st.st_nlink = 2; - st.st_blksize = VIRTUAL_BLKSIZE as _; - return Ok((st, Duration::MAX)); - } - Err(io::Error::from_raw_os_error(libc::ENOENT)) - } -} diff --git a/vendor/krun-devices/src/virtio/fs/overlay.rs b/vendor/krun-devices/src/virtio/fs/overlay.rs deleted file mode 100644 index 2dfcf91a5..000000000 --- a/vendor/krun-devices/src/virtio/fs/overlay.rs +++ /dev/null @@ -1,896 +0,0 @@ -//! Portable copy-on-write overlay served directly over virtio-fs. -//! -//! The union semantics live in `persisting-overlay-core`; the existing -//! platform passthrough implementation is retained for Linux permission -//! emulation and for the actual FUSE request I/O on each resolved layer. - -use std::collections::HashMap; -use std::ffi::{CStr, CString, OsStr}; -use std::io; -use std::os::unix::ffi::OsStrExt; -use std::path::{Path, PathBuf}; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::{Arc, Mutex}; -use std::time::Duration; - -use persisting_overlay_core::OverlayCore; - -use super::bindings; -use super::filesystem::{ - Context, DirEntry, Entry, Extensions, FileSystem, FsOptions, GetxattrReply, ListxattrReply, - OpenOptions, SetattrValid, ZeroCopyReader, ZeroCopyWriter, -}; -use super::fuse; -use super::inode_alloc::InodeAllocator; -use super::passthrough::{self, PassthroughFs}; - -const TTL: Duration = Duration::from_secs(1); -const RENAME_NOREPLACE: u32 = 1; -const RENAME_EXCHANGE: u32 = 2; - -#[derive(Clone, Debug)] -pub struct Config { - pub lower_dirs: Vec, - pub upper_dir: String, - pub work_dir: Option, - pub preimage_dir: Option, - pub excluded_paths: Vec, - pub semantics: passthrough::PermissionSemantics, -} - -#[derive(Clone, Copy, Debug)] -struct Layer(usize); - -#[derive(Debug)] -struct FileHandle { - layer: Layer, - inode: u64, - handle: u64, -} - -#[derive(Debug)] -struct DirectoryItem { - ino: u64, - name: Vec, - type_: u32, -} - -#[derive(Debug)] -enum Handle { - File(FileHandle), - Directory(Vec), -} - -#[derive(Default)] -struct Nodes { - by_inode: HashMap, - by_path: HashMap, -} - -pub struct OverlayFs { - core: OverlayCore, - roots: Vec, - layers: Vec, - inode_alloc: Arc, - nodes: Mutex, - handles: Mutex>, - next_handle: AtomicU64, -} - -impl OverlayFs { - pub fn new(cfg: Config, inode_alloc: Arc) -> io::Result { - if cfg.lower_dirs.is_empty() { - return Err(io::Error::from_raw_os_error(libc::EINVAL)); - } - let lowers = cfg.lower_dirs.iter().map(PathBuf::from).collect::>(); - let upper = PathBuf::from(&cfg.upper_dir); - let work = cfg.work_dir.as_ref().map(PathBuf::from); - let preimages = cfg.preimage_dir.as_ref().map(PathBuf::from); - let excluded = cfg.excluded_paths.iter().map(PathBuf::from).collect(); - let core = OverlayCore::new_with_exclusions_and_preimages( - lowers.clone(), - upper.clone(), - work, - excluded, - preimages, - )?; - - let mut roots = Vec::with_capacity(lowers.len() + 1); - roots.push(upper); - roots.extend(lowers); - let layers = roots - .iter() - .map(|root| { - PassthroughFs::new( - passthrough::Config { - root_dir: root.to_string_lossy().into_owned(), - semantics: cfg.semantics, - attr_timeout: TTL, - entry_timeout: TTL, - ..Default::default() - }, - inode_alloc.clone(), - ) - }) - .collect::>>()?; - let mut nodes = Nodes::default(); - nodes.by_inode.insert(fuse::ROOT_ID, PathBuf::new()); - nodes.by_path.insert(PathBuf::new(), fuse::ROOT_ID); - Ok(Self { - core, - roots, - layers, - inode_alloc, - nodes: Mutex::new(nodes), - handles: Mutex::new(HashMap::new()), - next_handle: AtomicU64::new(1), - }) - } - - fn path(&self, inode: u64) -> io::Result { - self.nodes - .lock() - .unwrap() - .by_inode - .get(&inode) - .cloned() - .ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT)) - } - - fn child(&self, parent: u64, name: &CStr) -> io::Result { - OverlayCore::child(&self.path(parent)?, OsStr::from_bytes(name.to_bytes())) - } - - fn allocate_inode(&self, path: PathBuf) -> u64 { - let mut nodes = self.nodes.lock().unwrap(); - if let Some(inode) = nodes.by_path.get(&path) { - return *inode; - } - let inode = self.inode_alloc.next(); - nodes.by_path.insert(path.clone(), inode); - nodes.by_inode.insert(inode, path); - inode - } - - fn remove_path(&self, prefix: &Path) { - let mut nodes = self.nodes.lock().unwrap(); - let paths = nodes - .by_path - .keys() - .filter(|path| *path == prefix || path.starts_with(prefix)) - .cloned() - .collect::>(); - for path in paths { - if let Some(inode) = nodes.by_path.remove(&path) { - nodes.by_inode.remove(&inode); - } - } - } - - fn remap_path(&self, old: &Path, new: &Path) { - let mut nodes = self.nodes.lock().unwrap(); - let changes = nodes - .by_path - .iter() - .filter(|(path, _)| *path == old || path.starts_with(old)) - .map(|(path, inode)| { - let suffix = path.strip_prefix(old).unwrap(); - let replacement = if suffix.as_os_str().is_empty() { - new.to_path_buf() - } else { - new.join(suffix) - }; - (path.clone(), replacement, *inode) - }) - .collect::>(); - for (old_path, _, _) in &changes { - nodes.by_path.remove(old_path); - } - for (_, new_path, inode) in changes { - nodes.by_path.insert(new_path.clone(), inode); - nodes.by_inode.insert(inode, new_path); - } - } - - fn layer(&self, path: &Path) -> io::Result { - let resolved = self - .core - .resolve(path) - .ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))?; - if resolved.is_upper { - return Ok(Layer(0)); - } - self.roots - .iter() - .enumerate() - .skip(1) - .find(|(_, root)| resolved.path == **root || resolved.path.starts_with(root)) - .map(|(index, _)| Layer(index)) - .ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT)) - } - - fn inner_inode(&self, layer: Layer, path: &Path, ctx: Context) -> io::Result { - let fs = &self.layers[layer.0]; - let mut inode = fuse::ROOT_ID; - for component in path.components() { - let name = CString::new(component.as_os_str().as_bytes())?; - let entry = fs.lookup(ctx, inode, &name)?; - if inode != fuse::ROOT_ID { - fs.forget(ctx, inode, 1); - } - inode = entry.inode; - } - Ok(inode) - } - - fn entry(&self, ctx: Context, path: &Path, inode: u64) -> io::Result { - let layer = self.layer(path)?; - let inner = self.inner_inode(layer, path, ctx)?; - let (mut attr, timeout) = self.layers[layer.0].getattr(ctx, inner, None)?; - if inner != fuse::ROOT_ID { - self.layers[layer.0].forget(ctx, inner, 1); - } - attr.st_ino = inode as _; - Ok(Entry { - inode, - generation: 0, - attr, - attr_flags: 0, - attr_timeout: timeout, - entry_timeout: TTL, - }) - } - - fn writable_inner(&self, ctx: Context, path: &Path) -> io::Result { - self.core.copy_up(path)?; - self.inner_inode(Layer(0), path, ctx) - } - - fn upper_parent(&self, ctx: Context, path: &Path) -> io::Result<(u64, CString)> { - self.core.clear_whiteout(path)?; - self.core.ensure_upper_parents(path)?; - let parent = path.parent().unwrap_or_else(|| Path::new("")); - let name = path - .file_name() - .ok_or_else(|| io::Error::from_raw_os_error(libc::EINVAL))?; - Ok(( - self.inner_inode(Layer(0), parent, ctx)?, - CString::new(name.as_bytes())?, - )) - } - - fn allocate_handle(&self, handle: Handle) -> u64 { - let id = self.next_handle.fetch_add(1, Ordering::Relaxed); - self.handles.lock().unwrap().insert(id, handle); - id - } - - fn with_file_handle( - &self, - id: u64, - f: impl FnOnce(&PassthroughFs, &FileHandle) -> io::Result, - ) -> io::Result { - let handles = self.handles.lock().unwrap(); - match handles.get(&id) { - Some(Handle::File(handle)) => f(&self.layers[handle.layer.0], handle), - _ => Err(io::Error::from_raw_os_error(libc::EBADF)), - } - } - - fn dtype(mode: libc::mode_t) -> u32 { - ((mode & libc::S_IFMT) >> 12) as u32 - } -} - -impl FileSystem for OverlayFs { - type Inode = u64; - type Handle = u64; - - fn init(&self, capable: FsOptions) -> io::Result { - let mut options = None; - for layer in &self.layers { - let layer_options = layer.init(capable)?; - options = Some(options.map_or(layer_options, |current| current & layer_options)); - } - Ok(options.unwrap_or_else(FsOptions::empty)) - } - - fn destroy(&self) { - self.handles.lock().unwrap().clear(); - for layer in &self.layers { - layer.destroy(); - } - } - - fn lookup(&self, ctx: Context, parent: u64, name: &CStr) -> io::Result { - let path = self.child(parent, name)?; - self.core.metadata(&path)?; - let inode = self.allocate_inode(path.clone()); - self.entry(ctx, &path, inode) - } - - fn getattr( - &self, - ctx: Context, - inode: u64, - _handle: Option, - ) -> io::Result<(bindings::stat64, Duration)> { - let entry = self.entry(ctx, &self.path(inode)?, inode)?; - Ok((entry.attr, entry.attr_timeout)) - } - - fn setattr( - &self, - ctx: Context, - inode: u64, - attr: bindings::stat64, - _handle: Option, - valid: SetattrValid, - ) -> io::Result<(bindings::stat64, Duration)> { - let path = self.path(inode)?; - let inner = self.writable_inner(ctx, &path)?; - let result = self.layers[0].setattr(ctx, inner, attr, None, valid); - self.layers[0].forget(ctx, inner, 1); - let (mut attr, timeout) = result?; - attr.st_ino = inode as _; - Ok((attr, timeout)) - } - - fn readlink(&self, ctx: Context, inode: u64) -> io::Result> { - let path = self.path(inode)?; - let layer = self.layer(&path)?; - let inner = self.inner_inode(layer, &path, ctx)?; - let result = self.layers[layer.0].readlink(ctx, inner); - self.layers[layer.0].forget(ctx, inner, 1); - result - } - - fn symlink( - &self, - ctx: Context, - linkname: &CStr, - parent: u64, - name: &CStr, - extensions: Extensions, - ) -> io::Result { - let path = self.child(parent, name)?; - let (upper_parent, upper_name) = self.upper_parent(ctx, &path)?; - self.layers[0].symlink(ctx, linkname, upper_parent, &upper_name, extensions)?; - if upper_parent != fuse::ROOT_ID { - self.layers[0].forget(ctx, upper_parent, 1); - } - let inode = self.allocate_inode(path.clone()); - self.entry(ctx, &path, inode) - } - - fn mknod( - &self, - ctx: Context, - parent: u64, - name: &CStr, - mode: u32, - rdev: u32, - umask: u32, - extensions: Extensions, - ) -> io::Result { - let path = self.child(parent, name)?; - let (upper_parent, upper_name) = self.upper_parent(ctx, &path)?; - self.layers[0].mknod( - ctx, - upper_parent, - &upper_name, - mode, - rdev, - umask, - extensions, - )?; - if upper_parent != fuse::ROOT_ID { - self.layers[0].forget(ctx, upper_parent, 1); - } - let inode = self.allocate_inode(path.clone()); - self.entry(ctx, &path, inode) - } - - fn mkdir( - &self, - ctx: Context, - parent: u64, - name: &CStr, - mode: u32, - umask: u32, - extensions: Extensions, - ) -> io::Result { - let path = self.child(parent, name)?; - let (upper_parent, upper_name) = self.upper_parent(ctx, &path)?; - self.layers[0].mkdir(ctx, upper_parent, &upper_name, mode, umask, extensions)?; - if upper_parent != fuse::ROOT_ID { - self.layers[0].forget(ctx, upper_parent, 1); - } - let inode = self.allocate_inode(path.clone()); - self.entry(ctx, &path, inode) - } - - fn unlink(&self, _ctx: Context, parent: u64, name: &CStr) -> io::Result<()> { - let path = self.child(parent, name)?; - self.core.remove(&path, false)?; - self.remove_path(&path); - Ok(()) - } - - fn rmdir(&self, _ctx: Context, parent: u64, name: &CStr) -> io::Result<()> { - let path = self.child(parent, name)?; - self.core.remove(&path, true)?; - self.remove_path(&path); - Ok(()) - } - - fn rename( - &self, - _ctx: Context, - olddir: u64, - oldname: &CStr, - newdir: u64, - newname: &CStr, - flags: u32, - ) -> io::Result<()> { - let old = self.child(olddir, oldname)?; - let new = self.child(newdir, newname)?; - match flags { - 0 => self.core.rename(&old, &new, false)?, - RENAME_NOREPLACE => self.core.rename(&old, &new, true)?, - RENAME_EXCHANGE => self.core.exchange(&old, &new)?, - _ => return Err(io::Error::from_raw_os_error(libc::ENOTSUP)), - } - if flags == RENAME_EXCHANGE { - let marker = PathBuf::from(format!( - ".pvisor-exchange-{}", - self.inode_alloc.next() - )); - self.remap_path(&old, &marker); - self.remap_path(&new, &old); - self.remap_path(&marker, &new); - } else { - self.remove_path(&new); - self.remap_path(&old, &new); - } - Ok(()) - } - - fn link(&self, ctx: Context, inode: u64, newparent: u64, newname: &CStr) -> io::Result { - let source = self.path(inode)?; - let destination = self.child(newparent, newname)?; - self.core.hard_link(&source, &destination)?; - let new_inode = self.allocate_inode(destination.clone()); - self.entry(ctx, &destination, new_inode) - } - - fn open( - &self, - ctx: Context, - inode: u64, - kill_priv: bool, - flags: u32, - ) -> io::Result<(Option, OpenOptions)> { - let path = self.path(inode)?; - let writing = flags as i32 & libc::O_ACCMODE != libc::O_RDONLY - || flags as i32 & (libc::O_APPEND | libc::O_TRUNC) != 0; - let layer = if writing { - self.core.copy_up(&path)?; - Layer(0) - } else { - self.layer(&path)? - }; - let inner = self.inner_inode(layer, &path, ctx)?; - let (handle, options) = self.layers[layer.0].open(ctx, inner, kill_priv, flags)?; - let handle = handle.ok_or_else(|| io::Error::from_raw_os_error(libc::EIO))?; - let id = self.allocate_handle(Handle::File(FileHandle { - layer, - inode: inner, - handle, - })); - Ok((Some(id), options)) - } - - fn create( - &self, - ctx: Context, - parent: u64, - name: &CStr, - mode: u32, - kill_priv: bool, - flags: u32, - umask: u32, - extensions: Extensions, - ) -> io::Result<(Entry, Option, OpenOptions)> { - let path = self.child(parent, name)?; - let inode = self.allocate_inode(path.clone()); - let (upper_parent, upper_name) = self.upper_parent(ctx, &path)?; - let (mut entry, handle, options) = self.layers[0].create( - ctx, - upper_parent, - &upper_name, - mode, - kill_priv, - flags, - umask, - extensions, - )?; - if upper_parent != fuse::ROOT_ID { - self.layers[0].forget(ctx, upper_parent, 1); - } - let inner_inode = entry.inode; - entry.inode = inode; - entry.attr.st_ino = inode as _; - let handle = handle.ok_or_else(|| io::Error::from_raw_os_error(libc::EIO))?; - let id = self.allocate_handle(Handle::File(FileHandle { - layer: Layer(0), - inode: inner_inode, - handle, - })); - Ok((entry, Some(id), options)) - } - - fn read( - &self, - ctx: Context, - _inode: u64, - handle: u64, - w: W, - size: u32, - offset: u64, - lock_owner: Option, - flags: u32, - ) -> io::Result { - self.with_file_handle(handle, |fs, h| { - fs.read(ctx, h.inode, h.handle, w, size, offset, lock_owner, flags) - }) - } - - fn write( - &self, - ctx: Context, - _inode: u64, - handle: u64, - r: R, - size: u32, - offset: u64, - lock_owner: Option, - delayed_write: bool, - kill_priv: bool, - flags: u32, - ) -> io::Result { - self.with_file_handle(handle, |fs, h| { - fs.write( - ctx, - h.inode, - h.handle, - r, - size, - offset, - lock_owner, - delayed_write, - kill_priv, - flags, - ) - }) - } - - fn flush(&self, ctx: Context, _inode: u64, handle: u64, lock_owner: u64) -> io::Result<()> { - self.with_file_handle(handle, |fs, h| fs.flush(ctx, h.inode, h.handle, lock_owner)) - } - - fn fsync(&self, ctx: Context, _inode: u64, datasync: bool, handle: u64) -> io::Result<()> { - self.with_file_handle(handle, |fs, h| fs.fsync(ctx, h.inode, datasync, h.handle)) - } - - fn release( - &self, - ctx: Context, - _inode: u64, - flags: u32, - handle: u64, - flush: bool, - flock_release: bool, - lock_owner: Option, - ) -> io::Result<()> { - let handle = self.handles.lock().unwrap().remove(&handle); - match handle { - Some(Handle::File(h)) => { - let result = self.layers[h.layer.0].release( - ctx, - h.inode, - flags, - h.handle, - flush, - flock_release, - lock_owner, - ); - self.layers[h.layer.0].forget(ctx, h.inode, 1); - result - } - _ => Err(io::Error::from_raw_os_error(libc::EBADF)), - } - } - - fn statfs(&self, ctx: Context, inode: u64) -> io::Result { - let path = self.path(inode)?; - let layer = self.layer(&path)?; - let inner = self.inner_inode(layer, &path, ctx)?; - let result = self.layers[layer.0].statfs(ctx, inner); - if inner != fuse::ROOT_ID { - self.layers[layer.0].forget(ctx, inner, 1); - } - result - } - - fn setxattr( - &self, - ctx: Context, - inode: u64, - name: &CStr, - value: &[u8], - flags: u32, - ) -> io::Result<()> { - let path = self.path(inode)?; - let inner = self.writable_inner(ctx, &path)?; - let result = self.layers[0].setxattr(ctx, inner, name, value, flags); - self.layers[0].forget(ctx, inner, 1); - result - } - - fn getxattr( - &self, - ctx: Context, - inode: u64, - name: &CStr, - size: u32, - ) -> io::Result { - let path = self.path(inode)?; - let layer = self.layer(&path)?; - let inner = self.inner_inode(layer, &path, ctx)?; - let result = self.layers[layer.0].getxattr(ctx, inner, name, size); - self.layers[layer.0].forget(ctx, inner, 1); - result - } - - fn listxattr(&self, ctx: Context, inode: u64, size: u32) -> io::Result { - let path = self.path(inode)?; - let layer = self.layer(&path)?; - let inner = self.inner_inode(layer, &path, ctx)?; - let result = self.layers[layer.0].listxattr(ctx, inner, size); - self.layers[layer.0].forget(ctx, inner, 1); - result - } - - fn removexattr(&self, ctx: Context, inode: u64, name: &CStr) -> io::Result<()> { - let path = self.path(inode)?; - let inner = self.writable_inner(ctx, &path)?; - let result = self.layers[0].removexattr(ctx, inner, name); - self.layers[0].forget(ctx, inner, 1); - result - } - - fn opendir( - &self, - ctx: Context, - inode: u64, - _flags: u32, - ) -> io::Result<(Option, OpenOptions)> { - let path = self.path(inode)?; - let mut items = Vec::new(); - for name in self.core.list_names(&path)? { - let child = OverlayCore::child(&path, &name)?; - let child_inode = self.allocate_inode(child.clone()); - let entry = self.entry(ctx, &child, child_inode)?; - items.push(DirectoryItem { - ino: child_inode, - name: name.as_bytes().to_vec(), - type_: Self::dtype(entry.attr.st_mode), - }); - } - let handle = self.allocate_handle(Handle::Directory(items)); - Ok((Some(handle), OpenOptions::empty())) - } - - fn readdir( - &self, - _ctx: Context, - _inode: u64, - handle: u64, - _size: u32, - offset: u64, - mut add_entry: F, - ) -> io::Result<()> - where - F: FnMut(DirEntry) -> io::Result, - { - let handles = self.handles.lock().unwrap(); - let items = match handles.get(&handle) { - Some(Handle::Directory(items)) => items, - _ => return Err(io::Error::from_raw_os_error(libc::EBADF)), - }; - for (index, item) in items.iter().enumerate().skip(offset as usize) { - if add_entry(DirEntry { - ino: item.ino as _, - offset: (index + 1) as u64, - type_: item.type_, - name: &item.name, - })? == 0 - { - break; - } - } - Ok(()) - } - - fn readdirplus( - &self, - ctx: Context, - _inode: u64, - handle: u64, - _size: u32, - offset: u64, - mut add_entry: F, - ) -> io::Result<()> - where - F: FnMut(DirEntry, Entry) -> io::Result, - { - let handles = self.handles.lock().unwrap(); - let items = match handles.get(&handle) { - Some(Handle::Directory(items)) => items, - _ => return Err(io::Error::from_raw_os_error(libc::EBADF)), - }; - for (index, item) in items.iter().enumerate().skip(offset as usize) { - let path = self.path(item.ino)?; - let entry = self.entry(ctx, &path, item.ino)?; - if add_entry( - DirEntry { - ino: item.ino as _, - offset: (index + 1) as u64, - type_: item.type_, - name: &item.name, - }, - entry, - )? == 0 - { - break; - } - } - Ok(()) - } - - fn releasedir(&self, _ctx: Context, _inode: u64, _flags: u32, handle: u64) -> io::Result<()> { - match self.handles.lock().unwrap().remove(&handle) { - Some(Handle::Directory(_)) => Ok(()), - _ => Err(io::Error::from_raw_os_error(libc::EBADF)), - } - } - - fn access(&self, ctx: Context, inode: u64, mask: u32) -> io::Result<()> { - let path = self.path(inode)?; - let layer = self.layer(&path)?; - let inner = self.inner_inode(layer, &path, ctx)?; - let result = self.layers[layer.0].access(ctx, inner, mask); - if inner != fuse::ROOT_ID { - self.layers[layer.0].forget(ctx, inner, 1); - } - result - } - - fn lseek( - &self, - ctx: Context, - _inode: u64, - handle: u64, - offset: u64, - whence: u32, - ) -> io::Result { - self.with_file_handle(handle, |fs, h| { - fs.lseek(ctx, h.inode, h.handle, offset, whence) - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn mutations_land_in_upper_and_lower_stays_immutable() { - let temp = tempfile::tempdir().unwrap(); - let lower = temp.path().join("lower"); - let upper = temp.path().join("upper"); - let work = temp.path().join("work"); - std::fs::create_dir_all(&lower).unwrap(); - std::fs::write(lower.join("original"), b"lower").unwrap(); - let fs = OverlayFs::new( - Config { - lower_dirs: vec![lower.to_string_lossy().into_owned()], - upper_dir: upper.to_string_lossy().into_owned(), - work_dir: Some(work.to_string_lossy().into_owned()), - preimage_dir: None, - excluded_paths: Vec::new(), - semantics: passthrough::PermissionSemantics::LinuxComplete, - }, - Arc::new(InodeAllocator::new()), - ) - .unwrap(); - fs.init(FsOptions::empty()).unwrap(); - let ctx = Context { - uid: 0, - gid: 0, - pid: 1, - }; - let original = CString::new("original").unwrap(); - fs.lookup(ctx, fuse::ROOT_ID, &original).unwrap(); - fs.unlink(ctx, fuse::ROOT_ID, &original).unwrap(); - assert_eq!(std::fs::read(lower.join("original")).unwrap(), b"lower"); - assert!(upper.join(".wh.original").is_file()); - - let created = CString::new("created").unwrap(); - let (entry, handle, _) = fs - .create( - ctx, - fuse::ROOT_ID, - &created, - libc::S_IFREG as u32 | 0o640, - false, - libc::O_RDWR as u32, - 0, - Extensions::default(), - ) - .unwrap(); - fs.release( - ctx, - entry.inode, - libc::O_RDWR as u32, - handle.unwrap(), - false, - false, - None, - ) - .unwrap(); - assert!(upper.join("created").is_file()); - assert!(!lower.join("created").exists()); - } - - #[test] - fn shares_inode_allocator_with_virtual_entries() { - let temp = tempfile::tempdir().unwrap(); - let lower = temp.path().join("lower"); - let upper = temp.path().join("upper"); - std::fs::create_dir_all(&lower).unwrap(); - std::fs::write(lower.join("real"), b"data").unwrap(); - let inode_alloc = Arc::new(InodeAllocator::new()); - let fs = OverlayFs::new( - Config { - lower_dirs: vec![lower.to_string_lossy().into_owned()], - upper_dir: upper.to_string_lossy().into_owned(), - work_dir: None, - preimage_dir: None, - excluded_paths: Vec::new(), - semantics: passthrough::PermissionSemantics::LinuxComplete, - }, - inode_alloc.clone(), - ) - .unwrap(); - fs.init(FsOptions::empty()).unwrap(); - - // AugmentFs registers /init.krun after constructing its inner - // filesystem. Simulate that allocation and verify the first real - // lookup cannot reuse the virtual inode number. - let virtual_inode = inode_alloc.next(); - let entry = fs - .lookup( - Context { - uid: 0, - gid: 0, - pid: 1, - }, - fuse::ROOT_ID, - c"real", - ) - .unwrap(); - assert_ne!(entry.inode, virtual_inode); - } -} diff --git a/vendor/krun-devices/src/virtio/fs/read_only.rs b/vendor/krun-devices/src/virtio/fs/read_only.rs deleted file mode 100644 index 5495db1ed..000000000 --- a/vendor/krun-devices/src/virtio/fs/read_only.rs +++ /dev/null @@ -1,501 +0,0 @@ -// Read-only wrapper for PassthroughFs. -// -// Delegates all read-only FUSE operations to the inner PassthroughFs and -// rejects all mutating operations with EROFS (read-only filesystem). -// -// IMPORTANT: When adding new methods to the FileSystem trait, review this -// wrapper to ensure mutating operations are explicitly blocked with EROFS. -// Unoverridden methods fall back to the trait defaults (which return ENOSYS), -// so the wrapper fails closed -- but new methods should still be explicitly -// handled here for correct error semantics. - -#[cfg(target_os = "macos")] -use crossbeam_channel::Sender; -use std::ffi::CStr; -use std::io; -use std::sync::atomic::AtomicI32; -use std::sync::Arc; -use std::time::Duration; - -#[cfg(target_os = "macos")] -use utils::worker_message::WorkerMessage; - -use super::filesystem::{ - Context, DirEntry, Entry, Extensions, FileSystem, FsOptions, GetxattrReply, ListxattrReply, - OpenOptions, SetattrValid, ZeroCopyReader, ZeroCopyWriter, -}; -use super::fuse; -use super::inode_alloc::InodeAllocator; -use super::passthrough::{self, PassthroughFs}; -use crate::virtio::bindings; - -type Inode = u64; -type Handle = u64; - -fn erofs() -> io::Error { - io::Error::from_raw_os_error(libc::EROFS) -} - -fn read_only_open_flags(flags: u32) -> io::Result { - let f = flags as i32; - if f & libc::O_ACCMODE != libc::O_RDONLY { - return Err(erofs()); - } - if f & libc::O_TRUNC != 0 { - return Err(erofs()); - } - #[cfg(target_os = "linux")] - if f & libc::O_TMPFILE != 0 { - return Err(erofs()); - } - - Ok((flags & !(libc::O_ACCMODE as u32)) | (libc::O_RDONLY as u32)) -} - -pub struct PassthroughFsRo { - inner: PassthroughFs, -} - -impl PassthroughFsRo { - pub fn new(cfg: passthrough::Config, inode_alloc: Arc) -> io::Result { - Ok(Self { - inner: PassthroughFs::new(cfg, inode_alloc)?, - }) - } -} - -impl FileSystem for PassthroughFsRo { - type Inode = Inode; - type Handle = Handle; - - // --- Delegated read-only operations --- - - fn init(&self, capable: FsOptions) -> io::Result { - let opts = self.inner.init(capable)?; - // Strip WRITEBACK_CACHE to prevent the guest kernel from buffering writes. - Ok(opts & !FsOptions::WRITEBACK_CACHE) - } - - fn destroy(&self) { - self.inner.destroy() - } - - fn lookup(&self, ctx: Context, parent: Inode, name: &CStr) -> io::Result { - self.inner.lookup(ctx, parent, name) - } - - fn forget(&self, ctx: Context, inode: Inode, count: u64) { - self.inner.forget(ctx, inode, count) - } - - fn batch_forget(&self, ctx: Context, requests: Vec<(Inode, u64)>) { - self.inner.batch_forget(ctx, requests) - } - - fn getattr( - &self, - ctx: Context, - inode: Inode, - handle: Option, - ) -> io::Result<(bindings::stat64, Duration)> { - self.inner.getattr(ctx, inode, handle) - } - - fn readlink(&self, ctx: Context, inode: Inode) -> io::Result> { - self.inner.readlink(ctx, inode) - } - - fn open( - &self, - ctx: Context, - inode: Inode, - kill_priv: bool, - flags: u32, - ) -> io::Result<(Option, OpenOptions)> { - let ro_flags = read_only_open_flags(flags)?; - self.inner.open(ctx, inode, kill_priv, ro_flags) - } - - fn read( - &self, - ctx: Context, - inode: Inode, - handle: Handle, - w: W, - size: u32, - offset: u64, - lock_owner: Option, - flags: u32, - ) -> io::Result { - self.inner - .read(ctx, inode, handle, w, size, offset, lock_owner, flags) - } - - fn flush(&self, ctx: Context, inode: Inode, handle: Handle, lock_owner: u64) -> io::Result<()> { - self.inner.flush(ctx, inode, handle, lock_owner) - } - - fn fsync(&self, ctx: Context, inode: Inode, datasync: bool, handle: Handle) -> io::Result<()> { - self.inner.fsync(ctx, inode, datasync, handle) - } - - fn release( - &self, - ctx: Context, - inode: Inode, - flags: u32, - handle: Handle, - flush: bool, - flock_release: bool, - lock_owner: Option, - ) -> io::Result<()> { - self.inner - .release(ctx, inode, flags, handle, flush, flock_release, lock_owner) - } - - fn statfs(&self, ctx: Context, inode: Inode) -> io::Result { - let mut st = self.inner.statfs(ctx, inode)?; - st.f_flag |= libc::ST_RDONLY; - Ok(st) - } - - fn getxattr( - &self, - ctx: Context, - inode: Inode, - name: &CStr, - size: u32, - ) -> io::Result { - self.inner.getxattr(ctx, inode, name, size) - } - - fn listxattr(&self, ctx: Context, inode: Inode, size: u32) -> io::Result { - self.inner.listxattr(ctx, inode, size) - } - - fn opendir( - &self, - ctx: Context, - inode: Inode, - flags: u32, - ) -> io::Result<(Option, OpenOptions)> { - let f = flags as i32; - let accmode = f & libc::O_ACCMODE; - if accmode != libc::O_RDONLY { - return Err(erofs()); - } - // Force O_RDONLY on the underlying call. - let ro_flags = (flags & !(libc::O_ACCMODE as u32)) | (libc::O_RDONLY as u32); - self.inner.opendir(ctx, inode, ro_flags) - } - - fn readdir( - &self, - ctx: Context, - inode: Inode, - handle: Handle, - size: u32, - offset: u64, - add_entry: F, - ) -> io::Result<()> - where - F: FnMut(DirEntry) -> io::Result, - { - self.inner - .readdir(ctx, inode, handle, size, offset, add_entry) - } - - fn readdirplus( - &self, - ctx: Context, - inode: Inode, - handle: Handle, - size: u32, - offset: u64, - add_entry: F, - ) -> io::Result<()> - where - F: FnMut(DirEntry, Entry) -> io::Result, - { - self.inner - .readdirplus(ctx, inode, handle, size, offset, add_entry) - } - - fn fsyncdir( - &self, - ctx: Context, - inode: Inode, - datasync: bool, - handle: Handle, - ) -> io::Result<()> { - self.inner.fsyncdir(ctx, inode, datasync, handle) - } - - fn releasedir(&self, ctx: Context, inode: Inode, flags: u32, handle: Handle) -> io::Result<()> { - self.inner.releasedir(ctx, inode, flags, handle) - } - - fn access(&self, ctx: Context, inode: Inode, mask: u32) -> io::Result<()> { - if mask & (libc::W_OK as u32) != 0 { - return Err(erofs()); - } - self.inner.access(ctx, inode, mask) - } - - fn lseek( - &self, - ctx: Context, - inode: Inode, - handle: Handle, - offset: u64, - whence: u32, - ) -> io::Result { - self.inner.lseek(ctx, inode, handle, offset, whence) - } - - fn setupmapping( - &self, - ctx: Context, - inode: Inode, - handle: Handle, - foffset: u64, - len: u64, - flags: u64, - moffset: u64, - host_shm_base: u64, - shm_size: u64, - #[cfg(target_os = "macos")] map_sender: &Option>, - ) -> io::Result<()> { - // Reject writable mappings. - if (flags & fuse::SetupmappingFlags::WRITE.bits()) != 0 { - return Err(erofs()); - } - self.inner.setupmapping( - ctx, - inode, - handle, - foffset, - len, - flags, - moffset, - host_shm_base, - shm_size, - #[cfg(target_os = "macos")] - map_sender, - ) - } - - fn removemapping( - &self, - ctx: Context, - requests: Vec, - host_shm_base: u64, - shm_size: u64, - #[cfg(target_os = "macos")] map_sender: &Option>, - ) -> io::Result<()> { - self.inner.removemapping( - ctx, - requests, - host_shm_base, - shm_size, - #[cfg(target_os = "macos")] - map_sender, - ) - } - - fn ioctl( - &self, - ctx: Context, - inode: Inode, - handle: Handle, - flags: u32, - cmd: u32, - arg: u64, - in_size: u32, - out_size: u32, - exit_code: &Arc, - ) -> io::Result> { - self.inner.ioctl( - ctx, inode, handle, flags, cmd, arg, in_size, out_size, exit_code, - ) - } - - // --- Write operations rejected with EROFS --- - - fn setattr( - &self, - _ctx: Context, - _inode: Inode, - _attr: bindings::stat64, - _handle: Option, - _valid: SetattrValid, - ) -> io::Result<(bindings::stat64, Duration)> { - Err(erofs()) - } - - fn symlink( - &self, - _ctx: Context, - _linkname: &CStr, - _parent: Inode, - _name: &CStr, - _extensions: Extensions, - ) -> io::Result { - Err(erofs()) - } - - fn mknod( - &self, - _ctx: Context, - _inode: Inode, - _name: &CStr, - _mode: u32, - _rdev: u32, - _umask: u32, - _extensions: Extensions, - ) -> io::Result { - Err(erofs()) - } - - fn mkdir( - &self, - _ctx: Context, - _parent: Inode, - _name: &CStr, - _mode: u32, - _umask: u32, - _extensions: Extensions, - ) -> io::Result { - Err(erofs()) - } - - fn unlink(&self, _ctx: Context, _parent: Inode, _name: &CStr) -> io::Result<()> { - Err(erofs()) - } - - fn rmdir(&self, _ctx: Context, _parent: Inode, _name: &CStr) -> io::Result<()> { - Err(erofs()) - } - - fn rename( - &self, - _ctx: Context, - _olddir: Inode, - _oldname: &CStr, - _newdir: Inode, - _newname: &CStr, - _flags: u32, - ) -> io::Result<()> { - Err(erofs()) - } - - fn link( - &self, - _ctx: Context, - _inode: Inode, - _newparent: Inode, - _newname: &CStr, - ) -> io::Result { - Err(erofs()) - } - - fn create( - &self, - _ctx: Context, - _parent: Inode, - _name: &CStr, - _mode: u32, - _kill_priv: bool, - _flags: u32, - _umask: u32, - _extensions: Extensions, - ) -> io::Result<(Entry, Option, OpenOptions)> { - Err(erofs()) - } - - fn write( - &self, - _ctx: Context, - _inode: Inode, - _handle: Handle, - _r: R, - _size: u32, - _offset: u64, - _lock_owner: Option, - _delayed_write: bool, - _kill_priv: bool, - _flags: u32, - ) -> io::Result { - Err(erofs()) - } - - fn fallocate( - &self, - _ctx: Context, - _inode: Inode, - _handle: Handle, - _mode: u32, - _offset: u64, - _length: u64, - ) -> io::Result<()> { - Err(erofs()) - } - - fn setxattr( - &self, - _ctx: Context, - _inode: Inode, - _name: &CStr, - _value: &[u8], - _flags: u32, - ) -> io::Result<()> { - Err(erofs()) - } - - fn removexattr(&self, _ctx: Context, _inode: Inode, _name: &CStr) -> io::Result<()> { - Err(erofs()) - } - - fn copyfilerange( - &self, - _ctx: Context, - _inode_in: Inode, - _handle_in: Handle, - _offset_in: u64, - _inode_out: Inode, - _handle_out: Handle, - _offset_out: u64, - _len: u64, - _flags: u64, - ) -> io::Result { - Err(erofs()) - } -} - -#[cfg(test)] -mod tests { - use super::read_only_open_flags; - - #[test] - fn read_only_open_flags_allow_append() { - let flags = (libc::O_RDONLY | libc::O_APPEND) as u32; - let ro_flags = read_only_open_flags(flags).unwrap(); - - assert_eq!((ro_flags as i32) & libc::O_ACCMODE, libc::O_RDONLY); - assert_ne!((ro_flags as i32) & libc::O_APPEND, 0); - } - - #[test] - fn read_only_open_flags_reject_write_access() { - let err = read_only_open_flags(libc::O_WRONLY as u32).unwrap_err(); - - assert_eq!(err.raw_os_error(), Some(libc::EROFS)); - } - - #[test] - fn read_only_open_flags_reject_truncate() { - let err = read_only_open_flags((libc::O_RDONLY | libc::O_TRUNC) as u32).unwrap_err(); - - assert_eq!(err.raw_os_error(), Some(libc::EROFS)); - } -} diff --git a/vendor/krun-devices/src/virtio/fs/server.rs b/vendor/krun-devices/src/virtio/fs/server.rs deleted file mode 100644 index 5b37c0bb7..000000000 --- a/vendor/krun-devices/src/virtio/fs/server.rs +++ /dev/null @@ -1,1667 +0,0 @@ -// Copyright 2019 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#[cfg(target_os = "macos")] -use crossbeam_channel::Sender; -#[cfg(target_os = "macos")] -use utils::worker_message::WorkerMessage; - -use std::convert::TryInto; -use std::ffi::{CStr, CString}; -use std::fs::File; -use std::io::{self, Read, Write}; -use std::mem::size_of; -use std::sync::atomic::{AtomicI32, AtomicU64, Ordering}; -use std::sync::Arc; - -use vm_memory::ByteValued; - -use super::super::linux_errno::linux_error; -use super::bindings; -use super::descriptor_utils::{Reader, Writer}; -use super::filesystem::{ - Context, DirEntry, Entry, Extensions, FileSystem, GetxattrReply, ListxattrReply, SecContext, - ZeroCopyReader, ZeroCopyWriter, -}; -use super::fs_utils::einval; -use super::fuse::*; -use super::{FsError as Error, Result}; -use crate::virtio::VirtioShmRegion; - -const MAX_BUFFER_SIZE: u32 = 1 << 20; -const BUFFER_HEADER_SIZE: u32 = 0x1000; -const DIRENT_PADDING: [u8; 8] = [0; 8]; - -struct ZCReader<'a>(Reader<'a>); - -impl ZeroCopyReader for ZCReader<'_> { - fn read_to(&mut self, f: &File, count: usize, off: u64) -> io::Result { - self.0.read_to_at(f, count, off) - } -} - -impl io::Read for ZCReader<'_> { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - self.0.read(buf) - } -} - -struct ZCWriter<'a>(Writer<'a>); - -impl ZeroCopyWriter for ZCWriter<'_> { - fn write_from(&mut self, f: &File, count: usize, off: u64) -> io::Result { - self.0.write_from_at(f, count, off) - } -} - -impl io::Write for ZCWriter<'_> { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.0.write(buf) - } - - fn flush(&mut self) -> io::Result<()> { - self.0.flush() - } -} - -pub struct Server { - fs: F, - options: AtomicU64, -} - -impl Server { - pub fn new(fs: F) -> Server { - Server { - fs, - options: AtomicU64::new(FsOptions::empty().bits()), - } - } - - #[allow(clippy::cognitive_complexity)] - pub fn handle_message( - &self, - mut r: Reader, - w: Writer, - allow_idmap: bool, - shm_region: &Option, - exit_code: &Arc, - #[cfg(target_os = "macos")] map_sender: &Option>, - ) -> Result { - let in_header: InHeader = r.read_obj().map_err(Error::DecodeMessage)?; - - if in_header.len > (MAX_BUFFER_SIZE + BUFFER_HEADER_SIZE) { - return reply_error( - linux_error(io::Error::from_raw_os_error(libc::ENOMEM)), - in_header.unique, - w, - ); - } - debug!("opcode: {}", in_header.opcode); - match in_header.opcode { - x if x == Opcode::Lookup as u32 => self.lookup(in_header, r, w), - x if x == Opcode::Forget as u32 => self.forget(in_header, r), // No reply. - x if x == Opcode::Getattr as u32 => self.getattr(in_header, r, w), - x if x == Opcode::Setattr as u32 => self.setattr(in_header, r, w), - x if x == Opcode::Readlink as u32 => self.readlink(in_header, w), - x if x == Opcode::Symlink as u32 => self.symlink(in_header, r, w), - x if x == Opcode::Mknod as u32 => self.mknod(in_header, r, w), - x if x == Opcode::Mkdir as u32 => self.mkdir(in_header, r, w), - x if x == Opcode::Unlink as u32 => self.unlink(in_header, r, w), - x if x == Opcode::Rmdir as u32 => self.rmdir(in_header, r, w), - x if x == Opcode::Rename as u32 => self.rename(in_header, r, w), - x if x == Opcode::Link as u32 => self.link(in_header, r, w), - x if x == Opcode::Open as u32 => self.open(in_header, r, w), - x if x == Opcode::Read as u32 => self.read(in_header, r, w), - x if x == Opcode::Write as u32 => self.write(in_header, r, w), - x if x == Opcode::Statfs as u32 => self.statfs(in_header, w), - x if x == Opcode::Release as u32 => self.release(in_header, r, w), - x if x == Opcode::Fsync as u32 => self.fsync(in_header, r, w), - x if x == Opcode::Setxattr as u32 => self.setxattr(in_header, r, w), - x if x == Opcode::Getxattr as u32 => self.getxattr(in_header, r, w), - x if x == Opcode::Listxattr as u32 => self.listxattr(in_header, r, w), - x if x == Opcode::Removexattr as u32 => self.removexattr(in_header, r, w), - x if x == Opcode::Flush as u32 => self.flush(in_header, r, w), - x if x == Opcode::Init as u32 => self.init(in_header, r, w, allow_idmap), - x if x == Opcode::Opendir as u32 => self.opendir(in_header, r, w), - x if x == Opcode::Readdir as u32 => self.readdir(in_header, r, w), - x if x == Opcode::Releasedir as u32 => self.releasedir(in_header, r, w), - x if x == Opcode::Fsyncdir as u32 => self.fsyncdir(in_header, r, w), - x if x == Opcode::Getlk as u32 => self.getlk(in_header, r, w), - x if x == Opcode::Setlk as u32 => self.setlk(in_header, r, w), - x if x == Opcode::Setlkw as u32 => self.setlkw(in_header, r, w), - x if x == Opcode::Access as u32 => self.access(in_header, r, w), - x if x == Opcode::Create as u32 => self.create(in_header, r, w), - x if x == Opcode::Interrupt as u32 => self.interrupt(in_header), - x if x == Opcode::Bmap as u32 => self.bmap(in_header, r, w), - x if x == Opcode::Destroy as u32 => self.destroy(), - x if x == Opcode::Ioctl as u32 => self.ioctl(in_header, r, w, exit_code), - x if x == Opcode::Poll as u32 => self.poll(in_header, r, w), - x if x == Opcode::NotifyReply as u32 => self.notify_reply(in_header, r, w), - x if x == Opcode::BatchForget as u32 => self.batch_forget(in_header, r, w), - x if x == Opcode::Fallocate as u32 => self.fallocate(in_header, r, w), - x if x == Opcode::Readdirplus as u32 => self.readdirplus(in_header, r, w), - x if x == Opcode::Rename2 as u32 => self.rename2(in_header, r, w), - x if x == Opcode::Lseek as u32 => self.lseek(in_header, r, w), - x if x == Opcode::CopyFileRange as u32 => self.copyfilerange(in_header, r, w), - x if (x == Opcode::SetupMapping as u32) && shm_region.is_some() => { - let shm = shm_region.as_ref().unwrap(); - #[cfg(target_os = "linux")] - let shm_base_addr = shm.host_addr; - #[cfg(target_os = "macos")] - let shm_base_addr = shm.guest_addr; - self.setupmapping( - in_header, - r, - w, - shm_base_addr, - shm.size as u64, - #[cfg(target_os = "macos")] - map_sender, - ) - } - x if (x == Opcode::RemoveMapping as u32) && shm_region.is_some() => { - let shm = shm_region.as_ref().unwrap(); - #[cfg(target_os = "linux")] - let shm_base_addr = shm.host_addr; - #[cfg(target_os = "macos")] - let shm_base_addr = shm.guest_addr; - self.removemapping( - in_header, - r, - w, - shm_base_addr, - shm.size as u64, - #[cfg(target_os = "macos")] - map_sender, - ) - } - _ => reply_error( - linux_error(io::Error::from_raw_os_error(libc::ENOSYS)), - in_header.unique, - w, - ), - } - } - - fn lookup(&self, in_header: InHeader, mut r: Reader, w: Writer) -> Result { - let namelen = (in_header.len as usize) - .checked_sub(size_of::()) - .ok_or(Error::InvalidHeaderLength)?; - - let mut buf = vec![0u8; namelen]; - - r.read_exact(&mut buf).map_err(Error::DecodeMessage)?; - - let name = bytes_to_cstr(buf.as_ref())?; - - match self - .fs - .lookup(Context::from(in_header), in_header.nodeid.into(), name) - { - Ok(entry) => { - let out = EntryOut::from(entry); - - reply_ok(Some(out), None, in_header.unique, w) - } - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn forget(&self, in_header: InHeader, mut r: Reader) -> Result { - let ForgetIn { nlookup } = r.read_obj().map_err(Error::DecodeMessage)?; - - self.fs - .forget(Context::from(in_header), in_header.nodeid.into(), nlookup); - - // There is no reply for forget messages. - Ok(0) - } - - fn getattr(&self, in_header: InHeader, mut r: Reader, w: Writer) -> Result { - let GetattrIn { flags, fh, .. } = r.read_obj().map_err(Error::DecodeMessage)?; - - let handle = if (flags & GETATTR_FH) != 0 { - Some(fh.into()) - } else { - None - }; - - match self - .fs - .getattr(Context::from(in_header), in_header.nodeid.into(), handle) - { - Ok((st, timeout)) => { - let out = AttrOut { - attr_valid: timeout.as_secs(), - attr_valid_nsec: timeout.subsec_nanos(), - dummy: 0, - attr: st.into(), - }; - reply_ok(Some(out), None, in_header.unique, w) - } - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn setattr(&self, in_header: InHeader, mut r: Reader, w: Writer) -> Result { - let setattr_in: SetattrIn = r.read_obj().map_err(Error::DecodeMessage)?; - - let handle = if setattr_in.valid & FATTR_FH != 0 { - Some(setattr_in.fh.into()) - } else { - None - }; - - let valid = SetattrValid::from_bits_truncate(setattr_in.valid); - - let st: bindings::stat64 = setattr_in.into(); - - match self.fs.setattr( - Context::from(in_header), - in_header.nodeid.into(), - st, - handle, - valid, - ) { - Ok((st, timeout)) => { - let out = AttrOut { - attr_valid: timeout.as_secs(), - attr_valid_nsec: timeout.subsec_nanos(), - dummy: 0, - attr: st.into(), - }; - reply_ok(Some(out), None, in_header.unique, w) - } - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn readlink(&self, in_header: InHeader, w: Writer) -> Result { - match self - .fs - .readlink(Context::from(in_header), in_header.nodeid.into()) - { - Ok(linkname) => { - // We need to disambiguate the option type here even though it is `None`. - reply_ok(None::, Some(&linkname), in_header.unique, w) - } - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn symlink(&self, in_header: InHeader, mut r: Reader, w: Writer) -> Result { - // Unfortunately the name and linkname are encoded one after another and - // separated by a nul character. - let len = (in_header.len as usize) - .checked_sub(size_of::()) - .ok_or(Error::InvalidHeaderLength)?; - let mut buf = vec![0; len]; - - r.read_exact(&mut buf).map_err(Error::DecodeMessage)?; - - let mut components = buf.split_inclusive(|c| *c == b'\0'); - - let name = components.next().ok_or(Error::MissingParameter)?; - let linkname = components.next().ok_or(Error::MissingParameter)?; - - let options = FsOptions::from_bits_truncate(self.options.load(Ordering::Relaxed)); - - let extensions = get_extensions(options, name.len() + linkname.len(), buf.as_slice())?; - - match self.fs.symlink( - Context::from(in_header), - bytes_to_cstr(linkname)?, - in_header.nodeid.into(), - bytes_to_cstr(name)?, - extensions, - ) { - Ok(entry) => { - let out = EntryOut::from(entry); - - reply_ok(Some(out), None, in_header.unique, w) - } - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn mknod(&self, in_header: InHeader, mut r: Reader, w: Writer) -> Result { - let MknodIn { - mode, rdev, umask, .. - } = r.read_obj().map_err(Error::DecodeMessage)?; - - let remaining_len = (in_header.len as usize) - .checked_sub(size_of::()) - .and_then(|l| l.checked_sub(size_of::())) - .ok_or(Error::InvalidHeaderLength)?; - let mut buf = vec![0; remaining_len]; - - r.read_exact(&mut buf).map_err(Error::DecodeMessage)?; - let mut components = buf.split_inclusive(|c| *c == b'\0'); - let name = components.next().ok_or(Error::MissingParameter)?; - - let options = FsOptions::from_bits_truncate(self.options.load(Ordering::Relaxed)); - - let extensions = get_extensions(options, name.len(), buf.as_slice())?; - - match self.fs.mknod( - Context::from(in_header), - in_header.nodeid.into(), - bytes_to_cstr(name)?, - mode, - rdev, - umask, - extensions, - ) { - Ok(entry) => { - let out = EntryOut::from(entry); - - reply_ok(Some(out), None, in_header.unique, w) - } - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn mkdir(&self, in_header: InHeader, mut r: Reader, w: Writer) -> Result { - let MkdirIn { mode, umask } = r.read_obj().map_err(Error::DecodeMessage)?; - - let remaining_len = (in_header.len as usize) - .checked_sub(size_of::()) - .and_then(|l| l.checked_sub(size_of::())) - .ok_or(Error::InvalidHeaderLength)?; - let mut buf = vec![0; remaining_len]; - - r.read_exact(&mut buf).map_err(Error::DecodeMessage)?; - let mut components = buf.split_inclusive(|c| *c == b'\0'); - let name = components.next().ok_or(Error::MissingParameter)?; - - let options = FsOptions::from_bits_truncate(self.options.load(Ordering::Relaxed)); - - let extensions = get_extensions(options, name.len(), buf.as_slice())?; - - match self.fs.mkdir( - Context::from(in_header), - in_header.nodeid.into(), - bytes_to_cstr(name)?, - mode, - umask, - extensions, - ) { - Ok(entry) => { - let out = EntryOut::from(entry); - - reply_ok(Some(out), None, in_header.unique, w) - } - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn unlink(&self, in_header: InHeader, mut r: Reader, w: Writer) -> Result { - let namelen = (in_header.len as usize) - .checked_sub(size_of::()) - .ok_or(Error::InvalidHeaderLength)?; - let mut name = vec![0; namelen]; - - r.read_exact(&mut name).map_err(Error::DecodeMessage)?; - - match self.fs.unlink( - Context::from(in_header), - in_header.nodeid.into(), - bytes_to_cstr(&name)?, - ) { - Ok(()) => reply_ok(None::, None, in_header.unique, w), - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn rmdir(&self, in_header: InHeader, mut r: Reader, w: Writer) -> Result { - let namelen = (in_header.len as usize) - .checked_sub(size_of::()) - .ok_or(Error::InvalidHeaderLength)?; - let mut name = vec![0; namelen]; - - r.read_exact(&mut name).map_err(Error::DecodeMessage)?; - - match self.fs.rmdir( - Context::from(in_header), - in_header.nodeid.into(), - bytes_to_cstr(&name)?, - ) { - Ok(()) => reply_ok(None::, None, in_header.unique, w), - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn do_rename( - &self, - in_header: InHeader, - msg_size: usize, - newdir: u64, - flags: u32, - mut r: Reader, - w: Writer, - ) -> Result { - let buflen = (in_header.len as usize) - .checked_sub(size_of::()) - .and_then(|l| l.checked_sub(msg_size)) - .ok_or(Error::InvalidHeaderLength)?; - let mut buf = vec![0; buflen]; - - r.read_exact(&mut buf).map_err(Error::DecodeMessage)?; - - // We want to include the '\0' byte in the first slice. - let split_pos = buf - .iter() - .position(|c| *c == b'\0') - .map(|p| p + 1) - .ok_or(Error::MissingParameter)?; - - let (oldname, newname) = buf.split_at(split_pos); - - match self.fs.rename( - Context::from(in_header), - in_header.nodeid.into(), - bytes_to_cstr(oldname)?, - newdir.into(), - bytes_to_cstr(newname)?, - flags, - ) { - Ok(()) => reply_ok(None::, None, in_header.unique, w), - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn rename(&self, in_header: InHeader, mut r: Reader, w: Writer) -> Result { - let RenameIn { newdir } = r.read_obj().map_err(Error::DecodeMessage)?; - - self.do_rename(in_header, size_of::(), newdir, 0, r, w) - } - - fn rename2(&self, in_header: InHeader, mut r: Reader, w: Writer) -> Result { - let Rename2In { newdir, flags, .. } = r.read_obj().map_err(Error::DecodeMessage)?; - - #[cfg(target_os = "linux")] - let flags = flags & (libc::RENAME_EXCHANGE | libc::RENAME_NOREPLACE); - - self.do_rename(in_header, size_of::(), newdir, flags, r, w) - } - - fn link(&self, in_header: InHeader, mut r: Reader, w: Writer) -> Result { - let LinkIn { oldnodeid } = r.read_obj().map_err(Error::DecodeMessage)?; - - let namelen = (in_header.len as usize) - .checked_sub(size_of::()) - .and_then(|l| l.checked_sub(size_of::())) - .ok_or(Error::InvalidHeaderLength)?; - let mut name = vec![0; namelen]; - - r.read_exact(&mut name).map_err(Error::DecodeMessage)?; - - match self.fs.link( - Context::from(in_header), - oldnodeid.into(), - in_header.nodeid.into(), - bytes_to_cstr(&name)?, - ) { - Ok(entry) => { - let out = EntryOut::from(entry); - - reply_ok(Some(out), None, in_header.unique, w) - } - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn open(&self, in_header: InHeader, mut r: Reader, w: Writer) -> Result { - let OpenIn { - flags, open_flags, .. - } = r.read_obj().map_err(Error::DecodeMessage)?; - - let kill_priv = open_flags & OPEN_KILL_SUIDGID != 0; - - match self.fs.open( - Context::from(in_header), - in_header.nodeid.into(), - kill_priv, - flags, - ) { - Ok((handle, opts)) => { - let out = OpenOut { - fh: handle.map(Into::into).unwrap_or(0), - open_flags: opts.bits(), - ..Default::default() - }; - - reply_ok(Some(out), None, in_header.unique, w) - } - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn read(&self, in_header: InHeader, mut r: Reader, mut w: Writer) -> Result { - let ReadIn { - fh, - offset, - size, - read_flags, - lock_owner, - flags, - .. - } = r.read_obj().map_err(Error::DecodeMessage)?; - - if size > MAX_BUFFER_SIZE { - return reply_error( - linux_error(io::Error::from_raw_os_error(libc::ENOMEM)), - in_header.unique, - w, - ); - } - - let owner = if read_flags & READ_LOCKOWNER != 0 { - Some(lock_owner) - } else { - None - }; - - // Split the writer into 2 pieces: one for the `OutHeader` and the rest for the data. - let data_writer = ZCWriter(w.split_at(size_of::()).unwrap()); - - match self.fs.read( - Context::from(in_header), - in_header.nodeid.into(), - fh.into(), - data_writer, - size, - offset, - owner, - flags, - ) { - Ok(count) => { - // Don't use `reply_ok` because we need to set a custom size length for the - // header. - let out = OutHeader { - len: (size_of::() + count) as u32, - error: 0, - unique: in_header.unique, - }; - - w.write_all(out.as_slice()).map_err(Error::EncodeMessage)?; - Ok(out.len as usize) - } - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn write(&self, in_header: InHeader, mut r: Reader, w: Writer) -> Result { - let WriteIn { - fh, - offset, - size, - write_flags, - lock_owner, - flags, - .. - } = r.read_obj().map_err(Error::DecodeMessage)?; - - if size > MAX_BUFFER_SIZE { - return reply_error( - linux_error(io::Error::from_raw_os_error(libc::ENOMEM)), - in_header.unique, - w, - ); - } - - let owner = if write_flags & WRITE_LOCKOWNER != 0 { - Some(lock_owner) - } else { - None - }; - - let delayed_write = write_flags & WRITE_CACHE != 0; - let kill_priv = write_flags & WRITE_KILL_PRIV != 0; - - let data_reader = ZCReader(r); - - match self.fs.write( - Context::from(in_header), - in_header.nodeid.into(), - fh.into(), - data_reader, - size, - offset, - owner, - delayed_write, - kill_priv, - flags, - ) { - Ok(count) => { - let out = WriteOut { - size: count as u32, - ..Default::default() - }; - - reply_ok(Some(out), None, in_header.unique, w) - } - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn statfs(&self, in_header: InHeader, w: Writer) -> Result { - match self - .fs - .statfs(Context::from(in_header), in_header.nodeid.into()) - { - Ok(st) => reply_ok(Some(Kstatfs::from(st)), None, in_header.unique, w), - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn release(&self, in_header: InHeader, mut r: Reader, w: Writer) -> Result { - let ReleaseIn { - fh, - flags, - release_flags, - lock_owner, - } = r.read_obj().map_err(Error::DecodeMessage)?; - - let flush = release_flags & RELEASE_FLUSH != 0; - let flock_release = release_flags & RELEASE_FLOCK_UNLOCK != 0; - let lock_owner = if flush || flock_release { - Some(lock_owner) - } else { - None - }; - - match self.fs.release( - Context::from(in_header), - in_header.nodeid.into(), - flags, - fh.into(), - flush, - flock_release, - lock_owner, - ) { - Ok(()) => reply_ok(None::, None, in_header.unique, w), - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn fsync(&self, in_header: InHeader, mut r: Reader, w: Writer) -> Result { - let FsyncIn { - fh, fsync_flags, .. - } = r.read_obj().map_err(Error::DecodeMessage)?; - let datasync = fsync_flags & 0x1 != 0; - - match self.fs.fsync( - Context::from(in_header), - in_header.nodeid.into(), - datasync, - fh.into(), - ) { - Ok(()) => reply_ok(None::, None, in_header.unique, w), - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn setxattr(&self, in_header: InHeader, mut r: Reader, w: Writer) -> Result { - let SetxattrIn { size, flags } = r.read_obj().map_err(Error::DecodeMessage)?; - - // The name and value and encoded one after another and separated by a '\0' character. - let len = (in_header.len as usize) - .checked_sub(size_of::()) - .and_then(|l| l.checked_sub(size_of::())) - .ok_or(Error::InvalidHeaderLength)?; - let mut buf = vec![0; len]; - - r.read_exact(&mut buf).map_err(Error::DecodeMessage)?; - - // We want to include the '\0' byte in the first slice. - let split_pos = buf - .iter() - .position(|c| *c == b'\0') - .map(|p| p + 1) - .ok_or(Error::MissingParameter)?; - - let (name, value) = buf.split_at(split_pos); - - if size != value.len() as u32 { - return Err(Error::InvalidXattrSize((size, value.len()))); - } - - match self.fs.setxattr( - Context::from(in_header), - in_header.nodeid.into(), - bytes_to_cstr(name)?, - value, - flags, - ) { - Ok(()) => reply_ok(None::, None, in_header.unique, w), - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn getxattr(&self, in_header: InHeader, mut r: Reader, w: Writer) -> Result { - let GetxattrIn { size, .. } = r.read_obj().map_err(Error::DecodeMessage)?; - - let namelen = (in_header.len as usize) - .checked_sub(size_of::()) - .and_then(|l| l.checked_sub(size_of::())) - .ok_or(Error::InvalidHeaderLength)?; - let mut name = vec![0; namelen]; - - r.read_exact(&mut name).map_err(Error::DecodeMessage)?; - - if size > MAX_BUFFER_SIZE { - return reply_error( - linux_error(io::Error::from_raw_os_error(libc::ENOMEM)), - in_header.unique, - w, - ); - } - - match self.fs.getxattr( - Context::from(in_header), - in_header.nodeid.into(), - bytes_to_cstr(&name)?, - size, - ) { - Ok(GetxattrReply::Value(val)) => reply_ok(None::, Some(&val), in_header.unique, w), - Ok(GetxattrReply::Count(count)) => { - let out = GetxattrOut { - size: count, - ..Default::default() - }; - - reply_ok(Some(out), None, in_header.unique, w) - } - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn listxattr(&self, in_header: InHeader, mut r: Reader, w: Writer) -> Result { - let GetxattrIn { size, .. } = r.read_obj().map_err(Error::DecodeMessage)?; - - if size > MAX_BUFFER_SIZE { - return reply_error( - linux_error(io::Error::from_raw_os_error(libc::ENOMEM)), - in_header.unique, - w, - ); - } - - match self - .fs - .listxattr(Context::from(in_header), in_header.nodeid.into(), size) - { - Ok(ListxattrReply::Names(val)) => reply_ok(None::, Some(&val), in_header.unique, w), - Ok(ListxattrReply::Count(count)) => { - let out = GetxattrOut { - size: count, - ..Default::default() - }; - - reply_ok(Some(out), None, in_header.unique, w) - } - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn removexattr(&self, in_header: InHeader, mut r: Reader, w: Writer) -> Result { - let namelen = (in_header.len as usize) - .checked_sub(size_of::()) - .ok_or(Error::InvalidHeaderLength)?; - - let mut buf = vec![0; namelen]; - - r.read_exact(&mut buf).map_err(Error::DecodeMessage)?; - - let name = bytes_to_cstr(&buf)?; - - match self - .fs - .removexattr(Context::from(in_header), in_header.nodeid.into(), name) - { - Ok(()) => reply_ok(None::, None, in_header.unique, w), - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn flush(&self, in_header: InHeader, mut r: Reader, w: Writer) -> Result { - let FlushIn { fh, lock_owner, .. } = r.read_obj().map_err(Error::DecodeMessage)?; - - match self.fs.flush( - Context::from(in_header), - in_header.nodeid.into(), - fh.into(), - lock_owner, - ) { - Ok(()) => reply_ok(None::, None, in_header.unique, w), - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn init( - &self, - in_header: InHeader, - mut r: Reader, - w: Writer, - allow_idmap: bool, - ) -> Result { - let InitInCompat { - major, - minor, - max_readahead, - flags, - } = r.read_obj().map_err(Error::DecodeMessage)?; - - let options = FsOptions::from_bits_truncate(flags as u64); - - let InitInExt { flags2, .. } = if options.contains(FsOptions::INIT_EXT) { - r.read_obj().map_err(Error::DecodeMessage)? - } else { - InitInExt::default() - }; - - if major < KERNEL_VERSION { - error!("Unsupported fuse protocol version: {major}.{minor}"); - return reply_error( - linux_error(io::Error::from_raw_os_error(libc::EPROTO)), - in_header.unique, - w, - ); - } - - if major > KERNEL_VERSION { - // Wait for the kernel to reply back with a 7.X version. - let out = InitOut { - major: KERNEL_VERSION, - minor: KERNEL_MINOR_VERSION, - ..Default::default() - }; - - return reply_ok(Some(out), None, in_header.unique, w); - } - - if minor < KERNEL_MINOR_VERSION { - error!("Unsupported fuse protocol minor version: {major}.{minor}"); - return reply_error( - linux_error(io::Error::from_raw_os_error(libc::EPROTO)), - in_header.unique, - w, - ); - } - - // These fuse features are supported by this server by default. - let mut supported = FsOptions::ASYNC_READ - | FsOptions::PARALLEL_DIROPS - | FsOptions::BIG_WRITES - | FsOptions::AUTO_INVAL_DATA - | FsOptions::ASYNC_DIO - | FsOptions::HAS_IOCTL_DIR - | FsOptions::ATOMIC_O_TRUNC - | FsOptions::MAX_PAGES - | FsOptions::SUBMOUNTS - | FsOptions::HANDLE_KILLPRIV_V2 - | FsOptions::INIT_EXT; - - if allow_idmap { - supported |= FsOptions::ALLOW_IDMAP; - } - - if cfg!(target_os = "macos") { - supported |= FsOptions::SECURITY_CTX; - } - - let flags_64 = ((flags2 as u64) << 32) | (flags as u64); - let capable = FsOptions::from_bits_truncate(flags_64); - - let page_size: u32 = unsafe { libc::sysconf(libc::_SC_PAGESIZE).try_into().unwrap() }; - let max_pages = ((MAX_BUFFER_SIZE - 1) / page_size) + 1; - - match self.fs.init(capable) { - Ok(want) => { - let enabled = (capable & (want | supported)).bits(); - self.options.store(enabled, Ordering::Relaxed); - - let out = InitOut { - major: KERNEL_VERSION, - minor: KERNEL_MINOR_VERSION, - max_readahead, - flags: enabled as u32, - max_background: u16::MAX, - congestion_threshold: (u16::MAX / 4) * 3, - max_write: MAX_BUFFER_SIZE, - time_gran: 1, // nanoseconds - max_pages: max_pages.try_into().unwrap(), - map_alignment: 0, - flags2: (enabled >> 32) as u32, - ..Default::default() - }; - - reply_ok(Some(out), None, in_header.unique, w) - } - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn opendir(&self, in_header: InHeader, mut r: Reader, w: Writer) -> Result { - let OpenIn { flags, .. } = r.read_obj().map_err(Error::DecodeMessage)?; - - match self - .fs - .opendir(Context::from(in_header), in_header.nodeid.into(), flags) - { - Ok((handle, opts)) => { - let out = OpenOut { - fh: handle.map(Into::into).unwrap_or(0), - open_flags: opts.bits(), - ..Default::default() - }; - - reply_ok(Some(out), None, in_header.unique, w) - } - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn do_readdir( - &self, - in_header: InHeader, - mut r: Reader, - mut w: Writer, - plus: bool, - ) -> Result { - let ReadIn { - fh, offset, size, .. - } = r.read_obj().map_err(Error::DecodeMessage)?; - - if size > MAX_BUFFER_SIZE { - return reply_error( - linux_error(io::Error::from_raw_os_error(libc::ENOMEM)), - in_header.unique, - w, - ); - } - - let available_bytes = w.available_bytes(); - if available_bytes < size as usize { - return reply_error( - linux_error(io::Error::from_raw_os_error(libc::ENOMEM)), - in_header.unique, - w, - ); - } - - // Skip over enough bytes for the header. - let mut cursor = w.split_at(size_of::()).unwrap(); - - let res = if plus { - self.fs.readdirplus( - Context::from(in_header), - in_header.nodeid.into(), - fh.into(), - size, - offset, - |d, e| add_dirent(&mut cursor, size, d, Some(e)), - ) - } else { - self.fs.readdir( - Context::from(in_header), - in_header.nodeid.into(), - fh.into(), - size, - offset, - |d| add_dirent(&mut cursor, size, d, None), - ) - }; - - if let Err(e) = res { - reply_error(e, in_header.unique, w) - } else { - // Don't use `reply_ok` because we need to set a custom size length for the - // header. - let out = OutHeader { - len: (size_of::() + cursor.bytes_written()) as u32, - error: 0, - unique: in_header.unique, - }; - - w.write_all(out.as_slice()).map_err(Error::EncodeMessage)?; - Ok(out.len as usize) - } - } - - fn readdir(&self, in_header: InHeader, r: Reader, w: Writer) -> Result { - self.do_readdir(in_header, r, w, false) - } - - fn readdirplus(&self, in_header: InHeader, r: Reader, w: Writer) -> Result { - self.do_readdir(in_header, r, w, true) - } - - fn releasedir(&self, in_header: InHeader, mut r: Reader, w: Writer) -> Result { - let ReleaseIn { fh, flags, .. } = r.read_obj().map_err(Error::DecodeMessage)?; - - match self.fs.releasedir( - Context::from(in_header), - in_header.nodeid.into(), - flags, - fh.into(), - ) { - Ok(()) => reply_ok(None::, None, in_header.unique, w), - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn fsyncdir(&self, in_header: InHeader, mut r: Reader, w: Writer) -> Result { - let FsyncIn { - fh, fsync_flags, .. - } = r.read_obj().map_err(Error::DecodeMessage)?; - let datasync = fsync_flags & 0x1 != 0; - - match self.fs.fsyncdir( - Context::from(in_header), - in_header.nodeid.into(), - datasync, - fh.into(), - ) { - Ok(()) => reply_ok(None::, None, in_header.unique, w), - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn getlk(&self, in_header: InHeader, mut _r: Reader, w: Writer) -> Result { - if let Err(e) = self.fs.getlk() { - reply_error(e, in_header.unique, w) - } else { - Ok(0) - } - } - - fn setlk(&self, in_header: InHeader, mut _r: Reader, w: Writer) -> Result { - if let Err(e) = self.fs.setlk() { - reply_error(e, in_header.unique, w) - } else { - Ok(0) - } - } - - fn setlkw(&self, in_header: InHeader, mut _r: Reader, w: Writer) -> Result { - if let Err(e) = self.fs.setlkw() { - reply_error(e, in_header.unique, w) - } else { - Ok(0) - } - } - - fn access(&self, in_header: InHeader, mut r: Reader, w: Writer) -> Result { - let AccessIn { mask, .. } = r.read_obj().map_err(Error::DecodeMessage)?; - - match self - .fs - .access(Context::from(in_header), in_header.nodeid.into(), mask) - { - Ok(()) => reply_ok(None::, None, in_header.unique, w), - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn create(&self, in_header: InHeader, mut r: Reader, w: Writer) -> Result { - let CreateIn { - flags, - mode, - umask, - open_flags, - .. - } = r.read_obj().map_err(Error::DecodeMessage)?; - - let namelen = (in_header.len as usize) - .checked_sub(size_of::()) - .and_then(|l| l.checked_sub(size_of::())) - .ok_or(Error::InvalidHeaderLength)?; - - let mut buf = vec![0; namelen]; - - r.read_exact(&mut buf).map_err(Error::DecodeMessage)?; - let mut components = buf.split_inclusive(|c| *c == b'\0'); - let name = components.next().ok_or(Error::MissingParameter)?; - - let options = FsOptions::from_bits_truncate(self.options.load(Ordering::Relaxed)); - - let extensions = get_extensions(options, name.len(), buf.as_slice())?; - - let kill_priv = open_flags & OPEN_KILL_SUIDGID != 0; - - match self.fs.create( - Context::from(in_header), - in_header.nodeid.into(), - bytes_to_cstr(name)?, - mode, - kill_priv, - flags, - umask, - extensions, - ) { - Ok((entry, handle, opts)) => { - let entry_out = EntryOut { - nodeid: entry.inode, - generation: entry.generation, - entry_valid: entry.entry_timeout.as_secs(), - attr_valid: entry.attr_timeout.as_secs(), - entry_valid_nsec: entry.entry_timeout.subsec_nanos(), - attr_valid_nsec: entry.attr_timeout.subsec_nanos(), - attr: entry.attr.into(), - }; - let open_out = OpenOut { - fh: handle.map(Into::into).unwrap_or(0), - open_flags: opts.bits(), - ..Default::default() - }; - - // Kind of a hack to write both structs. - reply_ok( - Some(entry_out), - Some(open_out.as_slice()), - in_header.unique, - w, - ) - } - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn interrupt(&self, _in_header: InHeader) -> Result { - Ok(0) - } - - fn bmap(&self, in_header: InHeader, mut _r: Reader, w: Writer) -> Result { - if let Err(e) = self.fs.bmap() { - reply_error(e, in_header.unique, w) - } else { - Ok(0) - } - } - - fn destroy(&self) -> Result { - // No reply to this function. - self.fs.destroy(); - - Ok(0) - } - - fn ioctl( - &self, - in_header: InHeader, - mut r: Reader, - w: Writer, - exit_code: &Arc, - ) -> Result { - let IoctlIn { - fh, - flags, - cmd, - arg, - in_size, - out_size, - } = r.read_obj().map_err(Error::DecodeMessage)?; - - match self.fs.ioctl( - Context::from(in_header), - in_header.nodeid.into(), - fh.into(), - flags, - cmd, - arg, - in_size, - out_size, - exit_code, - ) { - Ok(data) => { - let out = IoctlOut { - result: 0, - ..Default::default() - }; - reply_ok(Some(out), Some(&data), in_header.unique, w) - } - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn poll(&self, in_header: InHeader, mut _r: Reader, w: Writer) -> Result { - if let Err(e) = self.fs.poll() { - reply_error(e, in_header.unique, w) - } else { - Ok(0) - } - } - - fn notify_reply(&self, in_header: InHeader, mut _r: Reader, w: Writer) -> Result { - if let Err(e) = self.fs.notify_reply() { - reply_error(e, in_header.unique, w) - } else { - Ok(0) - } - } - - fn batch_forget(&self, in_header: InHeader, mut r: Reader, w: Writer) -> Result { - let BatchForgetIn { count, .. } = r.read_obj().map_err(Error::DecodeMessage)?; - - if let Some(size) = (count as usize).checked_mul(size_of::()) { - if size > MAX_BUFFER_SIZE as usize { - return reply_error( - linux_error(io::Error::from_raw_os_error(libc::ENOMEM)), - in_header.unique, - w, - ); - } - } else { - return reply_error( - linux_error(io::Error::from_raw_os_error(libc::EOVERFLOW)), - in_header.unique, - w, - ); - } - - let mut requests = Vec::with_capacity(count as usize); - for _ in 0..count { - requests.push( - r.read_obj::() - .map(|f| (f.nodeid.into(), f.nlookup)) - .map_err(Error::DecodeMessage)?, - ); - } - - self.fs.batch_forget(Context::from(in_header), requests); - - // No reply for forget messages. - Ok(0) - } - - fn fallocate(&self, in_header: InHeader, mut r: Reader, w: Writer) -> Result { - let FallocateIn { - fh, - offset, - length, - mode, - .. - } = r.read_obj().map_err(Error::DecodeMessage)?; - - match self.fs.fallocate( - Context::from(in_header), - in_header.nodeid.into(), - fh.into(), - mode, - offset, - length, - ) { - Ok(()) => reply_ok(None::, None, in_header.unique, w), - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn lseek(&self, in_header: InHeader, mut r: Reader, w: Writer) -> Result { - let LseekIn { - fh, offset, whence, .. - } = r.read_obj().map_err(Error::DecodeMessage)?; - - match self.fs.lseek( - Context::from(in_header), - in_header.nodeid.into(), - fh.into(), - offset, - whence, - ) { - Ok(offset) => { - let out = LseekOut { offset }; - - reply_ok(Some(out), None, in_header.unique, w) - } - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn copyfilerange(&self, in_header: InHeader, mut r: Reader, w: Writer) -> Result { - let CopyfilerangeIn { - fh_in, - off_in, - nodeid_out, - fh_out, - off_out, - len, - flags, - .. - } = r.read_obj().map_err(Error::DecodeMessage)?; - - match self.fs.copyfilerange( - Context::from(in_header), - in_header.nodeid.into(), - fh_in.into(), - off_in, - nodeid_out.into(), - fh_out.into(), - off_out, - len, - flags, - ) { - Ok(count) => { - let out = WriteOut { - size: count as u32, - ..Default::default() - }; - - reply_ok(Some(out), None, in_header.unique, w) - } - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn setupmapping( - &self, - in_header: InHeader, - mut r: Reader, - w: Writer, - host_shm_base: u64, - shm_size: u64, - #[cfg(target_os = "macos")] map_sender: &Option>, - ) -> Result { - let SetupmappingIn { - fh, - foffset, - len, - flags, - moffset, - } = r.read_obj().map_err(Error::DecodeMessage)?; - - match self.fs.setupmapping( - Context::from(in_header), - in_header.nodeid.into(), - fh.into(), - foffset, - len, - flags, - moffset, - host_shm_base, - shm_size, - #[cfg(target_os = "macos")] - map_sender, - ) { - Ok(()) => reply_ok(None::, None, in_header.unique, w), - Err(e) => reply_error(e, in_header.unique, w), - } - } - - fn removemapping( - &self, - in_header: InHeader, - mut r: Reader, - w: Writer, - host_shm_base: u64, - shm_size: u64, - #[cfg(target_os = "macos")] map_sender: &Option>, - ) -> Result { - let RemovemappingIn { count } = r.read_obj().map_err(Error::DecodeMessage)?; - - if let Some(size) = (count as usize).checked_mul(size_of::()) { - if size > MAX_BUFFER_SIZE as usize { - return reply_error( - linux_error(io::Error::from_raw_os_error(libc::ENOMEM)), - in_header.unique, - w, - ); - } - } else { - return reply_error( - linux_error(io::Error::from_raw_os_error(libc::EOVERFLOW)), - in_header.unique, - w, - ); - } - - let mut requests = Vec::with_capacity(count as usize); - for _ in 0..count { - requests.push( - r.read_obj::() - .map_err(Error::DecodeMessage)?, - ); - } - - match self.fs.removemapping( - Context::from(in_header), - requests, - host_shm_base, - shm_size, - #[cfg(target_os = "macos")] - map_sender, - ) { - Ok(()) => reply_ok(None::, None, in_header.unique, w), - Err(e) => reply_error(e, in_header.unique, w), - } - } -} - -fn reply_ok( - out: Option, - data: Option<&[u8]>, - unique: u64, - mut w: Writer, -) -> Result { - let mut len = size_of::(); - - if out.is_some() { - len += size_of::(); - } - - if let Some(data) = data { - len += data.len(); - } - - let header = OutHeader { - len: len as u32, - error: 0, - unique, - }; - - w.write_all(header.as_slice()) - .map_err(Error::EncodeMessage)?; - - if let Some(out) = out { - w.write_all(out.as_slice()).map_err(Error::EncodeMessage)?; - } - - if let Some(data) = data { - w.write_all(data).map_err(Error::EncodeMessage)?; - } - - debug_assert_eq!(len, w.bytes_written()); - Ok(w.bytes_written()) -} - -fn reply_error(e: io::Error, unique: u64, mut w: Writer) -> Result { - let header = OutHeader { - len: size_of::() as u32, - error: -e.raw_os_error().unwrap_or(libc::EIO), - unique, - }; - - w.write_all(header.as_slice()) - .map_err(Error::EncodeMessage)?; - - debug_assert_eq!(header.len as usize, w.bytes_written()); - Ok(w.bytes_written()) -} - -fn bytes_to_cstr(buf: &[u8]) -> Result<&CStr> { - // Convert to a `CStr` first so that we can drop the '\0' byte at the end - // and make sure there are no interior '\0' bytes. - CStr::from_bytes_with_nul(buf).map_err(Error::InvalidCString) -} - -fn add_dirent( - cursor: &mut Writer, - max: u32, - d: DirEntry, - entry: Option, -) -> io::Result { - if d.name.len() > u32::MAX as usize { - return Err(linux_error(io::Error::from_raw_os_error(libc::EOVERFLOW))); - } - - let dirent_len = size_of::() - .checked_add(d.name.len()) - .ok_or_else(|| linux_error(io::Error::from_raw_os_error(libc::EOVERFLOW)))?; - - // Directory entries must be padded to 8-byte alignment. If adding 7 causes - // an overflow then this dirent cannot be properly padded. - let padded_dirent_len = dirent_len - .checked_add(7) - .map(|l| l & !7) - .ok_or_else(|| linux_error(io::Error::from_raw_os_error(libc::EOVERFLOW)))?; - - let total_len = if entry.is_some() { - padded_dirent_len - .checked_add(size_of::()) - .ok_or_else(|| linux_error(io::Error::from_raw_os_error(libc::EOVERFLOW)))? - } else { - padded_dirent_len - }; - - if (max as usize).saturating_sub(cursor.bytes_written()) < total_len { - Ok(0) - } else { - if let Some(entry) = entry { - cursor.write_all(EntryOut::from(entry).as_slice())?; - } - - let dirent = Dirent { - ino: d.ino, - off: d.offset, - namelen: d.name.len() as u32, - type_: d.type_, - }; - - cursor.write_all(dirent.as_slice())?; - cursor.write_all(d.name)?; - - // We know that `dirent_len` <= `padded_dirent_len` due to the check above - // so there's no need for checked arithmetic. - let padding = padded_dirent_len - dirent_len; - if padding > 0 { - cursor.write_all(&DIRENT_PADDING[..padding])?; - } - - Ok(total_len) - } -} - -fn take_object(data: &[u8]) -> Result<(T, &[u8])> { - if data.len() < size_of::() { - return Err(Error::DecodeMessage(einval())); - } - - let (object_bytes, remaining_bytes) = data.split_at(size_of::()); - // SAFETY: `T` implements `ByteValued` that guarantees that it is safe to instantiate - // `T` with random data. - let object: T = unsafe { std::ptr::read_unaligned(object_bytes.as_ptr() as *const T) }; - Ok((object, remaining_bytes)) -} - -fn parse_security_context(nr_secctx: u32, data: &[u8]) -> Result> { - // Although the FUSE security context extension allows sending several security contexts, - // currently the guest kernel only sends one. - if nr_secctx > 1 { - return Err(Error::DecodeMessage(einval())); - } else if nr_secctx == 0 { - // No security context sent. May be no LSM supports it. - return Ok(None); - } - - let (secctx, data) = take_object::(data)?; - - if secctx.size == 0 { - return Err(Error::DecodeMessage(einval())); - } - - let mut components = data.split_inclusive(|c| *c == b'\0'); - let secctx_name = components.next().ok_or(Error::MissingParameter)?; - let (_, data) = data.split_at(secctx_name.len()); - - if data.len() < secctx.size as usize { - return Err(Error::DecodeMessage(einval())); - } - - // Fuse client aligns the whole security context block to 64 byte - // boundary. So it is possible that after actual security context - // of secctx.size, there are some null padding bytes left. If - // we ever parse more data after secctx, we will have to take those - // null bytes into account. Total size (including null bytes) is - // available in SecctxHeader->size. - let (remaining, _) = data.split_at(secctx.size as usize); - - let fuse_secctx = SecContext { - name: CString::from_vec_with_nul(secctx_name.to_vec()).map_err(Error::InvalidCString2)?, - secctx: remaining.to_vec(), - }; - - Ok(Some(fuse_secctx)) -} - -fn get_extensions(options: FsOptions, skip: usize, request_bytes: &[u8]) -> Result { - let mut extensions = Extensions::default(); - - if !(options.contains(FsOptions::SECURITY_CTX) - || options.contains(FsOptions::CREATE_SUPP_GROUP)) - { - return Ok(extensions); - } - - // It's not guaranty to receive an extension even if it's supported by the guest kernel - if request_bytes.len() < skip { - return Err(Error::DecodeMessage(einval())); - } - - // We need to track if a SecCtx was received, because it's valid - // for the guest to send an empty SecCtx (i.e, nr_secctx == 0) - let mut secctx_received = false; - - let mut buf = &request_bytes[skip..]; - while !buf.is_empty() { - let (extension_header, remaining_bytes) = take_object::(buf)?; - - let extension_size = (extension_header.size as usize) - .checked_sub(size_of::()) - .ok_or(Error::InvalidHeaderLength)?; - - let (current_extension_bytes, next_extension_bytes) = - remaining_bytes.split_at(extension_size); - - let ext_type = ExtType::try_from(extension_header.ext_type) - .map_err(|_| Error::DecodeMessage(einval()))?; - - match ext_type { - ExtType::SecCtx(nr_secctx) => { - if !options.contains(FsOptions::SECURITY_CTX) || secctx_received { - return Err(Error::DecodeMessage(einval())); - } - - secctx_received = true; - extensions.secctx = parse_security_context(nr_secctx, current_extension_bytes)?; - } - ExtType::SupGroups => { - // We're not exposing this feature to the guest, so we shouldn't get - // any messages including this extension. - unimplemented!("Support for supplemental groups is not implemented"); - } - } - - // Let's process the next extension - buf = next_extension_bytes; - } - - // The SupGroup extension can be missing, since it is only sent if needed. - // A SecCtx is always sent in create/synlink/mknod/mkdir if supported. - if options.contains(FsOptions::SECURITY_CTX) && !secctx_received { - return Err(Error::MissingExtension); - } - - Ok(extensions) -} diff --git a/vendor/krun-devices/src/virtio/fs/virtual_entry.rs b/vendor/krun-devices/src/virtio/fs/virtual_entry.rs deleted file mode 100644 index 06f6915b3..000000000 --- a/vendor/krun-devices/src/virtio/fs/virtual_entry.rs +++ /dev/null @@ -1,56 +0,0 @@ -// Virtual entry types for the virtiofs overlay. - -use std::ffi::CString; - -/// Block size reported by virtual entries in st_blksize. -pub const VIRTUAL_BLKSIZE: i64 = 4096; - -/// A synthetic filesystem entry that exists only in memory. -#[derive(Clone, Debug)] -pub struct VirtualEntry { - /// Permission bits. File type bits (S_IFMT) are ignored — the type - /// is derived from the `content` variant. - pub mode: u32, - /// If true, the entry can only be looked up once. - pub one_shot: bool, - pub content: VirtualEntryContent, -} - -#[derive(Clone, Debug)] -pub enum VirtualEntryContent { - /// A read-only file backed by a static byte slice. - File { data: &'static [u8] }, - /// A directory containing other virtual entries. - Dir { children: Vec }, -} - -impl VirtualEntry { - pub fn is_dir(&self) -> bool { - matches!(self.content, VirtualEntryContent::Dir { .. }) - } - - /// Returns the full st_mode: file type bits from the variant OR'd - /// with the permission bits from self.mode. - #[allow(clippy::unnecessary_cast)] // libc::S_IF* is u16 on macOS, u32 on Linux - pub fn st_mode(&self) -> u32 { - let file_type = match self.content { - VirtualEntryContent::File { .. } => libc::S_IFREG as u32, - VirtualEntryContent::Dir { .. } => libc::S_IFDIR as u32, - }; - file_type | (self.mode & !(libc::S_IFMT as u32)) - } - - pub fn data(&self) -> Option<&'static [u8]> { - match &self.content { - VirtualEntryContent::File { data } => Some(data), - VirtualEntryContent::Dir { .. } => None, - } - } -} - -/// A named entry in a virtual directory. -#[derive(Clone, Debug)] -pub struct VirtualDirEntry { - pub name: CString, - pub entry: VirtualEntry, -} diff --git a/vendor/krun-devices/src/virtio/fs/worker.rs b/vendor/krun-devices/src/virtio/fs/worker.rs deleted file mode 100644 index c264d0036..000000000 --- a/vendor/krun-devices/src/virtio/fs/worker.rs +++ /dev/null @@ -1,286 +0,0 @@ -#[cfg(target_os = "macos")] -use crossbeam_channel::Sender; -#[cfg(target_os = "macos")] -use utils::worker_message::WorkerMessage; - -use std::io; -use std::os::fd::AsRawFd; -use std::sync::atomic::AtomicI32; -use std::sync::Arc; -use std::thread; - -use utils::epoll::{ControlOperation, Epoll, EpollEvent, EventSet}; -use utils::eventfd::EventFd; -use vm_memory::GuestMemoryMmap; - -use super::super::{FsError, Queue}; -use super::augment_fs::AugmentFs; -use super::defs::{HPQ_INDEX, REQ_INDEX}; -use super::descriptor_utils::{Reader, Writer}; -use super::inode_alloc::InodeAllocator; -use super::null_fs::NullFs; -use super::overlay::{Config as OverlayConfig, OverlayFs}; -use super::passthrough::{self, PassthroughFs}; -use super::read_only::PassthroughFsRo; -use super::server::Server; -use super::virtual_entry::VirtualDirEntry; -use crate::virtio::{InterruptTransport, VirtioShmRegion}; - -enum FsServer { - ReadWrite(Server>), - ReadOnly(Server>), - Null(Server>), - Overlay(Server>), -} - -impl FsServer { - fn handle_message( - &self, - r: Reader, - w: Writer, - allow_idmap: bool, - shm_region: &Option, - exit_code: &Arc, - #[cfg(target_os = "macos")] map_sender: &Option>, - ) -> super::Result { - match self { - FsServer::ReadWrite(s) => s.handle_message( - r, - w, - allow_idmap, - shm_region, - exit_code, - #[cfg(target_os = "macos")] - map_sender, - ), - FsServer::ReadOnly(s) => s.handle_message( - r, - w, - allow_idmap, - shm_region, - exit_code, - #[cfg(target_os = "macos")] - map_sender, - ), - FsServer::Null(s) => s.handle_message( - r, - w, - allow_idmap, - shm_region, - exit_code, - #[cfg(target_os = "macos")] - map_sender, - ), - FsServer::Overlay(s) => s.handle_message( - r, - w, - allow_idmap, - shm_region, - exit_code, - #[cfg(target_os = "macos")] - map_sender, - ), - } - } -} - -pub struct FsWorker { - queues: Vec, - queue_evts: Vec>, - interrupt: InterruptTransport, - mem: GuestMemoryMmap, - allow_idmap: bool, - shm_region: Option, - server: FsServer, - stop_fd: EventFd, - exit_code: Arc, - #[cfg(target_os = "macos")] - map_sender: Option>, -} - -impl FsWorker { - #[allow(clippy::too_many_arguments)] - pub fn new( - queues: Vec, - queue_evts: Vec>, - interrupt: InterruptTransport, - mem: GuestMemoryMmap, - allow_idmap: bool, - shm_region: Option, - passthrough_cfg: Option, - overlay_cfg: Option, - read_only: bool, - virtual_entries: Vec, - stop_fd: EventFd, - exit_code: Arc, - #[cfg(target_os = "macos")] map_sender: Option>, - ) -> Result { - let inode_alloc = Arc::new(InodeAllocator::new()); - let server = match (overlay_cfg, passthrough_cfg) { - (Some(cfg), _) => { - let inner = OverlayFs::new(cfg, inode_alloc.clone())?; - FsServer::Overlay(Server::new(AugmentFs::new( - inner, - &inode_alloc, - virtual_entries, - ))) - } - (None, Some(cfg)) if read_only => { - let inner = PassthroughFsRo::new(cfg, inode_alloc.clone())?; - FsServer::ReadOnly(Server::new(AugmentFs::new( - inner, - &inode_alloc, - virtual_entries, - ))) - } - (None, Some(cfg)) => { - let inner = PassthroughFs::new(cfg, inode_alloc.clone())?; - FsServer::ReadWrite(Server::new(AugmentFs::new( - inner, - &inode_alloc, - virtual_entries, - ))) - } - (None, None) => FsServer::Null(Server::new(AugmentFs::new( - NullFs, - &inode_alloc, - virtual_entries, - ))), - }; - Ok(Self { - queues, - queue_evts, - interrupt, - mem, - allow_idmap, - shm_region, - server, - stop_fd, - exit_code, - #[cfg(target_os = "macos")] - map_sender, - }) - } - - pub fn run(self) -> thread::JoinHandle<()> { - thread::Builder::new() - .name("fs worker".into()) - .spawn(|| self.work()) - .unwrap() - } - - fn work(mut self) { - let virtq_hpq_ev_fd = self.queue_evts[HPQ_INDEX].as_raw_fd(); - let virtq_req_ev_fd = self.queue_evts[REQ_INDEX].as_raw_fd(); - let stop_ev_fd = self.stop_fd.as_raw_fd(); - - let epoll = Epoll::new().unwrap(); - - let _ = epoll.ctl( - ControlOperation::Add, - virtq_hpq_ev_fd, - &EpollEvent::new(EventSet::IN, virtq_hpq_ev_fd as u64), - ); - let _ = epoll.ctl( - ControlOperation::Add, - virtq_req_ev_fd, - &EpollEvent::new(EventSet::IN, virtq_req_ev_fd as u64), - ); - let _ = epoll.ctl( - ControlOperation::Add, - stop_ev_fd, - &EpollEvent::new(EventSet::IN, stop_ev_fd as u64), - ); - - loop { - let mut epoll_events = vec![EpollEvent::new(EventSet::empty(), 0); 32]; - match epoll.wait(epoll_events.len(), -1, epoll_events.as_mut_slice()) { - Ok(ev_cnt) => { - for event in &epoll_events[0..ev_cnt] { - let source = event.fd(); - let event_set = event.event_set(); - match event_set { - EventSet::IN if source == virtq_hpq_ev_fd => { - self.handle_event(HPQ_INDEX); - } - EventSet::IN if source == virtq_req_ev_fd => { - self.handle_event(REQ_INDEX); - } - EventSet::IN if source == stop_ev_fd => { - debug!("stopping worker thread"); - let _ = self.stop_fd.read(); - return; - } - _ => { - log::warn!( - "Received unknown event: {event_set:?} from fd: {source:?}" - ); - } - } - } - } - Err(e) => { - debug!("failed to consume muxer epoll event: {e}"); - } - } - } - } - - fn handle_event(&mut self, queue_index: usize) { - debug!("Fs: queue event: {queue_index}"); - if let Err(e) = self.queue_evts[queue_index].read() { - error!("Failed to get queue event: {e:?}"); - } - - loop { - self.queues[queue_index] - .disable_notification(&self.mem) - .unwrap(); - - self.process_queue(queue_index); - - if !self.queues[queue_index] - .enable_notification(&self.mem) - .unwrap() - { - break; - } - } - } - - fn process_queue(&mut self, queue_index: usize) { - let queue = &mut self.queues[queue_index]; - while let Some(head) = queue.pop(&self.mem) { - let reader = Reader::new(&self.mem, head.clone()) - .map_err(FsError::QueueReader) - .unwrap(); - let writer = Writer::new(&self.mem, head.clone()) - .map_err(FsError::QueueWriter) - .unwrap(); - - let len = match self.server.handle_message( - reader, - writer, - self.allow_idmap, - &self.shm_region, - &self.exit_code, - #[cfg(target_os = "macos")] - &self.map_sender, - ) { - Ok(len) => len, - Err(e) => { - error!("error handling message: {e:?}"); - 0 - } - }; - - if let Err(e) = queue.add_used(&self.mem, head.index, len as u32) { - error!("failed to add used elements to the queue: {e:?}"); - } - - if queue.needs_notification(&self.mem).unwrap() { - self.interrupt.signal_used_queue(); - } - } - } -} diff --git a/vendor/krun-devices/src/virtio/gpu/device.rs b/vendor/krun-devices/src/virtio/gpu/device.rs deleted file mode 100644 index 04f619c27..000000000 --- a/vendor/krun-devices/src/virtio/gpu/device.rs +++ /dev/null @@ -1,237 +0,0 @@ -use std::io::Write; - -#[cfg(target_os = "macos")] -use crossbeam_channel::Sender; -use vm_memory::{ByteValued, GuestMemoryMmap}; - -use super::super::{ - fs::ExportTable, ActivateError, ActivateResult, DeviceQueue, DeviceState, QueueConfig, - VirtioDevice, VirtioShmRegion, -}; -use super::defs; -use super::defs::uapi; -use super::defs::uapi::virtio_gpu_config; -use super::worker::Worker; -use crate::virtio::display::DisplayInfo; -use crate::virtio::InterruptTransport; -use krun_display::DisplayBackend; -#[cfg(target_os = "macos")] -use utils::worker_message::WorkerMessage; - -// Supported features. -pub(crate) const AVAIL_FEATURES: u64 = (1u64 << uapi::VIRTIO_F_VERSION_1) - | (1u64 << uapi::VIRTIO_GPU_F_VIRGL) - | (1u64 << uapi::VIRTIO_GPU_F_EDID) - | (1u64 << uapi::VIRTIO_GPU_F_RESOURCE_UUID) - | (1u64 << uapi::VIRTIO_GPU_F_RESOURCE_BLOB) - | (1u64 << uapi::VIRTIO_GPU_F_CONTEXT_INIT); - -const QUEUE_SIZE: u16 = 256; -static QUEUE_CONFIG: [QueueConfig; defs::NUM_QUEUES] = - [QueueConfig::new(QUEUE_SIZE); defs::NUM_QUEUES]; - -pub struct Gpu { - pub(crate) avail_features: u64, - pub(crate) acked_features: u64, - pub(crate) device_state: DeviceState, - shm_region: Option, - virgl_flags: u32, - #[cfg(target_os = "macos")] - map_sender: Sender, - export_table: Option, - displays: Box<[DisplayInfo]>, - display_backend: DisplayBackend<'static>, -} - -impl Gpu { - pub fn new( - virgl_flags: u32, - displays: Box<[DisplayInfo]>, - display_backend: DisplayBackend<'static>, - #[cfg(target_os = "macos")] map_sender: Sender, - ) -> super::Result { - Ok(Gpu { - avail_features: AVAIL_FEATURES, - acked_features: 0, - device_state: DeviceState::Inactive, - shm_region: None, - virgl_flags, - #[cfg(target_os = "macos")] - map_sender, - export_table: None, - displays, - display_backend, - }) - } - - pub fn id(&self) -> &str { - defs::GPU_DEV_ID - } - - pub fn set_shm_region(&mut self, shm_region: VirtioShmRegion) { - debug!("virtio_gpu: set_shm_region"); - self.shm_region = Some(shm_region); - } - - pub fn set_export_table(&mut self, export_table: ExportTable) { - self.export_table = Some(export_table); - } - - /* - pub fn process_ctl(&mut self) -> bool { - debug!("gpu: process_ctl()"); - let mem = match self.device_state { - DeviceState::Activated(ref mem) => mem, - // This should never happen, it's been already validated in the event handler. - DeviceState::Inactive => unreachable!(), - }; - - let mut have_used = false; - - //while let Some(head) = self.queues[CTL_INDEX].pop(mem) { - if let Some(head) = self.queues[CTL_INDEX].pop(mem) { - let index = head.index; - let mut written = 0; - for desc in head.into_iter() { - error!("gpu: process_ctl() unimplemented"); - self.queues[CTL_INDEX].go_to_previous_position(); - break; - } - - have_used = true; - self.queues[CTL_INDEX].add_used(mem, index, written); - } - - have_used - } - - pub fn process_cur(&mut self) -> bool { - debug!("gpu: process_cur()"); - let mem = match self.device_state { - DeviceState::Activated(ref mem) => mem, - // This should never happen, it's been already validated in the event handler. - DeviceState::Inactive => unreachable!(), - }; - - let mut have_used = false; - - while let Some(head) = self.queues[CTL_INDEX].pop(mem) { - let index = head.index; - let mut written = 0; - for desc in head.into_iter() { - error!("gpu: process_cur() unimplemented"); - self.queues[CTL_INDEX].go_to_previous_position(); - break; - } - - have_used = true; - self.queues[CTL_INDEX].add_used(mem, index, written); - } - - have_used - } - */ -} - -impl VirtioDevice for Gpu { - fn avail_features(&self) -> u64 { - self.avail_features - } - - fn acked_features(&self) -> u64 { - self.acked_features - } - - fn set_acked_features(&mut self, acked_features: u64) { - self.acked_features = acked_features - } - - fn device_type(&self) -> u32 { - uapi::VIRTIO_ID_GPU - } - - fn device_name(&self) -> &str { - "gpu" - } - - fn queue_config(&self) -> &[QueueConfig] { - &QUEUE_CONFIG - } - - fn read_config(&self, offset: u64, mut data: &mut [u8]) { - let config = virtio_gpu_config { - events_read: 0, - events_clear: 0, - num_scanouts: self.displays.len() as u32, - num_capsets: 5, - }; - - let config_slice = config.as_slice(); - let config_len = config_slice.len() as u64; - if offset >= config_len { - error!("Failed to read config space"); - return; - } - if let Some(end) = offset.checked_add(data.len() as u64) { - // This write can't fail, offset and end are checked against config_len. - data.write_all(&config_slice[offset as usize..std::cmp::min(end, config_len) as usize]) - .unwrap(); - } - } - - fn write_config(&mut self, offset: u64, data: &[u8]) { - warn!( - "gpu: guest driver attempted to write device config (offset={:x}, len={:x})", - offset, - data.len() - ); - } - - fn activate( - &mut self, - mem: GuestMemoryMmap, - interrupt: InterruptTransport, - queues: Vec, - ) -> ActivateResult { - let [control_q, _cursor_q]: [_; defs::NUM_QUEUES] = queues.try_into().map_err(|_| { - error!( - "Cannot perform activate. Expected {} queue(s)", - defs::NUM_QUEUES - ); - ActivateError::BadActivate - })?; - - let shm_region = match self.shm_region.as_ref() { - Some(s) => s.clone(), - None => panic!("virtio_gpu: missing SHM region"), - }; - - // cursor queue not used by worker - let worker = Worker::new( - control_q, - mem.clone(), - interrupt.clone(), - shm_region, - self.virgl_flags, - #[cfg(target_os = "macos")] - self.map_sender.clone(), - self.export_table.take(), - self.displays.clone(), - self.display_backend, - ); - worker.run(); - - self.device_state = DeviceState::Activated(mem, interrupt); - - Ok(()) - } - - fn is_activated(&self) -> bool { - self.device_state.is_activated() - } - - fn shm_region(&self) -> Option<&VirtioShmRegion> { - debug!("virtio_gpu: GET_shm_region"); - self.shm_region.as_ref() - } -} diff --git a/vendor/krun-devices/src/virtio/gpu/display.rs b/vendor/krun-devices/src/virtio/gpu/display.rs deleted file mode 100644 index 5bc6b17da..000000000 --- a/vendor/krun-devices/src/virtio/gpu/display.rs +++ /dev/null @@ -1,102 +0,0 @@ -use super::edid::EdidInfo; -use krun_display::{ - DisplayBackendBasicFramebuffer, DisplayBackendError, DisplayBackendNew, Rect, ResourceFormat, -}; -use virtio_bindings::virtio_gpu::VIRTIO_GPU_MAX_SCANOUTS; - -#[derive(Clone, Debug)] -pub struct DisplayInfo { - pub width: u32, - pub height: u32, - pub edid: DisplayInfoEdid, -} - -impl DisplayInfo { - pub fn edid_bytes(&self) -> Box<[u8]> { - match &self.edid { - DisplayInfoEdid::Provided(edid_bytes) => edid_bytes.clone(), - DisplayInfoEdid::Generated(edid_params) => { - let edid_info = EdidInfo::new(self.width, self.height, edid_params); - edid_info.bytes() - } - } - } -} - -#[derive(Debug, Clone)] -pub enum DisplayInfoEdid { - Generated(EdidParams), - Provided(Box<[u8]>), -} - -#[derive(Debug, Clone, Copy)] -pub struct EdidParams { - pub refresh_rate: u32, - pub physical_size: PhysicalSize, -} - -impl Default for EdidParams { - fn default() -> Self { - EdidParams { - refresh_rate: 60, - physical_size: PhysicalSize::Dpi(300), - } - } -} - -#[derive(Debug, Copy, Clone)] -pub enum PhysicalSize { - Dpi(u32), - DimensionsMillimeters(u16, u16), -} - -impl DisplayInfo { - pub fn new(width: u32, height: u32) -> Self { - Self { - width, - height, - edid: DisplayInfoEdid::Generated(EdidParams::default()), - } - } -} - -pub const MAX_DISPLAYS: usize = VIRTIO_GPU_MAX_SCANOUTS as usize; - -pub struct NoopDisplayBackend; - -impl DisplayBackendNew<()> for NoopDisplayBackend { - fn new(_userdata: Option<&()>) -> Self { - Self - } -} - -impl DisplayBackendBasicFramebuffer for NoopDisplayBackend { - fn configure_scanout( - &mut self, - _scanout_id: u32, - _display_width: u32, - _display_height: u32, - _width: u32, - _height: u32, - _format: ResourceFormat, - ) -> Result<(), DisplayBackendError> { - Err(DisplayBackendError::InvalidScanoutId) - } - - fn disable_scanout(&mut self, _scanout_id: u32) -> Result<(), DisplayBackendError> { - Err(DisplayBackendError::InvalidScanoutId) - } - - fn alloc_frame(&mut self, _scanout_id: u32) -> Result<(u32, &mut [u8]), DisplayBackendError> { - Err(DisplayBackendError::InvalidScanoutId) - } - - fn present_frame( - &mut self, - _scanout_id: u32, - _frame_id: u32, - _rect: Option<&Rect>, - ) -> Result<(), DisplayBackendError> { - Err(DisplayBackendError::InvalidScanoutId) - } -} diff --git a/vendor/krun-devices/src/virtio/gpu/edid.rs b/vendor/krun-devices/src/virtio/gpu/edid.rs deleted file mode 100644 index 9a26b26e6..000000000 --- a/vendor/krun-devices/src/virtio/gpu/edid.rs +++ /dev/null @@ -1,316 +0,0 @@ -// Copyright 2022 The ChromiumOS Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -//! Implementation of the EDID specification provided by software. -//! EDID spec: - -//! This module is used to create the Extended Display Identification Data (EDID), which will be -//! exposed to the guest system. -//! -//! We ignore most of the spec, the point here being for us to provide enough for graphics to work -//! and to allow us to configure the resolution and refresh rate (via the preferred timing mode -//! pixel clock). -//! -//! The EDID spec defines a number of methods to provide mode information, but in priority order the -//! "detailed" timing information is first, so we provide a single block of detailed timing -//! information and no other form of timing information. -//! -use super::display::{EdidParams, PhysicalSize}; - -const EDID_DATA_LENGTH: usize = 128; -const DEFAULT_HORIZONTAL_BLANKING: u16 = 560; -const DEFAULT_VERTICAL_BLANKING: u16 = 50; -const DEFAULT_HORIZONTAL_FRONT_PORCH: u16 = 64; -const DEFAULT_VERTICAL_FRONT_PORCH: u16 = 1; -const DEFAULT_HORIZONTAL_SYNC_PULSE: u16 = 192; -const DEFAULT_VERTICAL_SYNC_PULSE: u16 = 3; -const MILLIMETERS_PER_INCH: f32 = 25.4; - -#[derive(Copy, Clone)] -pub struct EdidInfo { - width: u32, - height: u32, - refresh_rate: u32, - horizontal_blanking: u16, - vertical_blanking: u16, - horizontal_front: u16, - vertical_front: u16, - horizontal_sync: u16, - vertical_sync: u16, - width_millimeters: u16, - height_millimeters: u16, -} - -impl EdidInfo { - /// Only width, height and refresh rate are required for the graphics stack to work, so instead - /// of pulling actual numbers from the system, we just use some typical values to populate other - /// fields for now. - pub fn new(width: u32, height: u32, params: &EdidParams) -> Self { - let (width_millimeters, height_millimeters) = match params.physical_size { - PhysicalSize::Dpi(dpi) => ( - ((width as f32 / dpi as f32) * MILLIMETERS_PER_INCH) as u16, - ((height as f32 / dpi as f32) * MILLIMETERS_PER_INCH) as u16, - ), - PhysicalSize::DimensionsMillimeters(width, height) => (width, height), - }; - - Self { - width, - height, - refresh_rate: params.refresh_rate, - horizontal_blanking: DEFAULT_HORIZONTAL_BLANKING, - vertical_blanking: DEFAULT_VERTICAL_BLANKING, - horizontal_front: DEFAULT_HORIZONTAL_FRONT_PORCH, - vertical_front: DEFAULT_VERTICAL_FRONT_PORCH, - horizontal_sync: DEFAULT_HORIZONTAL_SYNC_PULSE, - vertical_sync: DEFAULT_VERTICAL_SYNC_PULSE, - width_millimeters, - height_millimeters, - } - } - - pub fn width_centimeters(&self) -> u8 { - (self.width_millimeters / 10) as u8 - } - - pub fn height_centimeters(&self) -> u8 { - (self.height_millimeters / 10) as u8 - } - - pub fn bytes(self) -> Box<[u8]> { - let mut edid_box: Box<[u8]> = vec![0; EDID_DATA_LENGTH].into_boxed_slice(); - let edid = &mut edid_box[..]; - - populate_header(edid); - populate_edid_version(edid); - populate_size(edid, &self); - populate_standard_timings(edid); - - // 4 available descriptor blocks - let block0 = &mut edid[54..72]; - populate_detailed_timing(block0, &self); - - let block1 = &mut edid[72..90]; - populate_display_name(block1); - - calculate_checksum(edid); - - edid_box - } -} - -fn populate_display_name(edid_block: &mut [u8]) { - // Display Product Name String Descriptor Tag - edid_block[0..5].clone_from_slice(&[0x00, 0x00, 0x00, 0xFC, 0x00]); - // This should to be padded to 13 bytes, see Section 3.10.3.4 - let product_name: &[u8; 13] = b"krun-display\n"; - edid_block[5..].clone_from_slice(product_name); -} - -fn populate_detailed_timing(edid_block: &mut [u8], info: &EdidInfo) { - assert_eq!(edid_block.len(), 18); - - // Detailed timings - // - // 18 Byte Descriptors - 72 Bytes - // The 72 bytes in this section are divided into four data fields. Each of the four data fields - // are 18 bytes in length. These 18 byte data fields shall contain either detailed timing data - // as described in Section 3.10.2 or other types of data as described in Section 3.10.3. The - // addresses and the contents of the four 18 byte descriptors are shown in Table 3.20. - // - // We leave the bottom 6 bytes of this block purposefully empty. - let horizontal_blanking_lsb: u8 = (info.horizontal_blanking & 0xFF) as u8; - let horizontal_blanking_msb: u8 = ((info.horizontal_blanking >> 8) & 0x0F) as u8; - - let vertical_blanking_lsb: u8 = (info.vertical_blanking & 0xFF) as u8; - let vertical_blanking_msb: u8 = ((info.vertical_blanking >> 8) & 0x0F) as u8; - - // The pixel clock is what controls the refresh timing information. - // - // The formula for getting refresh rate out of this value is: - // refresh_rate = clk * 10000 / (htotal * vtotal) - // Solving for clk: - // clk = (refresh_rate * htotal * votal) / 10000 - // - // where: - // clk - The setting here - // vtotal - Total lines - // htotal - Total pixels per line - // - // Value here is pixel clock + 10,000, in 10khz steps. - // - // Pseudocode of kernel logic for vrefresh: - // vtotal := mode->vtotal; - // calc_val := (clock * 1000) / htotal - // refresh := (calc_val + vtotal / 2) / vtotal - // if flags & INTERLACE: refresh *= 2 - // if flags & DBLSCAN: refresh /= 2 - // if vscan > 1: refresh /= vscan - // - let htotal = info.width + (info.horizontal_blanking as u32); - let vtotal = info.height + (info.vertical_blanking as u32); - let mut clock: u16 = ((info.refresh_rate * htotal * vtotal) / 10000) as u16; - // Round to nearest 10khz. - clock = ((clock + 5) / 10) * 10; - edid_block[0..2].copy_from_slice(&clock.to_le_bytes()); - - let width_lsb: u8 = (info.width & 0xFF) as u8; - let width_msb: u8 = ((info.width >> 8) & 0x0F) as u8; - - // Horizointal Addressable Video in pixels. - edid_block[2] = width_lsb; - // Horizontal blanking in pixels. - edid_block[3] = horizontal_blanking_lsb; - // Upper bits of the two above vals. - edid_block[4] = horizontal_blanking_msb | (width_msb << 4); - - let vertical_active: u32 = info.height; - let vertical_active_lsb: u8 = (vertical_active & 0xFF) as u8; - let vertical_active_msb: u8 = ((vertical_active >> 8) & 0x0F) as u8; - - // Vertical addressable video in *lines* - edid_block[5] = vertical_active_lsb; - // Vertical blanking in lines - edid_block[6] = vertical_blanking_lsb; - // Sigbits of the above. - edid_block[7] = vertical_blanking_msb | (vertical_active_msb << 4); - - let horizontal_front_lsb: u8 = (info.horizontal_front & 0xFF) as u8; // least sig 8 bits - let horizontal_front_msb: u8 = ((info.horizontal_front >> 8) & 0x03) as u8; // most sig 2 bits - let horizontal_sync_lsb: u8 = (info.horizontal_sync & 0xFF) as u8; // least sig 8 bits - let horizontal_sync_msb: u8 = ((info.horizontal_sync >> 8) & 0x03) as u8; // most sig 2 bits - - let vertical_front_lsb: u8 = (info.vertical_front & 0x0F) as u8; // least sig 4 bits - let vertical_front_msb: u8 = ((info.vertical_front >> 8) & 0x0F) as u8; // most sig 2 bits - let vertical_sync_lsb: u8 = (info.vertical_sync & 0xFF) as u8; // least sig 4 bits - let vertical_sync_msb: u8 = ((info.vertical_sync >> 8) & 0x0F) as u8; // most sig 2 bits - - // Horizontal front porch in pixels. - edid_block[8] = horizontal_front_lsb; - // Horizontal sync pulse width in pixels. - edid_block[9] = horizontal_sync_lsb; - // LSB of vertical front porch and sync pulse - edid_block[10] = vertical_sync_lsb | (vertical_front_lsb << 4); - // Upper 2 bits of these values. - edid_block[11] = vertical_sync_msb - | (vertical_front_msb << 2) - | (horizontal_sync_msb << 4) - | (horizontal_front_msb << 6); - - let width_millimeters_lsb: u8 = (info.width_millimeters & 0xFF) as u8; // least sig 8 bits - let width_millimeters_msb: u8 = ((info.width_millimeters >> 8) & 0xF) as u8; // most sig 4 bits - - let height_millimeters_lsb: u8 = (info.height_millimeters & 0xFF) as u8; // least sig 8 bits - let height_millimeters_msb: u8 = ((info.height_millimeters >> 8) & 0xF) as u8; // most sig 4 bits - - edid_block[12] = width_millimeters_lsb; - edid_block[13] = height_millimeters_lsb; - edid_block[14] = height_millimeters_msb | (width_millimeters_msb << 4); -} - -// The EDID header. This is defined by the EDID spec. -fn populate_header(edid: &mut [u8]) { - edid[0] = 0x00; - edid[1] = 0xFF; - edid[2] = 0xFF; - edid[3] = 0xFF; - edid[4] = 0xFF; - edid[5] = 0xFF; - edid[6] = 0xFF; - edid[7] = 0x00; - - // Red Hat 'RHT' is also used in QEMU, though it is not technically officially assigned - let manufacturer_name = b"RHT"; - // 00001 -> A, 00010 -> B, etc - let manufacturer_id: u16 = manufacturer_name - .iter() - .map(|c| (c - b'A' + 1) & 0x1F) - .fold(0u16, |res, lsb| (res << 5) | (lsb as u16)); - edid[8..10].copy_from_slice(&manufacturer_id.to_be_bytes()); - - let manufacture_product_id: u16 = 1; - edid[10..12].copy_from_slice(&manufacture_product_id.to_le_bytes()); - - let serial_id: u32 = 1; - edid[12..16].copy_from_slice(&serial_id.to_le_bytes()); - - let manufacture_week: u8 = 30; - edid[16] = manufacture_week; - - let manufacture_year: u32 = 2025; - edid[17] = (manufacture_year - 1990u32) as u8; -} - -// The standard timings are 8 timing modes with a lower priority (and different data format) -// than the 4 detailed timing modes. -fn populate_standard_timings(edid: &mut [u8]) { - const fn aspect_ratio(width: u32, height: u32) -> (u32, u32) { - let divisor = gcd(width, height); - (width / divisor, height / divisor) - } - - const fn aspect_ratio_bits(width: u32, height: u32) -> u8 { - match aspect_ratio(width, height) { - (8, 5) => 0x0, - (4, 3) => 0x1, - (5, 4) => 0x2, - (16, 9) => 0x3, - _ => panic!("Not a standard aspect ratio"), - } - } - - const fn resolution(width: u32, height: u32) -> (u32, u32, u8) { - (width, height, aspect_ratio_bits(width, height)) - } - - const RESOLUTIONS: [(u32, u32, u8); 8] = [ - resolution(1440, 900), - resolution(1600, 900), - resolution(800, 600), - resolution(1680, 1050), - resolution(1856, 1392), - resolution(1280, 1024), - resolution(1400, 1050), - resolution(1920, 1200), - ]; - - // Index 0 is horizontal pixels / 8 - 31 - // Index 1 is a combination of the refresh_rate - 60 (so we are setting to 0, for now) and two - // bits for the aspect ratio. - for (index, (width, _height, aspect_ratio_bits)) in RESOLUTIONS.into_iter().enumerate() { - edid[0x26 + (index * 2)] = (width / 8 - 31) as u8; - edid[0x27 + (index * 2)] = aspect_ratio_bits; - } -} - -// Per the EDID spec, needs to be 1 and 4. -fn populate_edid_version(edid: &mut [u8]) { - edid[18] = 1; - edid[19] = 4; -} - -fn populate_size(edid: &mut [u8], info: &EdidInfo) { - edid[21] = info.width_centimeters(); - edid[22] = info.height_centimeters(); -} - -fn calculate_checksum(edid: &mut [u8]) { - let mut checksum: u8 = 0; - for byte in edid.iter().take(EDID_DATA_LENGTH - 1) { - checksum = checksum.wrapping_add(*byte); - } - - if checksum != 0 { - checksum = 255 - checksum + 1; - } - - edid[127] = checksum; -} - -const fn gcd(x: u32, y: u32) -> u32 { - match y { - 0 => x, - _ => gcd(y, x % y), - } -} diff --git a/vendor/krun-devices/src/virtio/gpu/mod.rs b/vendor/krun-devices/src/virtio/gpu/mod.rs deleted file mode 100644 index 64f42de8f..000000000 --- a/vendor/krun-devices/src/virtio/gpu/mod.rs +++ /dev/null @@ -1,61 +0,0 @@ -mod device; -pub mod display; -mod edid; -mod protocol; -mod virtio_gpu; -mod worker; - -use super::descriptor_utils::Error as DescriptorError; - -pub use self::defs::uapi::VIRTIO_ID_GPU as TYPE_GPU; -pub use self::device::Gpu; - -mod defs { - pub const GPU_DEV_ID: &str = "virtio_gpu"; - pub const NUM_QUEUES: usize = 2; - - #[allow(dead_code)] - pub mod uapi { - use vm_memory::ByteValued; - - pub const VIRTIO_F_VERSION_1: u32 = 32; - pub const VIRTIO_ID_GPU: u32 = 16; - - pub const VIRTIO_GPU_F_VIRGL: u32 = 0; - pub const VIRTIO_GPU_F_EDID: u32 = 1; - pub const VIRTIO_GPU_F_RESOURCE_UUID: u32 = 2; - pub const VIRTIO_GPU_F_RESOURCE_BLOB: u32 = 3; - pub const VIRTIO_GPU_F_CONTEXT_INIT: u32 = 4; - /* The following capabilities are not upstreamed. */ - pub const VIRTIO_GPU_F_RESOURCE_SYNC: u32 = 5; - pub const VIRTIO_GPU_F_CREATE_GUEST_HANDLE: u32 = 6; - - #[derive(Copy, Clone, Debug, Default)] - #[repr(C)] - pub struct virtio_gpu_config { - pub events_read: u32, - pub events_clear: u32, - pub num_scanouts: u32, - pub num_capsets: u32, - } - unsafe impl ByteValued for virtio_gpu_config {} - } -} - -#[derive(Debug)] -pub enum GpuError { - /// Failed to create event fd. - EventFd(std::io::Error), - /// Failed to decode incoming command. - DecodeCommand(std::io::Error), - /// Error creating Reader for Queue. - QueueReader(DescriptorError), - /// Error creating Writer for Queue. - QueueWriter(DescriptorError), - /// Error writting to the Queue. - WriteDescriptor(std::io::Error), - /// Error reading Guest Memory, - GuestMemory, -} - -type Result = std::result::Result; diff --git a/vendor/krun-devices/src/virtio/gpu/protocol.rs b/vendor/krun-devices/src/virtio/gpu/protocol.rs deleted file mode 100644 index 6c69d4ba3..000000000 --- a/vendor/krun-devices/src/virtio/gpu/protocol.rs +++ /dev/null @@ -1,944 +0,0 @@ -// Copyright 2019 The ChromiumOS Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#![allow(dead_code)] -#![allow(non_camel_case_types)] - -use super::super::descriptor_utils::{Reader, Writer}; -#[cfg(feature = "gpu")] -use krun_display::DisplayBackendError; -use rutabaga_gfx::RutabagaError; -use std::cmp::min; -use std::convert::From; -use std::fmt::Display; -use std::io::Write; -use std::marker::PhantomData; -use std::mem::{size_of, size_of_val}; -use std::str::from_utf8; -use std::{fmt, io}; -use thiserror::Error; -use vm_memory::ByteValued; -use zerocopy::{FromBytes, Immutable, IntoBytes}; - -pub const VIRTIO_GPU_UNDEFINED: u32 = 0x0; - -/* 2d commands */ -pub const VIRTIO_GPU_CMD_GET_DISPLAY_INFO: u32 = 0x100; -pub const VIRTIO_GPU_CMD_RESOURCE_CREATE_2D: u32 = 0x101; -pub const VIRTIO_GPU_CMD_RESOURCE_UNREF: u32 = 0x102; -pub const VIRTIO_GPU_CMD_SET_SCANOUT: u32 = 0x103; -pub const VIRTIO_GPU_CMD_RESOURCE_FLUSH: u32 = 0x104; -pub const VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D: u32 = 0x105; -pub const VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING: u32 = 0x106; -pub const VIRTIO_GPU_CMD_RESOURCE_DETACH_BACKING: u32 = 0x107; -pub const VIRTIO_GPU_CMD_GET_CAPSET_INFO: u32 = 0x108; -pub const VIRTIO_GPU_CMD_GET_CAPSET: u32 = 0x109; -pub const VIRTIO_GPU_CMD_GET_EDID: u32 = 0x10a; -pub const VIRTIO_GPU_CMD_RESOURCE_ASSIGN_UUID: u32 = 0x10b; -pub const VIRTIO_GPU_CMD_RESOURCE_CREATE_BLOB: u32 = 0x10c; -pub const VIRTIO_GPU_CMD_SET_SCANOUT_BLOB: u32 = 0x10d; - -/* 3d commands */ -pub const VIRTIO_GPU_CMD_CTX_CREATE: u32 = 0x200; -pub const VIRTIO_GPU_CMD_CTX_DESTROY: u32 = 0x201; -pub const VIRTIO_GPU_CMD_CTX_ATTACH_RESOURCE: u32 = 0x202; -pub const VIRTIO_GPU_CMD_CTX_DETACH_RESOURCE: u32 = 0x203; -pub const VIRTIO_GPU_CMD_RESOURCE_CREATE_3D: u32 = 0x204; -pub const VIRTIO_GPU_CMD_TRANSFER_TO_HOST_3D: u32 = 0x205; -pub const VIRTIO_GPU_CMD_TRANSFER_FROM_HOST_3D: u32 = 0x206; -pub const VIRTIO_GPU_CMD_SUBMIT_3D: u32 = 0x207; -pub const VIRTIO_GPU_CMD_RESOURCE_MAP_BLOB: u32 = 0x208; -pub const VIRTIO_GPU_CMD_RESOURCE_UNMAP_BLOB: u32 = 0x209; - -/* cursor commands */ -pub const VIRTIO_GPU_CMD_UPDATE_CURSOR: u32 = 0x300; -pub const VIRTIO_GPU_CMD_MOVE_CURSOR: u32 = 0x301; - -/* success responses */ -pub const VIRTIO_GPU_RESP_OK_NODATA: u32 = 0x1100; -pub const VIRTIO_GPU_RESP_OK_DISPLAY_INFO: u32 = 0x1101; -pub const VIRTIO_GPU_RESP_OK_CAPSET_INFO: u32 = 0x1102; -pub const VIRTIO_GPU_RESP_OK_CAPSET: u32 = 0x1103; -pub const VIRTIO_GPU_RESP_OK_EDID: u32 = 0x1104; -pub const VIRTIO_GPU_RESP_OK_RESOURCE_UUID: u32 = 0x1105; -pub const VIRTIO_GPU_RESP_OK_MAP_INFO: u32 = 0x1106; - -/* CHROMIUM(b/277982577): success responses */ -pub const VIRTIO_GPU_RESP_OK_RESOURCE_PLANE_INFO: u32 = 0x11FF; - -/* error responses */ -pub const VIRTIO_GPU_RESP_ERR_UNSPEC: u32 = 0x1200; -pub const VIRTIO_GPU_RESP_ERR_OUT_OF_MEMORY: u32 = 0x1201; -pub const VIRTIO_GPU_RESP_ERR_INVALID_SCANOUT_ID: u32 = 0x1202; -pub const VIRTIO_GPU_RESP_ERR_INVALID_RESOURCE_ID: u32 = 0x1203; -pub const VIRTIO_GPU_RESP_ERR_INVALID_CONTEXT_ID: u32 = 0x1204; -pub const VIRTIO_GPU_RESP_ERR_INVALID_PARAMETER: u32 = 0x1205; - -pub const VIRTIO_GPU_BLOB_MEM_GUEST: u32 = 0x0001; -pub const VIRTIO_GPU_BLOB_MEM_HOST3D: u32 = 0x0002; -pub const VIRTIO_GPU_BLOB_MEM_HOST3D_GUEST: u32 = 0x0003; - -pub const VIRTIO_GPU_BLOB_FLAG_USE_MAPPABLE: u32 = 0x0001; -pub const VIRTIO_GPU_BLOB_FLAG_USE_SHAREABLE: u32 = 0x0002; -pub const VIRTIO_GPU_BLOB_FLAG_USE_CROSS_DEVICE: u32 = 0x0004; -/* Create a OS-specific handle from guest memory (not upstreamed). */ -pub const VIRTIO_GPU_BLOB_FLAG_CREATE_GUEST_HANDLE: u32 = 0x0008; - -pub const VIRTIO_GPU_SHM_ID_NONE: u8 = 0x0000; -pub const VIRTIO_GPU_SHM_ID_HOST_VISIBLE: u8 = 0x0001; - -pub fn virtio_gpu_cmd_str(cmd: u32) -> &'static str { - match cmd { - VIRTIO_GPU_CMD_GET_DISPLAY_INFO => "VIRTIO_GPU_CMD_GET_DISPLAY_INFO", - VIRTIO_GPU_CMD_RESOURCE_CREATE_2D => "VIRTIO_GPU_CMD_RESOURCE_CREATE_2D", - VIRTIO_GPU_CMD_RESOURCE_UNREF => "VIRTIO_GPU_CMD_RESOURCE_UNREF", - VIRTIO_GPU_CMD_SET_SCANOUT => "VIRTIO_GPU_CMD_SET_SCANOUT", - VIRTIO_GPU_CMD_SET_SCANOUT_BLOB => "VIRTIO_GPU_CMD_SET_SCANOUT_BLOB", - VIRTIO_GPU_CMD_RESOURCE_FLUSH => "VIRTIO_GPU_CMD_RESOURCE_FLUSH", - VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D => "VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D", - VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING => "VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING", - VIRTIO_GPU_CMD_RESOURCE_DETACH_BACKING => "VIRTIO_GPU_CMD_RESOURCE_DETACH_BACKING", - VIRTIO_GPU_CMD_GET_CAPSET_INFO => "VIRTIO_GPU_CMD_GET_CAPSET_INFO", - VIRTIO_GPU_CMD_GET_CAPSET => "VIRTIO_GPU_CMD_GET_CAPSET", - VIRTIO_GPU_CMD_CTX_CREATE => "VIRTIO_GPU_CMD_CTX_CREATE", - VIRTIO_GPU_CMD_CTX_DESTROY => "VIRTIO_GPU_CMD_CTX_DESTROY", - VIRTIO_GPU_CMD_CTX_ATTACH_RESOURCE => "VIRTIO_GPU_CMD_CTX_ATTACH_RESOURCE", - VIRTIO_GPU_CMD_CTX_DETACH_RESOURCE => "VIRTIO_GPU_CMD_CTX_DETACH_RESOURCE", - VIRTIO_GPU_CMD_RESOURCE_ASSIGN_UUID => "VIRTIO_GPU_CMD_RESOURCE_ASSIGN_UUID", - VIRTIO_GPU_CMD_RESOURCE_CREATE_BLOB => "VIRTIO_GPU_CMD_RESOURCE_CREATE_BLOB", - VIRTIO_GPU_CMD_RESOURCE_CREATE_3D => "VIRTIO_GPU_CMD_RESOURCE_CREATE_3D", - VIRTIO_GPU_CMD_TRANSFER_TO_HOST_3D => "VIRTIO_GPU_CMD_TRANSFER_TO_HOST_3D", - VIRTIO_GPU_CMD_TRANSFER_FROM_HOST_3D => "VIRTIO_GPU_CMD_TRANSFER_FROM_HOST_3D", - VIRTIO_GPU_CMD_SUBMIT_3D => "VIRTIO_GPU_CMD_SUBMIT_3D", - VIRTIO_GPU_CMD_RESOURCE_MAP_BLOB => "VIRTIO_GPU_RESOURCE_MAP_BLOB", - VIRTIO_GPU_CMD_RESOURCE_UNMAP_BLOB => "VIRTIO_GPU_RESOURCE_UNMAP_BLOB", - VIRTIO_GPU_CMD_UPDATE_CURSOR => "VIRTIO_GPU_CMD_UPDATE_CURSOR", - VIRTIO_GPU_CMD_MOVE_CURSOR => "VIRTIO_GPU_CMD_MOVE_CURSOR", - VIRTIO_GPU_RESP_OK_NODATA => "VIRTIO_GPU_RESP_OK_NODATA", - VIRTIO_GPU_RESP_OK_DISPLAY_INFO => "VIRTIO_GPU_RESP_OK_DISPLAY_INFO", - VIRTIO_GPU_RESP_OK_CAPSET_INFO => "VIRTIO_GPU_RESP_OK_CAPSET_INFO", - VIRTIO_GPU_RESP_OK_CAPSET => "VIRTIO_GPU_RESP_OK_CAPSET", - VIRTIO_GPU_RESP_OK_RESOURCE_PLANE_INFO => "VIRTIO_GPU_RESP_OK_RESOURCE_PLANE_INFO", - VIRTIO_GPU_RESP_OK_RESOURCE_UUID => "VIRTIO_GPU_RESP_OK_RESOURCE_UUID", - VIRTIO_GPU_RESP_OK_MAP_INFO => "VIRTIO_GPU_RESP_OK_MAP_INFO", - VIRTIO_GPU_RESP_ERR_UNSPEC => "VIRTIO_GPU_RESP_ERR_UNSPEC", - VIRTIO_GPU_RESP_ERR_OUT_OF_MEMORY => "VIRTIO_GPU_RESP_ERR_OUT_OF_MEMORY", - VIRTIO_GPU_RESP_ERR_INVALID_SCANOUT_ID => "VIRTIO_GPU_RESP_ERR_INVALID_SCANOUT_ID", - VIRTIO_GPU_RESP_ERR_INVALID_RESOURCE_ID => "VIRTIO_GPU_RESP_ERR_INVALID_RESOURCE_ID", - VIRTIO_GPU_RESP_ERR_INVALID_CONTEXT_ID => "VIRTIO_GPU_RESP_ERR_INVALID_CONTEXT_ID", - VIRTIO_GPU_RESP_ERR_INVALID_PARAMETER => "VIRTIO_GPU_RESP_ERR_INVALID_PARAMETER", - _ => "UNKNOWN", - } -} - -pub const VIRTIO_GPU_FLAG_FENCE: u32 = 1 << 0; -pub const VIRTIO_GPU_FLAG_INFO_RING_IDX: u32 = 1 << 1; - -#[derive(Copy, Clone, Debug, Default, IntoBytes, Immutable, FromBytes)] -#[repr(C)] -pub struct virtio_gpu_ctrl_hdr { - pub type_: u32, - pub flags: u32, - pub fence_id: u64, - pub ctx_id: u32, - pub ring_idx: u8, - pub padding: [u8; 3], -} -unsafe impl ByteValued for virtio_gpu_ctrl_hdr {} - -/* data passed in the cursor vq */ - -#[derive(Copy, Clone, Debug, Default, FromBytes, IntoBytes, Immutable)] -#[repr(C)] -pub struct virtio_gpu_cursor_pos { - pub scanout_id: u32, - pub x: u32, - pub y: u32, - pub padding: u32, -} -unsafe impl ByteValued for virtio_gpu_cursor_pos {} - -/* VIRTIO_GPU_CMD_UPDATE_CURSOR, VIRTIO_GPU_CMD_MOVE_CURSOR */ -#[derive(Copy, Clone, Debug, Default, FromBytes, IntoBytes, Immutable)] -#[repr(C)] -pub struct virtio_gpu_update_cursor { - pub pos: virtio_gpu_cursor_pos, /* update & move */ - pub resource_id: u32, /* update only */ - pub hot_x: u32, /* update only */ - pub hot_y: u32, /* update only */ - pub padding: u32, -} -unsafe impl ByteValued for virtio_gpu_update_cursor {} - -/* data passed in the control vq, 2d related */ - -#[derive(Copy, Clone, Debug, Default, FromBytes, IntoBytes, Immutable)] -#[repr(C)] -pub struct virtio_gpu_rect { - pub x: u32, - pub y: u32, - pub width: u32, - pub height: u32, -} -unsafe impl ByteValued for virtio_gpu_rect {} - -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -#[repr(C)] -pub struct virtio_gpu_get_edid { - pub scanout: u32, - pub padding: u32, -} - -unsafe impl ByteValued for virtio_gpu_get_edid {} - -/* VIRTIO_GPU_CMD_RESOURCE_UNREF */ -#[derive(Copy, Clone, Debug, Default, FromBytes, IntoBytes, Immutable)] -#[repr(C)] -pub struct virtio_gpu_resource_unref { - pub resource_id: u32, - pub padding: u32, -} -unsafe impl ByteValued for virtio_gpu_resource_unref {} - -/* VIRTIO_GPU_CMD_RESOURCE_CREATE_2D: create a 2d resource with a format */ -#[derive(Copy, Clone, Debug, Default, FromBytes, IntoBytes, Immutable)] -#[repr(C)] -pub struct virtio_gpu_resource_create_2d { - pub resource_id: u32, - pub format: u32, - pub width: u32, - pub height: u32, -} -unsafe impl ByteValued for virtio_gpu_resource_create_2d {} - -/* VIRTIO_GPU_CMD_SET_SCANOUT */ -#[derive(Copy, Clone, Debug, Default, FromBytes, IntoBytes, Immutable)] -#[repr(C)] -pub struct virtio_gpu_set_scanout { - pub r: virtio_gpu_rect, - pub scanout_id: u32, - pub resource_id: u32, -} -unsafe impl ByteValued for virtio_gpu_set_scanout {} - -/* VIRTIO_GPU_CMD_RESOURCE_FLUSH */ -#[derive(Copy, Clone, Debug, Default, FromBytes, IntoBytes, Immutable)] -#[repr(C)] -pub struct virtio_gpu_resource_flush { - pub r: virtio_gpu_rect, - pub resource_id: u32, - pub padding: u32, -} -unsafe impl ByteValued for virtio_gpu_resource_flush {} - -/* VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D: simple transfer to_host */ -#[derive(Copy, Clone, Debug, Default, FromBytes, IntoBytes, Immutable)] -#[repr(C)] -pub struct virtio_gpu_transfer_to_host_2d { - pub r: virtio_gpu_rect, - pub offset: u64, - pub resource_id: u32, - pub padding: u32, -} -unsafe impl ByteValued for virtio_gpu_transfer_to_host_2d {} - -#[derive(Copy, Clone, Debug, Default, IntoBytes, Immutable, FromBytes)] -#[repr(C)] -pub struct virtio_gpu_mem_entry { - pub addr: u64, - pub length: u32, - pub padding: u32, -} -unsafe impl ByteValued for virtio_gpu_mem_entry {} - -/* VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING */ -#[derive(Copy, Clone, Debug, Default, FromBytes, IntoBytes, Immutable)] -#[repr(C)] -pub struct virtio_gpu_resource_attach_backing { - pub resource_id: u32, - pub nr_entries: u32, -} -unsafe impl ByteValued for virtio_gpu_resource_attach_backing {} - -/* VIRTIO_GPU_CMD_RESOURCE_DETACH_BACKING */ -#[derive(Copy, Clone, Debug, Default, FromBytes, IntoBytes, Immutable)] -#[repr(C)] -pub struct virtio_gpu_resource_detach_backing { - pub resource_id: u32, - pub padding: u32, -} -unsafe impl ByteValued for virtio_gpu_resource_detach_backing {} - -#[derive(Copy, Clone, Debug, Default, FromBytes, IntoBytes, Immutable)] -#[repr(C)] -pub struct virtio_gpu_display_one { - pub r: virtio_gpu_rect, - pub enabled: u32, - pub flags: u32, -} -unsafe impl ByteValued for virtio_gpu_display_one {} - -/* VIRTIO_GPU_RESP_OK_DISPLAY_INFO */ -pub const VIRTIO_GPU_MAX_SCANOUTS: u32 = 16; -#[derive(Copy, Clone, Debug, Default, FromBytes, IntoBytes, Immutable)] -#[repr(C)] -pub struct virtio_gpu_resp_display_info { - pub hdr: virtio_gpu_ctrl_hdr, - pub pmodes: [virtio_gpu_display_one; VIRTIO_GPU_MAX_SCANOUTS as usize], -} -unsafe impl ByteValued for virtio_gpu_resp_display_info {} - -const EDID_BLOB_MAX_SIZE: usize = 1024; - -#[derive(Debug, Copy, Clone)] -#[repr(C)] -pub struct virtio_gpu_resp_edid { - pub hdr: virtio_gpu_ctrl_hdr, - pub size: u32, - pub padding: u32, - pub edid: [u8; EDID_BLOB_MAX_SIZE], -} - -unsafe impl ByteValued for virtio_gpu_resp_edid {} - -/* data passed in the control vq, 3d related */ - -#[derive(Copy, Clone, Debug, Default, FromBytes, IntoBytes, Immutable)] -#[repr(C)] -pub struct virtio_gpu_box { - pub x: u32, - pub y: u32, - pub z: u32, - pub w: u32, - pub h: u32, - pub d: u32, -} -unsafe impl ByteValued for virtio_gpu_box {} - -/* VIRTIO_GPU_CMD_TRANSFER_TO_HOST_3D, VIRTIO_GPU_CMD_TRANSFER_FROM_HOST_3D */ -#[derive(Copy, Clone, Debug, Default, FromBytes, IntoBytes, Immutable)] -#[repr(C)] -pub struct virtio_gpu_transfer_host_3d { - pub box_: virtio_gpu_box, - pub offset: u64, - pub resource_id: u32, - pub level: u32, - pub stride: u32, - pub layer_stride: u32, -} -unsafe impl ByteValued for virtio_gpu_transfer_host_3d {} - -/* VIRTIO_GPU_CMD_RESOURCE_CREATE_3D */ -pub const VIRTIO_GPU_RESOURCE_FLAG_Y_0_TOP: u32 = 1 << 0; -#[derive(Copy, Clone, Debug, Default, FromBytes, IntoBytes, Immutable)] -#[repr(C)] -pub struct virtio_gpu_resource_create_3d { - pub resource_id: u32, - pub target: u32, - pub format: u32, - pub bind: u32, - pub width: u32, - pub height: u32, - pub depth: u32, - pub array_size: u32, - pub last_level: u32, - pub nr_samples: u32, - pub flags: u32, - pub padding: u32, -} -unsafe impl ByteValued for virtio_gpu_resource_create_3d {} - -/* VIRTIO_GPU_CMD_CTX_CREATE */ -pub const VIRTIO_GPU_CONTEXT_INIT_CAPSET_ID_MASK: u32 = 1 << 0; -#[derive(Copy, FromBytes, IntoBytes, Immutable)] -#[repr(C)] -pub struct virtio_gpu_ctx_create { - pub nlen: u32, - pub context_init: u32, - pub debug_name: [u8; 64], -} -unsafe impl ByteValued for virtio_gpu_ctx_create {} - -impl Default for virtio_gpu_ctx_create { - fn default() -> Self { - unsafe { ::std::mem::zeroed() } - } -} - -impl Clone for virtio_gpu_ctx_create { - fn clone(&self) -> virtio_gpu_ctx_create { - *self - } -} - -impl fmt::Debug for virtio_gpu_ctx_create { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let debug_name = - from_utf8(&self.debug_name[..min(64, self.nlen as usize)]).unwrap_or(""); - f.debug_struct("virtio_gpu_ctx_create") - .field("debug_name", &debug_name) - .finish() - } -} - -/* VIRTIO_GPU_CMD_CTX_DESTROY */ -#[derive(Copy, Clone, Debug, Default, FromBytes, IntoBytes, Immutable)] -#[repr(C)] -pub struct virtio_gpu_ctx_destroy {} -unsafe impl ByteValued for virtio_gpu_ctx_destroy {} - -/* VIRTIO_GPU_CMD_CTX_ATTACH_RESOURCE, VIRTIO_GPU_CMD_CTX_DETACH_RESOURCE */ -#[derive(Copy, Clone, Debug, Default, FromBytes, IntoBytes, Immutable)] -#[repr(C)] -pub struct virtio_gpu_ctx_resource { - pub resource_id: u32, - pub padding: u32, -} -unsafe impl ByteValued for virtio_gpu_ctx_resource {} - -/* VIRTIO_GPU_CMD_SUBMIT_3D */ -#[derive(Copy, Clone, Debug, Default, FromBytes, IntoBytes, Immutable)] -#[repr(C)] -pub struct virtio_gpu_cmd_submit { - pub size: u32, - - // The in-fence IDs are prepended to the cmd_buf and memory layout - // of the VIRTIO_GPU_CMD_SUBMIT_3D buffer looks like this: - // _________________ - // | CMD_SUBMIT_3D | - // ----------------- - // | header | - // | in-fence IDs | - // | cmd_buf | - // ----------------- - // - // This makes in-fence IDs naturally aligned to the sizeof(u64) inside - // of the virtio buffer. - pub num_in_fences: u32, -} -unsafe impl ByteValued for virtio_gpu_cmd_submit {} - -pub const VIRTIO_GPU_CAPSET_VIRGL: u32 = 1; -pub const VIRTIO_GPU_CAPSET_VIRGL2: u32 = 2; -pub const VIRTIO_GPU_CAPSET_GFXSTREAM: u32 = 3; -pub const VIRTIO_GPU_CAPSET_VENUS: u32 = 4; -pub const VIRTIO_GPU_CAPSET_CROSS_DOMAIN: u32 = 5; - -/* VIRTIO_GPU_CMD_GET_CAPSET_INFO */ -#[derive(Copy, Clone, Debug, Default, FromBytes, IntoBytes, Immutable)] -#[repr(C)] -pub struct virtio_gpu_get_capset_info { - pub capset_index: u32, - pub padding: u32, -} -unsafe impl ByteValued for virtio_gpu_get_capset_info {} - -/* VIRTIO_GPU_RESP_OK_CAPSET_INFO */ -#[derive(Copy, Clone, Debug, Default, FromBytes, IntoBytes, Immutable)] -#[repr(C)] -pub struct virtio_gpu_resp_capset_info { - pub hdr: virtio_gpu_ctrl_hdr, - pub capset_id: u32, - pub capset_max_version: u32, - pub capset_max_size: u32, - pub padding: u32, -} -unsafe impl ByteValued for virtio_gpu_resp_capset_info {} - -/* VIRTIO_GPU_CMD_GET_CAPSET */ -#[derive(Copy, Clone, Debug, Default, FromBytes, IntoBytes, Immutable)] -#[repr(C)] -pub struct virtio_gpu_get_capset { - pub capset_id: u32, - pub capset_version: u32, -} -unsafe impl ByteValued for virtio_gpu_get_capset {} - -/* VIRTIO_GPU_RESP_OK_CAPSET */ -#[derive(Copy, Clone, Debug, Default)] -#[repr(C)] -pub struct virtio_gpu_resp_capset { - pub hdr: virtio_gpu_ctrl_hdr, - pub capset_data: PhantomData<[u8]>, -} -unsafe impl ByteValued for virtio_gpu_resp_capset {} - -/* VIRTIO_GPU_RESP_OK_RESOURCE_PLANE_INFO */ -#[derive(Copy, Clone, Debug, Default, FromBytes, IntoBytes, Immutable)] -#[repr(C)] -pub struct virtio_gpu_resp_resource_plane_info { - pub hdr: virtio_gpu_ctrl_hdr, - pub count: u32, - pub padding: u32, - pub format_modifier: u64, - pub strides: [u32; 4], - pub offsets: [u32; 4], -} -unsafe impl ByteValued for virtio_gpu_resp_resource_plane_info {} - -pub const PLANE_INFO_MAX_COUNT: usize = 4; - -pub const VIRTIO_GPU_EVENT_DISPLAY: u32 = 1 << 0; - -#[derive(Copy, Clone, Debug, Default, FromBytes, IntoBytes, Immutable)] -#[repr(C)] -pub struct virtio_gpu_resource_create_blob { - pub resource_id: u32, - pub blob_mem: u32, - pub blob_flags: u32, - pub nr_entries: u32, - pub blob_id: u64, - pub size: u64, -} -unsafe impl ByteValued for virtio_gpu_resource_create_blob {} - -#[derive(Copy, Clone, Debug, Default, FromBytes, IntoBytes, Immutable)] -#[repr(C)] -pub struct virtio_gpu_resource_map_blob { - pub resource_id: u32, - pub padding: u32, - pub offset: u64, -} -unsafe impl ByteValued for virtio_gpu_resource_map_blob {} - -#[derive(Copy, Clone, Debug, Default, FromBytes, IntoBytes, Immutable)] -#[repr(C)] -pub struct virtio_gpu_resource_unmap_blob { - pub resource_id: u32, - pub padding: u32, -} -unsafe impl ByteValued for virtio_gpu_resource_unmap_blob {} - -#[derive(Copy, Clone, Debug, Default, FromBytes, IntoBytes, Immutable)] -#[repr(C)] -pub struct virtio_gpu_resp_map_info { - pub hdr: virtio_gpu_ctrl_hdr, - pub map_info: u32, - pub padding: u32, -} -unsafe impl ByteValued for virtio_gpu_resp_map_info {} - -#[derive(Copy, Clone, Debug, Default, FromBytes, IntoBytes, Immutable)] -#[repr(C)] -pub struct virtio_gpu_resource_assign_uuid { - pub resource_id: u32, - pub padding: u32, -} -unsafe impl ByteValued for virtio_gpu_resource_assign_uuid {} - -#[derive(Copy, Clone, Debug, Default, FromBytes, IntoBytes, Immutable)] -#[repr(C)] -pub struct virtio_gpu_resp_resource_uuid { - pub hdr: virtio_gpu_ctrl_hdr, - pub uuid: [u8; 16], -} -unsafe impl ByteValued for virtio_gpu_resp_resource_uuid {} - -/* VIRTIO_GPU_CMD_SET_SCANOUT_BLOB */ -#[derive(Copy, Clone, Debug, Default, FromBytes, IntoBytes, Immutable)] -#[repr(C)] -pub struct virtio_gpu_set_scanout_blob { - pub r: virtio_gpu_rect, - pub scanout_id: u32, - pub resource_id: u32, - pub width: u32, - pub height: u32, - pub format: u32, - pub padding: u32, - pub strides: [u32; 4], - pub offsets: [u32; 4], -} -unsafe impl ByteValued for virtio_gpu_set_scanout_blob {} - -/* simple formats for fbcon/X use */ -pub const VIRTIO_GPU_FORMAT_B8G8R8A8_UNORM: u32 = 1; -pub const VIRTIO_GPU_FORMAT_B8G8R8X8_UNORM: u32 = 2; -pub const VIRTIO_GPU_FORMAT_A8R8G8B8_UNORM: u32 = 3; -pub const VIRTIO_GPU_FORMAT_X8R8G8B8_UNORM: u32 = 4; -pub const VIRTIO_GPU_FORMAT_R8G8B8A8_UNORM: u32 = 67; -pub const VIRTIO_GPU_FORMAT_X8B8G8R8_UNORM: u32 = 68; -pub const VIRTIO_GPU_FORMAT_A8B8G8R8_UNORM: u32 = 121; -pub const VIRTIO_GPU_FORMAT_R8G8B8X8_UNORM: u32 = 134; - -/// A virtio gpu command and associated metadata specific to each command. -#[derive(Copy, Clone)] -pub enum GpuCommand { - GetDisplayInfo, - GetEdid(virtio_gpu_get_edid), - ResourceCreate2d(virtio_gpu_resource_create_2d), - ResourceUnref(virtio_gpu_resource_unref), - SetScanout(virtio_gpu_set_scanout), - SetScanoutBlob(virtio_gpu_set_scanout_blob), - ResourceFlush(virtio_gpu_resource_flush), - TransferToHost2d(virtio_gpu_transfer_to_host_2d), - ResourceAttachBacking(virtio_gpu_resource_attach_backing), - ResourceDetachBacking(virtio_gpu_resource_detach_backing), - GetCapsetInfo(virtio_gpu_get_capset_info), - GetCapset(virtio_gpu_get_capset), - CtxCreate(virtio_gpu_ctx_create), - CtxDestroy(virtio_gpu_ctx_destroy), - CtxAttachResource(virtio_gpu_ctx_resource), - CtxDetachResource(virtio_gpu_ctx_resource), - ResourceCreate3d(virtio_gpu_resource_create_3d), - TransferToHost3d(virtio_gpu_transfer_host_3d), - TransferFromHost3d(virtio_gpu_transfer_host_3d), - CmdSubmit3d(virtio_gpu_cmd_submit), - ResourceCreateBlob(virtio_gpu_resource_create_blob), - ResourceMapBlob(virtio_gpu_resource_map_blob), - ResourceUnmapBlob(virtio_gpu_resource_unmap_blob), - UpdateCursor(virtio_gpu_update_cursor), - MoveCursor(virtio_gpu_update_cursor), - ResourceAssignUuid(virtio_gpu_resource_assign_uuid), -} - -/// An error indicating something went wrong decoding a `GpuCommand`. These correspond to -/// `VIRTIO_GPU_CMD_*`. -#[derive(Error, Debug)] -pub enum GpuCommandDecodeError { - /// The type of the command was invalid. - #[error("invalid command type ({0})")] - InvalidType(u32), - /// An I/O error occurred. - #[error("an I/O error occurred: {0}")] - IO(io::Error), -} - -impl From for GpuCommandDecodeError { - fn from(e: io::Error) -> GpuCommandDecodeError { - GpuCommandDecodeError::IO(e) - } -} - -impl fmt::Debug for GpuCommand { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use self::GpuCommand::*; - match self { - GetDisplayInfo => f.debug_struct("GetDisplayInfo").finish(), - GetEdid(_info) => f.debug_struct("GetEdid").finish(), - ResourceCreate2d(_info) => f.debug_struct("ResourceCreate2d").finish(), - ResourceUnref(_info) => f.debug_struct("ResourceUnref").finish(), - SetScanout(_info) => f.debug_struct("SetScanout").finish(), - SetScanoutBlob(_info) => f.debug_struct("SetScanoutBlob").finish(), - ResourceFlush(_info) => f.debug_struct("ResourceFlush").finish(), - TransferToHost2d(_info) => f.debug_struct("TransferToHost2d").finish(), - ResourceAttachBacking(_info) => f.debug_struct("ResourceAttachBacking").finish(), - ResourceDetachBacking(_info) => f.debug_struct("ResourceDetachBacking").finish(), - GetCapsetInfo(_info) => f.debug_struct("GetCapsetInfo").finish(), - GetCapset(_info) => f.debug_struct("GetCapset").finish(), - CtxCreate(_info) => f.debug_struct("CtxCreate").finish(), - CtxDestroy(_info) => f.debug_struct("CtxDestroy").finish(), - CtxAttachResource(_info) => f.debug_struct("CtxAttachResource").finish(), - CtxDetachResource(_info) => f.debug_struct("CtxDetachResource").finish(), - ResourceCreate3d(_info) => f.debug_struct("ResourceCreate3d").finish(), - TransferToHost3d(_info) => f.debug_struct("TransferToHost3d").finish(), - TransferFromHost3d(_info) => f.debug_struct("TransferFromHost3d").finish(), - CmdSubmit3d(_info) => f.debug_struct("CmdSubmit3d").finish(), - ResourceCreateBlob(_info) => f.debug_struct("ResourceCreateBlob").finish(), - ResourceMapBlob(_info) => f.debug_struct("ResourceMapBlob").finish(), - ResourceUnmapBlob(_info) => f.debug_struct("ResourceUnmapBlob").finish(), - UpdateCursor(_info) => f.debug_struct("UpdateCursor").finish(), - MoveCursor(_info) => f.debug_struct("MoveCursor").finish(), - ResourceAssignUuid(_info) => f.debug_struct("ResourceAssignUuid").finish(), - } - } -} - -impl GpuCommand { - /// Decodes a command from the given chunk of memory. - pub fn decode( - cmd: &mut Reader, - ) -> Result<(virtio_gpu_ctrl_hdr, GpuCommand), GpuCommandDecodeError> { - use self::GpuCommand::*; - let hdr = cmd.read_obj::()?; - let cmd = match hdr.type_ { - VIRTIO_GPU_CMD_GET_DISPLAY_INFO => GetDisplayInfo, - VIRTIO_GPU_CMD_GET_EDID => GetEdid(cmd.read_obj()?), - VIRTIO_GPU_CMD_RESOURCE_CREATE_2D => ResourceCreate2d(cmd.read_obj()?), - VIRTIO_GPU_CMD_RESOURCE_UNREF => ResourceUnref(cmd.read_obj()?), - VIRTIO_GPU_CMD_SET_SCANOUT => SetScanout(cmd.read_obj()?), - VIRTIO_GPU_CMD_SET_SCANOUT_BLOB => SetScanoutBlob(cmd.read_obj()?), - VIRTIO_GPU_CMD_RESOURCE_FLUSH => ResourceFlush(cmd.read_obj()?), - VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D => TransferToHost2d(cmd.read_obj()?), - VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING => ResourceAttachBacking(cmd.read_obj()?), - VIRTIO_GPU_CMD_RESOURCE_DETACH_BACKING => ResourceDetachBacking(cmd.read_obj()?), - VIRTIO_GPU_CMD_GET_CAPSET_INFO => GetCapsetInfo(cmd.read_obj()?), - VIRTIO_GPU_CMD_GET_CAPSET => GetCapset(cmd.read_obj()?), - VIRTIO_GPU_CMD_CTX_CREATE => CtxCreate(cmd.read_obj()?), - VIRTIO_GPU_CMD_CTX_DESTROY => CtxDestroy(cmd.read_obj()?), - VIRTIO_GPU_CMD_CTX_ATTACH_RESOURCE => CtxAttachResource(cmd.read_obj()?), - VIRTIO_GPU_CMD_CTX_DETACH_RESOURCE => CtxDetachResource(cmd.read_obj()?), - VIRTIO_GPU_CMD_RESOURCE_CREATE_3D => ResourceCreate3d(cmd.read_obj()?), - VIRTIO_GPU_CMD_TRANSFER_TO_HOST_3D => TransferToHost3d(cmd.read_obj()?), - VIRTIO_GPU_CMD_TRANSFER_FROM_HOST_3D => TransferFromHost3d(cmd.read_obj()?), - VIRTIO_GPU_CMD_SUBMIT_3D => CmdSubmit3d(cmd.read_obj()?), - VIRTIO_GPU_CMD_RESOURCE_CREATE_BLOB => ResourceCreateBlob(cmd.read_obj()?), - VIRTIO_GPU_CMD_RESOURCE_MAP_BLOB => ResourceMapBlob(cmd.read_obj()?), - VIRTIO_GPU_CMD_RESOURCE_UNMAP_BLOB => ResourceUnmapBlob(cmd.read_obj()?), - VIRTIO_GPU_CMD_UPDATE_CURSOR => UpdateCursor(cmd.read_obj()?), - VIRTIO_GPU_CMD_MOVE_CURSOR => MoveCursor(cmd.read_obj()?), - VIRTIO_GPU_CMD_RESOURCE_ASSIGN_UUID => ResourceAssignUuid(cmd.read_obj()?), - _ => return Err(GpuCommandDecodeError::InvalidType(hdr.type_)), - }; - - Ok((hdr, cmd)) - } -} - -#[derive(Debug, PartialEq, Eq)] -pub struct GpuResponsePlaneInfo { - pub stride: u32, - pub offset: u32, -} - -/// A response to a `GpuCommand`. These correspond to `VIRTIO_GPU_RESP_*`. -#[derive(Debug)] -pub enum GpuResponse { - OkNoData, - OkDisplayInfo(Vec<(u32, u32, bool)>), - OkEdid(Box<[u8]>), - OkCapsetInfo { - capset_id: u32, - version: u32, - size: u32, - }, - OkCapset(Vec), - OkResourcePlaneInfo { - format_modifier: u64, - plane_info: Vec, - }, - OkResourceUuid { - uuid: [u8; 16], - }, - OkMapInfo { - map_info: u32, - }, - ErrUnspec, - ErrRutabaga(RutabagaError), - ErrScanout { - num_scanouts: u32, - }, - ErrOutOfMemory, - ErrInvalidScanoutId, - ErrInvalidResourceId, - ErrInvalidContextId, - ErrInvalidParameter, -} - -impl From for GpuResponse { - fn from(e: RutabagaError) -> GpuResponse { - GpuResponse::ErrRutabaga(e) - } -} -impl From for GpuResponse { - fn from(err: DisplayBackendError) -> GpuResponse { - match err { - DisplayBackendError::InternalError => { - error!("Unknown internal error occurred in display implementation"); - GpuResponse::ErrUnspec - } - DisplayBackendError::MethodNotSupported => { - // This is likely an implementation error in the GPU device - using a display method - // that has not been negotiated using the feature flags. - error!("Display does not support used method for scanout"); - GpuResponse::ErrUnspec - } - DisplayBackendError::OutOfBuffers => { - // We should never actually reach this, since we only ever use 1 buffer at a given - // moment. - error!("Display backend ran out of buffers"); - GpuResponse::ErrUnspec - } - DisplayBackendError::InvalidScanoutId => GpuResponse::ErrInvalidScanoutId, - DisplayBackendError::InvalidParam => GpuResponse::ErrInvalidParameter, - } - } -} - -impl Display for GpuResponse { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use self::GpuResponse::*; - match self { - ErrRutabaga(e) => write!(f, "renderer error: {e}"), - ErrScanout { num_scanouts } => write!(f, "non-zero scanout: {num_scanouts}"), - _ => Ok(()), - } - } -} - -/// An error indicating something went wrong decoding a `GpuCommand`. -#[derive(Error, Debug)] -pub enum GpuResponseEncodeError { - /// An I/O error occurred. - #[error("an I/O error occurred: {0}")] - IO(io::Error), - /// Bigger EDID blob than valid in a`OkEdid`. - #[error("{0} bytes is too big for an EDID blob")] - EdidTooBig(usize), - /// More displays than are valid were in a `OkDisplayInfo`. - #[error("{0} is more displays than are valid")] - TooManyDisplays(usize), - /// More planes than are valid were in a `OkResourcePlaneInfo`. - #[error("{0} is more planes than are valid")] - TooManyPlanes(usize), -} - -impl From for GpuResponseEncodeError { - fn from(e: io::Error) -> GpuResponseEncodeError { - GpuResponseEncodeError::IO(e) - } -} - -pub type VirtioGpuResult = std::result::Result; - -impl GpuResponse { - /// Encodes a this `GpuResponse` into `resp` and the given set of metadata. - pub fn encode( - &self, - flags: u32, - fence_id: u64, - ctx_id: u32, - ring_idx: u8, - resp: &mut Writer, - ) -> Result { - let hdr = virtio_gpu_ctrl_hdr { - type_: self.get_type(), - flags, - fence_id, - ctx_id, - ring_idx, - padding: Default::default(), - }; - let len = match *self { - GpuResponse::OkDisplayInfo(ref info) => { - if info.len() > VIRTIO_GPU_MAX_SCANOUTS as usize { - return Err(GpuResponseEncodeError::TooManyDisplays(info.len())); - } - let mut disp_info = virtio_gpu_resp_display_info { - hdr, - pmodes: Default::default(), - }; - for (disp_mode, &(width, height, enabled)) in disp_info.pmodes.iter_mut().zip(info) - { - disp_mode.r.width = width; - disp_mode.r.height = height; - disp_mode.enabled = enabled as u32; - } - resp.write_obj(disp_info)?; - size_of_val(&disp_info) - } - GpuResponse::OkEdid(ref blob) => { - if blob.len() > EDID_BLOB_MAX_SIZE { - return Err(GpuResponseEncodeError::EdidTooBig(blob.len())); - }; - - let mut edid_info = virtio_gpu_resp_edid { - hdr, - size: blob.len() as u32, - edid: [0; EDID_BLOB_MAX_SIZE], - padding: Default::default(), - }; - edid_info.edid[..blob.len()].copy_from_slice(blob); - resp.write_obj(edid_info)?; - size_of_val(&edid_info) - } - GpuResponse::OkCapsetInfo { - capset_id, - version, - size, - } => { - resp.write_obj(virtio_gpu_resp_capset_info { - hdr, - capset_id, - capset_max_version: version, - capset_max_size: size, - padding: 0u32, - })?; - size_of::() - } - GpuResponse::OkCapset(ref data) => { - resp.write_obj(hdr)?; - resp.write_all(data)?; - size_of_val(&hdr) + data.len() - } - GpuResponse::OkResourcePlaneInfo { - format_modifier, - ref plane_info, - } => { - if plane_info.len() > PLANE_INFO_MAX_COUNT { - return Err(GpuResponseEncodeError::TooManyPlanes(plane_info.len())); - } - let mut strides = [u32::default(); PLANE_INFO_MAX_COUNT]; - let mut offsets = [u32::default(); PLANE_INFO_MAX_COUNT]; - for (plane_index, plane) in plane_info.iter().enumerate() { - strides[plane_index] = plane.stride; - offsets[plane_index] = plane.offset; - } - let plane_info = virtio_gpu_resp_resource_plane_info { - hdr, - count: plane_info.len() as u32, - padding: 0u32, - format_modifier, - strides, - offsets, - }; - if resp.available_bytes() >= size_of_val(&plane_info) { - resp.write_obj(plane_info)?; - size_of_val(&plane_info) - } else { - // In case there is too little room in the response slice to store the - // entire virtio_gpu_resp_resource_plane_info, convert response to a regular - // VIRTIO_GPU_RESP_OK_NODATA and attempt to return that. - resp.write_obj(virtio_gpu_ctrl_hdr { - type_: VIRTIO_GPU_RESP_OK_NODATA, - ..hdr - })?; - size_of_val(&hdr) - } - } - GpuResponse::OkResourceUuid { uuid } => { - let resp_info = virtio_gpu_resp_resource_uuid { hdr, uuid }; - - resp.write_obj(resp_info)?; - size_of_val(&resp_info) - } - GpuResponse::OkMapInfo { map_info } => { - let resp_info = virtio_gpu_resp_map_info { - hdr, - map_info, - padding: Default::default(), - }; - - resp.write_obj(resp_info)?; - size_of_val(&resp_info) - } - _ => { - resp.write_obj(hdr)?; - size_of_val(&hdr) - } - }; - Ok(len as u32) - } - - /// Gets the `VIRTIO_GPU_*` enum value that corresponds to this variant. - pub fn get_type(&self) -> u32 { - match self { - GpuResponse::OkNoData => VIRTIO_GPU_RESP_OK_NODATA, - GpuResponse::OkDisplayInfo(_) => VIRTIO_GPU_RESP_OK_DISPLAY_INFO, - GpuResponse::OkEdid(_) => VIRTIO_GPU_RESP_OK_EDID, - GpuResponse::OkCapsetInfo { .. } => VIRTIO_GPU_RESP_OK_CAPSET_INFO, - GpuResponse::OkCapset(_) => VIRTIO_GPU_RESP_OK_CAPSET, - GpuResponse::OkResourcePlaneInfo { .. } => VIRTIO_GPU_RESP_OK_RESOURCE_PLANE_INFO, - GpuResponse::OkResourceUuid { .. } => VIRTIO_GPU_RESP_OK_RESOURCE_UUID, - GpuResponse::OkMapInfo { .. } => VIRTIO_GPU_RESP_OK_MAP_INFO, - GpuResponse::ErrUnspec => VIRTIO_GPU_RESP_ERR_UNSPEC, - GpuResponse::ErrRutabaga(_) => VIRTIO_GPU_RESP_ERR_UNSPEC, - GpuResponse::ErrScanout { num_scanouts: _ } => VIRTIO_GPU_RESP_ERR_UNSPEC, - GpuResponse::ErrOutOfMemory => VIRTIO_GPU_RESP_ERR_OUT_OF_MEMORY, - GpuResponse::ErrInvalidScanoutId => VIRTIO_GPU_RESP_ERR_INVALID_SCANOUT_ID, - GpuResponse::ErrInvalidResourceId => VIRTIO_GPU_RESP_ERR_INVALID_RESOURCE_ID, - GpuResponse::ErrInvalidContextId => VIRTIO_GPU_RESP_ERR_INVALID_CONTEXT_ID, - GpuResponse::ErrInvalidParameter => VIRTIO_GPU_RESP_ERR_INVALID_PARAMETER, - } - } -} diff --git a/vendor/krun-devices/src/virtio/gpu/virtio_gpu.rs b/vendor/krun-devices/src/virtio/gpu/virtio_gpu.rs deleted file mode 100644 index 43b4ef7ec..000000000 --- a/vendor/krun-devices/src/virtio/gpu/virtio_gpu.rs +++ /dev/null @@ -1,1055 +0,0 @@ -use std::collections::BTreeMap; -use std::env; -use std::io::IoSliceMut; -#[cfg(target_os = "linux")] -use std::os::fd::AsRawFd; -use std::path::PathBuf; -use std::sync::{Arc, Mutex}; - -use super::super::Queue as VirtQueue; -use super::protocol::GpuResponse::*; -use super::protocol::{ - GpuResponse, GpuResponsePlaneInfo, VirtioGpuResult, VIRTIO_GPU_BLOB_FLAG_CREATE_GUEST_HANDLE, - VIRTIO_GPU_BLOB_MEM_HOST3D, VIRTIO_GPU_MAX_SCANOUTS, -}; -#[cfg(target_os = "macos")] -use crossbeam_channel::{unbounded, Sender}; -use krun_display::{ - DisplayBackend, DisplayBackendBasicFramebuffer, DisplayBackendInstance, Rect, ResourceFormat, -}; -use libc::c_void; -#[cfg(target_os = "macos")] -use rutabaga_gfx::RUTABAGA_MEM_HANDLE_TYPE_APPLE; -#[cfg(all(feature = "virgl_resource_map2", target_os = "linux"))] -use rutabaga_gfx::RUTABAGA_MEM_HANDLE_TYPE_DMABUF; -#[cfg(all(not(feature = "virgl_resource_map2"), target_os = "linux"))] -use rutabaga_gfx::RUTABAGA_MEM_HANDLE_TYPE_OPAQUE_FD; -#[cfg(all(feature = "virgl_resource_map2", target_os = "linux"))] -use rutabaga_gfx::RUTABAGA_MEM_HANDLE_TYPE_SHM; -use rutabaga_gfx::{ - ResourceCreate3D, ResourceCreateBlob, Rutabaga, RutabagaBuilder, RutabagaChannel, - RutabagaFence, RutabagaFenceHandler, RutabagaIovec, Transfer3D, RUTABAGA_CHANNEL_TYPE_WAYLAND, - RUTABAGA_MAP_CACHE_MASK, -}; -#[cfg(target_os = "linux")] -use rutabaga_gfx::{ - RUTABAGA_CHANNEL_TYPE_PW, RUTABAGA_CHANNEL_TYPE_X11, RUTABAGA_MAP_ACCESS_MASK, - RUTABAGA_MAP_ACCESS_READ, RUTABAGA_MAP_ACCESS_RW, RUTABAGA_MAP_ACCESS_WRITE, -}; -#[cfg(target_os = "macos")] -use utils::worker_message::WorkerMessage; -use vm_memory::{GuestAddress, GuestMemory, GuestMemoryMmap, VolatileSlice}; - -use super::{GpuError, Result}; -use crate::virtio::display::DisplayInfo; -use crate::virtio::fs::ExportTable; -use crate::virtio::gpu::protocol::VIRTIO_GPU_FLAG_INFO_RING_IDX; -use crate::virtio::{InterruptTransport, VirtioShmRegion}; - -fn sglist_to_rutabaga_iovecs( - vecs: &[(GuestAddress, usize)], - mem: &GuestMemoryMmap, -) -> Result> { - if vecs - .iter() - .any(|&(addr, len)| mem.get_slice(addr, len).is_err()) - { - return Err(GpuError::GuestMemory); - } - - let mut rutabaga_iovecs: Vec = Vec::new(); - for &(addr, len) in vecs { - let slice = mem.get_slice(addr, len).unwrap(); - rutabaga_iovecs.push(RutabagaIovec { - base: slice.ptr_guard_mut().as_ptr() as *mut c_void, - len, - }); - } - Ok(rutabaga_iovecs) -} - -#[derive(PartialEq, Eq, PartialOrd, Ord)] -pub enum VirtioGpuRing { - Global, - ContextSpecific { ctx_id: u32, ring_idx: u8 }, -} - -struct FenceDescriptor { - ring: VirtioGpuRing, - fence_id: u64, - desc_index: u16, - len: u32, -} - -#[derive(Default)] -pub struct FenceState { - descs: Vec, - completed_fences: BTreeMap, -} - -#[derive(Copy, Clone, Debug, Default)] -struct AssociatedScanouts(u32); - -impl AssociatedScanouts { - fn enable(&mut self, scanout_id: u32) { - self.0 |= 1 << scanout_id; - } - - fn disable(&mut self, scanout_id: u32) { - self.0 ^= 1 << scanout_id; - } - - const fn has_any_enabled(self) -> bool { - self.0 != 0 - } - - fn iter_enabled(self) -> impl Iterator { - (0..VIRTIO_GPU_MAX_SCANOUTS).filter(move |i| ((self.0 >> i) & 1) == 1) - } -} - -#[derive(Copy, Clone)] -struct VirtioGpuResource { - id: u32, - width: u32, - height: u32, - scanouts: AssociatedScanouts, - format: Option, - size: u64, // only for blob resources - shmem_offset: Option, - rutabaga_external_mapping: bool, -} - -impl VirtioGpuResource { - /// Creates a new VirtioGpuResource with the given metadata. Width and height are used by the - /// display, while size is useful for hypervisor mapping. - pub fn new( - resource_id: u32, - width: u32, - height: u32, - format: Option, - size: u64, - ) -> VirtioGpuResource { - VirtioGpuResource { - id: resource_id, - width, - height, - scanouts: Default::default(), - size, - format, - shmem_offset: None, - rutabaga_external_mapping: false, - } - } -} - -pub struct VirtioGpuScanout { - resource_id: u32, -} - -pub struct VirtioGpu { - rutabaga: Rutabaga, - resources: BTreeMap, - fence_state: Arc>, - #[cfg(target_os = "macos")] - map_sender: Sender, - scanouts: [Option; VIRTIO_GPU_MAX_SCANOUTS as usize], - displays: Box<[DisplayInfo]>, - display_backend: DisplayBackendInstance, -} - -impl VirtioGpu { - fn create_fence_handler( - mem: GuestMemoryMmap, - queue_ctl: Arc>, - fence_state: Arc>, - interrupt: InterruptTransport, - ) -> RutabagaFenceHandler { - RutabagaFenceHandler::new(move |completed_fence: RutabagaFence| { - debug!( - "XXX - fence called: id={}, ring_idx={}", - completed_fence.fence_id, completed_fence.ring_idx - ); - - let mut queue = queue_ctl.lock().unwrap(); - let mut fence_state = fence_state.lock().unwrap(); - let mut i = 0; - - let ring = match completed_fence.flags & VIRTIO_GPU_FLAG_INFO_RING_IDX { - 0 => VirtioGpuRing::Global, - _ => VirtioGpuRing::ContextSpecific { - ctx_id: completed_fence.ctx_id, - ring_idx: completed_fence.ring_idx, - }, - }; - - while i < fence_state.descs.len() { - debug!("XXX - fence_id: {}", fence_state.descs[i].fence_id); - if fence_state.descs[i].ring == ring - && fence_state.descs[i].fence_id <= completed_fence.fence_id - { - let completed_desc = fence_state.descs.remove(i); - debug!( - "XXX - found fence: desc_index={}", - completed_desc.desc_index - ); - - if let Err(e) = - queue.add_used(&mem, completed_desc.desc_index, completed_desc.len) - { - error!("failed to add used elements to the queue: {e:?}"); - } - - interrupt.signal_used_queue(); - } else { - i += 1; - } - } - // Update the last completed fence for this context. - // Use max() to avoid a race where an out-of-order completion - // (e.g., immediate-retire for fence N+1 followed by timeline - // signal for fence N) would overwrite a higher fence_id with - // a lower one, causing fence N+1 to be stuck forever. - let entry = fence_state.completed_fences.entry(ring).or_insert(0); - *entry = (*entry).max(completed_fence.fence_id); - }) - } - - pub fn create_rutabaga( - mem: GuestMemoryMmap, - queue_ctl: Arc>, - interrupt: InterruptTransport, - fence_state: Arc>, - virgl_flags: u32, - export_table: Option, - ) -> Option { - let xdg_runtime_dir = match env::var("XDG_RUNTIME_DIR") { - Ok(dir) => dir, - Err(_) => "/run/user/1000".to_string(), - }; - let wayland_display = match env::var("WAYLAND_DISPLAY") { - Ok(display) => display, - Err(_) => "wayland-0".to_string(), - }; - let path = PathBuf::from(format!("{xdg_runtime_dir}/{wayland_display}")); - - #[allow(unused_mut)] - let mut rutabaga_channels: Vec = vec![RutabagaChannel { - base_channel: path, - channel_type: RUTABAGA_CHANNEL_TYPE_WAYLAND, - }]; - - #[cfg(target_os = "linux")] - if let Ok(x_display) = env::var("DISPLAY") { - if let Some(x_display) = x_display.strip_prefix(":") { - let x_path = PathBuf::from(format!("/tmp/.X11-unix/X{x_display}")); - rutabaga_channels.push(RutabagaChannel { - base_channel: x_path, - channel_type: RUTABAGA_CHANNEL_TYPE_X11, - }); - } - } - #[cfg(target_os = "linux")] - if let Ok(pw_sock_dir) = env::var("PIPEWIRE_RUNTIME_DIR") - .or_else(|_| env::var("XDG_RUNTIME_DIR")) - .or_else(|_| env::var("USERPROFILE")) - { - let name = env::var("PIPEWIRE_REMOTE").unwrap_or_else(|_| "pipewire-0".to_string()); - let mut pw_path = PathBuf::from(pw_sock_dir); - pw_path.push(name); - rutabaga_channels.push(RutabagaChannel { - base_channel: pw_path, - channel_type: RUTABAGA_CHANNEL_TYPE_PW, - }); - } - let rutabaga_channels_opt = Some(rutabaga_channels); - - let builder = RutabagaBuilder::new( - rutabaga_gfx::RutabagaComponentType::VirglRenderer, - virgl_flags, - 0, - ) - .set_rutabaga_channels(rutabaga_channels_opt); - let builder = if let Some(export_table) = export_table { - builder.set_export_table(export_table) - } else { - builder - }; - - let fence = - Self::create_fence_handler(mem, queue_ctl.clone(), fence_state.clone(), interrupt); - builder.clone().build(fence.clone(), None).ok() - } - - pub fn create_fallback_rutabaga( - mem: GuestMemoryMmap, - queue_ctl: Arc>, - interrupt: InterruptTransport, - fence_state: Arc>, - ) -> Option { - const VIRGLRENDERER_NO_VIRGL: u32 = 1 << 7; - let builder = RutabagaBuilder::new( - rutabaga_gfx::RutabagaComponentType::VirglRenderer, - VIRGLRENDERER_NO_VIRGL, - 0, - ); - - let fence = - Self::create_fence_handler(mem, queue_ctl.clone(), fence_state.clone(), interrupt); - builder.clone().build(fence.clone(), None).ok() - } - - #[allow(clippy::too_many_arguments)] - pub fn new( - mem: GuestMemoryMmap, - queue_ctl: Arc>, - interrupt: InterruptTransport, - virgl_flags: u32, - #[cfg(target_os = "macos")] map_sender: Sender, - export_table: Option, - displays: Box<[DisplayInfo]>, - display_backend: DisplayBackend, - ) -> Self { - let fence_state = Arc::new(Mutex::new(Default::default())); - - let rutabaga = match Self::create_rutabaga( - mem.clone(), - queue_ctl.clone(), - interrupt.clone(), - fence_state.clone(), - virgl_flags, - export_table.clone(), - ) { - Some(rutabaga) => rutabaga, - None => { - warn!("Failed to create virtio_gpu backend with the requested parameters. Falling back to safe defaults."); - Self::create_fallback_rutabaga( - mem.clone(), - queue_ctl.clone(), - interrupt.clone(), - fence_state.clone(), - ) - .expect("Fallback rutabaga initialization failed") - } - }; - - let display_backend = display_backend - .create_instance() - .expect("Failed to create display backend instance!"); - - Self { - rutabaga, - resources: Default::default(), - fence_state, - scanouts: Default::default(), - displays, - display_backend, - #[cfg(target_os = "macos")] - map_sender, - } - } - - // Non-public function -- no doc comment needed! - fn result_from_query(&mut self, resource_id: u32) -> GpuResponse { - match self.rutabaga.query(resource_id) { - Ok(query) => { - let mut plane_info = Vec::with_capacity(4); - for plane_index in 0..4 { - plane_info.push(GpuResponsePlaneInfo { - stride: query.strides[plane_index], - offset: query.offsets[plane_index], - }); - } - let format_modifier = query.modifier; - OkResourcePlaneInfo { - format_modifier, - plane_info, - } - } - Err(_) => OkNoData, - } - } - - pub fn force_ctx_0(&self) { - self.rutabaga.force_ctx_0() - } - - /// Creates a 3D resource with the given properties and resource_id. - pub fn resource_create_3d( - &mut self, - resource_id: u32, - resource_create_3d: ResourceCreate3D, - ) -> VirtioGpuResult { - self.rutabaga - .resource_create_3d(resource_id, resource_create_3d)?; - - let format = ResourceFormat::try_from(resource_create_3d.format).ok(); - if format.is_none() { - debug!( - "Unknown format {} for resource {}", - resource_create_3d.format, resource_id - ); - } - - let resource = VirtioGpuResource::new( - resource_id, - resource_create_3d.width, - resource_create_3d.height, - format, - 0, - ); - - // Rely on rutabaga to check for duplicate resource ids. - self.resources.insert(resource_id, resource); - Ok(self.result_from_query(resource_id)) - } - - /// Releases guest kernel reference on the resource. - pub fn unref_resource(&mut self, resource_id: u32) -> VirtioGpuResult { - let resource = self - .resources - .remove(&resource_id) - .ok_or(ErrInvalidResourceId)?; - - if resource.scanouts.has_any_enabled() { - warn!( - "The driver requested unref_resource, but resource {resource_id} has \ - associated scanouts, refusing to delete the resource." - ); - return Err(ErrUnspec); - } - - if resource.rutabaga_external_mapping { - self.rutabaga.unmap(resource_id)?; - } - - self.rutabaga.unref_resource(resource_id)?; - Ok(OkNoData) - } - - pub fn set_scanout( - &mut self, - scanout_id: u32, - resource_id: u32, - width: u32, - height: u32, - ) -> VirtioGpuResult { - let scanout = self - .scanouts - .get_mut(scanout_id as usize) - .ok_or(ErrInvalidScanoutId)?; - - // If a resource is already associated with this scanout, make sure to disable - // this scanout for that resource - if let Some(resource_id) = scanout.as_ref().map(|scanout| scanout.resource_id) { - let resource = self - .resources - .get_mut(&resource_id) - .ok_or(ErrInvalidResourceId)?; - - resource.scanouts.disable(scanout_id); - } - - // Virtio spec: "The driver can use resource_id = 0 to disable a scanout." - if resource_id == 0 { - debug!("Disabling scanout {scanout_id:?}"); - *scanout = None; - self.display_backend.disable_scanout(scanout_id)?; - return Ok(OkNoData); - } - - // Enable the scanout - let resource = self - .resources - .get_mut(&resource_id) - .ok_or(ErrInvalidResourceId)?; - resource.scanouts.enable(scanout_id); - - let Some(format) = resource.format else { - warn!("Cannot use resource {resource_id} with unknown format for scanout"); - return Err(ErrUnspec); - }; - - let display_info = self - .displays - .get(scanout_id as usize) - .ok_or(ErrInvalidScanoutId)?; - - self.display_backend.configure_scanout( - scanout_id, - display_info.width, - display_info.height, - width, - height, - format, - )?; - - *scanout = Some(VirtioGpuScanout { resource_id }); - Ok(OkNoData) - } - - fn read_2d_resource( - rutabaga: &mut Rutabaga, - resource: VirtioGpuResource, - output: &mut [u8], - ) -> VirtioGpuResult { - let transfer = Transfer3D { - x: 0, - y: 0, - z: 0, - w: resource.width, - h: resource.height, - d: 1, - level: 0, - stride: resource.width * ResourceFormat::BYTES_PER_PIXEL as u32, - layer_stride: 0, - offset: 0, - }; - - rutabaga - .transfer_read(0, resource.id, transfer, Some(IoSliceMut::new(output))) - .map_err(|e| format!("{e}")) - .unwrap(); - - Ok(OkNoData) - } - - /// If the resource is the scanout resource, flush it to the display. - pub fn flush_resource(&mut self, resource_id: u32, rect: Rect) -> VirtioGpuResult { - if resource_id == 0 { - return Ok(OkNoData); - } - - let resource = *self - .resources - .get(&resource_id) - .ok_or(ErrInvalidResourceId)?; - - for scanout_id in resource.scanouts.iter_enabled() { - let (frame_id, buffer) = self.display_backend.alloc_frame(scanout_id)?; - if let Err(e) = Self::read_2d_resource(&mut self.rutabaga, resource, buffer) { - log::error!("Failed to read resource {resource_id} for scanout {scanout_id}: {e}"); - return Err(ErrUnspec); - } - self.display_backend - .present_frame(scanout_id, frame_id, Some(&rect))? - } - - #[cfg(windows)] - match self.rutabaga.resource_flush(resource_id) { - Ok(_) => return Ok(OkNoData), - Err(RutabagaError::Unsupported) => {} - Err(e) => return Err(ErrRutabaga(e)), - } - - Ok(OkNoData) - } - - pub fn display_info(&self) -> VirtioGpuResult { - let display_info = self - .displays - .iter() - .map(|d| (d.width, d.height, true)) - .collect(); - - Ok(OkDisplayInfo(display_info)) - } - - pub fn get_edid(&self, scanout_id: u32) -> VirtioGpuResult { - let display = self - .displays - .get(scanout_id as usize) - .ok_or(ErrInvalidScanoutId)?; - - Ok(OkEdid(display.edid_bytes())) - } - - /// Copies data to host resource from the attached iovecs. Can also be used to flush caches. - pub fn transfer_write( - &mut self, - ctx_id: u32, - resource_id: u32, - transfer: Transfer3D, - ) -> VirtioGpuResult { - self.rutabaga - .transfer_write(ctx_id, resource_id, transfer)?; - Ok(OkNoData) - } - - /// Copies data from the host resource to: - /// 1) To the optional volatile slice - /// 2) To the host resource's attached iovecs - /// - /// Can also be used to invalidate caches. - pub fn transfer_read( - &mut self, - _ctx_id: u32, - _resource_id: u32, - _transfer: Transfer3D, - _buf: Option, - ) -> VirtioGpuResult { - panic!("virtio_gpu: transfer_read unimplemented"); - } - - /// Attaches backing memory to the given resource, represented by a `Vec` of `(address, size)` - /// tuples in the guest's physical address space. Converts to RutabagaIovec from the memory - /// mapping. - pub fn attach_backing( - &mut self, - resource_id: u32, - mem: &GuestMemoryMmap, - vecs: Vec<(GuestAddress, usize)>, - ) -> VirtioGpuResult { - let rutabaga_iovecs = sglist_to_rutabaga_iovecs(&vecs[..], mem).map_err(|_| ErrUnspec)?; - self.rutabaga.attach_backing(resource_id, rutabaga_iovecs)?; - Ok(OkNoData) - } - - /// Detaches any previously attached iovecs from the resource. - pub fn detach_backing(&mut self, resource_id: u32) -> VirtioGpuResult { - self.rutabaga.detach_backing(resource_id)?; - Ok(OkNoData) - } - - /// Returns a uuid for the resource. - pub fn resource_assign_uuid(&self, resource_id: u32) -> VirtioGpuResult { - if !self.resources.contains_key(&resource_id) { - return Err(ErrInvalidResourceId); - } - - // TODO(stevensd): use real uuids once the virtio wayland protocol is updated to - // handle more than 32 bits. For now, the virtwl driver knows that the uuid is - // actually just the resource id. - let mut uuid: [u8; 16] = [0; 16]; - for (idx, byte) in resource_id.to_be_bytes().iter().enumerate() { - uuid[12 + idx] = *byte; - } - Ok(OkResourceUuid { uuid }) - } - - /// Gets rutabaga's capset information associated with `index`. - pub fn get_capset_info(&self, index: u32) -> VirtioGpuResult { - let (capset_id, version, size) = self.rutabaga.get_capset_info(index)?; - Ok(OkCapsetInfo { - capset_id, - version, - size, - }) - } - - /// Gets a capset from rutabaga. - pub fn get_capset(&self, capset_id: u32, version: u32) -> VirtioGpuResult { - let capset = self.rutabaga.get_capset(capset_id, version)?; - Ok(OkCapset(capset)) - } - - /// Creates a rutabaga context. - pub fn create_context( - &mut self, - ctx_id: u32, - context_init: u32, - context_name: Option<&str>, - ) -> VirtioGpuResult { - self.rutabaga - .create_context(ctx_id, context_init, context_name)?; - Ok(OkNoData) - } - - /// Destroys a rutabaga context. - pub fn destroy_context(&mut self, ctx_id: u32) -> VirtioGpuResult { - self.rutabaga.destroy_context(ctx_id)?; - Ok(OkNoData) - } - - /// Attaches a resource to a rutabaga context. - pub fn context_attach_resource(&mut self, ctx_id: u32, resource_id: u32) -> VirtioGpuResult { - self.rutabaga.context_attach_resource(ctx_id, resource_id)?; - Ok(OkNoData) - } - - /// Detaches a resource from a rutabaga context. - pub fn context_detach_resource(&mut self, ctx_id: u32, resource_id: u32) -> VirtioGpuResult { - self.rutabaga.context_detach_resource(ctx_id, resource_id)?; - Ok(OkNoData) - } - - /// Submits a command buffer to a rutabaga context. - pub fn submit_command( - &mut self, - ctx_id: u32, - commands: &mut [u8], - fence_ids: &[u64], - ) -> VirtioGpuResult { - self.rutabaga.submit_command(ctx_id, commands, fence_ids)?; - Ok(OkNoData) - } - - /// Creates a fence with the RutabagaFence that can be used to determine when the previous - /// command completed. - pub fn create_fence(&mut self, rutabaga_fence: RutabagaFence) -> VirtioGpuResult { - self.rutabaga.create_fence(rutabaga_fence)?; - Ok(OkNoData) - } - - pub fn process_fence( - &mut self, - ring: VirtioGpuRing, - fence_id: u64, - desc_index: u16, - len: u32, - ) -> bool { - // In case the fence is signaled immediately after creation, don't add a return - // FenceDescriptor. - let mut fence_state = self.fence_state.lock().unwrap(); - if fence_id > *fence_state.completed_fences.get(&ring).unwrap_or(&0) { - fence_state.descs.push(FenceDescriptor { - ring, - fence_id, - desc_index, - len, - }); - - false - } else { - true - } - } - - /// Creates a blob resource using rutabaga. - pub fn resource_create_blob( - &mut self, - ctx_id: u32, - resource_id: u32, - resource_create_blob: ResourceCreateBlob, - vecs: Vec<(GuestAddress, usize)>, - mem: &GuestMemoryMmap, - ) -> VirtioGpuResult { - let mut rutabaga_iovecs = None; - - if resource_create_blob.blob_flags & VIRTIO_GPU_BLOB_FLAG_CREATE_GUEST_HANDLE != 0 { - panic!("GUEST_HANDLE unimplemented"); - } else if resource_create_blob.blob_mem != VIRTIO_GPU_BLOB_MEM_HOST3D { - rutabaga_iovecs = - Some(sglist_to_rutabaga_iovecs(&vecs[..], mem).map_err(|_| ErrUnspec)?); - } - - self.rutabaga.resource_create_blob( - ctx_id, - resource_id, - resource_create_blob, - rutabaga_iovecs, - None, - )?; - - let resource = VirtioGpuResource::new(resource_id, 0, 0, None, resource_create_blob.size); - - // Rely on rutabaga to check for duplicate resource ids. - self.resources.insert(resource_id, resource); - Ok(self.result_from_query(resource_id)) - } - - /// Uses the hypervisor to map the rutabaga blob resource. - /// - /// When sandboxing is disabled, external_blob is unset and opaque fds are mapped by - /// rutabaga as ExternalMapping. - /// When sandboxing is enabled, external_blob is set and opaque fds must be mapped in the - /// hypervisor process by Vulkano using metadata provided by Rutabaga::vulkan_info(). - #[cfg(all(not(feature = "virgl_resource_map2"), target_os = "linux"))] - pub fn resource_map_blob( - &mut self, - resource_id: u32, - shm_region: &VirtioShmRegion, - offset: u64, - ) -> VirtioGpuResult { - let resource = self - .resources - .get_mut(&resource_id) - .ok_or(ErrInvalidResourceId)?; - - let map_info = self.rutabaga.map_info(resource_id).map_err(|_| ErrUnspec)?; - - if let Ok(export) = self.rutabaga.export_blob(resource_id) { - if export.handle_type != RUTABAGA_MEM_HANDLE_TYPE_OPAQUE_FD { - let prot = match map_info & RUTABAGA_MAP_ACCESS_MASK { - RUTABAGA_MAP_ACCESS_READ => libc::PROT_READ, - RUTABAGA_MAP_ACCESS_WRITE => libc::PROT_WRITE, - RUTABAGA_MAP_ACCESS_RW => libc::PROT_READ | libc::PROT_WRITE, - _ => panic!("unexpected prot mode for mapping"), - }; - - if offset + resource.size > shm_region.size as u64 { - error!("mapping DOES NOT FIT"); - } - let addr = shm_region.host_addr + offset; - debug!( - "mapping: host_addr={:x}, addr={:x}, size={}", - shm_region.host_addr, addr, resource.size - ); - let ret = unsafe { - libc::mmap( - addr as *mut libc::c_void, - resource.size as usize, - prot, - libc::MAP_SHARED | libc::MAP_FIXED, - export.os_handle.as_raw_fd(), - 0 as libc::off_t, - ) - }; - if ret == libc::MAP_FAILED { - return Err(ErrUnspec); - } - } else { - return Err(ErrUnspec); - } - } else { - return Err(ErrUnspec); - } - - resource.shmem_offset = Some(offset); - // Access flags not a part of the virtio-gpu spec. - Ok(OkMapInfo { - map_info: map_info & RUTABAGA_MAP_CACHE_MASK, - }) - } - #[cfg(all(feature = "virgl_resource_map2", target_os = "linux"))] - pub fn resource_map_blob( - &mut self, - resource_id: u32, - shm_region: &VirtioShmRegion, - offset: u64, - ) -> VirtioGpuResult { - let resource = self - .resources - .get_mut(&resource_id) - .ok_or(ErrInvalidResourceId)?; - - let map_info = self.rutabaga.map_info(resource_id).map_err(|_| ErrUnspec)?; - - let prot = match map_info & RUTABAGA_MAP_ACCESS_MASK { - RUTABAGA_MAP_ACCESS_READ => libc::PROT_READ, - RUTABAGA_MAP_ACCESS_WRITE => libc::PROT_WRITE, - RUTABAGA_MAP_ACCESS_RW => libc::PROT_READ | libc::PROT_WRITE, - _ => panic!("unexpected prot mode for mapping"), - }; - - if offset + resource.size > shm_region.size as u64 { - error!("resource map doesn't fit in shm region"); - return Err(ErrUnspec); - } - let addr = shm_region.host_addr + offset; - - if let Ok(export) = self.rutabaga.export_blob(resource_id) { - // SHM and DMABUF are both regular host fds whose pages can be exposed - // to the guest by mmap'ing them directly into the virtio shm region. - // For SHM (memfd) this has always worked. For DMABUF it had been - // delegated to virgl_renderer_resource_map2, which only handles - // virglrenderer-allocated GPU memory and silently no-ops for external - // dma-bufs — leaving the guest blob backed by zero pages. That broke - // muvm camera capture, where the v4l2 source exports kernel buffers - // via VIDIOC_EXPBUF as dma-bufs, the muvm bridge forwards the fd - // across SCM_RIGHTS, libkrun classifies it as DMABUF, and the guest's - // CREATE_BLOB allocates a host-backed-by-nothing blob. Mapping the - // dma-buf fd directly here gives the guest real, live pages. - if export.handle_type == RUTABAGA_MEM_HANDLE_TYPE_SHM - || export.handle_type == RUTABAGA_MEM_HANDLE_TYPE_DMABUF - { - let ret = unsafe { - libc::mmap( - addr as *mut libc::c_void, - resource.size as usize, - prot, - libc::MAP_SHARED | libc::MAP_FIXED, - export.os_handle.as_raw_fd(), - 0 as libc::off_t, - ) - }; - if ret == libc::MAP_FAILED { - error!( - "failed to mmap resource in shm region (handle_type={:#x})", - export.handle_type - ); - return Err(ErrUnspec); - } - } else { - self.rutabaga.resource_map( - resource_id, - addr, - resource.size, - prot, - libc::MAP_SHARED | libc::MAP_FIXED, - )?; - } - } - - resource.shmem_offset = Some(offset); - // Access flags not a part of the virtio-gpu spec. - Ok(OkMapInfo { - map_info: map_info & RUTABAGA_MAP_CACHE_MASK, - }) - } - #[cfg(target_os = "macos")] - pub fn resource_map_blob( - &mut self, - resource_id: u32, - shm_region: &VirtioShmRegion, - offset: u64, - ) -> VirtioGpuResult { - let resource = self - .resources - .get_mut(&resource_id) - .ok_or(ErrInvalidResourceId)?; - - let map_info = self.rutabaga.map_info(resource_id).map_err(|_| ErrUnspec)?; - let map_ptr = self.rutabaga.map_ptr(resource_id).map_err(|_| ErrUnspec)?; - - if let Ok(export) = self.rutabaga.export_blob(resource_id) { - if export.handle_type == RUTABAGA_MEM_HANDLE_TYPE_APPLE { - if offset + resource.size > shm_region.size as u64 { - error!("mapping DOES NOT FIT"); - return Err(ErrUnspec); - } - - let guest_addr = shm_region.guest_addr + offset; - debug!( - "mapping: map_ptr={:x}, guest_addr={:x}, size={}", - map_ptr, guest_addr, resource.size - ); - - let (reply_sender, reply_receiver) = unbounded(); - self.map_sender - .send(WorkerMessage::GpuAddMapping( - reply_sender, - map_ptr, - guest_addr, - resource.size, - )) - .unwrap(); - if !reply_receiver.recv().unwrap() { - return Err(ErrUnspec); - } - } else { - return Err(ErrUnspec); - } - } else { - return Err(ErrUnspec); - } - - resource.shmem_offset = Some(offset); - // Access flags not a part of the virtio-gpu spec. - Ok(OkMapInfo { - map_info: map_info & RUTABAGA_MAP_CACHE_MASK, - }) - } - - /// Uses the hypervisor to unmap the blob resource. - #[cfg(target_os = "linux")] - pub fn resource_unmap_blob( - &mut self, - resource_id: u32, - shm_region: &VirtioShmRegion, - ) -> VirtioGpuResult { - let resource = self - .resources - .get_mut(&resource_id) - .ok_or(ErrInvalidResourceId)?; - - let shmem_offset = resource.shmem_offset.ok_or(ErrUnspec)?; - - let addr = shm_region.host_addr + shmem_offset; - - let ret = unsafe { - libc::mmap( - addr as *mut libc::c_void, - resource.size as usize, - libc::PROT_NONE, - libc::MAP_ANONYMOUS | libc::MAP_PRIVATE | libc::MAP_FIXED, - -1, - 0_i64, - ) - }; - if ret == libc::MAP_FAILED { - panic!("UNMAP failed"); - } - - resource.shmem_offset = None; - - Ok(OkNoData) - } - #[cfg(target_os = "macos")] - pub fn resource_unmap_blob( - &mut self, - resource_id: u32, - shm_region: &VirtioShmRegion, - ) -> VirtioGpuResult { - let resource = self - .resources - .get_mut(&resource_id) - .ok_or(ErrInvalidResourceId)?; - - debug!("resource_unmap_blob"); - let shmem_offset = resource.shmem_offset.ok_or(ErrUnspec)?; - - let guest_addr = shm_region.guest_addr + shmem_offset; - debug!( - "unmapping: guest_addr={:x}, size={}", - guest_addr, resource.size - ); - - let (reply_sender, reply_receiver) = unbounded(); - self.map_sender - .send(WorkerMessage::GpuRemoveMapping( - reply_sender, - guest_addr, - resource.size, - )) - .unwrap(); - if !reply_receiver.recv().unwrap() { - return Err(ErrUnspec); - } - - resource.shmem_offset = None; - - Ok(OkNoData) - } -} -#[cfg(test)] -mod test { - use crate::virtio::gpu::protocol::VIRTIO_GPU_MAX_SCANOUTS; - - #[test] - fn test_virtio_gpu_associated_scanouts() { - use super::AssociatedScanouts; - - let mut scanouts = AssociatedScanouts::default(); - - assert!(!scanouts.has_any_enabled()); - assert_eq!(scanouts.iter_enabled().next(), None); - - scanouts.enable(1); - assert!(scanouts.has_any_enabled()); - scanouts.disable(1); - assert!(!scanouts.has_any_enabled()); - - (0..VIRTIO_GPU_MAX_SCANOUTS).for_each(|scanout| scanouts.enable(scanout)); - assert!(scanouts.has_any_enabled()); - assert_eq!( - scanouts.iter_enabled().collect::>(), - (0..VIRTIO_GPU_MAX_SCANOUTS).collect::>() - ); - - (0..VIRTIO_GPU_MAX_SCANOUTS) - .filter(|&i| i % 2 == 0) - .for_each(|scanout| scanouts.disable(scanout)); - assert_eq!( - scanouts.iter_enabled().collect::>(), - (1..VIRTIO_GPU_MAX_SCANOUTS) - .step_by(2) - .collect::>() - ); - - (0..VIRTIO_GPU_MAX_SCANOUTS) - .filter(|&i| i % 2 != 0) - .for_each(|scanout| scanouts.disable(scanout)); - assert!(!scanouts.has_any_enabled()); - } -} diff --git a/vendor/krun-devices/src/virtio/gpu/worker.rs b/vendor/krun-devices/src/virtio/gpu/worker.rs deleted file mode 100644 index e00186c46..000000000 --- a/vendor/krun-devices/src/virtio/gpu/worker.rs +++ /dev/null @@ -1,456 +0,0 @@ -use std::io::Read; -use std::os::fd::{AsRawFd, BorrowedFd}; -use std::sync::{Arc, Mutex}; -use std::thread; - -use nix::fcntl::{fcntl, FcntlArg, OFlag}; -use utils::eventfd::EventFd; - -#[cfg(target_os = "macos")] -use crossbeam_channel::Sender; -use rutabaga_gfx::{ - ResourceCreate3D, ResourceCreateBlob, RutabagaFence, Transfer3D, - RUTABAGA_PIPE_BIND_RENDER_TARGET, RUTABAGA_PIPE_TEXTURE_2D, -}; -#[cfg(target_os = "macos")] -use utils::worker_message::WorkerMessage; -use vm_memory::{GuestAddress, GuestMemoryMmap}; - -use super::super::descriptor_utils::{Reader, Writer}; -use super::super::{DeviceQueue, GpuError, Queue as VirtQueue}; -use super::protocol::{ - virtio_gpu_ctrl_hdr, virtio_gpu_mem_entry, GpuCommand, GpuResponse, VirtioGpuResult, -}; -use super::virtio_gpu::VirtioGpu; -use crate::virtio::display::DisplayInfo; -use crate::virtio::fs::ExportTable; -use crate::virtio::gpu::protocol::{VIRTIO_GPU_FLAG_FENCE, VIRTIO_GPU_FLAG_INFO_RING_IDX}; -use crate::virtio::gpu::virtio_gpu::VirtioGpuRing; -use crate::virtio::{InterruptTransport, VirtioShmRegion}; -use krun_display::DisplayBackend; -use krun_display::Rect; - -pub struct Worker { - control_evt: EventFd, - control_queue: Arc>, - mem: GuestMemoryMmap, - interrupt: InterruptTransport, - shm_region: VirtioShmRegion, - virgl_flags: u32, - #[cfg(target_os = "macos")] - map_sender: Sender, - export_table: Option, - displays: Box<[DisplayInfo]>, - display_backend: DisplayBackend<'static>, -} - -impl Worker { - #[allow(clippy::too_many_arguments)] - pub fn new( - control_q: DeviceQueue, - mem: GuestMemoryMmap, - interrupt: InterruptTransport, - shm_region: VirtioShmRegion, - virgl_flags: u32, - #[cfg(target_os = "macos")] map_sender: Sender, - export_table: Option, - displays: Box<[DisplayInfo]>, - display_backend: DisplayBackend<'static>, - ) -> Self { - // Clone the eventfd so we have our own file description, then set it to blocking mode. - let control_evt = control_q.event.try_clone().unwrap(); - // SAFETY: control_evt is valid for the duration of the fcntl calls. - let fd = unsafe { BorrowedFd::borrow_raw(control_evt.as_raw_fd()) }; - let flags = - OFlag::from_bits_retain(fcntl(fd, FcntlArg::F_GETFL).unwrap()) & !OFlag::O_NONBLOCK; - fcntl(fd, FcntlArg::F_SETFL(flags)).unwrap(); - - Self { - control_evt, - control_queue: Arc::new(Mutex::new(control_q.queue)), - mem, - interrupt, - shm_region, - virgl_flags, - #[cfg(target_os = "macos")] - map_sender, - export_table, - displays, - display_backend, - } - } - - pub fn run(self) { - thread::Builder::new() - .name("gpu worker".into()) - .spawn(|| self.work()) - .unwrap(); - } - - fn work(mut self) { - let mut virtio_gpu = VirtioGpu::new( - self.mem.clone(), - self.control_queue.clone(), - self.interrupt.clone(), - self.virgl_flags, - #[cfg(target_os = "macos")] - self.map_sender.clone(), - self.export_table.take(), - self.displays.clone(), - self.display_backend, - ); - - loop { - if let Err(e) = self.control_evt.read() { - error!("Failed to read control_evt: {e:?}"); - continue; - } - if self.process_queue(&mut virtio_gpu, &self.control_queue.clone()) { - if let Err(e) = self.interrupt.try_signal_used_queue() { - error!("Error signaling queue: {e:?}"); - } - } - } - } - - fn process_gpu_command( - &mut self, - virtio_gpu: &mut VirtioGpu, - mem: &GuestMemoryMmap, - hdr: virtio_gpu_ctrl_hdr, - cmd: GpuCommand, - reader: &mut Reader, - ) -> VirtioGpuResult { - virtio_gpu.force_ctx_0(); - - match cmd { - GpuCommand::GetDisplayInfo => virtio_gpu.display_info(), - GpuCommand::GetEdid(info) => virtio_gpu.get_edid(info.scanout), - GpuCommand::ResourceCreate2d(info) => { - let resource_id = info.resource_id; - - let resource_create_3d = ResourceCreate3D { - target: RUTABAGA_PIPE_TEXTURE_2D, - format: info.format, - bind: RUTABAGA_PIPE_BIND_RENDER_TARGET, - width: info.width, - height: info.height, - depth: 1, - array_size: 1, - last_level: 0, - nr_samples: 0, - flags: 0, - }; - - virtio_gpu.resource_create_3d(resource_id, resource_create_3d) - } - GpuCommand::ResourceUnref(info) => virtio_gpu.unref_resource(info.resource_id), - GpuCommand::SetScanout(info) => virtio_gpu.set_scanout( - info.scanout_id, - info.resource_id, - info.r.width, - info.r.height, - ), - GpuCommand::ResourceFlush(info) => { - let rect = Rect { - x: info.r.x, - y: info.r.y, - width: info.r.width, - height: info.r.height, - }; - virtio_gpu.flush_resource(info.resource_id, rect) - } - GpuCommand::TransferToHost2d(info) => { - let resource_id = info.resource_id; - let transfer = Transfer3D::new_2d(info.r.x, info.r.y, info.r.width, info.r.height); - virtio_gpu.transfer_write(0, resource_id, transfer) - } - GpuCommand::ResourceAttachBacking(info) => { - let available_bytes = reader.available_bytes(); - if available_bytes != 0 { - let entry_count = info.nr_entries as usize; - let mut vecs = Vec::with_capacity(entry_count); - for _ in 0..entry_count { - match reader.read_obj::() { - Ok(entry) => { - let addr = GuestAddress(entry.addr); - let len = entry.length as usize; - vecs.push((addr, len)) - } - Err(_) => return Err(GpuResponse::ErrUnspec), - } - } - virtio_gpu.attach_backing(info.resource_id, mem, vecs) - } else { - error!("missing data for command {cmd:?}"); - Err(GpuResponse::ErrUnspec) - } - } - GpuCommand::ResourceDetachBacking(info) => virtio_gpu.detach_backing(info.resource_id), - GpuCommand::UpdateCursor(_info) => { - panic!("virtio_gpu: GpuCommand:UpdateCursor unimplemented"); - } - GpuCommand::MoveCursor(_info) => { - panic!("virtio_gpu: GpuCommand::MoveCursor unimplemented"); - } - GpuCommand::ResourceAssignUuid(info) => { - let resource_id = info.resource_id; - virtio_gpu.resource_assign_uuid(resource_id) - } - GpuCommand::GetCapsetInfo(info) => virtio_gpu.get_capset_info(info.capset_index), - GpuCommand::GetCapset(info) => { - virtio_gpu.get_capset(info.capset_id, info.capset_version) - } - - GpuCommand::CtxCreate(info) => { - let context_name: Option = String::from_utf8(info.debug_name.to_vec()).ok(); - virtio_gpu.create_context(hdr.ctx_id, info.context_init, context_name.as_deref()) - } - GpuCommand::CtxDestroy(_info) => virtio_gpu.destroy_context(hdr.ctx_id), - GpuCommand::CtxAttachResource(info) => { - virtio_gpu.context_attach_resource(hdr.ctx_id, info.resource_id) - } - GpuCommand::CtxDetachResource(info) => { - virtio_gpu.context_detach_resource(hdr.ctx_id, info.resource_id) - } - GpuCommand::ResourceCreate3d(info) => { - let resource_id = info.resource_id; - let resource_create_3d = ResourceCreate3D { - target: info.target, - format: info.format, - bind: info.bind, - width: info.width, - height: info.height, - depth: info.depth, - array_size: info.array_size, - last_level: info.last_level, - nr_samples: info.nr_samples, - flags: info.flags, - }; - - virtio_gpu.resource_create_3d(resource_id, resource_create_3d) - } - GpuCommand::TransferToHost3d(info) => { - let ctx_id = hdr.ctx_id; - let resource_id = info.resource_id; - - let transfer = Transfer3D { - x: info.box_.x, - y: info.box_.y, - z: info.box_.z, - w: info.box_.w, - h: info.box_.h, - d: info.box_.d, - level: info.level, - stride: info.stride, - layer_stride: info.layer_stride, - offset: info.offset, - }; - - virtio_gpu.transfer_write(ctx_id, resource_id, transfer) - } - GpuCommand::TransferFromHost3d(info) => { - let ctx_id = hdr.ctx_id; - let resource_id = info.resource_id; - - let transfer = Transfer3D { - x: info.box_.x, - y: info.box_.y, - z: info.box_.z, - w: info.box_.w, - h: info.box_.h, - d: info.box_.d, - level: info.level, - stride: info.stride, - layer_stride: info.layer_stride, - offset: info.offset, - }; - - virtio_gpu.transfer_read(ctx_id, resource_id, transfer, None) - } - GpuCommand::CmdSubmit3d(info) => { - if reader.available_bytes() != 0 { - let num_in_fences = info.num_in_fences as usize; - let cmd_size = info.size as usize; - let mut cmd_buf = vec![0; cmd_size]; - let mut fence_ids: Vec = Vec::with_capacity(num_in_fences); - - for _ in 0..num_in_fences { - match reader.read_obj::() { - Ok(fence_id) => { - fence_ids.push(fence_id); - } - Err(_) => return Err(GpuResponse::ErrUnspec), - } - } - - if reader.read_exact(&mut cmd_buf[..]).is_ok() { - virtio_gpu.submit_command(hdr.ctx_id, &mut cmd_buf[..], &fence_ids) - } else { - Err(GpuResponse::ErrInvalidParameter) - } - } else { - // Silently accept empty command buffers to allow for - // benchmarking. - Ok(GpuResponse::OkNoData) - } - } - GpuCommand::ResourceCreateBlob(info) => { - let resource_id = info.resource_id; - let ctx_id = hdr.ctx_id; - - let resource_create_blob = ResourceCreateBlob { - blob_mem: info.blob_mem, - blob_flags: info.blob_flags, - blob_id: info.blob_id, - size: info.size, - }; - - let entry_count = info.nr_entries; - if reader.available_bytes() == 0 && entry_count > 0 { - return Err(GpuResponse::ErrUnspec); - } - - let mut vecs = Vec::with_capacity(entry_count as usize); - for _ in 0..entry_count { - match reader.read_obj::() { - Ok(entry) => { - let addr = GuestAddress(entry.addr); - let len = entry.length as usize; - vecs.push((addr, len)) - } - Err(_) => return Err(GpuResponse::ErrUnspec), - } - } - - virtio_gpu.resource_create_blob( - ctx_id, - resource_id, - resource_create_blob, - vecs, - mem, - ) - } - GpuCommand::SetScanoutBlob(_info) => { - panic!("virtio_gpu: GpuCommand::SetScanoutBlob unimplemented"); - } - GpuCommand::ResourceMapBlob(info) => { - let resource_id = info.resource_id; - let offset = info.offset; - virtio_gpu.resource_map_blob(resource_id, &self.shm_region, offset) - } - GpuCommand::ResourceUnmapBlob(info) => { - let resource_id = info.resource_id; - virtio_gpu.resource_unmap_blob(resource_id, &self.shm_region) - } - } - } - - fn process_queue( - &mut self, - virtio_gpu: &mut VirtioGpu, - control_queue: &Arc>, - ) -> bool { - let mut used_any = false; - let mem = self.mem.clone(); - - loop { - let head = control_queue.lock().unwrap().pop(&mem); - - if let Some(head) = head { - let mut reader = Reader::new(&mem, head.clone()) - .map_err(GpuError::QueueReader) - .unwrap(); - let mut writer = Writer::new(&mem, head.clone()) - .map_err(GpuError::QueueWriter) - .unwrap(); - - let mut resp = Err(GpuResponse::ErrUnspec); - let mut gpu_cmd = None; - let mut ctrl_hdr = None; - let mut len = 0; - - match GpuCommand::decode(&mut reader) { - Ok((hdr, cmd)) => { - resp = self.process_gpu_command(virtio_gpu, &mem, hdr, cmd, &mut reader); - ctrl_hdr = Some(hdr); - gpu_cmd = Some(cmd); - } - Err(e) => debug!("descriptor decode error: {e:?}"), - } - - let mut gpu_response = match resp { - Ok(gpu_response) => gpu_response, - Err(gpu_response) => { - debug!("{gpu_cmd:?} -> {gpu_response:?}"); - gpu_response - } - }; - - let mut add_to_queue = true; - - if writer.available_bytes() != 0 { - let mut fence_id = 0; - let mut ctx_id = 0; - let mut flags = 0; - let mut ring_idx = 0; - if let Some(_cmd) = gpu_cmd { - let ctrl_hdr = ctrl_hdr.unwrap(); - if ctrl_hdr.flags & VIRTIO_GPU_FLAG_FENCE != 0 { - flags = ctrl_hdr.flags; - fence_id = ctrl_hdr.fence_id; - ctx_id = ctrl_hdr.ctx_id; - ring_idx = ctrl_hdr.ring_idx; - - let fence = RutabagaFence { - flags, - fence_id, - ctx_id, - ring_idx, - }; - gpu_response = match virtio_gpu.create_fence(fence) { - Ok(_) => gpu_response, - Err(fence_resp) => { - warn!("create_fence {fence_id} -> {fence_resp:?}"); - fence_resp - } - }; - } - } - - // Prepare the response now, even if it is going to wait until - // fence is complete. - match gpu_response.encode(flags, fence_id, ctx_id, ring_idx, &mut writer) { - Ok(l) => len = l, - Err(e) => debug!("ctrl queue response encode error: {e:?}"), - } - - if flags & VIRTIO_GPU_FLAG_FENCE != 0 { - let ring = match flags & VIRTIO_GPU_FLAG_INFO_RING_IDX { - 0 => VirtioGpuRing::Global, - _ => VirtioGpuRing::ContextSpecific { ctx_id, ring_idx }, - }; - - add_to_queue = virtio_gpu.process_fence(ring, fence_id, head.index, len); - } - } - - if add_to_queue { - if let Err(e) = control_queue - .lock() - .unwrap() - .add_used(&mem, head.index, len) - { - error!("failed to add used elements to the queue: {e:?}"); - } - used_any = true; - } - } else { - break; - } - } - - debug!("gpu: process_queue exit"); - used_any - } -} diff --git a/vendor/krun-devices/src/virtio/input/device.rs b/vendor/krun-devices/src/virtio/input/device.rs deleted file mode 100644 index c62a1c6e8..000000000 --- a/vendor/krun-devices/src/virtio/input/device.rs +++ /dev/null @@ -1,263 +0,0 @@ -use std::cmp; -use std::io::Write; -use std::thread::JoinHandle; - -use log::{debug, error}; -use utils::eventfd::{EventFd, EFD_NONBLOCK}; -use vm_memory::GuestMemoryMmap; - -use super::super::{ - ActivateError, ActivateResult, DeviceQueue, DeviceState, QueueConfig, VirtioDevice, -}; -use super::worker::InputWorker; -use super::{defs, defs::uapi, InputError}; - -use crate::virtio::input::defs::config_select; -use crate::virtio::input::defs::config_select::VIRTIO_INPUT_CFG_UNSET; -use crate::virtio::InterruptTransport; -use krun_input::{ - InputAbsInfo, InputConfigBackend, InputConfigInstance, InputDeviceIds, - InputEventProviderBackend, InputQueryConfig, -}; - -#[derive(Clone, Copy)] -union InputConfig { - bytes: [u8; size_of::()], - repr: InputConfigRepr, -} - -impl InputConfig { - pub fn new() -> Self { - Self { - bytes: [0u8; size_of::()], - } - } - - pub fn select(&self) -> u8 { - unsafe { self.repr.select } - } - - pub fn subsel(&self) -> u8 { - unsafe { self.repr.subsel } - } - - pub fn bytes(&self) -> &[u8; size_of::()] { - unsafe { &self.bytes } - } - - pub fn invalidate(&mut self) { - self.repr.select = VIRTIO_INPUT_CFG_UNSET; - self.repr.subsel = 0; - self.repr.size = 0; - } - - fn update_select(&mut self, cfg: &InputConfigInstance, select: u8, subsel: u8) { - if select == self.select() && subsel == self.subsel() { - return; - } - - unsafe { - self.repr.payload.bytes.fill(0); - } - - let result = match select { - config_select::VIRTIO_INPUT_CFG_ID_NAME => { - cfg.query_device_name(unsafe { &mut self.repr.payload.bytes }) - } - config_select::VIRTIO_INPUT_CFG_ID_SERIAL => { - cfg.query_serial_name(unsafe { &mut self.repr.payload.bytes }) - } - config_select::VIRTIO_INPUT_CFG_ID_DEVIDS => cfg - .query_device_ids(unsafe { &mut self.repr.payload.ids }) - .map(|_| size_of::() as u8), - config_select::VIRTIO_INPUT_CFG_PROP_BITS => { - cfg.query_properties(unsafe { &mut self.repr.payload.bytes }) - } - config_select::VIRTIO_INPUT_CFG_EV_BITS => { - cfg.query_event_capabilities(subsel, unsafe { &mut self.repr.payload.bytes }) - } - config_select::VIRTIO_INPUT_CFG_ABS_INFO => cfg - .query_abs_info(subsel, unsafe { &mut self.repr.payload.abs }) - .map(|_| size_of::() as u8), - select => { - error!("Invalid config selection select = {select}"); - self.invalidate(); - return; - } - }; - - match result { - Ok(len) => { - self.repr.size = len; - self.repr.select = select; - self.repr.subsel = subsel; - } - Err(e) => { - error!("Failed to query config select={select}, subsel={subsel}: {e:?}"); - self.invalidate(); - } - }; - } -} - -#[derive(Clone, Copy)] -#[repr(C)] -pub struct InputConfigRepr { - select: u8, - subsel: u8, - size: u8, - reserved: [u8; 5], - payload: ConfigPayload, -} - -#[derive(Clone, Copy)] -#[repr(C)] -union ConfigPayload { - bytes: [u8; 128], - abs: InputAbsInfo, - ids: InputDeviceIds, -} - -/// VirtIO Input device state -pub struct Input { - avail_features: u64, - acked_features: u64, - device_state: DeviceState, - cfg: InputConfig, - config_instance: InputConfigInstance, - event_provider_backend: InputEventProviderBackend<'static>, - - worker_thread: Option>, - worker_stopfd: EventFd, -} - -impl Input { - pub fn new( - config_backend: InputConfigBackend<'static>, - events_backend: InputEventProviderBackend<'static>, - ) -> super::Result { - Ok(Input { - avail_features: AVAIL_FEATURES, - acked_features: 0, - event_provider_backend: events_backend, - config_instance: config_backend.create_instance().unwrap(), - device_state: DeviceState::Inactive, - cfg: InputConfig::new(), - worker_thread: None, - worker_stopfd: EventFd::new(EFD_NONBLOCK).map_err(InputError::EventFd)?, - }) - } - - pub fn id(&self) -> &str { - defs::INPUT_DEV_ID - } -} - -const AVAIL_FEATURES: u64 = 1 << uapi::VIRTIO_F_VERSION_1; - -impl VirtioDevice for Input { - fn avail_features(&self) -> u64 { - self.avail_features - } - - fn acked_features(&self) -> u64 { - self.acked_features - } - - fn set_acked_features(&mut self, acked_features: u64) { - self.acked_features = acked_features - } - - fn device_type(&self) -> u32 { - uapi::VIRTIO_ID_INPUT - } - - fn device_name(&self) -> &str { - "input" - } - - fn queue_config(&self) -> &[QueueConfig] { - &defs::QUEUE_CONFIG - } - - fn read_config(&self, offset: u64, mut data: &mut [u8]) { - let cfg_slice = self.cfg.bytes(); - let cfg_len = cfg_slice.len() as u64; - - if offset >= cfg_len { - error!("Failed to read config space"); - return; - } - if let Some(end) = offset.checked_add(data.len() as u64) { - // This write can't fail, offset and end are checked against config_len. - data.write_all(&cfg_slice[offset as usize..cmp::min(end, cfg_len) as usize]) - .unwrap(); - } - } - - fn write_config(&mut self, offset: u64, data: &[u8]) { - let len = data.len() as u64; - - let mut select = self.cfg.select(); - let mut subsel = self.cfg.subsel(); - - if offset == 0 && len >= 1 { - select = data[0]; - if len >= 2 { - subsel = data[1] - } - } else if offset == 1 && len >= 1 { - subsel = data[0] - } - - self.cfg - .update_select(&self.config_instance, select, subsel); - } - - fn activate( - &mut self, - mem: GuestMemoryMmap, - interrupt: InterruptTransport, - queues: Vec, - ) -> ActivateResult { - let [event_q, status_q]: [_; defs::NUM_QUEUES] = queues.try_into().map_err(|_| { - error!( - "Cannot perform activate. Expected {} queue(s)", - defs::NUM_QUEUES - ); - ActivateError::BadActivate - })?; - - let worker = InputWorker::new( - event_q, - status_q, - interrupt.clone(), - mem.clone(), - self.event_provider_backend, - self.worker_stopfd.try_clone().unwrap(), - ); - - self.worker_thread = Some(worker.run()); - - self.device_state = DeviceState::Activated(mem, interrupt); - Ok(()) - } - - fn is_activated(&self) -> bool { - self.device_state.is_activated() - } - - fn reset(&mut self) -> bool { - if let Some(worker_thread) = self.worker_thread.take() { - self.worker_stopfd.write(1).unwrap(); - - match worker_thread.join() { - Ok(()) => debug!("Input worker thread stopped"), - Err(e) => { - error!("Failed to join worker thread: {e:?}"); - } - } - } - true - } -} diff --git a/vendor/krun-devices/src/virtio/input/mod.rs b/vendor/krun-devices/src/virtio/input/mod.rs deleted file mode 100644 index 8f0571a6d..000000000 --- a/vendor/krun-devices/src/virtio/input/mod.rs +++ /dev/null @@ -1,64 +0,0 @@ -mod device; -pub mod passthrough; -mod worker; - -pub use self::defs::uapi::VIRTIO_ID_INPUT as TYPE_INPUT; -pub use self::device::Input; - -use super::QueueConfig; - -mod defs { - use super::QueueConfig; - - pub const INPUT_DEV_ID: &str = "virtio_input"; - pub const NUM_QUEUES: usize = 2; - - const QUEUE_SIZE: u16 = 256; - pub static QUEUE_CONFIG: [QueueConfig; NUM_QUEUES] = [QueueConfig::new(QUEUE_SIZE); NUM_QUEUES]; - - pub mod uapi { - pub const VIRTIO_F_VERSION_1: u32 = 32; - pub const VIRTIO_ID_INPUT: u32 = 18; - } - - pub mod config_select { - pub const VIRTIO_INPUT_CFG_UNSET: u8 = 0x00; - pub const VIRTIO_INPUT_CFG_ID_NAME: u8 = 0x01; - pub const VIRTIO_INPUT_CFG_ID_SERIAL: u8 = 0x02; - pub const VIRTIO_INPUT_CFG_ID_DEVIDS: u8 = 0x03; - pub const VIRTIO_INPUT_CFG_PROP_BITS: u8 = 0x10; - pub const VIRTIO_INPUT_CFG_EV_BITS: u8 = 0x11; - pub const VIRTIO_INPUT_CFG_ABS_INFO: u8 = 0x12; - } -} - -#[derive(Debug)] -pub enum InputError { - /// Failed to create event fd. - EventFd(std::io::Error), - - /// Backend error - BackendError(String), - - SendNotificationFailed, - - EventFdError, - - HandleEventNotEpollIn, - - HandleEventUnknownEvent, - - UnexpectedConfig(u8), - - UnexpectedFetchEventError, - - UnexpectedDescriptorCount(usize), - - UnexpectedInputDeviceError, - - UnexpectedWriteDescriptorError, - - UnexpectedWriteVringError, -} - -type Result = std::result::Result; diff --git a/vendor/krun-devices/src/virtio/input/passthrough.rs b/vendor/krun-devices/src/virtio/input/passthrough.rs deleted file mode 100644 index 0f6b434da..000000000 --- a/vendor/krun-devices/src/virtio/input/passthrough.rs +++ /dev/null @@ -1,211 +0,0 @@ -use krun_input::{ - InputAbsInfo, InputBackendError, InputDeviceIds, InputEvent, InputEventsImpl, InputQueryConfig, - ObjectNew, -}; -use nix::fcntl::{fcntl, OFlag, F_GETFL, F_SETFL}; -use nix::{errno::Errno, ioctl_read, ioctl_read_buf, unistd}; -use std::mem; -use std::os::fd::{AsFd, AsRawFd, BorrowedFd, RawFd}; - -/// Internal passthrough input backend that forwards host /dev/input/* devices -pub struct PassthroughInputBackend { - fd: BorrowedFd<'static>, -} - -impl InputQueryConfig for PassthroughInputBackend { - fn query_serial_name(&self, serial_buf: &mut [u8]) -> Result { - match unsafe { eviocguniq(self.fd.as_raw_fd(), serial_buf) } { - Ok(len) => Ok(len as u8), - Err(e) => { - error!("Failed to get device serial (eviocguniq): {e}"); - Err(InputBackendError::InternalError) - } - } - } - - fn query_device_name(&self, name_buf: &mut [u8]) -> Result { - match unsafe { eviocgname(self.fd.as_raw_fd(), name_buf) } { - Ok(len) => Ok(len as u8), - Err(e) => { - error!("Failed to get device name (eviocgname): {e}"); - Err(InputBackendError::InternalError) - } - } - } - - fn query_device_ids(&self, ids: &mut InputDeviceIds) -> Result<(), InputBackendError> { - match unsafe { eviocgid(self.fd.as_raw_fd(), ids) } { - Ok(_) => Ok(()), - Err(e) => { - error!("Failed to get device information ids (eviocgid): {e}"); - Err(InputBackendError::InternalError) - } - } - } - - fn query_event_capabilities( - &self, - event_type: u8, - bitmap_buf: &mut [u8], - ) -> Result { - match unsafe { eviocgbit(self.fd.as_raw_fd(), event_type, bitmap_buf) } { - Ok(n) => { - let len = find_length(&bitmap_buf[..n as usize]) as u8; - debug!( - "eviocgbit: {event_type}, got {n} bytes (from n): {:#?}", - &bitmap_buf[..n as usize] - ); - Ok(len) - } - Err(e) => { - error!("Failed to get device event capabilities (eviocgbit): {e}"); - Err(InputBackendError::InternalError) - } - } - } - - fn query_abs_info( - &self, - abs_axis: u8, - abs_info: &mut InputAbsInfo, - ) -> Result<(), InputBackendError> { - let mut linux_abs_info = LinuxAbsInfo::default(); - match unsafe { eviocgabs(self.fd.as_raw_fd(), abs_axis, &mut linux_abs_info) } { - Ok(_) => { - *abs_info = InputAbsInfo { - min: linux_abs_info.minimum, - max: linux_abs_info.maximum, - fuzz: linux_abs_info.fuzz, - flat: linux_abs_info.flat, - res: linux_abs_info.resolution, - }; - Ok(()) - } - Err(e) => { - error!("Failed to get device abs_info (eviocgabs): {e}"); - Err(InputBackendError::InternalError) - } - } - } - - fn query_properties(&self, properties: &mut [u8]) -> Result { - match unsafe { eviocgprop(self.fd.as_raw_fd(), properties) } { - Ok(len) => Ok(len as u8), - Err(e) => { - error!("Failed to query device properties (eviocgprop): {e}"); - Err(InputBackendError::InternalError) - } - } - } -} - -impl ObjectNew> for PassthroughInputBackend { - fn new(userdata: Option<&BorrowedFd<'static>>) -> Self { - let fd = userdata - .copied() - .expect("Missing argument for PassthroughInputBackend::new"); - - make_non_blocking(&fd) - .expect("Cannot make device fd non-blocking (Invalid file descriptor?)"); - Self { fd } - } -} - -impl InputEventsImpl for PassthroughInputBackend { - fn get_read_notify_fd(&self) -> Result, InputBackendError> { - Ok(self.fd) - } - - fn next_event(&mut self) -> Result, InputBackendError> { - let mut linux_event = unsafe { std::mem::zeroed::() }; - let event_slice = unsafe { - std::slice::from_raw_parts_mut( - &mut linux_event as *mut _ as *mut u8, - size_of::(), - ) - }; - - match unistd::read(self.fd, event_slice) { - Ok(bytes_read) if bytes_read == size_of::() => { - trace!("Forwarding input: {linux_event:?}"); - Ok(Some(InputEvent { - type_: linux_event.type_, - code: linux_event.code, - value: linux_event.value, - })) - } - Ok(_bytes_read) => { - error!("Partial read from /dev/input was unexpected, not implemented!"); - Err(InputBackendError::InternalError) - } - Err(Errno::EAGAIN) => Ok(None), - Err(e) => { - error!("Failed to read event from input device: {e}"); - Err(InputBackendError::InternalError) - } - } - } -} - -#[repr(C)] -#[derive(Debug)] -struct LinuxInputEvent { - time: libc::timeval, - type_: u16, - code: u16, - value: u32, -} - -#[repr(C)] -#[derive(Debug, Default)] -struct LinuxAbsInfo { - value: u32, - minimum: u32, - maximum: u32, - fuzz: u32, - flat: u32, - resolution: u32, -} - -ioctl_read!(eviocgid, b'E', 0x02, InputDeviceIds); // Kernel uapi struct is the same as virtio -ioctl_read_buf!(eviocgname, b'E', 0x06, u8); -ioctl_read_buf!(eviocguniq, b'E', 0x08, u8); -ioctl_read_buf!(eviocgprop, b'E', 0x09, u8); - -unsafe fn eviocgbit(fd: RawFd, evt: u8, buf: &mut [u8]) -> Result { - let ioctl_num = nix::request_code_read!(b'E', 0x20 + evt, buf.len()); - - let n = libc::ioctl(fd, ioctl_num as _, buf.as_mut_ptr()); - if n < 0 { - return Err(Errno::last()); - } - Ok(n as u32) -} - -unsafe fn eviocgabs(fd: RawFd, axis: u8, abs_info: &mut LinuxAbsInfo) -> Result { - let ioctl_num = nix::request_code_read!(b'E', 0x40 + axis, size_of::()); - - let n = libc::ioctl(fd, ioctl_num as _, abs_info as *mut _); - if n < 0 { - return Err(Errno::last()); - } - Ok(mem::size_of::() as u32) -} - -fn make_non_blocking(fd: &impl AsFd) -> Result<(), nix::Error> { - let flags = fcntl(fd, F_GETFL)?; - fcntl( - fd, - F_SETFL(OFlag::from_bits_retain(flags) | OFlag::O_NONBLOCK), - )?; - - Ok(()) -} - -fn find_length(bytes: &[u8]) -> usize { - bytes - .iter() - .rposition(|b| *b != 0) - .map(|idx| idx + 1) - .unwrap_or(0) -} diff --git a/vendor/krun-devices/src/virtio/input/worker.rs b/vendor/krun-devices/src/virtio/input/worker.rs deleted file mode 100644 index bcde12ea6..000000000 --- a/vendor/krun-devices/src/virtio/input/worker.rs +++ /dev/null @@ -1,280 +0,0 @@ -use log::{debug, error}; -use std::io; -use std::io::Read; -use std::os::fd::AsRawFd; -use std::thread::{self, JoinHandle}; -use utils::epoll::{ControlOperation, Epoll, EpollEvent, EventSet}; -use utils::eventfd::EventFd; -use virtio_bindings::virtio_input; -use vm_memory::{ByteValued, GuestMemoryMmap}; - -use super::super::DeviceQueue; -use crate::virtio::descriptor_utils::{Reader, Writer}; -use crate::virtio::InterruptTransport; -use krun_input::{InputEventProviderBackend, InputEventProviderInstance, InputEventsImpl}; - -// Create a wrapper type to work around orphan rules -#[repr(C)] -#[derive(Copy, Clone, Debug)] -struct VirtioInputEvent { - type_: u16, - code: u16, - value: i32, -} - -unsafe impl ByteValued for VirtioInputEvent {} - -pub struct InputWorker { - event_q: DeviceQueue, // Device -> Guest events - status_q: DeviceQueue, // Guest -> Device events - interrupt: InterruptTransport, - mem: GuestMemoryMmap, - backend_wrapper: InputEventProviderBackend<'static>, - stop_fd: EventFd, -} - -impl InputWorker { - pub fn new( - event_q: DeviceQueue, - status_q: DeviceQueue, - interrupt: InterruptTransport, - mem: GuestMemoryMmap, - backend: InputEventProviderBackend<'static>, - stop_fd: EventFd, - ) -> Self { - Self { - event_q, - status_q, - interrupt, - mem, - backend_wrapper: backend, - stop_fd, - } - } - - pub fn run(self) -> JoinHandle<()> { - thread::Builder::new() - .name("input worker".into()) - .spawn(|| self.work()) - .unwrap() - } - - fn work(mut self) { - debug!("input worker: starting"); - - // Create the events instance in this thread - let mut events_instance = match self.backend_wrapper.create_instance() { - Ok(instance) => instance, - Err(e) => { - error!("Failed to create events instance: {:?}", e); - return; - } - }; - - const EVENTQ: u64 = 1; - const STATUSQ: u64 = 2; - const EVENTQ_USER: u64 = 3; - const QUIT: u64 = 4; - // Set up epoll to wait for events - let epoll = Epoll::new().expect("Failed to create epoll"); - - let ready_fd = match events_instance.get_read_notify_fd() { - Ok(fd) => fd, - Err(e) => { - error!("Failed to get ready fd: {:?}", e); - return; - } - }; - - epoll - .ctl( - ControlOperation::Add, - ready_fd.as_raw_fd(), - &EpollEvent::new(EventSet::IN, EVENTQ_USER), - ) - .expect("Failed to add ready fd to epoll"); - epoll - .ctl( - ControlOperation::Add, - self.event_q.event.as_raw_fd(), - &EpollEvent::new(EventSet::IN, EVENTQ), - ) - .expect("Failed to add ready fd to epoll"); - epoll - .ctl( - ControlOperation::Add, - self.status_q.event.as_raw_fd(), - &EpollEvent::new(EventSet::IN, STATUSQ), - ) - .expect("Failed to add ready fd to epoll"); - epoll - .ctl( - ControlOperation::Add, - self.stop_fd.as_raw_fd(), - &EpollEvent::new(EventSet::IN, QUIT), - ) - .expect("Failed to add stop fd to epoll"); - - let mut events = vec![EpollEvent::default(); 16]; - - 'event_loop: loop { - let num_events = match epoll.wait(events.len(), 1000, &mut events) { - Ok(n) => n, - Err(e) => { - error!("Epoll wait failed: {:?}", e); - break; - } - }; - - let mut needs_interrupt = false; - - for event in &events[..num_events] { - match event.data() { - EVENTQ_USER => { - trace!("EVENTQ_USER"); - needs_interrupt |= self.process_event_queue(&mut events_instance); - } - EVENTQ => { - self.event_q.event.read().unwrap(); - trace!("EVENTQ"); - needs_interrupt |= self.process_event_queue(&mut events_instance); - } - STATUSQ => { - self.status_q.event.read().unwrap(); - needs_interrupt |= self.process_status_queue(); - } - QUIT => { - // Stop signal received - let _ = self.stop_fd.read(); - break 'event_loop; - } - x => { - error!("TODO: {x}") - } - } - if needs_interrupt { - self.interrupt.signal_used_queue(); - } - } - } - - debug!("input worker: stopping"); - } - - /// Fills a virtqueue with events from the source. Returns the number of bytes written. - fn fill_event_virtqueue( - &mut self, - events_instance: &mut InputEventProviderInstance, - writer: &mut Writer, - ) -> Result<(usize, bool), ()> { - let avail_bytes = writer.available_bytes(); - let mut eof = false; - while writer.bytes_written() + size_of::() <= avail_bytes { - match events_instance.next_event() { - Ok(Some(event)) => { - let virtio_event = VirtioInputEvent { - type_: event.type_, - code: event.code, - value: event.value as i32, - }; - debug!("Writing: {virtio_event:?}"); - writer - .write_obj(virtio_event) - .expect("Failed to write input event to virtqueue"); - } - // No more events available - Ok(None) => { - eof = true; - break; - } - Err(e) => { - error!("Error getting next event: {:?}", e); - eof = true; - break; - } - } - } - Ok((writer.bytes_written(), eof)) - } - - fn process_event_queue(&mut self, events_instance: &mut InputEventProviderInstance) -> bool { - let mut needs_interrupt = false; - let mem = self.mem.clone(); - - while let Some(desc_chain) = self.event_q.queue.pop(&mem) { - let mut writer = match Writer::new(&mem, desc_chain.clone()) { - Ok(w) => w, - Err(e) => { - error!("Failed to create writer: {:?}", e); - break; - } - }; - - let (bytes_written, eof) = self - .fill_event_virtqueue(events_instance, &mut writer) - .unwrap(); - - if bytes_written != 0 { - self.event_q - .queue - .add_used(&mem, desc_chain.index, bytes_written as u32) - .expect("TODO"); - needs_interrupt = true; - } - - if bytes_written == 0 { - self.event_q.queue.undo_pop(); - break; - } - - if eof { - break; - } - } - needs_interrupt - } - - /// Reads events from guest and sends them to the event source (currently no-op) - fn read_status_virtqueue(&mut self, reader: &mut Reader) -> Result { - while reader.available_bytes() >= size_of::() { - let mut buffer: [u8; size_of::()] = - [0; size_of::()]; - reader.read_exact(&mut buffer)?; - debug!("Not implemented status queue request: {:?}", &buffer); - // For now, we don't send events back to the input source - // This would be used for things like setting LEDs on keyboards, haptic feedback, etc. - } - Ok(reader.bytes_read()) - } - - /// Process the status queue (guest -> device events) - fn process_status_queue(&mut self) -> bool { - let mut needs_interrupt = false; - let mem = self.mem.clone(); - - while let Some(desc_chain) = self.status_q.queue.pop(&mem) { - let mut reader = match Reader::new(&mem, desc_chain.clone()) { - Ok(r) => r, - Err(e) => { - error!("Failed to create reader for status queue: {e}"); - return false; - } - }; - match self.read_status_virtqueue(&mut reader) { - Ok(bytes_read) => { - self.status_q - .queue - .add_used(&mem, desc_chain.index, bytes_read as u32) - .unwrap(); - } - Err(e) => { - error!("Input: failed to read events from virtqueue: {:?}", e); - } - } - - needs_interrupt = true; - } - - needs_interrupt - } -} diff --git a/vendor/krun-devices/src/virtio/linux_errno.rs b/vendor/krun-devices/src/virtio/linux_errno.rs deleted file mode 100644 index 105f977b5..000000000 --- a/vendor/krun-devices/src/virtio/linux_errno.rs +++ /dev/null @@ -1,219 +0,0 @@ -const LINUX_EPERM: i32 = 1; -const LINUX_ENOENT: i32 = 2; -const LINUX_ESRCH: i32 = 3; -const LINUX_EINTR: i32 = 4; -const LINUX_EIO: i32 = 5; -const LINUX_ENXIO: i32 = 6; -const LINUX_ENOEXEC: i32 = 8; -const LINUX_EBADF: i32 = 9; -const LINUX_ECHILD: i32 = 10; -const LINUX_EAGAIN: i32 = 11; -const LINUX_ENOMEM: i32 = 12; -const LINUX_EACCES: i32 = 13; -const LINUX_EFAULT: i32 = 14; -const LINUX_ENOTBLK: i32 = 15; -const LINUX_EBUSY: i32 = 16; -const LINUX_EEXIST: i32 = 17; -const LINUX_EXDEV: i32 = 18; -const LINUX_ENODEV: i32 = 19; -const LINUX_ENOTDIR: i32 = 20; -const LINUX_EISDIR: i32 = 21; -const LINUX_EINVAL: i32 = 22; -const LINUX_ENFILE: i32 = 23; -const LINUX_EMFILE: i32 = 24; -const LINUX_ENOTTY: i32 = 25; -const LINUX_ETXTBSY: i32 = 26; -const LINUX_EFBIG: i32 = 27; -const LINUX_ENOSPC: i32 = 28; -const LINUX_ESPIPE: i32 = 29; -const LINUX_EROFS: i32 = 30; -const LINUX_EMLINK: i32 = 31; -const LINUX_EPIPE: i32 = 32; -const LINUX_EDOM: i32 = 33; -const LINUX_EDEADLK: i32 = 35; -const LINUX_ENAMETOOLONG: i32 = 36; -const LINUX_ENOLCK: i32 = 37; -const LINUX_ENOSYS: i32 = 38; -const LINUX_ENOTEMPTY: i32 = 39; -const LINUX_ELOOP: i32 = 40; -const LINUX_ENOMSG: i32 = 42; -const LINUX_EIDRM: i32 = 43; -const LINUX_ENOSTR: i32 = 60; -const LINUX_ENODATA: i32 = 61; -const LINUX_ETIME: i32 = 62; -const LINUX_ENOSR: i32 = 63; -const LINUX_EREMOTE: i32 = 66; -const LINUX_ENOLINK: i32 = 67; -const LINUX_EPROTO: i32 = 71; -const LINUX_EMULTIHOP: i32 = 72; -const LINUX_EBADMSG: i32 = 74; -const LINUX_EOVERFLOW: i32 = 75; -const LINUX_EILSEQ: i32 = 84; -const LINUX_EUSERS: i32 = 87; -const LINUX_ENOTSOCK: i32 = 88; -const LINUX_EDESTADDRREQ: i32 = 89; -const LINUX_EMSGSIZE: i32 = 90; -const LINUX_EPROTOTYPE: i32 = 91; -const LINUX_ENOPROTOOPT: i32 = 92; -const LINUX_EPROTONOSUPPORT: i32 = 93; -const LINUX_ESOCKTNOSUPPORT: i32 = 94; -const LINUX_EOPNOTSUPP: i32 = 95; -const LINUX_EPFNOSUPPORT: i32 = 96; -const LINUX_EAFNOSUPPORT: i32 = 97; -const LINUX_EADDRINUSE: i32 = 98; -const LINUX_EADDRNOTAVAIL: i32 = 99; -const LINUX_ENETDOWN: i32 = 100; -const LINUX_ENETUNREACH: i32 = 101; -const LINUX_ENETRESET: i32 = 102; -const LINUX_ECONNABORTED: i32 = 103; -const LINUX_ECONNRESET: i32 = 104; -const LINUX_ENOBUFS: i32 = 105; -const LINUX_EISCONN: i32 = 106; -const LINUX_ENOTCONN: i32 = 107; -const LINUX_ESHUTDOWN: i32 = 108; -const LINUX_ETOOMANYREFS: i32 = 109; -const LINUX_ETIMEDOUT: i32 = 110; -const LINUX_ECONNREFUSED: i32 = 111; -const LINUX_EHOSTDOWN: i32 = 112; -const LINUX_EHOSTUNREACH: i32 = 113; -const LINUX_EALREADY: i32 = 114; -const LINUX_EINPROGRESS: i32 = 115; -const LINUX_ESTALE: i32 = 116; -const LINUX_EDQUOT: i32 = 122; -const LINUX_ECANCELED: i32 = 125; -const LINUX_EOWNERDEAD: i32 = 130; -const LINUX_ENOTRECOVERABLE: i32 = 131; - -// Errors to be directly used. -pub const LINUX_ERANGE: i32 = 34; - -pub fn linux_error(error: std::io::Error) -> std::io::Error { - std::io::Error::from_raw_os_error(linux_errno_raw(error.raw_os_error().unwrap_or(libc::EIO))) -} - -pub fn linux_errno_raw(errno: i32) -> i32 { - match errno { - libc::EPERM => LINUX_EPERM, - libc::ENOENT => LINUX_ENOENT, - libc::ESRCH => LINUX_ESRCH, - libc::EINTR => LINUX_EINTR, - libc::EIO => LINUX_EIO, - libc::ENXIO => LINUX_ENXIO, - libc::ENOEXEC => LINUX_ENOEXEC, - libc::EBADF => LINUX_EBADF, - libc::ECHILD => LINUX_ECHILD, - libc::EDEADLK => LINUX_EDEADLK, - libc::ENOMEM => LINUX_ENOMEM, - libc::EACCES => LINUX_EACCES, - libc::EFAULT => LINUX_EFAULT, - libc::ENOTBLK => LINUX_ENOTBLK, - libc::EBUSY => LINUX_EBUSY, - libc::EEXIST => LINUX_EEXIST, - libc::EXDEV => LINUX_EXDEV, - libc::ENODEV => LINUX_ENODEV, - libc::ENOTDIR => LINUX_ENOTDIR, - libc::EISDIR => LINUX_EISDIR, - libc::EINVAL => LINUX_EINVAL, - libc::ENFILE => LINUX_ENFILE, - libc::EMFILE => LINUX_EMFILE, - libc::ENOTTY => LINUX_ENOTTY, - libc::ETXTBSY => LINUX_ETXTBSY, - libc::EFBIG => LINUX_EFBIG, - libc::ENOSPC => LINUX_ENOSPC, - libc::ESPIPE => LINUX_ESPIPE, - libc::EROFS => LINUX_EROFS, - libc::EMLINK => LINUX_EMLINK, - libc::EPIPE => LINUX_EPIPE, - libc::EDOM => LINUX_EDOM, - libc::EAGAIN => LINUX_EAGAIN, - libc::EINPROGRESS => LINUX_EINPROGRESS, - libc::EALREADY => LINUX_EALREADY, - libc::ENOTSOCK => LINUX_ENOTSOCK, - libc::EDESTADDRREQ => LINUX_EDESTADDRREQ, - libc::EMSGSIZE => LINUX_EMSGSIZE, - libc::EPROTOTYPE => LINUX_EPROTOTYPE, - libc::ENOPROTOOPT => LINUX_ENOPROTOOPT, - libc::EPROTONOSUPPORT => LINUX_EPROTONOSUPPORT, - libc::ESOCKTNOSUPPORT => LINUX_ESOCKTNOSUPPORT, - libc::EPFNOSUPPORT => LINUX_EPFNOSUPPORT, - libc::EAFNOSUPPORT => LINUX_EAFNOSUPPORT, - libc::EADDRINUSE => LINUX_EADDRINUSE, - libc::EADDRNOTAVAIL => LINUX_EADDRNOTAVAIL, - libc::ENETDOWN => LINUX_ENETDOWN, - libc::ENETUNREACH => LINUX_ENETUNREACH, - libc::ENETRESET => LINUX_ENETRESET, - libc::ECONNABORTED => LINUX_ECONNABORTED, - libc::ECONNRESET => LINUX_ECONNRESET, - libc::ENOBUFS => LINUX_ENOBUFS, - libc::EISCONN => LINUX_EISCONN, - libc::ENOTCONN => LINUX_ENOTCONN, - libc::ESHUTDOWN => LINUX_ESHUTDOWN, - libc::ETOOMANYREFS => LINUX_ETOOMANYREFS, - libc::ETIMEDOUT => LINUX_ETIMEDOUT, - libc::ECONNREFUSED => LINUX_ECONNREFUSED, - libc::ELOOP => LINUX_ELOOP, - libc::ENAMETOOLONG => LINUX_ENAMETOOLONG, - libc::EHOSTDOWN => LINUX_EHOSTDOWN, - libc::EHOSTUNREACH => LINUX_EHOSTUNREACH, - libc::ENOTEMPTY => LINUX_ENOTEMPTY, - libc::EUSERS => LINUX_EUSERS, - libc::EDQUOT => LINUX_EDQUOT, - libc::ESTALE => LINUX_ESTALE, - libc::EREMOTE => LINUX_EREMOTE, - libc::ENOLCK => LINUX_ENOLCK, - libc::ENOSYS => LINUX_ENOSYS, - libc::EOVERFLOW => LINUX_EOVERFLOW, - libc::ECANCELED => LINUX_ECANCELED, - libc::EIDRM => LINUX_EIDRM, - libc::ENOMSG => LINUX_ENOMSG, - libc::EILSEQ => LINUX_EILSEQ, - #[cfg(target_os = "macos")] - libc::ENOATTR => LINUX_ENODATA, - libc::EBADMSG => LINUX_EBADMSG, - libc::EMULTIHOP => LINUX_EMULTIHOP, - libc::ENODATA => LINUX_ENODATA, - libc::ENOLINK => LINUX_ENOLINK, - libc::ENOSR => LINUX_ENOSR, - libc::ENOSTR => LINUX_ENOSTR, - libc::EPROTO => LINUX_EPROTO, - libc::ETIME => LINUX_ETIME, - libc::EOPNOTSUPP => LINUX_EOPNOTSUPP, - libc::ENOTRECOVERABLE => LINUX_ENOTRECOVERABLE, - libc::EOWNERDEAD => LINUX_EOWNERDEAD, - _ => LINUX_EIO, - } -} - -// Helper functions returning io::Error with Linux errno values. -use std::io; - -pub fn eperm() -> io::Error { - io::Error::from_raw_os_error(LINUX_EPERM) -} -pub fn enoent() -> io::Error { - io::Error::from_raw_os_error(LINUX_ENOENT) -} -pub fn eacces() -> io::Error { - io::Error::from_raw_os_error(LINUX_EACCES) -} -pub fn eexist() -> io::Error { - io::Error::from_raw_os_error(LINUX_EEXIST) -} -pub fn einval() -> io::Error { - io::Error::from_raw_os_error(LINUX_EINVAL) -} -pub fn eisdir() -> io::Error { - io::Error::from_raw_os_error(LINUX_EISDIR) -} -pub fn exdev() -> io::Error { - io::Error::from_raw_os_error(LINUX_EXDEV) -} -pub fn enosys() -> io::Error { - io::Error::from_raw_os_error(LINUX_ENOSYS) -} -pub fn enodata() -> io::Error { - io::Error::from_raw_os_error(LINUX_ENODATA) -} -pub fn enxio() -> io::Error { - io::Error::from_raw_os_error(LINUX_ENXIO) -} diff --git a/vendor/krun-devices/src/virtio/mmio.rs b/vendor/krun-devices/src/virtio/mmio.rs deleted file mode 100644 index 319af15c2..000000000 --- a/vendor/krun-devices/src/virtio/mmio.rs +++ /dev/null @@ -1,1057 +0,0 @@ -// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -// -// Portions Copyright 2017 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the THIRD-PARTY file. - -use std::fmt::{Display, Formatter}; -use std::io; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex, MutexGuard}; - -use utils::eventfd::EFD_NONBLOCK; -use virtio_bindings::virtio_ring::VIRTIO_RING_F_EVENT_IDX; - -use super::device_status; -use super::*; -use crate::bus::BusDevice; -use crate::legacy::IrqChip; -use utils::{byte_order, eventfd::EventFd}; -use vm_memory::{GuestAddress, GuestMemoryMmap}; - -//TODO crosvm uses 0 here, but IIRC virtio specified some other vendor id that should be used -const VENDOR_ID: u32 = 0; - -//required by the virtio mmio device register layout at offset 0 from base -const MMIO_MAGIC_VALUE: u32 = 0x7472_6976; - -//current version specified by the mmio standard (legacy devices used 1 here) -const MMIO_VERSION: u32 = 2; - -#[derive(Debug)] -pub enum CreateMmioTransportError { - CreateInterruptEventFd(io::Error), -} - -impl Display for CreateMmioTransportError { - fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { - match self { - CreateMmioTransportError::CreateInterruptEventFd(err) => { - write!(f, "failed to create interrupt eventfd: {err}") - } - } - } -} - -/// Implements the -/// [MMIO](http://docs.oasis-open.org/virtio/virtio/v1.0/cs04/virtio-v1.0-cs04.html#x1-1090002) -/// transport for virtio devices. -/// -/// This requires 3 points of installation to work with a VM: -/// -/// 1. Mmio reads and writes must be sent to this device at what is referred to here as MMIO base. -/// 1. `Mmio::queue_evts` must be installed at `virtio::NOTIFY_REG_OFFSET` offset from the MMIO -/// base. Each event in the array must be signaled if the index is written at that offset. -/// 1. `Mmio::interrupt_evt` must signal an interrupt that the guest driver is listening to when it -/// is written to. -/// -/// Typically one page (4096 bytes) of MMIO address space is sufficient to handle this transport -/// and inner virtio device. -pub struct MmioTransport { - device: Arc>, - // The register where feature bits are stored. - pub(crate) features_select: u32, - // The register where features page is selected. - pub(crate) acked_features_select: u32, - pub(crate) queue_select: u32, - pub(crate) device_status: u32, - pub(crate) config_generation: u32, - mem: GuestMemoryMmap, - // Queues owned by the transport during negotiation. - // These are moved to the device on activation. - queues: Option>, - // Queue eventfds - kept by transport to send notifications. - // Arc clones are passed to the device on activation. - queue_evts: Vec>, - // Stored queue config from device for recreating queues after reset. - queue_config: Vec, - shm_region_select: u32, - interrupt: InterruptTransport, -} - -struct InterruptTransportInner { - log_target: String, - status: AtomicUsize, - event: EventFd, - intc: IrqChip, - irq_line: Option, -} - -#[derive(Clone)] -pub struct InterruptTransport(Arc); - -impl InterruptTransport { - pub fn new(intc: IrqChip, log_target: String) -> Result { - Ok(Self(Arc::new(InterruptTransportInner { - log_target, - status: AtomicUsize::new(0), - event: EventFd::new(0).map_err(CreateMmioTransportError::CreateInterruptEventFd)?, - intc, - irq_line: None, - }))) - } - - pub fn status(&self) -> &AtomicUsize { - &self.0.status - } - - pub fn event(&self) -> &EventFd { - &self.0.event - } - - pub fn intc(&self) -> &IrqChip { - &self.0.intc - } - - pub fn irq_line(&self) -> Option { - self.0.irq_line - } - - fn set_irq_line(&mut self, irq_line: u32) { - debug!(target: &self.0.log_target, "set_irq_line: {irq_line}"); - match Arc::get_mut(&mut self.0) { - None => { - error!("Cannot change irq_line of activated device"); - } - Some(interrupt) => { - interrupt.irq_line = Some(irq_line); - } - } - } - - fn try_signal(&self, status: u32) -> Result<(), crate::Error> { - self.status().fetch_or(status as usize, Ordering::SeqCst); - self.intc() - .lock() - .unwrap() - .set_irq(self.0.irq_line, Some(&self.0.event))?; - Ok(()) - } - - pub fn try_signal_used_queue(&self) -> Result<(), crate::Error> { - debug!(target: &self.0.log_target, "interrupt: signal_used_queue"); - self.try_signal(VIRTIO_MMIO_INT_VRING) - } - - pub fn try_signal_config_change(&self) -> Result<(), crate::Error> { - debug!(target: &self.0.log_target, "interrupt: signal_config_change"); - self.try_signal(VIRTIO_MMIO_INT_CONFIG) - } - - pub fn signal_used_queue(&self) { - if let Err(e) = self.try_signal_used_queue() { - warn!(target: &self.0.log_target, "Failed to signal used queue: {e:?}"); - } - } - - pub fn signal_config_change(&self) { - if let Err(e) = self.try_signal_config_change() { - warn!(target: &self.0.log_target, "Failed to signal config change: {e:?}"); - } - } -} - -impl MmioTransport { - /// Constructs a new MMIO transport for the given virtio device. - pub fn new( - mem: GuestMemoryMmap, - intc: IrqChip, - device: Arc>, - ) -> Result { - let locked = device - .try_lock() - .expect("Mutex of VirtioDevice should not be locked when calling MmioTransport::new"); - - let debug_log_target = format!("{}[{}]", module_path!(), locked.device_name()); - let queue_config: Vec = locked.queue_config().to_vec(); - drop(locked); - - let queues = Self::create_queues(&queue_config); - let queue_evts = Self::create_queue_evts(queue_config.len())?; - - Ok(MmioTransport { - interrupt: InterruptTransport::new(intc, debug_log_target)?, - device, - features_select: 0, - acked_features_select: 0, - queue_select: 0, - device_status: device_status::INIT, - config_generation: 0, - mem, - queues: Some(queues), - queue_evts, - queue_config, - shm_region_select: 0, - }) - } - - /// Create queues from queue configuration. - fn create_queues(queue_config: &[QueueConfig]) -> Vec { - queue_config.iter().map(|c| Queue::new(c.size)).collect() - } - - /// Create eventfds for queue notifications. - fn create_queue_evts(count: usize) -> Result>, CreateMmioTransportError> { - let mut queue_evts = Vec::with_capacity(count); - for _ in 0..count { - queue_evts.push(Arc::new( - EventFd::new(EFD_NONBLOCK) - .map_err(CreateMmioTransportError::CreateInterruptEventFd)?, - )); - } - Ok(queue_evts) - } - - /// Set the irq line for the device. - /// NOTE: Can only be called when the device is not activated - pub fn set_irq_line(&mut self, irq_line: u32) { - self.interrupt.set_irq_line(irq_line); - } - - pub fn interrupt_evt(&self) -> &EventFd { - self.interrupt.event() - } - - pub fn locked_device(&self) -> MutexGuard<'_, dyn VirtioDevice + 'static> { - self.device.lock().expect("Poisoned device lock") - } - - // Gets the encapsulated VirtioDevice. - pub fn device(&self) -> Arc> { - self.device.clone() - } - - /// Returns a reference to the queue eventfds. Used by the VMM to register - /// queue notifications with KVM. - pub fn queue_evts(&self) -> &[Arc] { - &self.queue_evts - } - - fn check_device_status(&self, set: u32, clr: u32) -> bool { - self.device_status & (set | clr) == set - } - - fn with_queue(&self, d: U, f: F) -> U - where - F: FnOnce(&Queue) -> U, - { - match &self.queues { - Some(queues) => match queues.get(self.queue_select as usize) { - Some(queue) => f(queue), - None => d, - }, - None => d, - } - } - - fn with_queue_mut(&mut self, f: F) -> bool { - match &mut self.queues { - Some(queues) => { - if let Some(queue) = queues.get_mut(self.queue_select as usize) { - f(queue); - true - } else { - false - } - } - None => false, - } - } - - fn update_queue_field(&mut self, f: F) { - if self.check_device_status(device_status::FEATURES_OK, device_status::FAILED) { - // FIXME: check if activated! - self.with_queue_mut(f); - } else { - warn!( - "update virtio queue in invalid state 0x{:x}", - self.device_status - ); - } - } - - fn reset(&mut self) { - if self.locked_device().is_activated() { - debug!("reset device while it's still in active state"); - } - self.features_select = 0; - self.acked_features_select = 0; - self.queue_select = 0; - self.interrupt.0.status.store(0, Ordering::SeqCst); - self.device_status = device_status::INIT; - // Do not reset config_generation and keep it monotonically increasing. - // Recreate queues from queue_config for the next negotiation cycle. - // Keep queue_evts as is - they are reused across reset cycles. - // TODO: consider resting the events when we refactor event handling - self.queues = Some(Self::create_queues(&self.queue_config)); - // . Do not reset config_generation and keep it monotonically increasing - } - - fn activate(&mut self) { - let Some(queues) = self.queues.take() else { - return; - }; - - let mut device_queues: Vec = queues - .into_iter() - .zip(self.queue_evts.iter().cloned()) - .map(|(queue, event)| DeviceQueue::new(queue, event)) - .collect(); - - let mut locked_device = self.locked_device(); - let event_idx_enabled = - (locked_device.acked_features() & (1 << VIRTIO_RING_F_EVENT_IDX)) != 0; - for dq in &mut device_queues { - dq.queue.set_event_idx(event_idx_enabled); - } - locked_device - .activate(self.mem.clone(), self.interrupt.clone(), device_queues) - .expect("Failed to activate device"); - } - - /// Update device status according to the state machine defined by VirtIO Spec 1.0. - /// Please refer to VirtIO Spec 1.0, section 2.1.1 and 3.1.1. - /// - /// The driver MUST update device status, setting bits to indicate the completed steps - /// of the driver initialization sequence specified in 3.1. The driver MUST NOT clear - /// a device status bit. If the driver sets the FAILED bit, the driver MUST later reset - /// the device before attempting to re-initialize. - #[allow(unused_assignments)] - fn set_device_status(&mut self, status: u32) { - use device_status::*; - // match changed bits - match !self.device_status & status { - ACKNOWLEDGE if self.device_status == INIT => { - self.device_status = status; - } - DRIVER if self.device_status == ACKNOWLEDGE => { - self.device_status = status; - } - FEATURES_OK if self.device_status == (ACKNOWLEDGE | DRIVER) => { - self.device_status = status; - } - DRIVER_OK if self.device_status == (ACKNOWLEDGE | DRIVER | FEATURES_OK) => { - self.device_status = status; - let device_activated = self.locked_device().is_activated(); - if !device_activated { - self.activate(); - } - } - _ if (status & FAILED) != 0 => { - // TODO: notify backend driver to stop the device - self.device_status |= FAILED; - } - _ if status == 0 => { - if self.locked_device().is_activated() && !self.locked_device().reset() { - self.device_status |= FAILED; - } - - // If the backend device driver doesn't support reset, - // just leave the device marked as FAILED. - if self.device_status & FAILED == 0 { - self.reset(); - } - } - _ => { - warn!( - "invalid virtio driver status transition: 0x{:x} -> 0x{:x}", - self.device_status, status - ); - } - } - } -} - -impl BusDevice for MmioTransport { - fn read(&mut self, _vcpuid: u64, offset: u64, data: &mut [u8]) { - match offset { - 0x00..=0xff if data.len() == 4 => { - let v = match offset { - 0x0 => MMIO_MAGIC_VALUE, - 0x04 => MMIO_VERSION, - 0x08 => self.locked_device().device_type(), - 0x0c => VENDOR_ID, // vendor id - 0x10 => { - let mut features = self - .locked_device() - .avail_features_by_page(self.features_select); - if self.features_select == 1 { - features |= 0x1; // enable support of VirtIO Version 1 - } - features - } - 0x34 => self - .queue_config - .get(self.queue_select as usize) - .map_or(0, |c| c.size as u32), - 0x44 => self.with_queue(0, |q| q.ready as u32), - 0x60 => self.interrupt.status().load(Ordering::SeqCst) as u32, - 0x70 => self.device_status, - 0xfc => self.config_generation, - 0xb0..=0xbc => { - // For no SHM region or invalid region the kernel looks for length of -1 - let (shm_base, shm_len) = if self.shm_region_select > 1 { - (0, !0) - } else { - match self.locked_device().shm_region() { - Some(region) => (region.guest_addr, region.size as u64), - None => (0, !0), - } - }; - match offset { - 0xb0 => shm_len as u32, - 0xb4 => (shm_len >> 32) as u32, - 0xb8 => shm_base as u32, - 0xbc => (shm_base >> 32) as u32, - _ => { - error!("invalid shm region offset"); - 0 - } - } - } - _ => { - warn!("unknown virtio mmio register read: 0x{offset:x}"); - return; - } - }; - byte_order::write_le_u32(data, v); - } - 0x100..=0xfff => self.locked_device().read_config(offset - 0x100, data), - _ => { - warn!( - "invalid virtio mmio read: 0x{:x}:0x{:x}", - offset, - data.len() - ); - } - }; - } - - fn write(&mut self, _vcpuid: u64, offset: u64, data: &[u8]) { - fn hi(v: &mut GuestAddress, x: u32) { - *v = (*v & 0xffff_ffff) | (u64::from(x) << 32) - } - - fn lo(v: &mut GuestAddress, x: u32) { - *v = (*v & !0xffff_ffff) | u64::from(x) - } - - match offset { - 0x00..=0xff if data.len() == 4 => { - let v = byte_order::read_le_u32(data); - match offset { - 0x14 => self.features_select = v, - 0x20 => { - if self.check_device_status( - device_status::DRIVER, - device_status::FEATURES_OK | device_status::FAILED, - ) { - self.locked_device() - .ack_features_by_page(self.acked_features_select, v); - } else { - warn!( - "ack virtio features in invalid state 0x{:x}", - self.device_status - ); - } - } - 0x24 => self.acked_features_select = v, - 0x30 => self.queue_select = v, - 0x38 => self.update_queue_field(|q| q.size = v as u16), - 0x44 => self.update_queue_field(|q| q.ready = v == 1), - 0x50 => { - // Queue notification - write to the eventfd for the specified queue. - if let Some(eventfd) = self.queue_evts.get(v as usize) { - eventfd.write(1).unwrap(); - } else { - warn!("invalid queue index for notification: {v}"); - } - } - 0x64 => { - if self.check_device_status(device_status::DRIVER_OK, 0) { - self.interrupt - .status() - .fetch_and(!(v as usize), Ordering::SeqCst); - } - } - 0x70 => self.set_device_status(v), - 0x80 => self.update_queue_field(|q| lo(&mut q.desc_table, v)), - 0x84 => self.update_queue_field(|q| hi(&mut q.desc_table, v)), - 0x90 => self.update_queue_field(|q| lo(&mut q.avail_ring, v)), - 0x94 => self.update_queue_field(|q| hi(&mut q.avail_ring, v)), - 0xa0 => self.update_queue_field(|q| lo(&mut q.used_ring, v)), - 0xa4 => self.update_queue_field(|q| hi(&mut q.used_ring, v)), - 0xac => self.shm_region_select = v, - _ => { - warn!("unknown virtio mmio register write: 0x{offset:x}"); - } - } - } - 0x100..=0xfff => { - if self.check_device_status(device_status::DRIVER, device_status::FAILED) { - self.locked_device().write_config(offset - 0x100, data) - } else { - warn!("can not write to device config data area before driver is ready"); - } - } - _ => { - warn!( - "invalid virtio mmio write: 0x{:x}:0x{:x}", - offset, - data.len() - ); - } - } - } - - fn interrupt(&self, irq_mask: u32) -> std::io::Result<()> { - self.interrupt - .status() - .fetch_or(irq_mask as usize, Ordering::SeqCst); - // interrupt_evt() is safe to unwrap because the inner interrupt_evt is initialized in the - // constructor. - // write() is safe to unwrap because the inner syscall is tailored to be safe as well. - self.interrupt.event().write(1).unwrap(); - Ok(()) - } -} - -#[cfg(test)] -pub(crate) mod tests { - use utils::byte_order::{read_le_u32, write_le_u32}; - - use super::*; - use crate::legacy::DummyIrqChip; - use vm_memory::GuestMemoryMmap; - - static QUEUE_CONFIG: [QueueConfig; 2] = [QueueConfig::new(16), QueueConfig::new(32)]; - - pub(crate) struct DummyDevice { - acked_features: u64, - avail_features: u64, - device_activated: bool, - config_bytes: [u8; 0xeff], - } - - impl DummyDevice { - pub(crate) fn new() -> Self { - DummyDevice { - acked_features: 0, - avail_features: 0, - device_activated: false, - config_bytes: [0; 0xeff], - } - } - - fn set_avail_features(&mut self, avail_features: u64) { - self.avail_features = avail_features; - } - } - - impl VirtioDevice for DummyDevice { - fn device_type(&self) -> u32 { - 123 - } - - fn device_name(&self) -> &str { - "dummy" - } - - fn read_config(&self, offset: u64, data: &mut [u8]) { - data.copy_from_slice(&self.config_bytes[offset as usize..]); - } - - fn write_config(&mut self, offset: u64, data: &[u8]) { - for (i, item) in data.iter().enumerate() { - self.config_bytes[offset as usize + i] = *item; - } - } - - fn avail_features(&self) -> u64 { - self.avail_features - } - - fn acked_features(&self) -> u64 { - self.acked_features - } - - fn set_acked_features(&mut self, acked_features: u64) { - self.acked_features = acked_features; - } - - fn queue_config(&self) -> &[QueueConfig] { - &QUEUE_CONFIG - } - - fn activate( - &mut self, - _mem: GuestMemoryMmap, - _interrupt: InterruptTransport, - _queues: Vec, - ) -> ActivateResult { - self.device_activated = true; - Ok(()) - } - - fn is_activated(&self) -> bool { - self.device_activated - } - } - - fn set_device_status(d: &mut MmioTransport, status: u32) { - let mut buf = [0; 4]; - write_le_u32(&mut buf[..], status); - d.write(0, 0x70, &buf[..]); - } - - #[test] - fn test_new() { - let m = GuestMemoryMmap::from_ranges(&[(GuestAddress(0), 0x1000)]).unwrap(); - let dummy = DummyDevice::new(); - let mut d = - MmioTransport::new(m, DummyIrqChip::new().into(), Arc::new(Mutex::new(dummy))).unwrap(); - - // We just make sure here that the implementation of a mmio device behaves as we expect, - // given a known virtio device implementation (the dummy device). - - // Transport now owns the queue_evts. - assert_eq!(d.queue_evts().len(), 2); - - d.queue_select = 0; - assert_eq!(d.with_queue(0, Queue::get_max_size), 16); - assert!(d.with_queue_mut(|q| q.size = 16)); - assert_eq!(d.queues.as_ref().unwrap()[d.queue_select as usize].size, 16); - - d.queue_select = 1; - assert_eq!(d.with_queue(0, Queue::get_max_size), 32); - assert!(d.with_queue_mut(|q| q.size = 16)); - assert_eq!(d.queues.as_ref().unwrap()[d.queue_select as usize].size, 16); - - d.queue_select = 2; - assert_eq!(d.with_queue(0, Queue::get_max_size), 0); - assert!(!d.with_queue_mut(|q| q.size = 16)); - } - - #[test] - fn test_bus_device_read() { - let m = GuestMemoryMmap::from_ranges(&[(GuestAddress(0), 0x1000)]).unwrap(); - let mut d = MmioTransport::new( - m, - DummyIrqChip::new().into(), - Arc::new(Mutex::new(DummyDevice::new())), - ) - .unwrap(); - - let mut buf = vec![0xff, 0, 0xfe, 0]; - let buf_copy = buf.to_vec(); - - // The following read shouldn't be valid, because the length of the buf is not 4. - buf.push(0); - d.read(0, 0, &mut buf[..]); - assert_eq!(buf[..4], buf_copy[..]); - - // the length is ok again - buf.pop(); - - // Now we test that reading at various predefined offsets works as intended. - - d.read(0, 0, &mut buf[..]); - assert_eq!(read_le_u32(&buf[..]), MMIO_MAGIC_VALUE); - - d.read(0, 0x04, &mut buf[..]); - assert_eq!(read_le_u32(&buf[..]), MMIO_VERSION); - - d.read(0, 0x08, &mut buf[..]); - assert_eq!(read_le_u32(&buf[..]), d.locked_device().device_type()); - - d.read(0, 0x0c, &mut buf[..]); - assert_eq!(read_le_u32(&buf[..]), VENDOR_ID); - - d.features_select = 0; - d.read(0, 0x10, &mut buf[..]); - assert_eq!( - read_le_u32(&buf[..]), - d.locked_device().avail_features_by_page(0) - ); - - d.features_select = 1; - d.read(0, 0x10, &mut buf[..]); - assert_eq!( - read_le_u32(&buf[..]), - d.locked_device().avail_features_by_page(0) | 0x1 - ); - - d.read(0, 0x34, &mut buf[..]); - assert_eq!(read_le_u32(&buf[..]), 16); - - d.read(0, 0x44, &mut buf[..]); - assert_eq!(read_le_u32(&buf[..]), false as u32); - - d.interrupt.status().store(111, Ordering::SeqCst); - d.read(0, 0x60, &mut buf[..]); - assert_eq!(read_le_u32(&buf[..]), 111); - - d.read(0, 0x70, &mut buf[..]); - assert_eq!(read_le_u32(&buf[..]), 0); - - d.config_generation = 5; - d.read(0, 0xfc, &mut buf[..]); - assert_eq!(read_le_u32(&buf[..]), 5); - - // This read shouldn't do anything, as it's past the readable generic registers, and - // before the device specific configuration space. Btw, reads from the device specific - // conf space are going to be tested a bit later, alongside writes. - buf = buf_copy.to_vec(); - d.read(0, 0xfd, &mut buf[..]); - assert_eq!(buf[..], buf_copy[..]); - - // Read from an invalid address in generic register range. - d.read(0, 0xfb, &mut buf[..]); - assert_eq!(buf[..], buf_copy[..]); - - // Read from an invalid length in generic register range. - d.read(0, 0xfc, &mut buf[..3]); - assert_eq!(buf[..], buf_copy[..]); - } - - #[test] - #[allow(clippy::cognitive_complexity)] - fn test_bus_device_write() { - let m = GuestMemoryMmap::from_ranges(&[(GuestAddress(0), 0x1000)]).unwrap(); - let dummy_dev = Arc::new(Mutex::new(DummyDevice::new())); - let mut d = MmioTransport::new(m, DummyIrqChip::new().into(), dummy_dev.clone()).unwrap(); - let mut buf = vec![0; 5]; - write_le_u32(&mut buf[..4], 1); - - // Nothing should happen, because the slice len > 4. - d.features_select = 0; - d.write(0, 0x14, &buf[..]); - assert_eq!(d.features_select, 0); - - buf.pop(); - - assert_eq!(d.device_status, device_status::INIT); - set_device_status(&mut d, device_status::ACKNOWLEDGE); - - // Acking features in invalid state shouldn't take effect. - assert_eq!(d.locked_device().acked_features(), 0x0); - d.acked_features_select = 0x0; - write_le_u32(&mut buf[..], 1); - d.write(0, 0x20, &buf[..]); - assert_eq!(d.locked_device().acked_features(), 0x0); - - // Write to device specific configuration space should be ignored before setting device_status::DRIVER - let buf1 = vec![1; 0xeff]; - for i in (0..0xeff).rev() { - let mut buf2 = vec![0; 0xeff]; - - d.write(0, 0x100 + i as u64, &buf1[i..]); - d.read(0, 0x100, &mut buf2[..]); - - for item in buf2.iter().take(0xeff) { - assert_eq!(*item, 0); - } - } - - set_device_status(&mut d, device_status::ACKNOWLEDGE | device_status::DRIVER); - assert_eq!( - d.device_status, - device_status::ACKNOWLEDGE | device_status::DRIVER - ); - - // now writes should work - d.features_select = 0; - write_le_u32(&mut buf[..], 1); - d.write(0, 0x14, &buf[..]); - assert_eq!(d.features_select, 1); - - // Test acknowledging features on bus. - d.acked_features_select = 0; - write_le_u32(&mut buf[..], 0x124); - - // Set the device available features in order to make acknowledging possible. - dummy_dev.lock().unwrap().set_avail_features(0x124); - d.write(0, 0x20, &buf[..]); - assert_eq!(d.locked_device().acked_features(), 0x124); - - d.acked_features_select = 0; - write_le_u32(&mut buf[..], 2); - d.write(0, 0x24, &buf[..]); - assert_eq!(d.acked_features_select, 2); - set_device_status( - &mut d, - device_status::ACKNOWLEDGE | device_status::DRIVER | device_status::FEATURES_OK, - ); - - // Acking features in invalid state shouldn't take effect. - assert_eq!(d.locked_device().acked_features(), 0x124); - d.acked_features_select = 0x0; - write_le_u32(&mut buf[..], 1); - d.write(0, 0x20, &buf[..]); - assert_eq!(d.locked_device().acked_features(), 0x124); - - // Setup queues - d.queue_select = 0; - write_le_u32(&mut buf[..], 3); - d.write(0, 0x30, &buf[..]); - assert_eq!(d.queue_select, 3); - - d.queue_select = 0; - assert_eq!(d.queues.as_ref().unwrap()[0].size, 0); - write_le_u32(&mut buf[..], 16); - d.write(0, 0x38, &buf[..]); - assert_eq!(d.queues.as_ref().unwrap()[0].size, 16); - - assert!(!d.queues.as_ref().unwrap()[0].ready); - write_le_u32(&mut buf[..], 1); - d.write(0, 0x44, &buf[..]); - assert!(d.queues.as_ref().unwrap()[0].ready); - - assert_eq!(d.queues.as_ref().unwrap()[0].desc_table.0, 0); - write_le_u32(&mut buf[..], 123); - d.write(0, 0x80, &buf[..]); - assert_eq!(d.queues.as_ref().unwrap()[0].desc_table.0, 123); - d.write(0, 0x84, &buf[..]); - assert_eq!( - d.queues.as_ref().unwrap()[0].desc_table.0, - 123 + (123 << 32) - ); - - assert_eq!(d.queues.as_ref().unwrap()[0].avail_ring.0, 0); - write_le_u32(&mut buf[..], 124); - d.write(0, 0x90, &buf[..]); - assert_eq!(d.queues.as_ref().unwrap()[0].avail_ring.0, 124); - d.write(0, 0x94, &buf[..]); - assert_eq!( - d.queues.as_ref().unwrap()[0].avail_ring.0, - 124 + (124 << 32) - ); - - assert_eq!(d.queues.as_ref().unwrap()[0].used_ring.0, 0); - write_le_u32(&mut buf[..], 125); - d.write(0, 0xa0, &buf[..]); - assert_eq!(d.queues.as_ref().unwrap()[0].used_ring.0, 125); - d.write(0, 0xa4, &buf[..]); - assert_eq!(d.queues.as_ref().unwrap()[0].used_ring.0, 125 + (125 << 32)); - - set_device_status( - &mut d, - device_status::ACKNOWLEDGE - | device_status::DRIVER - | device_status::FEATURES_OK - | device_status::DRIVER_OK, - ); - - d.interrupt.status().store(0b10_1010, Ordering::Relaxed); - write_le_u32(&mut buf[..], 0b111); - d.write(0, 0x64, &buf[..]); - assert_eq!(d.interrupt.status().load(Ordering::Relaxed), 0b10_1000); - - // Write to an invalid address in generic register range. - write_le_u32(&mut buf[..], 0xf); - d.config_generation = 0; - d.write(0, 0xfb, &buf[..]); - assert_eq!(d.config_generation, 0); - - // Write to an invalid length in generic register range. - d.write(0, 0xfc, &buf[..2]); - assert_eq!(d.config_generation, 0); - - // Here we test writes/read into/from the device specific configuration space. - let buf1 = vec![1; 0xeff]; - for i in (0..0xeff).rev() { - let mut buf2 = vec![0; 0xeff]; - - d.write(0, 0x100 + i as u64, &buf1[i..]); - d.read(0, 0x100, &mut buf2[..]); - - for item in buf2.iter().take(i) { - assert_eq!(*item, 0); - } - - assert_eq!(buf1[i..], buf2[i..]); - } - } - - #[test] - fn test_bus_device_activate() { - let m = GuestMemoryMmap::from_ranges(&[(GuestAddress(0), 0x1000)]).unwrap(); - let mut d = MmioTransport::new( - m, - DummyIrqChip::new().into(), - Arc::new(Mutex::new(DummyDevice::new())), - ) - .unwrap(); - - assert!(!d.locked_device().is_activated()); - assert_eq!(d.device_status, device_status::INIT); - - set_device_status(&mut d, device_status::ACKNOWLEDGE); - set_device_status(&mut d, device_status::ACKNOWLEDGE | device_status::DRIVER); - assert_eq!( - d.device_status, - device_status::ACKNOWLEDGE | device_status::DRIVER - ); - - // invalid state transition should have no effect - set_device_status( - &mut d, - device_status::ACKNOWLEDGE | device_status::DRIVER | device_status::DRIVER_OK, - ); - assert_eq!( - d.device_status, - device_status::ACKNOWLEDGE | device_status::DRIVER - ); - - set_device_status( - &mut d, - device_status::ACKNOWLEDGE | device_status::DRIVER | device_status::FEATURES_OK, - ); - assert_eq!( - d.device_status, - device_status::ACKNOWLEDGE | device_status::DRIVER | device_status::FEATURES_OK - ); - - let mut buf = [0; 4]; - let queue_len = d.queues.as_ref().unwrap().len(); - for q in 0..queue_len { - d.queue_select = q as u32; - write_le_u32(&mut buf[..], 16); - d.write(0, 0x38, &buf[..]); - write_le_u32(&mut buf[..], 1); - d.write(0, 0x44, &buf[..]); - } - assert!(!d.locked_device().is_activated()); - - // Device should be ready for activation now. - - // A couple of invalid writes; will trigger warnings; shouldn't activate the device. - d.write(0, 0xa8, &buf[..]); - d.write(0, 0x1000, &buf[..]); - assert!(!d.locked_device().is_activated()); - - set_device_status( - &mut d, - device_status::ACKNOWLEDGE - | device_status::DRIVER - | device_status::FEATURES_OK - | device_status::DRIVER_OK, - ); - assert_eq!( - d.device_status, - device_status::ACKNOWLEDGE - | device_status::DRIVER - | device_status::FEATURES_OK - | device_status::DRIVER_OK - ); - assert!(d.locked_device().is_activated()); - } - - fn activate_device(d: &mut MmioTransport) { - set_device_status(d, device_status::ACKNOWLEDGE); - set_device_status(d, device_status::ACKNOWLEDGE | device_status::DRIVER); - set_device_status( - d, - device_status::ACKNOWLEDGE | device_status::DRIVER | device_status::FEATURES_OK, - ); - - // Setup queue data structures - let mut buf = [0; 4]; - let queues_count = d.queues.as_ref().unwrap().len(); - for q in 0..queues_count { - d.queue_select = q as u32; - write_le_u32(&mut buf[..], 16); - d.write(0, 0x38, &buf[..]); - write_le_u32(&mut buf[..], 1); - d.write(0, 0x44, &buf[..]); - } - assert!(!d.locked_device().is_activated()); - - // Device should be ready for activation now. - set_device_status( - d, - device_status::ACKNOWLEDGE - | device_status::DRIVER - | device_status::FEATURES_OK - | device_status::DRIVER_OK, - ); - assert_eq!( - d.device_status, - device_status::ACKNOWLEDGE - | device_status::DRIVER - | device_status::FEATURES_OK - | device_status::DRIVER_OK - ); - assert!(d.locked_device().is_activated()); - } - - #[test] - fn test_bus_device_reset() { - let m = GuestMemoryMmap::from_ranges(&[(GuestAddress(0), 0x1000)]).unwrap(); - - let mut d = MmioTransport::new( - m, - DummyIrqChip::new().into(), - Arc::new(Mutex::new(DummyDevice::new())), - ) - .unwrap(); - let mut buf = [0; 4]; - - assert!(!d.locked_device().is_activated()); - assert_eq!(d.device_status, 0); - activate_device(&mut d); - - // Marking device as FAILED should not affect device_activated state - write_le_u32(&mut buf[..], 0x8f); - d.write(0, 0x70, &buf[..]); - assert_eq!(d.device_status, 0x8f); - assert!(d.locked_device().is_activated()); - - // Nothing happens when backend driver doesn't support reset - write_le_u32(&mut buf[..], 0x0); - d.write(0, 0x70, &buf[..]); - assert_eq!(d.device_status, 0x8f); - assert!(d.locked_device().is_activated()); - } - - #[test] - fn test_get_avail_features() { - let dummy_dev = DummyDevice::new(); - assert_eq!(dummy_dev.avail_features(), dummy_dev.avail_features); - } - - #[test] - fn test_get_acked_features() { - let dummy_dev = DummyDevice::new(); - assert_eq!(dummy_dev.acked_features(), dummy_dev.acked_features); - } - - #[test] - fn test_set_acked_features() { - let mut dummy_dev = DummyDevice::new(); - - assert_eq!(dummy_dev.acked_features(), 0); - dummy_dev.set_acked_features(16); - assert_eq!(dummy_dev.acked_features(), dummy_dev.acked_features); - } - - #[test] - fn test_ack_features_by_page() { - let mut dummy_dev = DummyDevice::new(); - dummy_dev.set_acked_features(16); - dummy_dev.set_avail_features(8); - dummy_dev.ack_features_by_page(0, 8); - assert_eq!(dummy_dev.acked_features(), 24); - } -} diff --git a/vendor/krun-devices/src/virtio/mod.rs b/vendor/krun-devices/src/virtio/mod.rs deleted file mode 100644 index 384aef5ac..000000000 --- a/vendor/krun-devices/src/virtio/mod.rs +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -// -// Portions Copyright 2017 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the THIRD-PARTY file. - -//! Implements virtio devices, queues, and transport mechanisms. -use std; -use std::any::Any; -use std::io::Error as IOError; - -#[cfg(not(feature = "tee"))] -pub mod balloon; -#[allow(dead_code)] -#[allow(non_camel_case_types)] -pub mod bindings; -#[cfg(feature = "blk")] -pub mod block; -pub mod console; -pub mod descriptor_utils; -pub mod device; -pub mod file_traits; -#[cfg(not(any(feature = "tee", feature = "aws-nitro")))] -pub mod fs; -#[cfg(feature = "gpu")] -pub mod gpu; -#[cfg(feature = "input")] -pub mod input; -pub mod linux_errno; -mod mmio; -#[cfg(feature = "net")] -pub mod net; -mod queue; -#[cfg(not(feature = "tee"))] -pub mod rng; -#[cfg(feature = "snd")] -pub mod snd; -pub mod vsock; - -#[cfg(not(feature = "tee"))] -pub use self::balloon::*; -#[cfg(feature = "blk")] -pub use self::block::{Block, CacheType}; -pub use self::console::*; -pub use self::device::*; -#[cfg(not(any(feature = "tee", feature = "aws-nitro")))] -pub use self::fs::*; -#[cfg(feature = "gpu")] -pub use self::gpu::*; -pub use self::mmio::*; -#[cfg(feature = "net")] -pub use self::net::Net; -pub use self::queue::{Descriptor, DescriptorChain, Queue}; -#[cfg(not(feature = "tee"))] -pub use self::rng::*; -#[cfg(feature = "snd")] -pub use self::snd::Snd; -pub use self::vsock::*; - -/// When the driver initializes the device, it lets the device know about the -/// completed stages using the Device Status Field. -/// -/// These following consts are defined in the order in which the bits would -/// typically be set by the driver. INIT -> ACKNOWLEDGE -> DRIVER and so on. -/// -/// This module is a 1:1 mapping for the Device Status Field in the virtio 1.0 -/// specification, section 2.1. -mod device_status { - pub const INIT: u32 = 0; - pub const ACKNOWLEDGE: u32 = 1; - pub const DRIVER: u32 = 2; - pub const FAILED: u32 = 128; - pub const FEATURES_OK: u32 = 8; - pub const DRIVER_OK: u32 = 4; -} - -/// Types taken from linux/virtio_ids.h. -/// Type 0 is not used by virtio. Use it as wildcard for non-virtio devices -pub const TYPE_NET: u32 = 1; -pub const TYPE_BLOCK: u32 = 2; - -/// Interrupt flags (re: interrupt status & acknowledge registers). -/// See linux/virtio_mmio.h. -pub const VIRTIO_MMIO_INT_VRING: u32 = 0x01; -pub const VIRTIO_MMIO_INT_CONFIG: u32 = 0x02; - -/// Offset from the base MMIO address of a virtio device used by the guest to notify the device of -/// queue events. -pub const NOTIFY_REG_OFFSET: u32 = 0x50; - -#[derive(Debug)] -pub enum ActivateError { - EpollCtl(IOError), - BadActivate, -} - -pub type ActivateResult = std::result::Result<(), ActivateError>; - -/// Trait that helps in upcasting an object to Any -pub trait AsAny { - fn as_any(&self) -> &dyn Any; - - fn as_mut_any(&mut self) -> &mut dyn Any; -} -impl AsAny for T { - fn as_any(&self) -> &dyn Any { - self - } - - fn as_mut_any(&mut self) -> &mut dyn Any { - self - } -} diff --git a/vendor/krun-devices/src/virtio/net/backend.rs b/vendor/krun-devices/src/virtio/net/backend.rs deleted file mode 100644 index dab01c943..000000000 --- a/vendor/krun-devices/src/virtio/net/backend.rs +++ /dev/null @@ -1,54 +0,0 @@ -use std::io; -use std::os::fd::RawFd; - -#[allow(dead_code)] -#[derive(Debug)] -pub enum ConnectError { - InvalidAddress(nix::Error), - CreateSocket(nix::Error), - Binding(nix::Error), - SendingMagic(nix::Error), - // Tap backend errors. - OpenNetTun(nix::Error), - TunSetIff(io::Error), - TunSetVnetHdrSz(io::Error), - TunSetOffload(io::Error), -} - -#[allow(dead_code)] -#[derive(Debug)] -pub enum ReadError { - /// Nothing was written - NothingRead, - /// Another internal error occurred - Internal(nix::Error), -} - -#[allow(dead_code)] -#[derive(Debug)] -pub enum WriteError { - /// Nothing was written, you can drop the frame or try to resend it later - NothingWritten, - /// Part of the buffer was written, the write has to be finished using try_finish_write - PartialWrite, - /// Passt doesnt seem to be running (received EPIPE) - ProcessNotRunning, - /// Another internal error occurred - Internal(nix::Error), -} - -pub trait NetBackend { - fn read_frame(&mut self, buf: &mut [u8]) -> Result; - fn write_frame(&mut self, hdr_len: usize, buf: &mut [u8]) -> Result<(), WriteError>; - fn has_unfinished_write(&self) -> bool; - fn try_finish_write(&mut self, hdr_len: usize, buf: &[u8]) -> Result<(), WriteError>; - fn raw_socket_fd(&self) -> RawFd; - - /// Delay in microseconds before retrying after NothingWritten. - /// Returns 0 if no delay-based retry is needed (e.g. on Linux where - /// EAGAIN + EPOLLET handles retries via writable events). - #[allow(dead_code)] - fn write_retry_delay_us(&self) -> u64 { - 0 - } -} diff --git a/vendor/krun-devices/src/virtio/net/device.rs b/vendor/krun-devices/src/virtio/net/device.rs deleted file mode 100644 index 9d4b4a1fc..000000000 --- a/vendor/krun-devices/src/virtio/net/device.rs +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -// -// Portions Copyright 2017 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the THIRD-PARTY file. -use crate::virtio::net::Result; -use crate::virtio::net::{NUM_QUEUES, QUEUE_CONFIG}; -use crate::virtio::queue::Error as QueueError; -use crate::virtio::{ - ActivateError, ActivateResult, DeviceQueue, DeviceState, InterruptTransport, QueueConfig, - VirtioDevice, TYPE_NET, -}; -use crate::Error as DeviceError; - -use super::backend::{ReadError, WriteError}; -use super::worker::NetWorker; - -use std::cmp; -use std::io::Write; -use std::os::fd::RawFd; -use std::path::PathBuf; -use virtio_bindings::virtio_net::VIRTIO_NET_F_MAC; -use virtio_bindings::virtio_ring::VIRTIO_RING_F_EVENT_IDX; -use vm_memory::{ByteValued, GuestMemoryError, GuestMemoryMmap}; - -const VIRTIO_F_VERSION_1: u32 = 32; - -#[derive(Debug)] -pub enum FrontendError { - DescriptorChainTooSmall, - EmptyQueue, - GuestMemory(GuestMemoryError), - QueueError(QueueError), - ReadOnlyDescriptor, -} - -#[derive(Debug)] -pub enum RxError { - Backend(ReadError), - DeviceError(DeviceError), -} - -#[derive(Debug)] -pub enum TxError { - Backend(WriteError), - DeviceError(DeviceError), - QueueError(QueueError), -} - -#[derive(Copy, Clone, Debug, Default)] -#[repr(C, packed)] -struct VirtioNetConfig { - mac: [u8; 6], - status: u16, - max_virtqueue_pairs: u16, -} - -// Safe because it only has data and has no implicit padding. -unsafe impl ByteValued for VirtioNetConfig {} - -#[derive(Clone)] -pub enum VirtioNetBackend { - UnixstreamFd(RawFd), - UnixstreamPath(PathBuf), - UnixgramFd(RawFd), - UnixgramPath(PathBuf, bool), - #[cfg(target_os = "linux")] - Tap(String), -} - -pub struct Net { - id: String, - pub cfg_backend: VirtioNetBackend, - - avail_features: u64, - acked_features: u64, - - pub(crate) device_state: DeviceState, - - config: VirtioNetConfig, -} - -impl Net { - /// Create a new virtio network device using the backend - pub fn new( - id: String, - cfg_backend: VirtioNetBackend, - mac: [u8; 6], - features: u32, - ) -> Result { - let avail_features = features as u64 - | (1 << VIRTIO_NET_F_MAC) - | (1 << VIRTIO_RING_F_EVENT_IDX) - | (1 << VIRTIO_F_VERSION_1); - - let config = VirtioNetConfig { - mac, - status: 0, - max_virtqueue_pairs: 0, - }; - - Ok(Net { - id, - cfg_backend, - - avail_features, - acked_features: 0u64, - - device_state: DeviceState::Inactive, - config, - }) - } - - /// Provides the ID of this net device. - pub fn id(&self) -> &str { - &self.id - } - - /// Provides the virtio-net backend of this net device. - pub fn backend(&self) -> &VirtioNetBackend { - &self.cfg_backend - } -} - -impl VirtioDevice for Net { - fn avail_features(&self) -> u64 { - self.avail_features - } - - fn acked_features(&self) -> u64 { - self.acked_features - } - - fn set_acked_features(&mut self, acked_features: u64) { - self.acked_features = acked_features; - } - - fn device_type(&self) -> u32 { - TYPE_NET - } - - fn device_name(&self) -> &str { - "net" - } - - fn queue_config(&self) -> &[QueueConfig] { - &QUEUE_CONFIG - } - - fn read_config(&self, offset: u64, mut data: &mut [u8]) { - let config_slice = self.config.as_slice(); - let config_len = config_slice.len() as u64; - if offset >= config_len { - error!("Failed to read config space"); - return; - } - if let Some(end) = offset.checked_add(data.len() as u64) { - // This write can't fail, offset and end are checked against config_len. - data.write_all(&config_slice[offset as usize..cmp::min(end, config_len) as usize]) - .unwrap(); - } - } - - fn write_config(&mut self, offset: u64, data: &[u8]) { - log::warn!( - "Net: guest driver attempted to write device config (offset={:x}, len={:x})", - offset, - data.len() - ); - } - - fn activate( - &mut self, - mem: GuestMemoryMmap, - interrupt: InterruptTransport, - queues: Vec, - ) -> ActivateResult { - let [rx_q, tx_q]: [_; NUM_QUEUES] = queues.try_into().map_err(|_| { - error!("Cannot perform activate. Expected {} queue(s)", NUM_QUEUES); - ActivateError::BadActivate - })?; - - match NetWorker::new( - rx_q, - tx_q, - interrupt.clone(), - mem.clone(), - self.acked_features, - self.cfg_backend.clone(), - ) { - Ok(worker) => { - worker.run(); - self.device_state = DeviceState::Activated(mem, interrupt); - Ok(()) - } - Err(err) => { - error!( - "Error activating virtio-net ({}) backend: {err:?}", - self.id() - ); - Err(ActivateError::BadActivate) - } - } - } - - fn is_activated(&self) -> bool { - self.device_state.is_activated() - } -} diff --git a/vendor/krun-devices/src/virtio/net/mod.rs b/vendor/krun-devices/src/virtio/net/mod.rs deleted file mode 100644 index e0c51f3fd..000000000 --- a/vendor/krun-devices/src/virtio/net/mod.rs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -use std::{io, mem, result}; -use virtio_bindings::virtio_net::virtio_net_hdr_v1; - -use super::QueueConfig; - -pub const MAX_BUFFER_SIZE: usize = 65562; -const QUEUE_SIZE: u16 = 1024; -pub const NUM_QUEUES: usize = 2; -pub static QUEUE_CONFIG: [QueueConfig; NUM_QUEUES] = [QueueConfig::new(QUEUE_SIZE); NUM_QUEUES]; - -mod backend; -pub mod device; -#[cfg(target_os = "linux")] -mod tap; -mod unixgram; -mod unixstream; -mod worker; - -// https://docs.oasis-open.org/virtio/virtio/v1.1/csprd01/virtio-v1.1-csprd01.html#x1-2050006 -const VNET_HDR_LEN: usize = mem::size_of::(); - -// This initializes to all 0 the virtio_net_hdr part of a buf and return the length of the header -fn write_virtio_net_hdr(buf: &mut [u8]) -> usize { - buf[0..VNET_HDR_LEN].fill(0); - VNET_HDR_LEN -} - -pub use self::device::Net; -#[derive(Debug)] -pub enum Error { - /// EventFd error. - EventFd(io::Error), -} - -pub type Result = result::Result; diff --git a/vendor/krun-devices/src/virtio/net/tap.rs b/vendor/krun-devices/src/virtio/net/tap.rs deleted file mode 100644 index 1c8bde34e..000000000 --- a/vendor/krun-devices/src/virtio/net/tap.rs +++ /dev/null @@ -1,128 +0,0 @@ -use libc::{ - c_char, c_int, ifreq, IFF_NO_PI, IFF_TAP, IFF_VNET_HDR, TUN_F_CSUM, TUN_F_TSO4, TUN_F_TSO6, - TUN_F_UFO, -}; -use nix::fcntl::{fcntl, open, FcntlArg, OFlag}; -use nix::sys::stat::Mode; -use nix::unistd::{read, write}; -use nix::{ioctl_write_int, ioctl_write_ptr}; -use std::os::fd::{AsRawFd, OwnedFd, RawFd}; -use std::{io, mem, ptr}; -use virtio_bindings::virtio_net::{ - VIRTIO_NET_F_GUEST_CSUM, VIRTIO_NET_F_GUEST_TSO4, VIRTIO_NET_F_GUEST_TSO6, - VIRTIO_NET_F_GUEST_UFO, -}; - -use super::backend::{ConnectError, NetBackend, ReadError, WriteError}; - -ioctl_write_ptr!(tunsetiff, b'T', 202, c_int); -ioctl_write_int!(tunsetoffload, b'T', 208); -ioctl_write_ptr!(tunsetvnethdrsz, b'T', 216, c_int); - -pub struct Tap { - fd: OwnedFd, -} - -impl Tap { - /// Create an endpoint using the file descriptor of a tap device - pub fn new(tap_name: String, vnet_features: u64) -> Result { - let fd = match open("/dev/net/tun", OFlag::O_RDWR, Mode::empty()) { - Ok(fd) => fd, - Err(err) => return Err(ConnectError::OpenNetTun(err)), - }; - - let mut req: ifreq = unsafe { mem::zeroed() }; - - unsafe { - ptr::copy_nonoverlapping( - tap_name.as_ptr() as *const c_char, - req.ifr_name.as_mut_ptr(), - tap_name.len(), - ); - } - - req.ifr_ifru.ifru_flags = IFF_TAP as i16 | IFF_NO_PI as i16 | IFF_VNET_HDR as i16; - - let mut offload_flags: u64 = 0; - if (vnet_features & (1 << VIRTIO_NET_F_GUEST_CSUM)) != 0 { - offload_flags |= TUN_F_CSUM as u64; - } - if (vnet_features & (1 << VIRTIO_NET_F_GUEST_TSO4)) != 0 { - offload_flags |= TUN_F_TSO4 as u64; - } - if (vnet_features & (1 << VIRTIO_NET_F_GUEST_TSO6)) != 0 { - offload_flags |= TUN_F_TSO6 as u64; - } - if (vnet_features & (1 << VIRTIO_NET_F_GUEST_UFO)) != 0 { - offload_flags |= TUN_F_UFO as u64; - } - - unsafe { - if let Err(err) = tunsetiff(fd.as_raw_fd(), &mut req as *mut _ as *mut _) { - return Err(ConnectError::TunSetIff(io::Error::from(err))); - } - - // TODO(slp): replace hardcoded vnet size with cons - if let Err(err) = tunsetvnethdrsz(fd.as_raw_fd(), &12) { - return Err(ConnectError::TunSetVnetHdrSz(io::Error::from(err))); - } - - if let Err(err) = tunsetoffload(fd.as_raw_fd(), offload_flags) { - return Err(ConnectError::TunSetOffload(io::Error::from(err))); - } - } - - match fcntl(&fd, FcntlArg::F_GETFL) { - Ok(flags) => { - if let Err(e) = fcntl( - &fd, - FcntlArg::F_SETFL(OFlag::from_bits_truncate(flags) | OFlag::O_NONBLOCK), - ) { - warn!("error switching to non-blocking: id={fd:?}, err={e}"); - } - } - Err(e) => error!("couldn't obtain fd flags id={fd:?}, err={e}"), - }; - - Ok(Self { fd }) - } -} - -impl NetBackend for Tap { - /// Try to read a frame from the tap devie. If no bytes are available reports - /// ReadError::NothingRead. - fn read_frame(&mut self, buf: &mut [u8]) -> Result { - let frame_length = match read(&self.fd, buf) { - Ok(f) => f, - #[allow(unreachable_patterns)] - Err(nix::Error::EAGAIN | nix::Error::EWOULDBLOCK) => { - return Err(ReadError::NothingRead) - } - Err(e) => { - return Err(ReadError::Internal(e)); - } - }; - debug!("Read eth frame from tap: {frame_length} bytes"); - Ok(frame_length) - } - - /// Try to write a frame to the tap device. - fn write_frame(&mut self, _hdr_len: usize, buf: &mut [u8]) -> Result<(), WriteError> { - let ret = write(&self.fd, buf).map_err(WriteError::Internal)?; - debug!("Written frame size={}, written={}", buf.len(), ret); - Ok(()) - } - - fn has_unfinished_write(&self) -> bool { - false - } - - fn try_finish_write(&mut self, _hdr_len: usize, _buf: &[u8]) -> Result<(), WriteError> { - // The tap backend doesn't do partial writes. - Ok(()) - } - - fn raw_socket_fd(&self) -> RawFd { - self.fd.as_raw_fd() - } -} diff --git a/vendor/krun-devices/src/virtio/net/unixgram.rs b/vendor/krun-devices/src/virtio/net/unixgram.rs deleted file mode 100644 index 2dafdaa3e..000000000 --- a/vendor/krun-devices/src/virtio/net/unixgram.rs +++ /dev/null @@ -1,189 +0,0 @@ -use nix::fcntl::{fcntl, FcntlArg, OFlag}; -use nix::sys::socket::{ - bind, connect, getsockopt, recv, send, setsockopt, socket, sockopt, AddressFamily, MsgFlags, - SockFlag, SockType, UnixAddr, -}; -use nix::unistd::unlink; -use std::os::fd::{AsRawFd, OwnedFd, RawFd}; -use std::path::PathBuf; -use std::process; -use std::sync::atomic::{AtomicU32, Ordering}; - -use super::backend::{ConnectError, NetBackend, ReadError, WriteError}; -use super::write_virtio_net_hdr; -#[cfg(target_os = "macos")] -use super::{MAX_BUFFER_SIZE, VNET_HDR_LEN}; - -const VFKIT_MAGIC: [u8; 4] = *b"VFKT"; - -/// Per-process counter to generate unique local unixgram socket filenames. -/// -/// The local socket is placed in the same directory as the peer using a short -/// PID+counter name. The peer filename always contains the machine name, so it -/// is longer than our fixed-format name for any reasonably-named machine, keeping -/// the local path within macOS's 104-byte unix socket limit. -static NET_SOCK_COUNTER: AtomicU32 = AtomicU32::new(0); - -const DEFAULT_SOCKET_BUF_SIZE: usize = 7 * 1024 * 1024; - -// On macOS, with UNIX datagram sockets the send buffer is not used for queuing; -// it determines the maximum frame size. -// https://github.com/apple-oss-distributions/xnu/blob/f6217f891ac0bb64f3d375211650a4c1ff8ca1ea/bsd/kern/uipc_usrreq.c#L953 -#[cfg(target_os = "macos")] -const SOCKET_SNDBUF: usize = MAX_BUFFER_SIZE - VNET_HDR_LEN; - -#[cfg(not(target_os = "macos"))] -const SOCKET_SNDBUF: usize = DEFAULT_SOCKET_BUF_SIZE; - -const SOCKET_RCVBUF: usize = DEFAULT_SOCKET_BUF_SIZE; - -pub struct Unixgram { - fd: OwnedFd, - retries: u64, -} - -impl Unixgram { - /// Create the backend with a pre-established connection to the userspace network proxy. - pub fn new(fd: OwnedFd) -> Self { - // Ensure the socket is in non-blocking mode. - match fcntl(&fd, FcntlArg::F_GETFL) { - Ok(flags) => match OFlag::from_bits(flags) { - Some(flags) => { - if let Err(e) = fcntl(&fd, FcntlArg::F_SETFL(flags | OFlag::O_NONBLOCK)) { - warn!("error switching to non-blocking: id={fd:?}, err={e}"); - } - } - None => error!("invalid fd flags id={fd:?}"), - }, - Err(e) => error!("couldn't obtain fd flags id={fd:?}, err={e}"), - }; - - #[cfg(target_os = "macos")] - { - // nix doesn't provide an abstraction for SO_NOSIGPIPE, fall back to libc. - let option_value: libc::c_int = 1; - unsafe { - libc::setsockopt( - fd.as_raw_fd(), - libc::SOL_SOCKET, - libc::SO_NOSIGPIPE, - &option_value as *const _ as *const libc::c_void, - std::mem::size_of_val(&option_value) as libc::socklen_t, - ) - }; - } - - Self { fd, retries: 0 } - } - - /// Create the backend opening a connection to the userspace network proxy. - pub fn open(path: PathBuf, send_vfkit_magic: bool) -> Result { - // We cannot create a non-blocking socket on macOS here. This is done later in new(). - let fd = socket( - AddressFamily::Unix, - SockType::Datagram, - SockFlag::empty(), - None, - ) - .map_err(ConnectError::CreateSocket)?; - let peer_addr = UnixAddr::new(&path).map_err(ConnectError::InvalidAddress)?; - let socket_name = format!( - "krun-net-{}-{}.sock", - process::id(), - NET_SOCK_COUNTER.fetch_add(1, Ordering::Relaxed), - ); - let local_path = std::env::temp_dir().join(&socket_name); - let local_addr = UnixAddr::new(&local_path).map_err(ConnectError::InvalidAddress)?; - if let Some(path) = local_addr.path() { - _ = unlink(path); - } - bind(fd.as_raw_fd(), &local_addr).map_err(ConnectError::Binding)?; - - // Connect so we don't need to use the peer address again. This also - // allows the server to remove the socket after the connection. - connect(fd.as_raw_fd(), &peer_addr).map_err(ConnectError::Binding)?; - - if send_vfkit_magic { - send(fd.as_raw_fd(), &VFKIT_MAGIC, MsgFlags::empty()) - .map_err(ConnectError::SendingMagic)?; - } - - if let Err(e) = setsockopt(&fd, sockopt::SndBuf, &SOCKET_SNDBUF) { - log::warn!("Failed to set SO_SNDBUF: {e}"); - } - if let Err(e) = setsockopt(&fd, sockopt::RcvBuf, &SOCKET_RCVBUF) { - log::warn!("Failed to set SO_RCVBUF: {e}"); - } - - log::debug!( - "network proxy socket (fd {fd:?}) buffer sizes: SndBuf={:?} RcvBuf={:?}", - getsockopt(&fd, sockopt::SndBuf), - getsockopt(&fd, sockopt::RcvBuf) - ); - - Ok(Self::new(fd)) - } -} - -impl NetBackend for Unixgram { - /// Try to read a frame the proxy. If no bytes are available reports ReadError::NothingRead - fn read_frame(&mut self, buf: &mut [u8]) -> Result { - let hdr_len = write_virtio_net_hdr(buf); - let frame_length = match recv(self.fd.as_raw_fd(), &mut buf[hdr_len..], MsgFlags::empty()) { - Ok(f) => f, - #[allow(unreachable_patterns)] - Err(nix::Error::EAGAIN | nix::Error::EWOULDBLOCK) => { - return Err(ReadError::NothingRead) - } - Err(e) => { - return Err(ReadError::Internal(e)); - } - }; - debug!("Read eth frame from proxy: {frame_length} bytes"); - Ok(hdr_len + frame_length) - } - - /// Try to write a frame to the proxy. - fn write_frame(&mut self, hdr_len: usize, buf: &mut [u8]) -> Result<(), WriteError> { - let ret = match send(self.fd.as_raw_fd(), &buf[hdr_len..], MsgFlags::empty()) { - Ok(ret) => ret, - // macOS returns ENOBUFS when the kernel socket buffer is full, - // rather than blocking or returning EAGAIN on non-blocking sockets. - Err(nix::Error::ENOBUFS) => { - if self.retries == 0 { - info!("write_frame: ENOBUFS"); - } - self.retries += 1; - return Err(WriteError::NothingWritten); - } - Err(e) => return Err(WriteError::Internal(e)), - }; - if self.retries > 0 { - info!( - "write_frame: ENOBUFS resolved after {} retries", - self.retries - ); - self.retries = 0; - } - debug!("Written eth frame to proxy: {ret} bytes"); - Ok(()) - } - - fn has_unfinished_write(&self) -> bool { - false - } - - fn try_finish_write(&mut self, _hdr_len: usize, _buf: &[u8]) -> Result<(), WriteError> { - // The unixgram backend doesn't do partial writes. - Ok(()) - } - - fn raw_socket_fd(&self) -> RawFd { - self.fd.as_raw_fd() - } - - #[cfg(target_os = "macos")] - fn write_retry_delay_us(&self) -> u64 { - 50 - } -} diff --git a/vendor/krun-devices/src/virtio/net/unixstream.rs b/vendor/krun-devices/src/virtio/net/unixstream.rs deleted file mode 100644 index 023be6b28..000000000 --- a/vendor/krun-devices/src/virtio/net/unixstream.rs +++ /dev/null @@ -1,222 +0,0 @@ -use nix::sys::socket::{ - connect, getsockopt, recv, send, setsockopt, socket, sockopt, AddressFamily, MsgFlags, - SockFlag, SockType, UnixAddr, -}; -use std::{ - os::fd::{AsRawFd, OwnedFd, RawFd}, - path::PathBuf, -}; - -use crate::virtio::net::backend::ConnectError; - -use super::backend::{NetBackend, ReadError, WriteError}; -use super::write_virtio_net_hdr; - -/// Each frame the network proxy is prepended by a 4 byte "header". -/// It is interpreted as a big-endian u32 integer and is the length of the following ethernet frame. -const FRAME_HEADER_LEN: usize = 4; - -pub struct Unixstream { - fd: OwnedFd, - // 0 when a frame length has not been read - expecting_frame_length: u32, - // 0 if last write is fully complete, otherwise the length that was written - last_partial_write_length: usize, -} - -impl Unixstream { - /// Create the backend with a pre-established connection to the userspace network proxy. - pub fn new(fd: OwnedFd) -> Self { - if let Err(e) = setsockopt(&fd, sockopt::SndBuf, &(16 * 1024 * 1024)) { - log::warn!("Failed to increase SO_SNDBUF (performance may be decreased): {e}"); - } - - log::debug!( - "network proxy socket (fd {fd:?}) buffer sizes: SndBuf={:?} RcvBuf={:?}", - getsockopt(&fd, sockopt::SndBuf), - getsockopt(&fd, sockopt::RcvBuf) - ); - - Self { - fd, - expecting_frame_length: 0, - last_partial_write_length: 0, - } - } - - /// Create the backend opening a connection to the userspace network proxy. - pub fn open(path: PathBuf) -> Result { - let fd = socket( - AddressFamily::Unix, - SockType::Stream, - SockFlag::empty(), - None, - ) - .map_err(ConnectError::CreateSocket)?; - let peer_addr = UnixAddr::new(&path).map_err(ConnectError::InvalidAddress)?; - connect(fd.as_raw_fd(), &peer_addr).map_err(ConnectError::Binding)?; - - if let Err(e) = setsockopt(&fd, sockopt::SndBuf, &(16 * 1024 * 1024)) { - log::warn!("Failed to increase SO_SNDBUF (performance may be decreased): {e}"); - } - - log::debug!( - "network socket (fd {fd:?}) buffer sizes: SndBuf={:?} RcvBuf={:?}", - getsockopt(&fd, sockopt::SndBuf), - getsockopt(&fd, sockopt::RcvBuf) - ); - - Ok(Self { - fd, - expecting_frame_length: 0, - last_partial_write_length: 0, - }) - } - - /// Try to read until filling the whole slice. - fn read_loop(&self, buf: &mut [u8], block_until_has_data: bool) -> Result<(), ReadError> { - let mut bytes_read = 0; - #[cfg(target_os = "linux")] - let flags = MsgFlags::MSG_DONTWAIT | MsgFlags::MSG_NOSIGNAL; - #[cfg(target_os = "macos")] - let flags = MsgFlags::MSG_DONTWAIT; - - if !block_until_has_data { - match recv(self.fd.as_raw_fd(), buf, flags) { - Ok(size) => bytes_read += size, - #[allow(unreachable_patterns)] - Err(nix::Error::EAGAIN | nix::Error::EWOULDBLOCK) => { - return Err(ReadError::NothingRead) - } - Err(e) => return Err(ReadError::Internal(e)), - } - } - - #[cfg(target_os = "linux")] - let flags = MsgFlags::MSG_WAITALL | MsgFlags::MSG_NOSIGNAL; - #[cfg(target_os = "macos")] - let flags = MsgFlags::MSG_WAITALL; - - while bytes_read < buf.len() { - match recv(self.fd.as_raw_fd(), &mut buf[bytes_read..], flags) { - #[allow(unreachable_patterns)] - Err(nix::Error::EAGAIN | nix::Error::EWOULDBLOCK) => { - log::warn!("read_loop: unexpected EAGAIN/EWOULDBLOCK on blocking socket"); - continue; - } - Err(e) => return Err(ReadError::Internal(e)), - Ok(size) => { - bytes_read += size; - //log::trace!("proxy recv {}/{}", bytes_read, buf.len()); - } - } - } - - Ok(()) - } - - fn write_loop(&mut self, buf: &[u8]) -> Result<(), WriteError> { - let mut bytes_send = 0; - - #[cfg(target_os = "linux")] - let flags = MsgFlags::MSG_DONTWAIT | MsgFlags::MSG_NOSIGNAL; - #[cfg(target_os = "macos")] - let flags = MsgFlags::MSG_DONTWAIT; - - while bytes_send < buf.len() { - match send(self.fd.as_raw_fd(), &buf[bytes_send..], flags) { - Ok(size) => bytes_send += size, - #[allow(unreachable_patterns)] - Err(nix::Error::EAGAIN | nix::Error::EWOULDBLOCK) => { - if bytes_send == 0 { - return Err(WriteError::NothingWritten); - } else { - log::trace!( - "Wrote {bytes_send} bytes, but socket blocked, will need try_finish_write() to finish" - ); - - self.last_partial_write_length += bytes_send; - return Err(WriteError::PartialWrite); - } - } - Err(nix::Error::EPIPE) => return Err(WriteError::ProcessNotRunning), - Err(e) => return Err(WriteError::Internal(e)), - } - } - self.last_partial_write_length = 0; - Ok(()) - } -} - -impl NetBackend for Unixstream { - /// Try to read a frame from the proxy. If no bytes are available reports ReadError::NothingRead - fn read_frame(&mut self, buf: &mut [u8]) -> Result { - if self.expecting_frame_length == 0 { - self.expecting_frame_length = { - let mut frame_length_buf = [0u8; FRAME_HEADER_LEN]; - self.read_loop(&mut frame_length_buf, false)?; - u32::from_be_bytes(frame_length_buf) - }; - } - - let hdr_len = write_virtio_net_hdr(buf); - let buf = &mut buf[hdr_len..]; - let frame_length = self.expecting_frame_length as usize; - self.read_loop(&mut buf[..frame_length], false)?; - self.expecting_frame_length = 0; - log::trace!("Read eth frame from network proxy: {frame_length} bytes"); - Ok(hdr_len + frame_length) - } - - /// Try to write a frame to the proxy. - /// (Will mutate and override parts of buf, with a frame header!) - /// - /// * `hdr_len` - specifies the size of any existing headers encapsulating the ethernet frame, - /// (such as vnet header), that can be overwritten. Must be >= FRAME_HEADER_LEN. - /// * `buf` - the buffer to write to the proxy, `buf[..hdr_len]` may be overwritten - /// - /// If this function returns WriteError::PartialWrite, you have to finish the write using - /// try_finish_write. - fn write_frame(&mut self, hdr_len: usize, buf: &mut [u8]) -> Result<(), WriteError> { - if self.last_partial_write_length != 0 { - panic!("Cannot write a frame to the proxy, while a partial write is not resolved."); - } - assert!( - hdr_len >= FRAME_HEADER_LEN, - "Not enough space to write the frame header" - ); - assert!(buf.len() > hdr_len); - let frame_length = buf.len() - hdr_len; - - buf[hdr_len - FRAME_HEADER_LEN..hdr_len] - .copy_from_slice(&(frame_length as u32).to_be_bytes()); - - self.write_loop(&buf[hdr_len - FRAME_HEADER_LEN..])?; - Ok(()) - } - - fn has_unfinished_write(&self) -> bool { - self.last_partial_write_length != 0 - } - - /// Try to finish a partial write - /// - /// If no partial write is required will do nothing and return Ok(()) - /// - /// * `hdr_len` - must be the same value as passed to write_frame, that caused the partial write - /// * `buf` - must be same buffer that was given to write_frame, that caused the partial write - fn try_finish_write(&mut self, hdr_len: usize, buf: &[u8]) -> Result<(), WriteError> { - if self.last_partial_write_length != 0 { - let already_written = self.last_partial_write_length; - log::trace!("Requested to finish partial write"); - self.write_loop(&buf[hdr_len - FRAME_HEADER_LEN + already_written..])?; - log::debug!("Finished partial write ({already_written}bytes written before)") - } - - Ok(()) - } - - fn raw_socket_fd(&self) -> RawFd { - self.fd.as_raw_fd() - } -} diff --git a/vendor/krun-devices/src/virtio/net/worker.rs b/vendor/krun-devices/src/virtio/net/worker.rs deleted file mode 100644 index 133e23b46..000000000 --- a/vendor/krun-devices/src/virtio/net/worker.rs +++ /dev/null @@ -1,475 +0,0 @@ -use crate::virtio::net::backend::ConnectError; -#[cfg(target_os = "linux")] -use crate::virtio::net::tap::Tap; -use crate::virtio::net::unixgram::Unixgram; -use crate::virtio::net::unixstream::Unixstream; -use crate::virtio::net::{MAX_BUFFER_SIZE, QUEUE_SIZE}; -use crate::virtio::{DeviceQueue, InterruptTransport}; - -use super::backend::{NetBackend, ReadError, WriteError}; -use super::device::{FrontendError, RxError, TxError, VirtioNetBackend}; -use super::VNET_HDR_LEN; - -#[cfg(target_os = "macos")] -use std::os::fd::RawFd; -use std::os::fd::{AsRawFd, FromRawFd, OwnedFd}; -use std::thread; -use std::{cmp, result}; -use utils::epoll::{ControlOperation, Epoll, EpollEvent, EventSet}; -use vm_memory::{Bytes, GuestAddress, GuestMemoryMmap}; - -pub struct NetWorker { - rx_q: DeviceQueue, - tx_q: DeviceQueue, - interrupt: InterruptTransport, - - mem: GuestMemoryMmap, - backend: Box, - - rx_frame_buf: [u8; MAX_BUFFER_SIZE], - rx_frame_buf_len: usize, - rx_has_deferred_frame: bool, - - tx_iovec: Vec<(GuestAddress, usize)>, - tx_frame_buf: [u8; MAX_BUFFER_SIZE], - tx_frame_len: usize, - tx_has_deferred_frame: bool, -} - -impl NetWorker { - pub fn new( - rx_q: DeviceQueue, - tx_q: DeviceQueue, - interrupt: InterruptTransport, - mem: GuestMemoryMmap, - _vnet_features: u64, - cfg_backend: VirtioNetBackend, - ) -> Result { - let backend = match cfg_backend { - VirtioNetBackend::UnixstreamFd(fd) => { - // SAFETY: we need to trust that the library user has configured - // the backend with a healthy file descriptor. - let owned_fd = unsafe { OwnedFd::from_raw_fd(fd) }; - Box::new(Unixstream::new(owned_fd)) as Box - } - VirtioNetBackend::UnixstreamPath(path) => { - Box::new(Unixstream::open(path)?) as Box - } - VirtioNetBackend::UnixgramFd(fd) => { - // SAFETY: we need to trust that the library user has configured - // the backend with a healthy file descriptor. - let owned_fd = unsafe { OwnedFd::from_raw_fd(fd) }; - Box::new(Unixgram::new(owned_fd)) as Box - } - VirtioNetBackend::UnixgramPath(path, vfkit_magic) => { - Box::new(Unixgram::open(path, vfkit_magic)?) as Box - } - #[cfg(target_os = "linux")] - VirtioNetBackend::Tap(tap_name) => { - Box::new(Tap::new(tap_name, _vnet_features)?) as Box - } - }; - - Ok(Self { - rx_q, - tx_q, - - mem, - backend, - interrupt, - - rx_frame_buf: [0u8; MAX_BUFFER_SIZE], - rx_frame_buf_len: 0, - rx_has_deferred_frame: false, - - tx_frame_buf: [0u8; MAX_BUFFER_SIZE], - tx_frame_len: 0, - tx_iovec: Vec::with_capacity(QUEUE_SIZE as usize), - tx_has_deferred_frame: false, - }) - } - - pub fn run(self) { - thread::Builder::new() - .name("virtio-net worker".into()) - .spawn(|| self.work()) - .unwrap(); - } - - fn work(mut self) { - #[cfg(target_os = "macos")] - const TX_TIMER_FD: RawFd = -2; - - let virtq_rx_ev_fd = self.rx_q.event.as_raw_fd(); - let virtq_tx_ev_fd = self.tx_q.event.as_raw_fd(); - let backend_socket = self.backend.raw_socket_fd(); - - let epoll = Epoll::new().unwrap(); - - let _ = epoll.ctl( - ControlOperation::Add, - virtq_rx_ev_fd, - &EpollEvent::new(EventSet::IN, virtq_rx_ev_fd as u64), - ); - let _ = epoll.ctl( - ControlOperation::Add, - virtq_tx_ev_fd, - &EpollEvent::new(EventSet::IN, virtq_tx_ev_fd as u64), - ); - let _ = epoll.ctl( - ControlOperation::Add, - backend_socket, - &EpollEvent::new( - EventSet::IN | EventSet::OUT | EventSet::EDGE_TRIGGERED | EventSet::READ_HANG_UP, - backend_socket as u64, - ), - ); - - loop { - let mut epoll_events = vec![EpollEvent::new(EventSet::empty(), 0); 32]; - match epoll.wait(epoll_events.len(), -1, epoll_events.as_mut_slice()) { - Ok(ev_cnt) => { - for event in &epoll_events[0..ev_cnt] { - let source = event.fd(); - let event_set = event.event_set(); - match event_set { - EventSet::IN if source == virtq_rx_ev_fd => { - self.process_rx_queue_event(); - } - EventSet::IN if source == virtq_tx_ev_fd => { - self.process_tx_queue_event(); - } - _ if source == backend_socket => { - if event_set.contains(EventSet::HANG_UP) - || event_set.contains(EventSet::READ_HANG_UP) - { - log::error!("Got {event_set:?} on backend fd, virtio-net will stop working"); - eprintln!("LIBKRUN VIRTIO-NET FATAL: Backend process seems to have quit or crashed! Networking is now disabled!"); - } else { - if event_set.contains(EventSet::IN) { - self.process_backend_socket_readable() - } - - if event_set.contains(EventSet::OUT) { - self.process_backend_socket_writeable() - } - } - } - #[cfg(target_os = "macos")] - _ if event_set.is_empty() && source == TX_TIMER_FD => { - self.process_tx_loop(); - } - _ => { - log::warn!( - "Received unknown event: {event_set:?} from fd: {source:?}" - ); - } - } - } - - // Arm the retry timer after processing all events, so it - // reflects the final state of tx_has_deferred_frame. - #[cfg(target_os = "macos")] - if self.tx_has_deferred_frame { - let delay = self.backend.write_retry_delay_us(); - if delay > 0 { - epoll.add_oneshot_timer(delay, TX_TIMER_FD as u64); - } - } - } - Err(e) => { - debug!("vsock: failed to consume muxer epoll event: {e}"); - } - } - } - } - - pub(crate) fn process_rx_queue_event(&mut self) { - if let Err(e) = self.rx_q.event.read() { - log::error!("Failed to get rx event from queue: {e:?}"); - } - if let Err(e) = self.rx_q.queue.disable_notification(&self.mem) { - error!("error disabling queue notifications: {e:?}"); - } - if let Err(e) = self.process_rx() { - log::error!("Failed to process rx: {e:?} (triggered by queue event)") - }; - if let Err(e) = self.rx_q.queue.enable_notification(&self.mem) { - error!("error disabling queue notifications: {e:?}"); - } - } - - pub(crate) fn process_tx_queue_event(&mut self) { - match self.tx_q.event.read() { - Ok(_) => self.process_tx_loop(), - Err(e) => { - log::error!("Failed to get tx queue event from queue: {e:?}"); - } - } - } - - pub(crate) fn process_backend_socket_readable(&mut self) { - if let Err(e) = self.rx_q.queue.enable_notification(&self.mem) { - error!("error disabling queue notifications: {e:?}"); - } - if let Err(e) = self.process_rx() { - log::error!("Failed to process rx: {e:?} (triggered by backend socket readable)"); - }; - if let Err(e) = self.rx_q.queue.disable_notification(&self.mem) { - error!("error disabling queue notifications: {e:?}"); - } - } - - pub(crate) fn process_backend_socket_writeable(&mut self) { - match self - .backend - .try_finish_write(VNET_HDR_LEN, &self.tx_frame_buf[..self.tx_frame_len]) - { - Ok(()) => self.process_tx_loop(), - Err(WriteError::PartialWrite | WriteError::NothingWritten) => {} - Err(e @ WriteError::Internal(_)) => { - log::error!("Failed to finish write: {e:?}"); - } - Err(e @ WriteError::ProcessNotRunning) => { - log::debug!("Failed to finish write: {e:?}"); - } - } - } - - fn process_rx(&mut self) -> result::Result<(), RxError> { - // if we have a deferred frame we try to process it first, - // if that is not possible, we don't continue processing other frames - if self.rx_has_deferred_frame { - if self.write_frame_to_guest() { - self.rx_has_deferred_frame = false; - } else { - return Ok(()); - } - } - - let mut signal_queue = false; - - // Read as many frames as possible. - let result = loop { - match self.read_into_rx_frame_buf_from_backend() { - Ok(()) => { - if self.write_frame_to_guest() { - signal_queue = true; - } else { - self.rx_has_deferred_frame = true; - break Ok(()); - } - } - Err(ReadError::NothingRead) => break Ok(()), - Err(e @ ReadError::Internal(_)) => break Err(RxError::Backend(e)), - } - }; - - // At this point we processed as many Rx frames as possible. - // We have to wake the guest if at least one descriptor chain has been used. - if signal_queue { - self.interrupt - .try_signal_used_queue() - .map_err(RxError::DeviceError)?; - } - - result - } - - fn process_tx_loop(&mut self) { - loop { - self.tx_q.queue.disable_notification(&self.mem).unwrap(); - - self.tx_has_deferred_frame = match self.process_tx() { - Err(TxError::Backend(WriteError::NothingWritten)) => true, - Err(e) => { - log::error!("Failed to process tx: {e:?}"); - false - } - _ => false, - }; - - let has_new_entries = self.tx_q.queue.enable_notification(&self.mem).unwrap(); - if self.tx_has_deferred_frame || !has_new_entries { - break; - } - } - } - - fn process_tx(&mut self) -> result::Result<(), TxError> { - let tx_queue = &mut self.tx_q.queue; - - if self.backend.has_unfinished_write() - && self - .backend - .try_finish_write(VNET_HDR_LEN, &self.tx_frame_buf[..self.tx_frame_len]) - .is_err() - { - log::trace!("Cannot process tx because of unfinished partial write!"); - return Ok(()); - } - - let mut raise_irq = false; - let mut result = Ok(()); - - while let Some(head) = tx_queue.pop(&self.mem) { - let head_index = head.index; - let mut next_desc = Some(head); - - self.tx_iovec.clear(); - while let Some(desc) = next_desc { - if desc.is_write_only() { - self.tx_iovec.clear(); - break; - } - self.tx_iovec.push((desc.addr, desc.len as usize)); - next_desc = desc.next_descriptor(); - } - - // Copy buffer from across multiple descriptors. - let mut read_count = 0; - for (desc_addr, desc_len) in self.tx_iovec.drain(..) { - let limit = cmp::min(read_count + desc_len, self.tx_frame_buf.len()); - - let read_result = self - .mem - .read_slice(&mut self.tx_frame_buf[read_count..limit], desc_addr); - match read_result { - Ok(()) => { - read_count += limit - read_count; - } - Err(e) => { - log::error!("Failed to read slice: {e:?}"); - read_count = 0; - break; - } - } - } - - self.tx_frame_len = read_count; - match self - .backend - .write_frame(VNET_HDR_LEN, &mut self.tx_frame_buf[..read_count]) - { - Ok(()) => { - self.tx_frame_len = 0; - tx_queue - .add_used(&self.mem, head_index, 0) - .map_err(TxError::QueueError)?; - raise_irq = true; - } - Err(WriteError::NothingWritten) => { - tx_queue.undo_pop(); - result = Err(TxError::Backend(WriteError::NothingWritten)); - break; - } - Err(WriteError::PartialWrite) => { - log::trace!("process_tx: partial write"); - /* - This situation should be pretty rare, assuming reasonably sized socket buffers. - We have written only a part of a frame to the backend socket (the socket is full). - - The frame we have read from the guest remains in tx_frame_buf, and will be sent - later. - - Note that we cannot wait for the backend to process our sending frames, because - the backend could be blocked on sending a remainder of a frame to us - us waiting - for backend would cause a deadlock. - */ - tx_queue - .add_used(&self.mem, head_index, 0) - .map_err(TxError::QueueError)?; - raise_irq = true; - break; - } - Err(e @ WriteError::Internal(_) | e @ WriteError::ProcessNotRunning) => { - return Err(TxError::Backend(e)) - } - } - } - - if raise_irq && tx_queue.needs_notification(&self.mem).unwrap() { - self.interrupt - .try_signal_used_queue() - .map_err(TxError::DeviceError)?; - } - - result - } - - // Copies a single frame from `self.rx_frame_buf` into the guest. - fn write_frame_to_guest_impl(&mut self) -> result::Result<(), FrontendError> { - let mut result: std::result::Result<(), FrontendError> = Ok(()); - - let queue = &mut self.rx_q.queue; - let head_descriptor = queue.pop(&self.mem).ok_or(FrontendError::EmptyQueue)?; - let head_index = head_descriptor.index; - - let mut frame_slice = &self.rx_frame_buf[..self.rx_frame_buf_len]; - - let frame_len = frame_slice.len(); - let mut maybe_next_descriptor = Some(head_descriptor); - while let Some(descriptor) = &maybe_next_descriptor { - if frame_slice.is_empty() { - break; - } - - if !descriptor.is_write_only() { - result = Err(FrontendError::ReadOnlyDescriptor); - break; - } - - let len = std::cmp::min(frame_slice.len(), descriptor.len as usize); - match self.mem.write_slice(&frame_slice[..len], descriptor.addr) { - Ok(()) => { - frame_slice = &frame_slice[len..]; - } - Err(e) => { - log::error!("Failed to write slice: {e:?}"); - result = Err(FrontendError::GuestMemory(e)); - break; - } - }; - - maybe_next_descriptor = descriptor.next_descriptor(); - } - if result.is_ok() && !frame_slice.is_empty() { - log::warn!("Receiving buffer is too small to hold frame of current size"); - result = Err(FrontendError::DescriptorChainTooSmall); - } - - // Mark the descriptor chain as used. If an error occurred, skip the descriptor chain. - let used_len = if result.is_err() { 0 } else { frame_len as u32 }; - queue - .add_used(&self.mem, head_index, used_len) - .map_err(FrontendError::QueueError)?; - result - } - - // Copies a single frame from `self.rx_frame_buf` into the guest. In case of an error retries - // the operation if possible. Returns true if the operation was successfull. - fn write_frame_to_guest(&mut self) -> bool { - let max_iterations = self.rx_q.queue.actual_size(); - for _ in 0..max_iterations { - match self.write_frame_to_guest_impl() { - Ok(()) => return true, - Err(FrontendError::EmptyQueue) => { - // retry - continue; - } - Err(_) => { - // retry - continue; - } - } - } - - false - } - - /// Fills self.rx_frame_buf with an ethernet frame from backend and prepends virtio_net_hdr to it - fn read_into_rx_frame_buf_from_backend(&mut self) -> result::Result<(), ReadError> { - self.rx_frame_buf_len = self.backend.read_frame(&mut self.rx_frame_buf)?; - Ok(()) - } -} diff --git a/vendor/krun-devices/src/virtio/queue.rs b/vendor/krun-devices/src/virtio/queue.rs deleted file mode 100644 index 2fb74289d..000000000 --- a/vendor/krun-devices/src/virtio/queue.rs +++ /dev/null @@ -1,1143 +0,0 @@ -// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -// -// Portions Copyright 2017 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the THIRD-PARTY file. - -use std::cmp::min; -use std::fmt::{self, Debug, Display}; -use std::num::Wrapping; -use std::sync::atomic::{fence, Ordering}; -use virtio_bindings::virtio_ring::VRING_USED_F_NO_NOTIFY; -use vm_memory::{ - Address, ByteValued, Bytes, GuestAddress, GuestMemory, GuestMemoryError, GuestMemoryMmap, - VolatileMemoryError, -}; - -/// Size of used ring header: flags (u16) + idx (u16) -pub(crate) const VIRTQ_USED_RING_HEADER_SIZE: u64 = 4; - -/// Size of one element in the used ring, id (le32) + len (le32). -pub(crate) const VIRTQ_USED_ELEMENT_SIZE: u64 = 8; - -/// Size of available ring header: flags(u16) + idx(u16) -pub(crate) const VIRTQ_AVAIL_RING_HEADER_SIZE: u64 = 4; - -/// Size of one element in the available ring (le16). -pub(crate) const VIRTQ_AVAIL_ELEMENT_SIZE: u64 = 2; - -pub(super) const VIRTQ_DESC_F_NEXT: u16 = 0x1; -pub(super) const VIRTQ_DESC_F_WRITE: u16 = 0x2; - -/// Virtio Queue related errors. -#[allow(clippy::enum_variant_names)] -#[derive(Debug)] -pub enum Error { - /// Address overflow. - AddressOverflow, - /// Failed to access guest memory. - GuestMemory(GuestMemoryError), - /// Invalid indirect descriptor. - InvalidIndirectDescriptor, - /// Invalid indirect descriptor table. - InvalidIndirectDescriptorTable, - /// Invalid descriptor chain. - InvalidChain, - /// Invalid descriptor index. - InvalidDescriptorIndex, - /// Invalid max_size. - InvalidMaxSize, - /// Invalid Queue Size. - InvalidSize, - /// Invalid alignment of descriptor table address. - InvalidDescTableAlign, - /// Invalid alignment of available ring address. - InvalidAvailRingAlign, - /// Invalid alignment of used ring address. - InvalidUsedRingAlign, - /// Invalid available ring index. - InvalidAvailRingIndex, - /// The queue is not ready for operation. - QueueNotReady, - /// Volatile memory error. - VolatileMemoryError(VolatileMemoryError), - /// The combined length of all the buffers in a `DescriptorChain` would overflow. - DescriptorChainOverflow, - /// No memory region for this address range. - FindMemoryRegion, - /// Descriptor guest memory error. - GuestMemoryError(GuestMemoryError), - /// DescriptorChain split is out of bounds. - SplitOutOfBounds(usize), -} - -impl Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use self::Error::*; - - match self { - AddressOverflow => write!(f, "address overflow"), - GuestMemory(_) => write!(f, "error accessing guest memory"), - InvalidChain => write!(f, "invalid descriptor chain"), - InvalidIndirectDescriptor => write!(f, "invalid indirect descriptor"), - InvalidIndirectDescriptorTable => write!(f, "invalid indirect descriptor table"), - InvalidDescriptorIndex => write!(f, "invalid descriptor index"), - InvalidMaxSize => write!(f, "invalid queue maximum size"), - InvalidSize => write!(f, "invalid queue size"), - InvalidDescTableAlign => write!( - f, - "virtio queue descriptor table breaks alignment constraints" - ), - InvalidAvailRingAlign => write!( - f, - "virtio queue available ring breaks alignment constraints" - ), - InvalidUsedRingAlign => { - write!(f, "virtio queue used ring breaks alignment constraints") - } - InvalidAvailRingIndex => write!( - f, - "invalid available ring index (more descriptors to process than queue size)" - ), - QueueNotReady => write!(f, "trying to process requests on a queue that's not ready"), - VolatileMemoryError(e) => write!(f, "volatile memory error: {e}"), - DescriptorChainOverflow => write!( - f, - "the combined length of all the buffers in a `DescriptorChain` would overflow" - ), - FindMemoryRegion => write!(f, "no memory region for this address range"), - GuestMemoryError(e) => write!(f, "descriptor guest memory error: {e}"), - SplitOutOfBounds(off) => write!(f, "`DescriptorChain` split is out of bounds: {off}"), - } - } -} - -impl std::error::Error for Error {} - -/// Represents the contents of an element from the used virtqueue ring. -// Note that the `ByteValued` implementation of this structure expects the `VirtqUsedElem` to store -// only plain old data types. -#[repr(C)] -#[derive(Clone, Copy, Default, Debug)] -pub struct VirtqUsedElem { - id: u32, - len: u32, -} - -impl VirtqUsedElem { - /// Create a new `VirtqUsedElem` instance. - /// - /// # Arguments - /// * `id` - the index of the used descriptor chain. - /// * `len` - the total length of the descriptor chain which was used (written to). - pub(crate) fn new(id: u32, len: u32) -> Self { - VirtqUsedElem { id, len } - } -} - -// SAFETY: This is safe because `VirtqUsedElem` contains only wrappers over POD types -// and all accesses through safe `vm-memory` API will validate any garbage that could be -// included in there. -unsafe impl ByteValued for VirtqUsedElem {} - -// GuestMemoryMmap::read_obj_from_addr() will be used to fetch the descriptor, -// which has an explicit constraint that the entire descriptor doesn't -// cross the page boundary. Otherwise the descriptor may be splitted into -// two mmap regions which causes failure of GuestMemoryMmap::read_obj_from_addr(). -// -// The Virtio Spec 1.0 defines the alignment of VirtIO descriptor is 16 bytes, -// which fulfills the explicit constraint of GuestMemoryMmap::read_obj_from_addr(). - -/// An iterator over a single descriptor chain. Not to be confused with AvailIter, -/// which iterates over the descriptor chain heads in a queue. -pub struct DescIter<'a> { - next: Option>, -} - -impl<'a> DescIter<'a> { - /// Returns an iterator that only yields the readable descriptors in the chain. - pub fn readable(self) -> impl Iterator> { - self.take_while(DescriptorChain::is_read_only) - } - - /// Returns an iterator that only yields the writable descriptors in the chain. - pub fn writable(self) -> impl Iterator> { - self.skip_while(DescriptorChain::is_read_only) - } -} - -impl<'a> Iterator for DescIter<'a> { - type Item = DescriptorChain<'a>; - - fn next(&mut self) -> Option { - if let Some(current) = self.next.take() { - self.next = current.next_descriptor(); - Some(current) - } else { - None - } - } -} - -/// A virtio descriptor constraints with C representive. -#[repr(C)] -#[derive(Default, Clone, Copy)] -pub struct Descriptor { - pub addr: u64, - pub len: u32, - pub flags: u16, - pub next: u16, -} - -unsafe impl ByteValued for Descriptor {} - -/// A virtio descriptor chain. -#[derive(Clone)] -pub struct DescriptorChain<'a> { - desc_table: GuestAddress, - queue_size: u16, - ttl: u16, // used to prevent infinite chain cycles - - /// Reference to guest memory - pub mem: &'a GuestMemoryMmap, - - /// Index into the descriptor table - pub index: u16, - - /// Guest physical address of device specific data - pub addr: GuestAddress, - - /// Length of device specific data - pub len: u32, - - /// Includes next, write, and indirect bits - pub flags: u16, - - /// Index into the descriptor table of the next descriptor if flags has - /// the next bit set - pub next: u16, -} - -impl<'a> DescriptorChain<'a> { - pub fn checked_new( - mem: &GuestMemoryMmap, - desc_table: GuestAddress, - queue_size: u16, - index: u16, - ) -> Option> { - if index >= queue_size { - return None; - } - - let desc_head = mem.checked_offset(desc_table, (index as usize) * 16)?; - mem.checked_offset(desc_head, 16)?; - - // These reads can't fail unless Guest memory is hopelessly broken. - let desc = match mem.read_obj::(desc_head) { - Ok(ret) => ret, - Err(_) => { - // TODO log address - error!("Failed to read from memory"); - return None; - } - }; - let chain = DescriptorChain { - mem, - desc_table, - queue_size, - ttl: queue_size, - index, - addr: GuestAddress(desc.addr), - len: desc.len, - flags: desc.flags, - next: desc.next, - }; - - if chain.is_valid() { - Some(chain) - } else { - None - } - } - - fn is_valid(&self) -> bool { - !self.has_next() || self.next < self.queue_size - } - - /// Gets if this descriptor chain has another descriptor chain linked after it. - pub fn has_next(&self) -> bool { - self.flags & VIRTQ_DESC_F_NEXT != 0 && self.ttl > 1 - } - - /// If the driver designated this as a write only descriptor. - /// - /// If this is false, this descriptor is read only. - /// Write only means the the emulated device can write and the driver can read. - pub fn is_write_only(&self) -> bool { - self.flags & VIRTQ_DESC_F_WRITE != 0 - } - - /// If the driver designated this as a read only descriptor. - /// - /// If this is false, this descriptor is write only. - /// Read only means the emulated device can read and the driver can write. - pub fn is_read_only(&self) -> bool { - self.flags & VIRTQ_DESC_F_WRITE == 0 - } - - /// Gets the next descriptor in this descriptor chain, if there is one. - /// - /// Note that this is distinct from the next descriptor chain returned by `AvailIter`, which is - /// the head of the next _available_ descriptor chain. - pub fn next_descriptor(&self) -> Option> { - if self.has_next() { - DescriptorChain::checked_new(self.mem, self.desc_table, self.queue_size, self.next).map( - |mut c| { - c.ttl = self.ttl - 1; - c - }, - ) - } else { - None - } - } - - /// Produces an iterator over all the descriptors in this chain. - #[allow(clippy::should_implement_trait)] - pub fn into_iter(self) -> DescIter<'a> { - DescIter { next: Some(self) } - } - - pub fn descriptor(&self) -> Descriptor { - Descriptor { - addr: self.addr.raw_value(), - len: self.len, - flags: self.flags, - next: self.next, - } - } -} - -#[derive(Debug, Eq, PartialEq)] -/// A virtio queue's parameters. -pub struct Queue { - /// The maximal size in elements offered by the device - pub(crate) max_size: u16, - - /// The queue size in elements the driver selected - pub size: u16, - - /// Indicates if the queue is finished with configuration - pub ready: bool, - - /// Guest physical address of the descriptor table - pub desc_table: GuestAddress, - - /// Guest physical address of the available ring - pub avail_ring: GuestAddress, - - /// Guest physical address of the used ring - pub used_ring: GuestAddress, - - pub(crate) next_avail: Wrapping, - pub(crate) next_used: Wrapping, - - /// VIRTIO_F_RING_EVENT_IDX negotiated. - event_idx_enabled: bool, - - /// The number of descriptor chains placed in the used ring via `add_used` - /// since the last time `needs_notification` was called on the associated queue. - num_added: Wrapping, -} - -impl Queue { - /// Constructs an empty virtio queue with the given `max_size`. - pub fn new(max_size: u16) -> Queue { - Queue { - max_size, - size: 0, - ready: false, - desc_table: GuestAddress(0), - avail_ring: GuestAddress(0), - used_ring: GuestAddress(0), - next_avail: Wrapping(0), - next_used: Wrapping(0), - event_idx_enabled: false, - num_added: Wrapping(0), - } - } - - pub fn get_max_size(&self) -> u16 { - self.max_size - } - - /// Return the actual size of the queue, as the driver may not set up a - /// queue as big as the device allows. - pub fn actual_size(&self) -> u16 { - min(self.size, self.max_size) - } - - pub fn is_valid(&self, mem: &GuestMemoryMmap) -> bool { - let queue_size = u64::from(self.actual_size()); - let desc_table = self.desc_table; - let desc_table_size = 16 * queue_size; - let avail_ring = self.avail_ring; - let avail_ring_size = 6 + 2 * queue_size; - let used_ring = self.used_ring; - let used_ring_size = 6 + 8 * queue_size; - if !self.ready { - error!("attempt to use virtio queue that is not marked ready"); - false - } else if self.size > self.max_size || self.size == 0 || (self.size & (self.size - 1)) != 0 - { - error!("virtio queue with invalid size: {}", self.size); - false - } else if desc_table - .checked_add(desc_table_size) - .is_none_or(|v| !mem.address_in_range(v)) - { - error!( - "virtio queue descriptor table goes out of bounds: start:0x{:08x} size:0x{:08x}", - desc_table.raw_value(), - desc_table_size - ); - false - } else if avail_ring - .checked_add(avail_ring_size) - .is_none_or(|v| !mem.address_in_range(v)) - { - error!( - "virtio queue available ring goes out of bounds: start:0x{:08x} size:0x{:08x}", - avail_ring.raw_value(), - avail_ring_size - ); - false - } else if used_ring - .checked_add(used_ring_size) - .is_none_or(|v| !mem.address_in_range(v)) - { - error!( - "virtio queue used ring goes out of bounds: start:0x{:08x} size:0x{:08x}", - used_ring.raw_value(), - used_ring_size - ); - false - } else if desc_table.raw_value() & 0xf != 0 { - error!("virtio queue descriptor table breaks alignment contraints"); - false - } else if avail_ring.raw_value() & 0x1 != 0 { - error!("virtio queue available ring breaks alignment contraints"); - false - } else if used_ring.raw_value() & 0x3 != 0 { - error!("virtio queue used ring breaks alignment contraints"); - false - } else { - true - } - } - - /// Returns the number of yet-to-be-popped descriptor chains in the avail ring. - #[allow(clippy::len_without_is_empty)] - pub fn len(&self, mem: &GuestMemoryMmap) -> u16 { - (self.avail_idx(mem, Ordering::Acquire).unwrap() - self.next_avail).0 - } - - /// Checks if the driver has made any descriptor chains available in the avail ring. - pub fn is_empty(&self, mem: &GuestMemoryMmap) -> bool { - self.len(mem) == 0 - } - - /// Pop the first available descriptor chain from the avail ring. - pub fn pop<'b>(&mut self, mem: &'b GuestMemoryMmap) -> Option> { - if self.len(mem) == 0 || self.actual_size() == 0 { - return None; - } - - // We'll need to find the first available descriptor, that we haven't yet popped. - // In a naive notation, that would be: - // `descriptor_table[avail_ring[next_avail]]`. - // - // First, we compute the byte-offset (into `self.avail_ring`) of the index of the next available - // descriptor. `self.avail_ring` stores the address of a `struct virtq_avail`, as defined by - // the VirtIO spec: - // - // ```C - // struct virtq_avail { - // le16 flags; - // le16 idx; - // le16 ring[QUEUE_SIZE]; - // le16 used_event - // } - // ``` - // - // We use `self.next_avail` to store the position, in `ring`, of the next available - // descriptor index, with a twist: we always only increment `self.next_avail`, so the - // actual position will be `self.next_avail % self.actual_size()`. - // We are now looking for the offset of `ring[self.next_avail % self.actual_size()]`. - // `ring` starts after `flags` and `idx` (4 bytes into `struct virtq_avail`), and holds - // 2-byte items, so the offset will be: - let index_offset = 4 + 2 * (self.next_avail.0 % self.actual_size()); - - // Make sure we catch all updates on the queue - fence(Ordering::Acquire); - - // `self.is_valid()` already performed all the bound checks on the descriptor table - // and virtq rings, so it's safe to unwrap guest memory reads and to use unchecked - // offsets. - let desc_index: u16 = mem - .read_obj(self.avail_ring.unchecked_add(u64::from(index_offset))) - .unwrap(); - - DescriptorChain::checked_new(mem, self.desc_table, self.actual_size(), desc_index) - .inspect(|_| self.next_avail += Wrapping(1)) - } - - /// Undo the effects of the last `self.pop()` call. - /// The caller can use this, if it was unable to consume the last popped descriptor chain. - pub fn undo_pop(&mut self) { - self.next_avail -= Wrapping(1); - } - - pub fn add_used( - &mut self, - mem: &GuestMemoryMmap, - head_index: u16, - len: u32, - ) -> Result<(), Error> { - if head_index >= self.size { - error!("attempted to add out of bounds descriptor to used ring: {head_index}"); - return Err(Error::InvalidDescriptorIndex); - } - - let next_used_index = u64::from(self.next_used.0 % self.size); - // This can not overflow an u64 since it is working with relatively small numbers compared - // to u64::MAX. - let offset = VIRTQ_USED_RING_HEADER_SIZE + next_used_index * VIRTQ_USED_ELEMENT_SIZE; - let addr = self - .used_ring - .checked_add(offset) - .ok_or(Error::AddressOverflow)?; - mem.write_obj(VirtqUsedElem::new(head_index.into(), len), addr) - .map_err(Error::GuestMemory)?; - - self.next_used += Wrapping(1); - self.num_added += Wrapping(1); - - mem.store( - self.next_used.0, - self.used_ring - .checked_add(2) - .ok_or(Error::AddressOverflow)?, - Ordering::Release, - ) - .map_err(Error::GuestMemory) - } - - // Return the value present in the used_event field of the avail ring. - // - // If the VIRTIO_F_EVENT_IDX feature bit is not negotiated, the flags field in the available - // ring offers a crude mechanism for the driver to inform the device that it doesn’t want - // interrupts when buffers are used. Otherwise virtq_avail.used_event is a more performant - // alternative where the driver specifies how far the device can progress before interrupting. - // - // Neither of these interrupt suppression methods are reliable, as they are not synchronized - // with the device, but they serve as useful optimizations. So we only ensure access to the - // virtq_avail.used_event is atomic, but do not need to synchronize with other memory accesses. - fn used_event(&self, mem: &GuestMemoryMmap, order: Ordering) -> Result, Error> { - // This can not overflow an u64 since it is working with relatively small numbers compared - // to u64::MAX. - let used_event_offset = - VIRTQ_AVAIL_RING_HEADER_SIZE + u64::from(self.size) * VIRTQ_AVAIL_ELEMENT_SIZE; - let used_event_addr = self - .avail_ring - .checked_add(used_event_offset) - .ok_or(Error::AddressOverflow)?; - - mem.load(used_event_addr, order) - .map(Wrapping) - .map_err(Error::GuestMemory) - } - - // Helper method that writes `val` to the `avail_event` field of the used ring, using - // the provided ordering. - fn set_avail_event( - &self, - mem: &GuestMemoryMmap, - val: u16, - order: Ordering, - ) -> Result<(), Error> { - // This can not overflow an u64 since it is working with relatively small numbers compared - // to u64::MAX. - let avail_event_offset = - VIRTQ_USED_RING_HEADER_SIZE + VIRTQ_USED_ELEMENT_SIZE * u64::from(self.size); - let addr = self - .used_ring - .checked_add(avail_event_offset) - .ok_or(Error::AddressOverflow)?; - - mem.store(val, addr, order).map_err(Error::GuestMemory) - } - - pub fn set_event_idx(&mut self, enabled: bool) { - self.event_idx_enabled = enabled; - } - - // Set the value of the `flags` field of the used ring, applying the specified ordering. - fn set_used_flags( - &mut self, - mem: &GuestMemoryMmap, - val: u16, - order: Ordering, - ) -> Result<(), Error> { - mem.store(val, self.used_ring, order) - .map_err(Error::GuestMemory) - } - - // Write the appropriate values to enable or disable notifications from the driver. - // - // Every access in this method uses `Relaxed` ordering because a fence is added by the caller - // when appropriate. - fn set_notification(&mut self, mem: &GuestMemoryMmap, enable: bool) -> Result<(), Error> { - if enable { - if self.event_idx_enabled { - // We call `set_avail_event` using the `next_avail` value, instead of reading - // and using the current `avail_idx` to avoid missing notifications. More - // details in `enable_notification`. - self.set_avail_event(mem, self.next_avail.0, Ordering::Relaxed) - } else { - self.set_used_flags(mem, 0, Ordering::Relaxed) - } - } else if !self.event_idx_enabled { - self.set_used_flags(mem, VRING_USED_F_NO_NOTIFY as u16, Ordering::Relaxed) - } else { - // Notifications are effectively disabled by default after triggering once when - // `VIRTIO_F_EVENT_IDX` is negotiated, so we don't do anything in that case. - Ok(()) - } - } - - // TODO: Turn this into a doc comment/example. - // With the current implementation, a common way of consuming entries from the available ring - // while also leveraging notification suppression is to use a loop, for example: - // - // loop { - // // We have to explicitly disable notifications if `VIRTIO_F_EVENT_IDX` has not been - // // negotiated. - // self.disable_notification()?; - // - // for chain in self.iter()? { - // // Do something with each chain ... - // // Let's assume we process all available chains here. - // } - // - // // If `enable_notification` returns `true`, the driver has added more entries to the - // // available ring. - // if !self.enable_notification()? { - // break; - // } - // } - pub fn enable_notification(&mut self, mem: &GuestMemoryMmap) -> Result { - self.set_notification(mem, true)?; - // Ensures the following read is not reordered before any previous write operation. - fence(Ordering::SeqCst); - - // We double check here to avoid the situation where the available ring has been updated - // just before we re-enabled notifications, and it's possible to miss one. We compare the - // current `avail_idx` value to `self.next_avail` because it's where we stopped processing - // entries. There are situations where we intentionally avoid processing everything in the - // available ring (which will cause this method to return `true`), but in that case we'll - // probably not re-enable notifications as we already know there are pending entries. - self.avail_idx(mem, Ordering::Relaxed) - .map(|idx| idx != self.next_avail) - } - - pub fn disable_notification(&mut self, mem: &GuestMemoryMmap) -> Result<(), Error> { - self.set_notification(mem, false) - } - - pub fn needs_notification(&mut self, mem: &GuestMemoryMmap) -> Result { - let used_idx = self.next_used; - - // Complete all the writes in add_used() before reading the event. - fence(Ordering::SeqCst); - - // The VRING_AVAIL_F_NO_INTERRUPT flag isn't supported yet. - - // When the `EVENT_IDX` feature is negotiated, the driver writes into `used_event` - // a value that's used by the device to determine whether a notification must - // be submitted after adding a descriptor chain to the used ring. According to the - // standard, the notification must be sent when `next_used == used_event + 1`, but - // various device model implementations rely on an inequality instead, most likely - // to also support use cases where a bunch of descriptor chains are added to the used - // ring first, and only afterwards the `needs_notification` logic is called. For example, - // the approach based on `num_added` below is taken from the Linux Kernel implementation - // (i.e. https://elixir.bootlin.com/linux/v5.15.35/source/drivers/virtio/virtio_ring.c#L661) - - // The `old` variable below is used to determine the value of `next_used` from when - // `needs_notification` was called last (each `needs_notification` call resets `num_added` - // to zero, while each `add_used` called increments it by one). Then, the logic below - // uses wrapped arithmetic to see whether `used_event` can be found between `old` and - // `next_used` in the circular sequence space of the used ring. - if self.event_idx_enabled { - let used_event = self.used_event(mem, Ordering::Relaxed)?; - let old = used_idx - self.num_added; - self.num_added = Wrapping(0); - return Ok(used_idx - used_event - Wrapping(1) < used_idx - old); - } - - Ok(true) - } - - /// Goes back one position in the available descriptor chain offered by the driver. - /// Rust does not support bidirectional iterators. This is the only way to revert the effect - /// of an iterator increment on the queue. - pub fn go_to_previous_position(&mut self) { - self.next_avail -= Wrapping(1); - } - - /// Fetch the available ring index (`virtq_avail->idx`) from guest memory. - /// This is written by the driver, to indicate the next slot that will be filled in the avail - /// ring. - fn avail_idx(&self, mem: &GuestMemoryMmap, order: Ordering) -> Result, Error> { - let addr = self - .avail_ring - .checked_add(2) - .ok_or(Error::AddressOverflow)?; - - mem.load(addr, order) - .map(Wrapping) - .map_err(Error::GuestMemory) - } -} - -#[cfg(test)] -pub(crate) mod tests { - use std::marker::PhantomData; - use std::mem; - - pub use super::*; - use vm_memory::{GuestAddress, GuestMemoryMmap}; - - // Represents a location in GuestMemoryMmap which holds a given type. - pub struct SomeplaceInMemory<'a, T> { - pub location: GuestAddress, - mem: &'a GuestMemoryMmap, - phantom: PhantomData<*const T>, - } - - // The ByteValued trait is required to use mem.read_obj_from_addr and write_obj_at_addr. - impl<'a, T> SomeplaceInMemory<'a, T> - where - T: vm_memory::ByteValued, - { - fn new(location: GuestAddress, mem: &'a GuestMemoryMmap) -> Self { - SomeplaceInMemory { - location, - mem, - phantom: PhantomData, - } - } - - // Reads from the actual memory location. - pub fn get(&self) -> T { - self.mem.read_obj(self.location).unwrap() - } - - // Writes to the actual memory location. - pub fn set(&self, val: T) { - self.mem.write_obj(val, self.location).unwrap() - } - - // This function returns a place in memory which holds a value of type U, and starts - // offset bytes after the current location. - fn map_offset(&self, offset: usize) -> SomeplaceInMemory<'a, U> { - SomeplaceInMemory { - location: self.location.checked_add(offset as u64).unwrap(), - mem: self.mem, - phantom: PhantomData, - } - } - - // This function returns a place in memory which holds a value of type U, and starts - // immediately after the end of self (which is location + sizeof(T)). - fn next_place(&self) -> SomeplaceInMemory<'a, U> { - self.map_offset::(mem::size_of::()) - } - - fn end(&self) -> GuestAddress { - self.location - .checked_add(mem::size_of::() as u64) - .unwrap() - } - } - - // Represents a virtio descriptor in guest memory. - pub struct VirtqDesc<'a> { - pub addr: SomeplaceInMemory<'a, u64>, - pub len: SomeplaceInMemory<'a, u32>, - pub flags: SomeplaceInMemory<'a, u16>, - pub next: SomeplaceInMemory<'a, u16>, - } - - impl<'a> VirtqDesc<'a> { - fn new(start: GuestAddress, mem: &'a GuestMemoryMmap) -> Self { - assert_eq!(start.0 & 0xf, 0); - - let addr = SomeplaceInMemory::new(start, mem); - let len = addr.next_place(); - let flags = len.next_place(); - let next = flags.next_place(); - - VirtqDesc { - addr, - len, - flags, - next, - } - } - - fn start(&self) -> GuestAddress { - self.addr.location - } - - fn end(&self) -> GuestAddress { - self.next.end() - } - - pub fn set(&self, addr: u64, len: u32, flags: u16, next: u16) { - self.addr.set(addr); - self.len.set(len); - self.flags.set(flags); - self.next.set(next); - } - } - - // Represents a virtio queue ring. The only difference between the used and available rings, - // is the ring element type. - pub struct VirtqRing<'a, T> { - pub flags: SomeplaceInMemory<'a, u16>, - pub idx: SomeplaceInMemory<'a, u16>, - pub ring: Vec>, - pub event: SomeplaceInMemory<'a, u16>, - } - - impl<'a, T> VirtqRing<'a, T> - where - T: vm_memory::ByteValued, - { - fn new( - start: GuestAddress, - mem: &'a GuestMemoryMmap, - qsize: u16, - alignment: usize, - ) -> Self { - assert_eq!(start.0 & (alignment as u64 - 1), 0); - - let flags = SomeplaceInMemory::new(start, mem); - let idx = flags.next_place(); - - let mut ring = Vec::with_capacity(qsize as usize); - - ring.push(idx.next_place()); - - for _ in 1..qsize as usize { - let x = ring.last().unwrap().next_place(); - ring.push(x) - } - - let event = ring.last().unwrap().next_place(); - - flags.set(0); - idx.set(0); - event.set(0); - - VirtqRing { - flags, - idx, - ring, - event, - } - } - - pub fn end(&self) -> GuestAddress { - self.event.end() - } - } - - #[repr(C)] - #[derive(Clone, Copy, Default)] - pub struct VirtqUsedElem { - pub id: u32, - pub len: u32, - } - - unsafe impl vm_memory::ByteValued for VirtqUsedElem {} - - pub type VirtqAvail<'a> = VirtqRing<'a, u16>; - pub type VirtqUsed<'a> = VirtqRing<'a, VirtqUsedElem>; - - pub struct VirtQueue<'a> { - pub dtable: Vec>, - pub avail: VirtqAvail<'a>, - pub used: VirtqUsed<'a>, - } - - impl<'a> VirtQueue<'a> { - // We try to make sure things are aligned properly :-s - pub fn new(start: GuestAddress, mem: &'a GuestMemoryMmap, qsize: u16) -> Self { - // power of 2? - assert!(qsize > 0 && qsize & (qsize - 1) == 0); - - let mut dtable = Vec::with_capacity(qsize as usize); - - let mut end = start; - - for _ in 0..qsize { - let d = VirtqDesc::new(end, mem); - end = d.end(); - dtable.push(d); - } - - const AVAIL_ALIGN: usize = 2; - - let avail = VirtqAvail::new(end, mem, qsize, AVAIL_ALIGN); - - const USED_ALIGN: u64 = 4; - - let mut x = avail.end().0; - x = (x + USED_ALIGN - 1) & !(USED_ALIGN - 1); - - let used = VirtqUsed::new(GuestAddress(x), mem, qsize, USED_ALIGN as usize); - - VirtQueue { - dtable, - avail, - used, - } - } - - pub fn size(&self) -> u16 { - self.dtable.len() as u16 - } - - fn dtable_start(&self) -> GuestAddress { - self.dtable.first().unwrap().start() - } - - fn avail_start(&self) -> GuestAddress { - self.avail.flags.location - } - - fn used_start(&self) -> GuestAddress { - self.used.flags.location - } - - // Creates a new Queue, using the underlying memory regions represented by the VirtQueue. - pub fn create_queue(&self) -> Queue { - let mut q = Queue::new(self.size()); - - q.size = self.size(); - q.ready = true; - q.desc_table = self.dtable_start(); - q.avail_ring = self.avail_start(); - q.used_ring = self.used_start(); - - q - } - - pub fn end(&self) -> GuestAddress { - self.used.end() - } - } - - #[test] - fn test_checked_new_descriptor_chain() { - let m = &GuestMemoryMmap::from_ranges(&[ - (GuestAddress(0), 0x10000), - (GuestAddress(0x20000), 0x2000), - ]) - .unwrap(); - let vq = VirtQueue::new(GuestAddress(0), m, 16); - - assert!(vq.end().0 < 0x1000); - - // index >= queue_size - assert!(DescriptorChain::checked_new(m, vq.dtable_start(), 16, 16).is_none()); - - // desc_table address is way off - assert!(DescriptorChain::checked_new(m, GuestAddress(0x00ff_ffff_ffff), 16, 0).is_none()); - - // Let's create an invalid chain. - { - // The first desc has a normal len, and the next_descriptor flag is set. - vq.dtable[0].addr.set(0x1000); - vq.dtable[0].len.set(0x1000); - vq.dtable[0].flags.set(VIRTQ_DESC_F_NEXT); - // .. but the the index of the next descriptor is too large - vq.dtable[0].next.set(16); - - assert!(DescriptorChain::checked_new(m, vq.dtable_start(), 16, 0).is_none()); - } - - // Finally, let's test an ok chain. - { - vq.dtable[0].next.set(1); - vq.dtable[1].set(0x2000, 0x1000, 0, 0); - - let c = DescriptorChain::checked_new(m, vq.dtable_start(), 16, 0).unwrap(); - - assert_eq!(c.mem as *const GuestMemoryMmap, m as *const GuestMemoryMmap); - assert_eq!(c.desc_table, vq.dtable_start()); - assert_eq!(c.queue_size, 16); - assert_eq!(c.ttl, c.queue_size); - assert_eq!(c.index, 0); - assert_eq!(c.addr, GuestAddress(0x1000)); - assert_eq!(c.len, 0x1000); - assert_eq!(c.flags, VIRTQ_DESC_F_NEXT); - assert_eq!(c.next, 1); - - assert!(c.next_descriptor().unwrap().next_descriptor().is_none()); - } - } - - #[test] - #[allow(unused)] - fn test_queue_validation() { - let m = &GuestMemoryMmap::from_ranges(&[(GuestAddress(0), 0x10000)]).unwrap(); - let vq = VirtQueue::new(GuestAddress(0), m, 16); - - let mut q = vq.create_queue(); - - // q is currently valid - assert!(q.is_valid(m)); - - // shouldn't be valid when not marked as ready - q.ready = false; - assert!(!q.is_valid(m)); - q.ready = true; - - // or when size > max_size - q.size = q.max_size << 1; - assert!(!q.is_valid(m)); - q.size = q.max_size; - - // or when size is 0 - q.size = 0; - assert!(!q.is_valid(m)); - q.size = q.max_size; - - // or when size is not a power of 2 - q.size = 11; - assert!(!q.is_valid(m)); - q.size = q.max_size; - - // or if the various addresses are off - - q.desc_table = GuestAddress(0xffff_ffff); - assert!(!q.is_valid(m)); - q.desc_table = GuestAddress(0x1001); - assert!(!q.is_valid(m)); - q.desc_table = vq.dtable_start(); - - q.avail_ring = GuestAddress(0xffff_ffff); - assert!(!q.is_valid(m)); - q.avail_ring = GuestAddress(0x1001); - assert!(!q.is_valid(m)); - q.avail_ring = vq.avail_start(); - - q.used_ring = GuestAddress(0xffff_ffff); - assert!(!q.is_valid(m)); - q.used_ring = GuestAddress(0x1001); - assert!(!q.is_valid(m)); - q.used_ring = vq.used_start(); - } - - #[test] - fn test_queue_processing() { - let m = &GuestMemoryMmap::from_ranges(&[(GuestAddress(0), 0x10000)]).unwrap(); - let vq = VirtQueue::new(GuestAddress(0), m, 16); - let mut q = vq.create_queue(); - - q.ready = true; - - // Let's create two simple descriptor chains. - - for j in 0..5 { - vq.dtable[j].set( - 0x1000 * (j + 1) as u64, - 0x1000, - VIRTQ_DESC_F_NEXT, - (j + 1) as u16, - ); - } - - // the chains are (0, 1) and (2, 3, 4) - vq.dtable[1].flags.set(0); - vq.dtable[4].flags.set(0); - vq.avail.ring[0].set(0); - vq.avail.ring[1].set(2); - vq.avail.idx.set(2); - - // We've just set up two chains. - assert_eq!(q.len(m), 2); - - // The first chain should hold exactly two descriptors. - let d = q.pop(m).unwrap().next_descriptor().unwrap(); - assert!(!d.has_next()); - assert!(d.next_descriptor().is_none()); - - // We popped one chain, so there should be only one left. - assert_eq!(q.len(m), 1); - - // The next chain holds three descriptors. - let d = q - .pop(m) - .unwrap() - .next_descriptor() - .unwrap() - .next_descriptor() - .unwrap(); - assert!(!d.has_next()); - assert!(d.next_descriptor().is_none()); - - // We've popped both chains, so the queue should be empty. - assert!(q.is_empty(m)); - assert!(q.pop(m).is_none()); - - // Undoing the last pop should let us walk the last chain again. - q.undo_pop(); - assert_eq!(q.len(m), 1); - - // Walk the last chain again (three descriptors). - let d = q - .pop(m) - .unwrap() - .next_descriptor() - .unwrap() - .next_descriptor() - .unwrap(); - assert!(!d.has_next()); - assert!(d.next_descriptor().is_none()); - } - - #[test] - fn test_add_used() { - let m = &GuestMemoryMmap::from_ranges(&[(GuestAddress(0), 0x10000)]).unwrap(); - let vq = VirtQueue::new(GuestAddress(0), m, 16); - - let mut q = vq.create_queue(); - assert_eq!(vq.used.idx.get(), 0); - - //index too large - let _ = q.add_used(m, 16, 0x1000); - assert_eq!(vq.used.idx.get(), 0); - - //should be ok - let _ = q.add_used(m, 1, 0x1000); - assert_eq!(vq.used.idx.get(), 1); - let x = vq.used.ring[0].get(); - assert_eq!(x.id, 1); - assert_eq!(x.len, 0x1000); - } -} diff --git a/vendor/krun-devices/src/virtio/rng/device.rs b/vendor/krun-devices/src/virtio/rng/device.rs deleted file mode 100644 index c008c6189..000000000 --- a/vendor/krun-devices/src/virtio/rng/device.rs +++ /dev/null @@ -1,158 +0,0 @@ -use rand::{rngs::OsRng, TryRngCore}; -use utils::eventfd::EventFd; -use vm_memory::{Bytes, GuestMemoryMmap}; - -use super::super::{ - ActivateError, ActivateResult, DeviceQueue, DeviceState, QueueConfig, RngError, VirtioDevice, -}; -use super::{defs, defs::uapi}; -use crate::virtio::InterruptTransport; - -// Request queue. -pub(crate) const REQ_INDEX: usize = 0; - -// Supported features. -pub(crate) const AVAIL_FEATURES: u64 = 1 << uapi::VIRTIO_F_VERSION_1 as u64; - -pub struct Rng { - pub(crate) queues: Option>, - pub(crate) avail_features: u64, - pub(crate) acked_features: u64, - pub(crate) activate_evt: EventFd, - pub(crate) device_state: DeviceState, -} - -impl Rng { - pub(crate) fn queue_event(&self, idx: usize) -> &std::sync::Arc { - &self.queues.as_ref().expect("queues should exist")[idx].event - } - - pub fn new() -> super::Result { - Ok(Rng { - queues: None, - avail_features: AVAIL_FEATURES, - acked_features: 0, - activate_evt: EventFd::new(utils::eventfd::EFD_NONBLOCK).map_err(RngError::EventFd)?, - device_state: DeviceState::Inactive, - }) - } - - pub fn id(&self) -> &str { - defs::RNG_DEV_ID - } - - pub fn process_req(&mut self) -> bool { - debug!("rng: process_req()"); - let mem = match self.device_state { - DeviceState::Activated(ref mem, _) => mem, - // This should never happen, it's been already validated in the event handler. - DeviceState::Inactive => unreachable!(), - }; - - let queues = self - .queues - .as_mut() - .expect("queues should exist when activated"); - let mut have_used = false; - - while let Some(head) = queues[REQ_INDEX].queue.pop(mem) { - let index = head.index; - let mut written = 0; - for desc in head.into_iter() { - let mut rand_bytes = vec![0u8; desc.len as usize]; - if let Err(e) = OsRng.try_fill_bytes(&mut rand_bytes) { - error!("Failed to fill buffer with random data: {e:?}"); - queues[REQ_INDEX].queue.go_to_previous_position(); - break; - } - if let Err(e) = mem.write_slice(&rand_bytes[..], desc.addr) { - error!("Failed to write slice: {e:?}"); - queues[REQ_INDEX].queue.go_to_previous_position(); - break; - } - written += desc.len; - } - - have_used = true; - if let Err(e) = queues[REQ_INDEX].queue.add_used(mem, index, written) { - error!("failed to add used elements to the queue: {e:?}"); - } - } - - have_used - } -} - -impl VirtioDevice for Rng { - fn avail_features(&self) -> u64 { - self.avail_features - } - - fn acked_features(&self) -> u64 { - self.acked_features - } - - fn set_acked_features(&mut self, acked_features: u64) { - self.acked_features = acked_features - } - - fn device_type(&self) -> u32 { - uapi::VIRTIO_ID_RNG - } - - fn device_name(&self) -> &str { - "rng" - } - - fn queue_config(&self) -> &[QueueConfig] { - &defs::QUEUE_CONFIG - } - - fn read_config(&self, _offset: u64, _data: &mut [u8]) { - error!("rng: invalid request to read config space"); - } - - fn write_config(&mut self, offset: u64, data: &[u8]) { - warn!( - "rng: guest driver attempted to write device config (offset={:x}, len={:x})", - offset, - data.len() - ); - } - - fn activate( - &mut self, - mem: GuestMemoryMmap, - interrupt: InterruptTransport, - queues: Vec, - ) -> ActivateResult { - if queues.len() != defs::NUM_QUEUES { - error!( - "Cannot perform activate. Expected {} queue(s), got {}", - defs::NUM_QUEUES, - queues.len() - ); - return Err(ActivateError::BadActivate); - } - - if self.activate_evt.write(1).is_err() { - error!("Cannot write to activate_evt",); - return Err(ActivateError::BadActivate); - } - - self.queues = Some(queues); - self.device_state = DeviceState::Activated(mem, interrupt); - - Ok(()) - } - - fn is_activated(&self) -> bool { - self.device_state.is_activated() - } - - fn reset(&mut self) -> bool { - self.queues = None; - self.device_state = DeviceState::Inactive; - true - } -} diff --git a/vendor/krun-devices/src/virtio/rng/event_handler.rs b/vendor/krun-devices/src/virtio/rng/event_handler.rs deleted file mode 100644 index 614e8b30d..000000000 --- a/vendor/krun-devices/src/virtio/rng/event_handler.rs +++ /dev/null @@ -1,80 +0,0 @@ -use std::os::unix::io::AsRawFd; - -use polly::event_manager::{EventManager, Subscriber}; -use utils::epoll::{EpollEvent, EventSet}; - -use super::device::{Rng, REQ_INDEX}; -use crate::virtio::device::VirtioDevice; - -impl Rng { - pub(crate) fn handle_req_event(&mut self, event: &EpollEvent) { - debug!("rng: request queue event"); - - let event_set = event.event_set(); - if event_set != EventSet::IN { - warn!("rng: request queue unexpected event {event_set:?}"); - return; - } - - if let Err(e) = self.queue_event(REQ_INDEX).read() { - error!("Failed to read request queue event: {e:?}"); - } else if self.process_req() { - self.device_state.signal_used_queue(); - } - } - - fn handle_activate_event(&self, event_manager: &mut EventManager) { - debug!("rng: activate event"); - if let Err(e) = self.activate_evt.read() { - error!("Failed to consume rng activate event: {e:?}"); - } - - // The subscriber must exist as we previously registered activate_evt via - // `interest_list()`. - let self_subscriber = event_manager - .subscriber(self.activate_evt.as_raw_fd()) - .unwrap(); - - event_manager - .register( - self.queue_event(REQ_INDEX).as_raw_fd(), - EpollEvent::new(EventSet::IN, self.queue_event(REQ_INDEX).as_raw_fd() as u64), - self_subscriber.clone(), - ) - .unwrap_or_else(|e| { - error!("Failed to register rng frq with event manager: {e:?}"); - }); - - event_manager - .unregister(self.activate_evt.as_raw_fd()) - .unwrap_or_else(|e| { - error!("Failed to unregister rng activate evt: {e:?}"); - }) - } -} - -impl Subscriber for Rng { - fn process(&mut self, event: &EpollEvent, event_manager: &mut EventManager) { - let source = event.fd(); - let activate_evt = self.activate_evt.as_raw_fd(); - if source == activate_evt { - self.handle_activate_event(event_manager); - } else if self.is_activated() { - let req = self.queue_event(REQ_INDEX).as_raw_fd(); - if source == req { - self.handle_req_event(event); - } else { - warn!("Unexpected rng event received: {source:?}") - } - } else { - warn!("rng: The device is not yet activated. Spurious event received: {source:?}"); - } - } - - fn interest_list(&self) -> Vec { - vec![EpollEvent::new( - EventSet::IN, - self.activate_evt.as_raw_fd() as u64, - )] - } -} diff --git a/vendor/krun-devices/src/virtio/rng/mod.rs b/vendor/krun-devices/src/virtio/rng/mod.rs deleted file mode 100644 index 864effd63..000000000 --- a/vendor/krun-devices/src/virtio/rng/mod.rs +++ /dev/null @@ -1,27 +0,0 @@ -mod device; -mod event_handler; - -pub use self::defs::uapi::VIRTIO_ID_RNG as TYPE_RNG; -pub use self::device::Rng; - -mod defs { - use crate::virtio::QueueConfig; - - pub const RNG_DEV_ID: &str = "virtio_rng"; - pub const NUM_QUEUES: usize = 1; - const QUEUE_SIZE: u16 = 256; - pub static QUEUE_CONFIG: [QueueConfig; NUM_QUEUES] = [QueueConfig::new(QUEUE_SIZE); NUM_QUEUES]; - - pub mod uapi { - pub const VIRTIO_F_VERSION_1: u32 = 32; - pub const VIRTIO_ID_RNG: u32 = 4; - } -} - -#[derive(Debug)] -pub enum RngError { - /// Failed to create event fd. - EventFd(std::io::Error), -} - -type Result = std::result::Result; diff --git a/vendor/krun-devices/src/virtio/snd/audio_backends.rs b/vendor/krun-devices/src/virtio/snd/audio_backends.rs deleted file mode 100644 index f35cbd72f..000000000 --- a/vendor/krun-devices/src/virtio/snd/audio_backends.rs +++ /dev/null @@ -1,81 +0,0 @@ -// Manos Pitsidianakis -// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause - -mod pipewire; - -use std::sync::{Arc, RwLock}; - -use self::pipewire::PwBackend; -use super::{stream::Stream, BackendType, Result, VirtioSndPcmSetParams}; - -pub trait AudioBackend { - fn write(&self, stream_id: u32) -> Result<()>; - - #[allow(dead_code)] - fn read(&self, stream_id: u32) -> Result<()>; - - fn set_parameters(&self, _stream_id: u32, _: VirtioSndPcmSetParams) -> Result<()> { - Ok(()) - } - - fn prepare(&self, _stream_id: u32) -> Result<()> { - Ok(()) - } - - fn release(&self, _stream_id: u32) -> Result<()> { - Ok(()) - } - - fn start(&self, _stream_id: u32) -> Result<()> { - Ok(()) - } - - fn stop(&self, _stream_id: u32) -> Result<()> { - Ok(()) - } - - #[cfg(test)] - fn as_any(&self) -> &dyn std::any::Any; -} - -pub fn alloc_audio_backend( - backend: BackendType, - streams: Arc>>, -) -> Result> { - log::trace!("allocating audio backend {backend:?}"); - match backend { - BackendType::Pipewire => Ok(Box::new(PwBackend::new(streams))), - } -} - -#[cfg(test)] -mod tests { - use std::any::TypeId; - - use super::*; - - #[test] - fn test_alloc_audio_backend() { - crate::init_logger(); - { - let v = BackendType::Null; - let value = alloc_audio_backend(v, Default::default()).unwrap(); - assert_eq!(TypeId::of::(), value.as_any().type_id()); - } - #[cfg(all(feature = "pw-backend", target_env = "gnu"))] - { - use pipewire::{test_utils::PipewireTestHarness, *}; - - let _test_harness = PipewireTestHarness::new(); - let v = BackendType::Pipewire; - let value = alloc_audio_backend(v, Default::default()).unwrap(); - assert_eq!(TypeId::of::(), value.as_any().type_id()); - } - #[cfg(all(feature = "alsa-backend", target_env = "gnu"))] - { - let v = BackendType::Alsa; - let value = alloc_audio_backend(v, Default::default()).unwrap(); - assert_eq!(TypeId::of::(), value.as_any().type_id()); - } - } -} diff --git a/vendor/krun-devices/src/virtio/snd/audio_backends/pipewire.rs b/vendor/krun-devices/src/virtio/snd/audio_backends/pipewire.rs deleted file mode 100644 index 89415b2fb..000000000 --- a/vendor/krun-devices/src/virtio/snd/audio_backends/pipewire.rs +++ /dev/null @@ -1,650 +0,0 @@ -// Pipewire backend device -// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause - -use std::{ - collections::HashMap, - convert::TryFrom, - mem::size_of, - ptr, - sync::{Arc, RwLock}, -}; - -use log::debug; -use pw::{ - context::ContextRc, core::CoreRc, properties::properties, spa, sys::PW_ID_CORE, - thread_loop::ThreadLoopRc, -}; -use spa::{ - param::{ - audio::{AudioFormat, AudioInfoRaw}, - ParamType, - }, - pod::{serialize::PodSerializer, Object, Pod, Value}, - sys::{ - spa_audio_info_raw, SPA_PARAM_EnumFormat, SPA_TYPE_OBJECT_Format, SPA_AUDIO_CHANNEL_FC, - SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_LFE, SPA_AUDIO_CHANNEL_MONO, - SPA_AUDIO_CHANNEL_RC, SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR, - SPA_AUDIO_CHANNEL_UNKNOWN, SPA_AUDIO_FORMAT_ALAW, SPA_AUDIO_FORMAT_F32, - SPA_AUDIO_FORMAT_F64, SPA_AUDIO_FORMAT_S16, SPA_AUDIO_FORMAT_S18_LE, SPA_AUDIO_FORMAT_S20, - SPA_AUDIO_FORMAT_S20_LE, SPA_AUDIO_FORMAT_S24, SPA_AUDIO_FORMAT_S24_LE, - SPA_AUDIO_FORMAT_S32, SPA_AUDIO_FORMAT_S8, SPA_AUDIO_FORMAT_U16, SPA_AUDIO_FORMAT_U18_LE, - SPA_AUDIO_FORMAT_U20, SPA_AUDIO_FORMAT_U20_LE, SPA_AUDIO_FORMAT_U24, - SPA_AUDIO_FORMAT_U24_LE, SPA_AUDIO_FORMAT_U32, SPA_AUDIO_FORMAT_U8, SPA_AUDIO_FORMAT_ULAW, - SPA_AUDIO_FORMAT_UNKNOWN, - }, -}; - -use super::super::{ - stream::{Error as StreamError, PCMState}, - virtio_sound::{ - VirtioSndPcmSetParams, VIRTIO_SND_PCM_FMT_A_LAW, VIRTIO_SND_PCM_FMT_FLOAT, - VIRTIO_SND_PCM_FMT_FLOAT64, VIRTIO_SND_PCM_FMT_MU_LAW, VIRTIO_SND_PCM_FMT_S16, - VIRTIO_SND_PCM_FMT_S18_3, VIRTIO_SND_PCM_FMT_S20, VIRTIO_SND_PCM_FMT_S20_3, - VIRTIO_SND_PCM_FMT_S24, VIRTIO_SND_PCM_FMT_S24_3, VIRTIO_SND_PCM_FMT_S32, - VIRTIO_SND_PCM_FMT_S8, VIRTIO_SND_PCM_FMT_U16, VIRTIO_SND_PCM_FMT_U18_3, - VIRTIO_SND_PCM_FMT_U20, VIRTIO_SND_PCM_FMT_U20_3, VIRTIO_SND_PCM_FMT_U24, - VIRTIO_SND_PCM_FMT_U24_3, VIRTIO_SND_PCM_FMT_U32, VIRTIO_SND_PCM_FMT_U8, - VIRTIO_SND_PCM_RATE_11025, VIRTIO_SND_PCM_RATE_16000, VIRTIO_SND_PCM_RATE_176400, - VIRTIO_SND_PCM_RATE_192000, VIRTIO_SND_PCM_RATE_22050, VIRTIO_SND_PCM_RATE_32000, - VIRTIO_SND_PCM_RATE_384000, VIRTIO_SND_PCM_RATE_44100, VIRTIO_SND_PCM_RATE_48000, - VIRTIO_SND_PCM_RATE_5512, VIRTIO_SND_PCM_RATE_64000, VIRTIO_SND_PCM_RATE_8000, - VIRTIO_SND_PCM_RATE_88200, VIRTIO_SND_PCM_RATE_96000, - }, - Direction, Error, Result, Stream, -}; -use super::AudioBackend; - -impl From for spa::utils::Direction { - fn from(val: Direction) -> Self { - match val { - Direction::Output => Self::Output, - Direction::Input => Self::Input, - } - } -} - -// SAFETY: Safe as the structure can be sent to another thread. -unsafe impl Send for PwBackend {} - -// SAFETY: Safe as the structure can be shared with another thread as the state -// is protected with a lock. -unsafe impl Sync for PwBackend {} - -// FIXME: make PwBackend impl Send on all fields. -#[allow(clippy::non_send_fields_in_send_ty)] -pub struct PwBackend { - pub stream_params: Arc>>, - thread_loop: ThreadLoopRc, - pub core: CoreRc, - #[allow(dead_code)] - context: ContextRc, - pub stream_hash: RwLock>, - pub stream_listener: RwLock>>, -} - -impl PwBackend { - pub fn new(stream_params: Arc>>) -> Self { - // SAFETY: safe as the thread loop cannot access objects associated - // with the loop while the lock is held - let thread_loop = unsafe { ThreadLoopRc::new(Some("Pipewire thread loop"), None).unwrap() }; - - let lock_guard = thread_loop.lock(); - - let context = ContextRc::new(&thread_loop, None).expect("failed to create context"); - thread_loop.start(); - let core = context.connect_rc(None).expect("Failed to connect to core"); - - // Create new reference for the variable so that it can be moved into the - // closure. - let thread_clone = thread_loop.clone(); - - // Trigger the sync event. The server's answer won't be processed until we start - // the thread loop, so we can safely do this before setting up a - // callback. This lets us avoid using a Cell. - let pending = core.sync(0).expect("sync failed"); - let _listener_core = core - .add_listener_local() - .done(move |id, seq| { - if id == PW_ID_CORE && seq == pending { - thread_clone.signal(false); - } - }) - .register(); - - thread_loop.wait(); - lock_guard.unlock(); - - log::trace!("pipewire backend running"); - - Self { - stream_params, - thread_loop, - core, - context, - stream_hash: RwLock::new(HashMap::new()), - stream_listener: RwLock::new(HashMap::new()), - } - } -} - -impl Drop for PwBackend { - fn drop(&mut self) { - self.thread_loop.stop(); - } -} - -impl AudioBackend for PwBackend { - fn write(&self, stream_id: u32) -> Result<()> { - if !matches!( - self.stream_params.read().unwrap()[stream_id as usize].state, - PCMState::Start | PCMState::Prepare - ) { - return Err(Error::Stream(StreamError::InvalidState( - "write", - self.stream_params.read().unwrap()[stream_id as usize].state, - ))); - } - Ok(()) - } - - fn read(&self, stream_id: u32) -> Result<()> { - log::trace!("PipewireBackend read stream_id {stream_id}"); - if !matches!( - self.stream_params.read().unwrap()[stream_id as usize].state, - PCMState::Start | PCMState::Prepare - ) { - return Err(Error::Stream(StreamError::InvalidState( - "read", - self.stream_params.read().unwrap()[stream_id as usize].state, - ))); - } - Ok(()) - } - - fn set_parameters(&self, stream_id: u32, request: VirtioSndPcmSetParams) -> Result<()> { - let stream_clone = self.stream_params.clone(); - let mut stream_params = stream_clone.write().unwrap(); - if let Some(st) = stream_params.get_mut(stream_id as usize) { - if let Err(err) = st.state.set_parameters() { - log::error!("Stream {stream_id} set_parameters {err}"); - return Err(Error::Stream(err)); - } else if !st.supports_format(request.format) || !st.supports_rate(request.rate) { - return Err(Error::UnexpectedAudioBackendConfiguration); - } else { - st.params.features = request.features; - st.params.buffer_bytes = request.buffer_bytes; - st.params.period_bytes = request.period_bytes; - st.params.channels = request.channels; - st.params.format = request.format; - st.params.rate = request.rate; - } - } else { - return Err(Error::StreamWithIdNotFound(stream_id)); - } - - Ok(()) - } - - fn prepare(&self, stream_id: u32) -> Result<()> { - debug!("pipewire prepare"); - let prepare_result = self - .stream_params - .write() - .unwrap() - .get_mut(stream_id as usize) - .ok_or(Error::StreamWithIdNotFound(stream_id))? - .state - .prepare(); - if let Err(err) = prepare_result { - log::error!("Stream {stream_id} prepare {err}"); - return Err(Error::Stream(err)); - } else { - let mut stream_hash = self.stream_hash.write().unwrap(); - let mut stream_listener = self.stream_listener.write().unwrap(); - let lock_guard = self.thread_loop.lock(); - let stream_params = self.stream_params.read().unwrap(); - - let params = &stream_params[stream_id as usize].params; - - if let Some(stream) = stream_hash.remove(&stream_id) { - stream_listener.remove(&stream_id); - if let Err(err) = stream.disconnect() { - log::error!("Stream {stream_id} disconnect {err}"); - return Err(Error::Stream(StreamError::CouldNotDisconnectStream)); - } - } - - let mut pos: [u32; 64] = [SPA_AUDIO_CHANNEL_UNKNOWN; 64]; - - match params.channels { - 6 => { - pos[0] = SPA_AUDIO_CHANNEL_FL; - pos[1] = SPA_AUDIO_CHANNEL_FR; - pos[2] = SPA_AUDIO_CHANNEL_FC; - pos[3] = SPA_AUDIO_CHANNEL_LFE; - pos[4] = SPA_AUDIO_CHANNEL_RL; - pos[5] = SPA_AUDIO_CHANNEL_RR; - } - 5 => { - pos[0] = SPA_AUDIO_CHANNEL_FL; - pos[1] = SPA_AUDIO_CHANNEL_FR; - pos[2] = SPA_AUDIO_CHANNEL_FC; - pos[3] = SPA_AUDIO_CHANNEL_LFE; - pos[4] = SPA_AUDIO_CHANNEL_RC; - } - 4 => { - pos[0] = SPA_AUDIO_CHANNEL_FL; - pos[1] = SPA_AUDIO_CHANNEL_FR; - pos[2] = SPA_AUDIO_CHANNEL_FC; - pos[3] = SPA_AUDIO_CHANNEL_RC; - } - 3 => { - pos[0] = SPA_AUDIO_CHANNEL_FL; - pos[1] = SPA_AUDIO_CHANNEL_FR; - pos[2] = SPA_AUDIO_CHANNEL_LFE; - } - 2 => { - pos[0] = SPA_AUDIO_CHANNEL_FL; - pos[1] = SPA_AUDIO_CHANNEL_FR; - } - 1 => { - pos[0] = SPA_AUDIO_CHANNEL_MONO; - } - _ => { - return Err(Error::ChannelNotSupported(params.channels)); - } - } - - let info = spa_audio_info_raw { - format: match params.format { - VIRTIO_SND_PCM_FMT_MU_LAW => SPA_AUDIO_FORMAT_ULAW, - VIRTIO_SND_PCM_FMT_A_LAW => SPA_AUDIO_FORMAT_ALAW, - VIRTIO_SND_PCM_FMT_S8 => SPA_AUDIO_FORMAT_S8, - VIRTIO_SND_PCM_FMT_U8 => SPA_AUDIO_FORMAT_U8, - VIRTIO_SND_PCM_FMT_S16 => SPA_AUDIO_FORMAT_S16, - VIRTIO_SND_PCM_FMT_U16 => SPA_AUDIO_FORMAT_U16, - VIRTIO_SND_PCM_FMT_S18_3 => SPA_AUDIO_FORMAT_S18_LE, - VIRTIO_SND_PCM_FMT_U18_3 => SPA_AUDIO_FORMAT_U18_LE, - VIRTIO_SND_PCM_FMT_S20_3 => SPA_AUDIO_FORMAT_S20_LE, - VIRTIO_SND_PCM_FMT_U20_3 => SPA_AUDIO_FORMAT_U20_LE, - VIRTIO_SND_PCM_FMT_S24_3 => SPA_AUDIO_FORMAT_S24_LE, - VIRTIO_SND_PCM_FMT_U24_3 => SPA_AUDIO_FORMAT_U24_LE, - VIRTIO_SND_PCM_FMT_S20 => SPA_AUDIO_FORMAT_S20, - VIRTIO_SND_PCM_FMT_U20 => SPA_AUDIO_FORMAT_U20, - VIRTIO_SND_PCM_FMT_S24 => SPA_AUDIO_FORMAT_S24, - VIRTIO_SND_PCM_FMT_U24 => SPA_AUDIO_FORMAT_U24, - VIRTIO_SND_PCM_FMT_S32 => SPA_AUDIO_FORMAT_S32, - VIRTIO_SND_PCM_FMT_U32 => SPA_AUDIO_FORMAT_U32, - VIRTIO_SND_PCM_FMT_FLOAT => SPA_AUDIO_FORMAT_F32, - VIRTIO_SND_PCM_FMT_FLOAT64 => SPA_AUDIO_FORMAT_F64, - _ => SPA_AUDIO_FORMAT_UNKNOWN, - }, - rate: match params.rate { - VIRTIO_SND_PCM_RATE_5512 => 5512, - VIRTIO_SND_PCM_RATE_8000 => 8000, - VIRTIO_SND_PCM_RATE_11025 => 11025, - VIRTIO_SND_PCM_RATE_16000 => 16000, - VIRTIO_SND_PCM_RATE_22050 => 22050, - VIRTIO_SND_PCM_RATE_32000 => 32000, - VIRTIO_SND_PCM_RATE_44100 => 44100, - VIRTIO_SND_PCM_RATE_48000 => 48000, - VIRTIO_SND_PCM_RATE_64000 => 64000, - VIRTIO_SND_PCM_RATE_88200 => 88200, - VIRTIO_SND_PCM_RATE_96000 => 96000, - VIRTIO_SND_PCM_RATE_176400 => 176400, - VIRTIO_SND_PCM_RATE_192000 => 192000, - VIRTIO_SND_PCM_RATE_384000 => 384000, - _ => 44100, - }, - flags: 0, - channels: u32::from(params.channels), - position: pos, - }; - - let mut audio_info = AudioInfoRaw::new(); - audio_info.set_format(AudioFormat::S16LE); - audio_info.set_rate(info.rate); - audio_info.set_channels(info.channels); - audio_info.set_position(pos); - - let values: Vec = PodSerializer::serialize( - std::io::Cursor::new(Vec::new()), - &Value::Object(Object { - type_: SPA_TYPE_OBJECT_Format, - id: SPA_PARAM_EnumFormat, - properties: audio_info.into(), - }), - ) - .unwrap() - .0 - .into_inner(); - - let value_clone = values.clone(); - - let mut param = [Pod::from_bytes(&values).unwrap()]; - - let direction = stream_params[stream_id as usize].direction; - - let media_category = match direction { - Direction::Input => "Capture", - Direction::Output => "Playback", - }; - let stream_name = match direction { - Direction::Input => "audio-input", - Direction::Output => "audio-output", - }; - - let props = properties! { - *pw::keys::MEDIA_TYPE => "Audio", - *pw::keys::MEDIA_CATEGORY => media_category, - }; - - let stream = pw::stream::StreamRc::new(self.core.clone(), stream_name, props) - .expect("could not create new stream"); - - let streams = self.stream_params.clone(); - - let listener_stream = stream - .add_local_listener() - .state_changed(|_, _, old, new| { - debug!("State changed: {old:?} -> {new:?}"); - }) - .param_changed(move |stream, _data, id, param| { - let Some(_param) = param else { - return; - }; - if id != ParamType::Format.as_raw() { - return; - } - let mut param = [Pod::from_bytes(&value_clone).unwrap()]; - - //callback to negotiate new set of streams - stream - .update_params(&mut param) - .expect("could not update params"); - }) - .process(move |stream, _data| match stream.dequeue_buffer() { - None => debug!("No buffer recieved"), - Some(mut buf) => { - match direction { - Direction::Input => { - let datas = buf.datas_mut(); - let data = &mut datas[0]; - let mut n_samples = data.chunk().size() as usize; - let Some(slice) = data.data() else { - return; - }; - let mut streams = streams.write().unwrap(); - let stream = streams - .get_mut(stream_id as usize) - .expect("Stream does not exist"); - - let mut start = 0; - while n_samples > 0 { - let Some(buffer) = stream.buffers.front_mut() else { - return; - }; - - let avail = usize::try_from(buffer.desc_len()) - .unwrap() - .saturating_sub(buffer.pos); - let n_bytes = n_samples.min(avail); - let p = &slice[start..start + n_bytes]; - - if buffer - .write_input(p) - .expect("Could not write data to guest memory") - == 0 - { - break; - } - - n_samples -= n_bytes; - start += n_bytes; - - if buffer.pos >= buffer.desc_len() as usize { - stream.buffers.pop_front(); - } - } - } - Direction::Output => { - let datas = buf.datas_mut(); - let frame_size = info.channels * size_of::() as u32; - let data = &mut datas[0]; - let n_bytes = if let Some(slice) = data.data() { - let mut n_bytes = slice.len(); - let mut streams = streams.write().unwrap(); - let streams = streams - .get_mut(stream_id as usize) - .expect("Stream does not exist"); - let Some(buffer) = streams.buffers.front_mut() else { - return; - }; - - let mut start = buffer.pos; - - let avail = usize::try_from(buffer.desc_len()) - .unwrap() - .saturating_sub(start); - - if avail < n_bytes { - n_bytes = avail; - } - let p = &mut slice[0..n_bytes]; - if avail == 0 { - // SAFETY: We have assured above that the pointer is not - // null - // safe to zero-initialize the pointer. - unsafe { - // pad with silence - ptr::write_bytes(p.as_mut_ptr(), 0, n_bytes); - } - } else { - // read_output() always reads (buffer.desc_len() - - // buffer.pos) bytes - buffer - .read_output(p) - .expect("failed to read buffer from guest"); - - start += n_bytes; - - buffer.pos = start; - - if start >= buffer.desc_len() as usize { - streams.buffers.pop_front(); - } - } - n_bytes - } else { - 0 - }; - let chunk = data.chunk_mut(); - *chunk.offset_mut() = 0; - *chunk.stride_mut() = i32::try_from(frame_size).unwrap(); - *chunk.size_mut() = u32::try_from(n_bytes).unwrap(); - } - }; - } - }) - .register() - .expect("failed to register stream listener"); - - stream_listener.insert(stream_id, listener_stream); - - stream - .connect( - stream_params[stream_id as usize].direction.into(), - Some(pw::constants::ID_ANY), - pw::stream::StreamFlags::RT_PROCESS - | pw::stream::StreamFlags::AUTOCONNECT - | pw::stream::StreamFlags::INACTIVE - | pw::stream::StreamFlags::MAP_BUFFERS, - &mut param, - ) - .expect("could not connect to the stream"); - - // insert created stream in a hash table - stream_hash.insert(stream_id, stream); - - lock_guard.unlock(); - } - - Ok(()) - } - - fn release(&self, stream_id: u32) -> Result<()> { - debug!("pipewire backend, release function"); - let release_result = self - .stream_params - .write() - .unwrap() - .get_mut(stream_id as usize) - .ok_or(Error::StreamWithIdNotFound(stream_id))? - .state - .release(); - if let Err(err) = release_result { - log::error!("Stream {stream_id} release {err}"); - return Err(Error::Stream(err)); - } - let lock_guard = self.thread_loop.lock(); - let mut stream_hash = self.stream_hash.write().unwrap(); - let mut stream_listener = self.stream_listener.write().unwrap(); - let st_buffer = &mut self.stream_params.write().unwrap(); - let stream = stream_hash - .get(&stream_id) - .expect("Could not find stream with this id in `stream_hash`."); - stream.disconnect().expect("could not disconnect stream"); - std::mem::take(&mut st_buffer[stream_id as usize].buffers); - stream_hash.remove(&stream_id); - stream_listener.remove(&stream_id); - lock_guard.unlock(); - Ok(()) - } - - fn start(&self, stream_id: u32) -> Result<()> { - debug!("pipewire start"); - let start_result = self - .stream_params - .write() - .unwrap() - .get_mut(stream_id as usize) - .ok_or(Error::StreamWithIdNotFound(stream_id))? - .state - .start(); - if let Err(err) = start_result { - // log the error and continue - log::error!("Stream {stream_id} start {err}"); - return Err(Error::Stream(err)); - } - let lock_guard = self.thread_loop.lock(); - let stream_hash = self.stream_hash.read().unwrap(); - let stream = stream_hash - .get(&stream_id) - .expect("Could not find stream with this id in `stream_hash`."); - stream.set_active(true).expect("could not start stream"); - lock_guard.unlock(); - Ok(()) - } - - fn stop(&self, stream_id: u32) -> Result<()> { - debug!("pipewire stop"); - let stop_result = self - .stream_params - .write() - .unwrap() - .get_mut(stream_id as usize) - .ok_or(Error::StreamWithIdNotFound(stream_id))? - .state - .stop(); - if let Err(err) = stop_result { - log::error!("Stream {stream_id} stop {err}"); - return Err(Error::Stream(err)); - } - let lock_guard = self.thread_loop.lock(); - let stream_hash = self.stream_hash.read().unwrap(); - let stream = stream_hash - .get(&stream_id) - .expect("Could not find stream with this id in `stream_hash`."); - stream.set_active(false).expect("could not stop stream"); - lock_guard.unlock(); - Ok(()) - } - - #[cfg(test)] - fn as_any(&self) -> &dyn std::any::Any { - self - } -} - -#[cfg(test)] -/// Utilities for building a temporary Dbus session and a pipewire instance for -/// testing. -pub mod test_utils; - -#[cfg(test)] -mod tests { - use super::{test_utils::PipewireTestHarness, *}; - - #[test] - fn test_pipewire_backend_success() { - crate::init_logger(); - let streams = Arc::new(RwLock::new(vec![Stream::default()])); - let stream_params = streams.clone(); - - let _test_harness = PipewireTestHarness::new(); - - let pw_backend = PwBackend::new(stream_params); - assert_eq!(pw_backend.stream_hash.read().unwrap().len(), 0); - assert_eq!(pw_backend.stream_listener.read().unwrap().len(), 0); - // set up minimal configuration for test - let request = VirtioSndPcmSetParams { - format: VIRTIO_SND_PCM_FMT_S16, - rate: VIRTIO_SND_PCM_RATE_11025, - channels: 1, - ..Default::default() - }; - pw_backend.set_parameters(0, request).unwrap(); - pw_backend.prepare(0).unwrap(); - pw_backend.start(0).unwrap(); - pw_backend.write(0).unwrap(); - pw_backend.read(0).unwrap(); - pw_backend.stop(0).unwrap(); - pw_backend.release(0).unwrap(); - let streams = streams.read().unwrap(); - assert_eq!(streams[0].buffers.len(), 0); - } - - #[test] - fn test_pipewire_backend_invalid_stream() { - crate::init_logger(); - let stream_params = Arc::new(RwLock::new(vec![])); - - let _test_harness = PipewireTestHarness::new(); - - let pw_backend = PwBackend::new(stream_params); - - let request = VirtioSndPcmSetParams::default(); - let res = pw_backend.set_parameters(0, request); - assert_eq!( - res.unwrap_err().to_string(), - Error::StreamWithIdNotFound(0).to_string() - ); - - for res in [ - pw_backend.prepare(0), - pw_backend.start(0), - pw_backend.stop(0), - ] { - assert_eq!( - res.unwrap_err().to_string(), - Error::StreamWithIdNotFound(0).to_string() - ); - } - - let res = pw_backend.release(0); - assert_eq!( - res.unwrap_err().to_string(), - Error::StreamWithIdNotFound(0).to_string() - ); - } -} diff --git a/vendor/krun-devices/src/virtio/snd/audio_backends/pipewire/test_utils.rs b/vendor/krun-devices/src/virtio/snd/audio_backends/pipewire/test_utils.rs deleted file mode 100644 index f7321213e..000000000 --- a/vendor/krun-devices/src/virtio/snd/audio_backends/pipewire/test_utils.rs +++ /dev/null @@ -1,134 +0,0 @@ -// Manos Pitsidianakis -// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause - -use std::{ - io::Read, - path::Path, - process::{Child, Command, Stdio}, -}; - -use tempfile::{tempdir, TempDir}; - -/// Temporary Dbus session which is killed in drop(). -pub struct DbusSession { - pub child: Child, - pub address: String, -} - -impl DbusSession { - pub fn new(working_dir: &Path) -> Self { - let address_prefix = format!("unix:path={}", working_dir.join("dbus").display()); - let child = Command::new("/usr/bin/dbus-daemon") - .args(["--session", "--address", &address_prefix, "--print-address"]) - .env("DBUS_VERBOSE", "1") - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .stdin(Stdio::null()) - .current_dir(working_dir) - .spawn() - .expect("ERROR: dbus-daemon binary not found"); - - Self { - child, - address: address_prefix, - } - } -} - -impl Drop for DbusSession { - fn drop(&mut self) { - println!("INFO: Killing Dbus session {}", self.child.id()); - if let Err(err) = self.child.kill() { - println!( - "ERROR: could not kill dbus process {}: {err}", - self.child.id() - ); - } - // We mustn't panic in drop(), so use a wrapper function for convenience - print_output(&mut self.child, "dbus"); - } -} - -/// The pipewire test harness. It must only be constructed via -/// `PipewireTestHarness::new()`. -#[non_exhaustive] -pub struct PipewireTestHarness { - pub dbus: DbusSession, - pub pipewire_child: Child, - pub tempdir: TempDir, -} - -pub fn launch_pipewire( - tempdir: &Path, - dbus_session_bus_address: &Path, -) -> Result { - Command::new("pipewire") - .env("DBUS_SESSION_BUS_ADDRESS", dbus_session_bus_address) - .env("XDG_RUNTIME_DIR", tempdir) - .current_dir(tempdir) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .stdin(Stdio::null()) - .spawn() -} - -impl PipewireTestHarness { - pub fn new() -> Self { - let tempdir = tempdir().unwrap(); - - let dbus_session = DbusSession::new(tempdir.path()); - println!("INFO: dbus_session_bus_address={}", dbus_session.address); - - println!("INFO: Wait for dbus to setup..."); - std::thread::sleep(std::time::Duration::from_secs(1)); - - println!("INFO: Launch pipewire."); - let pipewire_child = launch_pipewire(tempdir.path(), Path::new(&dbus_session.address)) - .expect("ERROR: Could not launch pipewire"); - println!("INFO: Wait for pipewire to setup..."); - std::thread::sleep(std::time::Duration::from_secs(1)); - - std::env::set_var("DBUS_SESSION_BUS_ADDRESS", &dbus_session.address); - std::env::set_var("XDG_RUNTIME_DIR", tempdir.path()); - - Self { - dbus: dbus_session, - pipewire_child, - tempdir, - } - } -} - -impl Drop for PipewireTestHarness { - fn drop(&mut self) { - println!("INFO: Killing pipewire pid {}", self.pipewire_child.id()); - if let Err(err) = self.pipewire_child.kill() { - println!( - "ERROR: could not kill Pipewire process {}: {err}", - self.pipewire_child.id() - ); - } - // We mustn't panic in drop(), so use a wrapper function for convenience - print_output(&mut self.pipewire_child, "pipewire"); - } -} - -fn print_output(child: &mut Child, id: &'static str) -> Option<()> { - let mut stdout = child.stdout.take()?; - let mut stderr = child.stderr.take()?; - - let mut buf = String::new(); - stdout.read_to_string(&mut buf).ok()?; - if !buf.trim().is_empty() { - println!("INFO: {id} stdout {buf}"); - } - - buf.clear(); - - stderr.read_to_string(&mut buf).ok()?; - if !buf.trim().is_empty() { - println!("ERROR: {id} stderr {buf}"); - } - - None -} diff --git a/vendor/krun-devices/src/virtio/snd/device.rs b/vendor/krun-devices/src/virtio/snd/device.rs deleted file mode 100644 index 4607790d7..000000000 --- a/vendor/krun-devices/src/virtio/snd/device.rs +++ /dev/null @@ -1,149 +0,0 @@ -use std::io::Write; -use std::thread::JoinHandle; - -use utils::eventfd::EventFd; -use vm_memory::{ByteValued, GuestMemoryMmap}; - -use super::super::{ActivateError, ActivateResult, DeviceQueue, QueueConfig, VirtioDevice}; -use super::virtio_sound::VirtioSoundConfig; -use super::worker::SndWorker; -use super::{defs, defs::uapi, Error}; - -use crate::virtio::{DeviceState, InterruptTransport}; - -// Supported features. -pub(crate) const AVAIL_FEATURES: u64 = 1 << uapi::VIRTIO_F_VERSION_1 as u64; - -pub struct Snd { - pub(crate) avail_features: u64, - pub(crate) acked_features: u64, - pub(crate) activate_evt: EventFd, - pub(crate) device_state: DeviceState, - worker_thread: Option>, - worker_stopfd: EventFd, -} - -impl Snd { - pub fn new() -> super::Result { - Ok(Snd { - avail_features: AVAIL_FEATURES, - acked_features: 0, - activate_evt: EventFd::new(utils::eventfd::EFD_NONBLOCK) - .map_err(Error::EventFdCreate)?, - device_state: DeviceState::Inactive, - worker_thread: None, - worker_stopfd: EventFd::new(utils::eventfd::EFD_NONBLOCK) - .map_err(Error::EventFdCreate)?, - }) - } - - pub fn id(&self) -> &str { - defs::SND_DEV_ID - } -} - -impl VirtioDevice for Snd { - fn avail_features(&self) -> u64 { - self.avail_features - } - - fn acked_features(&self) -> u64 { - self.acked_features - } - - fn set_acked_features(&mut self, acked_features: u64) { - self.acked_features = acked_features - } - - fn device_type(&self) -> u32 { - uapi::VIRTIO_ID_SND - } - - fn device_name(&self) -> &str { - "snd" - } - - fn queue_config(&self) -> &[QueueConfig] { - &defs::QUEUE_CONFIG - } - - fn read_config(&self, offset: u64, mut data: &mut [u8]) { - let config = VirtioSoundConfig { - jacks: 0.into(), - streams: 2.into(), - chmaps: 1.into(), - }; - - let config_slice = config.as_slice(); - let config_len = config_slice.len() as u64; - if offset >= config_len { - error!("Failed to read config space"); - return; - } - if let Some(end) = offset.checked_add(data.len() as u64) { - // This write can't fail, offset and end are checked against config_len. - data.write_all(&config_slice[offset as usize..std::cmp::min(end, config_len) as usize]) - .unwrap(); - } - } - - fn write_config(&mut self, offset: u64, data: &[u8]) { - warn!( - "snd: guest driver attempted to write device config (offset={:x}, len={:x})", - offset, - data.len() - ); - } - - fn activate( - &mut self, - mem: GuestMemoryMmap, - interrupt: InterruptTransport, - queues: Vec, - ) -> ActivateResult { - if self.worker_thread.is_some() { - panic!("virtio_snd: worker thread already exists"); - } - - if queues.len() != defs::NUM_QUEUES { - error!( - "Cannot perform activate. Expected {} queue(s), got {}", - defs::NUM_QUEUES, - queues.len() - ); - return Err(ActivateError::BadActivate); - } - - let worker = SndWorker::new( - queues, - interrupt.clone(), - mem.clone(), - self.worker_stopfd.try_clone().unwrap(), - ); - self.worker_thread = Some(worker.run()); - - if self.activate_evt.write(1).is_err() { - error!("Cannot write to activate_evt",); - return Err(ActivateError::BadActivate); - } - - self.device_state = DeviceState::Activated(mem, interrupt); - - Ok(()) - } - - fn is_activated(&self) -> bool { - self.device_state.is_activated() - } - - fn reset(&mut self) -> bool { - if let Some(worker) = self.worker_thread.take() { - let _ = self.worker_stopfd.write(1); - if let Err(e) = worker.join() { - error!("error waiting for worker thread: {e:?}"); - } - } - self.device_state = DeviceState::Inactive; - true - } -} diff --git a/vendor/krun-devices/src/virtio/snd/mod.rs b/vendor/krun-devices/src/virtio/snd/mod.rs deleted file mode 100644 index ea2f31f4a..000000000 --- a/vendor/krun-devices/src/virtio/snd/mod.rs +++ /dev/null @@ -1,312 +0,0 @@ -use std::{ - io::Error as IoError, - sync::{Arc, Mutex}, -}; - -mod audio_backends; -mod device; -pub mod stream; -#[allow(dead_code)] -mod virtio_sound; -mod worker; - -use thiserror::Error as ThisError; -use vm_memory::{ByteValued, Bytes, GuestAddress, GuestMemoryMmap}; - -pub use self::defs::uapi::VIRTIO_ID_SND as TYPE_SND; -pub use self::device::Snd; -pub use stream::Stream; -use virtio_sound::*; - -use super::{Descriptor, InterruptTransport, Queue}; -use crate::virtio::snd::virtio_sound::{VirtioSoundHeader, VirtioSoundPcmStatus}; - -mod defs { - use super::super::QueueConfig; - use super::virtio_sound::*; - - pub const SND_DEV_ID: &str = "virtio_snd"; - pub const NUM_QUEUES: usize = 4; - pub const CTL_INDEX: usize = 0; - pub const EVT_INDEX: usize = 1; - pub const TXQ_INDEX: usize = 2; - pub const RXQ_INDEX: usize = 3; - pub const QUEUE_INDEXES: [usize; 4] = [CTL_INDEX, EVT_INDEX, TXQ_INDEX, RXQ_INDEX]; - - const QUEUE_SIZE: u16 = 256; - pub static QUEUE_CONFIG: [QueueConfig; NUM_QUEUES] = [QueueConfig::new(QUEUE_SIZE); NUM_QUEUES]; - - pub const SUPPORTED_FORMATS: u64 = (1 << VIRTIO_SND_PCM_FMT_U8) - | (1 << VIRTIO_SND_PCM_FMT_S16) - | (1 << VIRTIO_SND_PCM_FMT_S24) - | (1 << VIRTIO_SND_PCM_FMT_S32); - - pub const SUPPORTED_RATES: u64 = (1 << VIRTIO_SND_PCM_RATE_8000) - | (1 << VIRTIO_SND_PCM_RATE_11025) - | (1 << VIRTIO_SND_PCM_RATE_16000) - | (1 << VIRTIO_SND_PCM_RATE_22050) - | (1 << VIRTIO_SND_PCM_RATE_32000) - | (1 << VIRTIO_SND_PCM_RATE_44100) - | (1 << VIRTIO_SND_PCM_RATE_48000); - - pub mod uapi { - pub const VIRTIO_F_VERSION_1: u32 = 32; - pub const VIRTIO_ID_SND: u32 = 25; - } -} - -pub type Result = std::result::Result; - -/// Stream direction. -/// -/// Equivalent to `VIRTIO_SND_D_OUTPUT` and `VIRTIO_SND_D_INPUT`. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[repr(u8)] -pub enum Direction { - /// [`VIRTIO_SND_D_OUTPUT`](crate::virtio_sound::VIRTIO_SND_D_OUTPUT) - Output = VIRTIO_SND_D_OUTPUT, - /// [`VIRTIO_SND_D_INPUT`](crate::virtio_sound::VIRTIO_SND_D_INPUT) - Input = VIRTIO_SND_D_INPUT, -} - -impl TryFrom for Direction { - type Error = Error; - - fn try_from(val: u8) -> std::result::Result { - Ok(match val { - virtio_sound::VIRTIO_SND_D_OUTPUT => Self::Output, - virtio_sound::VIRTIO_SND_D_INPUT => Self::Input, - other => { - return Err(Error::InvalidMessageValue( - stringify!(Direction), - other.into(), - )) - } - }) - } -} - -/// Custom error types -#[derive(Debug, ThisError)] -pub enum Error { - #[error("Notification send failed")] - SendNotificationFailed, - #[error("Descriptor not found")] - DescriptorNotFound, - #[error("Descriptor read failed")] - DescriptorReadFailed, - #[error("Descriptor write failed")] - DescriptorWriteFailed, - #[error("Failed to handle event other than EPOLLIN event")] - HandleEventNotEpollIn, - #[error("Failed to handle unknown event with id {0}")] - HandleUnknownEvent(u16), - #[error("Invalid control message code {0}")] - InvalidControlMessage(u32), - #[error("Invalid value in {0}: {1}")] - InvalidMessageValue(&'static str, u16), - #[error("Failed to create a new EventFd")] - EventFdCreate(IoError), - #[error("Request missing data buffer")] - SoundReqMissingData, - #[error("Audio backend not supported")] - AudioBackendNotSupported, - #[error("Audio backend unexpected error: {0}")] - UnexpectedAudioBackendError(String), - #[error("Audio backend configuration not supported")] - UnexpectedAudioBackendConfiguration, - #[error("No memory configured")] - NoMemoryConfigured, - #[error("Invalid virtio_snd_hdr size, expected: {0}, found: {1}")] - UnexpectedSoundHeaderSize(usize, u32), - #[error("Received unexpected write only descriptor at index {0}")] - UnexpectedWriteOnlyDescriptor(usize), - #[error("Received unexpected readable descriptor at index {0}")] - UnexpectedReadableDescriptor(usize), - #[error("Invalid descriptor count {0}")] - UnexpectedDescriptorCount(usize), - #[error("Invalid descriptor size, expected: {0}, found: {1}")] - UnexpectedDescriptorSize(usize, u32), - #[error("Protocol or device error: {0}")] - Stream(stream::Error), - #[error("Stream with id {0} not found")] - StreamWithIdNotFound(u32), - #[error("Channel number not supported: {0}")] - ChannelNotSupported(u8), - #[error("No audio backend in present")] - MissingAudioBackend, -} - -impl From for IoError { - fn from(e: Error) -> Self { - Self::other(e) - } -} - -impl From for Error { - fn from(val: stream::Error) -> Self { - Self::Stream(val) - } -} - -#[derive(Clone, Copy, Default, Debug, Eq, PartialEq)] -pub enum BackendType { - #[default] - Pipewire, -} - -#[derive(Debug, PartialEq, Eq)] -pub struct InvalidControlMessage(u32); - -impl std::fmt::Display for InvalidControlMessage { - fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(fmt, "Invalid control message code {}", self.0) - } -} - -impl From for Error { - fn from(val: InvalidControlMessage) -> Self { - Self::InvalidControlMessage(val.0) - } -} - -impl std::error::Error for InvalidControlMessage {} - -pub struct Vring { - mem: GuestMemoryMmap, - queue: Queue, - interrupt: InterruptTransport, -} - -impl Vring { - pub fn signal_used_queue(&self) { - debug!("snd: raising IRQ"); - if let Err(e) = self.interrupt.try_signal_used_queue() { - warn!("Failed to signal queue: {e:?}"); - } - } -} - -#[derive(Copy, Debug, Clone, Eq, PartialEq)] -#[repr(u32)] -pub enum ControlMessageKind { - JackInfo = 1, - JackRemap = 2, - PcmInfo = 0x0100, - PcmSetParams = 0x0101, - PcmPrepare = 0x0102, - PcmRelease = 0x0103, - PcmStart = 0x0104, - PcmStop = 0x0105, - ChmapInfo = 0x0200, -} - -impl TryFrom for ControlMessageKind { - type Error = InvalidControlMessage; - - fn try_from(val: u32) -> std::result::Result { - Ok(match val { - VIRTIO_SND_R_JACK_INFO => Self::JackInfo, - VIRTIO_SND_R_JACK_REMAP => Self::JackRemap, - VIRTIO_SND_R_PCM_INFO => Self::PcmInfo, - VIRTIO_SND_R_PCM_SET_PARAMS => Self::PcmSetParams, - VIRTIO_SND_R_PCM_PREPARE => Self::PcmPrepare, - VIRTIO_SND_R_PCM_RELEASE => Self::PcmRelease, - VIRTIO_SND_R_PCM_START => Self::PcmStart, - VIRTIO_SND_R_PCM_STOP => Self::PcmStop, - VIRTIO_SND_R_CHMAP_INFO => Self::ChmapInfo, - other => return Err(InvalidControlMessage(other)), - }) - } -} - -pub struct ControlMessage { - pub kind: ControlMessageKind, - pub code: u32, - pub desc_addr: GuestAddress, - pub head_index: u16, - pub vring: Arc>, -} - -impl std::fmt::Debug for ControlMessage { - fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { - fmt.debug_struct(stringify!(ControlMessage)) - .field("kind", &self.kind) - .field("code", &self.code) - .finish() - } -} - -impl Drop for ControlMessage { - fn drop(&mut self) { - debug!( - "dropping ControlMessage {:?} reply = {}", - self.kind, - match self.code { - virtio_sound::VIRTIO_SND_S_OK => "VIRTIO_SND_S_OK", - virtio_sound::VIRTIO_SND_S_BAD_MSG => "VIRTIO_SND_S_BAD_MSG", - virtio_sound::VIRTIO_SND_S_NOT_SUPP => "VIRTIO_SND_S_NOT_SUPP", - virtio_sound::VIRTIO_SND_S_IO_ERR => "VIRTIO_SND_S_IO_ERR", - _ => "other", - } - ); - let resp = VirtioSoundHeader { - code: self.code.into(), - }; - - let mut vring = self.vring.lock().unwrap(); - let mem = vring.mem.clone(); - - if let Err(err) = vring.mem.write_obj(resp, self.desc_addr) { - log::error!("Error::DescriptorWriteFailed: {err}"); - return; - } - if let Err(err) = vring - .queue - .add_used(&mem, self.head_index, resp.as_slice().len() as u32) - { - log::error!("Error adding used descriptors: {err}"); - return; - } - vring.signal_used_queue(); - } -} - -pub struct IOMessage { - status: std::sync::atomic::AtomicU32, - pub used_len: std::sync::atomic::AtomicU32, - pub latency_bytes: std::sync::atomic::AtomicU32, - - head_index: u16, - response_descriptor: Descriptor, - vring: Arc>, -} - -impl Drop for IOMessage { - fn drop(&mut self) { - let resp = VirtioSoundPcmStatus { - status: self.status.load(std::sync::atomic::Ordering::SeqCst).into(), - latency_bytes: self - .latency_bytes - .load(std::sync::atomic::Ordering::SeqCst) - .into(), - }; - let used_len: u32 = self.used_len.load(std::sync::atomic::Ordering::SeqCst); - log::trace!("dropping IOMessage {resp:?}"); - - let mut vring = self.vring.lock().unwrap(); - let mem = vring.mem.clone(); - if let Err(err) = mem.write_obj(resp, GuestAddress(self.response_descriptor.addr)) { - log::error!("Error::DescriptorWriteFailed: {err}"); - return; - } - if let Err(err) = vring.queue.add_used( - &mem, - self.head_index, - resp.as_slice().len() as u32 + used_len, - ) { - log::error!("Couldn't add used bytes count to vring: {err}"); - } - vring.signal_used_queue(); - } -} diff --git a/vendor/krun-devices/src/virtio/snd/stream.rs b/vendor/krun-devices/src/virtio/snd/stream.rs deleted file mode 100644 index f32d80d8f..000000000 --- a/vendor/krun-devices/src/virtio/snd/stream.rs +++ /dev/null @@ -1,624 +0,0 @@ -// Manos Pitsidianakis -// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause - -use std::{collections::VecDeque, sync::Arc}; - -use thiserror::Error as ThisError; -use vm_memory::{Address, Bytes, GuestAddress, Le32, Le64}; - -use super::super::Descriptor; -use super::defs::{SUPPORTED_FORMATS, SUPPORTED_RATES}; -use super::{virtio_sound::*, Direction, IOMessage}; - -/// Stream errors. -#[derive(Debug, ThisError, PartialEq, Eq)] -pub enum Error { - #[error("Guest driver request {0} in an invalid stream state {1}")] - InvalidState(&'static str, PCMState), - #[error("Guest driver request an invalid stream state transition from {0} to {1}.")] - InvalidStateTransition(PCMState, PCMState), - #[error("Guest requested an invalid stream id: {0}")] - InvalidStreamId(u32), - #[error("Descriptor read failed")] - DescriptorReadFailed, - #[error("Descriptor write failed")] - DescriptorWriteFailed, - #[error("Could not disconnect stream")] - CouldNotDisconnectStream, -} - -type Result = std::result::Result; - -/// PCM stream state machine. -/// -/// ## 5.14.6.6.1 PCM Command Lifecycle -/// -/// A PCM stream has the following command lifecycle: -/// -/// - `SET PARAMETERS` -/// -/// The driver negotiates the stream parameters (format, transport, etc) with -/// the device. -/// -/// Possible valid transitions: `SET PARAMETERS`, `PREPARE`. -/// -/// - `PREPARE` -/// -/// The device prepares the stream (allocates resources, etc). -/// -/// Possible valid transitions: `SET PARAMETERS`, `PREPARE`, `START`, -/// `RELEASE`. Output only: the driver transfers data for pre-buffing. -/// -/// - `START` -/// -/// The device starts the stream (unmute, putting into running state, etc). -/// -/// Possible valid transitions: `STOP`. -/// The driver transfers data to/from the stream. -/// -/// - `STOP` -/// -/// The device stops the stream (mute, putting into non-running state, etc). -/// -/// Possible valid transitions: `START`, `RELEASE`. -/// -/// - `RELEASE` -/// -/// The device releases the stream (frees resources, etc). -/// -/// Possible valid transitions: `SET PARAMETERS`, `PREPARE`. -/// -/// ```text -/// +---------------+ +---------+ +---------+ +-------+ +-------+ -/// | SetParameters | | Prepare | | Release | | Start | | Stop | -/// +---------------+ +---------+ +---------+ +-------+ +-------+ -/// | | | | | -/// |- | | | | -/// || | | | | -/// |< | | | | -/// | | | | | -/// |------------->| | | | -/// | | | | | -/// |<-------------| | | | -/// | | | | | -/// | |- | | | -/// | || | | | -/// | |< | | | -/// | | | | | -/// | |--------------------->| | -/// | | | | | -/// | |---------->| | | -/// | | | | | -/// | | | |-------->| -/// | | | | | -/// | | | |<--------| -/// | | | | | -/// | | |<-------------------| -/// | | | | | -/// |<-------------------------| | | -/// | | | | | -/// | |<----------| | | -/// ``` -#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] -pub enum PCMState { - #[default] - #[doc(alias = "VIRTIO_SND_R_PCM_SET_PARAMS")] - SetParameters, - #[doc(alias = "VIRTIO_SND_R_PCM_PREPARE")] - Prepare, - #[doc(alias = "VIRTIO_SND_R_PCM_RELEASE")] - Release, - #[doc(alias = "VIRTIO_SND_R_PCM_START")] - Start, - #[doc(alias = "VIRTIO_SND_R_PCM_STOP")] - Stop, -} - -macro_rules! set_new_state { - ($new_state_fn:ident, $new_state:expr, $($valid_source_states:tt)*) => { - pub fn $new_state_fn(&mut self) -> Result<()> { - if !matches!(self, $($valid_source_states)*) { - return Err(Error::InvalidStateTransition(*self, $new_state)); - } - *self = $new_state; - Ok(()) - } - }; -} - -impl PCMState { - pub fn new() -> Self { - Self::default() - } - - set_new_state!( - set_parameters, - Self::SetParameters, - Self::SetParameters | Self::Prepare | Self::Release - ); - - set_new_state!( - prepare, - Self::Prepare, - Self::SetParameters | Self::Prepare | Self::Release - ); - - set_new_state!(start, Self::Start, Self::Prepare | Self::Stop); - - set_new_state!(stop, Self::Stop, Self::Start); - - set_new_state!(release, Self::Release, Self::Prepare | Self::Stop); -} - -impl std::fmt::Display for PCMState { - fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { - use PCMState::*; - match *self { - SetParameters => { - write!(fmt, "VIRTIO_SND_R_PCM_SET_PARAMS") - } - Prepare => { - write!(fmt, "VIRTIO_SND_R_PCM_PREPARE") - } - Release => { - write!(fmt, "VIRTIO_SND_R_PCM_RELEASE") - } - Start => { - write!(fmt, "VIRTIO_SND_R_PCM_START") - } - Stop => { - write!(fmt, "VIRTIO_SND_R_PCM_STOP") - } - } - } -} - -/// Internal state of a PCM stream of the VIRTIO Sound device. -#[derive(Debug)] -pub struct Stream { - pub id: usize, - pub params: PcmParams, - pub formats: Le64, - pub rates: Le64, - pub direction: Direction, - pub channels_min: u8, - pub channels_max: u8, - pub state: PCMState, - pub buffers: VecDeque, -} - -impl Default for Stream { - fn default() -> Self { - Self { - id: 0, - direction: Direction::Output, - formats: SUPPORTED_FORMATS.into(), - rates: SUPPORTED_RATES.into(), - params: PcmParams::default(), - channels_min: 1, - channels_max: 6, - state: Default::default(), - buffers: VecDeque::new(), - } - } -} - -impl Stream { - #[inline] - pub fn supports_format(&self, format: u8) -> bool { - let formats: u64 = self.formats.into(); - (formats & (1_u64 << format)) != 0 - } - - #[inline] - pub fn supports_rate(&self, rate: u8) -> bool { - let rates: u64 = self.rates.into(); - (rates & (1_u64 << rate)) != 0 - } -} - -/// Stream params -#[derive(Debug)] -pub struct PcmParams { - /// size of hardware buffer in bytes - pub buffer_bytes: Le32, - /// size of hardware period in bytes - pub period_bytes: Le32, - pub features: Le32, - pub channels: u8, - pub format: u8, - pub rate: u8, -} - -impl Default for PcmParams { - fn default() -> Self { - Self { - buffer_bytes: 8192.into(), - period_bytes: 4096.into(), - features: 0.into(), - channels: 1, - format: VIRTIO_SND_PCM_FMT_S16, - rate: VIRTIO_SND_PCM_RATE_44100, - } - } -} - -pub struct Buffer { - data_descriptor: Descriptor, - pub pos: usize, - pub message: Arc, - direction: Direction, -} - -impl std::fmt::Debug for Buffer { - fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { - fmt.debug_struct(stringify!(Buffer)) - .field("pos", &self.pos) - .field("direction", &self.direction) - .field("message", &Arc::as_ptr(&self.message)) - .finish() - } -} - -impl Buffer { - pub fn new(data_descriptor: Descriptor, message: Arc, direction: Direction) -> Self { - Self { - pos: 0, - data_descriptor, - message, - direction, - } - } - - pub fn read_output(&self, buf: &mut [u8]) -> Result { - let addr = self.data_descriptor.addr; - let offset = self.pos as u64; - let len = self - .message - .vring - .lock() - .unwrap() - .mem - .read( - buf, - GuestAddress(addr) - .checked_add(offset) - .expect("invalid guest memory address"), - ) - .map_err(|_| Error::DescriptorReadFailed)?; - Ok(len as u32) - } - - pub fn write_input(&mut self, buf: &[u8]) -> Result { - if self.desc_len() <= self.pos as u32 { - return Ok(0); - } - let addr = self.data_descriptor.addr; - let offset = self.pos as u64; - let len = self - .message - .vring - .lock() - .unwrap() - .mem - .write( - buf, - GuestAddress(addr) - .checked_add(offset) - .expect("invalid guest memory address"), - ) - .map_err(|_| Error::DescriptorWriteFailed)?; - self.pos += len; - Ok(len as u32) - } - - #[inline] - /// Returns the length of the sound data [`virtio_queue::Descriptor`]. - pub fn desc_len(&self) -> u32 { - self.data_descriptor.len - } -} - -impl Drop for Buffer { - fn drop(&mut self) { - match self.direction { - Direction::Input => { - let used_len = std::cmp::min(self.pos as u32, self.desc_len()); - self.message - .used_len - .fetch_add(used_len, std::sync::atomic::Ordering::SeqCst); - self.message - .latency_bytes - .fetch_add(used_len, std::sync::atomic::Ordering::SeqCst); - } - Direction::Output => { - self.message - .latency_bytes - .fetch_add(self.desc_len(), std::sync::atomic::Ordering::SeqCst); - } - } - log::trace!("dropping {:?} buffer {:?}", self.direction, self); - } -} - -#[cfg(test)] -mod tests { - use std::fmt::Write; - - use vhost_user_backend::{VringRwLock, VringT}; - use virtio_bindings::bindings::virtio_ring::{VRING_DESC_F_NEXT, VRING_DESC_F_WRITE}; - use virtio_queue::{mock::MockSplitQueue, Descriptor, Queue, QueueOwnedT}; - use vm_memory::{ - Address, ByteValued, GuestAddress, GuestAddressSpace, GuestMemoryAtomic, GuestMemoryMmap, - }; - - use super::*; - use crate::SoundDescriptorChain; - - // Prepares a single chain of descriptors for request queue - fn prepare_desc_chain( - start_addr: GuestAddress, - hdr: R, - response_len: u32, - ) -> SoundDescriptorChain { - let mem = &GuestMemoryMmap::<()>::from_ranges(&[(start_addr, 0x1000)]).unwrap(); - let vq = MockSplitQueue::new(mem, 16); - let mut next_addr = vq.desc_table().total_size() + 0x100; - let mut index = 0; - - let desc_out = Descriptor::new( - next_addr, - std::mem::size_of::() as u32, - VRING_DESC_F_NEXT as u16, - index + 1, - ); - - mem.write_obj::(hdr, desc_out.addr()).unwrap(); - vq.desc_table().store(index, desc_out).unwrap(); - next_addr += u64::from(desc_out.len()); - index += 1; - - // In response descriptor - let desc_in = Descriptor::new(next_addr, response_len, VRING_DESC_F_WRITE as u16, 0); - vq.desc_table().store(index, desc_in).unwrap(); - - // Put the descriptor index 0 in the first available ring position. - mem.write_obj(0u16, vq.avail_addr().unchecked_add(4)) - .unwrap(); - - // Set `avail_idx` to 1. - mem.write_obj(1u16, vq.avail_addr().unchecked_add(2)) - .unwrap(); - - // Create descriptor chain from pre-filled memory - vq.create_queue::() - .unwrap() - .iter(GuestMemoryAtomic::new(mem.clone()).memory()) - .unwrap() - .next() - .unwrap() - } - - fn iomsg() -> IOMessage { - let hdr = VirtioSndPcmSetParams::default(); - let memr = GuestMemoryAtomic::new( - GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0), 0x10000)]).unwrap(), - ); - let vring = VringRwLock::new(memr, 0x1000).unwrap(); - let mem = &GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0), 0x1000)]).unwrap(); - let vq = MockSplitQueue::new(mem, 16); - let next_addr = vq.desc_table().total_size() + 0x100; - IOMessage { - status: VIRTIO_SND_S_OK.into(), - latency_bytes: 0.into(), - used_len: 0.into(), - desc_chain: prepare_desc_chain::(GuestAddress(0), hdr, 1), - response_descriptor: Descriptor::new(next_addr, 0x200, VRING_DESC_F_NEXT as u16, 1), - vring, - } - } - - #[test] - fn test_display_fmt() { - assert_eq!(&PCMState::Stop.to_string(), "VIRTIO_SND_R_PCM_STOP"); - } - - #[test] - fn test_logging() { - let data_descriptor = Descriptor::new(0, 0, 0, 0); - let msg = iomsg(); - let message = Arc::new(msg); - let direction = Direction::Input; - let buffer = Buffer::new(data_descriptor, message, direction); - assert_eq!(format!("{direction:?}"), "Input"); - assert_eq!( - format!("{buffer:?}"), - format!( - "Buffer {{ pos: 0, direction: Input, message: {:?} }}", - &Arc::as_ptr(&buffer.message) - ) - ); - } - - #[test] - fn test_pcm_state_transitions() { - let mut state = PCMState::new(); - assert_eq!(state, PCMState::SetParameters); - - state.set_parameters().unwrap(); - assert_eq!(state, PCMState::SetParameters); - - state.prepare().unwrap(); - assert_eq!(state, PCMState::Prepare); - - state.release().unwrap(); - assert_eq!(state, PCMState::Release); - } - - #[test] - fn test_invalid_state_transition() { - let mut state = PCMState::new(); - assert_eq!(state, PCMState::SetParameters); - - // Attempt to transition from set_params state to Release state - let result = state.release(); - assert_eq!( - result, - Err(Error::InvalidStateTransition( - PCMState::SetParameters, - PCMState::Release - )) - ); - - let result = state.start(); - assert_eq!( - result, - Err(Error::InvalidStateTransition( - PCMState::SetParameters, - PCMState::Start - )) - ); - - let result = state.stop(); - assert_eq!( - result, - Err(Error::InvalidStateTransition( - PCMState::SetParameters, - PCMState::Stop - )) - ); - - state.prepare().unwrap(); - let result = state.stop(); - assert_eq!( - result, - Err(Error::InvalidStateTransition( - PCMState::Prepare, - PCMState::Stop - )) - ); - - state.start().unwrap(); - let result = state.set_parameters(); - assert_eq!( - result, - Err(Error::InvalidStateTransition( - PCMState::Start, - PCMState::SetParameters - )) - ); - - let result = state.release(); - assert_eq!( - result, - Err(Error::InvalidStateTransition( - PCMState::Start, - PCMState::Release - )) - ); - - let result = state.prepare(); - assert_eq!( - result, - Err(Error::InvalidStateTransition( - PCMState::Start, - PCMState::Prepare - )) - ); - - state.stop().unwrap(); - let result = state.set_parameters(); - assert_eq!( - result, - Err(Error::InvalidStateTransition( - PCMState::Stop, - PCMState::SetParameters - )) - ); - - let result = state.prepare(); - assert_eq!( - result, - Err(Error::InvalidStateTransition( - PCMState::Stop, - PCMState::Prepare - )) - ); - } - - #[test] - fn test_stream_supports_format() { - let stream = Stream::default(); - assert!(stream.supports_format(VIRTIO_SND_PCM_FMT_S16)); - assert!(stream.supports_rate(VIRTIO_SND_PCM_RATE_44100)); - } - - #[test] - fn test_pcm_params_default() { - let params = PcmParams::default(); - assert_eq!(params.buffer_bytes, 8192); - assert_eq!(params.period_bytes, 4096); - assert_eq!(params.features, 0); - assert_eq!(params.channels, 1); - assert_eq!(params.format, VIRTIO_SND_PCM_FMT_S16); - assert_eq!(params.rate, VIRTIO_SND_PCM_RATE_44100); - } - - #[test] - fn test_buffer_read_output() { - let msg = iomsg(); - let message = Arc::new(msg); - let desc_msg = iomsg(); - let buffer = Buffer::new( - desc_msg.desc_chain.clone().readable().next().unwrap(), - message, - Direction::Output, - ); - - let mut buf = vec![0; 5]; - buffer.read_output(&mut buf).unwrap(); - } - - #[test] - fn test_buffer_write_input() { - let msg = iomsg(); - let message = Arc::new(msg); - let desc_msg = iomsg(); - let mut buffer = Buffer::new( - desc_msg.desc_chain.clone().readable().next().unwrap(), - message, - Direction::Input, - ); - - let buf = vec![0; 5]; - buffer.write_input(&buf).unwrap(); - } - - #[test] - fn test_buffer_fn() { - let data_descriptor = Descriptor::new(0, 0, 0, 0); - let msg = iomsg(); - let message = Arc::new(msg); - let direction = Direction::Input; - let buffer = Buffer::new(data_descriptor, message, direction); - - assert_eq!(buffer.desc_len() as usize, buffer.pos); - assert_eq!(buffer.desc_len(), 0); - assert_eq!(buffer.direction, Direction::Input); - - // Test debug format representation for Buffer - let mut debug_output = String::new(); - - // Format the Debug representation into the String. - write!(&mut debug_output, "{:?}", buffer).unwrap(); - - let expected_debug = format!( - "Buffer {{ pos: {}, direction: {:?}, message: {:?} }}", - buffer.pos, - buffer.direction, - Arc::as_ptr(&buffer.message) - ); - - assert_eq!(debug_output, expected_debug); - } -} diff --git a/vendor/krun-devices/src/virtio/snd/virtio_sound.rs b/vendor/krun-devices/src/virtio/snd/virtio_sound.rs deleted file mode 100644 index a666b3f19..000000000 --- a/vendor/krun-devices/src/virtio/snd/virtio_sound.rs +++ /dev/null @@ -1,512 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause -use vm_memory::{ByteValued, Le32, Le64}; - -// virtqueues - -pub const CONTROL_QUEUE_IDX: u16 = 0; -pub const EVENT_QUEUE_IDX: u16 = 1; -pub const TX_QUEUE_IDX: u16 = 2; -pub const RX_QUEUE_IDX: u16 = 3; -pub const NUM_QUEUES: u16 = 4; - -// jack control request types - -pub const VIRTIO_SND_R_JACK_INFO: u32 = 1; -pub const VIRTIO_SND_R_JACK_REMAP: u32 = 2; - -// PCM control request types - -pub const VIRTIO_SND_R_PCM_INFO: u32 = 0x0100; -pub const VIRTIO_SND_R_PCM_SET_PARAMS: u32 = 0x0101; -pub const VIRTIO_SND_R_PCM_PREPARE: u32 = 0x0102; -pub const VIRTIO_SND_R_PCM_RELEASE: u32 = 0x0103; -pub const VIRTIO_SND_R_PCM_START: u32 = 0x0104; -pub const VIRTIO_SND_R_PCM_STOP: u32 = 0x0105; - -// channel map control request types - -pub const VIRTIO_SND_R_CHMAP_INFO: u32 = 0x0200; - -// jack event types - -pub const VIRTIO_SND_EVT_JACK_CONNECTED: u32 = 0x1000; -pub const VIRTIO_SND_EVT_JACK_DISCONNECTED: u32 = 0x1001; - -// PCM event types - -pub const VIRTIO_SND_EVT_PCM_PERIOD_ELAPSED: u32 = 0x1100; -pub const VIRTIO_SND_EVT_PCM_XRUN: u32 = 0x1101; - -// common status codes - -pub const VIRTIO_SND_S_OK: u32 = 0x8000; -pub const VIRTIO_SND_S_BAD_MSG: u32 = 0x8001; -pub const VIRTIO_SND_S_NOT_SUPP: u32 = 0x8002; -pub const VIRTIO_SND_S_IO_ERR: u32 = 0x8003; - -// device data flow directions - -pub const VIRTIO_SND_D_OUTPUT: u8 = 0; -pub const VIRTIO_SND_D_INPUT: u8 = 1; - -// supported jack features - -pub const VIRTIO_SND_JACK_F_REMAP: u32 = 0; - -// supported PCM stream features - -pub const VIRTIO_SND_PCM_F_SHMEM_HOST: u8 = 0; -pub const VIRTIO_SND_PCM_F_SHMEM_GUEST: u8 = 1; -pub const VIRTIO_SND_PCM_F_MSG_POLLING: u8 = 2; -pub const VIRTIO_SND_PCM_F_EVT_SHMEM_PERIODS: u8 = 3; -pub const VIRTIO_SND_PCM_F_EVT_XRUNS: u8 = 4; - -// supported PCM sample formats - -pub const VIRTIO_SND_PCM_FMT_IMA_ADPCM: u8 = 0; -pub const VIRTIO_SND_PCM_FMT_MU_LAW: u8 = 1; -pub const VIRTIO_SND_PCM_FMT_A_LAW: u8 = 2; -pub const VIRTIO_SND_PCM_FMT_S8: u8 = 3; -pub const VIRTIO_SND_PCM_FMT_U8: u8 = 4; -pub const VIRTIO_SND_PCM_FMT_S16: u8 = 5; -pub const VIRTIO_SND_PCM_FMT_U16: u8 = 6; -pub const VIRTIO_SND_PCM_FMT_S18_3: u8 = 7; -pub const VIRTIO_SND_PCM_FMT_U18_3: u8 = 8; -pub const VIRTIO_SND_PCM_FMT_S20_3: u8 = 9; -pub const VIRTIO_SND_PCM_FMT_U20_3: u8 = 10; -pub const VIRTIO_SND_PCM_FMT_S24_3: u8 = 11; -pub const VIRTIO_SND_PCM_FMT_U24_3: u8 = 12; -pub const VIRTIO_SND_PCM_FMT_S20: u8 = 13; -pub const VIRTIO_SND_PCM_FMT_U20: u8 = 14; -pub const VIRTIO_SND_PCM_FMT_S24: u8 = 15; -pub const VIRTIO_SND_PCM_FMT_U24: u8 = 16; -pub const VIRTIO_SND_PCM_FMT_S32: u8 = 17; -pub const VIRTIO_SND_PCM_FMT_U32: u8 = 18; -pub const VIRTIO_SND_PCM_FMT_FLOAT: u8 = 19; -pub const VIRTIO_SND_PCM_FMT_FLOAT64: u8 = 20; -// digital formats (width / physical width) -pub const VIRTIO_SND_PCM_FMT_DSD_U8: u8 = 21; -pub const VIRTIO_SND_PCM_FMT_DSD_U16: u8 = 22; -pub const VIRTIO_SND_PCM_FMT_DSD_U32: u8 = 23; -pub const VIRTIO_SND_PCM_FMT_IEC958_SUBFRAME: u8 = 24; -pub(crate) const _VIRTIO_SND_PCM_FMT_MAX: u8 = 25; - -// supported PCM frame rates - -pub const VIRTIO_SND_PCM_RATE_5512: u8 = 0; -pub const VIRTIO_SND_PCM_RATE_8000: u8 = 1; -pub const VIRTIO_SND_PCM_RATE_11025: u8 = 2; -pub const VIRTIO_SND_PCM_RATE_16000: u8 = 3; -pub const VIRTIO_SND_PCM_RATE_22050: u8 = 4; -pub const VIRTIO_SND_PCM_RATE_32000: u8 = 5; -pub const VIRTIO_SND_PCM_RATE_44100: u8 = 6; -pub const VIRTIO_SND_PCM_RATE_48000: u8 = 7; -pub const VIRTIO_SND_PCM_RATE_64000: u8 = 8; -pub const VIRTIO_SND_PCM_RATE_88200: u8 = 9; -pub const VIRTIO_SND_PCM_RATE_96000: u8 = 10; -pub const VIRTIO_SND_PCM_RATE_176400: u8 = 11; -pub const VIRTIO_SND_PCM_RATE_192000: u8 = 12; -pub const VIRTIO_SND_PCM_RATE_384000: u8 = 13; -pub(crate) const _VIRTIO_SND_PCM_RATE_MAX: u8 = 14; - -// standard channel position definition - -pub const VIRTIO_SND_CHMAP_NONE: u8 = 0; /* undefined */ -pub const VIRTIO_SND_CHMAP_NA: u8 = 1; /* silent */ -pub const VIRTIO_SND_CHMAP_MONO: u8 = 2; /* mono stream */ -pub const VIRTIO_SND_CHMAP_FL: u8 = 3; /* front left */ -pub const VIRTIO_SND_CHMAP_FR: u8 = 4; /* front right */ -pub const VIRTIO_SND_CHMAP_RL: u8 = 5; /* rear left */ -pub const VIRTIO_SND_CHMAP_RR: u8 = 6; /* rear right */ -pub const VIRTIO_SND_CHMAP_FC: u8 = 7; /* front center */ -pub const VIRTIO_SND_CHMAP_LFE: u8 = 8; /* low frequency (LFE) */ -pub const VIRTIO_SND_CHMAP_SL: u8 = 9; /* side left */ -pub const VIRTIO_SND_CHMAP_SR: u8 = 10; /* side right */ -pub const VIRTIO_SND_CHMAP_RC: u8 = 11; /* rear center */ -pub const VIRTIO_SND_CHMAP_FLC: u8 = 12; /* front left center */ -pub const VIRTIO_SND_CHMAP_FRC: u8 = 13; /* front right center */ -pub const VIRTIO_SND_CHMAP_RLC: u8 = 14; /* rear left center */ -pub const VIRTIO_SND_CHMAP_RRC: u8 = 15; /* rear right center */ -pub const VIRTIO_SND_CHMAP_FLW: u8 = 16; /* front left wide */ -pub const VIRTIO_SND_CHMAP_FRW: u8 = 17; /* front right wide */ -pub const VIRTIO_SND_CHMAP_FLH: u8 = 18; /* front left high */ -pub const VIRTIO_SND_CHMAP_FCH: u8 = 19; /* front center high */ -pub const VIRTIO_SND_CHMAP_FRH: u8 = 20; /* front right high */ -pub const VIRTIO_SND_CHMAP_TC: u8 = 21; /* top center */ -pub const VIRTIO_SND_CHMAP_TFL: u8 = 22; /* top front left */ -pub const VIRTIO_SND_CHMAP_TFR: u8 = 23; /* top front right */ -pub const VIRTIO_SND_CHMAP_TFC: u8 = 24; /* top front center */ -pub const VIRTIO_SND_CHMAP_TRL: u8 = 25; /* top rear left */ -pub const VIRTIO_SND_CHMAP_TRR: u8 = 26; /* top rear right */ -pub const VIRTIO_SND_CHMAP_TRC: u8 = 27; /* top rear center */ -pub const VIRTIO_SND_CHMAP_TFLC: u8 = 28; /* top front left center */ -pub const VIRTIO_SND_CHMAP_TFRC: u8 = 29; /* top front right center */ -pub const VIRTIO_SND_CHMAP_TSL: u8 = 34; /* top side left */ -pub const VIRTIO_SND_CHMAP_TSR: u8 = 35; /* top side right */ -pub const VIRTIO_SND_CHMAP_LLFE: u8 = 36; /* left LFE */ -pub const VIRTIO_SND_CHMAP_RLFE: u8 = 37; /* right LFE */ -pub const VIRTIO_SND_CHMAP_BC: u8 = 38; /* bottom center */ -pub const VIRTIO_SND_CHMAP_BLC: u8 = 39; /* bottom left center */ -pub const VIRTIO_SND_CHMAP_BRC: u8 = 40; /* bottom right center */ -// maximum possible number of channels -pub const VIRTIO_SND_CHMAP_MAX_SIZE: usize = 18; - -/// Virtio Sound Configuration -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -#[repr(C)] -pub struct VirtioSoundConfig { - /// total number of all available jacks - pub jacks: Le32, - /// total number of all available PCM streams - pub streams: Le32, - /// total number of all available channel maps - pub chmaps: Le32, -} - -// SAFETY: The layout of the structure is fixed and can be initialized by -// reading its content from byte array. -unsafe impl ByteValued for VirtioSoundConfig {} - -/// Virtio Sound Request / Response common header -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -#[repr(C)] -pub struct VirtioSoundHeader { - /// request type / response status - pub code: Le32, -} -// SAFETY: The layout of the structure is fixed and can be initialized by -// reading its content from byte array. -unsafe impl ByteValued for VirtioSoundHeader {} - -/// Virtio Sound event notification -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -#[repr(C)] -pub struct VirtioSoundEvent { - /// PCM stream event type - pub hdr: VirtioSoundHeader, - /// PCM stream identifier from 0 to streams - 1 - pub data: Le32, -} -// SAFETY: The layout of the structure is fixed and can be initialized by -// reading its content from byte array. -unsafe impl ByteValued for VirtioSoundEvent {} - -/// Virtio Sound request information about any kind of configuration item -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -#[repr(C)] -pub struct VirtioSoundQueryInfo { - /// item request type (VIRTIO_SND_R_*_INFO) - pub hdr: VirtioSoundHeader, - /// starting identifier for the item - pub start_id: Le32, - /// number of items for which information is requested - pub count: Le32, - /// size of the structure containing information for one item - pub size: Le32, -} -// SAFETY: The layout of the structure is fixed and can be initialized by -// reading its content from byte array. -unsafe impl ByteValued for VirtioSoundQueryInfo {} - -/// Virtio Sound response common information header -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -#[repr(C)] -pub struct VirtioSoundInfo { - /// function group node identifier - pub hda_fn_nid: Le32, -} -// SAFETY: The layout of the structure is fixed and can be initialized by -// reading its content from byte array. -unsafe impl ByteValued for VirtioSoundInfo {} - -/// Jack control request / Jack common header -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -#[repr(C)] -pub struct VirtioSoundJackHeader { - /// jack request type (VIRTIO_SND_R_JACK_*) - pub hdr: VirtioSoundHeader, - /// jack identifier - pub jack_id: Le32, -} -// SAFETY: The layout of the structure is fixed and can be initialized by -// reading its content from byte array. -unsafe impl ByteValued for VirtioSoundJackHeader {} - -/// Jack response information about available jacks -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -#[repr(C)] -pub struct VirtioSoundJackInfo { - /// jack response header type - pub hdr: VirtioSoundInfo, - /// supported feature bit map (VIRTIO_SND_JACK_F_XXX) - pub feature: Le32, - /// pin default configuration value - pub hda_reg_defconf: Le32, - /// pin capabilities value - pub hda_reg_caps: Le32, - /// current jack connection status - pub connected: u8, - pub padding: [u8; 7], -} -// SAFETY: The layout of the structure is fixed and can be initialized by -// reading its content from byte array. -unsafe impl ByteValued for VirtioSoundJackInfo {} - -///If the VIRTIO_SND_JACK_F_REMAP feature bit is set in the jack information -/// Remap control request -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -#[repr(C)] -pub struct VirtioSoundJackRemap { - pub hdr: VirtioSoundJackHeader, /* .code = VIRTIO_SND_R_JACK_REMAP */ - pub association: Le32, - pub sequence: Le32, -} -// SAFETY: The layout of the structure is fixed and can be initialized by -// reading its content from byte array. -unsafe impl ByteValued for VirtioSoundJackRemap {} - -/// PCM control request / PCM common header -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -#[repr(C)] -pub struct VirtioSoundPcmHeader { - pub hdr: VirtioSoundHeader, - pub stream_id: Le32, -} -// SAFETY: The layout of the structure is fixed and can be initialized by -// reading its content from byte array. -unsafe impl ByteValued for VirtioSoundPcmHeader {} - -/// PCM response information -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -#[repr(C)] -pub struct VirtioSoundPcmInfo { - pub hdr: VirtioSoundInfo, - pub features: Le32, /* 1 << VIRTIO_SND_PCM_F_XXX */ - pub formats: Le64, /* 1 << VIRTIO_SND_PCM_FMT_XXX */ - pub rates: Le64, /* 1 << VIRTIO_SND_PCM_RATE_XXX */ - pub direction: u8, - pub channels_min: u8, - pub channels_max: u8, - - pub padding: [u8; 5], -} -// SAFETY: The layout of the structure is fixed and can be initialized by -// reading its content from byte array. -unsafe impl ByteValued for VirtioSoundPcmInfo {} - -/// Set selected stream parameters for the specified stream ID -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -#[repr(C)] -pub struct VirtioSndPcmSetParams { - pub hdr: VirtioSoundPcmHeader, - pub buffer_bytes: Le32, - pub period_bytes: Le32, - pub features: Le32, /* 1 << VIRTIO_SND_PCM_F_XXX */ - pub channels: u8, - pub format: u8, - pub rate: u8, - pub padding: u8, -} -// SAFETY: The layout of the structure is fixed and can be initialized by -// reading its content from byte array. -unsafe impl ByteValued for VirtioSndPcmSetParams {} - -/// PCM I/O header -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -#[repr(C)] -pub struct VirtioSoundPcmXfer { - pub stream_id: Le32, -} -// SAFETY: The layout of the structure is fixed and can be initialized by -// reading its content from byte array. -unsafe impl ByteValued for VirtioSoundPcmXfer {} - -/// PCM I/O status -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -#[repr(C)] -pub struct VirtioSoundPcmStatus { - pub status: Le32, - pub latency_bytes: Le32, -} -// SAFETY: The layout of the structure is fixed and can be initialized by -// reading its content from byte array. -unsafe impl ByteValued for VirtioSoundPcmStatus {} - -/// channel maps response information -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -#[repr(C)] -pub struct VirtioSoundChmapInfo { - pub hdr: VirtioSoundInfo, - pub direction: u8, - pub channels: u8, - pub positions: [u8; VIRTIO_SND_CHMAP_MAX_SIZE], -} -// SAFETY: The layout of the structure is fixed and can be initialized by -// reading its content from byte array. -unsafe impl ByteValued for VirtioSoundChmapInfo {} - -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn test_virtiosound_structs_debug() { - let val = VirtioSoundConfig::default(); - - let debug_output = format!("{:?}", val); - let expected_debug = - "VirtioSoundConfig { jacks: Le32(0), streams: Le32(0), chmaps: Le32(0) }".to_string(); - assert_eq!(debug_output, expected_debug); - - let val = VirtioSoundHeader::default(); - - let debug_output = format!("{:?}", val); - let expected_debug = "VirtioSoundHeader { code: Le32(0) }".to_string(); - assert_eq!(debug_output, expected_debug); - - let val = VirtioSoundEvent::default(); - - let debug_output = format!("{:?}", val); - let expected_debug = format!("VirtioSoundEvent {{ hdr: {:?}, data: Le32(0) }}", val.hdr); - - assert_eq!(debug_output, expected_debug); - - let val = VirtioSoundQueryInfo::default(); - - let debug_output = format!("{:?}", val); - let expected_debug = format!( - "VirtioSoundQueryInfo {{ hdr: {:?}, start_id: Le32(0), count: Le32(0), size: Le32(0) \ - }}", - val.hdr - ); - assert_eq!(debug_output, expected_debug); - - let val = VirtioSoundInfo::default(); - - let debug_output = format!("{:?}", val); - let expected_debug = "VirtioSoundInfo { hda_fn_nid: Le32(0) }".to_string(); - assert_eq!(debug_output, expected_debug); - - let val = VirtioSoundJackHeader::default(); - - let debug_output = format!("{:?}", val); - let expected_debug = format!( - "VirtioSoundJackHeader {{ hdr: {:?}, jack_id: Le32(0) }}", - val.hdr - ); - assert_eq!(debug_output, expected_debug); - - let val = VirtioSoundJackInfo::default(); - - let debug_output = format!("{:?}", val); - let expected_debug = format!( - "VirtioSoundJackInfo {{ hdr: {:?}, feature: Le32(0), hda_reg_defconf: Le32(0), \ - hda_reg_caps: Le32(0), connected: 0, padding: {:?} }}", - val.hdr, val.padding - ); - assert_eq!(debug_output, expected_debug); - - let val = VirtioSoundJackRemap::default(); - - let debug_output = format!("{:?}", val); - let expected_debug = format!( - "VirtioSoundJackRemap {{ hdr: {:?}, association: Le32(0), sequence: Le32(0) }}", - val.hdr - ); - assert_eq!(debug_output, expected_debug); - - let val = VirtioSoundPcmHeader::default(); - - let debug_output = format!("{:?}", val); - let expected_debug = format!( - "VirtioSoundPcmHeader {{ hdr: {:?}, stream_id: Le32(0) }}", - val.hdr - ); - assert_eq!(debug_output, expected_debug); - - let val = VirtioSoundPcmInfo::default(); - - let debug_output = format!("{:?}", val); - let expected_debug = format!( - "VirtioSoundPcmInfo {{ hdr: {:?}, features: Le32(0), formats: Le64(0), rates: \ - Le64(0), direction: 0, channels_min: 0, channels_max: 0, padding: {:?} }}", - val.hdr, val.padding - ); - assert_eq!(debug_output, expected_debug); - - let val = VirtioSndPcmSetParams::default(); - - let debug_output = format!("{:?}", val); - let expected_debug = format!( - "VirtioSndPcmSetParams {{ hdr: {:?}, buffer_bytes: Le32(0), period_bytes: Le32(0), \ - features: Le32(0), channels: 0, format: 0, rate: 0, padding: 0 }}", - val.hdr - ); - assert_eq!(debug_output, expected_debug); - - let val = VirtioSoundPcmXfer::default(); - - let debug_output = format!("{:?}", val); - let expected_debug = "VirtioSoundPcmXfer { stream_id: Le32(0) }".to_string(); - assert_eq!(debug_output, expected_debug); - - let val = VirtioSoundPcmStatus::default(); - - let debug_output = format!("{:?}", val); - let expected_debug = - "VirtioSoundPcmStatus { status: Le32(0), latency_bytes: Le32(0) }".to_string(); - assert_eq!(debug_output, expected_debug); - - let val = VirtioSoundChmapInfo::default(); - - let debug_output = format!("{:?}", val); - let expected_debug = format!( - "VirtioSoundChmapInfo {{ hdr: {:?}, direction: 0, channels: 0, positions: {:?} }}", - val.hdr, val.positions - ); - assert_eq!(debug_output, expected_debug); - } - #[test] - fn test_virtiosound_structs_clone() { - let val = VirtioSoundConfig::default(); - assert_eq!(val, val.clone()); - - let val = VirtioSoundHeader::default(); - assert_eq!(val, val.clone()); - - let val = VirtioSoundEvent::default(); - assert_eq!(val, val.clone()); - - let val = VirtioSoundQueryInfo::default(); - assert_eq!(val, val.clone()); - - let val = VirtioSoundInfo::default(); - assert_eq!(val, val.clone()); - - let val = VirtioSoundJackHeader::default(); - assert_eq!(val, val.clone()); - - let val = VirtioSoundJackInfo::default(); - assert_eq!(val, val.clone()); - - let val = VirtioSoundJackRemap::default(); - assert_eq!(val, val.clone()); - - let val = VirtioSoundPcmHeader::default(); - assert_eq!(val, val.clone()); - - let val = VirtioSoundPcmInfo::default(); - assert_eq!(val, val.clone()); - - let val = VirtioSndPcmSetParams::default(); - assert_eq!(val, val.clone()); - - let val = VirtioSoundPcmXfer::default(); - assert_eq!(val, val.clone()); - - let val = VirtioSoundPcmStatus::default(); - assert_eq!(val, val.clone()); - - let val = VirtioSoundChmapInfo::default(); - assert_eq!(val, val.clone()); - } -} diff --git a/vendor/krun-devices/src/virtio/snd/worker.rs b/vendor/krun-devices/src/virtio/snd/worker.rs deleted file mode 100644 index e7950d557..000000000 --- a/vendor/krun-devices/src/virtio/snd/worker.rs +++ /dev/null @@ -1,648 +0,0 @@ -use std::collections::BTreeSet; -use std::mem::size_of; -use std::os::fd::AsRawFd; -use std::sync::{Arc, Mutex, RwLock}; -use std::{result, thread}; - -use utils::epoll::{ControlOperation, Epoll, EpollEvent, EventSet}; -use utils::eventfd::EventFd; -use vm_memory::{ByteValued, Bytes, GuestMemoryMmap}; - -use super::super::DeviceQueue; -use super::audio_backends::{alloc_audio_backend, AudioBackend}; -use super::defs::{CTL_INDEX, EVT_INDEX, QUEUE_INDEXES, RXQ_INDEX, TXQ_INDEX}; -use super::stream::{Error as StreamError, Stream}; -use super::virtio_sound::{ - VirtioSndPcmSetParams, VirtioSoundHeader, VirtioSoundPcmHeader, VirtioSoundPcmInfo, - VirtioSoundPcmStatus, VirtioSoundPcmXfer, VirtioSoundQueryInfo, VIRTIO_SND_D_INPUT, - VIRTIO_SND_D_OUTPUT, VIRTIO_SND_S_BAD_MSG, VIRTIO_SND_S_IO_ERR, VIRTIO_SND_S_NOT_SUPP, - VIRTIO_SND_S_OK, -}; -use super::{ - BackendType, Direction, Error, VirtioSoundChmapInfo, VirtioSoundJackInfo, Vring, - VIRTIO_SND_CHMAP_FL, VIRTIO_SND_CHMAP_FR, VIRTIO_SND_CHMAP_MAX_SIZE, VIRTIO_SND_CHMAP_NONE, -}; -use crate::virtio::snd::stream::Buffer; -use crate::virtio::snd::{ControlMessageKind, IOMessage}; -use crate::virtio::{DescriptorChain, InterruptTransport}; - -pub struct SndWorker { - vrings: Vec>>, - queue_events: Vec>, - interrupt: InterruptTransport, - mem: GuestMemoryMmap, - streams: Arc>>, - streams_no: usize, - chmaps: Arc>>, - jacks: Arc>>, - audio_backend: RwLock>, - stop_fd: EventFd, -} - -impl SndWorker { - pub fn new( - queues: Vec, - interrupt: InterruptTransport, - mem: GuestMemoryMmap, - stop_fd: EventFd, - ) -> Self { - let streams = vec![ - Stream { - id: 0, - direction: Direction::Output, - ..Stream::default() - }, - Stream { - id: 1, - direction: Direction::Input, - ..Stream::default() - }, - ]; - let streams_no = streams.len(); - let streams = Arc::new(RwLock::new(streams)); - let jacks: Arc>> = Arc::new(RwLock::new(Vec::new())); - let mut positions = [VIRTIO_SND_CHMAP_NONE; VIRTIO_SND_CHMAP_MAX_SIZE]; - positions[0] = VIRTIO_SND_CHMAP_FL; - positions[1] = VIRTIO_SND_CHMAP_FR; - let chmaps_info: Vec = vec![ - VirtioSoundChmapInfo { - direction: VIRTIO_SND_D_OUTPUT, - channels: 2, - positions, - ..VirtioSoundChmapInfo::default() - }, - VirtioSoundChmapInfo { - direction: VIRTIO_SND_D_INPUT, - channels: 2, - positions, - ..VirtioSoundChmapInfo::default() - }, - ]; - let chmaps: Arc>> = Arc::new(RwLock::new(chmaps_info)); - - let audio_backend = - RwLock::new(alloc_audio_backend(BackendType::Pipewire, streams.clone()).unwrap()); - - let mut vrings: Vec>> = Vec::new(); - let mut queue_events: Vec> = Vec::new(); - - for dq in queues { - vrings.push(Arc::new(Mutex::new(Vring { - mem: mem.clone(), - queue: dq.queue, - interrupt: interrupt.clone(), - }))); - queue_events.push(dq.event); - } - - Self { - vrings, - queue_events, - interrupt, - mem, - streams, - streams_no, - jacks, - chmaps, - audio_backend, - stop_fd, - } - } - - pub fn run(self) -> thread::JoinHandle<()> { - thread::Builder::new() - .name("virtio-snd worker".into()) - .spawn(|| self.work()) - .unwrap() - } - - fn work(mut self) { - let epoll = Epoll::new().unwrap(); - - for idx in QUEUE_INDEXES { - let fd = self.queue_events[idx].as_raw_fd(); - epoll - .ctl( - ControlOperation::Add, - fd, - &EpollEvent::new(EventSet::IN, idx as u64), - ) - .unwrap(); - } - - let stop_ev_fd = self.stop_fd.as_raw_fd(); - epoll - .ctl( - ControlOperation::Add, - stop_ev_fd, - &EpollEvent::new(EventSet::IN, stop_ev_fd as u64), - ) - .unwrap(); - - loop { - let mut epoll_events = vec![EpollEvent::new(EventSet::empty(), 0); 32]; - match epoll.wait(epoll_events.len(), -1, epoll_events.as_mut_slice()) { - Ok(ev_cnt) => { - for event in &epoll_events[0..ev_cnt] { - let source = event.fd(); - let data = event.data(); - let event_set = event.event_set(); - match event_set { - EventSet::IN if data < QUEUE_INDEXES.len() as u64 => { - self.handle_event(data.try_into().unwrap()); - } - EventSet::IN if source == stop_ev_fd => { - debug!("stopping worker thread"); - let _ = self.stop_fd.read(); - return; - } - _ => { - log::warn!( - "Received unknown event: {event_set:?} from fd: {source:?}" - ); - } - } - } - } - Err(e) => { - debug!("failed to consume muxer epoll event: {e}"); - } - } - } - } - - fn handle_event(&mut self, queue_index: usize) { - debug!("Fs: queue event: {queue_index}"); - if let Err(e) = self.queue_events[queue_index].read() { - error!("Failed to get queue event: {e:?}"); - } - - let vring_lock = &self.vrings[queue_index]; - - loop { - vring_lock - .lock() - .unwrap() - .queue - .disable_notification(&self.mem) - .unwrap(); - - self.process_queue(vring_lock, queue_index); - - if !vring_lock - .lock() - .unwrap() - .queue - .enable_notification(&self.mem) - .unwrap() - { - break; - } - } - } - - pub fn process_queue(&self, vring_lock: &Arc>, queue_index: usize) { - debug!("snd: process_queue()"); - - loop { - let mut vring = vring_lock.lock().unwrap(); - let head = vring.queue.pop(&self.mem); - drop(vring); - - if let Some(head) = head { - let ret = match queue_index { - CTL_INDEX => self.process_ctl(vring_lock, head), - EVT_INDEX => self.process_evt(vring_lock, head), - RXQ_INDEX => self.process_io(vring_lock, head, Direction::Input), - TXQ_INDEX => self.process_io(vring_lock, head, Direction::Output), - _ => unreachable!(), - }; - if let Err(err) = ret { - error!("error processing queue {queue_index}: {err}"); - } - - if vring_lock - .lock() - .unwrap() - .queue - .needs_notification(&self.mem) - .unwrap() - { - self.interrupt.signal_used_queue(); - } - } else { - break; - } - } - } - - fn process_ctl( - &self, - vring_lock: &Arc>, - head: DescriptorChain, - ) -> result::Result<(), Error> { - let descriptors: Vec<_> = head.clone().into_iter().collect(); - if descriptors.len() < 2 { - return Err(Error::UnexpectedDescriptorCount(descriptors.len())); - } - - // Request descriptor. - let desc_request = &descriptors[0]; - if desc_request.is_write_only() { - return Err(Error::UnexpectedWriteOnlyDescriptor(0)); - } - - let request = self - .mem - .read_obj::(desc_request.addr) - .map_err(|_| Error::DescriptorReadFailed)?; - - // Keep track of bytes that will be written in the VQ. - let mut used_len = 0; - - // Reply header descriptor. - let desc_hdr = &descriptors[1]; - if !desc_hdr.is_write_only() { - return Err(Error::UnexpectedReadableDescriptor(1)); - } - - let mut resp = VirtioSoundHeader { - code: VIRTIO_SND_S_OK.into(), - }; - - let code = ControlMessageKind::try_from(request.code.to_native()).unwrap(); - match code { - ControlMessageKind::ChmapInfo => { - if descriptors.len() != 3 { - log::error!("a CHMAP_INFO request should have three descriptors total."); - return Err(Error::UnexpectedDescriptorCount(descriptors.len())); - } else if !descriptors[2].is_write_only() { - log::error!( - "a CHMAP_INFO request should have a writeable descriptor for the info \ - payload response after the header status response" - ); - return Err(Error::UnexpectedReadableDescriptor(2)); - } - let request = self - .mem - .read_obj::(desc_request.addr) - .map_err(|_| Error::DescriptorReadFailed)?; - let start_id = u32::from(request.start_id) as usize; - let count = u32::from(request.count) as usize; - let chmaps = self.chmaps.read().unwrap(); - if chmaps.len() <= start_id || chmaps.len() < start_id + count { - resp.code = VIRTIO_SND_S_BAD_MSG.into(); - } else { - let desc_response = &descriptors[2]; - let mut buf = vec![]; - - for i in chmaps.iter().skip(start_id).take(count) { - buf.extend_from_slice(i.as_slice()); - } - drop(chmaps); - self.mem - .write_slice(&buf, desc_response.addr) - .map_err(|_| Error::DescriptorWriteFailed)?; - used_len += desc_response.len; - } - } - ControlMessageKind::JackInfo => { - if descriptors.len() != 3 { - log::error!("a JACK_INFO request should have three descriptors total."); - return Err(Error::UnexpectedDescriptorCount(descriptors.len())); - } else if !descriptors[2].is_write_only() { - log::error!( - "a JACK_INFO request should have a writeable descriptor for the info \ - payload response after the header status response" - ); - return Err(Error::UnexpectedReadableDescriptor(2)); - } - let request = self - .mem - .read_obj::(desc_request.addr) - .map_err(|_| Error::DescriptorReadFailed)?; - - let start_id = u32::from(request.start_id) as usize; - let count = u32::from(request.count) as usize; - let jacks = self.jacks.read().unwrap(); - if jacks.len() <= start_id || jacks.len() < start_id + count { - resp.code = VIRTIO_SND_S_BAD_MSG.into(); - } else { - let desc_response = &descriptors[2]; - let mut buf = vec![]; - - for i in jacks.iter().skip(start_id).take(count) { - buf.extend_from_slice(i.as_slice()); - } - drop(jacks); - self.mem - .write_slice(&buf, desc_response.addr) - .map_err(|_| Error::DescriptorWriteFailed)?; - used_len += desc_response.len; - } - } - ControlMessageKind::JackRemap => { - resp.code = VIRTIO_SND_S_NOT_SUPP.into(); - } - ControlMessageKind::PcmInfo => { - if descriptors.len() != 3 { - log::error!("a PCM_INFO request should have three descriptors total."); - return Err(Error::UnexpectedDescriptorCount(descriptors.len())); - } else if !descriptors[2].is_write_only() { - log::error!( - "a PCM_INFO request should have a writeable descriptor for the info \ - payload response after the header status response" - ); - return Err(Error::UnexpectedReadableDescriptor(2)); - } - - let request = self - .mem - .read_obj::(desc_request.addr) - .map_err(|_| Error::DescriptorReadFailed)?; - - let start_id = u32::from(request.start_id) as usize; - let count = u32::from(request.count) as usize; - let streams = self.streams.read().unwrap(); - if streams.len() <= start_id || streams.len() < start_id + count { - resp.code = VIRTIO_SND_S_BAD_MSG.into(); - } else { - let desc_response = &descriptors[2]; - - let mut buf = vec![]; - let mut p: VirtioSoundPcmInfo; - - for s in streams - .iter() - .skip(u32::from(request.start_id) as usize) - .take(u32::from(request.count) as usize) - { - p = VirtioSoundPcmInfo::default(); - p.hdr.hda_fn_nid = 0.into(); - p.features = s.params.features; - p.formats = s.formats; - p.rates = s.rates; - p.direction = s.direction as u8; - p.channels_min = s.channels_min; - p.channels_max = s.channels_max; - buf.extend_from_slice(p.as_slice()); - } - drop(streams); - self.mem - .write_slice(&buf, desc_response.addr) - .map_err(|_| Error::DescriptorWriteFailed)?; - used_len += desc_response.len; - } - } - ControlMessageKind::PcmSetParams => { - let request = self - .mem - .read_obj::(desc_request.addr) - .map_err(|_| Error::DescriptorReadFailed)?; - let stream_id: u32 = request.hdr.stream_id.into(); - - if stream_id as usize >= self.streams_no { - log::error!("{}", Error::from(StreamError::InvalidStreamId(stream_id))); - resp.code = VIRTIO_SND_S_BAD_MSG.into(); - } else if let Err(err) = self - .audio_backend - .read() - .unwrap() - .set_parameters(stream_id, request) - { - match err { - Error::Stream(_) | Error::StreamWithIdNotFound(_) => { - resp.code = VIRTIO_SND_S_BAD_MSG.into() - } - Error::UnexpectedAudioBackendConfiguration => { - resp.code = VIRTIO_SND_S_NOT_SUPP.into() - } - _ => { - log::error!("{err}"); - resp.code = VIRTIO_SND_S_IO_ERR.into() - } - } - } - } - ControlMessageKind::PcmPrepare => { - let request = self - .mem - .read_obj::(desc_request.addr) - .map_err(|_| Error::DescriptorReadFailed)?; - let stream_id = request.stream_id.into(); - - if stream_id as usize >= self.streams_no { - log::error!("{}", Error::from(StreamError::InvalidStreamId(stream_id))); - resp.code = VIRTIO_SND_S_BAD_MSG.into(); - } else { - self.audio_backend - .write() - .unwrap() - .prepare(stream_id) - .unwrap(); - } - } - ControlMessageKind::PcmRelease => { - let request = self - .mem - .read_obj::(desc_request.addr) - .map_err(|_| Error::DescriptorReadFailed)?; - let stream_id = request.stream_id.into(); - - if stream_id as usize >= self.streams_no { - log::error!("{}", Error::from(StreamError::InvalidStreamId(stream_id))); - resp.code = VIRTIO_SND_S_BAD_MSG.into(); - } else if let Err(err) = self.audio_backend.write().unwrap().release(stream_id) { - match err { - Error::Stream(_) | Error::StreamWithIdNotFound(_) => { - resp.code = VIRTIO_SND_S_BAD_MSG.into() - } - _ => { - log::error!("{err}"); - resp.code = VIRTIO_SND_S_IO_ERR.into() - } - } - } - } - ControlMessageKind::PcmStart => { - let request = self - .mem - .read_obj::(desc_request.addr) - .map_err(|_| Error::DescriptorReadFailed)?; - let stream_id = request.stream_id.into(); - - if stream_id as usize >= self.streams_no { - log::error!("{}", Error::from(StreamError::InvalidStreamId(stream_id))); - resp.code = VIRTIO_SND_S_BAD_MSG.into(); - } else { - self.audio_backend - .write() - .unwrap() - .start(stream_id) - .unwrap(); - } - } - ControlMessageKind::PcmStop => { - let request = self - .mem - .read_obj::(desc_request.addr) - .map_err(|_| Error::DescriptorReadFailed)?; - let stream_id = request.stream_id.into(); - - if stream_id as usize >= self.streams_no { - log::error!("{}", Error::from(StreamError::InvalidStreamId(stream_id))); - resp.code = VIRTIO_SND_S_BAD_MSG.into(); - } else { - self.audio_backend.write().unwrap().stop(stream_id).unwrap(); - } - } - } - debug!( - "returned {} for ctrl msg {:?}", - match u32::from(resp.code) { - v if v == VIRTIO_SND_S_OK => "OK", - v if v == VIRTIO_SND_S_BAD_MSG => "BAD_MSG", - v if v == VIRTIO_SND_S_NOT_SUPP => "NOT_SUPP", - v if v == VIRTIO_SND_S_IO_ERR => "IO_ERR", - _ => unreachable!(), - }, - code - ); - - self.mem.write_obj(resp, desc_hdr.addr).unwrap(); - if let Err(err) = vring_lock - .lock() - .unwrap() - .queue - .add_used(&self.mem, head.index, used_len) - { - error!("Error adding used descriptors to the queue: {err}"); - } - - Ok(()) - } - - fn process_evt( - &self, - _vring_lock: &Arc>, - _head: DescriptorChain, - ) -> result::Result<(), Error> { - error!("virtio_snd: unimplemented process_evt"); - Ok(()) - } - - fn process_io( - &self, - vring_lock: &Arc>, - desc_chain: DescriptorChain, - direction: Direction, - ) -> result::Result<(), Error> { - #[derive(Copy, Clone, PartialEq, Debug)] - enum IoState { - Ready, - WaitingBufferForStreamId(u32), - Done, - } - - let mut stream_ids = BTreeSet::default(); - - let mut state = IoState::Ready; - let mut buffers: Vec = vec![]; - - let descriptors: Vec<_> = desc_chain.clone().into_iter().collect(); - let message = Arc::new(IOMessage { - status: VIRTIO_SND_S_OK.into(), - used_len: 0.into(), - latency_bytes: 0.into(), - head_index: desc_chain.index, - response_descriptor: descriptors - .last() - .ok_or_else(|| { - log::error!("Received IO request with an empty descriptor chain."); - Error::UnexpectedDescriptorCount(0) - })? - .descriptor(), - vring: vring_lock.clone(), - }); - - for descriptor in &descriptors { - match state { - IoState::Done => { - return Err(Error::UnexpectedDescriptorCount(descriptors.len())); - } - IoState::Ready - if matches!(direction, Direction::Output) && descriptor.is_write_only() => - { - if descriptor.len as usize != size_of::() { - return Err(Error::UnexpectedDescriptorSize( - size_of::(), - descriptor.len, - )); - } - state = IoState::Done; - } - IoState::WaitingBufferForStreamId(stream_id) - if descriptor.len as usize == size_of::() => - { - self.streams.write().unwrap()[stream_id as usize] - .buffers - .extend(std::mem::take(&mut buffers)); - state = IoState::Done; - } - IoState::Ready if descriptor.len as usize != size_of::() => { - return Err(Error::UnexpectedDescriptorSize( - size_of::(), - descriptor.len, - )); - } - IoState::Ready => { - let xfer = self - .mem - .read_obj::(descriptor.addr) - .map_err(|_| Error::DescriptorReadFailed)?; - let stream_id: u32 = xfer.stream_id.into(); - stream_ids.insert(stream_id); - - state = IoState::WaitingBufferForStreamId(stream_id); - } - IoState::WaitingBufferForStreamId(stream_id) - if descriptor.len as usize == size_of::() => - { - return Err(Error::UnexpectedDescriptorSize( - u32::from( - self.streams.read().unwrap()[stream_id as usize] - .params - .period_bytes, - ) as usize, - descriptor.len, - )); - } - IoState::WaitingBufferForStreamId(_) => { - // In the case of TX/Playback: - // - // Rather than copying the content of a descriptor, buffer keeps a pointer - // to it. When we copy just after the request is enqueued, the guest's - // userspace may or may not have updated the buffer contents. Guest driver - // simply moves buffers from the used ring to the available ring without - // knowing whether the content has been updated. The device only reads the - // buffer from guest memory when the audio engine requires it, which is - // about after a period thus ensuring that the buffer is up-to-date. - buffers.push(Buffer::new( - descriptor.descriptor(), - Arc::clone(&message), - direction, - )); - } - } - } - - if !stream_ids.is_empty() { - let b = self.audio_backend.read().unwrap(); - for id in stream_ids { - b.write(id).unwrap(); - } - } - - Ok(()) - } -} diff --git a/vendor/krun-devices/src/virtio/vsock/device.rs b/vendor/krun-devices/src/virtio/vsock/device.rs deleted file mode 100644 index a61043b4c..000000000 --- a/vendor/krun-devices/src/virtio/vsock/device.rs +++ /dev/null @@ -1,279 +0,0 @@ -// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -// -// Portions Copyright 2017 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the THIRD-PARTY file. - -use std::collections::HashMap; -use std::path::PathBuf; -use std::sync::{Arc, Mutex}; - -use utils::byte_order; -use utils::eventfd::EventFd; -use vm_memory::GuestMemoryMmap; - -use super::super::{ - ActivateError, ActivateResult, DeviceQueue, DeviceState, Queue as VirtQueue, QueueConfig, - VirtioDevice, -}; -use super::muxer::VsockMuxer; -use super::packet::VsockPacket; -use super::TsiFlags; -use super::{defs, defs::uapi}; -use crate::virtio::InterruptTransport; - -pub(crate) const RXQ_INDEX: usize = 0; -pub(crate) const TXQ_INDEX: usize = 1; -pub(crate) const EVQ_INDEX: usize = 2; - -/// The virtio features supported by our vsock device: -/// - VIRTIO_F_VERSION_1: the device conforms to at least version 1.0 of the VirtIO spec. -/// - VIRTIO_F_IN_ORDER: the device returns used buffers in the same order that the driver makes -/// them available. -pub(crate) const AVAIL_FEATURES: u64 = (1 << uapi::VIRTIO_F_VERSION_1 as u64) - | (1 << uapi::VIRTIO_F_IN_ORDER as u64) - | (1 << uapi::VIRTIO_VSOCK_F_DGRAM); - -pub struct Vsock { - cid: u64, - pub(crate) muxer: VsockMuxer, - pub(crate) queue_rx: Option>>, - pub(crate) queue_tx: Option>>, - // Queue events are stored separately for event handling. - pub(crate) queue_events: Vec>, - pub(crate) avail_features: u64, - pub(crate) acked_features: u64, - pub(crate) activate_evt: EventFd, - pub(crate) device_state: DeviceState, -} - -impl Vsock { - /// Create a new virtio-vsock device with the given VM CID. - pub fn new( - cid: u64, - host_port_map: Option>, - unix_ipc_port_map: Option>, - tsi_flags: TsiFlags, - ) -> super::Result { - Ok(Vsock { - cid, - muxer: VsockMuxer::new(cid, host_port_map, unix_ipc_port_map, tsi_flags), - queue_rx: None, - queue_tx: None, - queue_events: Vec::new(), - avail_features: AVAIL_FEATURES, - acked_features: 0, - activate_evt: EventFd::new(utils::eventfd::EFD_NONBLOCK) - .map_err(super::VsockError::EventFd)?, - device_state: DeviceState::Inactive, - }) - } - - pub fn id(&self) -> &str { - defs::VSOCK_DEV_ID - } - - pub fn cid(&self) -> u64 { - self.cid - } - - /// Walk the driver-provided RX queue buffers and attempt to fill them up with any data that we - /// have pending. Return `true` if descriptors have been added to the used ring, and `false` - /// otherwise. - pub fn process_stream_rx(&mut self) -> bool { - debug!("process_stream_rx()"); - let mem = match self.device_state { - DeviceState::Activated(ref mem, _) => mem, - // This should never happen, it's been already validated in the event handler. - DeviceState::Inactive => unreachable!(), - }; - - let mut have_used = false; - - debug!("process_rx before while"); - let queue_rx = self - .queue_rx - .as_ref() - .expect("queue_rx should exist when activated"); - let mut queue_rx = queue_rx.lock().unwrap(); - while let Some(head) = queue_rx.pop(mem) { - debug!("process_rx inside while"); - let used_len = match VsockPacket::from_rx_virtq_head(&head) { - Ok(mut pkt) => { - if self.muxer.recv_pkt(&mut pkt).is_ok() { - pkt.hdr().len() as u32 + pkt.len() - } else { - // We are using a consuming iterator over the virtio buffers, so, if we can't - // fill in this buffer, we'll need to undo the last iterator step. - queue_rx.undo_pop(); - break; - } - } - Err(e) => { - warn!("RX queue error: {e:?}"); - 0 - } - }; - - debug!("process_rx: something to queue"); - have_used = true; - if let Err(e) = queue_rx.add_used(mem, head.index, used_len) { - error!("failed to add used elements to the queue: {e:?}"); - } - } - - have_used - } - - /// Walk the driver-provided TX queue buffers, package them up as vsock packets, and process - /// them. Return `true` if descriptors have been added to the used ring, and `false` otherwise. - pub fn process_stream_tx(&mut self) -> bool { - debug!("process_stream_tx()"); - let mem = match self.device_state { - DeviceState::Activated(ref mem, _) => mem, - // This should never happen, it's been already validated in the event handler. - DeviceState::Inactive => unreachable!(), - }; - - let mut have_used = false; - - let queue_tx = self - .queue_tx - .as_ref() - .expect("queue_tx should exist when activated"); - let mut queue_tx = queue_tx.lock().unwrap(); - while let Some(head) = queue_tx.pop(mem) { - let pkt = match VsockPacket::from_tx_virtq_head(&head) { - Ok(pkt) => pkt, - Err(e) => { - error!("error reading TX packet: {e:?}"); - have_used = true; - if let Err(e) = queue_tx.add_used(mem, head.index, 0) { - error!("failed to add used elements to the queue: {e:?}"); - } - continue; - } - }; - - if pkt.type_() == uapi::VSOCK_TYPE_DGRAM { - debug!("process_stream_tx() is DGRAM"); - if self.muxer.send_dgram_pkt(&pkt).is_err() { - queue_tx.undo_pop(); - break; - } - } else { - debug!("process_stream_tx() is STREAM"); - if self.muxer.send_stream_pkt(&pkt).is_err() { - queue_tx.undo_pop(); - break; - } - } - - have_used = true; - if let Err(e) = queue_tx.add_used(mem, head.index, 0) { - error!("failed to add used elements to the queue: {e:?}"); - } - } - - have_used - } -} - -impl VirtioDevice for Vsock { - fn avail_features(&self) -> u64 { - self.avail_features - } - - fn acked_features(&self) -> u64 { - self.acked_features - } - - fn set_acked_features(&mut self, acked_features: u64) { - self.acked_features = acked_features - } - - fn device_type(&self) -> u32 { - uapi::VIRTIO_ID_VSOCK - } - - fn device_name(&self) -> &str { - "vsock" - } - - fn queue_config(&self) -> &[QueueConfig] { - &defs::QUEUE_CONFIG - } - - fn read_config(&self, offset: u64, data: &mut [u8]) { - match offset { - 0 if data.len() == 8 => byte_order::write_le_u64(data, self.cid()), - 0 if data.len() == 4 => { - byte_order::write_le_u32(data, (self.cid() & 0xffff_ffff) as u32) - } - 4 if data.len() == 4 => { - byte_order::write_le_u32(data, ((self.cid() >> 32) & 0xffff_ffff) as u32) - } - _ => warn!( - "virtio-vsock received invalid read request of {} bytes at offset {}", - data.len(), - offset - ), - } - } - - fn write_config(&mut self, offset: u64, data: &[u8]) { - warn!( - "guest driver attempted to write device config (offset={:x}, len={:x})", - offset, - data.len() - ); - } - - fn activate( - &mut self, - mem: GuestMemoryMmap, - interrupt: InterruptTransport, - queues: Vec, - ) -> ActivateResult { - if queues.len() != defs::NUM_QUEUES { - error!( - "Cannot perform activate. Expected {} queue(s), got {}", - defs::NUM_QUEUES, - queues.len() - ); - return Err(ActivateError::BadActivate); - } - - if self.activate_evt.write(1).is_err() { - error!("Cannot write to activate_evt",); - return Err(ActivateError::BadActivate); - } - - // Store queue events for event handling. - self.queue_events = queues.iter().map(|dq| dq.event.clone()).collect(); - - // Extract queues from DeviceQueues and wrap in Arc>. - let mut queues_vec: Vec = queues.into_iter().map(|dq| dq.queue).collect(); - // Note: EVQ (index 2) is currently unused, we just take it to maintain the vec. - let _evq = queues_vec.pop().unwrap(); - let tx_queue = queues_vec.pop().unwrap(); - let rx_queue = queues_vec.pop().unwrap(); - - self.queue_tx = Some(Arc::new(Mutex::new(tx_queue))); - self.queue_rx = Some(Arc::new(Mutex::new(rx_queue))); - self.muxer.activate( - mem.clone(), - self.queue_rx.clone().unwrap(), - interrupt.clone(), - ); - - self.device_state = DeviceState::Activated(mem, interrupt); - - Ok(()) - } - - fn is_activated(&self) -> bool { - self.device_state.is_activated() - } -} diff --git a/vendor/krun-devices/src/virtio/vsock/event_handler.rs b/vendor/krun-devices/src/virtio/vsock/event_handler.rs deleted file mode 100644 index b7fb15799..000000000 --- a/vendor/krun-devices/src/virtio/vsock/event_handler.rs +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -// -// Portions Copyright 2017 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the THIRD-PARTY file. - -use std::os::unix::io::AsRawFd; - -use polly::event_manager::{EventManager, Subscriber}; -use utils::epoll::{EpollEvent, EventSet}; - -use super::device::{Vsock, EVQ_INDEX, RXQ_INDEX, TXQ_INDEX}; -use crate::virtio::VirtioDevice; - -impl Vsock { - pub(crate) fn handle_rxq_event(&mut self, event: &EpollEvent) -> bool { - debug!("RX queue event"); - - let event_set = event.event_set(); - if event_set != EventSet::IN { - warn!("rxq unexpected event {event_set:?}"); - return false; - } - - let mut raise_irq = false; - if let Err(e) = self.queue_events[RXQ_INDEX].read() { - error!("Failed to get vsock rx queue event: {e:?}"); - } else { - raise_irq |= self.process_stream_rx(); - } - raise_irq - } - - pub(crate) fn handle_txq_event(&mut self, event: &EpollEvent) -> bool { - debug!("TX queue event"); - - let event_set = event.event_set(); - if event_set != EventSet::IN { - warn!("txq unexpected event {event_set:?}"); - return false; - } - - let mut raise_irq = false; - if let Err(e) = self.queue_events[TXQ_INDEX].read() { - error!("Failed to get vsock tx queue event: {e:?}"); - } else { - raise_irq |= self.process_stream_tx(); - // The backend may have queued up responses to the packets we sent during - // TX queue processing. If that happened, we need to fetch those responses - // and place them into RX buffers. - if self.muxer.has_pending_rx() { - raise_irq |= self.process_stream_rx(); - } - } - raise_irq - } - - fn handle_evq_event(&mut self, event: &EpollEvent) -> bool { - debug!("event queue event"); - - let event_set = event.event_set(); - if event_set != EventSet::IN { - warn!("evq unexpected event {event_set:?}"); - return false; - } - - if let Err(e) = self.queue_events[EVQ_INDEX].read() { - error!("Failed to consume vsock evq event: {e:?}"); - } - false - } - - fn handle_activate_event(&self, event_manager: &mut EventManager) { - debug!("activate event"); - if let Err(e) = self.activate_evt.read() { - error!("Failed to consume vsock activate event: {e:?}"); - } - - // The subscriber must exist as we previously registered activate_evt via - // `interest_list()`. - let self_subscriber = event_manager - .subscriber(self.activate_evt.as_raw_fd()) - .unwrap(); - - event_manager - .register( - self.queue_events[RXQ_INDEX].as_raw_fd(), - EpollEvent::new( - EventSet::IN, - self.queue_events[RXQ_INDEX].as_raw_fd() as u64, - ), - self_subscriber.clone(), - ) - .unwrap_or_else(|e| { - error!("Failed to register vsock rxq with event manager: {e:?}"); - }); - - event_manager - .register( - self.queue_events[TXQ_INDEX].as_raw_fd(), - EpollEvent::new( - EventSet::IN, - self.queue_events[TXQ_INDEX].as_raw_fd() as u64, - ), - self_subscriber.clone(), - ) - .unwrap_or_else(|e| { - error!("Failed to register vsock txq with event manager: {e:?}"); - }); - - event_manager - .unregister(self.activate_evt.as_raw_fd()) - .unwrap_or_else(|e| { - error!("Failed to unregister vsock activate evt: {e:?}"); - }) - } -} - -impl Subscriber for Vsock { - fn process(&mut self, event: &EpollEvent, event_manager: &mut EventManager) { - let source = event.fd(); - let rxq = self.queue_events[RXQ_INDEX].as_raw_fd(); - let txq = self.queue_events[TXQ_INDEX].as_raw_fd(); - let evq = self.queue_events[EVQ_INDEX].as_raw_fd(); - //let backend = self.backend.as_raw_fd(); - let activate_evt = self.activate_evt.as_raw_fd(); - - if self.is_activated() { - let mut raise_irq = false; - match source { - _ if source == rxq => raise_irq = self.handle_rxq_event(event), - _ if source == txq => raise_irq = self.handle_txq_event(event), - _ if source == evq => raise_irq = self.handle_evq_event(event), - /* - _ if source == backend => { - raise_irq = self.notify_backend(event); - } - */ - _ if source == activate_evt => { - self.handle_activate_event(event_manager); - } - _ => warn!("Unexpected vsock event received: {source:?}"), - } - if raise_irq { - debug!("raising IRQ"); - self.device_state.signal_used_queue(); - } - } else { - warn!("The device is not yet activated. Spurious event received: {source:?}"); - } - } - - fn interest_list(&self) -> Vec { - vec![EpollEvent::new( - EventSet::IN, - self.activate_evt.as_raw_fd() as u64, - )] - } -} diff --git a/vendor/krun-devices/src/virtio/vsock/mod.rs b/vendor/krun-devices/src/virtio/vsock/mod.rs deleted file mode 100644 index c307c5443..000000000 --- a/vendor/krun-devices/src/virtio/vsock/mod.rs +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -// -// Portions Copyright 2017 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the THIRD-PARTY file. - -mod device; -mod event_handler; -mod muxer; -mod muxer_rxq; -mod muxer_thread; -#[allow(dead_code)] -mod packet; -mod proxy; -mod reaper; -#[cfg(target_os = "macos")] -mod timesync; -mod tsi_dgram; -mod tsi_stream; -mod unix; - -pub use self::defs::uapi::VIRTIO_ID_VSOCK as TYPE_VSOCK; -pub use self::defs::TsiFlags; -pub use self::device::Vsock; - -use bitflags::bitflags; -use vm_memory::GuestMemoryError; - -mod defs { - use super::bitflags; - - bitflags! { - /// TSI (Transparent Socket Impersonation) feature flags. - /// - /// These flags control which socket families are hijacked by TSI. - pub struct TsiFlags: u32 { - /// Hijack AF_INET and AF_INET6 sockets - const HIJACK_INET = 1 << 0; - /// Hijack AF_UNIX sockets - const HIJACK_UNIX = 1 << 1; - } - } - - impl TsiFlags { - /// Returns true if any TSI hijacking is enabled. - pub fn tsi_enabled(&self) -> bool { - !self.is_empty() - } - } - - impl Default for TsiFlags { - fn default() -> Self { - TsiFlags::empty() - } - } - /// Device ID used in MMIO device identification. - /// Because Vsock is unique per-vm, this ID can be hardcoded. - pub const VSOCK_DEV_ID: &str = "vsock"; - - use crate::virtio::QueueConfig; - - /// Number of virtio queues. - pub const NUM_QUEUES: usize = 3; - const QUEUE_SIZE: u16 = 256; - /// Virtio queue config. - /// There are 3 queues for a virtio device (in this order): RX, TX, Event - pub static QUEUE_CONFIG: [QueueConfig; NUM_QUEUES] = [QueueConfig::new(QUEUE_SIZE); NUM_QUEUES]; - - /// Max vsock packet data/buffer size. - pub const MAX_PKT_BUF_SIZE: usize = 64 * 1024; - - /// Size of the muxer RX packet queue. - pub const MUXER_RXQ_SIZE: usize = 256; - - // Kernel side doesn't play nice with us supporting so many bytes - //pub const CONN_TX_BUF_SIZE: usize = i32::MAX as usize; - pub const CONN_TX_BUF_SIZE: usize = 8 * 1024 * 1024; - pub const SOCK_STREAM: u16 = 1; - pub const SOCK_DGRAM: u16 = 2; - - /// Misc - pub const TSI_PROXY_PORT: u32 = 620; - pub const TSI_PROXY_CREATE: u32 = 1024; - pub const TSI_CONNECT: u32 = 1025; - pub const TSI_GETNAME: u32 = 1026; - pub const TSI_SENDTO_ADDR: u32 = 1027; - pub const TSI_SENDTO_DATA: u32 = 1028; - pub const TSI_LISTEN: u32 = 1029; - pub const TSI_ACCEPT: u32 = 1030; - pub const TSI_PROXY_RELEASE: u32 = 1031; - - // Linux definitions that we need for cross-platform compatibility. - pub const LINUX_AF_UNIX: u16 = 1; - pub const LINUX_AF_INET: u16 = 2; - pub const LINUX_AF_INET6: u16 = 10; - - pub mod uapi { - - /// Virtio feature flags. - /// Defined in `/include/uapi/linux/virtio_config.h`. - /// - /// The device processes available buffers in the same order in which the device - /// offers them. - pub const VIRTIO_F_IN_ORDER: usize = 35; - /// The device conforms to the virtio spec version 1.0. - pub const VIRTIO_F_VERSION_1: u32 = 32; - /// The device supports DGRAM. - pub const VIRTIO_VSOCK_F_DGRAM: u32 = 3; - - /// Virtio vsock device ID. - /// Defined in `include/uapi/linux/virtio_ids.h`. - pub const VIRTIO_ID_VSOCK: u32 = 19; - - /// Vsock packet operation IDs. - /// Defined in `/include/uapi/linux/virtio_vsock.h`. - /// - /// Connection request. - pub const VSOCK_OP_REQUEST: u16 = 1; - /// Connection response. - pub const VSOCK_OP_RESPONSE: u16 = 2; - /// Connection reset. - pub const VSOCK_OP_RST: u16 = 3; - /// Connection clean shutdown. - pub const VSOCK_OP_SHUTDOWN: u16 = 4; - /// Connection data (read/write). - pub const VSOCK_OP_RW: u16 = 5; - /// Flow control credit update. - pub const VSOCK_OP_CREDIT_UPDATE: u16 = 6; - /// Flow control credit update request. - pub const VSOCK_OP_CREDIT_REQUEST: u16 = 7; - - /// Vsock packet flags. - /// Defined in `/include/uapi/linux/virtio_vsock.h`. - /// - /// Valid with a VSOCK_OP_SHUTDOWN packet: the packet sender will receive no more data. - pub const VSOCK_FLAGS_SHUTDOWN_RCV: u32 = 1; - /// Valid with a VSOCK_OP_SHUTDOWN packet: the packet sender will send no more data. - pub const VSOCK_FLAGS_SHUTDOWN_SEND: u32 = 2; - - /// Vsock packet type. - /// Defined in `/include/uapi/linux/virtio_vsock.h`. - /// - /// Stream / connection-oriented packet (the only currently valid type). - pub const VSOCK_TYPE_STREAM: u16 = 1; - //pub const VSOCK_TYPE_SEQPACKET: u16 = 2; - pub const VSOCK_TYPE_DGRAM: u16 = 3; - - pub const VSOCK_HOST_CID: u64 = 2; - } -} - -#[derive(Debug)] -pub enum VsockError { - /// The vsock data/buffer virtio descriptor length is smaller than expected. - BufDescTooSmall, - /// The vsock data/buffer virtio descriptor is expected, but missing. - BufDescMissing, - /// Chained GuestMemoryMmap error. - GuestMemoryMmap(GuestMemoryError), - /// Bounds check failed on guest memory pointer. - GuestMemoryBounds, - /// The vsock header descriptor length is too small. - HdrDescTooSmall(u32), - /// The vsock header `len` field holds an invalid value. - InvalidPktLen(u32), - /// A data fetch was attempted when no data was available. - NoData, - /// A data buffer was expected for the provided packet, but it is missing. - PktBufMissing, - /// Encountered an unexpected write-only virtio descriptor. - UnreadableDescriptor, - /// Encountered an unexpected read-only virtio descriptor. - UnwritableDescriptor, - /// EventFd error - EventFd(std::io::Error), -} - -type Result = std::result::Result; diff --git a/vendor/krun-devices/src/virtio/vsock/muxer.rs b/vendor/krun-devices/src/virtio/vsock/muxer.rs deleted file mode 100644 index f4c10247e..000000000 --- a/vendor/krun-devices/src/virtio/vsock/muxer.rs +++ /dev/null @@ -1,713 +0,0 @@ -use std::collections::HashMap; -use std::os::unix::io::RawFd; -use std::path::PathBuf; -use std::sync::{Arc, Mutex, RwLock}; - -use super::super::Queue as VirtQueue; -use super::defs; -use super::defs::uapi; -use super::muxer_rxq::{rx_to_pkt, MuxerRxQ}; -use super::muxer_thread::MuxerThread; -use super::packet::{TsiConnectReq, TsiGetnameRsp, VsockPacket}; -use super::proxy::{Proxy, ProxyRemoval, ProxyUpdate}; -use super::reaper::ReaperThread; -#[cfg(target_os = "macos")] -use super::timesync::TimesyncThread; -use super::tsi_dgram::TsiDgramProxy; -use super::tsi_stream::TsiStreamProxy; -use super::unix::UnixProxy; -use super::TsiFlags; -use super::VsockError; -use crossbeam_channel::{unbounded, Sender}; -use utils::epoll::{ControlOperation, Epoll, EpollEvent, EventSet}; -use vm_memory::GuestMemoryMmap; - -use crate::virtio::InterruptTransport; -use std::net::{Ipv4Addr, SocketAddrV4}; - -pub type ProxyMap = Arc>>>>; - -/// A muxer RX queue item. -#[derive(Debug)] -pub enum MuxerRx { - Reset { - local_port: u32, - peer_port: u32, - }, - GetnameResponse { - local_port: u32, - peer_port: u32, - data: TsiGetnameRsp, - }, - ConnResponse { - local_port: u32, - peer_port: u32, - result: i32, - }, - OpRequest { - local_port: u32, - peer_port: u32, - }, - OpResponse { - local_port: u32, - peer_port: u32, - }, - CreditRequest { - local_port: u32, - peer_port: u32, - fwd_cnt: u32, - }, - CreditUpdate { - local_port: u32, - peer_port: u32, - fwd_cnt: u32, - }, - ListenResponse { - local_port: u32, - peer_port: u32, - result: i32, - }, - AcceptResponse { - local_port: u32, - peer_port: u32, - result: i32, - }, -} - -pub fn push_packet( - cid: u64, - rx: MuxerRx, - rxq_mutex: &Arc>, - queue_mutex: &Arc>, - mem: &GuestMemoryMmap, -) { - let mut queue = queue_mutex.lock().unwrap(); - if let Some(head) = queue.pop(mem) { - if let Ok(mut pkt) = VsockPacket::from_rx_virtq_head(&head) { - rx_to_pkt(cid, rx, &mut pkt); - if let Err(e) = queue.add_used(mem, head.index, pkt.hdr().len() as u32 + pkt.len()) { - error!("failed to add used elements to the queue: {e:?}"); - } - } - } else { - error!("couldn't push pkt to queue, adding it to rxq"); - drop(queue); - rxq_mutex.lock().unwrap().push(rx); - } -} - -pub struct VsockMuxer { - cid: u64, - host_port_map: Option>, - queue: Option>>, - mem: Option, - rxq: Arc>, - epoll: Epoll, - interrupt: Option, - proxy_map: ProxyMap, - reaper_sender: Option>, - unix_ipc_port_map: Option>, - tsi_flags: TsiFlags, -} - -impl VsockMuxer { - pub(crate) fn new( - cid: u64, - host_port_map: Option>, - unix_ipc_port_map: Option>, - tsi_flags: TsiFlags, - ) -> Self { - VsockMuxer { - cid, - host_port_map, - queue: None, - mem: None, - rxq: Arc::new(Mutex::new(MuxerRxQ::new())), - epoll: Epoll::new().unwrap(), - interrupt: None, - proxy_map: Arc::new(RwLock::new(HashMap::new())), - reaper_sender: None, - unix_ipc_port_map, - tsi_flags, - } - } - - pub(crate) fn activate( - &mut self, - mem: GuestMemoryMmap, - queue: Arc>, - interrupt: InterruptTransport, - ) { - self.queue = Some(queue.clone()); - self.mem = Some(mem.clone()); - self.interrupt = Some(interrupt.clone()); - - #[cfg(target_os = "macos")] - { - let timesync = - TimesyncThread::new(self.cid, mem.clone(), queue.clone(), interrupt.clone()); - timesync.run(); - } - - let (sender, receiver) = unbounded(); - - let thread = MuxerThread::new( - self.cid, - self.epoll.clone(), - self.rxq.clone(), - self.proxy_map.clone(), - mem, - queue, - interrupt.clone(), - sender.clone(), - self.unix_ipc_port_map.clone().unwrap_or_default(), - ); - thread.run(); - - self.reaper_sender = Some(sender); - let reaper = ReaperThread::new(receiver, self.proxy_map.clone()); - reaper.run(); - } - - pub(crate) fn has_pending_rx(&self) -> bool { - !self.rxq.lock().unwrap().is_empty() - } - - pub(crate) fn recv_pkt(&mut self, pkt: &mut VsockPacket) -> super::Result<()> { - debug!("recv_stream_pkt"); - if self.rxq.lock().unwrap().is_empty() { - return Err(VsockError::NoData); - } - - if let Some(rx) = self.rxq.lock().unwrap().pop() { - rx_to_pkt(self.cid, rx, pkt); - } - - Ok(()) - } - - fn push_packet(&self, rx: MuxerRx) { - let mem = match self.mem.as_ref() { - Some(m) => m, - None => { - error!("proxy creation without mem"); - return; - } - }; - let queue_mutex = match self.queue.as_ref() { - Some(q) => q, - None => { - error!("stream proxy creation without stream queue"); - return; - } - }; - - let mut queue = queue_mutex.lock().unwrap(); - if let Some(head) = queue.pop(mem) { - if let Ok(mut pkt) = VsockPacket::from_rx_virtq_head(&head) { - rx_to_pkt(self.cid, rx, &mut pkt); - if let Err(e) = queue.add_used(mem, head.index, pkt.hdr().len() as u32 + pkt.len()) - { - error!("failed to add used elements to the queue: {e:?}"); - } - } - } else { - error!("couldn't push pkt to queue, adding it to rxq"); - drop(queue); - self.rxq.lock().unwrap().push(rx); - } - } - - pub fn update_polling(&self, id: u64, fd: RawFd, evset: EventSet) { - debug!("update_polling id={id} fd={fd:?} evset={evset:?}"); - let _ = self - .epoll - .ctl(ControlOperation::Delete, fd, &EpollEvent::default()); - if !evset.is_empty() { - let _ = self - .epoll - .ctl(ControlOperation::Add, fd, &EpollEvent::new(evset, id)); - } - } - - fn process_proxy_update(&self, id: u64, update: ProxyUpdate) { - if let Some(polling) = update.polling { - self.update_polling(polling.0, polling.1, polling.2); - } - - match update.remove_proxy { - ProxyRemoval::Keep => {} - ProxyRemoval::Immediate => { - info!("immediately removing proxy: {id}"); - self.proxy_map.write().unwrap().remove(&id); - } - ProxyRemoval::Deferred => { - info!("deferring proxy removal: {id}"); - if let Some(reaper_sender) = &self.reaper_sender { - if reaper_sender.send(id).is_err() { - self.proxy_map.write().unwrap().remove(&id); - } - } - } - } - - if update.signal_queue { - if let Some(interrupt) = &self.interrupt { - interrupt.signal_used_queue(); - } - } - } - - fn process_proxy_create(&self, pkt: &VsockPacket) { - debug!("proxy create request"); - if let Some(req) = pkt.read_proxy_create() { - debug!( - "proxy create request: peer_port={}, type={}", - req.peer_port, req._type - ); - let mem = match self.mem.as_ref() { - Some(m) => m, - None => { - error!("proxy creation without mem"); - return; - } - }; - let queue = match self.queue.as_ref() { - Some(q) => q, - None => { - error!("stream proxy creation without stream queue"); - return; - } - }; - match req._type { - defs::SOCK_STREAM => { - debug!("proxy create stream"); - let id = ((req.peer_port as u64) << 32) | (defs::TSI_PROXY_PORT as u64); - if req.family as i32 == libc::AF_UNIX - && !self.tsi_flags.contains(TsiFlags::HIJACK_UNIX) - { - warn!("rejecting stream unix proxy because HIJACK_UNIX is disabled"); - return; - } - if (req.family as i32 == libc::AF_INET || req.family as i32 == libc::AF_INET6) - && !self.tsi_flags.contains(TsiFlags::HIJACK_INET) - { - warn!("rejecting stream inet proxy because HIJACK_INET is disabled"); - return; - } - match TsiStreamProxy::new( - id, - self.cid, - req.family, - defs::TSI_PROXY_PORT, - req.peer_port, - pkt.src_port(), - mem.clone(), - queue.clone(), - self.rxq.clone(), - ) { - Ok(proxy) => { - self.proxy_map - .write() - .unwrap() - .insert(id, Mutex::new(Box::new(proxy))); - } - Err(e) => debug!("error creating tcp proxy: {e}"), - } - } - defs::SOCK_DGRAM => { - debug!("proxy create dgram"); - let id = ((req.peer_port as u64) << 32) | (defs::TSI_PROXY_PORT as u64); - if req.family as i32 == libc::AF_UNIX - && !self.tsi_flags.contains(TsiFlags::HIJACK_UNIX) - { - warn!("rejecting dgram unix proxy because HIJACK_UNIX is disabled"); - return; - } - if (req.family as i32 == libc::AF_INET || req.family as i32 == libc::AF_INET6) - && !self.tsi_flags.contains(TsiFlags::HIJACK_INET) - { - warn!("rejecting dgram inet proxy because HIJACK_INET is disabled"); - return; - } - match TsiDgramProxy::new( - id, - self.cid, - req.family, - req.peer_port, - mem.clone(), - queue.clone(), - self.rxq.clone(), - ) { - Ok(proxy) => { - self.proxy_map - .write() - .unwrap() - .insert(id, Mutex::new(Box::new(proxy))); - } - Err(e) => debug!("error creating udp proxy: {e}"), - } - } - _ => debug!("unknown type on connection request"), - }; - } - } - - fn process_connect(&self, pkt: &VsockPacket) { - debug!("proxy connect request"); - if let Some(req) = pkt.read_connect_req() { - let id = ((req.peer_port as u64) << 32) | (defs::TSI_PROXY_PORT as u64); - debug!("proxy connect request: id={id}"); - match self.proxy_map.read().unwrap().get(&id) { - Some(proxy) => { - self.process_proxy_update(id, proxy.lock().unwrap().connect(pkt, req)); - } - None => self.push_packet(MuxerRx::ConnResponse { - local_port: pkt.dst_port(), - peer_port: pkt.src_port(), - result: -libc::ECONNREFUSED, - }), - } - } - } - - fn process_getname(&self, pkt: &VsockPacket) { - debug!("new getname request"); - if let Some(req) = pkt.read_getname_req() { - let id = ((req.peer_port as u64) << 32) | (req.local_port as u64); - debug!( - "new getname request: id={}, peer_port={}, local_port={}", - id, req.peer_port, req.local_port - ); - - match self.proxy_map.read().unwrap().get(&id) { - Some(proxy) => proxy.lock().unwrap().getpeername(pkt), - None => self.push_packet(MuxerRx::GetnameResponse { - local_port: pkt.dst_port(), - peer_port: pkt.src_port(), - data: TsiGetnameRsp { - result: -libc::EINVAL, - addr_len: 0, - addr: SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 0).into(), - }, - }), - } - } - } - - fn process_sendto_addr(&self, pkt: &VsockPacket) { - debug!("new DGRAM sendto addr: src={}", pkt.src_port()); - if let Some(req) = pkt.read_sendto_addr() { - let id = ((req.peer_port as u64) << 32) | (defs::TSI_PROXY_PORT as u64); - debug!("new DGRAM sendto addr: id={id}"); - let update = self - .proxy_map - .read() - .unwrap() - .get(&id) - .map(|proxy| proxy.lock().unwrap().sendto_addr(req)); - - if let Some(update) = update { - self.process_proxy_update(id, update); - } - } - } - - fn process_sendto_data(&self, pkt: &VsockPacket) { - let id = ((pkt.src_port() as u64) << 32) | (defs::TSI_PROXY_PORT as u64); - debug!("DGRAM sendto data: id={} src={}", id, pkt.src_port()); - if let Some(proxy) = self.proxy_map.read().unwrap().get(&id) { - proxy.lock().unwrap().sendto_data(pkt); - } - } - - fn process_listen_request(&self, pkt: &VsockPacket) { - debug!("DGRAM listen request: src={}", pkt.src_port()); - if let Some(req) = pkt.read_listen_req() { - let id = ((req.peer_port as u64) << 32) | (defs::TSI_PROXY_PORT as u64); - debug!("DGRAM listen request: id={id}"); - match self.proxy_map.read().unwrap().get(&id) { - Some(proxy) => self.process_proxy_update( - id, - proxy.lock().unwrap().listen(pkt, req, &self.host_port_map), - ), - None => self.push_packet(MuxerRx::ListenResponse { - local_port: pkt.dst_port(), - peer_port: pkt.src_port(), - result: -libc::EPERM, - }), - }; - } - } - - fn process_accept_request(&self, pkt: &VsockPacket) { - debug!("DGRAM accept request: src={}", pkt.src_port()); - if let Some(req) = pkt.read_accept_req() { - let id = ((req.peer_port as u64) << 32) | (defs::TSI_PROXY_PORT as u64); - debug!("DGRAM accept request: id={id}"); - match self.proxy_map.read().unwrap().get(&id) { - Some(proxy) => self.process_proxy_update(id, proxy.lock().unwrap().accept(req)), - None => self.push_packet(MuxerRx::AcceptResponse { - local_port: pkt.dst_port(), - peer_port: pkt.src_port(), - result: -libc::EINVAL, - }), - } - } - } - - fn process_proxy_release(&self, pkt: &VsockPacket) { - debug!("DGRAM release request: src={}", pkt.src_port()); - if let Some(req) = pkt.read_release_req() { - let id = ((req.peer_port as u64) << 32) | (req.local_port as u64); - debug!( - "DGRAM release request: id={} local_port={} peer_port={}", - id, req.local_port, req.peer_port - ); - let update = if let Some(proxy) = self.proxy_map.read().unwrap().get(&id) { - Some(proxy.lock().unwrap().release()) - } else { - debug!( - "release without proxy: id={}, proxies={}", - id, - self.proxy_map.read().unwrap().len() - ); - None - }; - - if let Some(update) = update { - self.process_proxy_update(id, update); - } - } - debug!( - "DGRAM release request: proxies={}", - self.proxy_map.read().unwrap().len() - ); - } - - fn process_dgram_rw(&self, pkt: &VsockPacket) { - debug!("DGRAM OP_RW"); - let id = ((pkt.src_port() as u64) << 32) | (defs::TSI_PROXY_PORT as u64); - - if let Some(proxy_lock) = self.proxy_map.read().unwrap().get(&id) { - debug!("DGRAM allowing OP_RW for {}", pkt.src_port()); - let mut proxy = proxy_lock.lock().unwrap(); - let update = proxy.sendmsg(pkt); - self.process_proxy_update(id, update); - } else { - debug!("DGRAM ignoring OP_RW for {}", pkt.src_port()); - } - } - - pub(crate) fn send_dgram_pkt(&mut self, pkt: &VsockPacket) -> super::Result<()> { - debug!( - "send_dgram_pkt: src_port={} dst_port={}", - pkt.src_port(), - pkt.dst_port() - ); - - if pkt.dst_cid() != uapi::VSOCK_HOST_CID { - debug!("dropping guest packet for unknown CID: {:?}", pkt.hdr()); - return Ok(()); - } - - match pkt.dst_port() { - defs::TSI_PROXY_CREATE if self.tsi_flags.tsi_enabled() => { - self.process_proxy_create(pkt) - } - defs::TSI_CONNECT if self.tsi_flags.tsi_enabled() => self.process_connect(pkt), - defs::TSI_GETNAME if self.tsi_flags.tsi_enabled() => self.process_getname(pkt), - defs::TSI_SENDTO_ADDR if self.tsi_flags.tsi_enabled() => self.process_sendto_addr(pkt), - defs::TSI_SENDTO_DATA if self.tsi_flags.tsi_enabled() => self.process_sendto_data(pkt), - defs::TSI_LISTEN if self.tsi_flags.tsi_enabled() => self.process_listen_request(pkt), - defs::TSI_ACCEPT if self.tsi_flags.tsi_enabled() => self.process_accept_request(pkt), - defs::TSI_PROXY_RELEASE if self.tsi_flags.tsi_enabled() => { - self.process_proxy_release(pkt) - } - _ => { - if pkt.op() == uapi::VSOCK_OP_RW { - self.process_dgram_rw(pkt); - } else { - error!("unexpected dgram pkt: {}", pkt.op()); - } - } - } - - Ok(()) - } - - fn process_op_request(&mut self, pkt: &VsockPacket) { - debug!("OP_REQUEST"); - let id: u64 = ((pkt.src_port() as u64) << 32) | (pkt.dst_port() as u64); - let mut proxy_map = self.proxy_map.write().unwrap(); - - if let Some(proxy) = proxy_map.get(&id) { - if let Some(update) = proxy.lock().unwrap().confirm_connect(pkt) { - self.process_proxy_update(id, update); - } - } else if let Some(ref mut ipc_map) = &mut self.unix_ipc_port_map { - if let Some((path, listen)) = ipc_map.get(&pkt.dst_port()) { - let mem = self.mem.as_ref().unwrap(); - let queue = self.queue.as_ref().unwrap(); - if *listen { - warn!("Attempting to connect a socket that is listening, sending rst"); - let rx = MuxerRx::Reset { - local_port: pkt.dst_port(), - peer_port: pkt.src_port(), - }; - push_packet(self.cid, rx, &self.rxq, queue, mem); - return; - } - let rxq = self.rxq.clone(); - - let mut unix = UnixProxy::new( - id, - self.cid, - pkt.dst_port(), - pkt.src_port(), - mem.clone(), - queue.clone(), - rxq, - path.to_path_buf(), - ) - .unwrap(); - let tsi = TsiConnectReq { - peer_port: 0, - addr: SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 0).into(), - }; - let update = unix.connect(pkt, tsi); - unix.confirm_connect(pkt); - proxy_map.insert(id, Mutex::new(Box::new(unix))); - self.process_proxy_update(id, update); - } - } - } - - fn process_op_response(&self, pkt: &VsockPacket) { - debug!("OP_RESPONSE"); - let id: u64 = ((pkt.src_port() as u64) << 32) | (pkt.dst_port() as u64); - let update = self - .proxy_map - .read() - .unwrap() - .get(&id) - .map(|proxy| proxy.lock().unwrap().process_op_response(pkt)); - update - .as_ref() - .and_then(|u| u.push_accept) - .and_then(|(_id, parent_id)| { - self.proxy_map - .read() - .unwrap() - .get(&parent_id) - .map(|proxy| proxy.lock().unwrap().enqueue_accept()) - }); - - if let Some(update) = update { - self.process_proxy_update(id, update); - } - } - - fn process_op_shutdown(&self, pkt: &VsockPacket) { - debug!("OP_SHUTDOWN"); - let id: u64 = ((pkt.src_port() as u64) << 32) | (pkt.dst_port() as u64); - if let Some(proxy) = self.proxy_map.read().unwrap().get(&id) { - proxy.lock().unwrap().shutdown(pkt); - } - } - - fn process_op_credit_update(&self, pkt: &VsockPacket) { - debug!("OP_CREDIT_UPDATE"); - let id: u64 = ((pkt.src_port() as u64) << 32) | (pkt.dst_port() as u64); - let update = self - .proxy_map - .read() - .unwrap() - .get(&id) - .map(|proxy| proxy.lock().unwrap().update_peer_credit(pkt)); - if let Some(update) = update { - self.process_proxy_update(id, update); - } - } - - fn process_stream_rw(&self, pkt: &VsockPacket) { - debug!("OP_RW"); - let id: u64 = ((pkt.src_port() as u64) << 32) | (pkt.dst_port() as u64); - if let Some(proxy_lock) = self.proxy_map.read().unwrap().get(&id) { - debug!( - "allowing OP_RW: src={} dst={}", - pkt.src_port(), - pkt.dst_port() - ); - let mut proxy = proxy_lock.lock().unwrap(); - let update = proxy.sendmsg(pkt); - self.process_proxy_update(id, update); - } else { - debug!("invalid OP_RW for {}, sending reset", pkt.src_port()); - let mem = match self.mem.as_ref() { - Some(m) => m, - None => { - warn!("OP_RW without mem"); - return; - } - }; - let queue = match self.queue.as_ref() { - Some(q) => q, - None => { - warn!("OP_RW without queue"); - return; - } - }; - - // This response goes to the connection. - let rx = MuxerRx::Reset { - local_port: pkt.dst_port(), - peer_port: pkt.src_port(), - }; - push_packet(self.cid, rx, &self.rxq, queue, mem); - } - } - - fn process_stream_rst(&self, pkt: &VsockPacket) { - debug!("OP_RST"); - let id: u64 = ((pkt.src_port() as u64) << 32) | (pkt.dst_port() as u64); - if let Some(proxy_lock) = self.proxy_map.read().unwrap().get(&id) { - debug!( - "allowing OP_RST: id={} src={} dst={}", - id, - pkt.src_port(), - pkt.dst_port() - ); - let mut proxy = proxy_lock.lock().unwrap(); - let update = proxy.release(); - self.process_proxy_update(id, update); - } else { - debug!("invalid OP_RST for {}", pkt.src_port()); - } - } - - pub(crate) fn send_stream_pkt(&mut self, pkt: &VsockPacket) -> super::Result<()> { - debug!( - "send_pkt: src_port={} dst_port={}, op={}", - pkt.src_port(), - pkt.dst_port(), - pkt.op() - ); - - if pkt.dst_cid() != uapi::VSOCK_HOST_CID { - debug!("dropping guest packet for unknown CID: {:?}", pkt.hdr()); - return Ok(()); - } - - match pkt.op() { - uapi::VSOCK_OP_REQUEST => self.process_op_request(pkt), - uapi::VSOCK_OP_RESPONSE => self.process_op_response(pkt), - uapi::VSOCK_OP_SHUTDOWN => self.process_op_shutdown(pkt), - uapi::VSOCK_OP_CREDIT_UPDATE => self.process_op_credit_update(pkt), - uapi::VSOCK_OP_RW => self.process_stream_rw(pkt), - uapi::VSOCK_OP_RST => self.process_stream_rst(pkt), - _ => warn!("stream: unhandled op={}", pkt.op()), - } - Ok(()) - } -} diff --git a/vendor/krun-devices/src/virtio/vsock/muxer_rxq.rs b/vendor/krun-devices/src/virtio/vsock/muxer_rxq.rs deleted file mode 100644 index ef1f7d75f..000000000 --- a/vendor/krun-devices/src/virtio/vsock/muxer_rxq.rs +++ /dev/null @@ -1,227 +0,0 @@ -// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -// - -/// `MuxerRxQ` implements a helper object that `VsockMuxer` can use for queuing RX (host -> guest) -/// packets (or rather instructions on how to build said packets). -/// -/// Under ideal operation, every connection, that has pending RX data, will be present in the muxer -/// RX queue. However, since the RX queue is smaller than the connection pool, it may, under some -/// conditions, become full, meaning that it can no longer account for all the connections that can -/// yield RX data. When that happens, we say that it is no longer "synchronized" (i.e. with the -/// connection pool). A desynchronized RX queue still holds valid data, and the muxer will -/// continue to pop packets from it. However, when a desynchronized queue is drained, additional -/// data may still be available, so the muxer will have to perform a more costly walk of the entire -/// connection pool to find it. This walk is performed here, as part of building an RX queue from -/// the connection pool. When an out-of-sync is drained, the muxer will discard it, and attempt to -/// rebuild a synced one. -use std::collections::VecDeque; - -use super::defs; -use super::defs::uapi; -use super::muxer::MuxerRx; -use super::packet::{TsiAcceptRsp, TsiConnectRsp, TsiListenRsp, VsockPacket}; - -/// The muxer RX queue. -pub struct MuxerRxQ { - /// The RX queue data. - q: VecDeque, - /// The RX queue sync status. - synced: bool, -} - -impl MuxerRxQ { - const SIZE: usize = defs::MUXER_RXQ_SIZE; - - /// Trivial RX queue constructor. - pub fn new() -> Self { - Self { - q: VecDeque::with_capacity(Self::SIZE), - synced: true, - } - } - - /// Push a new RX item to the queue. - /// - /// A push will fail when: - /// - trying to push a connection key onto an out-of-sync, or full queue; or - /// - trying to push an RST onto a queue already full of RSTs. - /// - /// RSTs take precedence over connections, because connections can always be queried for - /// pending RX data later. Aside from this queue, there is no other storage for RSTs, so - /// failing to push one means that we have to drop the packet. - /// - /// Returns: - /// - `true` if the new item has been successfully queued; or - /// - `false` if there was no room left in the queue. - pub fn push(&mut self, rx: MuxerRx) -> bool { - // Pushing to a non-full, synchronized queue will always succeed. - if self.is_synced() && !self.is_full() { - self.q.push_back(rx); - return true; - } - - false - } - - /// Pop an RX item from the front of the queue. - pub fn pop(&mut self) -> Option { - self.q.pop_front() - } - - /// Check if the RX queue is synchronized with the connection pool. - pub fn is_synced(&self) -> bool { - self.synced - } - - /// Get the total number of items in the queue. - pub fn len(&self) -> usize { - self.q.len() - } - - /// Check if the queue is empty. - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - /// Check if the queue is full. - pub fn is_full(&self) -> bool { - self.len() == Self::SIZE - } -} - -pub fn rx_to_pkt(cid: u64, rx: MuxerRx, pkt: &mut VsockPacket) { - match rx { - MuxerRx::Reset { - local_port, - peer_port, - } => { - pkt.set_op(uapi::VSOCK_OP_RST) - .set_src_cid(uapi::VSOCK_HOST_CID) - .set_dst_cid(cid) - .set_src_port(local_port) - .set_dst_port(peer_port) - .set_len(0) - .set_type(uapi::VSOCK_TYPE_STREAM) - .set_flags(0) - .set_buf_alloc(0) - .set_fwd_cnt(0); - } - MuxerRx::ConnResponse { - local_port, - peer_port, - result, - } => { - pkt.set_op(uapi::VSOCK_OP_RW) - .set_src_cid(uapi::VSOCK_HOST_CID) - .set_dst_cid(cid) - .set_src_port(local_port) - .set_dst_port(peer_port) - .set_type(uapi::VSOCK_TYPE_DGRAM); - - pkt.write_connect_rsp(TsiConnectRsp { result }); - pkt.set_len(pkt.buf().unwrap().len() as u32); - } - MuxerRx::OpRequest { - local_port, - peer_port, - } => { - pkt.set_op(uapi::VSOCK_OP_REQUEST) - .set_src_cid(uapi::VSOCK_HOST_CID) - .set_dst_cid(cid) - .set_src_port(local_port) - .set_dst_port(peer_port) - .set_type(uapi::VSOCK_TYPE_STREAM) - .set_buf_alloc(defs::CONN_TX_BUF_SIZE as u32); - - pkt.set_len(0); - } - MuxerRx::OpResponse { - local_port, - peer_port, - } => { - pkt.set_op(uapi::VSOCK_OP_RESPONSE) - .set_src_cid(uapi::VSOCK_HOST_CID) - .set_dst_cid(cid) - .set_src_port(local_port) - .set_dst_port(peer_port) - .set_type(uapi::VSOCK_TYPE_STREAM) - .set_buf_alloc(defs::CONN_TX_BUF_SIZE as u32); - - pkt.set_len(0); - } - MuxerRx::GetnameResponse { - local_port, - peer_port, - data, - } => { - pkt.set_op(uapi::VSOCK_OP_RW) - .set_src_cid(uapi::VSOCK_HOST_CID) - .set_dst_cid(cid) - .set_src_port(local_port) - .set_dst_port(peer_port) - .set_type(uapi::VSOCK_TYPE_DGRAM); - - pkt.write_getname_rsp(data); - pkt.set_len(pkt.buf().unwrap().len() as u32); - } - MuxerRx::CreditRequest { - local_port, - peer_port, - fwd_cnt, - } => { - pkt.set_op(uapi::VSOCK_OP_CREDIT_REQUEST) - .set_src_cid(uapi::VSOCK_HOST_CID) - .set_dst_cid(cid) - .set_src_port(local_port) - .set_dst_port(peer_port) - .set_type(uapi::VSOCK_TYPE_STREAM) - .set_buf_alloc(defs::CONN_TX_BUF_SIZE as u32) - .set_fwd_cnt(fwd_cnt); - } - MuxerRx::CreditUpdate { - local_port, - peer_port, - fwd_cnt, - } => { - pkt.set_op(uapi::VSOCK_OP_CREDIT_UPDATE) - .set_src_cid(uapi::VSOCK_HOST_CID) - .set_dst_cid(cid) - .set_src_port(local_port) - .set_dst_port(peer_port) - .set_type(uapi::VSOCK_TYPE_STREAM) - .set_buf_alloc(defs::CONN_TX_BUF_SIZE as u32) - .set_fwd_cnt(fwd_cnt); - } - MuxerRx::ListenResponse { - local_port, - peer_port, - result, - } => { - pkt.set_op(uapi::VSOCK_OP_RW) - .set_src_cid(uapi::VSOCK_HOST_CID) - .set_dst_cid(cid) - .set_src_port(local_port) - .set_dst_port(peer_port) - .set_type(uapi::VSOCK_TYPE_DGRAM); - - pkt.write_listen_rsp(TsiListenRsp { result }); - pkt.set_len(pkt.buf().unwrap().len() as u32); - } - MuxerRx::AcceptResponse { - local_port, - peer_port, - result, - } => { - pkt.set_op(uapi::VSOCK_OP_RW) - .set_src_cid(uapi::VSOCK_HOST_CID) - .set_dst_cid(cid) - .set_src_port(local_port) - .set_dst_port(peer_port) - .set_type(uapi::VSOCK_TYPE_DGRAM); - - pkt.write_accept_rsp(TsiAcceptRsp { result }); - pkt.set_len(pkt.buf().unwrap().len() as u32); - } - } -} diff --git a/vendor/krun-devices/src/virtio/vsock/muxer_thread.rs b/vendor/krun-devices/src/virtio/vsock/muxer_thread.rs deleted file mode 100644 index 1a215887e..000000000 --- a/vendor/krun-devices/src/virtio/vsock/muxer_thread.rs +++ /dev/null @@ -1,206 +0,0 @@ -use std::collections::HashMap; -use std::os::unix::io::RawFd; -use std::path::PathBuf; -use std::sync::{Arc, Mutex}; -use std::thread; - -use super::super::Queue as VirtQueue; -use super::muxer::{push_packet, MuxerRx, ProxyMap}; -use super::muxer_rxq::MuxerRxQ; -use super::proxy::{NewProxyType, Proxy, ProxyRemoval, ProxyUpdate}; -use super::tsi_stream::TsiStreamProxy; - -use crate::virtio::vsock::defs; -use crate::virtio::vsock::unix::{UnixAcceptorProxy, UnixProxy}; -use crate::virtio::InterruptTransport; -use crossbeam_channel::Sender; -use rand::{rng, rngs::ThreadRng, Rng}; -use utils::epoll::{ControlOperation, Epoll, EpollEvent, EventSet}; -use vm_memory::GuestMemoryMmap; - -pub struct MuxerThread { - cid: u64, - pub epoll: Epoll, - rxq: Arc>, - proxy_map: ProxyMap, - mem: GuestMemoryMmap, - queue: Arc>, - interrupt: InterruptTransport, - reaper_sender: Sender, - unix_ipc_port_map: HashMap, -} - -impl MuxerThread { - #[allow(clippy::too_many_arguments)] - pub fn new( - cid: u64, - epoll: Epoll, - rxq: Arc>, - proxy_map: ProxyMap, - mem: GuestMemoryMmap, - queue: Arc>, - interrupt: InterruptTransport, - reaper_sender: Sender, - unix_ipc_port_map: HashMap, - ) -> Self { - MuxerThread { - cid, - epoll, - rxq, - proxy_map, - mem, - queue, - interrupt, - reaper_sender, - unix_ipc_port_map, - } - } - - pub fn run(self) { - thread::Builder::new() - .name("vsock muxer".into()) - .spawn(|| self.work()) - .unwrap(); - } - - fn send_credit_request(&self, credit_rx: MuxerRx) { - debug!("send_credit_request"); - push_packet(self.cid, credit_rx, &self.rxq, &self.queue, &self.mem); - } - - pub fn update_polling(&self, id: u64, fd: RawFd, evset: EventSet) { - debug!("update_polling id={id} fd={fd:?} evset={evset:?}"); - let _ = self - .epoll - .ctl(ControlOperation::Delete, fd, &EpollEvent::default()); - if !evset.is_empty() { - let _ = self - .epoll - .ctl(ControlOperation::Add, fd, &EpollEvent::new(evset, id)); - } - } - - fn process_proxy_update(&self, id: u64, update: ProxyUpdate, thread_rng: &mut ThreadRng) { - if let Some(polling) = update.polling { - self.update_polling(polling.0, polling.1, polling.2); - } - - if let Some(credit_rx) = update.push_credit_req { - debug!("send_credit_request"); - self.send_credit_request(credit_rx); - } - - match update.remove_proxy { - ProxyRemoval::Keep => {} - ProxyRemoval::Immediate => { - warn!("immediately removing proxy: {id}"); - self.proxy_map.write().unwrap().remove(&id); - } - ProxyRemoval::Deferred => { - warn!("deferring proxy removal: {id}"); - if self.reaper_sender.send(id).is_err() { - self.proxy_map.write().unwrap().remove(&id); - } - } - } - - let mut should_signal = update.signal_queue; - - if let Some((peer_port, accept_fd, family, proxy_type)) = update.new_proxy { - let local_port: u32 = thread_rng.random_range(1024..u32::MAX); - let new_id: u64 = ((peer_port as u64) << 32) | (local_port as u64); - let new_proxy: Box = match proxy_type { - NewProxyType::Tcp => Box::new(TsiStreamProxy::new_reverse( - new_id, - self.cid, - id, - family, - local_port, - peer_port, - accept_fd, - self.mem.clone(), - self.queue.clone(), - self.rxq.clone(), - )), - NewProxyType::Unix => Box::new(UnixProxy::new_reverse( - new_id, - self.cid, - local_port, - peer_port, - accept_fd, - self.mem.clone(), - self.queue.clone(), - self.rxq.clone(), - )), - }; - self.proxy_map - .write() - .unwrap() - .insert(new_id, Mutex::new(new_proxy)); - if let Some(proxy) = self.proxy_map.read().unwrap().get(&new_id) { - proxy.lock().unwrap().push_op_request(); - }; - should_signal = true; - } - - if should_signal { - debug!("signal IRQ"); - self.interrupt.signal_used_queue(); - } - } - - fn create_lisening_ipc_sockets(&self) { - for (port, (path, do_listen)) in &self.unix_ipc_port_map { - if !do_listen { - continue; - } - let id = ((*port as u64) << 32) | (defs::TSI_PROXY_PORT as u64); - let proxy = match UnixAcceptorProxy::new(id, path, *port) { - Ok(proxy) => proxy, - Err(e) => { - warn!("Failed to create listening proxy at {path:?}: {e:?}"); - continue; - } - }; - self.proxy_map - .write() - .unwrap() - .insert(id, Mutex::new(Box::new(proxy))); - if let Some(proxy) = self.proxy_map.read().unwrap().get(&id) { - self.update_polling(id, proxy.lock().unwrap().as_raw_fd(), EventSet::IN); - }; - } - } - - fn work(self) { - let mut thread_rng = rng(); - self.create_lisening_ipc_sockets(); - loop { - let mut epoll_events = vec![EpollEvent::new(EventSet::empty(), 0); 32]; - match self - .epoll - .wait(epoll_events.len(), -1, epoll_events.as_mut_slice()) - { - Ok(ev_cnt) => { - for ev in &epoll_events[0..ev_cnt] { - debug!("Event: ev.data={} ev.fd={}", ev.data(), ev.fd()); - let evset = EventSet::from_bits(ev.events).unwrap(); - let id = ev.data(); - - let update = self.proxy_map.read().unwrap().get(&id).map(|proxy_lock| { - let mut proxy = proxy_lock.lock().unwrap(); - proxy.process_event(evset) - }); - - if let Some(update) = update { - self.process_proxy_update(id, update, &mut thread_rng); - } - } - } - Err(e) => { - debug!("failed to consume muxer epoll event: {e}"); - } - } - } - } -} diff --git a/vendor/krun-devices/src/virtio/vsock/packet.rs b/vendor/krun-devices/src/virtio/vsock/packet.rs deleted file mode 100644 index 51b3cf1b2..000000000 --- a/vendor/krun-devices/src/virtio/vsock/packet.rs +++ /dev/null @@ -1,787 +0,0 @@ -// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -// - -/// `VsockPacket` provides a thin wrapper over the buffers exchanged via virtio queues. -/// There are two components to a vsock packet, each using its own descriptor in a -/// virtio queue: -/// - the packet header; and -/// - the packet data/buffer. -/// -/// There is a 1:1 relation between descriptor chains and packets: the first (chain head) holds -/// the header, and an optional second descriptor holds the data. The second descriptor is only -/// present for data packets (VSOCK_OP_RW). -/// -/// `VsockPacket` wraps these two buffers and provides direct access to the data stored -/// in guest memory. This is done to avoid unnecessarily copying data from guest memory -/// to temporary buffers, before passing it on to the vsock backend. -use std::convert::TryInto; -use std::ffi::CStr; -use std::net::{Ipv4Addr, SocketAddrV4}; -#[cfg(target_os = "macos")] -use std::net::{Ipv6Addr, SocketAddrV6}; -use std::os::raw::c_char; -use std::result; - -#[cfg(target_os = "linux")] -use nix::sys::socket::{sockaddr, AddressFamily}; -use nix::sys::socket::{SockaddrLike, SockaddrStorage}; -use utils::byte_order; -use vm_memory::{self, Address, GuestAddress, GuestMemory, GuestMemoryError}; - -use super::super::DescriptorChain; -use super::defs; -use super::{Result, VsockError}; - -// The vsock packet header is defined by the C struct: -// -// ```C -// struct virtio_vsock_hdr { -// le64 src_cid; -// le64 dst_cid; -// le32 src_port; -// le32 dst_port; -// le32 len; -// le16 type; -// le16 op; -// le32 flags; -// le32 buf_alloc; -// le32 fwd_cnt; -// }; -// ``` -// -// This structed will occupy the buffer pointed to by the head descriptor. We'll be accessing it -// as a byte slice. To that end, we define below the offsets for each field struct, as well as the -// packed struct size, as a bunch of `usize` consts. -// Note that these offsets are only used privately by the `VsockPacket` struct, the public interface -// consisting of getter and setter methods, for each struct field, that will also handle the correct -// endianess. - -/// The vsock packet header struct size (when packed). -pub const VSOCK_PKT_HDR_SIZE: usize = 44; - -// Source CID. -const HDROFF_SRC_CID: usize = 0; - -// Destination CID. -const HDROFF_DST_CID: usize = 8; - -// Source port. -const HDROFF_SRC_PORT: usize = 16; - -// Destination port. -const HDROFF_DST_PORT: usize = 20; - -// Data length (in bytes) - may be 0, if there is no data buffer. -const HDROFF_LEN: usize = 24; - -// Socket type. Currently, only connection-oriented streams are defined by the vsock protocol. -const HDROFF_TYPE: usize = 28; - -// Operation ID - one of the VSOCK_OP_* values; e.g. -// - VSOCK_OP_RW: a data packet; -// - VSOCK_OP_REQUEST: connection request; -// - VSOCK_OP_RST: forcefull connection termination; -// etc (see `super::defs::uapi` for the full list). -const HDROFF_OP: usize = 30; - -// Additional options (flags) associated with the current operation (`op`). -// Currently, only used with shutdown requests (VSOCK_OP_SHUTDOWN). -const HDROFF_FLAGS: usize = 32; - -// Size (in bytes) of the packet sender receive buffer (for the connection to which this packet -// belongs). -const HDROFF_BUF_ALLOC: usize = 36; - -// Number of bytes the sender has received and consumed (for the connection to which this packet -// belongs). For instance, for our Unix backend, this counter would be the total number of bytes -// we have successfully written to a backing Unix socket. -const HDROFF_FWD_CNT: usize = 40; - -#[repr(C)] -pub struct TsiProxyCreate { - pub peer_port: u32, - pub family: u16, - pub _type: u16, -} - -#[repr(C)] -pub struct TsiConnectReq { - pub peer_port: u32, - pub addr: SockaddrStorage, -} - -#[repr(C)] -pub struct TsiConnectRsp { - pub result: i32, -} - -#[repr(C)] -pub struct TsiGetnameReq { - pub peer_port: u32, - pub local_port: u32, - pub peer: u32, -} - -#[repr(C)] -#[derive(Debug)] -pub struct TsiGetnameRsp { - pub result: i32, - pub addr_len: u32, - pub addr: SockaddrStorage, -} - -impl Default for TsiGetnameRsp { - fn default() -> Self { - let addr: SockaddrStorage = SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 0).into(); - TsiGetnameRsp { - result: -1, - // It's fine to unwrap here sice we've just created the SocketAddrV4 above. - addr_len: addr.as_sockaddr_in().unwrap().len(), - addr, - } - } -} - -#[repr(C)] -#[derive(Debug)] -pub struct TsiSendtoAddr { - pub peer_port: u32, - pub addr: SockaddrStorage, -} - -#[repr(C)] -#[derive(Debug)] -pub struct TsiListenReq { - pub peer_port: u32, - pub vm_port: u32, - pub backlog: i32, - pub addr: SockaddrStorage, -} - -#[repr(C)] -#[derive(Debug)] -pub struct TsiListenRsp { - pub result: i32, -} - -#[repr(C)] -#[derive(Debug)] -pub struct TsiAcceptReq { - pub peer_port: u32, - pub flags: u32, -} - -#[repr(C)] -#[derive(Debug)] -pub struct TsiAcceptRsp { - pub result: i32, -} - -#[repr(C)] -pub struct TsiReleaseReq { - pub peer_port: u32, - pub local_port: u32, -} - -/// The vsock packet, implemented as a wrapper over a virtq descriptor chain: -/// - the chain head, holding the packet header; and -/// - (an optional) data/buffer descriptor, only present for data packets (VSOCK_OP_RW). -pub struct VsockPacket { - hdr: *mut u8, - buf: Option<*mut u8>, - buf_size: usize, - owned_buf: Option>, -} - -fn get_host_address( - mem: &T, - guest_addr: GuestAddress, - size: usize, -) -> result::Result<*mut u8, GuestMemoryError> { - Ok(mem.get_slice(guest_addr, size)?.ptr_guard_mut().as_ptr()) -} - -impl VsockPacket { - /// Create the packet wrapper from a TX virtq chain head. - /// - /// The chain head is expected to hold valid packet header data. A following packet buffer - /// descriptor can optionally end the chain. Bounds and pointer checks are performed when - /// creating the wrapper. - pub fn from_tx_virtq_head(head: &DescriptorChain) -> Result { - // All buffers in the TX queue must be readable. - // - if head.is_write_only() { - return Err(VsockError::UnreadableDescriptor); - } - - // The packet header should fit inside the head descriptor. - if head.len < VSOCK_PKT_HDR_SIZE as u32 { - return Err(VsockError::HdrDescTooSmall(head.len)); - } - - let mut pkt = Self { - hdr: get_host_address(head.mem, head.addr, VSOCK_PKT_HDR_SIZE) - .map_err(VsockError::GuestMemoryMmap)?, - buf: None, - buf_size: 0, - owned_buf: None, - }; - let pkt_len = pkt.len(); - - // No point looking for a data/buffer descriptor, if the packet is zero-lengthed. - if pkt_len == 0 { - return Ok(pkt); - } - - // Reject weirdly-sized packets. - // - if pkt_len > defs::MAX_PKT_BUF_SIZE as u32 { - return Err(VsockError::InvalidPktLen(pkt_len)); - } - - let head_data_size = head.len as usize - VSOCK_PKT_HDR_SIZE; - - // Single combined descriptor: header + data with no next descriptor. - if !head.has_next() { - if head_data_size == 0 { - return Err(VsockError::BufDescMissing); - } - let buf_addr = head - .addr - .checked_add(VSOCK_PKT_HDR_SIZE as u64) - .ok_or(VsockError::GuestMemoryBounds)?; - pkt.buf_size = head_data_size; - pkt.buf = Some( - get_host_address(head.mem, buf_addr, pkt.buf_size) - .map_err(VsockError::GuestMemoryMmap)?, - ); - if pkt.buf_size < pkt_len as usize { - return Err(VsockError::BufDescTooSmall); - } - return Ok(pkt); - } - - let buf_desc = head.next_descriptor().ok_or(VsockError::BufDescMissing)?; - if buf_desc.is_write_only() { - return Err(VsockError::UnreadableDescriptor); - } - - // Classic two-descriptor case: header in first, all data in second. Zero-copy. - if head_data_size == 0 && !buf_desc.has_next() { - if buf_desc.len < pkt_len { - return Err(VsockError::BufDescTooSmall); - } - pkt.buf_size = buf_desc.len as usize; - pkt.buf = Some( - get_host_address(buf_desc.mem, buf_desc.addr, pkt.buf_size) - .map_err(VsockError::GuestMemoryMmap)?, - ); - return Ok(pkt); - } - - // Multiple data regions: inline data after header and/or multiple data descriptors. - // Copy into a contiguous owned buffer. - let mut owned_buf: Vec = Vec::with_capacity(pkt_len as usize); - - if head_data_size > 0 { - let buf_addr = head - .addr - .checked_add(VSOCK_PKT_HDR_SIZE as u64) - .ok_or(VsockError::GuestMemoryBounds)?; - let src = get_host_address(head.mem, buf_addr, head_data_size) - .map_err(VsockError::GuestMemoryMmap)?; - owned_buf.extend_from_slice(unsafe { - std::slice::from_raw_parts(src as *const u8, head_data_size) - }); - } - - // First data descriptor (already validated as readable above). - if buf_desc.len > 0 { - let src = get_host_address(buf_desc.mem, buf_desc.addr, buf_desc.len as usize) - .map_err(VsockError::GuestMemoryMmap)?; - owned_buf.extend_from_slice(unsafe { - std::slice::from_raw_parts(src as *const u8, buf_desc.len as usize) - }); - } - - let mut next = buf_desc.next_descriptor(); - while let Some(desc) = next { - if desc.is_write_only() { - return Err(VsockError::UnreadableDescriptor); - } - if desc.len > 0 { - let src = get_host_address(desc.mem, desc.addr, desc.len as usize) - .map_err(VsockError::GuestMemoryMmap)?; - owned_buf.extend_from_slice(unsafe { - std::slice::from_raw_parts(src as *const u8, desc.len as usize) - }); - } - next = desc.next_descriptor(); - } - - if owned_buf.len() < (pkt_len as usize) { - return Err(VsockError::BufDescTooSmall); - } - - pkt.buf_size = owned_buf.len(); - pkt.owned_buf = Some(owned_buf); - - Ok(pkt) - } - - /// Create the packet wrapper from an RX virtq chain head. - /// - /// There must be two descriptors in the chain, both writable: a header descriptor and a data - /// descriptor. Bounds and pointer checks are performed when creating the wrapper. - pub fn from_rx_virtq_head(head: &DescriptorChain) -> Result { - // All RX buffers must be writable. - // - if !head.is_write_only() { - return Err(VsockError::UnwritableDescriptor); - } - - // The packet header should fit inside the head descriptor. - if head.len < VSOCK_PKT_HDR_SIZE as u32 { - return Err(VsockError::HdrDescTooSmall(head.len)); - } - - let mut pkt = Self { - hdr: get_host_address(head.mem, head.addr, VSOCK_PKT_HDR_SIZE) - .map_err(VsockError::GuestMemoryMmap)?, - buf: None, - buf_size: 0, - owned_buf: None, - }; - - // Starting from Linux 6.2 the virtio-vsock driver can use a single descriptor for both - // header and data. - if !head.has_next() && head.len > VSOCK_PKT_HDR_SIZE as u32 { - let buf_addr = head - .addr - .checked_add(VSOCK_PKT_HDR_SIZE as u64) - .ok_or(VsockError::GuestMemoryBounds)?; - - pkt.buf_size = head.len as usize - VSOCK_PKT_HDR_SIZE; - pkt.buf = Some( - get_host_address(head.mem, buf_addr, pkt.buf_size) - .map_err(VsockError::GuestMemoryMmap)?, - ); - } else { - let buf_desc = head.next_descriptor().ok_or(VsockError::BufDescMissing)?; - - pkt.buf_size = buf_desc.len as usize; - pkt.buf = Some( - get_host_address(buf_desc.mem, buf_desc.addr, pkt.buf_size) - .map_err(VsockError::GuestMemoryMmap)?, - ); - } - - Ok(pkt) - } - - /// Provides in-place, byte-slice, access to the vsock packet header. - pub fn hdr(&self) -> &[u8] { - // This is safe since bound checks have already been performed when creating the packet - // from the virtq descriptor. - unsafe { std::slice::from_raw_parts(self.hdr as *const u8, VSOCK_PKT_HDR_SIZE) } - } - - /// Provides in-place, byte-slice, mutable access to the vsock packet header. - pub fn hdr_mut(&mut self) -> &mut [u8] { - // This is safe since bound checks have already been performed when creating the packet - // from the virtq descriptor. - unsafe { std::slice::from_raw_parts_mut(self.hdr, VSOCK_PKT_HDR_SIZE) } - } - - /// Provides in-place, byte-slice access to the vsock packet data buffer. - /// - /// Note: control packets (e.g. connection request or reset) have no data buffer associated. - /// For those packets, this method will return `None`. - /// Also note: calling `len()` on the returned slice will yield the buffer size, which may be - /// (and often is) larger than the length of the packet data. The packet data length - /// is stored in the packet header, and accessible via `VsockPacket::len()`. - pub fn buf(&self) -> Option<&[u8]> { - if let Some(ref owned) = self.owned_buf { - Some(owned.as_slice()) - } else { - self.buf.map(|ptr| { - // This is safe since bound checks have already been performed when creating the - // packet from the virtq descriptor. - unsafe { std::slice::from_raw_parts(ptr as *const u8, self.buf_size) } - }) - } - } - - /// Provides in-place, byte-slice, mutable access to the vsock packet data buffer. - /// - /// Note: control packets (e.g. connection request or reset) have no data buffer associated. - /// For those packets, this method will return `None`. - /// Also note: calling `len()` on the returned slice will yield the buffer size, which may be - /// (and often is) larger than the length of the packet data. The packet data length - /// is stored in the packet header, and accessible via `VsockPacket::len()`. - pub fn buf_mut(&mut self) -> Option<&mut [u8]> { - if let Some(ref mut owned) = self.owned_buf { - Some(owned.as_mut_slice()) - } else { - self.buf.map(|ptr| { - // This is safe since bound checks have already been performed when creating the - // packet from the virtq descriptor. - unsafe { std::slice::from_raw_parts_mut(ptr, self.buf_size) } - }) - } - } - - pub fn src_cid(&self) -> u64 { - byte_order::read_le_u64(&self.hdr()[HDROFF_SRC_CID..]) - } - - pub fn set_src_cid(&mut self, cid: u64) -> &mut Self { - byte_order::write_le_u64(&mut self.hdr_mut()[HDROFF_SRC_CID..], cid); - self - } - - pub fn dst_cid(&self) -> u64 { - byte_order::read_le_u64(&self.hdr()[HDROFF_DST_CID..]) - } - - pub fn set_dst_cid(&mut self, cid: u64) -> &mut Self { - byte_order::write_le_u64(&mut self.hdr_mut()[HDROFF_DST_CID..], cid); - self - } - - pub fn src_port(&self) -> u32 { - byte_order::read_le_u32(&self.hdr()[HDROFF_SRC_PORT..]) - } - - pub fn set_src_port(&mut self, port: u32) -> &mut Self { - byte_order::write_le_u32(&mut self.hdr_mut()[HDROFF_SRC_PORT..], port); - self - } - - pub fn dst_port(&self) -> u32 { - byte_order::read_le_u32(&self.hdr()[HDROFF_DST_PORT..]) - } - - pub fn set_dst_port(&mut self, port: u32) -> &mut Self { - byte_order::write_le_u32(&mut self.hdr_mut()[HDROFF_DST_PORT..], port); - self - } - - pub fn len(&self) -> u32 { - byte_order::read_le_u32(&self.hdr()[HDROFF_LEN..]) - } - - pub fn set_len(&mut self, len: u32) -> &mut Self { - byte_order::write_le_u32(&mut self.hdr_mut()[HDROFF_LEN..], len); - self - } - - pub fn type_(&self) -> u16 { - byte_order::read_le_u16(&self.hdr()[HDROFF_TYPE..]) - } - - pub fn set_type(&mut self, type_: u16) -> &mut Self { - byte_order::write_le_u16(&mut self.hdr_mut()[HDROFF_TYPE..], type_); - self - } - - pub fn op(&self) -> u16 { - byte_order::read_le_u16(&self.hdr()[HDROFF_OP..]) - } - - pub fn set_op(&mut self, op: u16) -> &mut Self { - byte_order::write_le_u16(&mut self.hdr_mut()[HDROFF_OP..], op); - self - } - - pub fn flags(&self) -> u32 { - byte_order::read_le_u32(&self.hdr()[HDROFF_FLAGS..]) - } - - pub fn set_flags(&mut self, flags: u32) -> &mut Self { - byte_order::write_le_u32(&mut self.hdr_mut()[HDROFF_FLAGS..], flags); - self - } - - pub fn set_flag(&mut self, flag: u32) -> &mut Self { - self.set_flags(self.flags() | flag); - self - } - - pub fn buf_alloc(&self) -> u32 { - byte_order::read_le_u32(&self.hdr()[HDROFF_BUF_ALLOC..]) - } - - pub fn set_buf_alloc(&mut self, buf_alloc: u32) -> &mut Self { - byte_order::write_le_u32(&mut self.hdr_mut()[HDROFF_BUF_ALLOC..], buf_alloc); - self - } - - pub fn fwd_cnt(&self) -> u32 { - byte_order::read_le_u32(&self.hdr()[HDROFF_FWD_CNT..]) - } - - pub fn set_fwd_cnt(&mut self, fwd_cnt: u32) -> &mut Self { - byte_order::write_le_u32(&mut self.hdr_mut()[HDROFF_FWD_CNT..], fwd_cnt); - self - } - - pub fn sa_family(&self) -> Option { - if self.buf_size >= 2 { - Some(byte_order::read_le_u16(&self.buf().unwrap()[0..])) - } else { - None - } - } - - pub fn inet_port(&self) -> Option { - if self.buf_size >= 4 { - Some(byte_order::read_be_u16(&self.buf().unwrap()[2..])) - } else { - None - } - } - - pub fn inet_addr(&self) -> Option<[u8; 4]> { - if self.buf_size >= 8 { - let ptr = &self.buf().unwrap()[4]; - let slice = unsafe { std::slice::from_raw_parts(ptr as *const u8, 4) }; - slice[0..4].try_into().ok() - } else { - None - } - } - - pub fn unix_path(&self) -> Option<&str> { - if self.buf_size >= 108 { - let cstr = - unsafe { CStr::from_ptr(&self.buf().unwrap()[2] as *const _ as *const c_char) }; - cstr.to_str().ok() - } else { - None - } - } - - #[cfg(target_os = "linux")] - fn parse_address(buf: &[u8], addr_len: u32) -> Option { - let sockaddr: SockaddrStorage = unsafe { - SockaddrStorage::from_raw(&buf[0] as *const _ as *const sockaddr, Some(addr_len))? - }; - - match sockaddr.family() { - Some(AddressFamily::Inet) => debug!("parse_address: AF_INET"), - Some(AddressFamily::Inet6) => debug!("parse_address: AF_INET6"), - Some(AddressFamily::Unix) => debug!("parse_address: AF_UNIX"), - _ => { - if let Some(family) = sockaddr.family() { - warn!("parse_address: unsupported family {family:?}"); - } else { - warn!("parse_address: error parsing family"); - } - return None; - } - } - - Some(sockaddr) - } - - #[cfg(target_os = "macos")] - fn parse_address(buf: &[u8], _addr_len: u32) -> Option { - let family: u16 = byte_order::read_le_u16(&buf[0..2]); - - match family { - defs::LINUX_AF_INET => { - debug!("parse_address: AF_INET"); - let in_port: u16 = byte_order::read_be_u16(&buf[2..4]); - let in_addr = Ipv4Addr::new(buf[4], buf[5], buf[6], buf[7]); - Some(SocketAddrV4::new(in_addr, in_port).into()) - } - defs::LINUX_AF_INET6 => { - debug!("parse_address: AF_INET6"); - let in_port: u16 = byte_order::read_be_u16(&buf[2..4]); - let flowinfo: u32 = byte_order::read_be_u32(&buf[4..8]); - let in6_addr = Ipv6Addr::new( - byte_order::read_be_u16(&buf[8..10]), - byte_order::read_be_u16(&buf[10..12]), - byte_order::read_be_u16(&buf[12..14]), - byte_order::read_be_u16(&buf[14..16]), - byte_order::read_be_u16(&buf[16..18]), - byte_order::read_be_u16(&buf[18..20]), - byte_order::read_be_u16(&buf[20..22]), - byte_order::read_be_u16(&buf[22..24]), - ); - let scope_id: u32 = byte_order::read_be_u32(&buf[24..28]); - Some(SocketAddrV6::new(in6_addr, in_port, flowinfo, scope_id).into()) - } - defs::LINUX_AF_UNIX => { - // On macOS, SockaddrStorage doesn't implement `from_raw` for - // Unix sockets, nor a way to cast an UnixPath to it. - error!("AF_UNIX sockets aren't yet supported on macOS"); - None - } - _ => None, - } - } - - pub fn read_proxy_create(&self) -> Option { - if self.buf_size >= 6 { - let peer_port: u32 = byte_order::read_le_u32(&self.buf().unwrap()[0..]); - let family: u16 = byte_order::read_le_u16(&self.buf().unwrap()[4..]); - let _type: u16 = byte_order::read_le_u16(&self.buf().unwrap()[6..]); - - Some(TsiProxyCreate { - peer_port, - family, - _type, - }) - } else { - None - } - } - - pub fn read_connect_req(&self) -> Option { - if self.buf_size >= 4 { - let buf = self.buf().unwrap(); - let peer_port: u32 = byte_order::read_le_u32(&buf[0..]); - let addr_len: u32 = byte_order::read_le_u32(&buf[4..]); - let addr = Self::parse_address(&buf[8..], addr_len)?; - - Some(TsiConnectReq { peer_port, addr }) - } else { - None - } - } - - pub fn write_connect_rsp(&mut self, rsp: TsiConnectRsp) { - if self.buf_size >= 4 { - if let Some(buf) = self.buf_mut() { - byte_order::write_le_u32(&mut buf[0..], rsp.result as u32); - } - } - } - - pub fn read_getname_req(&self) -> Option { - if self.buf_size >= 12 { - let peer_port: u32 = byte_order::read_le_u32(&self.buf().unwrap()[0..]); - let local_port: u32 = byte_order::read_le_u32(&self.buf().unwrap()[4..]); - let peer: u32 = byte_order::read_le_u32(&self.buf().unwrap()[8..]); - Some(TsiGetnameReq { - peer_port, - local_port, - peer, - }) - } else { - None - } - } - - pub fn write_getname_rsp(&mut self, rsp: TsiGetnameRsp) { - if self.buf_size >= 132 { - if let Some(buf) = self.buf_mut() { - byte_order::write_le_u32(&mut buf[0..], rsp.result as u32); - byte_order::write_le_u32(&mut buf[4..], rsp.addr_len); - let addr_ptr = rsp.addr.as_ptr(); - let slice = unsafe { - std::slice::from_raw_parts(addr_ptr as *const u8, rsp.addr.len() as usize) - }; - buf[8..(rsp.addr.len() + 8) as usize].copy_from_slice(slice); - - // On macOS, convert BSD sockaddr (u8 sa_len + u8 sa_family) to - // Linux wire format (u16 sa_family). Also translate macOS AF_* - // values to their Linux equivalents (e.g. AF_INET6: 30 → 10). - #[cfg(target_os = "macos")] - { - let bsd_family = buf[9]; - let linux_family: u16 = match bsd_family as i32 { - libc::AF_INET => defs::LINUX_AF_INET, - libc::AF_INET6 => defs::LINUX_AF_INET6, - _ => 0, // AF_UNSPEC - }; - byte_order::write_le_u16(&mut buf[8..], linux_family); - } - } - } - } - - pub fn read_sendto_addr(&self) -> Option { - if self.buf_size >= 4 { - let buf = self.buf().unwrap(); - let peer_port: u32 = byte_order::read_le_u32(&buf[0..]); - let addr_len: u32 = byte_order::read_le_u32(&buf[4..]); - let addr = Self::parse_address(&buf[8..], addr_len)?; - - Some(TsiSendtoAddr { peer_port, addr }) - } else { - None - } - } - - pub fn read_listen_req(&self) -> Option { - if self.buf_size >= 12 { - let buf = self.buf().unwrap(); - let peer_port: u32 = byte_order::read_le_u32(&buf[0..]); - let vm_port: u32 = byte_order::read_le_u32(&buf[4..]); - let backlog: u32 = byte_order::read_le_u32(&buf[8..]); - let addr_len: u32 = byte_order::read_le_u32(&buf[12..]); - let addr = Self::parse_address(&buf[16..], addr_len)?; - - Some(TsiListenReq { - peer_port, - vm_port, - backlog: backlog as i32, - addr, - }) - } else { - None - } - } - - pub fn write_listen_rsp(&mut self, rsp: TsiListenRsp) { - if self.buf_size >= 4 { - if let Some(buf) = self.buf_mut() { - byte_order::write_le_u32(&mut buf[0..], rsp.result as u32); - } - } - } - - pub fn read_accept_req(&self) -> Option { - if self.buf_size >= 8 { - let peer_port: u32 = byte_order::read_le_u32(&self.buf().unwrap()[0..]); - let flags: u32 = byte_order::read_le_u32(&self.buf().unwrap()[4..]); - - Some(TsiAcceptReq { peer_port, flags }) - } else { - None - } - } - - pub fn write_accept_rsp(&mut self, rsp: TsiAcceptRsp) { - if self.buf_size >= 4 { - if let Some(buf) = self.buf_mut() { - byte_order::write_le_u32(&mut buf[0..], rsp.result as u32); - } - } - } - - pub fn read_release_req(&self) -> Option { - if self.buf_size >= 8 { - let peer_port: u32 = byte_order::read_le_u32(&self.buf().unwrap()[0..]); - let local_port: u32 = byte_order::read_le_u32(&self.buf().unwrap()[4..]); - Some(TsiReleaseReq { - peer_port, - local_port, - }) - } else { - None - } - } - - pub fn write_time_sync(&mut self, time: u64) { - if self.buf_size >= 8 { - if let Some(buf) = self.buf_mut() { - byte_order::write_le_u64(&mut buf[0..], time); - } - } - } -} diff --git a/vendor/krun-devices/src/virtio/vsock/proxy.rs b/vendor/krun-devices/src/virtio/vsock/proxy.rs deleted file mode 100644 index 254f4b98c..000000000 --- a/vendor/krun-devices/src/virtio/vsock/proxy.rs +++ /dev/null @@ -1,98 +0,0 @@ -use std::collections::HashMap; -use std::fmt; -use std::os::fd::OwnedFd; -use std::os::unix::io::{AsRawFd, RawFd}; - -use super::muxer::MuxerRx; -use super::packet::{TsiAcceptReq, TsiConnectReq, TsiListenReq, TsiSendtoAddr, VsockPacket}; -use nix::sys::socket::AddressFamily; -use utils::epoll::EventSet; - -#[derive(Debug)] -pub enum RecvPkt { - Close, - Error, - Read(usize), - WaitForCredit, -} - -#[allow(dead_code)] -#[derive(Debug)] -pub enum ProxyError { - CreatingSocket(nix::errno::Errno), - InvalidFamily, - SettingReuseAddr(nix::errno::Errno), - SettingReusePort(nix::errno::Errno), -} - -#[derive(Eq, PartialEq, Clone, Copy, Debug)] -pub enum ProxyStatus { - Idle, - Connecting, - Connected, - Listening, - Closed, - WaitingCreditUpdate, - ReverseInit, - WaitingOnAccept, -} - -#[derive(Default)] -pub enum ProxyRemoval { - #[default] - Keep, - Immediate, - Deferred, -} - -#[derive(Default)] -pub enum NewProxyType { - #[default] - Tcp, - Unix, -} - -#[derive(Default)] -pub struct ProxyUpdate { - pub signal_queue: bool, - pub remove_proxy: ProxyRemoval, - pub polling: Option<(u64, RawFd, EventSet)>, - pub new_proxy: Option<(u32, OwnedFd, AddressFamily, NewProxyType)>, - pub push_accept: Option<(u64, u64)>, - pub push_credit_req: Option, -} - -impl fmt::Display for ProxyError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{self:?}") - } -} - -pub trait Proxy: Send + AsRawFd { - fn id(&self) -> u64; - #[allow(dead_code)] - fn status(&self) -> ProxyStatus; - fn connect(&mut self, pkt: &VsockPacket, req: TsiConnectReq) -> ProxyUpdate; - fn confirm_connect(&mut self, _pkt: &VsockPacket) -> Option { - None - } - fn getpeername(&mut self, pkt: &VsockPacket); - fn sendmsg(&mut self, pkt: &VsockPacket) -> ProxyUpdate; - fn sendto_addr(&mut self, req: TsiSendtoAddr) -> ProxyUpdate; - fn sendto_data(&mut self, _pkt: &VsockPacket) {} - fn listen( - &mut self, - pkt: &VsockPacket, - req: TsiListenReq, - host_port_map: &Option>, - ) -> ProxyUpdate; - fn accept(&mut self, req: TsiAcceptReq) -> ProxyUpdate; - fn update_peer_credit(&mut self, pkt: &VsockPacket) -> ProxyUpdate; - fn push_op_request(&self) {} - fn process_op_response(&mut self, pkt: &VsockPacket) -> ProxyUpdate; - fn enqueue_accept(&mut self) {} - fn push_accept_rsp(&self, _result: i32) {} - fn shutdown(&mut self, _pkt: &VsockPacket) {} - fn release(&mut self) -> ProxyUpdate; - fn process_event(&mut self, evset: EventSet) -> ProxyUpdate; -} diff --git a/vendor/krun-devices/src/virtio/vsock/reaper.rs b/vendor/krun-devices/src/virtio/vsock/reaper.rs deleted file mode 100644 index 8768aa899..000000000 --- a/vendor/krun-devices/src/virtio/vsock/reaper.rs +++ /dev/null @@ -1,74 +0,0 @@ -use std::collections::HashMap; -use std::sync::{Arc, Mutex, RwLock}; -use std::thread; -use std::time::{Duration, Instant}; - -use super::proxy::Proxy; -use crossbeam_channel::Receiver; - -pub type ProxyMap = Arc>>>>; -const TIMEOUT: Duration = Duration::new(5, 0); - -pub struct ReaperThread { - receiver: Receiver, - proxy_map: ProxyMap, - released_map: HashMap, -} - -impl ReaperThread { - pub fn new(receiver: Receiver, proxy_map: ProxyMap) -> Self { - Self { - receiver, - proxy_map, - released_map: HashMap::new(), - } - } - - fn check_expiration(&mut self) -> Duration { - let mut highest_elapsed = Duration::ZERO; - let mut expired: Vec = Vec::new(); - let now = Instant::now(); - - for (id, exptime) in self.released_map.iter() { - let elapsed = now.duration_since(*exptime); - if elapsed >= TIMEOUT { - expired.push(*id); - } else if elapsed > highest_elapsed { - highest_elapsed = elapsed; - } - } - - if !expired.is_empty() { - let mut pmap = self.proxy_map.write().unwrap(); - for id in expired { - debug!("removing proxy: {id}"); - pmap.remove(&id); - self.released_map.remove(&id); - } - debug!("remainig proxies: {}", pmap.len()); - } - - let mut timeout = Duration::MAX; - if highest_elapsed > Duration::ZERO { - timeout = TIMEOUT - highest_elapsed; - assert!(timeout > Duration::ZERO); - } - timeout - } - - fn work(&mut self) { - loop { - let timeout = self.check_expiration(); - if let Ok(id) = self.receiver.recv_timeout(timeout) { - self.released_map.insert(id, Instant::now()); - } - } - } - - pub fn run(mut self) { - thread::Builder::new() - .name("vsock reaper".into()) - .spawn(move || self.work()) - .unwrap(); - } -} diff --git a/vendor/krun-devices/src/virtio/vsock/timesync.rs b/vendor/krun-devices/src/virtio/vsock/timesync.rs deleted file mode 100644 index 206c4c1fb..000000000 --- a/vendor/krun-devices/src/virtio/vsock/timesync.rs +++ /dev/null @@ -1,88 +0,0 @@ -use std::sync::{Arc, Mutex}; -use std::thread; -use std::time; - -use super::super::Queue as VirtQueue; -use super::defs::uapi; -use super::packet::VsockPacket; - -use crate::virtio::InterruptTransport; -use vm_memory::GuestMemoryMmap; - -const UPDATE_INTERVAL: u64 = 60 * 1000 * 1000 * 1000; -const SLEEP_NSECS: u64 = 2 * 1000 * 1000 * 1000; -const TSYNC_PORT: u32 = 123; - -pub struct TimesyncThread { - cid: u64, - mem: GuestMemoryMmap, - queue_mutex: Arc>, - interrupt: InterruptTransport, -} - -impl TimesyncThread { - pub fn new( - cid: u64, - mem: GuestMemoryMmap, - queue_mutex: Arc>, - interrupt: InterruptTransport, - ) -> Self { - Self { - cid, - mem, - queue_mutex, - interrupt, - } - } - - fn send_time(&self, time: u64) { - let mut queue = self.queue_mutex.lock().unwrap(); - if let Some(head) = queue.pop(&self.mem) { - if let Ok(mut pkt) = VsockPacket::from_rx_virtq_head(&head) { - pkt.set_op(uapi::VSOCK_OP_RW) - .set_src_cid(uapi::VSOCK_HOST_CID) - .set_dst_cid(self.cid) - .set_src_port(TSYNC_PORT) - .set_dst_port(TSYNC_PORT) - .set_type(uapi::VSOCK_TYPE_DGRAM); - - pkt.write_time_sync(time); - pkt.set_len(pkt.buf().unwrap().len() as u32); - if let Err(e) = - queue.add_used(&self.mem, head.index, pkt.hdr().len() as u32 + pkt.len()) - { - error!("failed to add used elements to the queue: {e:?}"); - } - self.interrupt.signal_used_queue(); - } - } - } - - fn work(&mut self) { - let mut last_update = 0u64; - let mut last_awake = utils::time::get_time(utils::time::ClockType::Real); - loop { - let now = utils::time::get_time(utils::time::ClockType::Real); - /* - * We send a time sync packet if we slept for 3 times more - * nanoseconds than expected (which is an indication the - * system forced us to take a long nap), or if UPDATE_INTERVAL - * has been reached. - */ - if (now - last_awake) >= (SLEEP_NSECS * 3) || (now - last_update) >= UPDATE_INTERVAL { - self.send_time(now); - last_update = now; - } - - last_awake = utils::time::get_time(utils::time::ClockType::Real); - thread::sleep(time::Duration::from_nanos(SLEEP_NSECS)); - } - } - - pub fn run(mut self) { - thread::Builder::new() - .name("vsock timesync".into()) - .spawn(move || self.work()) - .unwrap(); - } -} diff --git a/vendor/krun-devices/src/virtio/vsock/tsi_dgram.rs b/vendor/krun-devices/src/virtio/vsock/tsi_dgram.rs deleted file mode 100644 index de0850c37..000000000 --- a/vendor/krun-devices/src/virtio/vsock/tsi_dgram.rs +++ /dev/null @@ -1,493 +0,0 @@ -use std::collections::HashMap; -use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6}; -use std::num::Wrapping; -use std::os::fd::OwnedFd; -use std::os::unix::io::{AsRawFd, RawFd}; -use std::sync::{Arc, Mutex}; - -use nix::fcntl::{fcntl, FcntlArg, OFlag}; -#[cfg(target_os = "linux")] -use nix::sys::socket::UnixAddr; -use nix::sys::socket::{ - bind, connect, getpeername, recv, send, sendto, socket, AddressFamily, MsgFlags, SockFlag, - SockType, SockaddrIn, SockaddrLike, SockaddrStorage, -}; - -#[cfg(target_os = "macos")] -use super::super::linux_errno::linux_errno_raw; -use super::super::Queue as VirtQueue; -use super::defs; -use super::defs::uapi; -use super::muxer::{push_packet, MuxerRx}; -use super::muxer_rxq::MuxerRxQ; -use super::packet::{ - TsiAcceptReq, TsiConnectReq, TsiGetnameRsp, TsiListenReq, TsiSendtoAddr, VsockPacket, -}; -use super::proxy::{Proxy, ProxyError, ProxyRemoval, ProxyStatus, ProxyUpdate, RecvPkt}; -use utils::epoll::EventSet; - -use vm_memory::GuestMemoryMmap; - -pub struct TsiDgramProxy { - pub id: u64, - cid: u64, - local_port: u32, - peer_port: u32, - fd: OwnedFd, - pub status: ProxyStatus, - sendto_addr: Option, - listening: bool, - family: AddressFamily, - mem: GuestMemoryMmap, - queue: Arc>, - rxq: Arc>, - rx_cnt: Wrapping, - tx_cnt: Wrapping, - peer_buf_alloc: u32, - peer_fwd_cnt: Wrapping, -} - -impl TsiDgramProxy { - pub fn new( - id: u64, - cid: u64, - family: u16, - peer_port: u32, - mem: GuestMemoryMmap, - queue: Arc>, - rxq: Arc>, - ) -> Result { - let family = match family { - defs::LINUX_AF_INET => AddressFamily::Inet, - defs::LINUX_AF_INET6 => AddressFamily::Inet6, - #[cfg(target_os = "linux")] - defs::LINUX_AF_UNIX => AddressFamily::Unix, - _ => return Err(ProxyError::InvalidFamily), - }; - - let fd = socket(family, SockType::Datagram, SockFlag::empty(), None) - .map_err(ProxyError::CreatingSocket)?; - - // macOS forces us to do this here instead of just using SockFlag::SOCK_NONBLOCK above. - match fcntl(&fd, FcntlArg::F_GETFL) { - Ok(flags) => match OFlag::from_bits(flags) { - Some(flags) => { - if let Err(e) = fcntl(&fd, FcntlArg::F_SETFL(flags | OFlag::O_NONBLOCK)) { - warn!("error switching to non-blocking: id={id}, err={e}"); - } - } - None => error!("invalid fd flags id={id}"), - }, - Err(e) => error!("couldn't obtain fd flags id={id}, err={e}"), - }; - - #[cfg(target_os = "macos")] - { - // nix doesn't provide an abstraction for SO_NOSIGPIPE, fall back to libc. - let option_value: libc::c_int = 1; - unsafe { - libc::setsockopt( - fd.as_raw_fd(), - libc::SOL_SOCKET, - libc::SO_NOSIGPIPE, - &option_value as *const _ as *const libc::c_void, - std::mem::size_of_val(&option_value) as libc::socklen_t, - ) - }; - } - - Ok(TsiDgramProxy { - id, - cid, - local_port: 0, - peer_port, - fd, - status: ProxyStatus::Idle, - sendto_addr: None, - listening: false, - family, - mem, - queue, - rxq, - rx_cnt: Wrapping(0), - tx_cnt: Wrapping(0), - peer_buf_alloc: 0, - peer_fwd_cnt: Wrapping(0), - }) - } - - fn init_pkt(&self, pkt: &mut VsockPacket) { - debug!( - "init_pkt: id={}, src_port={}, dst_port={}", - self.id, self.local_port, self.peer_port - ); - pkt.set_op(uapi::VSOCK_OP_RW) - .set_src_cid(self.cid) - .set_dst_cid(uapi::VSOCK_HOST_CID) - .set_dst_port(self.peer_port) - .set_src_port(0) - .set_type(uapi::VSOCK_TYPE_DGRAM) - .set_buf_alloc(defs::CONN_TX_BUF_SIZE as u32) - .set_fwd_cnt(self.tx_cnt.0); - } - - /* - fn peer_avail_credit(&self) -> usize { - (Wrapping(self.peer_buf_alloc) - (self.rx_cnt - self.peer_fwd_cnt)).0 as usize - } - - fn send_credit_request(&self) { - // This response goes to the connection. - let rx = MuxerRx::CreditRequest { - local_port: self.local_port, - peer_port: self.peer_port, - fwd_cnt: self.tx_cnt.0, - }; - push_packet(self.cid, rx, &self.rxq, &self.queue, &self.mem); - } - */ - - fn recv_to_pkt(&self, pkt: &mut VsockPacket) -> RecvPkt { - if let Some(buf) = pkt.buf_mut() { - // Disable UDP credit accounting until is fixed in the kernel - //let peer_credit = self.peer_avail_credit(); - //let max_len = std::cmp::min(buf.len(), peer_credit); - let max_len = buf.len(); - - /* - debug!( - "recv_to_pkt: peer_avail_credit={}, buf.len={}, max_len={}", - self.peer_avail_credit(), - buf.len(), - max_len, - ); - - if max_len == 0 { - return RecvPkt::WaitForCredit; - } - */ - - match recv(self.fd.as_raw_fd(), &mut buf[..max_len], MsgFlags::empty()) { - Ok(cnt) => { - debug!("recv cnt={cnt}"); - if cnt > 0 { - RecvPkt::Read(cnt) - } else { - RecvPkt::Close - } - } - Err(e) => { - debug!("recv_pkt: recv error: {e:?}"); - RecvPkt::Error - } - } - } else { - debug!("recv_pkt: pkt without buf"); - RecvPkt::Error - } - } - - fn recv_pkt(&mut self) -> (bool, bool) { - let mut have_used = false; - let mut wait_credit = false; - let mut queue = self.queue.lock().unwrap(); - - while let Some(head) = queue.pop(&self.mem) { - let len = match VsockPacket::from_rx_virtq_head(&head) { - Ok(mut pkt) => match self.recv_to_pkt(&mut pkt) { - RecvPkt::WaitForCredit => { - wait_credit = true; - 0 - } - RecvPkt::Read(cnt) => { - self.rx_cnt += Wrapping(cnt as u32); - self.init_pkt(&mut pkt); - pkt.set_len(cnt as u32); - pkt.hdr().len() + cnt - } - RecvPkt::Close => { - self.status = ProxyStatus::Closed; - 0 - } - RecvPkt::Error => 0, - }, - Err(e) => { - debug!("recv_pkt: RX queue error: {e:?}"); - 0 - } - }; - - if len == 0 { - queue.undo_pop(); - break; - } else { - have_used = true; - debug!("recv_pkt: pushing packet with {len} bytes"); - if let Err(e) = queue.add_used(&self.mem, head.index, len as u32) { - error!("failed to add used elements to the queue: {e:?}"); - } - } - } - - debug!("recv_pkt: have_used={have_used}"); - (have_used, wait_credit) - } -} - -impl Proxy for TsiDgramProxy { - fn id(&self) -> u64 { - self.id - } - - fn status(&self) -> ProxyStatus { - self.status - } - - fn connect(&mut self, pkt: &VsockPacket, req: TsiConnectReq) -> ProxyUpdate { - debug!("connect: addr={}", req.addr); - let res = match connect(self.fd.as_raw_fd(), &req.addr) { - Ok(()) => { - debug!("connect: Connected"); - self.status = ProxyStatus::Connected; - 0 - } - Err(e) => { - debug!("Error connecting: {e}"); - #[cfg(target_os = "macos")] - let errno = -linux_errno_raw(e as i32); - #[cfg(target_os = "linux")] - let errno = -(e as i32); - errno - } - }; - - self.peer_buf_alloc = pkt.buf_alloc(); - self.peer_fwd_cnt = Wrapping(pkt.fwd_cnt()); - - // This response goes to the connection. - let rx = MuxerRx::ConnResponse { - local_port: pkt.dst_port(), - peer_port: pkt.src_port(), - result: res, - }; - push_packet(self.cid, rx, &self.rxq, &self.queue, &self.mem); - - let mut update = ProxyUpdate::default(); - if res == 0 && !self.listening { - update.polling = Some((self.id, self.fd.as_raw_fd(), EventSet::IN)); - } - update - } - - fn getpeername(&mut self, pkt: &VsockPacket) { - debug!("process_getpeername"); - - let (result, addr): (i32, SockaddrStorage) = match getpeername(self.fd.as_raw_fd()) { - Ok(name) => (0, name), - Err(e) => { - #[cfg(target_os = "macos")] - let errno = -linux_errno_raw(e as i32); - #[cfg(target_os = "linux")] - let errno = -(e as i32); - ( - errno, - SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 0).into(), - ) - } - }; - - let data = TsiGetnameRsp { - result, - addr_len: addr.len(), - addr, - }; - - // This response goes to the connection. - let rx = MuxerRx::GetnameResponse { - local_port: pkt.dst_port(), - peer_port: pkt.src_port(), - data, - }; - push_packet(self.cid, rx, &self.rxq, &self.queue, &self.mem); - } - - fn sendmsg(&mut self, pkt: &VsockPacket) -> ProxyUpdate { - debug!("sendmsg"); - - let ret = if let Some(buf) = pkt.buf() { - #[cfg(target_os = "macos")] - let flags = MsgFlags::empty(); - #[cfg(target_os = "linux")] - let flags = MsgFlags::MSG_NOSIGNAL; - - match send(self.fd.as_raw_fd(), buf, flags) { - Ok(sent) => { - self.tx_cnt += Wrapping(sent as u32); - sent as i32 - } - Err(err) => -(err as i32), - } - } else { - -libc::EINVAL - }; - - debug!("sendmsg ret={ret}"); - - ProxyUpdate::default() - } - - fn sendto_addr(&mut self, req: TsiSendtoAddr) -> ProxyUpdate { - debug!("sendto_addr: addr={}", req.addr); - - let mut update = ProxyUpdate::default(); - - self.sendto_addr = Some(req.addr); - if !self.listening { - let bind_result = match self.family { - AddressFamily::Inet => bind(self.fd.as_raw_fd(), &SockaddrIn::new(0, 0, 0, 0, 0)), - AddressFamily::Inet6 => { - let addr6: SockaddrStorage = - SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, 0, 0, 0).into(); - bind(self.fd.as_raw_fd(), &addr6) - } - #[cfg(target_os = "linux")] - AddressFamily::Unix => { - let addr = UnixAddr::new_unnamed(); - bind(self.fd.as_raw_fd(), &addr) - } - _ => { - warn!("sendto_addr: unsupported address family: {:?}", self.family); - return update; - } - }; - - match bind_result { - Ok(_) => { - self.listening = true; - update.polling = Some((self.id, self.fd.as_raw_fd(), EventSet::IN)); - } - Err(e) => debug!("couldn't bind socket: {e}"), - } - } - - update - } - - fn sendto_data(&mut self, pkt: &VsockPacket) { - debug!("sendto_data"); - - self.peer_buf_alloc = pkt.buf_alloc(); - self.peer_fwd_cnt = Wrapping(pkt.fwd_cnt()); - - if let Some(addr) = self.sendto_addr { - if let Some(buf) = pkt.buf() { - #[cfg(target_os = "macos")] - let flags = MsgFlags::empty(); - #[cfg(target_os = "linux")] - let flags = MsgFlags::MSG_NOSIGNAL; - - match sendto(self.fd.as_raw_fd(), buf, &addr, flags) { - Ok(sent) => { - self.tx_cnt += Wrapping(sent as u32); - } - Err(err) => debug!("error in sendto: {err}"), - } - } else { - debug!("sendto_data pkt without buffer"); - } - } else { - debug!("sendto_data without sendto_addr"); - } - } - - fn listen( - &mut self, - _pkt: &VsockPacket, - _req: TsiListenReq, - _host_port_map: &Option>, - ) -> ProxyUpdate { - ProxyUpdate::default() - } - - fn accept(&mut self, _req: TsiAcceptReq) -> ProxyUpdate { - ProxyUpdate::default() - } - - fn update_peer_credit(&mut self, pkt: &VsockPacket) -> ProxyUpdate { - debug!( - "update_credit: buf_alloc={} rx_cnt={} fwd_cnt={}", - pkt.buf_alloc(), - self.rx_cnt, - pkt.fwd_cnt() - ); - self.peer_buf_alloc = pkt.buf_alloc(); - self.peer_fwd_cnt = Wrapping(pkt.fwd_cnt()); - - ProxyUpdate { - polling: Some((self.id, self.fd.as_raw_fd(), EventSet::IN)), - ..Default::default() - } - } - - fn process_op_response(&mut self, _pkt: &VsockPacket) -> ProxyUpdate { - ProxyUpdate::default() - } - - fn release(&mut self) -> ProxyUpdate { - debug!("release"); - let remove_proxy = if self.status == ProxyStatus::Listening { - ProxyRemoval::Immediate - } else { - ProxyRemoval::Deferred - }; - ProxyUpdate { - remove_proxy, - ..Default::default() - } - } - - fn process_event(&mut self, evset: EventSet) -> ProxyUpdate { - let mut update = ProxyUpdate::default(); - - if evset.contains(EventSet::HANG_UP) { - update.remove_proxy = if self.status == ProxyStatus::Listening { - ProxyRemoval::Immediate - } else { - ProxyRemoval::Deferred - }; - return update; - } - - if evset.contains(EventSet::IN) { - let (signal_queue, wait_credit) = self.recv_pkt(); - update.signal_queue = signal_queue || wait_credit; - - if wait_credit && self.status != ProxyStatus::WaitingCreditUpdate { - self.status = ProxyStatus::WaitingCreditUpdate; - let rx = MuxerRx::CreditRequest { - local_port: self.local_port, - peer_port: self.peer_port, - fwd_cnt: self.tx_cnt.0, - }; - update.push_credit_req = Some(rx); - } - - if self.status == ProxyStatus::WaitingCreditUpdate { - debug!("process_event: WaitingCreditUpdate"); - update.polling = Some((self.id(), self.fd.as_raw_fd(), EventSet::empty())); - } - } - - if evset.contains(EventSet::OUT) { - error!("EventSet::OUT unexpected"); - } - - update - } -} - -impl AsRawFd for TsiDgramProxy { - fn as_raw_fd(&self) -> RawFd { - self.fd.as_raw_fd() - } -} diff --git a/vendor/krun-devices/src/virtio/vsock/tsi_stream.rs b/vendor/krun-devices/src/virtio/vsock/tsi_stream.rs deleted file mode 100644 index c819398d3..000000000 --- a/vendor/krun-devices/src/virtio/vsock/tsi_stream.rs +++ /dev/null @@ -1,901 +0,0 @@ -use std::collections::HashMap; -use std::fs; -use std::net::{Ipv4Addr, SocketAddrV4, SocketAddrV6}; -use std::num::Wrapping; -use std::os::fd::{FromRawFd, OwnedFd}; -use std::os::unix::fs::FileTypeExt; -use std::os::unix::io::{AsRawFd, RawFd}; -use std::path::PathBuf; -use std::str::FromStr; -use std::sync::{Arc, Mutex}; - -#[cfg(target_os = "linux")] -use libc::EINVAL; -#[cfg(target_os = "macos")] -use libc::EINVAL; -use nix::errno::Errno; -use nix::fcntl::{fcntl, FcntlArg, OFlag}; -use nix::sys::socket::{ - accept, bind, connect, getpeername, listen, recv, send, setsockopt, shutdown, socket, sockopt, - AddressFamily, Backlog, MsgFlags, Shutdown, SockFlag, SockType, SockaddrLike, SockaddrStorage, -}; - -#[cfg(target_os = "macos")] -use super::super::linux_errno::linux_errno_raw; -use super::super::Queue as VirtQueue; -use super::defs; -use super::defs::uapi; -use super::muxer::{push_packet, MuxerRx}; -use super::muxer_rxq::MuxerRxQ; -use super::packet::{ - TsiAcceptReq, TsiConnectReq, TsiGetnameRsp, TsiListenReq, TsiSendtoAddr, VsockPacket, -}; -use super::proxy::{ - NewProxyType, Proxy, ProxyError, ProxyRemoval, ProxyStatus, ProxyUpdate, RecvPkt, -}; -use utils::epoll::EventSet; - -use vm_memory::GuestMemoryMmap; - -pub struct TsiStreamProxy { - id: u64, - cid: u64, - parent_id: u64, - family: AddressFamily, - local_port: u32, - peer_port: u32, - control_port: u32, - fd: OwnedFd, - pub status: ProxyStatus, - mem: GuestMemoryMmap, - queue: Arc>, - rxq: Arc>, - rx_cnt: Wrapping, - tx_cnt: Wrapping, - last_tx_cnt_sent: Wrapping, - peer_buf_alloc: u32, - peer_fwd_cnt: Wrapping, - push_cnt: Wrapping, - pending_accepts: u64, - unixsock_path: Option, -} - -impl TsiStreamProxy { - #[allow(clippy::too_many_arguments)] - pub fn new( - id: u64, - cid: u64, - family: u16, - local_port: u32, - peer_port: u32, - control_port: u32, - mem: GuestMemoryMmap, - queue: Arc>, - rxq: Arc>, - ) -> Result { - let family = match family { - defs::LINUX_AF_INET => AddressFamily::Inet, - defs::LINUX_AF_INET6 => AddressFamily::Inet6, - #[cfg(target_os = "linux")] - defs::LINUX_AF_UNIX => AddressFamily::Unix, - _ => return Err(ProxyError::InvalidFamily), - }; - let fd = socket(family, SockType::Stream, SockFlag::empty(), None) - .map_err(ProxyError::CreatingSocket)?; - - // macOS forces us to do this here instead of just using SockFlag::SOCK_NONBLOCK above. - match fcntl(&fd, FcntlArg::F_GETFL) { - Ok(flags) => match OFlag::from_bits(flags) { - Some(flags) => { - if let Err(e) = fcntl(&fd, FcntlArg::F_SETFL(flags | OFlag::O_NONBLOCK)) { - warn!("error switching to non-blocking: id={id}, err={e}"); - } - } - None => error!("invalid fd flags id={id}"), - }, - Err(e) => error!("couldn't obtain fd flags id={id}, err={e}"), - }; - - if family == AddressFamily::Unix { - setsockopt(&fd, sockopt::ReuseAddr, &true).map_err(ProxyError::SettingReuseAddr)?; - } else { - setsockopt(&fd, sockopt::ReusePort, &true).map_err(ProxyError::SettingReusePort)?; - } - - #[cfg(target_os = "macos")] - { - // nix doesn't provide an abstraction for SO_NOSIGPIPE, fall back to libc. - let option_value: libc::c_int = 1; - unsafe { - libc::setsockopt( - fd.as_raw_fd(), - libc::SOL_SOCKET, - libc::SO_NOSIGPIPE, - &option_value as *const _ as *const libc::c_void, - std::mem::size_of_val(&option_value) as libc::socklen_t, - ) - }; - } - - Ok(TsiStreamProxy { - id, - cid, - parent_id: 0, - family, - local_port, - peer_port, - control_port, - fd, - status: ProxyStatus::Idle, - mem, - queue, - rxq, - rx_cnt: Wrapping(0), - tx_cnt: Wrapping(0), - last_tx_cnt_sent: Wrapping(0), - peer_buf_alloc: 0, - peer_fwd_cnt: Wrapping(0), - push_cnt: Wrapping(0), - pending_accepts: 0, - unixsock_path: None, - }) - } - - #[allow(clippy::too_many_arguments)] - pub fn new_reverse( - id: u64, - cid: u64, - parent_id: u64, - family: AddressFamily, - local_port: u32, - peer_port: u32, - fd: OwnedFd, - mem: GuestMemoryMmap, - queue: Arc>, - rxq: Arc>, - ) -> Self { - debug!("new_reverse: id={id} local_port={local_port} peer_port={peer_port}"); - TsiStreamProxy { - id, - cid, - parent_id, - family, - local_port, - peer_port, - control_port: 0, - fd, - status: ProxyStatus::ReverseInit, - mem, - queue, - rxq, - rx_cnt: Wrapping(0), - tx_cnt: Wrapping(0), - last_tx_cnt_sent: Wrapping(0), - peer_buf_alloc: 0, - peer_fwd_cnt: Wrapping(0), - push_cnt: Wrapping(0), - pending_accepts: 0, - unixsock_path: None, - } - } - - fn init_data_pkt(&self, pkt: &mut VsockPacket) { - debug!( - "init_data_pkt: id={}, local_port={}, peer_port={}", - self.id, self.local_port, self.peer_port - ); - pkt.set_op(uapi::VSOCK_OP_RW) - .set_src_cid(uapi::VSOCK_HOST_CID) - .set_dst_cid(self.cid) - .set_src_port(self.local_port) - .set_dst_port(self.peer_port) - .set_type(uapi::VSOCK_TYPE_STREAM) - .set_buf_alloc(defs::CONN_TX_BUF_SIZE as u32) - .set_fwd_cnt(self.tx_cnt.0); - } - - fn try_listen(&mut self, req: &TsiListenReq, host_port_map: &Option>) -> i32 { - if self.status == ProxyStatus::Listening || self.status == ProxyStatus::WaitingOnAccept { - return 0; - } - - let addr: SockaddrStorage = if let Some(port_map) = host_port_map { - if let Some(sin) = req.addr.as_sockaddr_in() { - debug!("sockaddr is ipv4"); - if let Some(port) = port_map.get(&sin.port()) { - SocketAddrV4::new(sin.ip(), *port).into() - } else { - req.addr - } - } else if let Some(sin6) = req.addr.as_sockaddr_in6() { - debug!("sockaddr is ipv6"); - if let Some(port) = port_map.get(&sin6.port()) { - SocketAddrV6::new(sin6.ip(), *port, sin6.flowinfo(), sin6.flowinfo()).into() - } else { - req.addr - } - } else if req.addr.as_unix_addr().is_some() { - debug!("sockaddr is unix"); - req.addr - } else { - return -libc::EINVAL; - } - } else { - req.addr - }; - - let unixsock_path = self.get_unixsock_path(&addr); - // If the userspace process in the guest has already created the socket, - // we need to unlink it to take ownership of the node in the filesystem. - if let Some(path) = &unixsock_path { - if let Err(e) = fs::remove_file(path) { - debug!("error removing socket: {e}"); - } - } - - match bind(self.fd.as_raw_fd(), &addr) { - Ok(_) => { - debug!("tcp bind: id={}", self.id); - - // For unix sockets we need to unlink the path on Drop, since - // it's possible the userspace application can't do it itself. - self.unixsock_path = unixsock_path; - - // Clamp backlog to SOMAXCONN, mirroring Linux kernel's __sys_listen behavior. - // The nix crate's Backlog::new() rejects values above SOMAXCONN with EINVAL. - let clamped_backlog = req.backlog.clamp(0, libc::SOMAXCONN); - match Backlog::new(clamped_backlog) { - Ok(backlog) => match listen(&self.fd, backlog) { - Ok(_) => { - debug!("proxy: id={}", self.id); - 0 - } - Err(e) => { - warn!("proxy: id={} err={}", self.id, e); - #[cfg(target_os = "macos")] - let errno = -linux_errno_raw(e as i32); - #[cfg(target_os = "linux")] - let errno = -(e as i32); - errno - } - }, - Err(e) => { - warn!("proxy: id={} err={}", self.id, e); - #[cfg(target_os = "macos")] - let errno = -linux_errno_raw(e as i32); - #[cfg(target_os = "linux")] - let errno = -(e as i32); - errno - } - } - } - Err(e) => { - warn!("tcp bind: id={} err={}", self.id, e); - #[cfg(target_os = "macos")] - let errno = -linux_errno_raw(e as i32); - #[cfg(target_os = "linux")] - let errno = -(e as i32); - errno - } - } - } - - fn peer_avail_credit(&self) -> usize { - (Wrapping(self.peer_buf_alloc) - (self.rx_cnt - self.peer_fwd_cnt)).0 as usize - } - - fn recv_to_pkt(&self, pkt: &mut VsockPacket) -> RecvPkt { - if let Some(buf) = pkt.buf_mut() { - let peer_credit = self.peer_avail_credit(); - let max_len = std::cmp::min(buf.len(), peer_credit); - - debug!( - "recv_to_pkt: peer_avail_credit={}, buf.len={}, max_len={}", - self.peer_avail_credit(), - buf.len(), - max_len, - ); - - if max_len == 0 { - return RecvPkt::WaitForCredit; - } - - match recv( - self.fd.as_raw_fd(), - &mut buf[..max_len], - MsgFlags::MSG_DONTWAIT, - ) { - Ok(cnt) => { - debug!("recv cnt={cnt}"); - if cnt > 0 { - debug!("recv rx_cnt={}", self.rx_cnt); - RecvPkt::Read(cnt) - } else { - RecvPkt::Close - } - } - Err(e) => { - debug!("recv_pkt: recv error: {e:?}"); - RecvPkt::Error - } - } - } else { - debug!("recv_pkt: pkt without buf"); - RecvPkt::Error - } - } - - fn recv_pkt(&mut self) -> (bool, bool) { - let mut have_used = false; - let mut wait_credit = false; - let mut queue = self.queue.lock().unwrap(); - - while let Some(head) = queue.pop(&self.mem) { - let len = match VsockPacket::from_rx_virtq_head(&head) { - Ok(mut pkt) => match self.recv_to_pkt(&mut pkt) { - RecvPkt::WaitForCredit => { - wait_credit = true; - 0 - } - RecvPkt::Read(cnt) => { - self.rx_cnt += Wrapping(cnt as u32); - self.init_data_pkt(&mut pkt); - pkt.set_len(cnt as u32); - pkt.hdr().len() + cnt - } - RecvPkt::Close => { - self.status = ProxyStatus::Closed; - 0 - } - RecvPkt::Error => 0, - }, - Err(e) => { - debug!("recv_pkt: RX queue error: {e:?}"); - 0 - } - }; - - if len == 0 { - queue.undo_pop(); - break; - } else { - have_used = true; - self.push_cnt += Wrapping(len as u32); - debug!( - "recv_pkt: pushing packet with {} bytes, push_cnt={}", - len, self.push_cnt - ); - if let Err(e) = queue.add_used(&self.mem, head.index, len as u32) { - error!("failed to add used elements to the queue: {e:?}"); - } - } - } - - debug!("recv_pkt: have_used={have_used}"); - (have_used, wait_credit) - } - - fn push_connect_rsp(&self, result: i32) { - debug!( - "push_connect_rsp: id: {}, control_port: {}, result: {}", - self.id, self.control_port, result - ); - - // This response goes to the control port (DGRAM). - let rx = MuxerRx::ConnResponse { - local_port: 1025, - peer_port: self.control_port, - result, - }; - push_packet(self.cid, rx, &self.rxq, &self.queue, &self.mem); - } - - fn push_reset(&self) { - debug!( - "push_reset: id: {}, peer_port: {}, local_port: {}", - self.id, self.peer_port, self.local_port - ); - - // This response goes to the connection. - let rx = MuxerRx::Reset { - local_port: self.local_port, - peer_port: self.peer_port, - }; - push_packet(self.cid, rx, &self.rxq, &self.queue, &self.mem); - } - - fn switch_to_connected(&mut self) { - self.status = ProxyStatus::Connected; - match fcntl(&self.fd, FcntlArg::F_GETFL) { - Ok(flags) => match OFlag::from_bits(flags) { - Some(flags) => { - if let Err(e) = fcntl(&self.fd, FcntlArg::F_SETFL(flags & !OFlag::O_NONBLOCK)) { - warn!("error switching to blocking: id={}, err={}", self.id, e); - } - } - None => error!("invalid fd flags id={}", self.id), - }, - Err(e) => error!("couldn't obtain fd flags id={}, err={}", self.id, e), - }; - } - - fn get_addr_len(&self, addr: &SockaddrStorage) -> Option { - let addr_len = match self.family { - AddressFamily::Inet => addr.as_sockaddr_in()?.len(), - AddressFamily::Inet6 => addr.as_sockaddr_in6()?.len(), - AddressFamily::Unix => addr.as_unix_addr()?.len(), - _ => 0, - }; - - Some(addr_len) - } - - fn get_unixsock_path(&self, addr: &SockaddrStorage) -> Option { - if let Some(addr) = addr.as_unix_addr() { - if let Some(path) = addr.path() { - // SockaddrStorage doesn't clean up NULLs. This is fine when - // using addr with other nix methods, but we need to clean them - // up to be able to treat it as a path with other Rust crates. - let path_str = path.to_str()?.replace("\0", ""); - debug!("unix socket path_str={path_str}"); - - match fs::metadata(&path_str) { - Ok(metadata) => { - if metadata.file_type().is_socket() { - debug!("unix socket path is socket"); - return PathBuf::from_str(&path_str).ok(); - } else { - debug!("unix socket path is NOT a socket"); - } - } - Err(e) => debug!("metadata failed with {e}"), - } - } - } - - None - } -} - -impl Proxy for TsiStreamProxy { - fn id(&self) -> u64 { - self.id - } - - fn status(&self) -> ProxyStatus { - self.status - } - - fn connect(&mut self, _pkt: &VsockPacket, req: TsiConnectReq) -> ProxyUpdate { - let mut update = ProxyUpdate::default(); - - let result = match connect(self.fd.as_raw_fd(), &req.addr) { - Ok(()) => { - debug!("connect: Connected"); - self.switch_to_connected(); - 0 - } - Err(nix::errno::Errno::EINPROGRESS) => { - debug!("connect: Connecting"); - self.status = ProxyStatus::Connecting; - 0 - } - Err(e) => { - debug!("TcpProxy: Error connecting: {e}"); - #[cfg(target_os = "macos")] - let errno = -linux_errno_raw(Errno::last_raw()); - #[cfg(target_os = "linux")] - let errno = -Errno::last_raw(); - errno - } - }; - - if self.status == ProxyStatus::Connecting { - update.polling = Some(( - self.id, - self.fd.as_raw_fd(), - EventSet::OUT | EventSet::EDGE_TRIGGERED, - )); - } else { - if self.status == ProxyStatus::Connected { - update.polling = Some((self.id, self.fd.as_raw_fd(), EventSet::IN)); - } - self.push_connect_rsp(result); - } - - update - } - - fn confirm_connect(&mut self, pkt: &VsockPacket) -> Option { - debug!( - "confirm_connect: local_port={} peer_port={}, src_port={}, dst_port={}", - pkt.dst_port(), - pkt.src_port(), - self.local_port, - self.peer_port, - ); - - self.peer_buf_alloc = pkt.buf_alloc(); - self.peer_fwd_cnt = Wrapping(pkt.fwd_cnt()); - - self.local_port = pkt.dst_port(); - self.peer_port = pkt.src_port(); - - // This response goes to the connection. - let rx = MuxerRx::OpResponse { - local_port: pkt.dst_port(), - peer_port: pkt.src_port(), - }; - push_packet(self.cid, rx, &self.rxq, &self.queue, &self.mem); - - // Now that the vsock transport is fully established, start listening - // for events in the TCP socket again. - Some(ProxyUpdate { - polling: Some((self.id, self.fd.as_raw_fd(), EventSet::IN)), - ..Default::default() - }) - } - - fn getpeername(&mut self, pkt: &VsockPacket) { - debug!("getpeername: id={}", self.id); - - let (result, addr_len, addr): (i32, u32, SockaddrStorage) = - match getpeername(self.fd.as_raw_fd()) { - Ok(addr) => { - if let Some(addr_len) = self.get_addr_len(&addr) { - (0, addr_len, addr) - } else { - #[cfg(target_os = "macos")] - let errno = -linux_errno_raw(EINVAL); - #[cfg(target_os = "linux")] - let errno = -EINVAL; - (errno, 0, addr) - } - } - Err(e) => { - #[cfg(target_os = "macos")] - let errno = -linux_errno_raw(e as i32); - #[cfg(target_os = "linux")] - let errno = -(e as i32); - ( - errno, - 0, - SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 0).into(), - ) - } - }; - - let data = TsiGetnameRsp { - result, - addr_len, - addr, - }; - - debug!("getpeername: reply={data:?}"); - - // This response goes to the control port (DGRAM). - let rx = MuxerRx::GetnameResponse { - local_port: pkt.dst_port(), - peer_port: pkt.src_port(), - data, - }; - push_packet(self.cid, rx, &self.rxq, &self.queue, &self.mem); - } - - fn sendmsg(&mut self, pkt: &VsockPacket) -> ProxyUpdate { - debug!("sendmsg"); - - let mut update = ProxyUpdate::default(); - - let ret = if let Some(buf) = pkt.buf() { - #[cfg(target_os = "macos")] - let flags = MsgFlags::empty(); - #[cfg(target_os = "linux")] - let flags = MsgFlags::MSG_NOSIGNAL; - - match send(self.fd.as_raw_fd(), buf, flags) { - Ok(sent) => { - if sent != buf.len() { - error!("couldn't set everything: buf={}, sent={}", buf.len(), sent); - } - self.tx_cnt += Wrapping(sent as u32); - sent as i32 - } - Err(err) => { - #[cfg(target_os = "macos")] - let errno = -linux_errno_raw(err as i32); - #[cfg(target_os = "linux")] - let errno = -(err as i32); - errno - } - } - } else { - -libc::EINVAL - }; - - if ret > 0 && (self.tx_cnt - self.last_tx_cnt_sent).0 >= self.peer_buf_alloc / 2 { - debug!( - "sending credit update: id={}, tx_cnt={}, last_tx_cnt={}", - self.id, self.tx_cnt, self.last_tx_cnt_sent - ); - self.last_tx_cnt_sent = self.tx_cnt; - // This packet goes to the connection. - let rx = MuxerRx::CreditUpdate { - local_port: pkt.dst_port(), - peer_port: pkt.src_port(), - fwd_cnt: self.tx_cnt.0, - }; - push_packet(self.cid, rx, &self.rxq, &self.queue, &self.mem); - update.signal_queue = true; - } - - debug!("sendmsg ret={ret}"); - update - } - - fn sendto_addr(&mut self, _req: TsiSendtoAddr) -> ProxyUpdate { - ProxyUpdate::default() - } - - fn listen( - &mut self, - pkt: &VsockPacket, - req: TsiListenReq, - host_port_map: &Option>, - ) -> ProxyUpdate { - debug!( - "listen: id={} addr={}, vm_port={} backlog={}", - self.id, req.addr, req.vm_port, req.backlog - ); - let mut update = ProxyUpdate::default(); - - let result = self.try_listen(&req, host_port_map); - - // This packet goes to the control port (DGRAM). - let rx = MuxerRx::ListenResponse { - local_port: pkt.dst_port(), - peer_port: pkt.src_port(), - result, - }; - push_packet(self.cid, rx, &self.rxq, &self.queue, &self.mem); - - if result == 0 { - self.peer_port = req.vm_port; - self.status = ProxyStatus::Listening; - update.polling = Some((self.id, self.fd.as_raw_fd(), EventSet::IN)); - } - - update - } - - fn accept(&mut self, req: TsiAcceptReq) -> ProxyUpdate { - debug!("accept: id={} flags={}", req.peer_port, req.flags); - - let mut update = ProxyUpdate::default(); - - if self.pending_accepts > 0 { - self.pending_accepts -= 1; - self.push_accept_rsp(0); - update.signal_queue = true; - } else if (req.flags & libc::O_NONBLOCK as u32) != 0 { - self.push_accept_rsp(-libc::EWOULDBLOCK); - update.signal_queue = true; - } else { - self.status = ProxyStatus::WaitingOnAccept; - } - - update - } - - fn update_peer_credit(&mut self, pkt: &VsockPacket) -> ProxyUpdate { - debug!( - "update_credit: buf_alloc={} rx_cnt={} fwd_cnt={}", - pkt.buf_alloc(), - self.rx_cnt, - pkt.fwd_cnt() - ); - self.peer_buf_alloc = pkt.buf_alloc(); - self.peer_fwd_cnt = Wrapping(pkt.fwd_cnt()); - - self.status = ProxyStatus::Connected; - - ProxyUpdate { - polling: Some((self.id, self.fd.as_raw_fd(), EventSet::IN)), - ..Default::default() - } - } - - fn push_op_request(&self) { - debug!( - "push_op_request: id={}, local_port={} peer_port={}", - self.id, self.local_port, self.peer_port - ); - - // This packet goes to the connection. - let rx = MuxerRx::OpRequest { - local_port: self.local_port, - peer_port: self.peer_port, - }; - push_packet(self.cid, rx, &self.rxq, &self.queue, &self.mem); - } - - fn process_op_response(&mut self, pkt: &VsockPacket) -> ProxyUpdate { - debug!( - "process_op_response: id={} src_port={} dst_port={}", - self.id, - pkt.src_port(), - pkt.dst_port() - ); - - self.peer_buf_alloc = pkt.buf_alloc(); - self.peer_fwd_cnt = Wrapping(pkt.fwd_cnt()); - - self.switch_to_connected(); - - ProxyUpdate { - polling: Some((self.id, self.fd.as_raw_fd(), EventSet::IN)), - push_accept: Some((self.id, self.parent_id)), - ..Default::default() - } - } - - fn enqueue_accept(&mut self) { - debug!("enqueue_accept: control_port: {}", self.control_port); - - if self.status == ProxyStatus::WaitingOnAccept { - self.status = ProxyStatus::Listening; - self.push_accept_rsp(0); - } else { - self.pending_accepts += 1; - } - } - - fn push_accept_rsp(&self, result: i32) { - debug!( - "push_accept_rsp: control_port: {}, result: {}", - self.control_port, result - ); - - // This packet goes to the control port (DGRAM). - let rx = MuxerRx::AcceptResponse { - local_port: 1030, - peer_port: self.control_port, - result, - }; - push_packet(self.cid, rx, &self.rxq, &self.queue, &self.mem); - } - - fn shutdown(&mut self, pkt: &VsockPacket) { - let recv_off = pkt.flags() & uapi::VSOCK_FLAGS_SHUTDOWN_RCV != 0; - let send_off = pkt.flags() & uapi::VSOCK_FLAGS_SHUTDOWN_SEND != 0; - - let how = if recv_off && send_off { - Shutdown::Both - } else if recv_off { - Shutdown::Read - } else { - Shutdown::Write - }; - - if let Err(e) = shutdown(self.fd.as_raw_fd(), how) { - warn!("error sending shutdown to socket: {e}"); - } - } - - fn release(&mut self) -> ProxyUpdate { - debug!( - "release: id={}, tx_cnt={}, last_tx_cnt={}", - self.id, self.tx_cnt, self.last_tx_cnt_sent - ); - let remove_proxy = if self.status == ProxyStatus::Listening { - ProxyRemoval::Immediate - } else { - ProxyRemoval::Deferred - }; - ProxyUpdate { - remove_proxy, - ..Default::default() - } - } - - fn process_event(&mut self, evset: EventSet) -> ProxyUpdate { - let mut update = ProxyUpdate::default(); - - if evset.contains(EventSet::HANG_UP) { - debug!("process_event: HANG_UP"); - if self.status == ProxyStatus::Connecting { - self.push_connect_rsp(-libc::ECONNREFUSED); - } else { - self.push_reset(); - } - - self.status = ProxyStatus::Closed; - update.polling = Some((self.id, self.fd.as_raw_fd(), EventSet::empty())); - update.signal_queue = true; - update.remove_proxy = if self.status == ProxyStatus::Listening { - ProxyRemoval::Immediate - } else { - ProxyRemoval::Deferred - }; - return update; - } - - if evset.contains(EventSet::IN) { - debug!("process_event: IN"); - if self.status == ProxyStatus::Connected { - let (signal_queue, wait_credit) = self.recv_pkt(); - update.signal_queue = signal_queue; - - if wait_credit && self.status != ProxyStatus::WaitingCreditUpdate { - self.status = ProxyStatus::WaitingCreditUpdate; - let rx = MuxerRx::CreditRequest { - local_port: self.local_port, - peer_port: self.peer_port, - fwd_cnt: self.tx_cnt.0, - }; - update.push_credit_req = Some(rx); - } - - if self.status == ProxyStatus::Closed { - debug!( - "process_event: endpoint closed, sending reset: id={}", - self.id - ); - self.push_reset(); - update.signal_queue = true; - update.polling = Some((self.id(), self.fd.as_raw_fd(), EventSet::empty())); - return update; - } else if self.status == ProxyStatus::WaitingCreditUpdate { - debug!("process_event: WaitingCreditUpdate"); - update.polling = Some((self.id(), self.fd.as_raw_fd(), EventSet::empty())); - } - } else if self.status == ProxyStatus::Listening - || self.status == ProxyStatus::WaitingOnAccept - { - match accept(self.fd.as_raw_fd()) { - Ok(accept_fd) => { - // Safe because we've just obtained the FD from the `accept` call above. - let new_fd = unsafe { OwnedFd::from_raw_fd(accept_fd) }; - update.new_proxy = - Some((self.peer_port, new_fd, self.family, NewProxyType::Tcp)); - } - Err(e) => warn!("error accepting connection: id={}, err={}", self.id, e), - }; - update.signal_queue = true; - return update; - } else { - debug!("EventSet::IN while not connected: {:?}", self.status); - } - } - - if evset.contains(EventSet::OUT) { - debug!("process_event: OUT"); - if self.status == ProxyStatus::Connecting { - self.switch_to_connected(); - self.push_connect_rsp(0); - update.signal_queue = true; - // Stop listening for events in the TCP socket until we receive - // OP_REQUEST and the vsock transport is fully established. - update.polling = Some((self.id(), self.fd.as_raw_fd(), EventSet::empty())); - } else { - debug!("EventSet::OUT while not connecting"); - } - } - - update - } -} - -impl AsRawFd for TsiStreamProxy { - fn as_raw_fd(&self) -> RawFd { - self.fd.as_raw_fd() - } -} - -impl Drop for TsiStreamProxy { - fn drop(&mut self) { - if let Some(path) = &self.unixsock_path { - _ = fs::remove_file(path); - } - } -} diff --git a/vendor/krun-devices/src/virtio/vsock/unix.rs b/vendor/krun-devices/src/virtio/vsock/unix.rs deleted file mode 100644 index 2f51c05fa..000000000 --- a/vendor/krun-devices/src/virtio/vsock/unix.rs +++ /dev/null @@ -1,721 +0,0 @@ -use super::{ - defs::{self, uapi}, - proxy::{ProxyRemoval, RecvPkt}, -}; - -use nix::errno::Errno; -use nix::fcntl::{fcntl, FcntlArg, OFlag}; -use nix::sys::socket::{ - accept, bind, connect, listen, recv, send, shutdown, socket, AddressFamily, Backlog, MsgFlags, - Shutdown, SockFlag, SockType, UnixAddr, -}; -use std::collections::HashMap; -use std::num::Wrapping; -use std::os::fd::{FromRawFd, OwnedFd}; -use std::os::unix::io::{AsRawFd, RawFd}; -use std::path::PathBuf; -use std::sync::{Arc, Mutex}; - -#[cfg(target_os = "macos")] -use super::super::linux_errno::linux_errno_raw; -use super::super::Queue as VirtQueue; -use super::muxer::{push_packet, MuxerRx}; -use super::muxer_rxq::MuxerRxQ; -use super::packet::{TsiAcceptReq, TsiConnectReq, TsiListenReq, TsiSendtoAddr, VsockPacket}; -use super::proxy::{NewProxyType, Proxy, ProxyError, ProxyStatus, ProxyUpdate}; -use utils::epoll::EventSet; - -use vm_memory::GuestMemoryMmap; - -pub struct UnixProxy { - id: u64, - cid: u64, - fd: OwnedFd, - pub status: ProxyStatus, - mem: GuestMemoryMmap, - queue: Arc>, - rxq: Arc>, - path: PathBuf, - peer_port: u32, - local_port: u32, - control_port: u32, - peer_fwd_cnt: Wrapping, - peer_buf_alloc: u32, - tx_cnt: Wrapping, - last_tx_cnt_sent: Wrapping, - push_cnt: Wrapping, - rx_cnt: Wrapping, -} - -fn proxy_fd_create(id: u64) -> Result { - let fd = socket( - AddressFamily::Unix, - SockType::Stream, - SockFlag::empty(), - None, - ) - .map_err(ProxyError::CreatingSocket)?; - - // macOS forces us to do this here instead of just using SockFlag::SOCK_NONBLOCK above. - match fcntl(&fd, FcntlArg::F_GETFL) { - Ok(flags) => match OFlag::from_bits(flags) { - Some(flags) => { - if let Err(e) = fcntl(&fd, FcntlArg::F_SETFL(flags | OFlag::O_NONBLOCK)) { - warn!("error switching to non-blocking: id={id}, err={e}"); - } - } - None => error!("invalid fd flags id={id}"), - }, - Err(e) => error!("couldn't obtain fd flags id={id}, err={e}"), - }; - - #[cfg(target_os = "macos")] - { - // nix doesn't provide an abstraction for SO_NOSIGPIPE, fall back to libc. - let option_value: libc::c_int = 1; - unsafe { - libc::setsockopt( - fd.as_raw_fd(), - libc::SOL_SOCKET, - libc::SO_NOSIGPIPE, - &option_value as *const _ as *const libc::c_void, - std::mem::size_of_val(&option_value) as libc::socklen_t, - ) - }; - } - - Ok(fd) -} - -impl UnixProxy { - #[allow(clippy::too_many_arguments)] - pub fn new( - id: u64, - cid: u64, - local_port: u32, - control_port: u32, - mem: GuestMemoryMmap, - queue: Arc>, - rxq: Arc>, - path: PathBuf, - ) -> Result { - let fd = proxy_fd_create(id)?; - - Ok(UnixProxy { - id, - cid, - local_port, - peer_port: 0, - control_port, - fd, - status: ProxyStatus::Idle, - mem, - queue, - rxq, - peer_buf_alloc: 0, - peer_fwd_cnt: Wrapping(0), - path, - tx_cnt: Wrapping(0), - last_tx_cnt_sent: Wrapping(0), - push_cnt: Wrapping(0), - rx_cnt: Wrapping(0), - }) - } - - #[allow(clippy::too_many_arguments)] - pub fn new_reverse( - id: u64, - cid: u64, - local_port: u32, - peer_port: u32, - fd: OwnedFd, - mem: GuestMemoryMmap, - queue: Arc>, - rxq: Arc>, - ) -> Self { - debug!("new_reverse: id={id} local_port={local_port} peer_port={peer_port}"); - UnixProxy { - id, - cid, - local_port, - peer_port, - control_port: 0, - fd, - status: ProxyStatus::ReverseInit, - mem, - queue, - rxq, - rx_cnt: Wrapping(0), - tx_cnt: Wrapping(0), - last_tx_cnt_sent: Wrapping(0), - peer_buf_alloc: 0, - peer_fwd_cnt: Wrapping(0), - push_cnt: Wrapping(0), - path: Default::default(), - } - } - - fn switch_to_connected(&mut self) { - self.status = ProxyStatus::Connected; - match fcntl(&self.fd, FcntlArg::F_GETFL) { - Ok(flags) => match OFlag::from_bits(flags) { - Some(flags) => { - if let Err(e) = fcntl(&self.fd, FcntlArg::F_SETFL(flags & !OFlag::O_NONBLOCK)) { - warn!("error switching to blocking: id={}, err={}", self.id, e); - } - } - None => error!("invalid fd flags id={}", self.id), - }, - Err(e) => error!("couldn't obtain fd flags id={}, err={}", self.id, e), - }; - } - - fn push_connect_rsp(&self, result: i32) { - debug!( - "push_connect_rsp: id: {}, control_port: {}, result: {}", - self.id, self.control_port, result - ); - - // This response goes to the control port (DGRAM). - let rx = MuxerRx::ConnResponse { - local_port: 1025, - peer_port: self.control_port, - result, - }; - push_packet(self.cid, rx, &self.rxq, &self.queue, &self.mem); - } - - fn push_reset(&self) { - debug!( - "push_reset: id: {}, peer_port: {}, local_port: {}", - self.id, self.peer_port, self.local_port - ); - - let rx = MuxerRx::Reset { - local_port: self.local_port, - peer_port: self.peer_port, - }; - - push_packet(self.cid, rx, &self.rxq, &self.queue, &self.mem); - } - - fn peer_avail_credit(&self) -> usize { - (Wrapping(self.peer_buf_alloc) - (self.rx_cnt - self.peer_fwd_cnt)).0 as usize - } - - fn recv_to_pkt(&self, pkt: &mut VsockPacket) -> RecvPkt { - if let Some(buf) = pkt.buf_mut() { - let peer_credit = self.peer_avail_credit(); - let max_len = std::cmp::min(buf.len(), peer_credit); - - debug!( - "recv_to_pkt: peer_avail_credit={}, buf.len={}, max_len={}", - self.peer_avail_credit(), - buf.len(), - max_len - ); - - if max_len == 0 { - return RecvPkt::WaitForCredit; - } - - match recv( - self.fd.as_raw_fd(), - &mut buf[..max_len], - MsgFlags::MSG_DONTWAIT, - ) { - Ok(cnt) => { - debug!("recv cnt={cnt}"); - if cnt > 0 { - debug!("recv rx_cnt={}", self.rx_cnt); - RecvPkt::Read(cnt) - } else { - RecvPkt::Close - } - } - Err(e) => { - debug!("recv_pkt: recv error: {e:?}"); - RecvPkt::Error - } - } - } else { - debug!("recv_pkt: pkt without buf"); - RecvPkt::Error - } - } - - fn recv_pkt(&mut self) -> (bool, bool) { - let mut have_used = false; - let mut wait_credit = false; - let mut queue = self.queue.lock().unwrap(); - - while let Some(head) = queue.pop(&self.mem) { - let len = match VsockPacket::from_rx_virtq_head(&head) { - Ok(mut pkt) => match self.recv_to_pkt(&mut pkt) { - RecvPkt::WaitForCredit => { - wait_credit = true; - 0 - } - RecvPkt::Read(cnt) => { - self.rx_cnt += Wrapping(cnt as u32); - self.init_data_pkt(&mut pkt); - pkt.set_len(cnt as u32); - pkt.hdr().len() + cnt - } - RecvPkt::Close => { - self.status = ProxyStatus::Closed; - 0 - } - RecvPkt::Error => 0, - }, - Err(e) => { - debug!("recv_pkt: RX queue error: {e:?}"); - 0 - } - }; - - if len == 0 { - queue.undo_pop(); - break; - } else { - have_used = true; - self.push_cnt += Wrapping(len as u32); - debug!( - "recv_pkt: pushing packet with {} bytes, push_cnt={}", - len, self.push_cnt - ); - if let Err(e) = queue.add_used(&self.mem, head.index, len as u32) { - error!("failed to add used elements to the queue: {e:?}"); - } - } - } - - debug!("recv_pkt: have_used={have_used}"); - (have_used, wait_credit) - } - - fn init_data_pkt(&self, pkt: &mut VsockPacket) { - debug!( - "init_data_pkt: id={}, local_port={}, peer_port={}", - self.id, self.local_port, self.peer_port - ); - - pkt.set_op(uapi::VSOCK_OP_RW) - .set_src_cid(uapi::VSOCK_HOST_CID) - .set_dst_cid(self.cid) - .set_src_port(self.local_port) - .set_dst_port(self.peer_port) - .set_type(uapi::VSOCK_TYPE_STREAM) - .set_buf_alloc(defs::CONN_TX_BUF_SIZE as u32) - .set_fwd_cnt(self.tx_cnt.0); - } -} - -impl Proxy for UnixProxy { - fn id(&self) -> u64 { - self.id - } - - fn status(&self) -> ProxyStatus { - self.status - } - - fn connect(&mut self, _pkt: &VsockPacket, _req: TsiConnectReq) -> ProxyUpdate { - let mut update = ProxyUpdate::default(); - - let addr = UnixAddr::new(&self.path).unwrap(); - - let result = match connect(self.fd.as_raw_fd(), &addr) { - Ok(()) => { - debug!("connect: Connected"); - self.switch_to_connected(); - 0 - } - Err(nix::errno::Errno::EINPROGRESS) => { - debug!("connect: Connecting"); - self.status = ProxyStatus::Connecting; - 0 - } - Err(e) => { - debug!("Error connecting: {e}"); - #[cfg(target_os = "macos")] - let errno = -linux_errno_raw(Errno::last_raw()); - #[cfg(target_os = "linux")] - let errno = -Errno::last_raw(); - errno - } - }; - - if self.status == ProxyStatus::Connecting { - update.polling = Some((self.id, self.fd.as_raw_fd(), EventSet::IN | EventSet::OUT)); - } else { - if self.status == ProxyStatus::Connected { - update.polling = Some((self.id, self.fd.as_raw_fd(), EventSet::IN)); - } - self.push_connect_rsp(result); - } - - update - } - - fn confirm_connect(&mut self, pkt: &VsockPacket) -> Option { - debug!( - "confirm_connect: local_port={} peer_port={}, src_port={}, dst_port={}", - pkt.dst_port(), - pkt.src_port(), - self.local_port, - self.peer_port, - ); - - self.peer_buf_alloc = pkt.buf_alloc(); - self.peer_fwd_cnt = Wrapping(pkt.fwd_cnt()); - - self.local_port = pkt.dst_port(); - self.peer_port = pkt.src_port(); - - // This response goes to the connection. - let rx = MuxerRx::OpResponse { - local_port: pkt.dst_port(), - peer_port: pkt.src_port(), - }; - push_packet(self.cid, rx, &self.rxq, &self.queue, &self.mem); - - None - } - - fn getpeername(&mut self, _pkt: &VsockPacket) { - todo!(); - } - - fn sendmsg(&mut self, pkt: &VsockPacket) -> ProxyUpdate { - let mut update = ProxyUpdate::default(); - - let ret = if let Some(buf) = pkt.buf() { - #[cfg(target_os = "macos")] - let flags = MsgFlags::empty(); - - #[cfg(target_os = "linux")] - let flags = MsgFlags::MSG_NOSIGNAL; - - match send(self.fd.as_raw_fd(), buf, flags) { - Ok(sent) => { - if sent != buf.len() { - error!("couldn't set everything: buf={}, sent={}", buf.len(), sent); - } - self.tx_cnt += Wrapping(sent as u32); - sent as i32 - } - Err(err) => { - #[cfg(target_os = "macos")] - let errno = -linux_errno_raw(err as i32); - - #[cfg(target_os = "linux")] - let errno = -(err as i32); - errno - } - } - } else { - -libc::EINVAL - }; - - if ret > 0 && (self.tx_cnt - self.last_tx_cnt_sent).0 >= self.peer_buf_alloc / 2 { - debug!( - "sending credit update: id={}, tx_cnt={}, last_tx_cnt={}", - self.id, self.tx_cnt, self.last_tx_cnt_sent - ); - self.last_tx_cnt_sent = self.tx_cnt; - - let rx = MuxerRx::CreditUpdate { - local_port: pkt.dst_port(), - peer_port: pkt.src_port(), - fwd_cnt: self.tx_cnt.0, - }; - - push_packet(self.cid, rx, &self.rxq, &self.queue, &self.mem); - update.signal_queue = true; - } - - debug!("sendmsg ret={ret}"); - - update - } - - fn sendto_addr(&mut self, _req: TsiSendtoAddr) -> ProxyUpdate { - todo!(); - } - - fn listen( - &mut self, - _pkt: &VsockPacket, - _req: TsiListenReq, - _host_port_map: &Option>, - ) -> ProxyUpdate { - todo!(); - } - - fn accept(&mut self, _req: TsiAcceptReq) -> ProxyUpdate { - todo!(); - } - - fn update_peer_credit(&mut self, pkt: &VsockPacket) -> ProxyUpdate { - debug!( - "update_credit: buf_alloc={} rx_cnt={} fwd_cnt={}", - pkt.buf_alloc(), - self.rx_cnt, - pkt.fwd_cnt() - ); - self.peer_buf_alloc = pkt.buf_alloc(); - self.peer_fwd_cnt = Wrapping(pkt.fwd_cnt()); - - self.status = ProxyStatus::Connected; - - ProxyUpdate { - polling: Some((self.id, self.fd.as_raw_fd(), EventSet::IN)), - ..Default::default() - } - } - - fn push_op_request(&self) { - debug!( - "push_op_request: id={}, local_port={} peer_port={}", - self.id, self.local_port, self.peer_port - ); - - // This packet goes to the connection. - let rx = MuxerRx::OpRequest { - local_port: self.local_port, - peer_port: self.peer_port, - }; - push_packet(self.cid, rx, &self.rxq, &self.queue, &self.mem); - } - - fn process_op_response(&mut self, pkt: &VsockPacket) -> ProxyUpdate { - debug!( - "process_op_response: id={} src_port={} dst_port={}", - self.id, - pkt.src_port(), - pkt.dst_port() - ); - - self.peer_buf_alloc = pkt.buf_alloc(); - self.peer_fwd_cnt = Wrapping(pkt.fwd_cnt()); - - self.switch_to_connected(); - - ProxyUpdate { - polling: Some((self.id, self.fd.as_raw_fd(), EventSet::IN)), - ..Default::default() - } - } - - fn enqueue_accept(&mut self) { - todo!(); - } - - fn shutdown(&mut self, pkt: &VsockPacket) { - let recv_off = pkt.flags() & uapi::VSOCK_FLAGS_SHUTDOWN_RCV != 0; - let send_off = pkt.flags() & uapi::VSOCK_FLAGS_SHUTDOWN_SEND != 0; - - let how = if recv_off && send_off { - Shutdown::Both - } else if recv_off { - Shutdown::Read - } else { - Shutdown::Write - }; - - if let Err(e) = shutdown(self.fd.as_raw_fd(), how) { - warn!("error sending shutdown to socket: {e}"); - } - } - - fn release(&mut self) -> ProxyUpdate { - debug!( - "release: id={}, tx_cnt={}, last_tx_cnt={}", - self.id, self.tx_cnt, self.last_tx_cnt_sent - ); - let remove_proxy = ProxyRemoval::Deferred; - - ProxyUpdate { - remove_proxy, - ..Default::default() - } - } - - fn process_event(&mut self, evset: EventSet) -> ProxyUpdate { - let mut update = ProxyUpdate::default(); - - if evset.contains(EventSet::HANG_UP) { - debug!("process_event: HANG_UP"); - - if self.status == ProxyStatus::Connecting { - self.push_connect_rsp(-libc::ECONNREFUSED); - } else { - self.push_reset(); - } - - self.status = ProxyStatus::Closed; - update.polling = Some((self.id, self.fd.as_raw_fd(), EventSet::empty())); - update.signal_queue = true; - update.remove_proxy = ProxyRemoval::Deferred; - - return update; - } - - if evset.contains(EventSet::IN) { - debug!("process_event: IN"); - if self.status == ProxyStatus::Connected { - let (signal_queue, wait_credit) = self.recv_pkt(); - update.signal_queue = signal_queue; - - if wait_credit && self.status != ProxyStatus::WaitingCreditUpdate { - self.status = ProxyStatus::WaitingCreditUpdate; - let rx = MuxerRx::CreditRequest { - local_port: self.local_port, - peer_port: self.peer_port, - fwd_cnt: self.tx_cnt.0, - }; - update.push_credit_req = Some(rx); - } - - if self.status == ProxyStatus::Closed { - debug!( - "process_event: endpoint closed, sending reset: id={}", - self.id - ); - - self.push_reset(); - update.signal_queue = true; - update.polling = Some((self.id(), self.fd.as_raw_fd(), EventSet::empty())); - return update; - } else if self.status == ProxyStatus::WaitingCreditUpdate { - debug!("process_event: WaitingCreditUpdate"); - update.polling = Some((self.id(), self.fd.as_raw_fd(), EventSet::empty())); - } - } else { - debug!("EventSet::IN while not connected: {:?}", self.status); - } - } - - if evset.contains(EventSet::OUT) { - debug!("process_event: OUT"); - if self.status == ProxyStatus::Connecting { - self.switch_to_connected(); - self.push_connect_rsp(0); - update.signal_queue = true; - update.polling = Some((self.id(), self.fd.as_raw_fd(), EventSet::IN)); - } else { - error!("EventSet::OUT while not connecting"); - } - } - - update - } -} - -impl AsRawFd for UnixProxy { - fn as_raw_fd(&self) -> RawFd { - self.fd.as_raw_fd() - } -} - -pub struct UnixAcceptorProxy { - id: u64, - fd: OwnedFd, - peer_port: u32, -} - -impl UnixAcceptorProxy { - pub fn new(id: u64, path: &PathBuf, peer_port: u32) -> Result { - let fd = socket( - AddressFamily::Unix, - SockType::Stream, - SockFlag::empty(), - None, - ) - .map_err(ProxyError::CreatingSocket)?; - bind( - fd.as_raw_fd(), - &UnixAddr::new(path).map_err(ProxyError::CreatingSocket)?, - ) - .map_err(ProxyError::CreatingSocket)?; - listen(&fd, Backlog::new(5).map_err(ProxyError::CreatingSocket)?) - .map_err(ProxyError::CreatingSocket)?; - Ok(UnixAcceptorProxy { id, fd, peer_port }) - } -} - -impl Proxy for UnixAcceptorProxy { - fn id(&self) -> u64 { - self.id - } - fn status(&self) -> ProxyStatus { - ProxyStatus::WaitingOnAccept - } - fn connect(&mut self, _: &VsockPacket, _: TsiConnectReq) -> ProxyUpdate { - unreachable!() - } - fn getpeername(&mut self, _: &VsockPacket) { - unreachable!() - } - fn sendmsg(&mut self, _: &VsockPacket) -> ProxyUpdate { - unreachable!() - } - fn sendto_addr(&mut self, _: TsiSendtoAddr) -> ProxyUpdate { - unreachable!() - } - fn listen( - &mut self, - _: &VsockPacket, - _: TsiListenReq, - _: &Option>, - ) -> ProxyUpdate { - unreachable!() - } - fn accept(&mut self, _: TsiAcceptReq) -> ProxyUpdate { - unreachable!() - } - fn update_peer_credit(&mut self, _: &VsockPacket) -> ProxyUpdate { - unreachable!() - } - fn process_op_response(&mut self, _: &VsockPacket) -> ProxyUpdate { - unreachable!() - } - fn release(&mut self) -> ProxyUpdate { - unreachable!() - } - fn process_event(&mut self, evset: EventSet) -> ProxyUpdate { - let mut update = ProxyUpdate::default(); - - if evset.contains(EventSet::HANG_UP) { - debug!("process_event: HANG_UP"); - update.polling = Some((self.id, self.fd.as_raw_fd(), EventSet::empty())); - update.signal_queue = true; - update.remove_proxy = ProxyRemoval::Deferred; - return update; - } - if evset.contains(EventSet::IN) { - match accept(self.fd.as_raw_fd()) { - Ok(accept_fd) => { - // Safe because we've just obtained the FD from the `accept` call above. - let new_fd = unsafe { OwnedFd::from_raw_fd(accept_fd) }; - update.new_proxy = Some(( - self.peer_port, - new_fd, - AddressFamily::Unix, - NewProxyType::Unix, - )); - } - Err(e) => warn!("error accepting connection: id={}, err={}", self.id, e), - }; - update.signal_queue = true; - } - update - } -} - -impl AsRawFd for UnixAcceptorProxy { - fn as_raw_fd(&self) -> RawFd { - self.fd.as_raw_fd() - } -} diff --git a/vendor/krun-init-blob/.cargo-ok b/vendor/krun-init-blob/.cargo-ok deleted file mode 100644 index 5f8b79583..000000000 --- a/vendor/krun-init-blob/.cargo-ok +++ /dev/null @@ -1 +0,0 @@ -{"v":1} \ No newline at end of file diff --git a/vendor/krun-init-blob/.cargo_vcs_info.json b/vendor/krun-init-blob/.cargo_vcs_info.json deleted file mode 100644 index dae46d803..000000000 --- a/vendor/krun-init-blob/.cargo_vcs_info.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "git": { - "sha1": "654e4a6045858d5ced90efae106e91d0eca3469f" - }, - "path_in_vcs": "src/init_blob" -} \ No newline at end of file diff --git a/vendor/krun-init-blob/Cargo.lock b/vendor/krun-init-blob/Cargo.lock deleted file mode 100644 index 1c3900533..000000000 --- a/vendor/krun-init-blob/Cargo.lock +++ /dev/null @@ -1,7 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "krun-init-blob" -version = "0.1.0-1.19.3" diff --git a/vendor/krun-init-blob/Cargo.toml b/vendor/krun-init-blob/Cargo.toml deleted file mode 100644 index a993cbd2e..000000000 --- a/vendor/krun-init-blob/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO -# -# When uploading crates to the registry Cargo will automatically -# "normalize" Cargo.toml files for maximal compatibility -# with all versions of Cargo and also rewrite `path` dependencies -# to registry (e.g., crates.io) dependencies. -# -# If you are reading this file be aware that the original Cargo.toml -# will likely look very different (and much more reasonable). -# See Cargo.toml.orig for the original contents. - -[package] -edition = "2024" -name = "krun-init-blob" -version = "0.1.0-1.19.3" -build = "build.rs" -autolib = false -autobins = false -autoexamples = false -autotests = false -autobenches = false -description = "Default init binary blob for libkrun guests" -readme = false -license = "Apache-2.0" -repository = "https://github.com/containers/libkrun" -resolver = "2" - -[lib] -name = "krun_init_blob" -path = "src/lib.rs" diff --git a/vendor/krun-init-blob/Cargo.toml.orig b/vendor/krun-init-blob/Cargo.toml.orig deleted file mode 100644 index 64b7a266c..000000000 --- a/vendor/krun-init-blob/Cargo.toml.orig +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "krun-init-blob" -version = "0.1.0-1.19.3" -edition = "2024" -description = "Default init binary blob for libkrun guests" -license = "Apache-2.0" -repository = "https://github.com/containers/libkrun" -build = "build.rs" - -[lib] -path = "src/lib.rs" diff --git a/vendor/krun-init-blob/build.rs b/vendor/krun-init-blob/build.rs deleted file mode 100644 index abb46947e..000000000 --- a/vendor/krun-init-blob/build.rs +++ /dev/null @@ -1,68 +0,0 @@ -use std::ffi::OsStr; -use std::path::PathBuf; -use std::process::Command; - -fn build_default_init() -> PathBuf { - let manifest_dir = PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap()); - let init_dir = manifest_dir.join("init"); - let init_src = init_dir.join("init.c"); - let dhcp_src = init_dir.join("dhcp.c"); - - let out_dir = PathBuf::from(std::env::var_os("OUT_DIR").unwrap()); - let init_bin = out_dir.join("init"); - - println!("cargo:rerun-if-env-changed=CC_LINUX"); - println!("cargo:rerun-if-env-changed=CC"); - println!("cargo:rerun-if-env-changed=TIMESYNC"); - println!("cargo:rerun-if-changed={}", init_src.display()); - println!("cargo:rerun-if-changed={}", dhcp_src.display()); - println!( - "cargo:rerun-if-changed={}", - init_dir.join("jsmn.h").display() - ); - println!( - "cargo:rerun-if-changed={}", - init_dir.join("dhcp.h").display() - ); - - let mut init_cc_flags = vec!["-O2", "-static", "-Wall"]; - if std::env::var_os("TIMESYNC").as_deref() == Some(OsStr::new("1")) { - init_cc_flags.push("-D__TIMESYNC__"); - } - - let cc_value = std::env::var("CC_LINUX") - .or_else(|_| std::env::var("CC")) - .unwrap_or_else(|_| "cc".to_string()); - let mut cc_parts = cc_value.split_ascii_whitespace(); - let cc = cc_parts.next().expect("CC_LINUX/CC must not be empty"); - let status = Command::new(cc) - .args(cc_parts) - .args(&init_cc_flags) - .arg("-o") - .arg(&init_bin) - .arg(&init_src) - .arg(&dhcp_src) - .status() - .unwrap_or_else(|e| panic!("failed to execute {cc}: {e}")); - - if !status.success() { - panic!("failed to compile {}: {status}", init_src.display()); - } - init_bin -} - -fn main() { - let init_binary_path = std::env::var_os("KRUN_INIT_BINARY_PATH") - .map(PathBuf::from) - .unwrap_or_else(|| { - let init_path = build_default_init(); - // SAFETY: The build script is single threaded. - unsafe { std::env::set_var("KRUN_INIT_BINARY_PATH", &init_path) }; - init_path - }); - println!( - "cargo:rustc-env=KRUN_INIT_BINARY_PATH={}", - init_binary_path.display() - ); - println!("cargo:rerun-if-env-changed=KRUN_INIT_BINARY_PATH"); -} diff --git a/vendor/krun-init-blob/init/dhcp.c b/vendor/krun-init-blob/init/dhcp.c deleted file mode 100644 index d89b5e9db..000000000 --- a/vendor/krun-init-blob/init/dhcp.c +++ /dev/null @@ -1,634 +0,0 @@ -/* - * DHCP Client Implementation - * - * Standalone DHCP client for configuring IPv4 network interfaces. - * Translated from Rust implementation in muvm/src/guest/net.rs - */ - -#include "dhcp.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#ifdef __linux__ -/* - * glibc before ~2.24 (manylinux2014 / CentOS 7) cannot include both - * and : they both define IFF_* and struct ifreq. - * libc already provides the ioctl types we need; skip the UAPI header. - */ -#define _LINUX_IF_H -#endif -#include -#include -#include -#include - -#define DHCP_BUFFER_SIZE 576 -#define DHCP_MSG_OFFER 2 -#define DHCP_MSG_ACK 5 - -/* Helper function to send netlink message */ -static int nl_send(int sock, struct nlmsghdr *nlh) -{ - struct sockaddr_nl sa = { - .nl_family = AF_NETLINK, - }; - - struct iovec iov = { - .iov_base = nlh, - .iov_len = nlh->nlmsg_len, - }; - - struct msghdr msg = { - .msg_name = &sa, - .msg_namelen = sizeof(sa), - .msg_iov = &iov, - .msg_iovlen = 1, - }; - - return sendmsg(sock, &msg, 0); -} - -/* Helper function to receive netlink response */ -static int nl_recv(int sock, char *buf, size_t len) -{ - struct sockaddr_nl sa; - struct iovec iov = { - .iov_base = buf, - .iov_len = len, - }; - - struct msghdr msg = { - .msg_name = &sa, - .msg_namelen = sizeof(sa), - .msg_iov = &iov, - .msg_iovlen = 1, - }; - - return recvmsg(sock, &msg, 0); -} - -/* Add routing attribute to netlink message */ -static void add_rtattr(struct nlmsghdr *nlh, int type, const void *data, - int len) -{ - int rtalen = RTA_SPACE(len); - struct rtattr *rta = - (struct rtattr *)(((char *)nlh) + NLMSG_ALIGN(nlh->nlmsg_len)); - rta->rta_type = type; - rta->rta_len = RTA_LENGTH(len); - memcpy(RTA_DATA(rta), data, len); - nlh->nlmsg_len = NLMSG_ALIGN(nlh->nlmsg_len) + rtalen; -} - -/* Set MTU */ -static int set_mtu(int nl_sock, int iface_index, unsigned int mtu) -{ - char buf[4096]; - struct nlmsghdr *nlh; - struct nlmsgerr *err; - struct ifinfomsg *ifi; - - memset(buf, 0, sizeof(buf)); - nlh = (struct nlmsghdr *)buf; - nlh->nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)); - nlh->nlmsg_type = RTM_NEWLINK; - nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; - nlh->nlmsg_seq = 1; - nlh->nlmsg_pid = getpid(); - - ifi = (struct ifinfomsg *)NLMSG_DATA(nlh); - ifi->ifi_family = AF_UNSPEC; - ifi->ifi_type = ARPHRD_ETHER; - ifi->ifi_index = iface_index; - - add_rtattr(nlh, IFLA_MTU, &mtu, sizeof(mtu)); - - if (nl_send(nl_sock, nlh) < 0) { - perror("nl_send failed for set_mtu"); - return -1; - } - - /* Receive ACK */ - int len = nl_recv(nl_sock, buf, sizeof(buf)); - if (len < (int)NLMSG_LENGTH(sizeof(struct nlmsgerr))) { - perror("nl_recv failed for set_mtu"); - return -1; - } - - if (nlh->nlmsg_type != NLMSG_ERROR) { - printf("netlink didn't return a valid answer for set_mtu\n"); - return -1; - } - - err = (struct nlmsgerr *)NLMSG_DATA(nlh); - if (err->error != 0) { - printf("netlink returned an error for set_mtu: %d\n", err->error); - return -1; - } - - return 0; -} - -/* Add or delete IPv4 route */ -static int mod_route4(int nl_sock, int iface_index, int cmd, struct in_addr gw) -{ - char buf[4096]; - struct nlmsghdr *nlh; - struct nlmsgerr *err; - struct rtmsg *rtm; - struct in_addr dst = {.s_addr = INADDR_ANY}; - - memset(buf, 0, sizeof(buf)); - nlh = (struct nlmsghdr *)buf; - nlh->nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); - nlh->nlmsg_type = cmd; - nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_ACK; - nlh->nlmsg_seq = 1; - nlh->nlmsg_pid = getpid(); - - rtm = (struct rtmsg *)NLMSG_DATA(nlh); - rtm->rtm_family = AF_INET; - rtm->rtm_dst_len = 0; - rtm->rtm_src_len = 0; - rtm->rtm_tos = 0; - rtm->rtm_table = RT_TABLE_MAIN; - rtm->rtm_protocol = RTPROT_BOOT; - rtm->rtm_scope = RT_SCOPE_UNIVERSE; - rtm->rtm_type = RTN_UNICAST; - rtm->rtm_flags = 0; - - add_rtattr(nlh, RTA_OIF, &iface_index, sizeof(iface_index)); - add_rtattr(nlh, RTA_DST, &dst, sizeof(dst)); - add_rtattr(nlh, RTA_GATEWAY, &gw, sizeof(gw)); - - if (nl_send(nl_sock, nlh) < 0) { - perror("nl_send failed for mod_route4"); - return -1; - } - - /* Receive ACK */ - int len = nl_recv(nl_sock, buf, sizeof(buf)); - if (len < (int)NLMSG_LENGTH(sizeof(struct nlmsgerr))) { - perror("nl_recv failed for mod_route4"); - return -1; - } - - if (nlh->nlmsg_type != NLMSG_ERROR) { - printf("netlink didn't return a valid answer for mod_route4\n"); - return -1; - } - - err = (struct nlmsgerr *)NLMSG_DATA(nlh); - if (err->error != 0) { - printf("netlink returned an error for mod_route4: %d\n", err->error); - return -1; - } - - return 0; -} - -/* Add or delete IPv4 address */ -static int mod_addr4(int nl_sock, int iface_index, int cmd, struct in_addr addr, - unsigned char prefix_len) -{ - char buf[4096]; - struct nlmsghdr *nlh; - struct nlmsgerr *err; - struct ifaddrmsg *ifa; - - memset(buf, 0, sizeof(buf)); - nlh = (struct nlmsghdr *)buf; - nlh->nlmsg_len = NLMSG_LENGTH(sizeof(struct ifaddrmsg)); - nlh->nlmsg_type = cmd; - nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_ACK; - nlh->nlmsg_seq = 1; - nlh->nlmsg_pid = getpid(); - - ifa = (struct ifaddrmsg *)NLMSG_DATA(nlh); - ifa->ifa_family = AF_INET; - ifa->ifa_prefixlen = prefix_len; - ifa->ifa_flags = 0; - ifa->ifa_scope = RT_SCOPE_UNIVERSE; - ifa->ifa_index = iface_index; - - add_rtattr(nlh, IFA_LOCAL, &addr, sizeof(addr)); - add_rtattr(nlh, IFA_ADDRESS, &addr, sizeof(addr)); - - if (nl_send(nl_sock, nlh) < 0) { - perror("nl_send failed for mod_addr4"); - return -1; - } - - /* Receive ACK */ - int len = nl_recv(nl_sock, buf, sizeof(buf)); - if (len < (int)NLMSG_LENGTH(sizeof(struct nlmsgerr))) { - perror("nl_recv failed for mod_addr4"); - return -1; - } - - if (nlh->nlmsg_type != NLMSG_ERROR) { - printf("netlink didn't return a valid answer for mod_addr4\n"); - return -1; - } - - err = (struct nlmsgerr *)NLMSG_DATA(nlh); - if (err->error != 0) { - printf("netlink returned an error for mod_addr4: %d\n", err->error); - return -1; - } - - return 0; -} - -/* Count leading ones in a 32-bit value */ -static unsigned char count_leading_ones(uint32_t val) -{ - unsigned char count = 0; - for (int i = 31; i >= 0; i--) { - if (val & (1U << i)) { - count++; - } else { - break; - } - } - return count; -} - -/* Return the DHCP message type (option 53) from a response, or 0 */ -static unsigned char get_dhcp_msg_type(const unsigned char *response, - ssize_t len) -{ - /* Walk DHCP options (TLV chain starting after the magic cookie) */ - size_t p = 240; - while (p < (size_t)len) { - unsigned char opt = response[p]; - - if (opt == 0xff) /* end */ - break; - if (opt == 0) { /* padding */ - p++; - continue; - } - - if (p + 1 >= (size_t)len) - break; - - unsigned char opt_len = response[p + 1]; - p += 2; - - if (p + opt_len > (size_t)len) - break; - if (opt == 53 && opt_len >= 1) /* Message Type */ - return response[p]; - - p += opt_len; - } - return 0; -} - -/* Parse a DHCP ACK and configure the interface. Returns 0 or -1 on error. */ -static int handle_dhcp_ack(int nl_sock, int iface_index, - const unsigned char *response, ssize_t len) -{ - FILE *resolv = NULL; - bool tried_opening_resolv = false; - - /* Need at least 240 bytes (DHCP header + magic cookie) + 1 for options */ - if (len < 241) { - printf("DHCPACK too short (%zd bytes)\n", len); - return -1; - } - - /* Parse DHCP response */ - struct in_addr addr; - /* yiaddr is at offset 16-19 in network byte order */ - memcpy(&addr.s_addr, &response[16], sizeof(addr.s_addr)); - - if (addr.s_addr == INADDR_ANY) { - printf("DHCPACK has no address (yiaddr is 0.0.0.0)\n"); - return -1; - } - - struct in_addr netmask = {.s_addr = INADDR_ANY}; - struct in_addr router = {.s_addr = INADDR_ANY}; - /* Clamp MTU to passt's limit */ - uint16_t mtu = 65520; - - /* Parse DHCP options (start at offset 240 after magic cookie) */ - size_t p = 240; - while (p < (size_t)len) { - unsigned char opt = response[p]; - - if (opt == 0xff) { - /* Option 255: End (of options) */ - break; - } - - if (opt == 0) { /* Padding */ - p++; - continue; - } - - if (p + 1 >= (size_t)len) - break; - - unsigned char opt_len = response[p + 1]; - p += 2; /* Length doesn't include code and length field itself */ - - if (p + opt_len > (size_t)len) { - /* Malformed packet, option length exceeds packet boundary */ - break; - } - - if (opt == 1 && opt_len >= 4) { - /* Option 1: Subnet Mask */ - memcpy(&netmask.s_addr, &response[p], sizeof(netmask.s_addr)); - } else if (opt == 3 && opt_len >= 4) { - /* Option 3: Router */ - memcpy(&router.s_addr, &response[p], sizeof(router.s_addr)); - } else if (opt == 6 && opt_len >= 4) { - /* Option 6: Domain Name Server */ - if (!resolv && !tried_opening_resolv) { - tried_opening_resolv = true; - resolv = fopen("/etc/resolv.conf", "w"); - if (!resolv) { - perror("Failed to open /etc/resolv.conf"); - } - } - - if (resolv) { - for (int dns_p = p; dns_p + 4 <= p + opt_len; dns_p += 4) { - fprintf(resolv, "nameserver %d.%d.%d.%d\n", response[dns_p], - response[dns_p + 1], response[dns_p + 2], - response[dns_p + 3]); - } - } - } else if (opt == 26 && opt_len >= 2) { - /* Option 26: Interface MTU */ - mtu = (response[p] << 8) | response[p + 1]; - - /* We don't know yet if IPv6 is available: don't go below 1280 B - */ - if (mtu < 1280) - mtu = 1280; - if (mtu > 65520) - mtu = 65520; - } - - p += opt_len; - } - - if (resolv) { - fclose(resolv); - } - - /* Calculate prefix length from netmask */ - unsigned char prefix_len = count_leading_ones(ntohl(netmask.s_addr)); - - if (mod_addr4(nl_sock, iface_index, RTM_NEWADDR, addr, prefix_len) != 0) { - printf("couldn't add the address provided by the DHCP server\n"); - return -1; - } - if (mod_route4(nl_sock, iface_index, RTM_NEWROUTE, router) != 0) { - printf("couldn't add the default route provided by the DHCP server\n"); - return -1; - } - set_mtu(nl_sock, iface_index, mtu); - return 0; -} - -/* Send DISCOVER with Rapid Commit, process ACK, configure address and route */ -int do_dhcp(const char *iface) -{ - struct sockaddr_in bind_addr, dest_addr; - struct dhcp_packet request = {0}; - unsigned char response[DHCP_BUFFER_SIZE]; - struct timeval timeout; - int iface_index; - int broadcast = 1; - int nl_sock = -1; - int sock = -1; - int ret = -1; - - iface_index = if_nametoindex(iface); - if (iface_index == 0) { - perror("Failed to find index for network interface"); - return ret; - } - - nl_sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); - if (nl_sock < 0) { - perror("Failed to create netlink socket"); - return ret; - } - - struct sockaddr_nl sa = { - .nl_family = AF_NETLINK, - .nl_pid = getpid(), - .nl_groups = 0, - }; - - if (bind(nl_sock, (struct sockaddr *)&sa, sizeof(sa)) < 0) { - perror("Failed to bind netlink socket"); - goto cleanup; - } - - /* Send request (DHCPDISCOVER) */ - sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - if (sock < 0) { - perror("socket failed"); - goto cleanup; - } - - /* Allow broadcast */ - if (setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &broadcast, - sizeof(broadcast)) < 0) { - perror("setsockopt SO_BROADCAST failed"); - goto cleanup; - } - - if (setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, iface, - strlen(iface) + 1) < 0) { - perror("setsockopt SO_BINDTODEVICE failed"); - goto cleanup; - } - - /* Bind to port 68 (DHCP client) */ - memset(&bind_addr, 0, sizeof(bind_addr)); - bind_addr.sin_family = AF_INET; - bind_addr.sin_port = htons(68); - bind_addr.sin_addr.s_addr = INADDR_ANY; - - if (bind(sock, (struct sockaddr *)&bind_addr, sizeof(bind_addr)) < 0) { - perror("bind failed"); - goto cleanup; - } - - request.op = 1; /* BOOTREQUEST */ - request.htype = 1; /* Hardware address type: Ethernet */ - request.hlen = 6; /* Hardware address length */ - request.hops = 0; /* DHCP relay Hops */ - request.xid = - htonl(getpid()); /* Transaction ID: use PID for some randomness */ - request.secs = - 0; /* Seconds elapsed since beginning of acquisition or renewal */ - request.flags = htons(0x8000); /* DHCP message flags: Broadcast */ - request.ciaddr = 0; /* Client IP address (not set yet) */ - request.yiaddr = 0; /* 'your' IP address (server will fill) */ - request.siaddr = 0; /* Server IP address (not set) */ - request.giaddr = 0; /* Relay agent IP address (not set) */ - request.magic = htonl(0x63825363); /* Magic cookie */ - - /* Populate chaddr with the interface's MAC address */ - struct ifreq mac_ifr; - memset(&mac_ifr, 0, sizeof(mac_ifr)); - strncpy(mac_ifr.ifr_name, iface, IFNAMSIZ); - - if (ioctl(sock, SIOCGIFHWADDR, &mac_ifr) < 0) { - perror("ioctl(SIOCGIFHWADDR) failed"); - goto cleanup; - } - memcpy(request.chaddr, mac_ifr.ifr_hwaddr.sa_data, 6); - - /* Build DHCP options */ - int opt_offset = 0; - - /* Option 53: DHCP Message Type = DISCOVER (1) */ - request.options[opt_offset++] = 53; - request.options[opt_offset++] = 1; - request.options[opt_offset++] = 1; - - /* Option 80: Rapid Commit (RFC 4039) */ - request.options[opt_offset++] = 80; - request.options[opt_offset++] = 0; - - /* Option 255: End of options */ - request.options[opt_offset++] = 0xff; - - /* Remaining bytes are padding (up to 300 bytes) */ - - /* Send DHCP DISCOVER */ - memset(&dest_addr, 0, sizeof(dest_addr)); - dest_addr.sin_family = AF_INET; - dest_addr.sin_port = htons(67); - dest_addr.sin_addr.s_addr = INADDR_BROADCAST; - - if (sendto(sock, &request, sizeof(request), 0, - (struct sockaddr *)&dest_addr, sizeof(dest_addr)) < 0) { - perror("sendto failed"); - goto cleanup; - } - - /* Keep IPv6-only fast: set receive timeout to 100ms */ - timeout.tv_sec = 0; - timeout.tv_usec = 100000; - if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < - 0) { - perror("setsockopt SO_RCVTIMEO failed"); - goto cleanup; - } - - /* Get response: DHCPACK (Rapid Commit) or DHCPOFFER */ - struct sockaddr_in from_addr; - socklen_t from_len = sizeof(from_addr); - ssize_t len = recvfrom(sock, response, sizeof(response), 0, - (struct sockaddr *)&from_addr, &from_len); - - if (len <= 0) - goto done; /* No DHCP response — not an error, VM may be IPv6-only */ - - unsigned char msg_type = get_dhcp_msg_type(response, len); - - if (msg_type == DHCP_MSG_ACK) { - /* Rapid Commit — server sent ACK directly */ - close(sock); - sock = -1; - if (handle_dhcp_ack(nl_sock, iface_index, response, len) != 0) - goto cleanup; - } else if (msg_type == DHCP_MSG_OFFER) { - /* - * DHCPOFFER — complete the 4-way handshake by sending DHCPREQUEST - * and waiting for DHCPACK. Servers without Rapid Commit (e.g. - * gvproxy) require this. - */ - struct in_addr offered_addr; - memcpy(&offered_addr.s_addr, &response[16], - sizeof(offered_addr.s_addr)); - - /* Build DHCPREQUEST */ - memset(request.options, 0, sizeof(request.options)); - opt_offset = 0; - - /* Option 53: DHCP Message Type = REQUEST (3) */ - request.options[opt_offset++] = 53; - request.options[opt_offset++] = 1; - request.options[opt_offset++] = 3; - - /* Option 50: Requested IP Address */ - request.options[opt_offset++] = 50; - request.options[opt_offset++] = 4; - memcpy(&request.options[opt_offset], &offered_addr.s_addr, 4); - opt_offset += 4; - - /* Option 54: Server Identifier (from_addr) */ - request.options[opt_offset++] = 54; - request.options[opt_offset++] = 4; - memcpy(&request.options[opt_offset], &from_addr.sin_addr.s_addr, 4); - opt_offset += 4; - - /* Option 255: End */ - request.options[opt_offset++] = 0xff; - - if (sendto(sock, &request, sizeof(request), 0, - (struct sockaddr *)&dest_addr, sizeof(dest_addr)) < 0) { - perror("sendto DHCPREQUEST failed"); - goto cleanup; - } - - from_len = sizeof(from_addr); - len = recvfrom(sock, response, sizeof(response), 0, - (struct sockaddr *)&from_addr, &from_len); - - close(sock); - sock = -1; - - if (len <= 0) { - printf("no DHCPACK received\n"); - goto cleanup; - } - - if (get_dhcp_msg_type(response, len) != DHCP_MSG_ACK) { - printf("expected DHCPACK but got message type %d\n", - get_dhcp_msg_type(response, len)); - goto cleanup; - } - - if (handle_dhcp_ack(nl_sock, iface_index, response, len) != 0) - goto cleanup; - } else { - printf("unexpected DHCP message type %d\n", msg_type); - goto cleanup; - } - -done: - ret = 0; -cleanup: - if (sock >= 0) { - close(sock); - } - if (nl_sock >= 0) { - close(nl_sock); - } - return ret; -} diff --git a/vendor/krun-init-blob/init/dhcp.h b/vendor/krun-init-blob/init/dhcp.h deleted file mode 100644 index 39e20ead7..000000000 --- a/vendor/krun-init-blob/init/dhcp.h +++ /dev/null @@ -1,61 +0,0 @@ -/* - * DHCP Client Implementation - * - * Standalone DHCP client for configuring IPv4 network interfaces. - * Translated from Rust implementation in muvm/src/guest/net.rs - */ - -#ifndef DHCP_H -#define DHCP_H - -#include - -/* BOOTP vendor-specific area size (64) - magic cookie (4) */ -#define DHCP_OPTIONS_SIZE 60 - -/* DHCP packet structure (RFC 2131) */ -struct dhcp_packet { - uint8_t op; /* Message op code / message type (1 = BOOTREQUEST) */ - uint8_t htype; /* Hardware address type (1 = Ethernet) */ - uint8_t hlen; /* Hardware address length (6 for Ethernet) */ - uint8_t hops; /* Client sets to zero */ - uint32_t xid; /* Transaction ID */ - uint16_t secs; /* Seconds elapsed since client began address acquisition */ - uint16_t flags; /* Flags (0x8000 = Broadcast) */ - uint32_t ciaddr; /* Client IP address */ - uint32_t yiaddr; /* 'your' (client) IP address */ - uint32_t siaddr; /* IP address of next server to use in bootstrap */ - uint32_t giaddr; /* Relay agent IP address */ - uint8_t chaddr[16]; /* Client hardware address */ - uint8_t sname[64]; /* Optional server host name */ - uint8_t file[128]; /* Boot file name */ - uint32_t magic; /* Magic cookie (0x63825363) */ - uint8_t options[DHCP_OPTIONS_SIZE]; /* Options field */ -} __attribute__((packed)); - -/* - * Perform DHCP discovery and configuration for a network interface - * - * This function: - * 1. Binds a UDP socket to the interface using SO_BINDTODEVICE - * 2. Sends a DHCP DISCOVER message with Rapid Commit option - * 3. Waits up to 100ms for a response: - * - If DHCPACK (Rapid Commit): applies configuration directly - * - If DHCPOFFER: sends DHCPREQUEST and waits for DHCPACK - * - If no response: returns success (VM may be IPv6-only) - * 4. Parses the ACK and configures: - * - IPv4 address with appropriate prefix length - * - Default gateway route - * - DNS servers (overwriting /etc/resolv.conf) - * - Interface MTU - * - * Parameters: - * iface - The name of the network interface to be configured. - * - * Returns: - * 0 on success (whether or not DHCP response was received) - * -1 on error - */ -int do_dhcp(const char *iface); - -#endif /* DHCP_H */ diff --git a/vendor/krun-init-blob/init/init.c b/vendor/krun-init-blob/init/init.c deleted file mode 100644 index 877848322..000000000 --- a/vendor/krun-init-blob/init/init.c +++ /dev/null @@ -1,1578 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#if __FreeBSD__ -#include -#include -#include -#else -#include -#endif -#include -#include -#include - -#if __linux__ -#include -#include -#include -#endif - -#include "dhcp.h" -#include "jsmn.h" - -#ifdef SEV -#include "tee/snp_attest.h" -#endif - -#define KRUN_EXIT_CODE_IOCTL 0x7602 - -#define KRUN_MAGIC "KRUN" -#define KRUN_FOOTER_LEN 12 -#define CMDLINE_SECRET_PATH "/sfs/secrets/coco/cmdline" -#define CONFIG_FILE_PATH "/.krun_config.json" -#define MAX_PASS_SIZE 512 -#define MAX_TOKENS 16384 - -static int jsoneq(const char *, jsmntok_t *, const char *); - -#ifdef SEV -static char *sev_get_luks_passphrase(int *); -static char *snp_get_luks_passphrase(char *, char *, char *, int *); -#endif - -char DEFAULT_KRUN_INIT[] = "/bin/sh"; - -#if __FreeBSD__ -static char *get_kenv(const char *name) -{ - static char kenv_value[KENV_MVALLEN + 1]; - if (kenv(KENV_GET, name, kenv_value, KENV_MVALLEN + 1) < 0) { - return NULL; - } - return kenv_value; -} - -#define getenv get_kenv - -#define _PATH_CONSOLE "/dev/console" -#define _PATH_DEVNULL "/dev/null" -#define _PATH_INITLOG "/init.log" -/* - * Start a session and allocate a controlling terminal. - * Only called by children of init after forking. - */ -static void open_console(void) -{ - int fd; - - /* - * Try to open /dev/console. Open the device with O_NONBLOCK to - * prevent potential blocking on a carrier. - */ - revoke(_PATH_CONSOLE); - if ((fd = open(_PATH_CONSOLE, O_RDWR | O_NONBLOCK)) != -1) { - (void)fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) & ~O_NONBLOCK); - if (login_tty(fd) == 0) - return; - close(fd); - } - - /* No luck. Log output to file if possible. */ - if ((fd = open(_PATH_DEVNULL, O_RDWR)) == -1) { - _exit(1); - } - if (fd != STDIN_FILENO) { - dup2(fd, STDIN_FILENO); - close(fd); - } - fd = open(_PATH_INITLOG, O_WRONLY | O_APPEND | O_CREAT, 0644); - if (fd == -1) - dup2(STDIN_FILENO, STDOUT_FILENO); - else if (fd != STDOUT_FILENO) { - dup2(fd, STDOUT_FILENO); - close(fd); - } - dup2(STDOUT_FILENO, STDERR_FILENO); -} - -#define KRUN_CONFIG_ISO_DEV "/dev/iso9660/KRUN_CONFIG" -#define ISO_CONFIG_FILE_PATH "/mnt/krun_config.json" - -bool config_file_from_iso(const char **path) -{ - const char *iov_args[] = {"fstype", "cd9660", "fspath", - "/mnt", "from", KRUN_CONFIG_ISO_DEV}; - - const int iovlen = sizeof(iov_args) / sizeof(iov_args[0]); - struct iovec iov[iovlen]; - int i; - - struct stat st; - // mkdir can fail with read-only fs error, - // so we rather check if /mnt exists first - if (stat("/mnt", &st) != 0) { - if (errno != ENOENT) { - perror("stat(/mnt)"); - exit(-1); - } - if (mkdir("/mnt", 0755) < 0) { - perror("mkdir(/mnt)"); - exit(-1); - } - } - - for (i = 0; i < iovlen; i++) { - iov[i].iov_base = (void *)iov_args[i]; - iov[i].iov_len = strlen(iov_args[i]) + 1; - } - - if (nmount(iov, iovlen, MNT_RDONLY) < 0) { - *path = NULL; - return false; - } - *path = ISO_CONFIG_FILE_PATH; - return true; -} - -int unmount_config_iso() -{ - return unmount("/mnt", 0); -} -#endif - -static void set_rlimits(const char *rlimits) -{ - unsigned long long int lim_id, lim_cur, lim_max; - struct rlimit rlim; - char *item = (char *)rlimits; - - while (1) { - lim_id = lim_cur = lim_max = ULLONG_MAX; - - lim_id = strtoull(item, &item, 10); - if (lim_id == ULLONG_MAX) { - printf("Invalid rlimit ID\n"); - break; - } - - item++; - lim_cur = strtoull(item, &item, 10); - item++; - lim_max = strtoull(item, &item, 10); - - rlim.rlim_cur = lim_cur; - rlim.rlim_max = lim_max; - if (setrlimit(lim_id, &rlim) != 0) { - printf("Error setting rlimit for ID=%lld\n", lim_id); - } - - if (*item != '\0') { - item++; - } else { - break; - } - } -} - -#ifdef SEV -/* - * The LUKS passphrase is obtained from a KBS attestation server, complete an - * SNP attestation to get the passphrase. - */ -static char *get_luks_passphrase(int *pass_len) -{ - int fd, ret, num_tokens, wid_found, url_found, tee_found, tee_data_found; - uint64_t dev_size, tc_size; - char wid[256], url[256], *tc_json, *tok_start, *tok_end; - char footer[KRUN_FOOTER_LEN], tee[256], tee_data[256], *return_str; - jsmn_parser parser; - jsmntok_t *tokens; - size_t tok_size; - - return_str = NULL; - - /* - * If a user registered the TEE config data disk with - * krun_set_data_disk(), it would appear as /dev/vdb in the guest. - * Mount this device and read the config. - */ - if (mkdir("/dev", 0755) < 0 && errno != EEXIST) { - perror("mkdir(/dev)"); - goto finish; - } - - if (mount("devtmpfs", "/dev", "devtmpfs", MS_RELATIME, NULL) < 0 && - errno != EBUSY) { - perror("mount(devtmpfs)"); - - goto rmdir_dev; - } - - fd = open("/dev/vda", O_RDONLY); - if (fd < 0) { - perror("open(/dev/vda)"); - - goto umount_dev; - } - - ret = ioctl(fd, BLKGETSIZE64, &dev_size); - if (ret != 0) { - perror("ioctl(BLKGETSIZE64)"); - - goto close_dev; - } - - if (lseek(fd, dev_size - KRUN_FOOTER_LEN, SEEK_SET) == -1) { - perror("lseek(END - KRUN_FOOTER_LEN)"); - - goto close_dev; - } - - ret = read(fd, &footer[0], KRUN_FOOTER_LEN); - if (ret != KRUN_FOOTER_LEN) { - perror("read(KRUN_FOOTER_LEN)"); - - goto close_dev; - } - - if (memcmp(&footer[0], KRUN_MAGIC, 4) != 0) { - printf("Couldn't find KRUN footer signature, falling back to SEV\n"); - return_str = sev_get_luks_passphrase(pass_len); - - goto close_dev; - } - - tc_size = *(uint64_t *)&footer[4]; - - if (lseek(fd, dev_size - tc_size - KRUN_FOOTER_LEN, SEEK_SET) == -1) { - perror("lseek(END - tc_size - KRUN_FOOTER_LEN)"); - - goto close_dev; - } - - tc_json = malloc(tc_size + 1); - if (tc_json == NULL) { - perror("malloc(tc_size)"); - - goto close_dev; - } - - ret = read(fd, tc_json, tc_size); - if (ret != tc_size) { - perror("read(tc_size)"); - - goto free_mem; - } - tc_json[tc_size] = '\0'; - - /* - * Parse the TEE config's workload_id and attestation_url field. - */ - jsmn_init(&parser); - - tokens = (jsmntok_t *)malloc(sizeof(jsmntok_t) * MAX_TOKENS); - if (tokens == NULL) { - perror("malloc(jsmntok_t)"); - - goto free_mem; - } - - num_tokens = - jsmn_parse(&parser, tc_json, strlen(tc_json), tokens, MAX_TOKENS); - if (num_tokens < 0) { - printf("Unable to allocate JSON tokens\n"); - - goto free_mem; - } else if (num_tokens < 1 || tokens[0].type != JSMN_OBJECT) { - printf("Unable to find object in TEE configuration file\n"); - - goto free_mem; - } - - wid_found = url_found = tee_found = tee_data_found = 0; - - for (int i = 1; i < num_tokens - 1; ++i) { - tok_start = tc_json + tokens[i + 1].start; - tok_end = tc_json + tokens[i + 1].end; - tok_size = tok_end - tok_start; - if (!jsoneq(tc_json, &tokens[i], "workload_id")) { - strncpy(wid, tok_start, tok_size); - wid_found = 1; - } else if (!jsoneq(tc_json, &tokens[i], "attestation_url")) { - strncpy(url, tok_start, tok_size); - url_found = 1; - } else if (!jsoneq(tc_json, &tokens[i], "tee")) { - strncpy(tee, tok_start, tok_size); - tee_found = 1; - } else if (!jsoneq(tc_json, &tokens[i], "tee_data")) { - strncpy(tee_data, tok_start, tok_size); - tee_data_found = 1; - } - } - - if (!wid_found) { - printf("Unable to find attestation workload ID\n"); - - goto free_mem; - } else if (!url_found) { - printf("Unable to find attestation server URL\n"); - - goto free_mem; - } else if (!tee_found) { - printf("Unable to find TEE generation server URL\n"); - - goto free_mem; - } - - if (strcmp(tee, "snp") == 0) { - if (tee_data_found == 0) { - printf("Unable to find SNP generation\n"); - goto free_mem; - } - - return_str = snp_get_luks_passphrase(url, wid, tee_data, pass_len); - } else if (strcmp(tee, "sev") == 0) { - return_str = sev_get_luks_passphrase(pass_len); - } - -free_mem: - free(tc_json); - -close_dev: - close(fd); - -umount_dev: - umount("/dev"); - -rmdir_dev: - rmdir("/dev"); - -finish: - return return_str; -} - -static char *snp_get_luks_passphrase(char *url, char *wid, char *tee_data, - int *pass_len) -{ - char *pass; - - pass = (char *)malloc(MAX_PASS_SIZE); - if (pass == NULL) { - return NULL; - } - - if (snp_attest(pass, url, wid, tee_data) == 0) { - *pass_len = strlen(pass); - return pass; - } - - free(pass); - - return NULL; -} - -static char *sev_get_luks_passphrase(int *pass_len) -{ - char *pass = NULL; - int len; - int fd; - - pass = getenv("KRUN_PASS"); - if (pass) { - *pass_len = strnlen(pass, MAX_PASS_SIZE); - return pass; - } - if (mkdir("/sfs", 0755) < 0 && errno != EEXIST) { - perror("mkdir(/sfs)"); - return NULL; - } - - if (mount("securityfs", "/sfs", "securityfs", - MS_NODEV | MS_NOEXEC | MS_NOSUID | MS_RELATIME, NULL) < 0) { - perror("mount(/sfs)"); - goto cleanup_dir; - } - - fd = open(CMDLINE_SECRET_PATH, O_RDONLY); - if (fd < 0) { - goto cleanup_sfs; - } - - pass = malloc(MAX_PASS_SIZE); - if (!pass) { - goto cleanup_fd; - } - - if ((len = read(fd, pass, MAX_PASS_SIZE)) < 0) { - free(pass); - pass = NULL; - } else { - *pass_len = len; - unlink(CMDLINE_SECRET_PATH); - } - -cleanup_fd: - close(fd); -cleanup_sfs: - umount("/sfs"); -cleanup_dir: - rmdir("/sfs"); - - return pass; -} - -static int chroot_luks() -{ - char *pass; - int pass_len; - int pid; - int pipefd[2]; - int wstatus; - - pass = get_luks_passphrase(&pass_len); - if (!pass) { - printf("Couldn't find LUKS passphrase\n"); - return -1; - } - - printf("Unlocking LUKS root filesystem\n"); - - if (mount("proc", "/proc", "proc", - MS_NODEV | MS_NOEXEC | MS_NOSUID | MS_RELATIME, NULL) < 0) { - perror("mount(/proc)"); - return -1; - } - - pipe(pipefd); - - pid = fork(); - if (pid == 0) { - close(pipefd[1]); - dup2(pipefd[0], 0); - close(pipefd[0]); - - if (execl("/sbin/cryptsetup", "cryptsetup", "open", "/dev/vda", - "luksroot", "-", NULL) < 0) { - perror("execl"); - return -1; - } - } else { - write(pipefd[1], pass, strnlen(pass, pass_len)); - close(pipefd[1]); - waitpid(pid, &wstatus, 0); - } - - memset(pass, 0, pass_len); - - printf("Mounting LUKS root filesystem\n"); - - if (mount("/dev/mapper/luksroot", "/luksroot", "ext4", 0, NULL) < 0) { - perror("mount(/luksroot)"); - return -1; - } - - chdir("/luksroot"); - - if (mount(".", "/", NULL, MS_MOVE, NULL)) { - perror("remount root"); - return -1; - } - chroot("."); - - return 0; -} -#endif - -static int mount_filesystems() -{ -#if __linux__ - char *const DIRS_LEVEL1[] = {"/dev", "/proc", "/sys"}; - char *const DIRS_LEVEL2[] = {"/dev/pts", "/dev/shm"}; - int i; - - for (i = 0; i < 3; ++i) { - if (mkdir(DIRS_LEVEL1[i], 0755) < 0 && errno != EEXIST) { - printf("Error creating directory (%s)\n", DIRS_LEVEL1[i]); - return -1; - } - } - - if (mount("devtmpfs", "/dev", "devtmpfs", MS_RELATIME, NULL) < 0 && - errno != EBUSY) { - perror("mount(/dev)"); - return -1; - } - - /* - * Best effort loosen /dev/kvm permissions to allow nested virtualization by - * unprivileged processes inside the microVM (usually a single purpose - * environment). Log errors but don't log ENOENT since the guest kernel may - * not support KVM or nested virtualization might not be enabled. - */ - if (chmod("/dev/kvm", 0666) < 0 && errno != ENOENT) { - perror("chmod(/dev/kvm)"); - } - - if (mount("proc", "/proc", "proc", - MS_NODEV | MS_NOEXEC | MS_NOSUID | MS_RELATIME, NULL) < 0 && - errno != EBUSY) { - perror("mount(/proc)"); - return -1; - } - - if (mount("sysfs", "/sys", "sysfs", - MS_NODEV | MS_NOEXEC | MS_NOSUID | MS_RELATIME, NULL) < 0 && - errno != EBUSY) { - perror("mount(/sys)"); - return -1; - } - - if (mount("cgroup2", "/sys/fs/cgroup", "cgroup2", - MS_NODEV | MS_NOEXEC | MS_NOSUID | MS_RELATIME, NULL) < 0 && - errno != EBUSY) { - perror("mount(/sys/fs/cgroup)"); - return -1; - } - - for (i = 0; i < 2; ++i) { - if (mkdir(DIRS_LEVEL2[i], 0755) < 0 && errno != EEXIST) { - printf("Error creating directory (%s)\n", DIRS_LEVEL2[i]); - return -1; - } - } - - if (mount("devpts", "/dev/pts", "devpts", - MS_NOEXEC | MS_NOSUID | MS_RELATIME, NULL) < 0 && - errno != EBUSY) { - perror("mount(/dev/pts)"); - return -1; - } - - if (mount("tmpfs", "/dev/shm", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_RELATIME, - NULL) < 0 && - errno != EBUSY) { - perror("mount(/dev/shm)"); - return -1; - } - - /* May fail if already exists and that's fine. */ - symlink("/proc/self/fd", "/dev/fd"); -#endif - return 0; -} - -/* - * hexToDigit, Utf32toUtf8 and parts of unescape_string are taken from libyajl: - * - * Copyright (c) 2007-2014, Lloyd Hilaiel - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ -static void hexToDigit(unsigned int *val, const unsigned char *hex) -{ - unsigned int i; - for (i = 0; i < 4; i++) { - unsigned char c = hex[i]; - if (c >= 'A') - c = (c & ~0x20) - 7; - c -= '0'; - *val = (*val << 4) | c; - } -} - -static void Utf32toUtf8(unsigned int codepoint, char *utf8Buf) -{ - if (codepoint < 0x80) { - utf8Buf[0] = (char)codepoint; - utf8Buf[1] = 0; - } else if (codepoint < 0x0800) { - utf8Buf[0] = (char)((codepoint >> 6) | 0xC0); - utf8Buf[1] = (char)((codepoint & 0x3F) | 0x80); - utf8Buf[2] = 0; - } else if (codepoint < 0x10000) { - utf8Buf[0] = (char)((codepoint >> 12) | 0xE0); - utf8Buf[1] = (char)(((codepoint >> 6) & 0x3F) | 0x80); - utf8Buf[2] = (char)((codepoint & 0x3F) | 0x80); - utf8Buf[3] = 0; - } else if (codepoint < 0x200000) { - utf8Buf[0] = (char)((codepoint >> 18) | 0xF0); - utf8Buf[1] = (char)(((codepoint >> 12) & 0x3F) | 0x80); - utf8Buf[2] = (char)(((codepoint >> 6) & 0x3F) | 0x80); - utf8Buf[3] = (char)((codepoint & 0x3F) | 0x80); - utf8Buf[4] = 0; - } else { - utf8Buf[0] = '?'; - utf8Buf[1] = 0; - } -} - -/* Do not worry about invalid JSON, it was already parsed by jsmn. */ -static void unescape_string(char *string, int len) -{ - unsigned char *val = (unsigned char *)string; - unsigned char *end; - int i = 0; - - end = val + len; - while (val < end) { - if (*val != '\\') { - string[i++] = *val++; - continue; - } - switch (*++val) { - case 'n': - string[i++] = '\n'; - val++; - break; - case 't': - string[i++] = '\t'; - val++; - break; - case 'r': - string[i++] = '\r'; - val++; - break; - case 'b': - string[i++] = '\b'; - val++; - break; - case 'f': - string[i++] = '\f'; - val++; - break; - case '\\': - string[i++] = '\\'; - val++; - break; - case '\"': - string[i++] = '\"'; - val++; - break; - case '/': - string[i++] = '/'; - val++; - break; - case 'u': { - const char *unescaped = "?"; - char utf8Buf[5]; - unsigned int codepoint = 0; - hexToDigit(&codepoint, val++); - val += 3; - /* check if this is a surrogate */ - if ((codepoint & 0xFC00) == 0xD800) { - val++; - if (val[0] == '\\' && val[1] == 'u') { - unsigned int surrogate = 0; - hexToDigit(&surrogate, val + 2); - codepoint = (((codepoint & 0x3F) << 10) | - ((((codepoint >> 6) & 0xF) + 1) << 16) | - (surrogate & 0x3FF)); - val += 5; - } else { - unescaped = "?"; - break; - } - } - - Utf32toUtf8(codepoint, utf8Buf); - unescaped = utf8Buf; - - if (codepoint == 0) { - memcpy(&string[i++], unescaped, 1); - continue; - } - memcpy(&string[i], unescaped, (unsigned int)strlen(unescaped)); - break; - } - } - } - string[i] = '\0'; -} - -static void config_parse_env(char *data, jsmntok_t *token) -{ - jsmntok_t *tenv; - char *env, *env_val; - int len; - int i; - - for (i = 0; i < token->size; i++) { - tenv = &token[i + 1]; - - env = data + tenv->start; - len = tenv->end - tenv->start; - - unescape_string(env, len); - - env_val = strstr(env, "="); - if (!env_val) { - continue; - } - - env[len] = '\0'; - *env_val = '\0'; - env_val++; - - if ((strcmp(env, "HOME") == 0) || (strcmp(env, "TERM") == 0)) { - setenv(env, env_val, 1); - } else { - setenv(env, env_val, 0); - } - } -} - -static char **config_parse_args(char *data, jsmntok_t *token) -{ - jsmntok_t *targ; - char *arg, *value; - char **argv; - int len; - int i; - const int n_args = token->size; - - argv = malloc((n_args + 1) * sizeof(char *)); - if (!argv) { - perror("malloc(config_parse_args)"); - return NULL; - } - - for (i = 0; i < n_args; i++) { - targ = &token[i + 1]; - - value = data + targ->start; - len = targ->end - targ->start; - - arg = malloc(len + 1); - if (!arg) { - perror("malloc(config_parse_args arg)"); - while (--i >= 0) - free(argv[i]); - free(argv); - return NULL; - } - memcpy(arg, value, len); - arg[len] = '\0'; - - unescape_string(arg, len); - - argv[i] = arg; - } - - if (i == 0) { - free(argv); - argv = NULL; - } else { - argv[i] = NULL; - } - - return argv; -} - -static char *config_parse_string(char *data, jsmntok_t *token) -{ - char *string; - char *val; - int len; - - val = data + token->start; - len = token->end - token->start; - if (!len) { - return NULL; - } - - string = malloc(len + 1); - - if (!string) { - return NULL; - } - memcpy(string, val, len); - string[len] = '\0'; - - unescape_string(string, len); - - return string; -} - -static int jsoneq(const char *json, jsmntok_t *tok, const char *s) -{ - if (tok->type == JSMN_STRING && (int)strlen(s) == tok->end - tok->start && - strncasecmp(json + tok->start, s, tok->end - tok->start) == 0) { - return 0; - } - return -1; -} - -char **concat_entrypoint_argv(char **entrypoint, char **config_argv) -{ - char **argv; - int i, j; - int n_args = 0; - - for (i = 0; entrypoint[i]; i++) - n_args++; - for (j = 0; config_argv[j]; j++) - n_args++; - - argv = malloc((n_args + 1) * sizeof(char *)); - if (!argv) { - perror("malloc(concat_entrypoint_argv)"); - return NULL; - } - - for (i = 0; entrypoint[i]; i++) { - argv[i] = entrypoint[i]; - } - - for (j = 0; config_argv[j]; i++, j++) { - argv[i] = config_argv[j]; - } - - argv[i] = NULL; - - return argv; -} - -static int config_parse_file(char ***argv, char **workdir, - const char *config_file) -{ - jsmn_parser parser; - jsmntok_t *tokens; - struct stat stat; - char *data; - off_t data_len; - char **config_argv; - char **entrypoint; - int parsed_env, parsed_workdir, parsed_args, parsed_entrypoint; - int num_tokens; - int ret = -1; - int fd; - int i; - - fd = open(config_file, O_RDONLY); - if (fd < 0) { - return ret; - } - - if (fstat(fd, &stat) != 0) { - perror("Couldn't stat config file"); - goto cleanup_fd; - } - - data_len = stat.st_size; - data = malloc(data_len); - if (!data) { - perror("Couldn't allocate memory"); - goto cleanup_fd; - } - - if (read(fd, data, data_len) < 0) { - perror("Error reading config file"); - goto cleanup_data; - } - - tokens = malloc(MAX_TOKENS * sizeof(jsmntok_t)); - if (!tokens) { - perror("Couldn't allocate memory"); - goto cleanup_data; - } - - jsmn_init(&parser); - num_tokens = jsmn_parse(&parser, data, data_len, tokens, MAX_TOKENS); - if (num_tokens < 0) { - printf("Error parsing config file\n"); - goto cleanup_tokens; - } - - if (num_tokens < 1 || tokens[0].type != JSMN_OBJECT) { - printf("Couldn't find object in config file\n"); - goto cleanup_tokens; - } - - config_argv = NULL; - entrypoint = NULL; - parsed_env = parsed_workdir = parsed_args = parsed_entrypoint = 0; - - for (i = 1; i < num_tokens && (!parsed_env || !parsed_args || - !parsed_workdir || !parsed_entrypoint); - i++) { - if (!parsed_env && jsoneq(data, &tokens[i], "Env") == 0 && - (i + 1) < num_tokens && tokens[i + 1].type == JSMN_ARRAY) { - config_parse_env(data, &tokens[i + 1]); - parsed_env = 1; - } - - if (!parsed_args && jsoneq(data, &tokens[i], "args") == 0 && - (i + 1) < num_tokens) { - config_argv = config_parse_args(data, &tokens[i + 1]); - parsed_args = 1; - } - - if (!parsed_args && jsoneq(data, &tokens[i], "Cmd") == 0 && - (i + 1) < num_tokens) { - config_argv = config_parse_args(data, &tokens[i + 1]); - parsed_args = 1; - } - - if (!parsed_workdir && jsoneq(data, &tokens[i], "WorkingDir") == 0 && - (i + 1) < num_tokens) { - *workdir = config_parse_string(data, &tokens[i + 1]); - parsed_workdir = 1; - } - - if (!parsed_workdir && jsoneq(data, &tokens[i], "Cwd") == 0 && - (i + 1) < num_tokens) { - *workdir = config_parse_string(data, &tokens[i + 1]); - parsed_workdir = 1; - } - - if (!parsed_entrypoint && jsoneq(data, &tokens[i], "Entrypoint") == 0 && - (i + 1) < num_tokens) { - entrypoint = config_parse_args(data, &tokens[i + 1]); - parsed_entrypoint = 1; - } - } - - if (config_argv && entrypoint) { - *argv = concat_entrypoint_argv(entrypoint, config_argv); - } else { - *argv = config_argv; - } - - ret = 0; - -cleanup_tokens: - free(tokens); -cleanup_data: - free(data); -cleanup_fd: - close(fd); - - return ret; -} - -#ifdef __TIMESYNC__ - -#define TSYNC_PORT 123 -#define BUFSIZE 8 -#define NANOS_IN_SECOND 1000000000 -/* Set clock if delta is bigger than 100ms */ -#define DELTA_SYNC 100000000 - -void clock_worker() -{ - int sockfd, n; - struct sockaddr_vm serveraddr; - char buf[BUFSIZE]; - struct timespec gtime; - struct timespec htime; - uint64_t gtime_ns; - uint64_t htime_ns; - - sockfd = socket(AF_VSOCK, SOCK_DGRAM, 0); - if (sockfd < 0) { - perror("Couldn't create timesync socket"); - return; - } - - bzero((char *)&serveraddr, sizeof(serveraddr)); - serveraddr.svm_family = AF_VSOCK; - serveraddr.svm_port = TSYNC_PORT; - serveraddr.svm_cid = 3; - - bzero(buf, BUFSIZE); - - n = bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)); - if (n < 0) { - printf("Couldn't bind timesync socket\n"); - return; - } - - while (1) { - n = recv(sockfd, buf, BUFSIZE, 0); - if (n < 0) { - perror("Error in timesync recv"); - return; - } else if (n != 8) { - printf("Ignoring bogus timesync packet\n"); - continue; - } - - htime_ns = *(uint64_t *)&buf[0]; - clock_gettime(CLOCK_REALTIME, >ime); - gtime_ns = gtime.tv_sec * NANOS_IN_SECOND; - gtime_ns += gtime.tv_nsec; - - if (llabs(htime_ns - gtime_ns) > DELTA_SYNC) { - htime.tv_sec = htime_ns / NANOS_IN_SECOND; - htime.tv_nsec = htime_ns % NANOS_IN_SECOND; - clock_settime(CLOCK_REALTIME, &htime); - } - } -} -#endif - -int reopen_fd(int fd, char *path, int flags) -{ - int newfd = open(path, flags); - if (newfd < 0) { - printf("Failed to open '%s': %s\n", path, strerror(errno)); - return -1; - } - - close(fd); - if (dup2(newfd, fd) < 0) { - perror("dup2"); - close(newfd); - return -1; - } - close(newfd); - return 0; -} - -int setup_redirects() -{ - DIR *ports_dir = opendir("/sys/class/virtio-ports"); - if (ports_dir == NULL) { - printf("Unable to open ports directory!\n"); - return -4; - } - - char path[2048]; - char name_buf[1024]; - - struct dirent *entry = NULL; - while ((entry = readdir(ports_dir))) { - char *port_identifier = entry->d_name; - int result_len = - snprintf(path, sizeof(path), "/sys/class/virtio-ports/%s/name", - port_identifier); - - // result was truncated - if (result_len > sizeof(name_buf) - 1) { - printf("Path buffer too small"); - return -1; - } - - FILE *port_name_file = fopen(path, "r"); - if (port_name_file == NULL) { - continue; - } - - char *port_name = fgets(name_buf, sizeof(name_buf), port_name_file); - fclose(port_name_file); - - if (port_name != NULL && strcmp(port_name, "krun-stdin\n") == 0) { - // if previous snprintf didn't fail, this one cannot fail either - snprintf(path, sizeof(path), "/dev/%s", port_identifier); - reopen_fd(STDIN_FILENO, path, O_RDONLY); - } else if (port_name != NULL && - strcmp(port_name, "krun-stdout\n") == 0) { - snprintf(path, sizeof(path), "/dev/%s", port_identifier); - reopen_fd(STDOUT_FILENO, path, O_WRONLY); - } else if (port_name != NULL && - strcmp(port_name, "krun-stderr\n") == 0) { - snprintf(path, sizeof(path), "/dev/%s", port_identifier); - reopen_fd(STDERR_FILENO, path, O_WRONLY); - } - } - - closedir(ports_dir); - return 0; -} - -int is_virtiofs(const char *path) -{ - struct statfs fs; - - if (statfs(path, &fs) != 0) { - perror("statfs"); - return -1; - } - - // virtiofs magic number: 0x65735546 - return (fs.f_type == 0x65735546) ? 1 : 0; -} - -void set_exit_code(int code) -{ - int fd; - int ret; - int virtiofs_check; - - // Only use the ioctl if virtiofs is used for root filesystem - virtiofs_check = is_virtiofs("/"); - if (virtiofs_check < 0) { - printf("Warning: Could not determine filesystem type for root\n"); - } - - if (virtiofs_check == 0) { - // Root filesystem is not virtiofs, skip the ioctl - return; - } - - fd = open("/", O_RDONLY); - if (fd < 0) { - perror("Couldn't open root filesystem to report exit code"); - return; - } - - ret = ioctl(fd, KRUN_EXIT_CODE_IOCTL, code); - if (ret < 0) { - perror("Error using the ioctl to set the exit code"); - } - - close(fd); -} - -#if __linux__ -int try_mount(const char *source, const char *target, const char *fstype, - unsigned long mountflags, const void *data) -{ - FILE *f; - char line[129]; - int mount_status = -1; - - if (fstype) { - return mount(source, target, fstype, mountflags, data); - } - - f = fopen("/proc/filesystems", "r"); - if (f == NULL) { - perror("fopen(/proc/filesystems)"); - return -1; - } - while (fgets(line, sizeof(line), f)) { - char fstype[sizeof(line)]; - if (!strncmp(line, "nodev", 5)) { - continue; - } - if (sscanf(line, "%128s\n", fstype) != 1) { - continue; - } - - mount_status = mount(source, target, fstype, mountflags, data); - if (mount_status == 0) { - break; - } - } - fclose(f); - - return mount_status; -} -#endif - -char *clone_str(const char *str) -{ - if (str == NULL) { - return NULL; - } - return strdup(str); -} - -#if __linux__ -static bool tsi_enabled() -{ - const char *const option = "tsi_hijack"; - bool enabled = false; - char *cmdline = NULL; - size_t cmdline_length = 0; - FILE *f; - const char *const delimiters = " \n"; - char *token; - - f = fopen("/proc/cmdline", "r"); - if (f == NULL) { - perror("fopen(/proc/cmdline)"); - return false; - } - - if (getline(&cmdline, &cmdline_length, f) < 0) { - perror("getline(/proc/cmdline)"); - fclose(f); - goto cleanup; - } - fclose(f); - - token = strtok(cmdline, delimiters); - while (token != NULL) { - if (strcmp(token, "--") == 0) { - break; - } - if (strcmp(token, option) == 0) { - enabled = true; - break; - } - token = strtok(NULL, delimiters); - } - -cleanup: - free(cmdline); - - return enabled; -} - -static int enable_dummy_interface() -{ - // See https://www.man7.org/linux/man-pages/man7/netdevice.7.html - - const char *const name = "dummy0"; - struct ifreq ifr; - int sockfd; - struct sockaddr_in *addr = (struct sockaddr_in *)&ifr.ifr_addr; - struct sockaddr_in *netmask = (struct sockaddr_in *)&ifr.ifr_netmask; - int result = -1; - - if (snprintf(ifr.ifr_name, IFNAMSIZ, "%s", name) >= IFNAMSIZ) { - printf("dummy interface name too long\n"); - return -1; - } - - sockfd = socket(PF_INET, SOCK_DGRAM, 0); - if (sockfd < 0) { - perror("dummy interface socket"); - return -1; - } - - ifr.ifr_flags = IFF_UP; - if (ioctl(sockfd, SIOCSIFFLAGS, &ifr) < 0) { - if (errno == ENODEV) { - // Most likely not enabled in the kernel, ignore quietly - result = 0; - goto close_socket; - } - perror("dummy interface up"); - goto close_socket; - } - - addr->sin_family = AF_INET; - if (inet_pton(AF_INET, "203.0.113.1", &addr->sin_addr) <= 0) { - printf("inet_pton address conversion failed\n"); - goto close_socket; - } - if (ioctl(sockfd, SIOCSIFADDR, &ifr) < 0) { - perror("dummy interface address"); - goto close_socket; - } - - netmask->sin_family = AF_INET; - if (inet_pton(AF_INET, "255.255.255.0", &netmask->sin_addr) <= 0) { - printf("inet_pton netmask conversion failed\n"); - goto close_socket; - } - if (ioctl(sockfd, SIOCSIFNETMASK, &ifr) < 0) { - perror("dummy interface mask"); - goto close_socket; - } - - result = 0; - -close_socket: - close(sockfd); - return result; -} -#endif - -int main(int argc, char **argv) -{ - struct ifreq ifr; - int sockfd; - int status; - int saved_errno; - bool init_pid1 = false; - char localhost[] = "localhost\0"; - char *hostname; - char *krun_home; - char *krun_term; - char *krun_init; -#if __linux__ - char *krun_dhcp; - int fd; - char *krun_root; - char *krun_root_fstype; - char *krun_root_options; -#endif - char *env_init_pid1; - char *config_workdir, *env_workdir; - char *rlimits; - char **config_argv, **exec_argv; - const char *config_file; -#if __FreeBSD__ - bool config_file_mounted = false; - - open_console(); -#endif - -#ifdef TDX - if (mkdir("/tmp", 0755) < 0 && errno != EEXIST) { - perror("mkdir(/tmp)"); - exit(-1); - } - if (mkdir("/tmp/vda", 0755) < 0 && errno != EEXIST) { - perror("mkdir(/tmp/vda)"); - exit(-1); - } - if (mount("/dev/vda", "/tmp/vda", "ext4", MS_RELATIME, NULL) < 0) { - perror("mount(/dev/vda)"); - exit(-1); - } - chdir("/tmp/vda"); - if (mount(".", "/", NULL, MS_MOVE, NULL) < 0) { - perror("remount root"); - exit(-1); - } - chroot("."); - -#endif - -#ifdef SEV - if (chroot_luks() < 0) { - printf("Couldn't switch to LUKS volume, bailing out\n"); - exit(-1); - } -#endif - if (mount_filesystems() < 0) { - printf("Couldn't mount filesystems, bailing out\n"); - exit(-2); - } - -#if __linux__ - krun_root = clone_str(getenv("KRUN_BLOCK_ROOT_DEVICE")); - if (krun_root) { - if (mkdir("/newroot", 0755) < 0 && errno != EEXIST) { - perror("mkdir(/newroot)"); - exit(-1); - } - - krun_root_fstype = clone_str(getenv("KRUN_BLOCK_ROOT_FSTYPE")); - krun_root_options = clone_str(getenv("KRUN_BLOCK_ROOT_OPTIONS")); - - if (try_mount(krun_root, "/newroot", krun_root_fstype, 0, - krun_root_options) < 0) { - perror("mount KRUN_BLOCK_ROOT_DEVICE"); - exit(-1); - } - free(krun_root); - free(krun_root_fstype); - free(krun_root_options); - - chdir("/newroot"); - - if (mount(".", "/", NULL, MS_MOVE, NULL) < 0) { - perror("remount root"); - exit(-1); - } - chroot("."); - - // we must mount filesystems again after chrooting - if (mount_filesystems() < 0) { - printf("Couldn't mount filesystems, bailing out\n"); - exit(-2); - } - } - - if (mount(NULL, "/", NULL, MS_REC | MS_SHARED, NULL) < 0) { - perror("Couldn't set shared propagation on the root mount"); - exit(-1); - } -#endif - - setsid(); - ioctl(0, TIOCSCTTY, 1); - -#if __FreeBSD__ - setlogin("root"); -#endif - - sockfd = socket(AF_INET, SOCK_DGRAM, 0); - if (sockfd >= 0) { - memset(&ifr, 0, sizeof ifr); - strncpy(ifr.ifr_name, "lo", IFNAMSIZ); - ifr.ifr_flags |= IFF_UP; - ioctl(sockfd, SIOCSIFFLAGS, &ifr); - -#if __linux__ - krun_dhcp = getenv("KRUN_DHCP"); - if (krun_dhcp && strcmp(krun_dhcp, "1") == 0) { - memset(&ifr, 0, sizeof ifr); - strncpy(ifr.ifr_name, "eth0", IFNAMSIZ); - if (ioctl(sockfd, SIOCGIFFLAGS, &ifr) == 0) { - /* eth0 exists, bring it up first */ - ifr.ifr_flags |= IFF_UP; - ioctl(sockfd, SIOCSIFFLAGS, &ifr); - - /* Configure eth0 with DHCP */ - if (do_dhcp("eth0") != 0) { - printf("Warning: DHCP configuration for eth0 failed\n"); - } - } - } -#endif - - close(sockfd); - } - -#if __linux__ - if (tsi_enabled()) { - if (enable_dummy_interface() < 0) { - printf("Warning: Couldn't enable dummy interface\n"); - } - } -#endif - - config_argv = NULL; - config_workdir = NULL; - - config_file = getenv("KRUN_CONFIG"); - -#if __FreeBSD__ - if (!config_file) { - config_file_mounted = config_file_from_iso(&config_file); - } -#endif - - if (!config_file) { - config_file = CONFIG_FILE_PATH; - } - - config_parse_file(&config_argv, &config_workdir, config_file); - -#if __FreeBSD__ - if (config_file_mounted) { - unmount_config_iso(); - } -#endif - - krun_home = getenv("KRUN_HOME"); - if (krun_home) { - setenv("HOME", krun_home, 1); - } - - krun_term = getenv("KRUN_TERM"); - if (krun_term) { - setenv("TERM", krun_term, 1); - } - - hostname = getenv("HOSTNAME"); - if (hostname) { - sethostname(hostname, strlen(hostname)); - } else { - sethostname(&localhost[0], strlen(localhost)); - } - - rlimits = getenv("KRUN_RLIMITS"); - if (rlimits) { - set_rlimits(rlimits); - } - - env_workdir = getenv("KRUN_WORKDIR"); - if (env_workdir) { - chdir(env_workdir); - } else if (config_workdir) { - chdir(config_workdir); - } - - exec_argv = argv; - krun_init = getenv("KRUN_INIT"); - if (krun_init) { - exec_argv[0] = clone_str(krun_init); - } else if (config_argv) { - exec_argv = config_argv; - } else { - exec_argv[0] = &DEFAULT_KRUN_INIT[0]; - } - - env_init_pid1 = getenv("KRUN_INIT_PID1"); - if (env_init_pid1 && *env_init_pid1 == '1') { - init_pid1 = true; - } - -#ifdef __TIMESYNC__ - if (fork() == 0) { - clock_worker(); - _exit(1); - } -#endif - - if (init_pid1) { - goto exec_init; - } - - // We need to fork ourselves, because pid 1 cannot doesn't receive SIGINT - // signal - int child = fork(); - if (child < 0) { - perror("fork"); - set_exit_code(125); - exit(125); - } - if (child == 0) { // child - exec_init: -#if __FreeBSD__ - open_console(); -#else - if (setup_redirects() < 0) { - exit(125); - } -#endif - if (execvp(exec_argv[0], exec_argv) < 0) { - saved_errno = errno; - printf("Couldn't execute '%s' inside the vm: %s\n", exec_argv[0], - strerror(errno)); - // Use the same exit code as chroot and podman do. - if (saved_errno == ENOENT) { - exit(127); - } else { - exit(126); - } - } - } else { // parent - // Wait until the workload's entrypoint has exited, ignoring any other - // children. - while (waitpid(-1, &status, 0) != child) { - // Not the first child, ignore it. - }; - - // The workload's entrypoint has exited, record its exit code and exit - // ourselves. - if (WIFEXITED(status)) { - set_exit_code(WEXITSTATUS(status)); - } else if (WIFSIGNALED(status)) { - set_exit_code(WTERMSIG(status) + 128); - } - - sync(); -#if __linux__ - reboot(LINUX_REBOOT_CMD_RESTART); -#endif - } - - return 0; -} diff --git a/vendor/krun-init-blob/init/jsmn.h b/vendor/krun-init-blob/init/jsmn.h deleted file mode 100644 index 30d37a24a..000000000 --- a/vendor/krun-init-blob/init/jsmn.h +++ /dev/null @@ -1,494 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2010 Serge Zaitsev - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -#ifndef JSMN_H -#define JSMN_H - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -#define JSMN_API static - -/** - * JSON type identifier. Basic types are: - * o Object - * o Array - * o String - * o Other primitive: number, boolean (true/false) or null - */ -typedef enum { - JSMN_UNDEFINED = 0, - JSMN_OBJECT = 1 << 0, - JSMN_ARRAY = 1 << 1, - JSMN_STRING = 1 << 2, - JSMN_PRIMITIVE = 1 << 3 -} jsmntype_t; - -enum jsmnerr { - /* Not enough tokens were provided */ - JSMN_ERROR_NOMEM = -1, - /* Invalid character inside JSON string */ - JSMN_ERROR_INVAL = -2, - /* The string is not a full JSON packet, more bytes expected */ - JSMN_ERROR_PART = -3 -}; - -/** - * JSON token description. - * type type (object, array, string etc.) - * start start position in JSON data string - * end end position in JSON data string - */ -typedef struct jsmntok { - jsmntype_t type; - int start; - int end; - int size; -#ifdef JSMN_PARENT_LINKS - int parent; -#endif -} jsmntok_t; - -/** - * JSON parser. Contains an array of token blocks available. Also stores - * the string being parsed now and current position in that string. - */ -typedef struct jsmn_parser { - unsigned int pos; /* offset in the JSON string */ - unsigned int toknext; /* next token to allocate */ - int toksuper; /* superior token node, e.g. parent object or array */ -} jsmn_parser; - -/** - * Create JSON parser over an array of tokens - */ -JSMN_API void jsmn_init(jsmn_parser *parser); - -/** - * Run JSON parser. It parses a JSON data string into and array of tokens, each - * describing - * a single JSON object. - */ -JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len, - jsmntok_t *tokens, const unsigned int num_tokens); - -#ifndef JSMN_HEADER -/** - * Allocates a fresh unused token from the token pool. - */ -static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser, jsmntok_t *tokens, - const size_t num_tokens) -{ - jsmntok_t *tok; - if (parser->toknext >= num_tokens) { - return NULL; - } - tok = &tokens[parser->toknext++]; - tok->start = tok->end = -1; - tok->size = 0; -#ifdef JSMN_PARENT_LINKS - tok->parent = -1; -#endif - return tok; -} - -/** - * Fills token type and boundaries. - */ -static void jsmn_fill_token(jsmntok_t *token, const jsmntype_t type, - const int start, const int end) -{ - token->type = type; - token->start = start; - token->end = end; - token->size = 0; -} - -/** - * Fills next available token with JSON primitive. - */ -static int jsmn_parse_primitive(jsmn_parser *parser, const char *js, - const size_t len, jsmntok_t *tokens, - const size_t num_tokens) -{ - jsmntok_t *token; - int start; - - start = parser->pos; - - for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { - switch (js[parser->pos]) { -#ifndef JSMN_STRICT - /* In strict mode primitive must be followed by "," or "}" or "]" */ - case ':': -#endif - case '\t': - case '\r': - case '\n': - case ' ': - case ',': - case ']': - case '}': - goto found; - default: - /* to quiet a warning from gcc*/ - break; - } - /* libkrun: Let's be permissive with non-ASCII bytes - if (js[parser->pos] < 32 || js[parser->pos] >= 127) { - parser->pos = start; - return JSMN_ERROR_INVAL; - } - */ - } -#ifdef JSMN_STRICT - /* In strict mode primitive must be followed by a comma/object/array */ - parser->pos = start; - return JSMN_ERROR_PART; -#endif - -found: - if (tokens == NULL) { - parser->pos--; - return 0; - } - token = jsmn_alloc_token(parser, tokens, num_tokens); - if (token == NULL) { - parser->pos = start; - return JSMN_ERROR_NOMEM; - } - jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos); -#ifdef JSMN_PARENT_LINKS - token->parent = parser->toksuper; -#endif - parser->pos--; - return 0; -} - -/** - * Fills next token with JSON string. - */ -static int jsmn_parse_string(jsmn_parser *parser, char *js, const size_t len, - jsmntok_t *tokens, const size_t num_tokens) -{ - jsmntok_t *token; - - int start = parser->pos; - - /* Skip starting quote */ - parser->pos++; - - for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { - char c = js[parser->pos]; - - /* Quote: end of string */ - if (c == '\"') { - if (tokens == NULL) { - return 0; - } - token = jsmn_alloc_token(parser, tokens, num_tokens); - if (token == NULL) { - parser->pos = start; - return JSMN_ERROR_NOMEM; - } - jsmn_fill_token(token, JSMN_STRING, start + 1, parser->pos); -#ifdef JSMN_PARENT_LINKS - token->parent = parser->toksuper; -#endif - return 0; - } - - /* Backslash: Quoted symbol expected */ - if (c == '\\' && parser->pos + 1 < len) { - int i; - parser->pos++; - switch (js[parser->pos]) { - /* Allowed escaped symbols */ - case '\"': - case '/': - case '\\': - case 'b': - case 'f': - case 'r': - case 'n': - case 't': - break; - /* Allows escaped symbol \uXXXX */ - case 'u': { - char unicode[5]; - long ascii; - - parser->pos++; - for (i = 0; - i < 4 && parser->pos < len && js[parser->pos] != '\0'; - i++) { - /* If it isn't a hex character we have an error */ - if (!((js[parser->pos] >= 48 && - js[parser->pos] <= 57) || /* 0-9 */ - (js[parser->pos] >= 65 && - js[parser->pos] <= 70) || /* A-F */ - (js[parser->pos] >= 97 && - js[parser->pos] <= 102))) { /* a-f */ - parser->pos = start; - return JSMN_ERROR_INVAL; - } - unicode[i] = js[parser->pos]; - parser->pos++; - } - - unicode[4] = '\0'; - ascii = strtol(&unicode[0], NULL, 16); - if (ascii < 0 || ascii > 127) { - /* This unicode char doesn't translate directly to ASCII */ - parser->pos = start; - return JSMN_ERROR_INVAL; - } - - parser->pos--; - js[parser->pos] = (char)ascii; - break; - } - /* Unexpected symbol */ - default: - parser->pos = start; - return JSMN_ERROR_INVAL; - } - } - } - parser->pos = start; - return JSMN_ERROR_PART; -} - -/** - * Parse JSON string and fill tokens. - */ -JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len, - jsmntok_t *tokens, const unsigned int num_tokens) -{ - int r; - int i; - jsmntok_t *token; - int count = parser->toknext; - - for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { - char c; - jsmntype_t type; - - c = js[parser->pos]; - switch (c) { - case '{': - case '[': - count++; - if (tokens == NULL) { - break; - } - token = jsmn_alloc_token(parser, tokens, num_tokens); - if (token == NULL) { - return JSMN_ERROR_NOMEM; - } - if (parser->toksuper != -1) { - jsmntok_t *t = &tokens[parser->toksuper]; -#ifdef JSMN_STRICT - /* In strict mode an object or array can't become a key */ - if (t->type == JSMN_OBJECT) { - return JSMN_ERROR_INVAL; - } -#endif - t->size++; -#ifdef JSMN_PARENT_LINKS - token->parent = parser->toksuper; -#endif - } - token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY); - token->start = parser->pos; - parser->toksuper = parser->toknext - 1; - break; - case '}': - case ']': - if (tokens == NULL) { - break; - } - type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY); -#ifdef JSMN_PARENT_LINKS - if (parser->toknext < 1) { - return JSMN_ERROR_INVAL; - } - token = &tokens[parser->toknext - 1]; - for (;;) { - if (token->start != -1 && token->end == -1) { - if (token->type != type) { - return JSMN_ERROR_INVAL; - } - token->end = parser->pos + 1; - parser->toksuper = token->parent; - break; - } - if (token->parent == -1) { - if (token->type != type || parser->toksuper == -1) { - return JSMN_ERROR_INVAL; - } - break; - } - token = &tokens[token->parent]; - } -#else - for (i = parser->toknext - 1; i >= 0; i--) { - token = &tokens[i]; - if (token->start != -1 && token->end == -1) { - if (token->type != type) { - return JSMN_ERROR_INVAL; - } - parser->toksuper = -1; - token->end = parser->pos + 1; - break; - } - } - /* Error if unmatched closing bracket */ - if (i == -1) { - return JSMN_ERROR_INVAL; - } - for (; i >= 0; i--) { - token = &tokens[i]; - if (token->start != -1 && token->end == -1) { - parser->toksuper = i; - break; - } - } -#endif - break; - case '\"': - r = jsmn_parse_string(parser, (char *)js, len, tokens, num_tokens); - if (r < 0) { - return r; - } - count++; - if (parser->toksuper != -1 && tokens != NULL) { - tokens[parser->toksuper].size++; - } - break; - case '\t': - case '\r': - case '\n': - case ' ': - break; - case ':': - parser->toksuper = parser->toknext - 1; - break; - case ',': - if (tokens != NULL && parser->toksuper != -1 && - tokens[parser->toksuper].type != JSMN_ARRAY && - tokens[parser->toksuper].type != JSMN_OBJECT) { -#ifdef JSMN_PARENT_LINKS - parser->toksuper = tokens[parser->toksuper].parent; -#else - for (i = parser->toknext - 1; i >= 0; i--) { - if (tokens[i].type == JSMN_ARRAY || - tokens[i].type == JSMN_OBJECT) { - if (tokens[i].start != -1 && tokens[i].end == -1) { - parser->toksuper = i; - break; - } - } - } -#endif - } - break; -#ifdef JSMN_STRICT - /* In strict mode primitives are: numbers and booleans */ - case '-': - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - case 't': - case 'f': - case 'n': - /* And they must not be keys of the object */ - if (tokens != NULL && parser->toksuper != -1) { - const jsmntok_t *t = &tokens[parser->toksuper]; - if (t->type == JSMN_OBJECT || - (t->type == JSMN_STRING && t->size != 0)) { - return JSMN_ERROR_INVAL; - } - } -#else - /* In non-strict mode every unquoted value is a primitive */ - default: -#endif - r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens); - if (r < 0) { - return r; - } - count++; - if (parser->toksuper != -1 && tokens != NULL) { - tokens[parser->toksuper].size++; - } - break; - -#ifdef JSMN_STRICT - /* Unexpected char in strict mode */ - default: - return JSMN_ERROR_INVAL; -#endif - } - } - - if (tokens != NULL) { - for (i = parser->toknext - 1; i >= 0; i--) { - /* Unmatched opened object or array */ - if (tokens[i].start != -1 && tokens[i].end == -1) { - return JSMN_ERROR_PART; - } - } - } - - return count; -} - -/** - * Creates a new parser based over a given buffer with an array of tokens - * available. - */ -JSMN_API void jsmn_init(jsmn_parser *parser) -{ - parser->pos = 0; - parser->toknext = 0; - parser->toksuper = -1; -} - -#endif /* JSMN_HEADER */ - -#ifdef __cplusplus -} -#endif - -#endif /* JSMN_H */ diff --git a/vendor/krun-init-blob/src/lib.rs b/vendor/krun-init-blob/src/lib.rs deleted file mode 100644 index 4397da679..000000000 --- a/vendor/krun-init-blob/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -pub static INIT_BINARY: &[u8] = include_bytes!(env!("KRUN_INIT_BINARY_PATH")); diff --git a/vendor/krun-vmm/.cargo-ok b/vendor/krun-vmm/.cargo-ok deleted file mode 100644 index 5f8b79583..000000000 --- a/vendor/krun-vmm/.cargo-ok +++ /dev/null @@ -1 +0,0 @@ -{"v":1} \ No newline at end of file diff --git a/vendor/krun-vmm/.cargo_vcs_info.json b/vendor/krun-vmm/.cargo_vcs_info.json deleted file mode 100644 index 991eca9a9..000000000 --- a/vendor/krun-vmm/.cargo_vcs_info.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "git": { - "sha1": "654e4a6045858d5ced90efae106e91d0eca3469f" - }, - "path_in_vcs": "src/vmm" -} \ No newline at end of file diff --git a/vendor/krun-vmm/Cargo.lock b/vendor/krun-vmm/Cargo.lock deleted file mode 100644 index 30489b68c..000000000 --- a/vendor/krun-vmm/Cargo.lock +++ /dev/null @@ -1,1558 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "annotate-snippets" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4" -dependencies = [ - "anstyle", - "unicode-width", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bincode" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" -dependencies = [ - "bincode_derive", - "unty", -] - -[[package]] -name = "bincode_derive" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" -dependencies = [ - "virtue", -] - -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "annotate-snippets", - "bitflags 2.11.0", - "cexpr", - "clang-sys", - "itertools", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn", -] - -[[package]] -name = "bitfield" -version = "0.19.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21ba6517c6b0f2bf08be60e187ab64b038438f22dd755614d8fe4d4098c46419" -dependencies = [ - "bitfield-macros", -] - -[[package]] -name = "bitfield-macros" -version = "0.19.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f48d6ace212fdf1b45fd6b566bb40808415344642b76c3224c07c8df9da81e97" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bumpalo" -version = "3.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" - -[[package]] -name = "bzip2" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" -dependencies = [ - "bzip2-sys", -] - -[[package]] -name = "bzip2-sys" -version = "0.1.13+1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" -dependencies = [ - "cc", - "pkg-config", -] - -[[package]] -name = "caps" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd1ddba47aba30b6a889298ad0109c3b8dcb0e8fc993b459daa7067d46f865e0" -dependencies = [ - "libc", -] - -[[package]] -name = "cc" -version = "1.2.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom 7.1.3", -] - -[[package]] -name = "cfg-expr" -version = "0.20.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6b04e07d8080154ed4ac03546d9a2b303cc2fe1901ba0b35b301516e289368" -dependencies = [ - "smallvec", - "target-lexicon", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "convert_case" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "cookie-factory" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "imago" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae7cfee876c698a1a2ed9c705ab18f21acbed82110f19b51cc458de73426fe2c" -dependencies = [ - "async-trait", - "bincode", - "cfg-if", - "libc", - "miniz_oxide", - "nix", - "page_size", - "rustc_version", - "tokio", - "tracing", - "windows-sys", -] - -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "iocuddle" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8972d5be69940353d5347a1344cb375d9b457d6809b428b05bb1ca2fb9ce007" - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "kbs-types" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2119e8aaa1382675e879f0cbc95fc948d28284d134896f57676b8ef2c90212e" -dependencies = [ - "base64", - "serde", - "serde_json", - "sha2", - "sm3", - "strum", - "thiserror 2.0.18", -] - -[[package]] -name = "krun-arch" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e408be4923e881a3fb6536322d569a845e55fb0f5af4a9af3202ed97becbb192" -dependencies = [ - "krun-arch-gen", - "krun-smbios", - "krun-utils", - "kvm-bindings", - "kvm-ioctls", - "libc", - "tdx", - "vm-memory", - "vmm-sys-util 0.14.0", -] - -[[package]] -name = "krun-arch-gen" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e903977e89dbb2f77008307ce9953281f681a099e647b79e9220169278ce1970" - -[[package]] -name = "krun-cpuid" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e0bda6161895cc919dff67cf5d51d03085a93e2f2022aa52da406d758a566a" -dependencies = [ - "kvm-bindings", - "kvm-ioctls", - "vmm-sys-util 0.14.0", -] - -[[package]] -name = "krun-devices" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78dba5c05da51af1b53fc3eb7cb324f7929707d72641ece0da21216ee7d13f0f" -dependencies = [ - "bitflags 1.3.2", - "caps", - "crossbeam-channel", - "imago", - "krun-arch", - "krun-display", - "krun-hvf", - "krun-input", - "krun-polly", - "krun-rutabaga-gfx", - "krun-utils", - "kvm-bindings", - "kvm-ioctls", - "libc", - "libloading", - "log", - "lru", - "nix", - "pipewire", - "rand", - "thiserror 2.0.18", - "virtio-bindings", - "vm-fdt", - "vm-memory", - "zerocopy", -] - -[[package]] -name = "krun-display" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e13751337ed633505118ddea0182350eb2d0dbfffca299da641fe36c50e8e93" -dependencies = [ - "bindgen", - "bitflags 2.11.0", - "log", - "static_assertions", - "thiserror 2.0.18", -] - -[[package]] -name = "krun-hvf" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f7e78f0c5431195ca36aded1886024872699a6a56f0a600f619aeb7ef2161bc" -dependencies = [ - "crossbeam-channel", - "krun-arch", - "libloading", - "log", -] - -[[package]] -name = "krun-input" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f67de5755f01ea31499764a4a850613e21ec209ad207b8e93156491e53c2d3" -dependencies = [ - "bindgen", - "bitflags 2.11.0", - "libc", - "log", - "static_assertions", - "thiserror 2.0.18", -] - -[[package]] -name = "krun-kernel" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48953b0f707aafee40684fdf45fcb0ea068745aaa9500f672c1a39da113e97a" -dependencies = [ - "krun-utils", - "vm-memory", -] - -[[package]] -name = "krun-polly" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d2c61890357072d5751ef813aea744b93a67bfc3b820f36061f9706f72af84d" -dependencies = [ - "krun-utils", - "libc", -] - -[[package]] -name = "krun-rutabaga-gfx" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cb903397f5798f49d0aa8e481e54a79ef163e0621f1020a363e798081210991" -dependencies = [ - "anyhow", - "cfg-if", - "libc", - "log", - "nix", - "pkg-config", - "remain", - "thiserror 1.0.69", - "vmm-sys-util 0.14.0", - "winapi", - "zerocopy", -] - -[[package]] -name = "krun-smbios" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01462ad735097a9a9650564e7f7ba082db720a41696f94ec9fb56ecaf072c744" -dependencies = [ - "vm-memory", -] - -[[package]] -name = "krun-utils" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8315551f3fd0c86191ff14318ac595a1e62d9c1e0690d30355d3a4e0ac741c87" -dependencies = [ - "bitflags 1.3.2", - "crossbeam-channel", - "kvm-bindings", - "libc", - "log", - "nix", - "vmm-sys-util 0.14.0", -] - -[[package]] -name = "krun-vmm" -version = "0.1.0-1.19.3" -dependencies = [ - "bitfield", - "bitflags 2.11.0", - "bzip2", - "crossbeam-channel", - "flate2", - "iocuddle", - "kbs-types", - "krun-arch", - "krun-arch-gen", - "krun-cpuid", - "krun-devices", - "krun-display", - "krun-hvf", - "krun-input", - "krun-kernel", - "krun-polly", - "krun-utils", - "kvm-bindings", - "kvm-ioctls", - "libc", - "linux-loader", - "log", - "nix", - "serde", - "serde_json", - "tdx", - "vm-memory", - "vmm-sys-util 0.15.0", - "zstd", -] - -[[package]] -name = "kvm-bindings" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a537873e15e8daabb416667e606d9b0abc2a8fb9a45bd5853b888ae0ead82f9" -dependencies = [ - "vmm-sys-util 0.14.0", -] - -[[package]] -name = "kvm-ioctls" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8f7370330b4f57981e300fa39b02088f2f2a5c2d0f1f994e8090589619c56d" -dependencies = [ - "bitflags 2.11.0", - "kvm-bindings", - "libc", - "vmm-sys-util 0.14.0", -] - -[[package]] -name = "libc" -version = "0.2.183" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" - -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link", -] - -[[package]] -name = "libspa" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6b8cfa2a7656627b4c92c6b9ef929433acd673d5ab3708cda1b18478ac00df4" -dependencies = [ - "bitflags 2.11.0", - "cc", - "convert_case", - "cookie-factory", - "libc", - "libspa-sys", - "nix", - "nom 8.0.0", - "system-deps", -] - -[[package]] -name = "libspa-sys" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "901049455d2eb6decf9058235d745237952f4804bc584c5fcb41412e6adcc6e0" -dependencies = [ - "bindgen", - "cc", - "system-deps", -] - -[[package]] -name = "linux-loader" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de72cb02c55ecffcf75fe78295926f872eb6eb0a58d629c58a8c324dc26380f6" -dependencies = [ - "vm-memory", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "lru" -version = "0.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" -dependencies = [ - "hashbrown", -] - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags 2.11.0", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "nom" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "page_size" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pipewire" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9688b89abf11d756499f7c6190711d6dbe5a3acdb30c8fbf001d6596d06a8d44" -dependencies = [ - "anyhow", - "bitflags 2.11.0", - "libc", - "libspa", - "libspa-sys", - "nix", - "once_cell", - "pipewire-sys", - "thiserror 2.0.18", -] - -[[package]] -name = "pipewire-sys" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb028afee0d6ca17020b090e3b8fa2d7de23305aef975c7e5192a5050246ea36" -dependencies = [ - "bindgen", - "libspa-sys", - "system-deps", -] - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" - -[[package]] -name = "remain" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7ef12e84481ab4006cb942f8682bba28ece7270743e649442027c5db87df126" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_spanned" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" -dependencies = [ - "serde_core", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "simd-adler32" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" - -[[package]] -name = "sm3" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebb9a3b702d0a7e33bc4d85a14456633d2b165c2ad839c5fd9a8417c1ab15860" -dependencies = [ - "digest", -] - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strum" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "system-deps" -version = "7.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7" -dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml", - "version-compare", -] - -[[package]] -name = "target-lexicon" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" - -[[package]] -name = "tdx" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad59e5bf374211a1fdd8e7439a07d5a5e617fe97f5cf21d03bcd1bf8c82b73af" -dependencies = [ - "bitflags 2.11.0", - "iocuddle", - "kvm-bindings", - "kvm-ioctls", - "libc", - "uuid", - "vmm-sys-util 0.12.1", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio" -version = "1.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" -dependencies = [ - "pin-project-lite", -] - -[[package]] -name = "toml" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "toml_writer", - "winnow", -] - -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow", -] - -[[package]] -name = "toml_writer" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "unty" -version = "0.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" - -[[package]] -name = "uuid" -version = "1.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "version-compare" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "virtio-bindings" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091f1f09cfbf2a78563b562e7a949465cce1aef63b6065645188d995162f8868" - -[[package]] -name = "virtue" -version = "0.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" - -[[package]] -name = "vm-fdt" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e21282841a059bb62627ce8441c491f09603622cd5a21c43bfedc85a2952f23" - -[[package]] -name = "vm-memory" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f39348a049689cabd3377cdd9182bf526ec76a6f823b79903896452e9d7a7380" -dependencies = [ - "libc", - "thiserror 2.0.18", - "winapi", -] - -[[package]] -name = "vmm-sys-util" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1435039746e20da4f8d507a72ee1b916f7b4b05af7a91c093d2c6561934ede" -dependencies = [ - "bitflags 1.3.2", - "libc", -] - -[[package]] -name = "vmm-sys-util" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d21f366bf22bfba3e868349978766a965cbe628c323d58e026be80b8357ab789" -dependencies = [ - "bitflags 1.3.2", - "libc", -] - -[[package]] -name = "vmm-sys-util" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "506c62fdf617a5176827c2f9afbcf1be155b03a9b4bf9617a60dbc07e3a1642f" -dependencies = [ - "bitflags 1.3.2", - "libc", -] - -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "winnow" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" - -[[package]] -name = "zerocopy" -version = "0.8.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/vendor/krun-vmm/Cargo.toml b/vendor/krun-vmm/Cargo.toml deleted file mode 100644 index 709c5673e..000000000 --- a/vendor/krun-vmm/Cargo.toml +++ /dev/null @@ -1,201 +0,0 @@ -# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO -# -# When uploading crates to the registry Cargo will automatically -# "normalize" Cargo.toml files for maximal compatibility -# with all versions of Cargo and also rewrite `path` dependencies -# to registry (e.g., crates.io) dependencies. -# -# If you are reading this file be aware that the original Cargo.toml -# will likely look very different (and much more reasonable). -# See Cargo.toml.orig for the original contents. - -[package] -edition = "2021" -name = "krun-vmm" -version = "0.1.0-1.19.3" -authors = ["The libkrun Authors"] -build = "build.rs" -autolib = false -autobins = false -autoexamples = false -autotests = false -autobenches = false -description = "Virtual machine monitor for libkrun" -readme = false -license = "Apache-2.0" -repository = "https://github.com/containers/libkrun" - -[features] -amd-sev = [ - "blk", - "bitfield", - "bitflags", - "iocuddle", - "tee", - "kbs-types", - "serde", - "serde_json", - "devices/amd-sev", - "arch/amd-sev", -] -aws-nitro = [] -blk = ["devices/blk"] -efi = [ - "blk", - "net", - "devices/efi", - "arch/efi", -] -gpu = [ - "devices/gpu", - "krun_display", -] -input = [ - "devices/input", - "krun_input", -] -net = ["devices/net"] -snd = ["devices/snd"] -tdx = [ - "blk", - "tee", - "kbs-types", - "serde", - "serde_json", - "dep:tdx", - "devices/tdx", - "arch/tdx", - "cpuid/tdx", -] -tee = [ - "devices/tee", - "arch/tee", -] - -[lib] -name = "krun_vmm" -path = "src/lib.rs" - -[dependencies.arch] -version = "=0.1.0-1.19.3" -package = "krun-arch" - -[dependencies.arch_gen] -version = "=0.1.0-1.19.3" -package = "krun-arch-gen" - -[dependencies.bitfield] -version = "0.19.4" -optional = true - -[dependencies.bitflags] -version = "2.10.0" -optional = true - -[dependencies.crossbeam-channel] -version = ">=0.5.15" - -[dependencies.devices] -version = "=0.1.0-1.19.3" -package = "krun-devices" - -[dependencies.flate2] -version = "1.0.35" - -[dependencies.iocuddle] -version = "0.1.1" -optional = true - -[dependencies.kbs-types] -version = "0.13.0" -optional = true - -[dependencies.kernel] -version = "=0.1.0-1.19.3" -package = "krun-kernel" - -[dependencies.krun_display] -version = "0.1.0" -features = ["bindgen_clang_runtime"] -optional = true -package = "krun-display" - -[dependencies.krun_input] -version = "0.1.0" -features = ["bindgen_clang_runtime"] -optional = true -package = "krun-input" - -[dependencies.libc] -version = ">=0.2.39" - -[dependencies.linux-loader] -version = "0.13.2" -features = [ - "bzimage", - "elf", - "pe", -] - -[dependencies.log] -version = "0.4.0" - -[dependencies.nix] -version = "0.30.1" -features = [ - "fs", - "term", -] - -[dependencies.polly] -version = "=0.1.0-1.19.3" -package = "krun-polly" - -[dependencies.serde] -version = "1.0.125" -optional = true - -[dependencies.serde_json] -version = "1.0.64" -optional = true - -[dependencies.utils] -version = "=0.1.0-1.19.3" -package = "krun-utils" - -[dependencies.vm-memory] -version = "=0.17.1" -features = ["backend-mmap"] - -[dependencies.vmm-sys-util] -version = "0.15" - -[dev-dependencies.devices] -version = "=0.1.0-1.19.3" -features = ["test_utils"] -package = "krun-devices" - -[target.'cfg(target_arch = "x86_64")'.dependencies.bzip2] -version = "0.5" - -[target.'cfg(target_arch = "x86_64")'.dependencies.cpuid] -version = "=0.1.0-1.19.3" -package = "krun-cpuid" - -[target.'cfg(target_arch = "x86_64")'.dependencies.zstd] -version = "0.13" - -[target.'cfg(target_os = "linux")'.dependencies.kvm-bindings] -version = "0.12" -features = ["fam-wrappers"] - -[target.'cfg(target_os = "linux")'.dependencies.kvm-ioctls] -version = "0.22" - -[target.'cfg(target_os = "linux")'.dependencies.tdx] -version = "0.1.0" -optional = true - -[target.'cfg(target_os = "macos")'.dependencies.hvf] -version = "=0.1.0-1.19.3" -package = "krun-hvf" diff --git a/vendor/krun-vmm/Cargo.toml.orig b/vendor/krun-vmm/Cargo.toml.orig deleted file mode 100644 index 17a595270..000000000 --- a/vendor/krun-vmm/Cargo.toml.orig +++ /dev/null @@ -1,64 +0,0 @@ -[package] -name = "krun-vmm" -version = "0.1.0-1.19.3" -authors = ["The libkrun Authors"] -edition = "2021" -build = "build.rs" -description = "Virtual machine monitor for libkrun" -license = "Apache-2.0" -repository = "https://github.com/containers/libkrun" - -[features] -tee = ["devices/tee", "arch/tee"] -amd-sev = ["blk", "bitfield", "bitflags", "iocuddle", "tee", "kbs-types", "serde", "serde_json", "devices/amd-sev", "arch/amd-sev"] -tdx = ["blk", "tee", "kbs-types", "serde", "serde_json", "dep:tdx", "devices/tdx", "arch/tdx", "cpuid/tdx"] -net = ["devices/net"] -blk = ["devices/blk"] -efi = ["blk", "net", "devices/efi", "arch/efi"] -gpu = ["devices/gpu", "krun_display"] -snd = ["devices/snd"] -input = ["devices/input", "krun_input"] -aws-nitro = [] - -[dependencies] -crossbeam-channel = ">=0.5.15" -flate2 = "1.0.35" -libc = ">=0.2.39" -linux-loader = { version = "0.13.2", features = ["bzimage", "elf", "pe"] } -log = "0.4.0" -nix = { version = "0.30.1", features = ["fs", "term"] } -vm-memory = { version = "=0.17.1", features = ["backend-mmap"] } -vmm-sys-util = "0.15" -krun_display = { package = "krun-display", version = "0.1.0", path = "../display", optional = true, features = ["bindgen_clang_runtime"] } -krun_input = { package = "krun-input", version = "0.1.0", path = "../input", optional = true, features = ["bindgen_clang_runtime"] } - -arch = { package = "krun-arch", version = "=0.1.0-1.19.3", path = "../arch" } -arch_gen = { package = "krun-arch-gen", version = "=0.1.0-1.19.3", path = "../arch_gen" } -devices = { package = "krun-devices", version = "=0.1.0-1.19.3", path = "../devices" } -kernel = { package = "krun-kernel", version = "=0.1.0-1.19.3", path = "../kernel" } -utils = { package = "krun-utils", version = "=0.1.0-1.19.3", path = "../utils" } -polly = { package = "krun-polly", version = "=0.1.0-1.19.3", path = "../polly" } - -# Dependencies for amd-sev -kbs-types = { version = "0.13.0", optional = true } -serde = { version = "1.0.125", optional = true } -serde_json = { version = "1.0.64", optional = true } -iocuddle = { version = "0.1.1", optional = true } -bitfield = { version = "0.19.4", optional = true } -bitflags = { version = "2.10.0", optional = true } - -[target.'cfg(target_arch = "x86_64")'.dependencies] -bzip2 = "0.5" -cpuid = { package = "krun-cpuid", version = "=0.1.0-1.19.3", path = "../cpuid" } -zstd = "0.13" - -[target.'cfg(target_os = "linux")'.dependencies] -tdx = { version = "0.1.0", optional = true } -kvm-bindings = { version = "0.12", features = ["fam-wrappers"] } -kvm-ioctls = "0.22" - -[target.'cfg(target_os = "macos")'.dependencies] -hvf = { package = "krun-hvf", version = "=0.1.0-1.19.3", path = "../hvf" } - -[dev-dependencies] -devices = { package = "krun-devices", version = "=0.1.0-1.19.3", path = "../devices", features = ["test_utils"] } diff --git a/vendor/krun-vmm/build.rs b/vendor/krun-vmm/build.rs deleted file mode 100644 index 93b6ed21d..000000000 --- a/vendor/krun-vmm/build.rs +++ /dev/null @@ -1,18 +0,0 @@ -fn main() { - if std::env::var("CARGO_CFG_TARGET_ARCH").as_deref() == Ok("aarch64") - && std::env::var_os("CARGO_FEATURE_EFI").is_some() - { - let edk2_binary_path = std::env::var("KRUN_EDK2_BINARY_PATH").unwrap_or_else(|_| { - format!( - "{}/edk2/KRUN_EFI.silent.fd", - std::env::var("CARGO_MANIFEST_DIR").unwrap() - ) - }); - println!("cargo:rustc-env=KRUN_EDK2_BINARY_PATH={edk2_binary_path}"); - println!("cargo:rerun-if-env-changed=KRUN_EDK2_BINARY_PATH"); - } - - if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") { - println!("cargo:rustc-link-lib=framework=Hypervisor"); - } -} diff --git a/vendor/krun-vmm/edk2/KRUN_EFI.silent.fd b/vendor/krun-vmm/edk2/KRUN_EFI.silent.fd deleted file mode 100644 index 6ae840f9817c79380be05eaac2657b7a0c7b7685..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2097152 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 Fr8A=WO)w+K-qoYh$IKhwpHN&%^`W$Z$K69aI| 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 z00@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p z2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?x zfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=9 z00@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p z2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?x zfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=9 z00@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p z2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?x zfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=9 z00@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p z2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?x zfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=9 z00@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p z2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?x zfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=9 z00@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p z2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?x zfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=9 z00@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p z2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?x zfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=9 z00@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p z2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?x zfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2>jO$9SZ;e000c~x1Pk#Ldbvt z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM s7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*@R1jPzsyZ`_I diff --git a/vendor/krun-vmm/edk2/License.txt b/vendor/krun-vmm/edk2/License.txt deleted file mode 100644 index ee840505c..000000000 --- a/vendor/krun-vmm/edk2/License.txt +++ /dev/null @@ -1,51 +0,0 @@ -Copyright (c) 2019, TianoCore and contributors. All rights reserved. - -SPDX-License-Identifier: BSD-2-Clause-Patent - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -Subject to the terms and conditions of this license, each copyright holder -and contributor hereby grants to those receiving rights under this license -a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable -(except for failure to satisfy the conditions of this license) patent -license to make, have made, use, offer to sell, sell, import, and otherwise -transfer this software, where such license applies only to those patent -claims, already acquired or hereafter acquired, licensable by such copyright -holder or contributor that are necessarily infringed by: - -(a) their Contribution(s) (the licensed copyrights of copyright holders and - non-copyrightable additions of contributors, in source or binary form) - alone; or - -(b) combination of their Contribution(s) with the work of authorship to - which such Contribution(s) was added by such copyright holder or - contributor, if, at the time the Contribution is added, such addition - causes such combination to be necessarily infringed. The patent license - shall not apply to any other combinations which include the - Contribution. - -Except as expressly stated above, no rights or licenses from any copyright -holder or contributor is granted under this license, whether expressly, by -implication, estoppel or otherwise. - -DISCLAIMER - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/krun-vmm/edk2/Sources.txt b/vendor/krun-vmm/edk2/Sources.txt deleted file mode 100644 index d2629c9d1..000000000 --- a/vendor/krun-vmm/edk2/Sources.txt +++ /dev/null @@ -1 +0,0 @@ -KRUN_EFI.silent.fd was built from commit 13e8adac8a83141b51375c799996946082e1eb43 of the https://github.com/slp/edk2 repository. diff --git a/vendor/krun-vmm/src/builder.rs b/vendor/krun-vmm/src/builder.rs deleted file mode 100644 index a96275b17..000000000 --- a/vendor/krun-vmm/src/builder.rs +++ /dev/null @@ -1,2494 +0,0 @@ -// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! Enables pre-boot setup, instantiation and booting of a Firecracker VMM. - -#[cfg(target_os = "macos")] -use crossbeam_channel::unbounded; -use crossbeam_channel::Sender; -use kernel::cmdline::Cmdline; -#[cfg(target_os = "macos")] -use std::collections::HashMap; -use std::fmt::{Display, Formatter}; -use std::fs::File; -use std::io::{self, IsTerminal, Read}; -use std::os::fd::AsRawFd; -use std::os::fd::{BorrowedFd, FromRawFd}; -use std::path::PathBuf; -use std::sync::atomic::AtomicI32; -use std::sync::{Arc, Mutex}; - -use super::{Error, Vmm}; - -#[cfg(target_arch = "x86_64")] -use crate::device_manager::legacy::PortIODeviceManager; -use crate::device_manager::mmio::MMIODeviceManager; -use crate::resources::{ - DefaultVirtioConsoleConfig, PortConfig, TsiFlags, VirtioConsoleConfigMode, VmResources, -}; -use crate::vmm_config::external_kernel::{ExternalKernel, KernelFormat}; -#[cfg(feature = "net")] -use crate::vmm_config::net::NetBuilder; -#[cfg(target_arch = "x86_64")] -use devices::legacy::Cmos; -#[cfg(all(target_os = "linux", target_arch = "riscv64"))] -use devices::legacy::KvmAia; -#[cfg(target_arch = "x86_64")] -use devices::legacy::KvmIoapic; -use devices::legacy::Serial; -#[cfg(target_os = "macos")] -use devices::legacy::VcpuList; -#[cfg(target_os = "macos")] -use devices::legacy::{GicV3, HvfGicV3}; -#[cfg(target_arch = "x86_64")] -use devices::legacy::{IoApic, IrqChipT}; -use devices::legacy::{IrqChip, IrqChipDevice}; -#[cfg(all(target_os = "linux", target_arch = "aarch64"))] -use devices::legacy::{KvmGicV2, KvmGicV3}; -use devices::virtio::{port_io, MmioTransport, PortDescription, VirtioDevice, Vsock}; - -#[cfg(feature = "tee")] -use kbs_types::Tee; - -use crate::device_manager; -#[cfg(target_os = "linux")] -use crate::signal_handler::register_sigint_handler; -#[cfg(target_os = "linux")] -use crate::signal_handler::register_sigwinch_handler; -use crate::terminal::{term_restore_mode, term_set_raw_mode}; -#[cfg(feature = "blk")] -use crate::vmm_config::block::BlockBuilder; -#[cfg(not(any(feature = "tee", feature = "aws-nitro")))] -use crate::vmm_config::fs::FsDeviceConfig; -use crate::vmm_config::kernel_cmdline::DEFAULT_KERNEL_CMDLINE; -#[cfg(target_os = "linux")] -use crate::vstate::KvmContext; -#[cfg(all(target_os = "linux", feature = "tee"))] -use crate::vstate::MeasuredRegion; -use crate::vstate::{Error as VstateError, Vcpu, VcpuConfig, Vm}; -use arch::{ArchMemoryInfo, InitrdConfig}; -use device_manager::shm::ShmManager; -#[cfg(feature = "gpu")] -use devices::virtio::display::DisplayInfo; -#[cfg(feature = "gpu")] -use devices::virtio::display::NoopDisplayBackend; -#[cfg(not(any(feature = "tee", feature = "aws-nitro")))] -use devices::virtio::{fs::ExportTable, VirtioShmRegion}; -use flate2::read::GzDecoder; -#[cfg(feature = "gpu")] -use krun_display::DisplayBackend; -#[cfg(feature = "gpu")] -use krun_display::IntoDisplayBackend; -#[cfg(feature = "amd-sev")] -use kvm_bindings::KVM_MAX_CPUID_ENTRIES; -use libc::{STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO}; -#[cfg(target_arch = "x86_64")] -use linux_loader::loader::{self, KernelLoader}; -use nix::unistd::isatty; -use polly::event_manager::{Error as EventManagerError, EventManager}; -use utils::eventfd::EventFd; -use utils::worker_message::WorkerMessage; -#[cfg(all(target_arch = "x86_64", not(feature = "efi"), not(feature = "tee")))] -use vm_memory::mmap::MmapRegion; -#[cfg(not(any(feature = "tee", feature = "aws-nitro")))] -use vm_memory::Address; -use vm_memory::Bytes; -#[cfg(not(feature = "aws-nitro"))] -use vm_memory::GuestMemory; -#[cfg(all(target_arch = "x86_64", not(feature = "tee")))] -use vm_memory::GuestRegionMmap; -use vm_memory::{GuestAddress, GuestMemoryMmap}; - -#[cfg(all(target_arch = "aarch64", feature = "efi"))] -#[allow(dead_code)] -static EDK2_BINARY: &[u8] = include_bytes!(env!("KRUN_EDK2_BINARY_PATH")); - -/// Errors associated with starting the instance. -#[derive(Debug)] -pub enum StartMicrovmError { - /// Unable to attach block device to Vmm. - AttachBlockDevice(io::Error), - #[cfg(target_os = "macos")] - /// Failed to create HVF in-kernel IrqChip. - CreateHvfIrqChip(hvf::Error), - #[cfg(target_os = "linux")] - /// Failed to create KVM in-kernel IrqChip. - CreateKvmIrqChip(kvm_ioctls::Error), - /// Failed to create a `RateLimiter` object. - CreateRateLimiter(io::Error), - /// Cannot open the file containing the kernel code. - ElfOpenKernel(io::Error), - /// Cannot load the kernel into the VM. - ElfLoadKernel(linux_loader::loader::Error), - /// The firmware can't be loaded into the provided memory address. - FirmwareInvalidAddress(vm_memory::GuestMemoryError), - /// Cannot read firmware contents from file. - FirmwareRead(io::Error), - /// Memory regions are overlapping or mmap fails. - GuestMemoryMmap(String), - /// The BZIP2 decoder couldn't decompress the kernel. - ImageBz2Decoder(io::Error), - /// Cannot find compressed kernel in file. - ImageBz2Invalid, - /// Cannot load the kernel from the uncompressed ELF data. - ImageBz2LoadKernel(linux_loader::loader::Error), - /// Cannot open the file containing the kernel code. - ImageBz2OpenKernel(io::Error), - /// The GZIP decoder couldn't decompress the kernel. - ImageGzDecoder(io::Error), - /// Cannot find compressed kernel in file. - ImageGzInvalid, - /// Cannot load the kernel from the uncompressed ELF data. - ImageGzLoadKernel(linux_loader::loader::Error), - /// Cannot open the file containing the kernel code. - ImageGzOpenKernel(io::Error), - /// The ZSTD decoder couldn't decompress the kernel. - ImageZstdDecoder(io::Error), - /// Cannot find compressed kernel in file. - ImageZstdInvalid, - /// Cannot load the kernel from the uncompressed ELF data. - ImageZstdLoadKernel(linux_loader::loader::Error), - /// Cannot open the file containing the kernel code. - ImageZstdOpenKernel(io::Error), - /// Cannot load initrd due to an invalid memory configuration. - InitrdLoad, - /// Cannot load initrd due to an invalid image. - InitrdRead(io::Error), - /// Internal error encountered while starting a microVM. - Internal(Error), - /// Cannot inject the kernel into the guest memory due to a problem with the bundle. - InvalidKernelBundle(vm_memory::mmap::MmapRegionError), - /// The kernel command line is invalid. - KernelCmdline(String), - /// The kernel doesn't fit into the microVM memory. - KernelDoesNotFit(u64, usize), - /// The supplied kernel format is not supported. - KernelFormatUnsupported, - /// Cannot load command line string. - LoadCommandline(kernel::cmdline::Error), - /// The start command was issued more than once. - MicroVMAlreadyRunning, - /// Cannot start the VM because the kernel was not configured. - MissingKernelConfig, - /// Cannot start the VM because the size of the guest memory was not specified. - MissingMemSizeConfig, - /// The net device configuration is missing the tap device. - NetDeviceNotConfigured, - /// Cannot open the block device backing file. - OpenBlockDevice(io::Error), - /// Cannot open console output file. - OpenConsoleFile(io::Error), - /// The GZIP decoder couldn't decompress the kernel. - PeGzDecoder(io::Error), - /// Cannot open the file containing the kernel code. - PeGzOpenKernel(io::Error), - /// Cannot find compressed kernel in file. - PeGzInvalid, - /// Cannot open the file containing the kernel code. - RawOpenKernel(io::Error), - /// Cannot initialize a MMIO Balloon device or add a device to the MMIO Bus. - RegisterBalloonDevice(device_manager::mmio::Error), - /// Cannot initialize a MMIO Block Device or add a device to the MMIO Bus. - RegisterBlockDevice(device_manager::mmio::Error), - /// Cannot register an EventHandler. - RegisterEvent(EventManagerError), - /// Cannot initialize a MMIO Fs Device or add ad device to the MMIO Bus. - RegisterFsDevice(device_manager::mmio::Error), - // Cannot initialize a MMIO Fs Device or add ad device to the MMIO Bus. - RegisterConsoleDevice(device_manager::mmio::Error), - /// Cannot register SIGWINCH event file descriptor. - #[cfg(target_os = "linux")] - RegisterFsSigwinch(kvm_ioctls::Error), - /// Cannot initialize a MMIO Gpu device or add a device to the MMIO Bus. - RegisterGpuDevice(device_manager::mmio::Error), - /// Cannot initialize a MMIO Input device or add a device to the MMIO Bus. - RegisterInputDevice(device_manager::mmio::Error), - /// Cannot initialize a MMIO Network Device or add a device to the MMIO Bus. - RegisterNetDevice(device_manager::mmio::Error), - /// Cannot initialize a MMIO Rng device or add a device to the MMIO Bus. - RegisterRngDevice(device_manager::mmio::Error), - /// Cannot initialize a MMIO Snd device or add a device to the MMIO Bus. - RegisterSndDevice(device_manager::mmio::Error), - /// Cannot initialize a MMIO Vsock Device or add a device to the MMIO Bus. - RegisterVsockDevice(device_manager::mmio::Error), - /// Cannot attest the VM in the Secure Virtualization context. - SecureVirtAttest(VstateError), - /// Cannot initialize the Secure Virtualization backend. - SecureVirtPrepare(VstateError), - /// Error configuring an SHM region. - ShmConfig(device_manager::shm::Error), - /// Error creating an SHM region. - ShmCreate(device_manager::shm::Error), - /// Error obtaining the host address of an SHM region. - ShmHostAddr(vm_memory::GuestMemoryError), - /// The TEE specified is not supported. - InvalidTee, -} - -/// It's convenient to automatically convert `kernel::cmdline::Error`s -/// to `StartMicrovmError`s. -impl std::convert::From for StartMicrovmError { - fn from(e: kernel::cmdline::Error) -> StartMicrovmError { - StartMicrovmError::KernelCmdline(e.to_string()) - } -} - -impl Display for StartMicrovmError { - fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { - use self::StartMicrovmError::*; - match *self { - AttachBlockDevice(ref err) => { - write!(f, "Unable to attach block device to Vmm. Error: {err}") - } - #[cfg(target_os = "macos")] - CreateHvfIrqChip(ref err) => { - write!(f, "Cannot create HVF in-kernel IrqChip: {err}") - } - #[cfg(target_os = "linux")] - CreateKvmIrqChip(ref err) => { - write!(f, "Cannot create KVM in-kernel IrqChip: {err}") - } - CreateRateLimiter(ref err) => write!(f, "Cannot create RateLimiter: {err}"), - ElfOpenKernel(ref err) => { - write!(f, "Cannot open the file containing the kernel code: {err}") - } - ElfLoadKernel(ref err) => { - write!(f, "Cannot load the kernel into the VM: {err}") - } - FirmwareInvalidAddress(ref err) => { - write!( - f, - "The firmware can't be loaded into the guest memory: {err}" - ) - } - FirmwareRead(ref err) => { - write!(f, "Cannot read firmware contents from file: {err}") - } - GuestMemoryMmap(ref err) => { - // Remove imbricated quotes from error message. - let mut err_msg = format!("{err:?}"); - err_msg = err_msg.replace('\"', ""); - write!(f, "Invalid Memory Configuration: {err_msg}") - } - ImageBz2Decoder(ref err) => { - write!(f, "The BZIP2 decoder couldn't decompress the kernel. {err}") - } - ImageBz2Invalid => { - write!(f, "Cannot find compressed kernel in file.") - } - ImageBz2LoadKernel(ref err) => { - write!( - f, - "Cannot load the kernel from the uncompressed ELF data. {err}" - ) - } - ImageBz2OpenKernel(ref err) => { - write!(f, "Cannot open the file containing the kernel code. {err}") - } - ImageGzDecoder(ref err) => { - write!(f, "The GZIP decoder couldn't decompress the kernel. {err}") - } - ImageGzInvalid => { - write!(f, "Cannot find compressed kernel in file.") - } - ImageGzLoadKernel(ref err) => { - write!( - f, - "Cannot load the kernel from the uncompressed ELF data. {err}" - ) - } - ImageGzOpenKernel(ref err) => { - write!(f, "Cannot open the file containing the kernel code. {err}") - } - ImageZstdDecoder(ref err) => { - write!(f, "The ZSTD decoder couldn't decompress the kernel. {err}") - } - ImageZstdInvalid => { - write!(f, "Cannot find compressed kernel in file.") - } - ImageZstdLoadKernel(ref err) => { - write!( - f, - "Cannot load the kernel from the uncompressed ELF data. {err}" - ) - } - ImageZstdOpenKernel(ref err) => { - write!(f, "Cannot open the file containing the kernel code. {err}") - } - InitrdLoad => write!( - f, - "Cannot load initrd due to an invalid memory configuration." - ), - InitrdRead(ref err) => write!(f, "Cannot load initrd due to an invalid image: {err}"), - Internal(ref err) => write!(f, "Internal error while starting microVM: {err:?}"), - InvalidKernelBundle(ref err) => { - let mut err_msg = format!("{err}"); - err_msg = err_msg.replace('\"', ""); - write!( - f, - "Cannot inject the kernel into the guest memory due to a problem with the \ - bundle. {err_msg}" - ) - } - KernelCmdline(ref err) => write!(f, "Invalid kernel command line: {err}"), - KernelDoesNotFit(load_addr, size) => write!( - f, - "The kernel doesn't fit in the microVM memory (load_addr={load_addr}, size={size})" - ), - KernelFormatUnsupported => { - write!(f, "The supplied kernel format is not supported.") - } - LoadCommandline(ref err) => { - let mut err_msg = format!("{err}"); - err_msg = err_msg.replace('\"', ""); - write!(f, "Cannot load command line string. {err_msg}") - } - MicroVMAlreadyRunning => write!(f, "Microvm already running."), - MissingKernelConfig => write!(f, "Cannot start microvm without kernel configuration."), - MissingMemSizeConfig => { - write!(f, "Cannot start microvm without guest mem_size config.") - } - NetDeviceNotConfigured => { - write!(f, "The net device configuration is missing the tap device.") - } - OpenBlockDevice(ref err) => { - let mut err_msg = format!("{err:?}"); - err_msg = err_msg.replace('\"', ""); - - write!(f, "Cannot open the block device backing file. {err_msg}") - } - OpenConsoleFile(ref err) => { - let mut err_msg = format!("{err:?}"); - err_msg = err_msg.replace('\"', ""); - - write!(f, "Cannot open the console output file. {err_msg}") - } - PeGzDecoder(ref err) => { - write!(f, "The GZIP decoder couldn't decompress the kernel. {err}") - } - PeGzOpenKernel(ref err) => { - write!(f, "Cannot open the file containing the kernel code. {err}") - } - PeGzInvalid => { - write!(f, "Cannot find compressed kernel in file.") - } - RawOpenKernel(ref err) => { - write!(f, "Cannot open the file containing the kernel code: {err}") - } - RegisterBalloonDevice(ref err) => { - let mut err_msg = format!("{err}"); - err_msg = err_msg.replace('\"', ""); - write!( - f, - "Cannot initialize a MMIO Balloon Device or add a device to the MMIO Bus. {err_msg}" - ) - } - RegisterBlockDevice(ref err) => { - let mut err_msg = format!("{err}"); - err_msg = err_msg.replace('\"', ""); - write!( - f, - "Cannot initialize a MMIO Block Device or add a device to the MMIO Bus. {err_msg}" - ) - } - RegisterEvent(ref err) => write!(f, "Cannot register EventHandler. {err:?}"), - RegisterFsDevice(ref err) => { - let mut err_msg = format!("{err}"); - err_msg = err_msg.replace('\"', ""); - - write!( - f, - "Cannot initialize a MMIO Fs Device or add a device to the MMIO Bus. {err_msg}" - ) - } - RegisterConsoleDevice(ref err) => { - let mut err_msg = format!("{err}"); - err_msg = err_msg.replace('\"', ""); - - write!( - f, - "Cannot initialize a MMIO Console Device or add a device to the MMIO Bus. {err_msg}" - ) - } - #[cfg(target_os = "linux")] - RegisterFsSigwinch(ref err) => { - let mut err_msg = format!("{err}"); - err_msg = err_msg.replace('\"', ""); - - write!( - f, - "Cannot register SIGWINCH file descriptor for Fs Device. {err_msg}" - ) - } - RegisterGpuDevice(ref err) => { - let mut err_msg = format!("{err}"); - err_msg = err_msg.replace('\"', ""); - write!( - f, - "Cannot initialize a MMIO Gpu Device or add a device to the MMIO Bus. {err_msg}" - ) - } - RegisterInputDevice(ref err) => { - let mut err_msg = format!("{err}"); - err_msg = err_msg.replace('\"', ""); - write!( - f, - "Cannot initialize a MMIO Input Device or add a device to the MMIO Bus. {err_msg}" - ) - } - RegisterNetDevice(ref err) => { - let mut err_msg = format!("{err}"); - err_msg = err_msg.replace('\"', ""); - - write!( - f, - "Cannot initialize a MMIO Network Device or add a device to the MMIO Bus. {err_msg}" - ) - } - RegisterRngDevice(ref err) => { - let mut err_msg = format!("{err}"); - err_msg = err_msg.replace('\"', ""); - write!( - f, - "Cannot initialize a MMIO Rng Device or add a device to the MMIO Bus. {err_msg}" - ) - } - RegisterSndDevice(ref err) => { - let mut err_msg = format!("{err}"); - err_msg = err_msg.replace('\"', ""); - write!( - f, - "Cannot initialize a MMIO Snd Device or add a device to the MMIO Bus. {err_msg}" - ) - } - RegisterVsockDevice(ref err) => { - let mut err_msg = format!("{err}"); - err_msg = err_msg.replace('\"', ""); - - write!( - f, - "Cannot initialize a MMIO Vsock Device or add a device to the MMIO Bus. {err_msg}" - ) - } - SecureVirtAttest(ref err) => { - let mut err_msg = format!("{err}"); - err_msg = err_msg.replace('\"', ""); - - write!( - f, - "Cannot attest the VM in the Secure Virtualization context. {err_msg}" - ) - } - SecureVirtPrepare(ref err) => { - let mut err_msg = format!("{err}"); - err_msg = err_msg.replace('\"', ""); - - write!( - f, - "Cannot initialize the Secure Virtualization backend. {err_msg}" - ) - } - ShmHostAddr(ref err) => { - let mut err_msg = format!("{err:?}"); - err_msg = err_msg.replace('\"', ""); - - write!( - f, - "Error obtaining the host address of an SHM region. {err_msg}" - ) - } - ShmConfig(ref err) => { - let mut err_msg = format!("{err:?}"); - err_msg = err_msg.replace('\"', ""); - - write!(f, "Error while configuring an SHM region. {err_msg}") - } - ShmCreate(ref err) => { - let mut err_msg = format!("{err:?}"); - err_msg = err_msg.replace('\"', ""); - - write!(f, "Error while creating an SHM region. {err_msg}") - } - InvalidTee => { - write!(f, "TEE selected is not currently supported") - } - } - } -} - -pub enum Payload { - #[cfg(all(target_arch = "x86_64", not(feature = "tee")))] - KernelMmap, - #[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))] - KernelCopy, - ExternalKernel(ExternalKernel), - #[cfg(test)] - Empty, - Firmware, - #[cfg(feature = "tee")] - Tee, -} - -fn choose_payload(vm_resources: &VmResources) -> Result { - if let Some(_kernel_bundle) = &vm_resources.kernel_bundle { - #[cfg(feature = "tee")] - if vm_resources.qboot_bundle.is_none() || vm_resources.initrd_bundle.is_none() { - return Err(StartMicrovmError::MissingKernelConfig); - } - - #[cfg(feature = "tee")] - return Ok(Payload::Tee); - - #[cfg(all(target_os = "linux", target_arch = "x86_64", not(feature = "tee")))] - return Ok(Payload::KernelMmap); - - #[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))] - return Ok(Payload::KernelCopy); - } else if let Some(external_kernel) = vm_resources.external_kernel() { - Ok(Payload::ExternalKernel(external_kernel.clone())) - } else if cfg!(feature = "efi") || vm_resources.firmware_config.is_some() { - Ok(Payload::Firmware) - } else { - Err(StartMicrovmError::MissingKernelConfig) - } -} - -/// Builds and starts a microVM based on the current Firecracker VmResources configuration. -/// -/// This is the default build recipe, one could build other microVM flavors by using the -/// independent functions in this module instead of calling this recipe. -/// -/// An `Arc` reference of the built `Vmm` is also plugged in the `EventManager`, while another -/// is returned. -pub fn build_microvm( - vm_resources: &super::resources::VmResources, - event_manager: &mut EventManager, - _shutdown_efd: Option, - _sender: Sender, -) -> std::result::Result>, StartMicrovmError> { - let payload = choose_payload(vm_resources)?; - - let (guest_memory, arch_memory_info, mut _shm_manager, payload_config) = create_guest_memory( - vm_resources - .vm_config() - .mem_size_mib - .ok_or(StartMicrovmError::MissingMemSizeConfig)?, - vm_resources, - &payload, - )?; - - let vcpu_config = vm_resources.vcpu_config(); - - // Clone the command-line so that a failed boot doesn't pollute the original. - #[allow(unused_mut)] - let mut kernel_cmdline = Cmdline::new(arch::CMDLINE_MAX_SIZE); - if let Some(cmdline) = payload_config.kernel_cmdline { - kernel_cmdline.insert_str(cmdline.as_str()).unwrap(); - } else if let Some(cmdline) = &vm_resources.kernel_cmdline.prolog { - kernel_cmdline.insert_str(cmdline).unwrap(); - } else { - kernel_cmdline.insert_str(DEFAULT_KERNEL_CMDLINE).unwrap(); - } - - if let Some(cmdline) = &vm_resources.kernel_cmdline.krun_env { - kernel_cmdline.insert_str(cmdline.as_str()).unwrap(); - } - - if let Some(kernel_console) = &vm_resources.kernel_console { - let cmdline = kernel_cmdline.as_str(); - let console_start_idx = cmdline.find("console=").unwrap(); - let console_end_idx = cmdline - .get(console_start_idx..) - .and_then(|s| s.find(" ").map(|i| i + console_start_idx)); - - let cmdline = cmdline.replace( - &cmdline[console_start_idx..console_end_idx.unwrap()], - format!("console={kernel_console}").as_str(), - ); - kernel_cmdline = Cmdline::new(arch::CMDLINE_MAX_SIZE); - kernel_cmdline.insert_str(cmdline).unwrap(); - } - - #[cfg(not(feature = "tee"))] - #[allow(unused_mut)] - let mut vm = setup_vm(&guest_memory, vm_resources.nested_enabled)?; - - #[cfg(feature = "tee")] - let (_kvm, vm) = { - let kvm = KvmContext::new() - .map_err(Error::KvmContext) - .map_err(StartMicrovmError::Internal)?; - let vm = setup_vm( - &kvm, - &guest_memory, - vm_resources, - #[cfg(feature = "tdx")] - _sender.clone(), - )?; - (kvm, vm) - }; - - #[cfg(feature = "tee")] - let tee = vm_resources.tee_config().tee; - - #[cfg(feature = "amd-sev")] - let snp_launcher = match tee { - Tee::Snp => Some( - vm.snp_secure_virt_prepare(&guest_memory) - .map_err(StartMicrovmError::SecureVirtPrepare)?, - ), - _ => None, - }; - - #[cfg(feature = "tdx")] - let mut tdx_launcher = match tee { - Tee::Tdx => vm - .tdx_secure_virt_prepare() - .map_err(StartMicrovmError::SecureVirtPrepare)?, - _ => panic!(), - }; - - #[cfg(all(feature = "tee", not(feature = "tdx")))] - let measured_regions = { - println!("Injecting and measuring memory regions. This may take a while."); - - let qboot_size = if let Some(qboot_bundle) = &vm_resources.qboot_bundle { - qboot_bundle.size - } else { - return Err(StartMicrovmError::MissingKernelConfig); - }; - let (kernel_guest_addr, kernel_size) = - if let Some(kernel_bundle) = &vm_resources.kernel_bundle { - (kernel_bundle.guest_addr, kernel_bundle.size) - } else { - return Err(StartMicrovmError::MissingKernelConfig); - }; - let (initrd_addr, initrd_size) = if let Some(initrd_config) = &payload_config.initrd_config - { - (initrd_config.address, initrd_config.size) - } else { - return Err(StartMicrovmError::MissingKernelConfig); - }; - - vec![ - MeasuredRegion { - guest_addr: arch::FIRMWARE_START, - host_addr: guest_memory - .get_host_address(GuestAddress(arch::FIRMWARE_START)) - .unwrap() as u64, - size: qboot_size, - }, - MeasuredRegion { - guest_addr: kernel_guest_addr, - host_addr: guest_memory - .get_host_address(GuestAddress(kernel_guest_addr)) - .unwrap() as u64, - size: kernel_size, - }, - MeasuredRegion { - guest_addr: initrd_addr.0, - host_addr: guest_memory.get_host_address(initrd_addr).unwrap() as u64, - size: initrd_size, - }, - MeasuredRegion { - guest_addr: arch::x86_64::layout::ZERO_PAGE_START, - host_addr: guest_memory - .get_host_address(GuestAddress(arch::x86_64::layout::ZERO_PAGE_START)) - .unwrap() as u64, - size: 4096, - }, - ] - }; - - #[cfg(feature = "tdx")] - let measured_regions = { - println!("Injecting and measuring memory regions. This may take a while."); - let qboot_size = if let Some(qboot_bundle) = &vm_resources.qboot_bundle { - qboot_bundle.size - } else { - return Err(StartMicrovmError::MissingKernelConfig); - }; - let m = vec![ - MeasuredRegion { - guest_addr: 0, - host_addr: guest_memory.get_host_address(GuestAddress(0)).unwrap() as u64, - size: 0x8000_0000, - }, - MeasuredRegion { - guest_addr: arch::FIRMWARE_START, - host_addr: guest_memory - .get_host_address(GuestAddress(arch::FIRMWARE_START)) - .unwrap() as u64, - size: qboot_size, - }, - ]; - - m - }; - - let mut serial_devices = Vec::new(); - - // Create the legacy serial device if we're booting from a firmware - if (cfg!(feature = "efi") || vm_resources.firmware_config.is_some()) - && !vm_resources.disable_implicit_console - { - serial_devices.push(setup_serial_device( - event_manager, - None, - None, - // Uncomment this to get EFI output when debugging EDK2. - //Some(Box::new(io::stdout())), - )?); - }; - - // We can't call to `setup_terminal_raw_mode` until `Vmm` is created, - // so let's keep track of FDs connected to legacy serial devices here - // and set raw mode on them later. - let mut serial_ttys = Vec::new(); - - for s in &vm_resources.serial_consoles { - let input: Option> = if s.input_fd >= 0 { - let file = unsafe { File::from_raw_fd(s.input_fd) }; - if file.is_terminal() { - serial_ttys.push(unsafe { BorrowedFd::borrow_raw(file.as_raw_fd()) }); - } - Some(Box::new(file)) - } else { - None - }; - - let output: Option> = if s.output_fd >= 0 { - Some(Box::new(unsafe { File::from_raw_fd(s.output_fd) })) - } else { - None - }; - - serial_devices.push(setup_serial_device(event_manager, input, output)?); - } - - let exit_evt = EventFd::new(utils::eventfd::EFD_NONBLOCK) - .map_err(Error::EventFd) - .map_err(StartMicrovmError::Internal)?; - - #[cfg(target_arch = "x86_64")] - // Safe to unwrap 'serial_device' as it's always 'Some' on x86_64. - // x86_64 uses the i8042 reset event as the Vmm exit event. - let mut pio_device_manager = PortIODeviceManager::new( - Arc::new(Mutex::new(Cmos::new( - arch_memory_info.ram_below_gap, - arch_memory_info.ram_above_gap, - ))), - serial_devices, - exit_evt - .try_clone() - .map_err(Error::EventFd) - .map_err(StartMicrovmError::Internal)?, - ) - .map_err(Error::CreateLegacyDevice) - .map_err(StartMicrovmError::Internal)?; - - // Instantiate the MMIO device manager. - // 'mmio_base' address has to be an address which is protected by the kernel - // and is architectural specific. - #[allow(unused_mut)] - let mut mmio_device_manager = MMIODeviceManager::new( - &mut (arch::MMIO_MEM_START.clone()), - (arch::IRQ_BASE, arch::IRQ_MAX), - ); - - #[cfg(target_os = "macos")] - let vcpu_list = { - let cpu_count = vm_resources.vm_config().vcpu_count.unwrap(); - Arc::new(VcpuList::new(cpu_count as u64)) - }; - - let vcpus; - let intc: IrqChip; - // For x86_64 we need to create the interrupt controller before calling `KVM_CREATE_VCPUS` - // while on aarch64 we need to do it the other way around. - #[cfg(target_arch = "x86_64")] - { - let ioapic: Box = if vm_resources.split_irqchip { - Box::new( - IoApic::new(vm.fd(), _sender.clone()) - .map_err(StartMicrovmError::CreateKvmIrqChip)?, - ) - } else { - Box::new(KvmIoapic::new(vm.fd()).map_err(StartMicrovmError::CreateKvmIrqChip)?) - }; - intc = Arc::new(Mutex::new(IrqChipDevice::new(ioapic))); - - attach_legacy_devices( - &vm, - vm_resources.split_irqchip, - &mut pio_device_manager, - &mut mmio_device_manager, - Some(intc.clone()), - )?; - - let kernel_boot = vm_resources.firmware_config.is_none() && !cfg!(feature = "tee"); - - vcpus = create_vcpus_x86_64( - &vm, - &vcpu_config, - &guest_memory, - payload_config.entry_addr, - &pio_device_manager.io_bus, - &exit_evt, - kernel_boot, - #[cfg(feature = "tee")] - _sender, - ) - .map_err(StartMicrovmError::Internal)?; - } - - #[cfg(feature = "tdx")] - { - for vcpu in &vcpus { - vcpu.tdx_secure_virt_prepare(&mut tdx_launcher); - } - vm.tdx_secure_virt_init_vcpus(&mut tdx_launcher).unwrap(); - } - - // On aarch64, the vCPUs need to be created (i.e call KVM_CREATE_VCPU) and configured before - // setting up the IRQ chip because the `KVM_CREATE_VCPU` ioctl will return error if the IRQCHIP - // was already initialized. - // Search for `kvm_arch_vcpu_create` in arch/arm/kvm/arm.c. - #[cfg(all(target_arch = "aarch64", target_os = "linux"))] - { - vcpus = create_vcpus_aarch64( - &vm, - &vcpu_config, - &arch_memory_info, - payload_config.entry_addr, - &exit_evt, - ) - .map_err(StartMicrovmError::Internal)?; - - intc = { - // The SoC in some popular boards (namely, the RPi family) doesn't support an - // architected vGIC, which is required for requesting KVM the instantiation of a - // GICv3. To relieve the users from having to configure the gic version manually, - // try first to instantiate a GICv3, and fall back to a GICv2 if it fails. - let vcpu_count = vm_resources.vm_config().vcpu_count.unwrap() as u64; - let gic = match KvmGicV3::new(vm.fd(), vcpu_count) { - Ok(gicv3) => IrqChipDevice::new(Box::new(gicv3)), - Err(_) => { - warn!("KVM GICv3 creation failed, falling back to KVM GICv2"); - IrqChipDevice::new(Box::new(KvmGicV2::new(vm.fd(), vcpu_count))) - } - }; - Arc::new(Mutex::new(gic)) - }; - - attach_legacy_devices( - &vm, - &mut mmio_device_manager, - &mut kernel_cmdline, - intc.clone(), - serial_devices, - )?; - } - - #[cfg(all(target_arch = "aarch64", target_os = "macos"))] - { - intc = { - // If the system supports the in-kernel GIC, use it. Otherwise, fall back to the - // userspace implementation. - let gic = match HvfGicV3::new(vm_resources.vm_config().vcpu_count.unwrap() as u64) { - Ok(hvfgic) => IrqChipDevice::new(Box::new(hvfgic)), - Err(_) => IrqChipDevice::new(Box::new(GicV3::new(vcpu_list.clone()))), - }; - Arc::new(Mutex::new(gic)) - }; - - vcpus = create_vcpus_aarch64( - &vm, - &vcpu_config, - &arch_memory_info, - payload_config.entry_addr, - &exit_evt, - vcpu_list.clone(), - vm_resources.nested_enabled, - ) - .map_err(StartMicrovmError::Internal)?; - - attach_legacy_devices( - &vm, - &mut mmio_device_manager, - &mut kernel_cmdline, - intc.clone(), - serial_devices, - event_manager, - _shutdown_efd, - )?; - } - - #[cfg(all(target_arch = "riscv64", target_os = "linux"))] - { - vcpus = create_vcpus_riscv64( - &vm, - &vcpu_config, - &guest_memory, - payload_config.entry_addr, - &exit_evt, - ) - .map_err(StartMicrovmError::Internal)?; - - intc = Arc::new(Mutex::new(IrqChipDevice::new(Box::new( - KvmAia::new(vm.fd(), vm_resources.vm_config().vcpu_count.unwrap() as u32).unwrap(), - )))); - - attach_legacy_devices( - &vm, - &mut mmio_device_manager, - &mut kernel_cmdline, - intc.clone(), - serial_devices, - )?; - } - - // We use this atomic to record the exit code set by init/init.c in the VM. - let exit_code = Arc::new(AtomicI32::new(i32::MAX)); - - let mut vmm = Vmm { - guest_memory, - arch_memory_info, - kernel_cmdline, - vcpus_handles: Vec::new(), - exit_evt, - exit_observers: Vec::new(), - exit_code: exit_code.clone(), - vm, - mmio_device_manager, - #[cfg(target_arch = "x86_64")] - pio_device_manager, - }; - - // Set raw mode for FDs that are connected to legacy serial devices. - for serial_tty in serial_ttys { - setup_terminal_raw_mode(&mut vmm, Some(serial_tty), false); - } - - #[cfg(not(feature = "tee"))] - attach_balloon_device(&mut vmm, event_manager, intc.clone())?; - #[cfg(not(feature = "tee"))] - attach_rng_device(&mut vmm, event_manager, intc.clone())?; - let mut console_id = 0; - if !vm_resources.disable_implicit_console { - attach_console_devices( - &mut vmm, - event_manager, - intc.clone(), - vm_resources, - None, - console_id, - )?; - console_id += 1; - } - - for console_cfg in vm_resources.virtio_consoles.iter() { - attach_console_devices( - &mut vmm, - event_manager, - intc.clone(), - vm_resources, - Some(console_cfg), - console_id, - )?; - console_id += 1; - } - - #[cfg(not(any(feature = "tee", feature = "aws-nitro")))] - let export_table: Option = if cfg!(feature = "gpu") { - Some(Default::default()) - } else { - None - }; - - #[cfg(feature = "gpu")] - if let Some(virgl_flags) = vm_resources.gpu_virgl_flags { - let display_backend = vm_resources - .display_backend - .unwrap_or_else(|| NoopDisplayBackend::into_display_backend(None)); - - attach_gpu_device( - &mut vmm, - &mut _shm_manager, - #[cfg(not(feature = "tee"))] - export_table.clone(), - intc.clone(), - virgl_flags, - Box::from(&vm_resources.displays[..]), - display_backend, - #[cfg(target_os = "macos")] - _sender.clone(), - )?; - } - - #[cfg(feature = "input")] - if !vm_resources.input_backends.is_empty() { - attach_input_devices(&mut vmm, &vm_resources.input_backends, intc.clone())?; - } - - #[cfg(not(any(feature = "tee", feature = "aws-nitro")))] - attach_fs_devices( - &mut vmm, - &vm_resources.fs, - &mut _shm_manager, - #[cfg(not(feature = "tee"))] - export_table, - intc.clone(), - exit_code, - #[cfg(target_os = "macos")] - _sender, - )?; - #[cfg(feature = "blk")] - attach_block_devices(&mut vmm, &vm_resources.block, intc.clone())?; - - if let Some(vsock) = vm_resources.vsock.get() { - attach_unixsock_vsock_device(&mut vmm, vsock, event_manager, intc.clone())?; - let tsi_flags = vm_resources.vsock.tsi_flags(); - if tsi_flags.contains(TsiFlags::HIJACK_INET) { - vmm.kernel_cmdline.insert_str("tsi_hijack")?; - } - if tsi_flags.contains(TsiFlags::HIJACK_UNIX) { - vmm.kernel_cmdline.insert_str("tsi_hijack_unix")?; - } - } - - #[cfg(feature = "net")] - attach_net_devices(&mut vmm, &vm_resources.net, intc.clone())?; - #[cfg(feature = "net")] - if vm_resources.dhcp_client { - vmm.kernel_cmdline.insert_str("KRUN_DHCP=1")?; - } - - #[cfg(feature = "snd")] - if vm_resources.snd_device { - attach_snd_device(&mut vmm, intc.clone())?; - } - - if let Some(s) = &vm_resources.kernel_cmdline.epilog { - vmm.kernel_cmdline.insert_str(s).unwrap(); - }; - - // Write the kernel command line to guest memory. This is x86_64 specific, since on - // aarch64 the command line will be specified through the FDT. - #[cfg(all(target_arch = "x86_64", not(feature = "tee")))] - load_cmdline(&vmm)?; - - vmm.configure_system( - vcpus.as_slice(), - &intc, - &payload_config.initrd_config, - &vm_resources.smbios_oem_strings, - ) - .map_err(StartMicrovmError::Internal)?; - - #[cfg(feature = "tee")] - { - match tee { - #[cfg(feature = "amd-sev")] - Tee::Snp => { - let cpuid = _kvm - .fd() - .get_supported_cpuid(KVM_MAX_CPUID_ENTRIES) - .map_err(VstateError::KvmCpuId) - .map_err(StartMicrovmError::SecureVirtAttest)?; - vmm.kvm_vm() - .snp_secure_virt_measure( - cpuid, - vmm.guest_memory(), - measured_regions, - snp_launcher.unwrap(), - ) - .map_err(StartMicrovmError::SecureVirtAttest)?; - } - #[cfg(feature = "tdx")] - Tee::Tdx => { - vmm.kvm_vm() - .tdx_secure_virt_prepare_memory(&mut tdx_launcher, &measured_regions) - .unwrap(); - vmm.kvm_vm() - .tdx_secure_virt_finalize_vm(tdx_launcher) - .map_err(StartMicrovmError::SecureVirtPrepare)?; - } - _ => return Err(StartMicrovmError::InvalidTee), - } - - println!("Starting TEE/microVM."); - } - - vmm.start_vcpus(vcpus) - .map_err(StartMicrovmError::Internal)?; - - // Clippy thinks we don't need Arc std::result::Result<(GuestAddress, Option, Option), StartMicrovmError> { - let entry_addr = match external_kernel.format { - // Raw images are treated as bundled kernels on x86_64 - #[cfg(target_arch = "x86_64")] - KernelFormat::Raw => unreachable!(), - #[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))] - KernelFormat::Raw => { - let data: Vec = std::fs::read(external_kernel.path.clone()) - .map_err(StartMicrovmError::RawOpenKernel)?; - guest_mem.write(&data, GuestAddress(0x8000_0000)).unwrap(); - GuestAddress(0x8000_0000) - } - #[cfg(target_arch = "x86_64")] - KernelFormat::Elf => { - let mut file = File::options() - .read(true) - .write(false) - .open(external_kernel.path.clone()) - .map_err(StartMicrovmError::ElfOpenKernel)?; - let load_result = loader::Elf::load(guest_mem, None, &mut file, None) - .map_err(StartMicrovmError::ElfLoadKernel)?; - load_result.kernel_load - } - #[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))] - KernelFormat::PeGz => { - let data: Vec = std::fs::read(external_kernel.path.clone()) - .map_err(StartMicrovmError::PeGzOpenKernel)?; - if let Some(magic) = data - .windows(3) - .position(|window| window == [0x1f, 0x8b, 0x8]) - { - debug!("Found GZIP header on PE file at: 0x{magic:x}"); - let (_, compressed) = data.split_at(magic); - let mut gz = GzDecoder::new(compressed); - let mut kernel_data: Vec = Vec::new(); - gz.read_to_end(&mut kernel_data) - .map_err(StartMicrovmError::PeGzDecoder)?; - guest_mem - .write(&kernel_data, GuestAddress(0x8000_0000)) - .unwrap(); - GuestAddress(0x8000_0000) - } else { - return Err(StartMicrovmError::PeGzInvalid); - } - } - #[cfg(target_arch = "x86_64")] - KernelFormat::ImageBz2 => { - let data: Vec = std::fs::read(external_kernel.path.clone()) - .map_err(StartMicrovmError::ImageBz2OpenKernel)?; - if let Some(magic) = data - .windows(3) - .position(|window| window == [b'B', b'Z', b'h']) - { - debug!("Found BZIP2 header on Image file at: 0x{magic:x}"); - let (_, compressed) = data.split_at(magic); - let mut kernel_data: Vec = Vec::new(); - let mut bz2 = bzip2::read::BzDecoder::new(compressed); - bz2.read_to_end(&mut kernel_data) - .map_err(StartMicrovmError::ImageBz2Decoder)?; - let load_result = loader::Elf::load( - guest_mem, - None, - &mut std::io::Cursor::new(kernel_data), - None, - ) - .map_err(StartMicrovmError::ImageBz2LoadKernel)?; - load_result.kernel_load - } else { - return Err(StartMicrovmError::ImageBz2Invalid); - } - } - #[cfg(target_arch = "x86_64")] - KernelFormat::ImageGz => { - let data: Vec = std::fs::read(external_kernel.path.clone()) - .map_err(StartMicrovmError::ImageGzOpenKernel)?; - if let Some(magic) = data - .windows(3) - .position(|window| window == [0x1f, 0x8b, 0x8]) - { - debug!("Found GZIP header on Image file at: 0x{magic:x}"); - let (_, compressed) = data.split_at(magic); - let mut gz = GzDecoder::new(compressed); - let mut kernel_data: Vec = Vec::new(); - gz.read_to_end(&mut kernel_data) - .map_err(StartMicrovmError::ImageGzDecoder)?; - let load_result = loader::Elf::load( - guest_mem, - None, - &mut std::io::Cursor::new(kernel_data), - None, - ) - .map_err(StartMicrovmError::ImageGzLoadKernel)?; - load_result.kernel_load - } else { - return Err(StartMicrovmError::ImageGzInvalid); - } - } - #[cfg(target_arch = "x86_64")] - KernelFormat::ImageZstd => { - let data: Vec = std::fs::read(external_kernel.path.clone()) - .map_err(StartMicrovmError::ImageZstdOpenKernel)?; - if let Some(magic) = data - .windows(4) - .position(|window| window == [0x28, 0xb5, 0x2f, 0xfd]) - { - debug!("Found ZSTD header on Image file at: 0x{magic:x}"); - let (_, zstd_data) = data.split_at(magic); - let mut kernel_data: Vec = Vec::new(); - let _ = zstd::stream::copy_decode(zstd_data, &mut kernel_data); - let load_result = loader::Elf::load( - guest_mem, - None, - &mut std::io::Cursor::new(kernel_data), - None, - ) - .map_err(StartMicrovmError::ImageZstdLoadKernel)?; - load_result.kernel_load - } else { - return Err(StartMicrovmError::ImageZstdInvalid); - } - } - _ => return Err(StartMicrovmError::KernelFormatUnsupported), - }; - - debug!("load_external_kernel: 0x{:x}", entry_addr.0); - - let initrd_config = if let Some(initramfs_path) = &external_kernel.initramfs_path { - let data = std::fs::read(initramfs_path).map_err(StartMicrovmError::InitrdRead)?; - guest_mem - .write(&data, GuestAddress(arch_mem_info.initrd_addr)) - .unwrap(); - Some(InitrdConfig { - address: GuestAddress(arch_mem_info.initrd_addr), - size: data.len(), - }) - } else { - None - }; - - Ok((entry_addr, initrd_config, external_kernel.cmdline.clone())) -} - -fn load_payload( - _vm_resources: &VmResources, - guest_mem: GuestMemoryMmap, - _arch_mem_info: &ArchMemoryInfo, - payload: &Payload, -) -> std::result::Result< - ( - GuestMemoryMmap, - GuestAddress, - Option, - Option, - ), - StartMicrovmError, -> { - match payload { - #[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))] - Payload::KernelCopy => { - let (kernel_entry_addr, kernel_host_addr, kernel_guest_addr, kernel_size) = - if let Some(kernel_bundle) = &_vm_resources.kernel_bundle { - ( - kernel_bundle.entry_addr, - kernel_bundle.host_addr, - kernel_bundle.guest_addr, - kernel_bundle.size, - ) - } else { - return Err(StartMicrovmError::MissingKernelConfig); - }; - - let kernel_data = - unsafe { std::slice::from_raw_parts(kernel_host_addr as *mut u8, kernel_size) }; - if kernel_guest_addr + kernel_size as u64 > _arch_mem_info.ram_last_addr { - return Err(StartMicrovmError::KernelDoesNotFit( - kernel_guest_addr, - kernel_size, - )); - } - guest_mem - .write(kernel_data, GuestAddress(kernel_guest_addr)) - .unwrap(); - Ok((guest_mem, GuestAddress(kernel_entry_addr), None, None)) - } - #[cfg(all(target_arch = "x86_64", not(feature = "tee")))] - Payload::KernelMmap => { - let (kernel_entry_addr, kernel_host_addr, kernel_guest_addr, kernel_size) = - if let Some(kernel_bundle) = &_vm_resources.kernel_bundle { - ( - kernel_bundle.entry_addr, - kernel_bundle.host_addr, - kernel_bundle.guest_addr, - kernel_bundle.size, - ) - } else { - return Err(StartMicrovmError::MissingKernelConfig); - }; - - let kernel_region = unsafe { - MmapRegion::build_raw(kernel_host_addr as *mut u8, kernel_size, 0, 0) - .map_err(StartMicrovmError::InvalidKernelBundle)? - }; - - Ok(( - guest_mem - .insert_region(Arc::new( - GuestRegionMmap::new(kernel_region, GuestAddress(kernel_guest_addr)) - .ok_or_else(|| { - StartMicrovmError::GuestMemoryMmap( - "Failed to create GuestRegionMmap".to_string(), - ) - })?, - )) - .map_err(|e| StartMicrovmError::GuestMemoryMmap(format!("{e:?}")))?, - GuestAddress(kernel_entry_addr), - None, - None, - )) - } - Payload::ExternalKernel(external_kernel) => { - let (entry_addr, initrd_config, cmdline) = - load_external_kernel(&guest_mem, _arch_mem_info, external_kernel)?; - Ok((guest_mem, entry_addr, initrd_config, cmdline)) - } - #[cfg(test)] - Payload::Empty => Ok((guest_mem, GuestAddress(0), None, None)), - #[cfg(feature = "tee")] - Payload::Tee => { - let (kernel_host_addr, kernel_guest_addr, kernel_size) = - if let Some(kernel_bundle) = &_vm_resources.kernel_bundle { - ( - kernel_bundle.host_addr, - kernel_bundle.guest_addr, - kernel_bundle.size, - ) - } else { - return Err(StartMicrovmError::MissingKernelConfig); - }; - let kernel_data = - unsafe { std::slice::from_raw_parts(kernel_host_addr as *mut u8, kernel_size) }; - guest_mem - .write(kernel_data, GuestAddress(kernel_guest_addr)) - .unwrap(); - - let (qboot_host_addr, qboot_size) = - if let Some(qboot_bundle) = &_vm_resources.qboot_bundle { - (qboot_bundle.host_addr, qboot_bundle.size) - } else { - return Err(StartMicrovmError::MissingKernelConfig); - }; - let qboot_data = - unsafe { std::slice::from_raw_parts(qboot_host_addr as *mut u8, qboot_size) }; - guest_mem - .write(qboot_data, GuestAddress(arch::FIRMWARE_START)) - .unwrap(); - - let (initrd_host_addr, initrd_size) = - if let Some(initrd_bundle) = &_vm_resources.initrd_bundle { - (initrd_bundle.host_addr, initrd_bundle.size) - } else { - return Err(StartMicrovmError::MissingKernelConfig); - }; - let initrd_data = - unsafe { std::slice::from_raw_parts(initrd_host_addr as *mut u8, initrd_size) }; - guest_mem - .write(initrd_data, GuestAddress(_arch_mem_info.initrd_addr)) - .unwrap(); - - let initrd_config = InitrdConfig { - address: GuestAddress(_arch_mem_info.initrd_addr), - size: initrd_data.len(), - }; - - Ok(( - guest_mem, - GuestAddress(arch::RESET_VECTOR), - Some(initrd_config), - None, - )) - } - Payload::Firmware => Ok((guest_mem, GuestAddress(arch::RESET_VECTOR), None, None)), - } -} - -pub struct PayloadConfig { - entry_addr: GuestAddress, - initrd_config: Option, - kernel_cmdline: Option, -} - -pub fn create_guest_memory( - mem_size: usize, - vm_resources: &VmResources, - payload: &Payload, -) -> std::result::Result< - (GuestMemoryMmap, ArchMemoryInfo, ShmManager, PayloadConfig), - StartMicrovmError, -> { - let mem_size = mem_size << 20; - - #[cfg(not(feature = "efi"))] - let (firmware_data, firmware_size) = if let Some(firmware) = &vm_resources.firmware_config { - let data = std::fs::read(firmware.path.clone()).map_err(StartMicrovmError::FirmwareRead)?; - let len = data.len(); - (Some(data), Some(len)) - } else { - (None, None) - }; - #[cfg(feature = "efi")] - let (firmware_data, firmware_size) = (Some(EDK2_BINARY), Some(EDK2_BINARY.len())); - - #[cfg(target_arch = "x86_64")] - let (arch_mem_info, mut arch_mem_regions) = match payload { - #[cfg(not(feature = "tee"))] - Payload::KernelMmap => { - let (kernel_guest_addr, kernel_size) = - if let Some(kernel_bundle) = &vm_resources.kernel_bundle { - (kernel_bundle.guest_addr, kernel_bundle.size) - } else { - return Err(StartMicrovmError::MissingKernelConfig); - }; - arch::arch_memory_regions(mem_size, Some(kernel_guest_addr), kernel_size, 0, None) - } - Payload::ExternalKernel(external_kernel) => arch::arch_memory_regions( - mem_size, - None, - 0, - external_kernel.initramfs_size, - firmware_size, - ), - #[cfg(feature = "tee")] - Payload::Tee => { - let (kernel_guest_addr, kernel_size) = - if let Some(kernel_bundle) = &vm_resources.kernel_bundle { - (kernel_bundle.guest_addr, kernel_bundle.size) - } else { - return Err(StartMicrovmError::MissingKernelConfig); - }; - arch::arch_memory_regions(mem_size, Some(kernel_guest_addr), kernel_size, 0, None) - } - #[cfg(test)] - Payload::Empty => arch::arch_memory_regions(mem_size, None, 0, 0, None), - Payload::Firmware => arch::arch_memory_regions(mem_size, None, 0, 0, firmware_size), - }; - #[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))] - let (arch_mem_info, mut arch_mem_regions) = match payload { - Payload::ExternalKernel(external_kernel) => { - arch::arch_memory_regions(mem_size, external_kernel.initramfs_size, None) - } - _ => arch::arch_memory_regions(mem_size, 0, firmware_size), - }; - - let mut shm_manager = ShmManager::new(&arch_mem_info); - - #[cfg(not(feature = "tee"))] - for (index, fs) in vm_resources.fs.iter().enumerate() { - if let Some(shm_size) = fs.shm_size { - shm_manager - .create_fs_region(index, shm_size) - .map_err(StartMicrovmError::ShmCreate)?; - } - } - if vm_resources.gpu_virgl_flags.is_some() { - let size = vm_resources.gpu_shm_size.unwrap_or(1 << 33); - shm_manager - .create_gpu_region(size) - .map_err(StartMicrovmError::ShmCreate)?; - } - - arch_mem_regions.extend(shm_manager.regions()); - - let guest_mem = GuestMemoryMmap::from_ranges(&arch_mem_regions) - .map_err(|e| StartMicrovmError::GuestMemoryMmap(format!("{e:?}")))?; - - let (guest_mem, entry_addr, initrd_config, cmdline) = - load_payload(vm_resources, guest_mem, &arch_mem_info, payload)?; - - // Only write firmware if data exists AND this isn't an ExternalKernel payload - // (ExternalKernel does direct kernel boot and doesn't use EFI firmware) - if !matches!(payload, Payload::ExternalKernel(_)) { - if let Some(firmware_data) = firmware_data.as_ref() { - guest_mem - .write(firmware_data, GuestAddress(arch_mem_info.firmware_addr)) - .map_err(StartMicrovmError::FirmwareInvalidAddress)?; - } - } - - let payload_config = PayloadConfig { - entry_addr, - initrd_config, - kernel_cmdline: cmdline.clone(), - }; - - Ok((guest_mem, arch_mem_info, shm_manager, payload_config)) -} - -#[cfg(all(target_arch = "x86_64", not(feature = "tee")))] -fn load_cmdline(vmm: &Vmm) -> std::result::Result<(), StartMicrovmError> { - kernel::loader::load_cmdline( - vmm.guest_memory(), - GuestAddress(arch::x86_64::layout::CMDLINE_START), - &vmm.kernel_cmdline - .as_cstring() - .map_err(StartMicrovmError::LoadCommandline)?, - ) - .map_err(StartMicrovmError::LoadCommandline) -} - -#[cfg(all(target_os = "linux", not(feature = "tee")))] -pub(crate) fn setup_vm( - guest_memory: &GuestMemoryMmap, - _nested_enabled: bool, -) -> std::result::Result { - let kvm = KvmContext::new() - .map_err(Error::KvmContext) - .map_err(StartMicrovmError::Internal)?; - let mut vm = Vm::new(kvm.fd()) - .map_err(Error::Vm) - .map_err(StartMicrovmError::Internal)?; - vm.memory_init(guest_memory, kvm.max_memslots()) - .map_err(Error::Vm) - .map_err(StartMicrovmError::Internal)?; - Ok(vm) -} -#[cfg(all(target_os = "linux", feature = "tee"))] -pub(crate) fn setup_vm( - kvm: &KvmContext, - guest_memory: &GuestMemoryMmap, - resources: &super::resources::VmResources, - #[cfg(feature = "tdx")] _sender: Sender, -) -> std::result::Result { - let mut vm = Vm::new( - kvm.fd(), - resources.tee_config(), - #[cfg(feature = "tdx")] - _sender, - ) - .map_err(Error::Vm) - .map_err(StartMicrovmError::Internal)?; - vm.memory_init(guest_memory, kvm.max_memslots()) - .map_err(Error::Vm) - .map_err(StartMicrovmError::Internal)?; - Ok(vm) -} -#[cfg(target_os = "macos")] -pub(crate) fn setup_vm( - guest_memory: &GuestMemoryMmap, - nested_enabled: bool, -) -> std::result::Result { - let mut vm = Vm::new(nested_enabled) - .map_err(Error::Vm) - .map_err(StartMicrovmError::Internal)?; - vm.memory_init(guest_memory) - .map_err(Error::Vm) - .map_err(StartMicrovmError::Internal)?; - Ok(vm) -} - -/// Sets up the serial device. -pub fn setup_serial_device( - event_manager: &mut EventManager, - input: Option>, - out: Option>, -) -> std::result::Result>, StartMicrovmError> { - let interrupt_evt = EventFd::new(utils::eventfd::EFD_NONBLOCK) - .map_err(Error::EventFd) - .map_err(StartMicrovmError::Internal)?; - let has_input = input.is_some(); - let serial = Arc::new(Mutex::new(Serial::new(interrupt_evt, out, input))); - if has_input { - if let Err(e) = event_manager.add_subscriber(serial.clone()) { - // TODO: We just log this message, and immediately return Ok, instead of returning the - // actual error because this operation always fails with EPERM when adding a fd which - // has been redirected to /dev/null via dup2 (this may happen inside the jailer). - // Find a better solution to this (and think about the state of the serial device - // while we're at it). - warn!("Could not add serial input event to epoll: {e:?}"); - } - } - Ok(serial) -} - -#[cfg(target_arch = "x86_64")] -fn attach_legacy_devices( - vm: &Vm, - split_irqchip: bool, - pio_device_manager: &mut PortIODeviceManager, - mmio_device_manager: &mut MMIODeviceManager, - intc: Option>>, -) -> std::result::Result<(), StartMicrovmError> { - pio_device_manager - .register_devices() - .map_err(Error::LegacyIOBus) - .map_err(StartMicrovmError::Internal)?; - - if split_irqchip { - mmio_device_manager - .register_mmio_ioapic(intc) - .map_err(Error::RegisterMMIODevice) - .map_err(StartMicrovmError::Internal)?; - } - - macro_rules! register_irqfd_evt { - ($evt: ident, $index: expr) => {{ - vm.fd() - .register_irqfd(&pio_device_manager.$evt, $index) - .map_err(|e| { - Error::LegacyIOBus(device_manager::legacy::Error::EventFd( - io::Error::from_raw_os_error(e.errno()), - )) - }) - .map_err(StartMicrovmError::Internal)?; - }}; - } - - register_irqfd_evt!(com_evt_1, 4); - register_irqfd_evt!(com_evt_2, 3); - register_irqfd_evt!(com_evt_3, 4); - register_irqfd_evt!(com_evt_4, 3); - register_irqfd_evt!(kbd_evt, 1); - Ok(()) -} - -#[cfg(all( - any(target_arch = "aarch64", target_arch = "riscv64"), - target_os = "linux" -))] -fn attach_legacy_devices( - vm: &Vm, - mmio_device_manager: &mut MMIODeviceManager, - kernel_cmdline: &mut kernel::cmdline::Cmdline, - intc: IrqChip, - serial: Vec>>, -) -> std::result::Result<(), StartMicrovmError> { - for s in serial { - mmio_device_manager - .register_mmio_serial(vm.fd(), kernel_cmdline, intc.clone(), s) - .map_err(Error::RegisterMMIODevice) - .map_err(StartMicrovmError::Internal)?; - } - - #[cfg(all(target_arch = "aarch64", target_os = "linux"))] - mmio_device_manager - .register_mmio_rtc(vm.fd()) - .map_err(Error::RegisterMMIODevice) - .map_err(StartMicrovmError::Internal)?; - - Ok(()) -} - -#[cfg(all(target_arch = "aarch64", target_os = "macos"))] -fn attach_legacy_devices( - vm: &Vm, - mmio_device_manager: &mut MMIODeviceManager, - kernel_cmdline: &mut kernel::cmdline::Cmdline, - intc: IrqChip, - serial: Vec>>, - event_manager: &mut EventManager, - shutdown_efd: Option, -) -> Result<(), StartMicrovmError> { - for s in serial { - mmio_device_manager - .register_mmio_serial(vm, kernel_cmdline, intc.clone(), s) - .map_err(Error::RegisterMMIODevice) - .map_err(StartMicrovmError::Internal)?; - } - - mmio_device_manager - .register_mmio_rtc(vm, intc.clone()) - .map_err(Error::RegisterMMIODevice) - .map_err(StartMicrovmError::Internal)?; - - mmio_device_manager - .register_mmio_gic(vm, intc.clone()) - .map_err(Error::RegisterMMIODevice) - .map_err(StartMicrovmError::Internal)?; - - if let Some(shutdown_efd) = shutdown_efd { - mmio_device_manager - .register_mmio_gpio(vm, intc.clone(), event_manager, shutdown_efd) - .map_err(Error::RegisterMMIODevice) - .map_err(StartMicrovmError::Internal)?; - } - - Ok(()) -} - -#[cfg(target_arch = "x86_64")] -#[allow(clippy::too_many_arguments)] -fn create_vcpus_x86_64( - vm: &Vm, - vcpu_config: &VcpuConfig, - guest_mem: &GuestMemoryMmap, - entry_addr: GuestAddress, - io_bus: &devices::Bus, - exit_evt: &EventFd, - kernel_boot: bool, - #[cfg(feature = "tee")] pm_sender: Sender, -) -> super::Result> { - let mut vcpus = Vec::with_capacity(vcpu_config.vcpu_count as usize); - for cpu_index in 0..vcpu_config.vcpu_count { - let mut vcpu = Vcpu::new_x86_64( - cpu_index, - vm.fd(), - vm.supported_cpuid().clone(), - vm.supported_msrs().clone(), - io_bus.clone(), - exit_evt.try_clone().map_err(Error::EventFd)?, - #[cfg(feature = "tee")] - pm_sender.clone(), - ) - .map_err(Error::Vcpu)?; - - vcpu.configure_x86_64(guest_mem, entry_addr, vcpu_config, kernel_boot) - .map_err(Error::Vcpu)?; - - vcpus.push(vcpu); - } - Ok(vcpus) -} - -#[cfg(all(target_arch = "aarch64", target_os = "linux"))] -fn create_vcpus_aarch64( - vm: &Vm, - vcpu_config: &VcpuConfig, - mem_info: &ArchMemoryInfo, - entry_addr: GuestAddress, - exit_evt: &EventFd, -) -> super::Result> { - let mut vcpus = Vec::with_capacity(vcpu_config.vcpu_count as usize); - for cpu_index in 0..vcpu_config.vcpu_count { - let mut vcpu = Vcpu::new_aarch64( - cpu_index, - vm.fd(), - exit_evt.try_clone().map_err(Error::EventFd)?, - ) - .map_err(Error::Vcpu)?; - - vcpu.configure_aarch64(vm.fd(), mem_info, entry_addr) - .map_err(Error::Vcpu)?; - - vcpus.push(vcpu); - } - Ok(vcpus) -} - -#[cfg(all(target_arch = "aarch64", target_os = "macos"))] -fn create_vcpus_aarch64( - _vm: &Vm, - vcpu_config: &VcpuConfig, - mem_info: &ArchMemoryInfo, - entry_addr: GuestAddress, - exit_evt: &EventFd, - vcpu_list: Arc, - nested_enabled: bool, -) -> super::Result> { - let mut vcpus = Vec::with_capacity(vcpu_config.vcpu_count as usize); - let mut boot_senders: HashMap> = HashMap::new(); - - for cpu_index in 0..vcpu_config.vcpu_count { - let (boot_sender, boot_receiver) = if cpu_index != 0 { - let (boot_sender, boot_receiver) = unbounded(); - (Some(boot_sender), Some(boot_receiver)) - } else { - (None, None) - }; - - let mut vcpu = Vcpu::new_aarch64( - cpu_index, - entry_addr, - boot_receiver, - exit_evt.try_clone().map_err(Error::EventFd)?, - vcpu_list.clone(), - nested_enabled, - ) - .map_err(Error::Vcpu)?; - - vcpu.configure_aarch64(mem_info).map_err(Error::Vcpu)?; - - if let Some(boot_sender) = boot_sender { - boot_senders.insert(vcpu.get_mpidr(), boot_sender); - } - - vcpus.push(vcpu); - } - - vcpus[0].set_boot_senders(boot_senders); - - Ok(vcpus) -} - -#[cfg(all(target_arch = "riscv64", target_os = "linux"))] -fn create_vcpus_riscv64( - vm: &Vm, - vcpu_config: &VcpuConfig, - guest_mem: &GuestMemoryMmap, - entry_addr: GuestAddress, - exit_evt: &EventFd, -) -> super::Result> { - let mut vcpus = Vec::with_capacity(vcpu_config.vcpu_count as usize); - for cpu_index in 0..vcpu_config.vcpu_count { - let mut vcpu = Vcpu::new_riscv64( - cpu_index, - vm.fd(), - exit_evt.try_clone().map_err(Error::EventFd)?, - ) - .map_err(Error::Vcpu)?; - - vcpu.configure_riscv64(vm.fd(), guest_mem, entry_addr) - .map_err(Error::Vcpu)?; - - vcpus.push(vcpu); - } - Ok(vcpus) -} - -/// Attaches an virtio mmio device to the device manager. -fn attach_mmio_device( - vmm: &mut Vmm, - id: String, - intc: IrqChip, - device: Arc>, -) -> std::result::Result<(), device_manager::mmio::Error> { - let mmio_device = MmioTransport::new(vmm.guest_memory().clone(), intc, device)?; - - let type_id = mmio_device.locked_device().device_type(); - let _cmdline = &mut vmm.kernel_cmdline; - - #[cfg(target_os = "linux")] - let (_mmio_base, _irq) = - vmm.mmio_device_manager - .register_mmio_device(vmm.vm.fd(), mmio_device, type_id, id)?; - #[cfg(target_os = "macos")] - let (_mmio_base, _irq) = - vmm.mmio_device_manager - .register_mmio_device(mmio_device, type_id, id)?; - - #[cfg(target_arch = "x86_64")] - vmm.mmio_device_manager - .add_device_to_cmdline(_cmdline, _mmio_base, _irq)?; - - Ok(()) -} - -#[cfg(not(any(feature = "tee", feature = "aws-nitro")))] -fn attach_fs_devices( - vmm: &mut Vmm, - fs_devs: &[FsDeviceConfig], - shm_manager: &mut ShmManager, - #[cfg(not(feature = "tee"))] export_table: Option, - intc: IrqChip, - exit_code: Arc, - #[cfg(target_os = "macos")] map_sender: Sender, -) -> std::result::Result<(), StartMicrovmError> { - use self::StartMicrovmError::*; - - for (i, config) in fs_devs.iter().enumerate() { - let fs = Arc::new(Mutex::new( - devices::virtio::Fs::new( - config.fs_id.clone(), - config.semantics, - config.shared_dir.clone(), - exit_code.clone(), - config.read_only, - config.virtual_entries.clone(), - config.overlay.clone(), - ) - .unwrap(), - )); - - let id = format!("{}{}", String::from(fs.lock().unwrap().id()), i); - - if let Some(shm_region) = shm_manager.fs_region(i) { - fs.lock().unwrap().set_shm_region(VirtioShmRegion { - host_addr: vmm - .guest_memory - .get_host_address(shm_region.guest_addr) - .map_err(StartMicrovmError::ShmHostAddr)? as u64, - guest_addr: shm_region.guest_addr.raw_value(), - size: shm_region.size, - }); - } - - #[cfg(not(feature = "tee"))] - if let Some(export_table) = export_table.as_ref() { - fs.lock().unwrap().set_export_table(export_table.clone()); - } - - #[cfg(target_os = "macos")] - fs.lock().unwrap().set_map_sender(map_sender.clone()); - - // The device mutex mustn't be locked here otherwise it will deadlock. - attach_mmio_device(vmm, id, intc.clone(), fs).map_err(RegisterFsDevice)?; - } - - Ok(()) -} - -fn autoconfigure_console_ports( - vmm: &mut Vmm, - vm_resources: &VmResources, - cfg: Option<&DefaultVirtioConsoleConfig>, - creating_implicit_console: bool, -) -> std::result::Result, StartMicrovmError> { - use self::StartMicrovmError::*; - - let mut console_output_path: Option = None; - if let Some(path) = vm_resources.console_output.clone() { - if !vm_resources.disable_implicit_console && creating_implicit_console { - console_output_path = Some(path) - } - } - - if let Some(console_output_path) = console_output_path { - let file = File::create(console_output_path).map_err(OpenConsoleFile)?; - // Manually emulate our Legacy behavior: In the case of output_path we have always used the - // stdin to determine the console size - let stdin_fd = unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) }; - let term_fd = if isatty(stdin_fd).is_ok_and(|v| v) { - port_io::term_fd(stdin_fd.as_raw_fd()).unwrap() - } else { - port_io::term_fixed_size(0, 0) - }; - Ok(vec![PortDescription::console( - Some(port_io::input_empty().unwrap()), - Some(port_io::output_file(file).unwrap()), - term_fd, - )]) - } else { - let (input_fd, output_fd, err_fd) = match cfg { - Some(c) => (c.input_fd, c.output_fd, c.err_fd), - None => (STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO), - }; - let input_is_terminal = - input_fd >= 0 && isatty(unsafe { BorrowedFd::borrow_raw(input_fd) }).unwrap_or(false); - let output_is_terminal = - output_fd >= 0 && isatty(unsafe { BorrowedFd::borrow_raw(output_fd) }).unwrap_or(false); - let error_is_terminal = - err_fd >= 0 && isatty(unsafe { BorrowedFd::borrow_raw(err_fd) }).unwrap_or(false); - - let term_fd = if input_is_terminal { - Some(unsafe { BorrowedFd::borrow_raw(input_fd) }) - } else if output_is_terminal { - Some(unsafe { BorrowedFd::borrow_raw(output_fd) }) - } else if error_is_terminal { - Some(unsafe { BorrowedFd::borrow_raw(err_fd) }) - } else { - None - }; - - let forwarding_sigint; - let console_input = if input_is_terminal && input_fd >= 0 { - forwarding_sigint = false; - Some(port_io::input_to_raw_fd_dup(input_fd).unwrap()) - } else { - #[cfg(target_os = "linux")] - { - forwarding_sigint = true; - let sigint_input = port_io::PortInputSigInt::new(); - let sigint_input_fd = sigint_input.sigint_evt().as_raw_fd(); - register_sigint_handler(sigint_input_fd).map_err(RegisterFsSigwinch)?; - Some(Box::new(sigint_input) as _) - } - #[cfg(not(target_os = "linux"))] - { - forwarding_sigint = false; - Some(port_io::input_empty().unwrap()) - } - }; - - let console_output = if output_is_terminal && output_fd >= 0 { - Some(port_io::output_to_raw_fd_dup(output_fd).unwrap()) - } else { - Some(port_io::output_to_log_as_err()) - }; - - let terminal_properties = term_fd - .map(|fd| port_io::term_fd(fd.as_raw_fd()).unwrap()) - .unwrap_or_else(|| port_io::term_fixed_size(0, 0)); - - setup_terminal_raw_mode(vmm, term_fd, forwarding_sigint); - - let mut ports = vec![PortDescription::console( - console_input, - console_output, - terminal_properties, - )]; - - if input_fd >= 0 && !input_is_terminal { - ports.push(PortDescription::input_pipe( - "krun-stdin", - port_io::input_to_raw_fd_dup(input_fd).unwrap(), - )); - } - - if output_fd >= 0 && !output_is_terminal { - ports.push(PortDescription::output_pipe( - "krun-stdout", - port_io::output_to_raw_fd_dup(output_fd).unwrap(), - )); - }; - - if err_fd >= 0 && !error_is_terminal { - ports.push(PortDescription::output_pipe( - "krun-stderr", - port_io::output_to_raw_fd_dup(err_fd).unwrap(), - )); - } - - Ok(ports) - } -} - -fn setup_terminal_raw_mode( - vmm: &mut Vmm, - term_fd: Option>, - handle_signals_by_terminal: bool, -) { - if let Some(term_fd) = term_fd { - match term_set_raw_mode(term_fd, handle_signals_by_terminal) { - Ok(old_mode) => { - let raw_fd = term_fd.as_raw_fd(); - vmm.exit_observers.push(Arc::new(Mutex::new(move || { - if let Err(e) = - term_restore_mode(unsafe { BorrowedFd::borrow_raw(raw_fd) }, &old_mode) - { - log::error!("Failed to restore terminal mode: {e}") - } - }))); - } - Err(e) => { - log::error!("Failed to set terminal to raw mode: {e}") - } - }; - } -} - -fn create_explicit_ports( - vmm: &mut Vmm, - port_configs: &[PortConfig], -) -> std::result::Result, StartMicrovmError> { - let mut ports = Vec::with_capacity(port_configs.len()); - - for port_cfg in port_configs { - let port_desc = match port_cfg { - PortConfig::Tty { name, tty_fd } => { - assert!(*tty_fd > 0, "PortConfig::Tty must have a valid tty_fd"); - let term_fd = unsafe { BorrowedFd::borrow_raw(*tty_fd) }; - setup_terminal_raw_mode(vmm, Some(term_fd), false); - - PortDescription { - name: name.clone().into(), - input: Some(port_io::input_to_raw_fd_dup(*tty_fd).unwrap()), - output: Some(port_io::output_to_raw_fd_dup(*tty_fd).unwrap()), - terminal: Some(port_io::term_fd(*tty_fd).unwrap()), - } - } - PortConfig::InOut { - name, - input_fd, - output_fd, - } => PortDescription { - name: name.clone().into(), - input: if *input_fd < 0 { - None - } else { - Some(port_io::input_to_raw_fd_dup(*input_fd).unwrap()) - }, - output: if *output_fd < 0 { - None - } else { - Some(port_io::output_to_raw_fd_dup(*output_fd).unwrap()) - }, - terminal: None, - }, - }; - - ports.push(port_desc); - } - - Ok(ports) -} - -fn attach_console_devices( - vmm: &mut Vmm, - event_manager: &mut EventManager, - intc: IrqChip, - vm_resources: &VmResources, - cfg: Option<&VirtioConsoleConfigMode>, - id_number: u32, -) -> std::result::Result<(), StartMicrovmError> { - use self::StartMicrovmError::*; - - let creating_implicit_console = cfg.is_none(); - - let ports = match cfg { - None => autoconfigure_console_ports(vmm, vm_resources, None, creating_implicit_console)?, - Some(VirtioConsoleConfigMode::Autoconfigure(autocfg)) => autoconfigure_console_ports( - vmm, - vm_resources, - Some(autocfg), - creating_implicit_console, - )?, - Some(VirtioConsoleConfigMode::Explicit(ports)) => create_explicit_ports(vmm, ports)?, - }; - - let console = Arc::new(Mutex::new(devices::virtio::Console::new(ports).unwrap())); - - vmm.exit_observers.push(console.clone()); - - event_manager - .add_subscriber(console.clone()) - .map_err(RegisterEvent)?; - - #[cfg(target_os = "linux")] - register_sigwinch_handler(console.lock().unwrap().get_sigwinch_fd()) - .map_err(RegisterFsSigwinch)?; - - // The device mutex mustn't be locked here otherwise it will deadlock. - attach_mmio_device(vmm, format!("hvc{id_number}"), intc, console) - .map_err(RegisterConsoleDevice)?; - - Ok(()) -} - -#[cfg(feature = "net")] -fn attach_net_devices( - vmm: &mut Vmm, - net_devices: &NetBuilder, - intc: IrqChip, -) -> Result<(), StartMicrovmError> { - for net_device in net_devices.list.iter() { - let id = net_device.lock().unwrap().id().to_string(); - - attach_mmio_device(vmm, id, intc.clone(), net_device.clone()) - .map_err(StartMicrovmError::RegisterNetDevice)?; - } - Ok(()) -} - -fn attach_unixsock_vsock_device( - vmm: &mut Vmm, - unix_vsock: &Arc>, - event_manager: &mut EventManager, - intc: IrqChip, -) -> std::result::Result<(), StartMicrovmError> { - use self::StartMicrovmError::*; - - event_manager - .add_subscriber(unix_vsock.clone()) - .map_err(RegisterEvent)?; - - let id = String::from(unix_vsock.lock().unwrap().id()); - - // The device mutex mustn't be locked here otherwise it will deadlock. - attach_mmio_device(vmm, id, intc, unix_vsock.clone()).map_err(RegisterVsockDevice)?; - - Ok(()) -} - -#[cfg(not(feature = "tee"))] -fn attach_balloon_device( - vmm: &mut Vmm, - event_manager: &mut EventManager, - intc: IrqChip, -) -> std::result::Result<(), StartMicrovmError> { - use self::StartMicrovmError::*; - - let balloon = Arc::new(Mutex::new(devices::virtio::Balloon::new().unwrap())); - - event_manager - .add_subscriber(balloon.clone()) - .map_err(RegisterEvent)?; - - let id = String::from(balloon.lock().unwrap().id()); - - // The device mutex mustn't be locked here otherwise it will deadlock. - attach_mmio_device(vmm, id, intc.clone(), balloon).map_err(RegisterBalloonDevice)?; - - Ok(()) -} - -#[cfg(feature = "blk")] -fn attach_block_devices( - vmm: &mut Vmm, - block_devs: &BlockBuilder, - intc: IrqChip, -) -> std::result::Result<(), StartMicrovmError> { - use self::StartMicrovmError::*; - - for block in block_devs.list.iter() { - let id = String::from(block.lock().unwrap().id()); - - // The device mutex mustn't be locked here otherwise it will deadlock. - attach_mmio_device(vmm, id, intc.clone(), block.clone()).map_err(RegisterBlockDevice)?; - } - - Ok(()) -} - -#[cfg(not(feature = "tee"))] -fn attach_rng_device( - vmm: &mut Vmm, - event_manager: &mut EventManager, - intc: IrqChip, -) -> std::result::Result<(), StartMicrovmError> { - use self::StartMicrovmError::*; - - let rng = Arc::new(Mutex::new(devices::virtio::Rng::new().unwrap())); - - event_manager - .add_subscriber(rng.clone()) - .map_err(RegisterEvent)?; - - let id = String::from(rng.lock().unwrap().id()); - - // The device mutex mustn't be locked here otherwise it will deadlock. - attach_mmio_device(vmm, id, intc.clone(), rng).map_err(RegisterRngDevice)?; - - Ok(()) -} - -#[cfg(feature = "gpu")] -#[allow(clippy::too_many_arguments)] -fn attach_gpu_device( - vmm: &mut Vmm, - shm_manager: &mut ShmManager, - #[cfg(not(feature = "tee"))] mut export_table: Option, - intc: IrqChip, - virgl_flags: u32, - displays: Box<[DisplayInfo]>, - display_backend: DisplayBackend<'static>, - #[cfg(target_os = "macos")] map_sender: Sender, -) -> std::result::Result<(), StartMicrovmError> { - use self::StartMicrovmError::*; - - let gpu = Arc::new(Mutex::new( - devices::virtio::Gpu::new( - virgl_flags, - displays, - display_backend, - #[cfg(target_os = "macos")] - map_sender, - ) - .unwrap(), - )); - - let id = String::from(gpu.lock().unwrap().id()); - - if let Some(shm_region) = shm_manager.gpu_region() { - gpu.lock().unwrap().set_shm_region(VirtioShmRegion { - host_addr: vmm - .guest_memory - .get_host_address(shm_region.guest_addr) - .map_err(StartMicrovmError::ShmHostAddr)? as u64, - guest_addr: shm_region.guest_addr.raw_value(), - size: shm_region.size, - }); - } - - #[cfg(not(feature = "tee"))] - if let Some(export_table) = export_table.take() { - gpu.lock().unwrap().set_export_table(export_table); - } - - // The device mutex mustn't be locked here otherwise it will deadlock. - attach_mmio_device(vmm, id, intc, gpu).map_err(RegisterGpuDevice)?; - - Ok(()) -} - -#[cfg(feature = "input")] -fn attach_input_devices( - vmm: &mut Vmm, - input_backends: &[( - krun_input::InputConfigBackend<'static>, - krun_input::InputEventProviderBackend<'static>, - )], - intc: IrqChip, -) -> std::result::Result<(), StartMicrovmError> { - use self::StartMicrovmError::*; - - for (index, (config_backend, events_backend)) in input_backends.iter().enumerate() { - let input_device = Arc::new(Mutex::new( - devices::virtio::input::Input::new(*config_backend, *events_backend).unwrap(), - )); - - let id = format!("input{}", index); - attach_mmio_device(vmm, id, intc.clone(), input_device).map_err(RegisterInputDevice)?; - } - - Ok(()) -} - -#[cfg(feature = "snd")] -fn attach_snd_device(vmm: &mut Vmm, intc: IrqChip) -> std::result::Result<(), StartMicrovmError> { - use self::StartMicrovmError::*; - - let snd = Arc::new(Mutex::new(devices::virtio::Snd::new().unwrap())); - let id = String::from(snd.lock().unwrap().id()); - - // The device mutex mustn't be locked here otherwise it will deadlock. - attach_mmio_device(vmm, id, intc, snd).map_err(RegisterSndDevice)?; - - Ok(()) -} - -#[cfg(test)] -pub mod tests { - use super::*; - use crate::vmm_config::kernel_bundle::KernelBundle; - - #[allow(unused)] - fn default_guest_memory( - mem_size_mib: usize, - ) -> std::result::Result< - (GuestMemoryMmap, ArchMemoryInfo, ShmManager, PayloadConfig), - StartMicrovmError, - > { - let mut vm_resources = VmResources::default(); - vm_resources.kernel_bundle = Some(KernelBundle { - host_addr: 0x1000, - guest_addr: 0x1000, - entry_addr: 0x1000, - size: 0x1000, - }); - - create_guest_memory(mem_size_mib, &vm_resources, &Payload::Empty) - } - - #[test] - #[cfg(target_arch = "x86_64")] - fn test_create_vcpus_x86_64() { - let vcpu_count = 2; - - let vcpu_config = VcpuConfig { - vcpu_count, - ht_enabled: false, - cpu_template: None, - nested_enabled: false, - }; - - let (guest_memory, _arch_memory_info, _shm_manager, _payload_config) = - default_guest_memory(128).unwrap(); - let vm = setup_vm(&guest_memory, false).unwrap(); - let _kvmioapic = KvmIoapic::new(&vm.fd()).unwrap(); - - // Dummy entry_addr, vcpus will not boot. - let entry_addr = GuestAddress(0); - let bus = devices::Bus::new(); - let vcpu_vec = create_vcpus_x86_64( - &vm, - &vcpu_config, - &guest_memory, - entry_addr, - &bus, - &EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap(), - true, - ) - .unwrap(); - assert_eq!(vcpu_vec.len(), vcpu_count as usize); - } - - #[test] - #[cfg(all(target_arch = "aarch64", target_os = "linux"))] - fn test_create_vcpus_aarch64() { - let (guest_memory, arch_memory_info, _shm_manager, _payload_config) = - default_guest_memory(128).unwrap(); - let vm = setup_vm(&guest_memory, false).unwrap(); - let vcpu_count = 2; - - let vcpu_config = VcpuConfig { - vcpu_count, - ht_enabled: false, - cpu_template: None, - nested_enabled: false, - }; - - // Dummy entry_addr, vcpus will not boot. - let entry_addr = GuestAddress(0); - let vcpu_vec = create_vcpus_aarch64( - &vm, - &vcpu_config, - &arch_memory_info, - entry_addr, - &EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap(), - ) - .unwrap(); - assert_eq!(vcpu_vec.len(), vcpu_count as usize); - } - - #[test] - fn test_error_messages() { - use crate::builder::StartMicrovmError::*; - let err = AttachBlockDevice(io::Error::from_raw_os_error(0)); - let _ = format!("{err}{err:?}"); - - let err = CreateRateLimiter(io::Error::from_raw_os_error(0)); - let _ = format!("{err}{err:?}"); - - let err = Internal(Error::Serial(io::Error::from_raw_os_error(0))); - let _ = format!("{err}{err:?}"); - - let err = InvalidKernelBundle(vm_memory::mmap::MmapRegionError::InvalidPointer); - let _ = format!("{err}{err:?}"); - - let err = KernelCmdline(String::from("dummy --cmdline")); - let _ = format!("{err}{err:?}"); - - let err = LoadCommandline(kernel::cmdline::Error::TooLarge); - let _ = format!("{err}{err:?}"); - - let err = MicroVMAlreadyRunning; - let _ = format!("{err}{err:?}"); - - let err = MissingKernelConfig; - let _ = format!("{err}{err:?}"); - - let err = MissingMemSizeConfig; - let _ = format!("{err}{err:?}"); - - let err = NetDeviceNotConfigured; - let _ = format!("{err}{err:?}"); - - let err = OpenBlockDevice(io::Error::from_raw_os_error(0)); - let _ = format!("{err}{err:?}"); - - let err = RegisterBlockDevice(device_manager::mmio::Error::EventFd( - io::Error::from_raw_os_error(0), - )); - let _ = format!("{err}{err:?}"); - - let err = RegisterEvent(EventManagerError::EpollCreate( - io::Error::from_raw_os_error(0), - )); - let _ = format!("{err}{err:?}"); - - let err = RegisterNetDevice(device_manager::mmio::Error::EventFd( - io::Error::from_raw_os_error(0), - )); - let _ = format!("{err}{err:?}"); - - let err = RegisterVsockDevice(device_manager::mmio::Error::EventFd( - io::Error::from_raw_os_error(0), - )); - let _ = format!("{err}{err:?}"); - } - - #[test] - fn test_kernel_cmdline_err_to_startuvm_err() { - let err = StartMicrovmError::from(kernel::cmdline::Error::HasSpace); - let _ = format!("{err}{err:?}"); - } -} diff --git a/vendor/krun-vmm/src/device_manager/hvf/mmio.rs b/vendor/krun-vmm/src/device_manager/hvf/mmio.rs deleted file mode 100644 index 6fd545465..000000000 --- a/vendor/krun-vmm/src/device_manager/hvf/mmio.rs +++ /dev/null @@ -1,564 +0,0 @@ -// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -// -// Portions Copyright 2017 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the THIRD-PARTY file. - -use std::collections::HashMap; -use std::sync::{Arc, Mutex}; -use std::{fmt, io}; - -use devices::fdt::DeviceInfoForFDT; -use devices::legacy::IrqChip; -use devices::{BusDevice, DeviceType}; -use kernel::cmdline as kernel_cmdline; -use polly::event_manager::EventManager; -#[cfg(target_arch = "aarch64")] -use utils::eventfd::EventFd; - -use crate::vstate::Vm; - -/// Errors for MMIO device manager. -#[allow(clippy::enum_variant_names)] -#[derive(Debug)] -pub enum Error { - /// Failed to create MmioTransport - CreateMmioTransport(devices::virtio::CreateMmioTransportError), - /// Failed to perform an operation on the bus. - BusError(devices::BusError), - /// Appending to kernel command line failed. - Cmdline(kernel_cmdline::Error), - /// Failure in creating or cloning an event fd. - EventFd(io::Error), - /// No more IRQs are available. - IrqsExhausted, - /// Registering an IO Event failed. - RegisterIoEvent, - /// Registering an IRQ FD failed. - RegisterIrqFd, - /// The device couldn't be found - DeviceNotFound, - /// Failed to update the mmio device. - UpdateFailed, -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - Error::CreateMmioTransport(ref e) => { - write!(f, "failed to create mmio transport for the device {e}") - } - Error::BusError(ref e) => write!(f, "failed to perform bus operation: {e}"), - Error::Cmdline(ref e) => { - write!(f, "unable to add device to kernel command line: {e}") - } - Error::EventFd(ref e) => write!(f, "failed to create or clone event descriptor: {e}"), - Error::IrqsExhausted => write!(f, "no more IRQs are available"), - Error::RegisterIoEvent => write!(f, "failed to register IO event"), - Error::RegisterIrqFd => write!(f, "failed to register irqfd"), - Error::DeviceNotFound => write!(f, "the device couldn't be found"), - Error::UpdateFailed => write!(f, "failed to update the mmio device"), - } - } -} - -impl From for crate::device_manager::mmio::Error { - fn from(e: devices::virtio::CreateMmioTransportError) -> Self { - Self::CreateMmioTransport(e) - } -} - -type Result = ::std::result::Result; - -/// This represents the size of the mmio device specified to the kernel as a cmdline option -/// It has to be larger than 0x100 (the offset where the configuration space starts from -/// the beginning of the memory mapped device registers) + the size of the configuration space -/// Currently hardcoded to 4K. -const MMIO_LEN: u64 = 0x1000; - -/// Manages the complexities of registering a MMIO device. -pub struct MMIODeviceManager { - pub bus: devices::Bus, - mmio_base: u64, - irq: u32, - last_irq: u32, - id_to_dev_info: HashMap<(DeviceType, String), MMIODeviceInfo>, -} - -impl MMIODeviceManager { - /// Create a new DeviceManager handling mmio devices (virtio net, block). - pub fn new(mmio_base: &mut u64, irq_interval: (u32, u32)) -> MMIODeviceManager { - if cfg!(target_arch = "aarch64") { - *mmio_base += MMIO_LEN; - } - - MMIODeviceManager { - mmio_base: *mmio_base, - irq: irq_interval.0, - last_irq: irq_interval.1, - bus: devices::Bus::new(), - id_to_dev_info: HashMap::new(), - } - } - - /// Register an already created MMIO device to be used via MMIO transport. - pub fn register_mmio_device( - &mut self, - mut mmio_device: devices::virtio::MmioTransport, - type_id: u32, - device_id: String, - ) -> Result<(u64, u32)> { - if self.irq > self.last_irq { - return Err(Error::IrqsExhausted); - } - - mmio_device.set_irq_line(self.irq); - - self.bus - .insert(Arc::new(Mutex::new(mmio_device)), self.mmio_base, MMIO_LEN) - .map_err(Error::BusError)?; - let ret = (self.mmio_base, self.irq); - self.id_to_dev_info.insert( - (DeviceType::Virtio(type_id), device_id), - MMIODeviceInfo { - addr: self.mmio_base, - len: MMIO_LEN, - irq: self.irq, - }, - ); - self.mmio_base += MMIO_LEN; - self.irq += 1; - - Ok(ret) - } - - #[cfg(target_arch = "aarch64")] - /// Register an early console at some MMIO address. - pub fn register_mmio_serial( - &mut self, - _vm: &Vm, - cmdline: &mut kernel_cmdline::Cmdline, - intc: IrqChip, - serial: Arc>, - ) -> Result<()> { - if self.irq > self.last_irq { - return Err(Error::IrqsExhausted); - } - - { - let mut serial = serial.lock().unwrap(); - serial.set_intc(intc); - serial.set_irq_line(self.irq); - } - - self.bus - .insert(serial, self.mmio_base, MMIO_LEN) - .map_err(Error::BusError)?; - - cmdline - .insert( - "earlycon", - &format!("pl011,mmio32,0x{:08x}", self.mmio_base), - ) - .map_err(Error::Cmdline)?; - - let ret = self.mmio_base; - self.id_to_dev_info.insert( - (DeviceType::Serial, DeviceType::Serial.to_string()), - MMIODeviceInfo { - addr: ret, - len: MMIO_LEN, - irq: self.irq, - }, - ); - - self.mmio_base += MMIO_LEN; - self.irq += 1; - - Ok(()) - } - - #[cfg(target_arch = "aarch64")] - /// Register a MMIO RTC device. - pub fn register_mmio_rtc(&mut self, _vm: &Vm, _intc: IrqChip) -> Result<()> { - if self.irq > self.last_irq { - return Err(Error::IrqsExhausted); - } - - // Attaching the RTC device. - let rtc_evt = EventFd::new(utils::eventfd::EFD_NONBLOCK).map_err(Error::EventFd)?; - let device = devices::legacy::RTC::new(rtc_evt.try_clone().map_err(Error::EventFd)?); - - self.bus - .insert(Arc::new(Mutex::new(device)), self.mmio_base, MMIO_LEN) - .map_err(Error::BusError)?; - - let ret = self.mmio_base; - self.id_to_dev_info.insert( - (DeviceType::RTC, "rtc".to_string()), - MMIODeviceInfo { - addr: ret, - len: MMIO_LEN, - irq: self.irq, - }, - ); - - self.mmio_base += MMIO_LEN; - self.irq += 1; - - Ok(()) - } - - #[cfg(target_arch = "aarch64")] - /// Register a GPIO - pub fn register_mmio_gpio( - &mut self, - _vm: &Vm, - intc: IrqChip, - event_manager: &mut EventManager, - shutdown_efd: EventFd, - ) -> Result<()> { - // Attaching the GPIO device. - let gpio_evt = EventFd::new(utils::eventfd::EFD_NONBLOCK).map_err(Error::EventFd)?; - let gpio = Arc::new(Mutex::new(devices::legacy::Gpio::new( - shutdown_efd, - gpio_evt.try_clone().map_err(Error::EventFd)?, - ))); - - event_manager.add_subscriber(gpio.clone()).unwrap(); - - if self.irq > self.last_irq { - return Err(Error::IrqsExhausted); - } - - { - let mut gpio = gpio.lock().unwrap(); - gpio.set_intc(intc); - gpio.set_irq_line(self.irq); - } - - self.bus - .insert(gpio, self.mmio_base, MMIO_LEN) - .map_err(Error::BusError)?; - - let ret = self.mmio_base; - self.id_to_dev_info.insert( - (DeviceType::Gpio, DeviceType::Gpio.to_string()), - MMIODeviceInfo { - addr: ret, - len: MMIO_LEN, - irq: self.irq, - }, - ); - - self.mmio_base += MMIO_LEN; - self.irq += 1; - - Ok(()) - } - - #[cfg(target_arch = "aarch64")] - /// Register a MMIO GIC device. - pub fn register_mmio_gic(&mut self, _vm: &Vm, intc: IrqChip) -> Result<()> { - let (mmio_addr, mmio_size) = { - let intc = intc.lock().unwrap(); - (intc.get_mmio_addr(), intc.get_mmio_size()) - }; - - // The in-kernel GIC reports a size of 0 to tell us we don't need to map - // anything in the guest. - if mmio_size != 0 { - self.bus - .insert(intc, mmio_addr, mmio_size) - .map_err(Error::BusError)?; - } - - Ok(()) - } - - #[cfg(target_arch = "aarch64")] - /// Gets the information of the devices registered up to some point in time. - pub fn get_device_info(&self) -> &HashMap<(DeviceType, String), MMIODeviceInfo> { - &self.id_to_dev_info - } - - /// Gets the specified device. - pub fn get_device( - &self, - device_type: DeviceType, - device_id: &str, - ) -> Option<&Mutex> { - if let Some(dev_info) = self - .id_to_dev_info - .get(&(device_type, device_id.to_string())) - { - if let Some((_, device)) = self.bus.get_device(dev_info.addr) { - return Some(device); - } - } - None - } -} - -/// Private structure for storing information about the MMIO device registered at some address on the bus. -#[derive(Clone, Debug)] -pub struct MMIODeviceInfo { - addr: u64, - irq: u32, - len: u64, -} - -#[cfg(target_arch = "aarch64")] -impl DeviceInfoForFDT for MMIODeviceInfo { - fn addr(&self) -> u64 { - self.addr - } - fn irq(&self) -> u32 { - self.irq - } - fn length(&self) -> u64 { - self.len - } -} - -#[cfg(test)] -mod tests { - use super::*; - use arch; - use devices::legacy::DummyIrqChip; - use devices::virtio::{ - ActivateResult, DeviceQueue, InterruptTransport, QueueConfig, VirtioDevice, - }; - use std::sync::Arc; - use vm_memory::{GuestAddress, GuestMemoryMmap}; - - const QUEUE_SIZES: &[u16] = &[64]; - - impl MMIODeviceManager { - fn register_virtio_device( - &mut self, - guest_mem: GuestMemoryMmap, - device: Arc>, - _cmdline: &mut kernel_cmdline::Cmdline, - type_id: u32, - device_id: &str, - ) -> Result { - let mmio_device = - devices::virtio::MmioTransport::new(guest_mem, DummyIrqChip::new().into(), device) - .unwrap(); - let (mmio_base, _irq) = - self.register_mmio_device(mmio_device, type_id, device_id.to_string())?; - Ok(mmio_base) - } - } - - #[allow(dead_code)] - struct DummyDevice { - dummy: u32, - queue_config: Vec, - } - - impl DummyDevice { - pub fn new() -> Self { - DummyDevice { - dummy: 0, - queue_config: QUEUE_SIZES.iter().map(|&s| QueueConfig::new(s)).collect(), - } - } - } - - impl devices::virtio::VirtioDevice for DummyDevice { - fn avail_features(&self) -> u64 { - 0 - } - - fn acked_features(&self) -> u64 { - 0 - } - - fn set_acked_features(&mut self, _: u64) {} - - fn device_type(&self) -> u32 { - 0 - } - - fn device_name(&self) -> &str { - "dummy" - } - - fn queue_config(&self) -> &[QueueConfig] { - &self.queue_config - } - - fn read_config(&self, offset: u64, data: &mut [u8]) { - let _ = offset; - let _ = data; - } - - fn write_config(&mut self, offset: u64, data: &[u8]) { - let _ = offset; - let _ = data; - } - - fn activate( - &mut self, - _mem: GuestMemoryMmap, - _interrupt: InterruptTransport, - _queues: Vec, - ) -> ActivateResult { - Ok(()) - } - - fn is_activated(&self) -> bool { - false - } - } - - #[test] - fn test_register_virtio_device() { - let start_addr1 = GuestAddress(0x0); - let start_addr2 = GuestAddress(0x1000); - let guest_mem = - GuestMemoryMmap::from_ranges(&[(start_addr1, 0x1000), (start_addr2, 0x1000)]).unwrap(); - let mut device_manager = - MMIODeviceManager::new(&mut 0xd000_0000, (arch::IRQ_BASE, arch::IRQ_MAX)); - - let mut cmdline = kernel_cmdline::Cmdline::new(4096); - let dummy = Arc::new(Mutex::new(DummyDevice::new())); - - assert!(device_manager - .register_virtio_device(guest_mem, dummy, &mut cmdline, 0, "dummy") - .is_ok()); - } - - #[test] - fn test_register_too_many_devices() { - let start_addr1 = GuestAddress(0x0); - let start_addr2 = GuestAddress(0x1000); - let guest_mem = - GuestMemoryMmap::from_ranges(&[(start_addr1, 0x1000), (start_addr2, 0x1000)]).unwrap(); - let mut device_manager = - MMIODeviceManager::new(&mut 0xd000_0000, (arch::IRQ_BASE, arch::IRQ_MAX)); - - let mut cmdline = kernel_cmdline::Cmdline::new(4096); - - for _i in arch::IRQ_BASE..=arch::IRQ_MAX { - device_manager - .register_virtio_device( - guest_mem.clone(), - Arc::new(Mutex::new(DummyDevice::new())), - &mut cmdline, - 0, - "dummy1", - ) - .unwrap(); - } - assert_eq!( - format!( - "{}", - device_manager - .register_virtio_device( - guest_mem, - Arc::new(Mutex::new(DummyDevice::new())), - &mut cmdline, - 0, - "dummy2" - ) - .unwrap_err() - ), - "no more IRQs are available".to_string() - ); - } - - #[test] - fn test_dummy_device() { - let dummy = DummyDevice::new(); - assert_eq!(dummy.device_type(), 0); - assert_eq!(dummy.queue_config().len(), QUEUE_SIZES.len()); - } - - #[test] - fn test_error_messages() { - let device_manager = - MMIODeviceManager::new(&mut 0xd000_0000, (arch::IRQ_BASE, arch::IRQ_MAX)); - let mut cmdline = kernel_cmdline::Cmdline::new(4096); - let e = Error::Cmdline( - cmdline - .insert( - "virtio_mmio=device", - &format!( - "{}K@0x{:08x}:{}", - MMIO_LEN / 1024, - device_manager.mmio_base, - device_manager.irq - ), - ) - .unwrap_err(), - ); - assert_eq!( - format!("{}", e), - format!( - "unable to add device to kernel command line: {}", - kernel_cmdline::Error::HasEquals - ), - ); - assert_eq!( - format!("{}", Error::UpdateFailed), - "failed to update the mmio device" - ); - assert_eq!( - format!("{}", Error::BusError(devices::BusError::Overlap)), - format!( - "failed to perform bus operation: {}", - devices::BusError::Overlap - ) - ); - assert_eq!( - format!("{}", Error::IrqsExhausted), - "no more IRQs are available" - ); - assert_eq!( - format!("{}", Error::RegisterIoEvent), - "failed to register IO event" - ); - assert_eq!( - format!("{}", Error::RegisterIrqFd), - "failed to register irqfd" - ); - } - - #[test] - fn test_device_info() { - let start_addr1 = GuestAddress(0x0); - let start_addr2 = GuestAddress(0x1000); - let guest_mem = - GuestMemoryMmap::from_ranges(&[(start_addr1, 0x1000), (start_addr2, 0x1000)]).unwrap(); - let mut device_manager = - MMIODeviceManager::new(&mut 0xd000_0000, (arch::IRQ_BASE, arch::IRQ_MAX)); - let mut cmdline = kernel_cmdline::Cmdline::new(4096); - let dummy = Arc::new(Mutex::new(DummyDevice::new())); - - let type_id = 0; - let id = String::from("foo"); - if let Ok(addr) = - device_manager.register_virtio_device(guest_mem, dummy, &mut cmdline, type_id, &id) - { - assert!(device_manager - .get_device(DeviceType::Virtio(type_id), &id) - .is_some()); - assert_eq!( - addr, - device_manager.id_to_dev_info[&(DeviceType::Virtio(type_id), id.clone())].addr - ); - assert_eq!( - arch::IRQ_BASE, - device_manager.id_to_dev_info[&(DeviceType::Virtio(type_id), id.clone())].irq - ); - } - let id = "bar"; - assert!(device_manager - .get_device(DeviceType::Virtio(type_id), &id) - .is_none()); - } -} diff --git a/vendor/krun-vmm/src/device_manager/hvf/mod.rs b/vendor/krun-vmm/src/device_manager/hvf/mod.rs deleted file mode 100644 index a17106ea6..000000000 --- a/vendor/krun-vmm/src/device_manager/hvf/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod mmio; diff --git a/vendor/krun-vmm/src/device_manager/kvm/mmio.rs b/vendor/krun-vmm/src/device_manager/kvm/mmio.rs deleted file mode 100644 index e739afb42..000000000 --- a/vendor/krun-vmm/src/device_manager/kvm/mmio.rs +++ /dev/null @@ -1,578 +0,0 @@ -// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -// -// Portions Copyright 2017 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the THIRD-PARTY file. - -use std::collections::HashMap; -use std::sync::{Arc, Mutex}; -use std::{fmt, io}; - -#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))] -use devices::fdt::DeviceInfoForFDT; -#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))] -use devices::legacy::IrqChip; -use devices::{BusDevice, DeviceType}; -use kernel::cmdline as kernel_cmdline; -use kvm_ioctls::{IoEventAddress, VmFd}; -#[cfg(target_arch = "aarch64")] -use utils::eventfd::EventFd; - -/// Errors for MMIO device manager. -#[allow(clippy::enum_variant_names)] -#[derive(Debug)] -pub enum Error { - /// Failed to create MmioTransport - CreateMmioTransport(devices::virtio::CreateMmioTransportError), - /// Failed to perform an operation on the bus. - BusError(devices::BusError), - /// Appending to kernel command line failed. - Cmdline(kernel_cmdline::Error), - /// Failure in creating or cloning an event fd. - EventFd(io::Error), - /// No more IRQs are available. - IrqsExhausted, - /// Registering an IO Event failed. - RegisterIoEvent(kvm_ioctls::Error), - /// Registering an IRQ FD failed. - RegisterIrqFd(kvm_ioctls::Error), - /// The device couldn't be found - DeviceNotFound, - /// Failed to update the mmio device. - UpdateFailed, -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - Error::CreateMmioTransport(ref e) => { - write!(f, "failed to create mmio transport for the device {e}") - } - Error::BusError(ref e) => write!(f, "failed to perform bus operation: {e}"), - Error::Cmdline(ref e) => { - write!(f, "unable to add device to kernel command line: {e}") - } - Error::EventFd(ref e) => write!(f, "failed to create or clone event descriptor: {e}"), - Error::IrqsExhausted => write!(f, "no more IRQs are available"), - Error::RegisterIoEvent(ref e) => write!(f, "failed to register IO event: {e}"), - Error::RegisterIrqFd(ref e) => write!(f, "failed to register irqfd: {e}"), - Error::DeviceNotFound => write!(f, "the device couldn't be found"), - Error::UpdateFailed => write!(f, "failed to update the mmio device"), - } - } -} - -impl From for Error { - fn from(e: devices::virtio::CreateMmioTransportError) -> Self { - Self::CreateMmioTransport(e) - } -} - -type Result = ::std::result::Result; - -/// This represents the size of the mmio device specified to the kernel as a cmdline option -/// It has to be larger than 0x100 (the offset where the configuration space starts from -/// the beginning of the memory mapped device registers) + the size of the configuration space -/// Currently hardcoded to 4K. -const MMIO_LEN: u64 = 0x1000; - -/// Manages the complexities of registering a MMIO device. -pub struct MMIODeviceManager { - pub bus: devices::Bus, - mmio_base: u64, - irq: u32, - last_irq: u32, - id_to_dev_info: HashMap<(DeviceType, String), MMIODeviceInfo>, -} - -impl MMIODeviceManager { - /// Create a new DeviceManager handling mmio devices (virtio net, block). - pub fn new(mmio_base: &mut u64, irq_interval: (u32, u32)) -> MMIODeviceManager { - if cfg!(any(target_arch = "aarch64", target_arch = "riscv64")) { - *mmio_base += MMIO_LEN; - } - MMIODeviceManager { - mmio_base: *mmio_base, - irq: irq_interval.0, - last_irq: irq_interval.1, - bus: devices::Bus::new(), - id_to_dev_info: HashMap::new(), - } - } - - /// Register a MMIO IOAPIC device. - #[cfg(target_arch = "x86_64")] - pub fn register_mmio_ioapic( - &mut self, - intc: Option>>, - ) -> Result<()> { - if let Some(intc) = intc { - let (addr, size) = { - let intc = intc.lock().unwrap(); - (intc.get_mmio_addr(), intc.get_mmio_size()) - }; - self.bus.insert(intc, addr, size).map_err(Error::BusError)?; - } - - Ok(()) - } - - /// Register an already created MMIO device to be used via MMIO transport. - pub fn register_mmio_device( - &mut self, - vm: &VmFd, - mut mmio_device: devices::virtio::MmioTransport, - type_id: u32, - device_id: String, - ) -> Result<(u64, u32)> { - if self.irq > self.last_irq { - return Err(Error::IrqsExhausted); - } - - for (i, queue_evt) in mmio_device.queue_evts().iter().enumerate() { - let io_addr = IoEventAddress::Mmio( - self.mmio_base + u64::from(devices::virtio::NOTIFY_REG_OFFSET), - ); - - vm.register_ioevent(queue_evt, &io_addr, i as u32) - .map_err(Error::RegisterIoEvent)?; - } - - vm.register_irqfd(mmio_device.interrupt_evt(), self.irq) - .map_err(Error::RegisterIrqFd)?; - - mmio_device.set_irq_line(self.irq); - - self.bus - .insert(Arc::new(Mutex::new(mmio_device)), self.mmio_base, MMIO_LEN) - .map_err(Error::BusError)?; - let ret = (self.mmio_base, self.irq); - self.id_to_dev_info.insert( - (DeviceType::Virtio(type_id), device_id), - MMIODeviceInfo { - addr: self.mmio_base, - _len: MMIO_LEN, - _irq: self.irq, - }, - ); - self.mmio_base += MMIO_LEN; - self.irq += 1; - - Ok(ret) - } - - /// Append a registered MMIO device to the kernel cmdline. - #[cfg(target_arch = "x86_64")] - pub fn add_device_to_cmdline( - &mut self, - cmdline: &mut kernel_cmdline::Cmdline, - mmio_base: u64, - irq: u32, - ) -> Result<()> { - // as per doc, [virtio_mmio.]device=@: needs to be appended - // to kernel commandline for virtio mmio devices to get recognized - // the size parameter has to be transformed to KiB, so dividing hexadecimal value in - // bytes to 1024; further, the '{}' formatting rust construct will automatically - // transform it to decimal - cmdline - .insert( - "virtio_mmio.device", - &format!("{}K@0x{:08x}:{}", MMIO_LEN / 1024, mmio_base, irq), - ) - .map_err(Error::Cmdline) - } - - #[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))] - /// Register an early console at some MMIO address. - pub fn register_mmio_serial( - &mut self, - vm: &VmFd, - cmdline: &mut kernel_cmdline::Cmdline, - intc: IrqChip, - serial: Arc>, - ) -> Result<()> { - if self.irq > self.last_irq { - return Err(Error::IrqsExhausted); - } - - vm.register_irqfd(serial.lock().unwrap().interrupt_evt(), self.irq) - .map_err(Error::RegisterIrqFd)?; - - { - let mut serial = serial.lock().unwrap(); - serial.set_intc(intc); - serial.set_irq_line(self.irq); - } - - self.bus - .insert(serial, self.mmio_base, MMIO_LEN) - .map_err(Error::BusError)?; - - cmdline - .insert( - "earlycon", - #[cfg(target_arch = "aarch64")] - &format!("pl011,mmio32,0x{:08x}", self.mmio_base), - #[cfg(target_arch = "riscv64")] - &format!("uart,mmio,0x{:08x}", self.mmio_base), - ) - .map_err(Error::Cmdline)?; - - let ret = self.mmio_base; - self.id_to_dev_info.insert( - (DeviceType::Serial, DeviceType::Serial.to_string()), - MMIODeviceInfo { - addr: ret, - _len: MMIO_LEN, - _irq: self.irq, - }, - ); - - self.mmio_base += MMIO_LEN; - self.irq += 1; - - Ok(()) - } - - #[cfg(target_arch = "aarch64")] - /// Register a MMIO RTC device. - pub fn register_mmio_rtc(&mut self, vm: &VmFd) -> Result<()> { - if self.irq > self.last_irq { - return Err(Error::IrqsExhausted); - } - - // Attaching the RTC device. - let rtc_evt = EventFd::new(utils::eventfd::EFD_NONBLOCK).map_err(Error::EventFd)?; - let device = devices::legacy::RTC::new(rtc_evt.try_clone().map_err(Error::EventFd)?); - vm.register_irqfd(&rtc_evt, self.irq) - .map_err(Error::RegisterIrqFd)?; - - self.bus - .insert(Arc::new(Mutex::new(device)), self.mmio_base, MMIO_LEN) - .map_err(Error::BusError)?; - - let ret = self.mmio_base; - self.id_to_dev_info.insert( - (DeviceType::RTC, "rtc".to_string()), - MMIODeviceInfo { - addr: ret, - _len: MMIO_LEN, - _irq: self.irq, - }, - ); - - self.mmio_base += MMIO_LEN; - self.irq += 1; - - Ok(()) - } - - #[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))] - /// Gets the information of the devices registered up to some point in time. - pub fn get_device_info(&self) -> &HashMap<(DeviceType, String), MMIODeviceInfo> { - &self.id_to_dev_info - } - - /// Gets the the specified device. - pub fn get_device( - &self, - device_type: DeviceType, - device_id: &str, - ) -> Option<&Mutex> { - if let Some(dev_info) = self - .id_to_dev_info - .get(&(device_type, device_id.to_string())) - { - if let Some((_, device)) = self.bus.get_device(dev_info.addr) { - return Some(device); - } - } - None - } -} - -/// Private structure for storing information about the MMIO device registered at some address on the bus. -#[derive(Clone, Debug)] -pub struct MMIODeviceInfo { - addr: u64, - _irq: u32, - _len: u64, -} - -#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))] -impl DeviceInfoForFDT for MMIODeviceInfo { - fn addr(&self) -> u64 { - self.addr - } - fn irq(&self) -> u32 { - self._irq - } - fn length(&self) -> u64 { - self._len - } -} - -#[cfg(test)] -mod tests { - use super::super::super::super::builder; - use super::*; - use arch; - use devices::legacy::DummyIrqChip; - #[cfg(target_arch = "aarch64")] - use devices::legacy::KvmGicV3; - #[cfg(target_arch = "x86_64")] - use devices::legacy::KvmIoapic; - use devices::virtio::{ - ActivateResult, DeviceQueue, InterruptTransport, QueueConfig, VirtioDevice, - }; - use std::sync::Arc; - use utils::errno; - use vm_memory::{GuestAddress, GuestMemoryMmap}; - - const QUEUE_CONFIG: &[QueueConfig] = &[QueueConfig::new(64)]; - - impl MMIODeviceManager { - fn register_virtio_device( - &mut self, - vm: &VmFd, - guest_mem: GuestMemoryMmap, - device: Arc>, - _cmdline: &mut kernel_cmdline::Cmdline, - type_id: u32, - device_id: &str, - ) -> Result { - let mmio_device = - devices::virtio::MmioTransport::new(guest_mem, DummyIrqChip::new().into(), device) - .unwrap(); - let (mmio_base, _irq) = - self.register_mmio_device(vm, mmio_device, type_id, device_id.to_string())?; - #[cfg(target_arch = "x86_64")] - self.add_device_to_cmdline(_cmdline, mmio_base, _irq)?; - Ok(mmio_base) - } - } - - #[allow(dead_code)] - struct DummyDevice { - dummy: u32, - } - - impl DummyDevice { - pub fn new() -> Self { - DummyDevice { dummy: 0 } - } - } - - impl devices::virtio::VirtioDevice for DummyDevice { - fn avail_features(&self) -> u64 { - 0 - } - - fn acked_features(&self) -> u64 { - 0 - } - - fn set_acked_features(&mut self, _: u64) {} - - fn device_type(&self) -> u32 { - 0 - } - - fn device_name(&self) -> &str { - "dummy" - } - - fn queue_config(&self) -> &[QueueConfig] { - &QUEUE_CONFIG - } - - fn read_config(&self, offset: u64, data: &mut [u8]) { - let _ = offset; - let _ = data; - } - - fn write_config(&mut self, offset: u64, data: &[u8]) { - let _ = offset; - let _ = data; - } - - fn activate( - &mut self, - _mem: GuestMemoryMmap, - _intc: InterruptTransport, - _queues: Vec, - ) -> ActivateResult { - Ok(()) - } - - fn is_activated(&self) -> bool { - false - } - } - - #[test] - fn test_register_virtio_device() { - let start_addr1 = GuestAddress(0x0); - let start_addr2 = GuestAddress(0x1000); - let guest_mem = - GuestMemoryMmap::from_ranges(&[(start_addr1, 0x1000), (start_addr2, 0x1000)]).unwrap(); - let vm = builder::setup_vm(&guest_mem, false).unwrap(); - let mut device_manager = - MMIODeviceManager::new(&mut 0xd000_0000, (arch::IRQ_BASE, arch::IRQ_MAX)); - #[cfg(target_arch = "x86_64")] - let _kvmioapic = KvmIoapic::new(vm.fd()).unwrap(); - #[cfg(target_arch = "aarch64")] - let _gic = KvmGicV3::new(vm.fd(), 1).unwrap(); - - let mut cmdline = kernel_cmdline::Cmdline::new(4096); - let dummy = Arc::new(Mutex::new(DummyDevice::new())); - - assert!(device_manager - .register_virtio_device(vm.fd(), guest_mem, dummy, &mut cmdline, 0, "dummy") - .is_ok()); - } - - #[test] - fn test_register_too_many_devices() { - let start_addr1 = GuestAddress(0x0); - let start_addr2 = GuestAddress(0x1000); - let guest_mem = - GuestMemoryMmap::from_ranges(&[(start_addr1, 0x1000), (start_addr2, 0x1000)]).unwrap(); - let vm = builder::setup_vm(&guest_mem, false).unwrap(); - let mut device_manager = - MMIODeviceManager::new(&mut 0xd000_0000, (arch::IRQ_BASE, arch::IRQ_MAX)); - #[cfg(target_arch = "x86_64")] - let _kvmioapic = KvmIoapic::new(vm.fd()).unwrap(); - #[cfg(target_arch = "aarch64")] - let _gic = KvmGicV3::new(vm.fd(), 1).unwrap(); - - let mut cmdline = kernel_cmdline::Cmdline::new(4096); - - for _i in arch::IRQ_BASE..=arch::IRQ_MAX { - device_manager - .register_virtio_device( - vm.fd(), - guest_mem.clone(), - Arc::new(Mutex::new(DummyDevice::new())), - &mut cmdline, - 0, - "dummy1", - ) - .unwrap(); - } - assert_eq!( - format!( - "{}", - device_manager - .register_virtio_device( - vm.fd(), - guest_mem, - Arc::new(Mutex::new(DummyDevice::new())), - &mut cmdline, - 0, - "dummy2" - ) - .unwrap_err() - ), - "no more IRQs are available".to_string() - ); - } - - #[test] - fn test_dummy_device() { - let dummy = DummyDevice::new(); - assert_eq!(dummy.device_type(), 0); - assert_eq!(dummy.queue_config().len(), QUEUE_CONFIG.len()); - } - - #[test] - fn test_error_messages() { - let device_manager = - MMIODeviceManager::new(&mut 0xd000_0000, (arch::IRQ_BASE, arch::IRQ_MAX)); - let mut cmdline = kernel_cmdline::Cmdline::new(4096); - let e = Error::Cmdline( - cmdline - .insert( - "virtio_mmio=device", - &format!( - "{}K@0x{:08x}:{}", - MMIO_LEN / 1024, - device_manager.mmio_base, - device_manager.irq - ), - ) - .unwrap_err(), - ); - assert_eq!( - format!("{e}"), - format!( - "unable to add device to kernel command line: {}", - kernel_cmdline::Error::HasEquals - ), - ); - assert_eq!( - format!("{}", Error::UpdateFailed), - "failed to update the mmio device" - ); - assert_eq!( - format!("{}", Error::BusError(devices::BusError::Overlap)), - format!( - "failed to perform bus operation: {}", - devices::BusError::Overlap - ) - ); - assert_eq!( - format!("{}", Error::IrqsExhausted), - "no more IRQs are available" - ); - assert_eq!( - format!("{}", Error::RegisterIoEvent(errno::Error::new(0))), - format!("failed to register IO event: {}", errno::Error::new(0)) - ); - assert_eq!( - format!("{}", Error::RegisterIrqFd(errno::Error::new(0))), - format!("failed to register irqfd: {}", errno::Error::new(0)) - ); - } - - #[test] - fn test_device_info() { - let start_addr1 = GuestAddress(0x0); - let start_addr2 = GuestAddress(0x1000); - let guest_mem = - GuestMemoryMmap::from_ranges(&[(start_addr1, 0x1000), (start_addr2, 0x1000)]).unwrap(); - let vm = builder::setup_vm(&guest_mem, false).unwrap(); - let mut device_manager = - MMIODeviceManager::new(&mut 0xd000_0000, (arch::IRQ_BASE, arch::IRQ_MAX)); - let mut cmdline = kernel_cmdline::Cmdline::new(4096); - let dummy = Arc::new(Mutex::new(DummyDevice::new())); - - let type_id = 0; - let id = String::from("foo"); - if let Ok(addr) = device_manager.register_virtio_device( - vm.fd(), - guest_mem, - dummy, - &mut cmdline, - type_id, - &id, - ) { - assert!(device_manager - .get_device(DeviceType::Virtio(type_id), &id) - .is_some()); - assert_eq!( - addr, - device_manager.id_to_dev_info[&(DeviceType::Virtio(type_id), id.clone())].addr - ); - assert_eq!( - arch::IRQ_BASE, - device_manager.id_to_dev_info[&(DeviceType::Virtio(type_id), id.clone())]._irq - ); - } - let id = "bar"; - assert!(device_manager - .get_device(DeviceType::Virtio(type_id), id) - .is_none()); - } -} diff --git a/vendor/krun-vmm/src/device_manager/kvm/mod.rs b/vendor/krun-vmm/src/device_manager/kvm/mod.rs deleted file mode 100644 index a17106ea6..000000000 --- a/vendor/krun-vmm/src/device_manager/kvm/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod mmio; diff --git a/vendor/krun-vmm/src/device_manager/legacy.rs b/vendor/krun-vmm/src/device_manager/legacy.rs deleted file mode 100644 index 27033aade..000000000 --- a/vendor/krun-vmm/src/device_manager/legacy.rs +++ /dev/null @@ -1,184 +0,0 @@ -// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -// -// Portions Copyright 2017 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the THIRD-PARTY file. -#![cfg(target_arch = "x86_64")] - -use std::fmt; -use std::sync::{Arc, Mutex}; - -use devices; -use utils::eventfd::EventFd; - -/// Errors corresponding to the `PortIODeviceManager`. -#[derive(Debug)] -pub enum Error { - /// Cannot add legacy device to Bus. - BusError(devices::BusError), - /// Cannot create EventFd. - EventFd(std::io::Error), -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use self::Error::*; - - match *self { - BusError(ref err) => write!(f, "Failed to add legacy device to Bus: {err}"), - EventFd(ref err) => write!(f, "Failed to create EventFd: {err}"), - } - } -} - -type Result = ::std::result::Result; - -/// The `PortIODeviceManager` is a wrapper that is used for registering legacy devices -/// on an I/O Bus. It currently manages the uart and i8042 devices. -/// The `LegacyDeviceManger` should be initialized only by using the constructor. -pub struct PortIODeviceManager { - pub io_bus: devices::Bus, - pub cmos: Arc>, - pub stdio_serial: Vec>>, - pub i8042: Arc>, - - pub com_evt_1: EventFd, - pub com_evt_2: EventFd, - pub com_evt_3: EventFd, - pub com_evt_4: EventFd, - pub kbd_evt: EventFd, -} - -impl PortIODeviceManager { - /// Create a new DeviceManager handling legacy devices (uart, i8042). - pub fn new( - cmos: Arc>, - stdio_serial: Vec>>, - i8042_reset_evfd: EventFd, - ) -> Result { - let io_bus = devices::Bus::new(); - let mut evts: Vec = Vec::new(); - for i in 0..4 { - let com_evt = match stdio_serial.get(i) { - Some(s) => s - .lock() - .unwrap() - .interrupt_evt() - .try_clone() - .map_err(Error::EventFd)?, - None => EventFd::new(utils::eventfd::EFD_NONBLOCK).map_err(Error::EventFd)?, - }; - evts.push(com_evt); - } - - let kbd_evt = EventFd::new(utils::eventfd::EFD_NONBLOCK).map_err(Error::EventFd)?; - - let i8042 = Arc::new(Mutex::new(devices::legacy::I8042Device::new( - i8042_reset_evfd, - kbd_evt.try_clone().map_err(Error::EventFd)?, - ))); - - Ok(PortIODeviceManager { - io_bus, - cmos, - stdio_serial, - i8042, - com_evt_1: evts[0].try_clone().map_err(Error::EventFd)?, - com_evt_2: evts[1].try_clone().map_err(Error::EventFd)?, - com_evt_3: evts[2].try_clone().map_err(Error::EventFd)?, - com_evt_4: evts[3].try_clone().map_err(Error::EventFd)?, - kbd_evt, - }) - } - - /// Register supported legacy devices. - pub fn register_devices(&mut self) -> Result<()> { - self.io_bus - .insert(self.cmos.clone(), 0x70, 0x8) - .map_err(Error::BusError)?; - - if let Some(serial) = self.stdio_serial.first() { - self.io_bus - .insert(serial.clone(), 0x3f8, 0x8) - .map_err(Error::BusError)?; - } - self.io_bus - .insert( - self.stdio_serial - .get(1) - .unwrap_or(&Arc::new(Mutex::new(devices::legacy::Serial::new_sink( - self.com_evt_2.try_clone().map_err(Error::EventFd)?, - )))) - .clone(), - 0x2f8, - 0x8, - ) - .map_err(Error::BusError)?; - self.io_bus - .insert( - self.stdio_serial - .get(2) - .unwrap_or(&Arc::new(Mutex::new(devices::legacy::Serial::new_sink( - self.com_evt_3.try_clone().map_err(Error::EventFd)?, - )))) - .clone(), - 0x3e8, - 0x8, - ) - .map_err(Error::BusError)?; - self.io_bus - .insert( - self.stdio_serial - .get(3) - .unwrap_or(&Arc::new(Mutex::new(devices::legacy::Serial::new_sink( - self.com_evt_4.try_clone().map_err(Error::EventFd)?, - )))) - .clone(), - 0x2e8, - 0x8, - ) - .map_err(Error::BusError)?; - self.io_bus - .insert(self.i8042.clone(), 0x060, 0x5) - .map_err(Error::BusError)?; - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_register_legacy_devices() { - let serial = - devices::legacy::Serial::new_sink(EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap()); - let cmos = devices::legacy::Cmos::new(0, 0); - let ldm = PortIODeviceManager::new( - Arc::new(Mutex::new(cmos)), - vec![Arc::new(Mutex::new(serial))], - EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap(), - ); - assert!(ldm.is_ok()); - assert!(&ldm.unwrap().register_devices().is_ok()); - } - - #[test] - fn test_debug_error() { - assert_eq!( - format!("{}", Error::BusError(devices::BusError::Overlap)), - format!( - "Failed to add legacy device to Bus: {}", - devices::BusError::Overlap - ) - ); - assert_eq!( - format!("{}", Error::EventFd(std::io::Error::from_raw_os_error(1))), - format!( - "Failed to create EventFd: {}", - std::io::Error::from_raw_os_error(1) - ) - ); - } -} diff --git a/vendor/krun-vmm/src/device_manager/mod.rs b/vendor/krun-vmm/src/device_manager/mod.rs deleted file mode 100644 index b73c8efeb..000000000 --- a/vendor/krun-vmm/src/device_manager/mod.rs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -// -// Portions Copyright 2017 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the THIRD-PARTY file. - -/// Legacy Device Manager. -pub mod legacy; - -/// Device Shared Memory Region Manager. -pub mod shm; - -/// Memory Mapped I/O Manager. -#[cfg(target_os = "linux")] -pub mod kvm; -#[cfg(target_os = "linux")] -pub use self::kvm::mmio; -#[cfg(target_os = "macos")] -pub mod hvf; -#[cfg(target_os = "macos")] -pub use self::hvf::mmio; diff --git a/vendor/krun-vmm/src/device_manager/shm.rs b/vendor/krun-vmm/src/device_manager/shm.rs deleted file mode 100644 index e3c3b9bf9..000000000 --- a/vendor/krun-vmm/src/device_manager/shm.rs +++ /dev/null @@ -1,91 +0,0 @@ -use std::collections::BTreeMap; - -use arch::ArchMemoryInfo; -use vm_memory::GuestAddress; -use vmm_sys_util::align_upwards; - -#[derive(Debug)] -pub enum Error { - DuplicatedGpuRegion, - OutOfSpace, -} - -#[derive(Clone)] -pub struct ShmRegion { - pub guest_addr: GuestAddress, - pub size: usize, -} - -pub struct ShmManager { - next_guest_addr: u64, - page_size: usize, - fs_regions: BTreeMap, - gpu_region: Option, -} - -impl ShmManager { - pub fn new(info: &ArchMemoryInfo) -> ShmManager { - Self { - next_guest_addr: info.shm_start_addr, - page_size: info.page_size, - fs_regions: BTreeMap::new(), - gpu_region: None, - } - } - - pub fn regions(&self) -> Vec<(GuestAddress, usize)> { - let mut regions: Vec<(GuestAddress, usize)> = Vec::new(); - - for region in self.fs_regions.iter() { - regions.push((region.1.guest_addr, region.1.size)); - } - - if let Some(region) = &self.gpu_region { - regions.push((region.guest_addr, region.size)); - } - - regions - } - - #[cfg(not(any(feature = "tee", feature = "aws-nitro")))] - pub fn fs_region(&self, index: usize) -> Option<&ShmRegion> { - self.fs_regions.get(&index) - } - - #[cfg(feature = "gpu")] - pub fn gpu_region(&self) -> Option<&ShmRegion> { - self.gpu_region.as_ref() - } - - fn create_region(&mut self, size: usize) -> Result { - let size = align_upwards!(size, self.page_size); - - let region = ShmRegion { - guest_addr: GuestAddress(self.next_guest_addr), - size, - }; - - if let Some(addr) = self.next_guest_addr.checked_add(size as u64) { - self.next_guest_addr = addr; - Ok(region) - } else { - Err(Error::OutOfSpace) - } - } - - pub fn create_gpu_region(&mut self, size: usize) -> Result<(), Error> { - if self.gpu_region.is_some() { - Err(Error::DuplicatedGpuRegion) - } else { - self.gpu_region = Some(self.create_region(size)?); - Ok(()) - } - } - - #[cfg(not(feature = "tee"))] - pub fn create_fs_region(&mut self, index: usize, size: usize) -> Result<(), Error> { - let region = self.create_region(size)?; - self.fs_regions.insert(index, region); - Ok(()) - } -} diff --git a/vendor/krun-vmm/src/lib.rs b/vendor/krun-vmm/src/lib.rs deleted file mode 100644 index 0f0f8c258..000000000 --- a/vendor/krun-vmm/src/lib.rs +++ /dev/null @@ -1,442 +0,0 @@ -// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -// -// Portions Copyright 2017 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the THIRD-PARTY file. - -//! Virtual Machine Monitor that leverages the Linux Kernel-based Virtual Machine (KVM), -//! and other virtualization features to run a single lightweight micro-virtual -//! machine (microVM). -//#![deny(missing_docs)] - -#[macro_use] -extern crate log; - -/// Handles setup and initialization a `Vmm` object. -pub mod builder; -pub(crate) mod device_manager; -/// Resource store for configured microVM resources. -pub mod resources; -/// Signal handling utilities. -#[cfg(target_os = "linux")] -pub mod signal_handler; -/// Wrappers over structures used to configure the VMM. -pub mod vmm_config; - -#[cfg(target_os = "linux")] -mod linux; -#[cfg(target_os = "linux")] -use crate::linux::vstate; -#[cfg(target_os = "macos")] -mod macos; -mod terminal; -pub mod worker; - -#[cfg(target_os = "macos")] -use macos::vstate; - -use std::fmt::{Display, Formatter}; -use std::io; -use std::os::unix::io::AsRawFd; -use std::sync::atomic::{AtomicI32, Ordering}; -use std::sync::{Arc, Mutex}; -#[cfg(target_os = "linux")] -use std::time::Duration; - -#[cfg(target_arch = "x86_64")] -use crate::device_manager::legacy::PortIODeviceManager; -use crate::device_manager::mmio::MMIODeviceManager; -#[cfg(target_os = "linux")] -use crate::vstate::VcpuEvent; -use crate::vstate::{Vcpu, VcpuHandle, VcpuResponse, Vm}; - -use arch::{ArchMemoryInfo, InitrdConfig}; -#[cfg(target_os = "macos")] -use crossbeam_channel::Sender; -#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))] -use devices::fdt; -use devices::legacy::IrqChip; -use devices::virtio::VmmExitObserver; -use devices::{BusDevice, DeviceType}; -use kernel::cmdline::Cmdline as KernelCmdline; -use polly::event_manager::{self, EventManager, Subscriber}; -use utils::epoll::{EpollEvent, EventSet}; -use utils::eventfd::EventFd; -use vm_memory::GuestMemoryMmap; - -/// Success exit code. -pub const FC_EXIT_CODE_OK: u8 = 0; -/// Generic error exit code. -pub const FC_EXIT_CODE_GENERIC_ERROR: u8 = 1; -/// Generic exit code for an error considered not possible to occur if the program logic is sound. -pub const FC_EXIT_CODE_UNEXPECTED_ERROR: u8 = 2; -/// Firecracker was shut down after intercepting a restricted system call. -pub const FC_EXIT_CODE_BAD_SYSCALL: u8 = 148; -/// Firecracker was shut down after intercepting `SIGBUS`. -pub const FC_EXIT_CODE_SIGBUS: u8 = 149; -/// Firecracker was shut down after intercepting `SIGSEGV`. -pub const FC_EXIT_CODE_SIGSEGV: u8 = 150; -/// Bad configuration for microvm's resources, when using a single json. -pub const FC_EXIT_CODE_BAD_CONFIGURATION: u8 = 152; -/// Command line arguments parsing error. -pub const FC_EXIT_CODE_ARG_PARSING: u8 = 153; - -/// Errors associated with the VMM internal logic. These errors cannot be generated by direct user -/// input, but can result from bad configuration of the host (for example if Firecracker doesn't -/// have permissions to open the KVM fd). -#[derive(Debug)] -pub enum Error { - /// This error is thrown by the minimal boot loader implementation. - ConfigureSystem(arch::Error), - /// Legacy devices work with Event file descriptors and the creation can fail because - /// of resource exhaustion. - #[cfg(target_arch = "x86_64")] - CreateLegacyDevice(device_manager::legacy::Error), - /// Cannot read from an Event file descriptor. - EventFd(io::Error), - /// Polly error wrapper. - EventManager(event_manager::Error), - /// I8042 Error. - #[cfg(target_arch = "x86_64")] - I8042Error(devices::legacy::I8042DeviceError), - /// Cannot access kernel file. - KernelFile(io::Error), - /// Cannot open /dev/kvm. Either the host does not have KVM or Firecracker does not have - /// permission to open the file descriptor. - KvmContext(vstate::Error), - #[cfg(target_arch = "x86_64")] - /// Cannot add devices to the Legacy I/O Bus. - LegacyIOBus(device_manager::legacy::Error), - /// Cannot load command line. - LoadCommandline(kernel::cmdline::Error), - /// Cannot add a device to the MMIO Bus. - RegisterMMIODevice(device_manager::mmio::Error), - /// Write to the serial console failed. - Serial(io::Error), - #[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))] - /// Cannot generate or write FDT - SetupFDT(devices::fdt::Error), - /// Cannot create Timer file descriptor. - TimerFd(io::Error), - /// Vcpu error. - Vcpu(vstate::Error), - /// Cannot send event to vCPU. - VcpuEvent(vstate::Error), - /// Cannot create a vCPU handle. - VcpuHandle(vstate::Error), - /// vCPU resume failed. - VcpuResume, - /// Cannot spawn a new Vcpu thread. - VcpuSpawn(std::io::Error), - /// Vm error. - Vm(vstate::Error), - /// Error thrown by observer object on Vmm initialization. - VmmObserverInit(utils::errno::Error), - /// Error thrown by observer object on Vmm teardown. - VmmObserverTeardown(utils::errno::Error), -} - -impl Display for Error { - fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { - use self::Error::*; - - match self { - ConfigureSystem(e) => write!(f, "System configuration error: {e:?}"), - #[cfg(target_arch = "x86_64")] - CreateLegacyDevice(e) => write!(f, "Error creating legacy device: {e:?}"), - EventFd(e) => write!(f, "Event fd error: {e}"), - EventManager(e) => write!(f, "Event manager error: {e:?}"), - #[cfg(target_arch = "x86_64")] - I8042Error(e) => write!(f, "I8042 error: {e}"), - KernelFile(e) => write!(f, "Cannot access kernel file: {e}"), - KvmContext(e) => write!(f, "Failed to validate KVM support: {e:?}"), - #[cfg(target_arch = "x86_64")] - LegacyIOBus(e) => write!(f, "Cannot add devices to the legacy I/O Bus. {e}"), - LoadCommandline(e) => write!(f, "Cannot load command line: {e}"), - RegisterMMIODevice(e) => write!(f, "Cannot add a device to the MMIO Bus. {e}"), - Serial(e) => write!(f, "Error writing to the serial console: {e:?}"), - #[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))] - SetupFDT(e) => write!(f, "Error generating or writing FDT: {e:?}"), - TimerFd(e) => write!(f, "Error creating timer fd: {e}"), - Vcpu(e) => write!(f, "Vcpu error: {e}"), - VcpuEvent(e) => write!(f, "Cannot send event to vCPU. {e:?}"), - VcpuHandle(e) => write!(f, "Cannot create a vCPU handle. {e}"), - VcpuResume => write!(f, "vCPUs resume failed."), - VcpuSpawn(e) => write!(f, "Cannot spawn Vcpu thread: {e}"), - Vm(e) => write!(f, "Vm error: {e}"), - VmmObserverInit(e) => write!( - f, - "Error thrown by observer object on Vmm initialization: {e}" - ), - VmmObserverTeardown(e) => { - write!(f, "Error thrown by observer object on Vmm teardown: {e}") - } - } - } -} - -/// Trait for objects that need custom initialization and teardown during the Vmm lifetime. -pub trait VmmEventsObserver { - /// This function will be called during microVm boot. - fn on_vmm_boot(&mut self) -> std::result::Result<(), utils::errno::Error> { - Ok(()) - } - /// This function will be called on microVm teardown. - fn on_vmm_stop(&mut self) -> std::result::Result<(), utils::errno::Error> { - Ok(()) - } -} - -/// Shorthand result type for internal VMM commands. -pub type Result = std::result::Result; - -/// Contains the state and associated methods required for the Firecracker VMM. -pub struct Vmm { - // Guest VM core resources. - guest_memory: GuestMemoryMmap, - arch_memory_info: ArchMemoryInfo, - - kernel_cmdline: KernelCmdline, - - vcpus_handles: Vec, - exit_evt: EventFd, - vm: Vm, - exit_observers: Vec>>, - exit_code: Arc, - - // Guest VM devices. - mmio_device_manager: MMIODeviceManager, - #[cfg(target_arch = "x86_64")] - pio_device_manager: PortIODeviceManager, -} - -impl Vmm { - /// Gets the the specified bus device. - pub fn get_bus_device( - &self, - device_type: DeviceType, - device_id: &str, - ) -> Option<&Mutex> { - self.mmio_device_manager.get_device(device_type, device_id) - } - - /// Starts the microVM vcpus. - pub fn start_vcpus(&mut self, mut vcpus: Vec) -> Result<()> { - let vcpu_count = vcpus.len(); - - Vcpu::register_kick_signal_handler(); - - self.vcpus_handles.reserve(vcpu_count); - - for mut vcpu in vcpus.drain(..) { - vcpu.set_mmio_bus(self.mmio_device_manager.bus.clone()); - - self.vcpus_handles - .push(vcpu.start_threaded().map_err(Error::VcpuHandle)?); - } - - // The vcpus start off in the `Paused` state, let them run. - self.resume_vcpus()?; - - Ok(()) - } - - /// Sends a resume command to the vcpus. - #[cfg(target_os = "linux")] - pub fn resume_vcpus(&mut self) -> Result<()> { - for handle in self.vcpus_handles.iter() { - handle - .send_event(VcpuEvent::Resume) - .map_err(Error::VcpuEvent)?; - } - for handle in self.vcpus_handles.iter() { - match handle - .response_receiver() - .recv_timeout(Duration::from_millis(1000)) - { - Ok(VcpuResponse::Resumed) => (), - _ => return Err(Error::VcpuResume), - } - } - Ok(()) - } - - #[cfg(target_os = "macos")] - pub fn resume_vcpus(&mut self) -> Result<()> { - Ok(()) - } - - /// Configures the system for boot. - pub fn configure_system( - &self, - vcpus: &[Vcpu], - _intc: &IrqChip, - initrd: &Option, - _smbios_oem_strings: &Option>, - ) -> Result<()> { - #[cfg(target_arch = "x86_64")] - { - let cmdline_len = if cfg!(feature = "tee") { - arch::x86_64::layout::CMDLINE_SEV_SIZE - } else { - self.kernel_cmdline.len() + 1 - }; - - arch::x86_64::configure_system( - &self.guest_memory, - &self.arch_memory_info, - vm_memory::GuestAddress(arch::x86_64::layout::CMDLINE_START), - cmdline_len, - initrd, - vcpus.len() as u8, - ) - .map_err(Error::ConfigureSystem)?; - } - - #[cfg(target_arch = "aarch64")] - { - let vcpu_mpidr = vcpus.iter().map(|cpu| cpu.get_mpidr()).collect(); - fdt::create_fdt( - &self.guest_memory, - &self.arch_memory_info, - vcpu_mpidr, - self.kernel_cmdline.as_str(), - self.mmio_device_manager.get_device_info(), - _intc, - initrd, - ) - .map_err(Error::SetupFDT)?; - } - - #[cfg(target_arch = "aarch64")] - { - arch::aarch64::configure_system( - &self.guest_memory, - &self.arch_memory_info, - _smbios_oem_strings, - ) - .map_err(Error::ConfigureSystem)?; - } - - #[cfg(target_arch = "riscv64")] - { - fdt::create_fdt( - &self.guest_memory, - &self.arch_memory_info, - vcpus.len() as u32, - self.kernel_cmdline.as_str(), - self.mmio_device_manager.get_device_info(), - _intc, - initrd, - ) - .map_err(Error::SetupFDT)?; - - arch::riscv64::configure_system(&self.guest_memory, _smbios_oem_strings) - .map_err(Error::ConfigureSystem)?; - } - - Ok(()) - } - - /// Returns a reference to the inner `GuestMemoryMmap` object if present, or `None` otherwise. - pub fn guest_memory(&self) -> &GuestMemoryMmap { - &self.guest_memory - } - - /// Injects CTRL+ALT+DEL keystroke combo in the i8042 device. - #[cfg(target_arch = "x86_64")] - pub fn send_ctrl_alt_del(&mut self) -> Result<()> { - self.pio_device_manager - .i8042 - .lock() - .expect("i8042 lock was poisoned") - .trigger_ctrl_alt_del() - .map_err(Error::I8042Error) - } - - /// Waits for all vCPUs to exit and terminates the Firecracker process. - pub fn stop(&mut self, exit_code: i32) { - info!("Vmm is stopping."); - - for observer in &self.exit_observers { - observer - .lock() - .expect("Poisoned mutex for exit observer") - .on_vmm_exit(); - } - - // Exit from Firecracker using the provided exit code. Safe because we're terminating - // the process anyway. - unsafe { - libc::_exit(exit_code); - } - } - - /// Returns a reference to the inner KVM Vm object. - pub fn kvm_vm(&self) -> &Vm { - &self.vm - } - - #[cfg(target_os = "macos")] - pub fn add_mapping( - &self, - reply_sender: Sender, - host_addr: u64, - guest_addr: u64, - len: u64, - ) { - self.vm - .add_mapping(reply_sender, host_addr, guest_addr, len); - } - - #[cfg(target_os = "macos")] - pub fn remove_mapping(&self, reply_sender: Sender, guest_addr: u64, len: u64) { - self.vm.remove_mapping(reply_sender, guest_addr, len); - } -} - -impl Subscriber for Vmm { - /// Handle a read event (EPOLLIN). - fn process(&mut self, event: &EpollEvent, _: &mut EventManager) { - let source = event.fd(); - let event_set = event.event_set(); - - if source == self.exit_evt.as_raw_fd() && event_set == EventSet::IN { - let _ = self.exit_evt.read(); - // Query each vcpu for the exit_code. - // If the exit_code can't be found on any vcpu, it means that the exit signal - // has been issued by the i8042 controller in which case we exit with - // FC_EXIT_CODE_OK. - // - // The exit code set up by the guest takes preference over the one reported - // by either a vcpu or the i8042 controller. - let vcpu_exit_code = self - .vcpus_handles - .iter() - .find_map(|handle| match handle.response_receiver().try_recv() { - Ok(VcpuResponse::Exited(exit_code)) => Some(exit_code), - _ => None, - }) - .unwrap_or(FC_EXIT_CODE_OK); - let vmm_exit_code = self.exit_code.load(Ordering::SeqCst); - let exit_code = if vmm_exit_code != i32::MAX { - debug!("using vmm exit code: {vmm_exit_code}"); - vmm_exit_code - } else { - debug!("using vcpu exit code: {vcpu_exit_code}"); - vcpu_exit_code as i32 - }; - self.stop(exit_code); - } else { - error!("Spurious EventManager event for handler: Vmm"); - } - } - - fn interest_list(&self) -> Vec { - vec![EpollEvent::new( - EventSet::IN, - self.exit_evt.as_raw_fd() as u64, - )] - } -} diff --git a/vendor/krun-vmm/src/linux/mod.rs b/vendor/krun-vmm/src/linux/mod.rs deleted file mode 100644 index b447ad9e1..000000000 --- a/vendor/krun-vmm/src/linux/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -#[cfg(feature = "tee")] -pub mod tee; - -pub mod vstate; diff --git a/vendor/krun-vmm/src/linux/tee/amdsnp/launch/error.rs b/vendor/krun-vmm/src/linux/tee/amdsnp/launch/error.rs deleted file mode 100644 index c9c61549d..000000000 --- a/vendor/krun-vmm/src/linux/tee/amdsnp/launch/error.rs +++ /dev/null @@ -1,481 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -use std::{ - convert::From, - error, - fmt::{Debug, Display}, - io, -}; - -use std::os::raw::c_int; - -/// Error conditions returned by the SEV platform or by layers above it -/// (i.e., the Linux kernel). -/// -/// These error conditions are documented in the AMD SEV API spec, but -/// their documentation has been copied here for completeness. -#[derive(Debug, Clone, Copy, PartialEq)] -#[repr(u32)] -pub enum SevError { - /// The platform state is invalid for this command. - InvalidPlatformState = 0x0001, - - /// The guest state is invalid for this command. - InvalidGuestState = 0x0002, - - /// The platform configuration is invalid. - InvalidConfig = 0x0003, - - /// A memory buffer is too small. - InvalidLen = 0x0004, - - /// The platform is already owned. - AlreadyOwned = 0x0005, - - /// The certificate is invalid. - InvalidCertificate = 0x0006, - - /// Request is not allowed by guest policy. - PolicyFailure = 0x0007, - - /// The guest is inactive. - Inactive = 0x0008, - - /// The address provided is invalid. - InvalidAddress = 0x0009, - - /// The provided signature is invalid. - BadSignature = 0x000A, - - /// The provided measurement is invalid. - BadMeasurement = 0x000B, - - /// The ASID is already owned. - AsidOwned = 0x000C, - - /// The ASID is invalid. - InvalidAsid = 0x000D, - - /// WBINVD instruction required. - WbinvdRequired = 0x000E, - - /// `DF_FLUSH` invocation required. - DfFlushRequired = 0x000F, - - /// The guest handle is invalid. - InvalidGuest = 0x0010, - - /// The command issued is invalid. - InvalidCommand = 0x0011, - - /// The guest is active. - Active = 0x0012, - - /// A hardware condition has occurred affecting the platform. It is safe - /// to re-allocate parameter buffers. - HardwarePlatform = 0x0013, - - /// A hardware condition has occurred affecting the platform. Re-allocating - /// parameter buffers is not safe. - HardwareUnsafe = 0x0014, - - /// Feature is unsupported. - Unsupported = 0x0015, - - /// A given parameter is invalid. - InvalidParam = 0x0016, - - /// The SEV firmware has run out of a resource required to carry out the - /// command. - ResourceLimit = 0x0017, - - /// The SEV platform observed a failed integrity check. - SecureDataInvalid = 0x0018, - - /// The RMP page size is incorrect. - InvalidPageSize = 0x0019, - - /// The RMP page state is incorrect - InvalidPageState = 0x001A, - - /// The metadata entry is invalid. - InvalidMdataEntry = 0x001B, - - /// The page ownership is incorrect - InvalidPageOwner = 0x001C, - - /// The AEAD algorithm would have overflowed - AEADOFlow = 0x001D, - - /// A Mailbox mode command was sent while the SEV FW was in Ring Buffer - /// mode. Ring Buffer mode has been exited; the Mailbox mode command - /// has been ignored. Retry is recommended. - RbModeExited = 0x001F, // 0x001F - - /// The RMP must be reinitialized. - RMPInitRequired = 0x0020, // 0x0020 - - /// SVN of provided image is lower than the committed SVN. - BadSvn = 0x0021, - - /// Firmware version anti-rollback. - BadVersion = 0x0022, - - /// An invocation of SNP_SHUTDOWN is required to complete this action. - ShutdownRequired = 0x0023, - - /// Update of the firmware internal state or a guest context page has failed. - UpdateFailed = 0x0024, - - /// Installation of the committed firmware image required - RestoreRequired = 0x0025, - - /// The RMP initialization failed. - RMPInitFailed = 0x0026, - - /// The key requested is invalid, not present, or not allowed. - InvalidKey = 0x0027, - - /// Unknown status code - UnknownError = 0x0000, -} - -impl std::fmt::Display for SevError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - let code = *self as u32; - match self { - SevError::InvalidPlatformState => { - write!(f, "Status Code: 0x{:x}: Invalid platform state.", code) - } - SevError::InvalidGuestState => { - write!(f, "Status Code: 0x{:x}: Invalid guest state.", code) - } - SevError::InvalidConfig => write!( - f, - "Status Code: 0x{:x}: Platform configuration invalid.", - code - ), - SevError::InvalidLen => { - write!(f, "Status Code: 0x{:x}: Memory buffer too small.", code) - } - SevError::AlreadyOwned => { - write!(f, "Status Code: 0x{:x}: Platform is already owned.", code) - } - SevError::InvalidCertificate => { - write!(f, "Status Code: 0x{:x}: Invalid certificate.", code) - } - SevError::PolicyFailure => write!(f, "Status Code: 0x{:x}: Policy failure.", code), - SevError::Inactive => write!(f, "Status Code: 0x{:x}: Guest is inactive.", code), - SevError::InvalidAddress => { - write!(f, "Status Code: 0x{:x}: Provided address is invalid.", code) - } - SevError::BadSignature => write!( - f, - "Status Code: 0x{:x}: Provided signature is invalid.", - code - ), - SevError::BadMeasurement => write!( - f, - "Status Code: 0x{:x}: Provided measurement is invalid.", - code - ), - SevError::AsidOwned => write!(f, "Status Code: 0x{:x}: ASID is already owned.", code), - SevError::InvalidAsid => write!(f, "Status Code: 0x{:x}: ASID is invalid.", code), - SevError::WbinvdRequired => { - write!(f, "Status Code: 0x{:x}: WBINVD instruction required.", code) - } - SevError::DfFlushRequired => write!( - f, - "Status Code: 0x{:x}: DF_FLUSH invocation required.", - code - ), - SevError::InvalidGuest => { - write!(f, "Status Code: 0x{:x}: Guest handle is invalid.", code) - } - SevError::InvalidCommand => { - write!(f, "Status Code: 0x{:x}: Issued command is invalid.", code) - } - SevError::Active => write!(f, "Status Code: 0x{:x}: Guest is active.", code), - SevError::HardwarePlatform => { - write!( - f, - "Status Code: 0x{:x}: Hardware condition occured, safe to re-allocate parameter buffers.", - code - ) - } - SevError::HardwareUnsafe => { - write!( - f, - "Status Code: 0x{:x}: Hardware condition occured, unsafe to re-allocate parameter buffers.", - code - ) - } - SevError::Unsupported => { - write!(f, "Status Code: 0x{:x}: Feature is unsupported.", code) - } - SevError::InvalidParam => { - write!(f, "Status Code: 0x{:x}: Given parameter is invalid.", code) - } - SevError::ResourceLimit => { - write!( - f, - "Status Code: 0x{:x}: SEV firmware has run out of required resources to carry out command.", - code - ) - } - SevError::SecureDataInvalid => write!( - f, - "Status Code: 0x{:x}: SEV platform observed a failed integrity check.", - code - ), - SevError::InvalidPageSize => write!( - f, - "Status Code: 0x{:x}: The RMP page size is incorrect.", - code - ), - SevError::InvalidPageState => write!( - f, - "Status Code: 0x{:x}: The RMP page state is incorrect.", - code - ), - SevError::InvalidMdataEntry => write!( - f, - "Status Code: 0x{:x}: The metadata entry is invalid.", - code - ), - SevError::InvalidPageOwner => write!( - f, - "Status Code: 0x{:x}: The page ownership is incorrect.", - code - ), - SevError::AEADOFlow => write!( - f, - "Status Code: 0x{:x}: The AEAD algorithm would have overflowed.", - code - ), - SevError::RbModeExited => write!( - f, - "Status Code: 0x{:x}: A Mailbox mode command was sent while the SEV FW was in Ring Buffer \ - mode. Ring Buffer mode has been exited; the Mailbox mode command has \ - been ignored. Retry is recommended.", - code - ), - SevError::RMPInitRequired => write!( - f, - "Status Code: 0x{:x}: The RMP must be reinitialized.", - code - ), - SevError::BadSvn => write!( - f, - "Status Code: 0x{:x}: SVN of provided image is lower than the committed SVN.", - code - ), - SevError::BadVersion => write!( - f, - "Status Code: 0x{:x}: Firmware version anti-rollback.", - code - ), - SevError::ShutdownRequired => write!( - f, - "Status Code: 0x{:x}: An invocation of SNP_SHUTDOWN is required to complete this action.", - code - ), - SevError::UpdateFailed => write!( - f, - "Status Code: 0x{:x}: Update of the firmware internal state or a guest context page has failed.", - code - ), - SevError::RestoreRequired => write!( - f, - "Status Code: 0x{:x}: Installation of the committed firmware image required.", - code - ), - SevError::RMPInitFailed => write!( - f, - "Status Code: 0x{:x}: The RMP initialization failed.", - code - ), - SevError::InvalidKey => write!( - f, - "Status Code: 0x{:x}: The key requested is invalid, not present, or not allowed.", - code - ), - SevError::UnknownError => write!(f, "Unknown SEV Error"), - } - } -} - -impl From for SevError { - fn from(value: u64) -> Self { - Self::from(value as u32) - } -} - -impl From for SevError { - #[inline] - fn from(error: u32) -> SevError { - match error { - 0x01 => SevError::InvalidPlatformState, - 0x02 => SevError::InvalidGuestState, - 0x03 => SevError::InvalidConfig, - 0x04 => SevError::InvalidLen, - 0x05 => SevError::AlreadyOwned, - 0x06 => SevError::InvalidCertificate, - 0x07 => SevError::PolicyFailure, - 0x08 => SevError::Inactive, - 0x09 => SevError::InvalidAddress, - 0x0A => SevError::BadSignature, - 0x0B => SevError::BadMeasurement, - 0x0C => SevError::AsidOwned, - 0x0D => SevError::InvalidAsid, - 0x0E => SevError::WbinvdRequired, - 0x0F => SevError::DfFlushRequired, - 0x10 => SevError::InvalidGuest, - 0x11 => SevError::InvalidCommand, - 0x12 => SevError::Active, - 0x13 => SevError::HardwarePlatform, - 0x14 => SevError::HardwareUnsafe, - 0x15 => SevError::Unsupported, - 0x16 => SevError::InvalidParam, - 0x17 => SevError::ResourceLimit, - 0x18 => SevError::SecureDataInvalid, - 0x19 => SevError::InvalidPageSize, - 0x1A => SevError::InvalidPageState, - 0x1B => SevError::InvalidMdataEntry, - 0x1C => SevError::InvalidPageOwner, - 0x1D => SevError::AEADOFlow, - 0x1F => SevError::RbModeExited, - 0x20 => SevError::RMPInitRequired, - 0x21 => SevError::BadSvn, - 0x22 => SevError::BadVersion, - 0x23 => SevError::ShutdownRequired, - 0x24 => SevError::UpdateFailed, - 0x25 => SevError::RestoreRequired, - 0x26 => SevError::RMPInitFailed, - 0x27 => SevError::InvalidKey, - _ => SevError::UnknownError, - } - } -} - -impl From for c_int { - fn from(err: SevError) -> Self { - match err { - SevError::InvalidPlatformState => 0x01, - SevError::InvalidGuestState => 0x02, - SevError::InvalidConfig => 0x03, - SevError::InvalidLen => 0x04, - SevError::AlreadyOwned => 0x05, - SevError::InvalidCertificate => 0x06, - SevError::PolicyFailure => 0x07, - SevError::Inactive => 0x08, - SevError::InvalidAddress => 0x09, - SevError::BadSignature => 0x0A, - SevError::BadMeasurement => 0x0B, - SevError::AsidOwned => 0x0C, - SevError::InvalidAsid => 0x0D, - SevError::WbinvdRequired => 0x0E, - SevError::DfFlushRequired => 0x0F, - SevError::InvalidGuest => 0x10, - SevError::InvalidCommand => 0x11, - SevError::Active => 0x12, - SevError::HardwarePlatform => 0x13, - SevError::HardwareUnsafe => 0x14, - SevError::Unsupported => 0x15, - SevError::InvalidParam => 0x16, - SevError::ResourceLimit => 0x17, - SevError::SecureDataInvalid => 0x18, - SevError::InvalidPageSize => 0x19, - SevError::InvalidPageState => 0x1A, - SevError::InvalidMdataEntry => 0x1B, - SevError::InvalidPageOwner => 0x1C, - SevError::AEADOFlow => 0x1D, - SevError::RbModeExited => 0x1F, - SevError::RMPInitRequired => 0x20, - SevError::BadSvn => 0x21, - SevError::BadVersion => 0x22, - SevError::ShutdownRequired => 0x23, - SevError::UpdateFailed => 0x24, - SevError::RestoreRequired => 0x25, - SevError::RMPInitFailed => 0x26, - SevError::InvalidKey => 0x27, - SevError::UnknownError => -1, - } - } -} - -impl std::error::Error for SevError {} - -/// There are a number of error conditions that can occur between this -/// layer all the way down to the SEV platform. Most of these cases have -/// been enumerated; however, there is a possibility that some error -/// conditions are not encapsulated here. -#[derive(Debug)] -pub enum FirmwareError { - /// The error condition is known. - KnownSev(SevError), - - /// The error condition is unknown. - UnknownSev(u32), - - /// IO Error - Io(std::io::Error), -} - -impl error::Error for FirmwareError {} - -impl Display for FirmwareError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let err_description = match self { - FirmwareError::KnownSev(error) => format!("Known SEV FW Error: {error}"), - FirmwareError::UnknownSev(code) => { - format!("Unknown SEV FW Error Encountered: {code}") - } - FirmwareError::Io(error) => format!("IO Error Encountered: {error}"), - }; - - write!(f, "{err_description}") - } -} - -impl std::convert::From for FirmwareError { - fn from(sev_error: SevError) -> Self { - match sev_error { - SevError::UnknownError => FirmwareError::UnknownSev(sev_error as u32), - _ => FirmwareError::KnownSev(sev_error), - } - } -} - -impl From for FirmwareError { - #[inline] - fn from(error: io::Error) -> FirmwareError { - FirmwareError::Io(error) - } -} - -impl From for FirmwareError { - fn from(value: u64) -> Self { - Self::from(value as u32) - } -} - -impl From for FirmwareError { - #[inline] - fn from(error: u32) -> FirmwareError { - match error { - 0x00 => FirmwareError::Io(io::Error::last_os_error()), - 0x01..0x027 => FirmwareError::KnownSev(error.into()), - _ => FirmwareError::UnknownSev(error), - } - } -} - -impl From for c_int { - fn from(err: FirmwareError) -> Self { - match err { - FirmwareError::UnknownSev(_) | FirmwareError::Io(_) => -0x01, - FirmwareError::KnownSev(e) => e.into(), - } - } -} diff --git a/vendor/krun-vmm/src/linux/tee/amdsnp/launch/firmware.rs b/vendor/krun-vmm/src/linux/tee/amdsnp/launch/firmware.rs deleted file mode 100644 index a5e1b86ae..000000000 --- a/vendor/krun-vmm/src/linux/tee/amdsnp/launch/firmware.rs +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! Operations for managing the SEV platform. - -use std::{ - fs::{File, OpenOptions}, - os::fd::{AsRawFd, RawFd}, -}; - -/// A handle to the SEV platform. -#[cfg(target_os = "linux")] -pub struct Firmware(File); - -#[cfg(target_os = "linux")] -impl Firmware { - /// Create a handle to the SEV platform. - pub fn open() -> std::io::Result { - Ok(Firmware( - OpenOptions::new().read(true).write(true).open("/dev/sev")?, - )) - } -} - -#[cfg(target_os = "linux")] -impl AsRawFd for Firmware { - fn as_raw_fd(&self) -> RawFd { - self.0.as_raw_fd() - } -} diff --git a/vendor/krun-vmm/src/linux/tee/amdsnp/launch/linux/ioctl.rs b/vendor/krun-vmm/src/linux/tee/amdsnp/launch/linux/ioctl.rs deleted file mode 100644 index 831ff850b..000000000 --- a/vendor/krun-vmm/src/linux/tee/amdsnp/launch/linux/ioctl.rs +++ /dev/null @@ -1,150 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! A collection of type-safe ioctl implementations for the AMD Secure Encrypted Virtualization -//! (SEV) platform. These ioctls are exported by the Linux kernel. - -use crate::impl_const_id; - -use crate::linux::tee::amdsnp::launch::{error::FirmwareError, linux::snp}; - -use std::{ - marker::PhantomData, - os::{raw::c_ulong, unix::io::AsRawFd}, -}; - -use iocuddle::*; - -// These enum ordinal values are defined in the Linux kernel -// source code: arch/x86/include/uapi/asm/kvm.h -impl_const_id! { - /// The ioctl sub number - pub Id => u32; - - snp::Init2 = 22, - - snp::LaunchStart = 100, - snp::LaunchUpdate<'_> = 101, - snp::LaunchFinish<'_> = 102, -} - -const KVM: Group = Group::new(0xAE); -const ENC_OP: Ioctl = unsafe { KVM.write_read(0xBA) }; - -pub const KVM_MEMORY_ATTRIBUTE_PRIVATE: u64 = 1 << 3; - -// Note: the iocuddle::Ioctl::lie() constructor has been used here because -// KVM_MEMORY_ENCRYPT_OP ioctl was defined like this: -// -// _IOWR(KVMIO, 0xba, unsigned long) -// -// Instead of something like this: -// -// _IOWR(KVMIO, 0xba, struct kvm_sev_cmd) -// -// which would require extra work to wrap around the design decision for -// that ioctl. - -/// Use the KVM_SEV_INIT2 ioctl to initialize the SEV platform context. -pub const INIT2: Ioctl> = unsafe { ENC_OP.lie() }; - -/// Corresponds to the `KVM_MEMORY_ENCRYPT_REG_REGION` ioctl -pub const ENC_REG_REGION: Ioctl = - unsafe { KVM.read::(0xBB).lie() }; - -/// Corresponds to the `KVM_SET_MEMORY_ATTRIBUTES` ioctl -pub const SET_MEMORY_ATTRIBUTES: Ioctl = - unsafe { KVM.write::(0xd2) }; - -/// Initialize the flow to launch a guest. -pub const SNP_LAUNCH_START: Ioctl> = unsafe { ENC_OP.lie() }; - -/// Insert pages into the guest physical address space. -pub const SNP_LAUNCH_UPDATE: Ioctl> = - unsafe { ENC_OP.lie() }; - -/// Complete the guest launch flow. -pub const SNP_LAUNCH_FINISH: Ioctl> = - unsafe { ENC_OP.lie() }; - -/// Corresponds to the kernel struct `kvm_enc_region` -#[repr(C)] -#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] -pub struct KvmEncRegion<'a> { - addr: u64, - size: u64, - phantom: PhantomData<&'a [u8]>, -} - -impl<'a> KvmEncRegion<'a> { - /// Create a new `KvmEncRegion` referencing some memory assigned to the virtual machine. - pub fn new(data: &'a [u8]) -> Self { - Self { - addr: data.as_ptr() as _, - size: data.len() as _, - phantom: PhantomData, - } - } - - /// Register the encrypted memory region to a virtual machine - pub fn register(&mut self, vm_fd: &mut impl AsRawFd) -> std::io::Result { - ENC_REG_REGION.ioctl(vm_fd, self) - } -} - -/// Corresponds to the kernel struct `kvm_memory_attributes` -#[repr(C)] -#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] -pub struct KvmSetMemoryAttributes { - addr: u64, - size: u64, - attributes: u64, - flags: u64, -} - -impl KvmSetMemoryAttributes { - /// Create a new `KvmEncRegion` referencing some memory assigned to the virtual machine. - pub fn new(data: u64, len: u64, attributes: u64) -> Self { - Self { - addr: data, - size: len, - attributes, - flags: 0, - } - } - - /// Register the encrypted memory region to a virtual machine - pub fn set_attributes( - &mut self, - vm_fd: &mut impl AsRawFd, - ) -> std::io::Result { - SET_MEMORY_ATTRIBUTES.ioctl(vm_fd, self) - } -} - -/// A generic SEV command -#[repr(C)] -pub struct Command<'a, T: Id> { - code: u32, - data: u64, - error: u32, - sev_fd: u32, - _phantom: PhantomData<&'a T>, -} - -impl<'a, T: Id> Command<'a, T> { - /// create the command from a subcommand reference - pub fn from(sev: &'a impl AsRawFd, subcmd: &'a T) -> Self { - Self { - code: T::ID, - data: subcmd as *const T as _, - error: 0, - sev_fd: sev.as_raw_fd() as _, - _phantom: PhantomData, - } - } - - /// encapsulate a SEV errors in command as a Firmware error. - pub fn encapsulate(&self) -> FirmwareError { - FirmwareError::from(self.error) - } -} diff --git a/vendor/krun-vmm/src/linux/tee/amdsnp/launch/linux/mod.rs b/vendor/krun-vmm/src/linux/tee/amdsnp/launch/linux/mod.rs deleted file mode 100644 index 580014265..000000000 --- a/vendor/krun-vmm/src/linux/tee/amdsnp/launch/linux/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! Operations and types for launching on Linux -pub(crate) mod ioctl; - -pub(crate) mod snp; diff --git a/vendor/krun-vmm/src/linux/tee/amdsnp/launch/linux/snp.rs b/vendor/krun-vmm/src/linux/tee/amdsnp/launch/linux/snp.rs deleted file mode 100644 index 844f3ed52..000000000 --- a/vendor/krun-vmm/src/linux/tee/amdsnp/launch/linux/snp.rs +++ /dev/null @@ -1,162 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! Types for interacting with the KVM SEV-SNP guest management API. - -use crate::linux::tee::amdsnp::*; - -use std::marker::PhantomData; - -/// Structure passed into KVM_SEV_INIT2 command. -#[derive(Default)] -#[repr(C, packed)] -pub struct Init2 { - /// Initial value of features field in VMSA. (Must be 0 for SEV) - vmsa_features: u64, - - /// Always set to 0 - flags: u32, - - /// Maximum guest GHCB version allowed. (Currently 0 for SEV and 1 for SEV-ES and SEV-SNP) - ghcb_version: u16, - - pad1: u16, - - pad2: [u32; 8], -} - -impl Init2 { - /// Default INIT2 values for SEV-SNP - pub fn init_default_snp() -> Self { - Self { - vmsa_features: 0, - flags: 0, - ghcb_version: 2, - pad1: Default::default(), - pad2: Default::default(), - } - } -} - -#[repr(C)] -pub struct LaunchStart { - /// Guest policy. See Table 7 of the AMD SEV-SNP Firmware - /// specification for a description of the guest policy structure. - policy: u64, - - /// Hypervisor provided value to indicate guest OS visible workarounds. - /// The format is hypervisor defined. - gosvw: [u8; 16], - - flags: u16, - - pad0: [u8; 6], - - pad1: [u64; 4], -} - -impl From for LaunchStart { - fn from(start: Start) -> Self { - Self { - policy: start.policy.into(), - gosvw: start.gosvw, - flags: 0, - pad0: [0u8; 6], - pad1: [0u64; 4], - } - } -} - -/// Insert pages into the guest physical address space. -#[repr(C)] -pub struct LaunchUpdate<'a> { - /// guest start frame number. - pub start_gfn: u64, - - /// Userspace address of the page needed to be encrypted. - pub uaddr: u64, - - /// Length of the page needed to be encrypted: - /// (end encryption uaddr = uaddr + len). - pub len: u64, - - /// Encoded page type. See Table 58 if the SNP Firmware specification. - pub page_type: u8, - - pad0: u8, - - flags: u16, - - pad1: u32, - - pad2: [u64; 4], - - _phantom: PhantomData<&'a [u8]>, -} - -impl From> for LaunchUpdate<'_> { - fn from(update: Update) -> Self { - Self { - start_gfn: update.start_gfn, - uaddr: update.uaddr.as_ptr() as _, - len: update.uaddr.len() as _, - page_type: update.page_type as _, - pad0: 0, - flags: 0, - pad1: 0, - pad2: [0u64; 4], - _phantom: PhantomData, - } - } -} - -pub const KVM_SEV_SNP_FINISH_DATA_SIZE: usize = 32; - -/// Complete the guest launch flow. -#[repr(C)] -pub struct LaunchFinish<'a> { - /// Userspace address of the ID block. Ignored if ID_BLOCK_EN is 0. - id_block_uaddr: u64, - - /// Userspace address of the authentication information of the ID block. Ignored if ID_BLOCK_EN is 0. - id_auth_uaddr: u64, - - /// Indicates that the ID block is present. - id_block_en: u8, - - /// Indicates that the author key is present in the ID authentication information structure. - /// Ignored if ID_BLOCK_EN is 0. - auth_key_en: u8, - - /// Opaque host-supplied data to describe the guest. The firmware does not interpret this value. - host_data: [u8; KVM_SEV_SNP_FINISH_DATA_SIZE], - - pad: [u8; 6], - - _phantom: PhantomData<&'a [u8]>, -} - -impl From> for LaunchFinish<'_> { - fn from(finish: Finish) -> Self { - let id_block = if let Some(addr) = finish.id_block { - addr.as_ptr() as u64 - } else { - 0 - }; - - let id_auth = if let Some(addr) = finish.id_auth { - addr.as_ptr() as u64 - } else { - 0 - }; - - Self { - id_block_uaddr: id_block, - id_auth_uaddr: id_auth, - id_block_en: u8::from(finish.id_block.is_some()), - auth_key_en: u8::from(finish.id_auth.is_some()), - host_data: finish.host_data, - pad: [0u8; 6], - _phantom: PhantomData, - } - } -} diff --git a/vendor/krun-vmm/src/linux/tee/amdsnp/launch/mod.rs b/vendor/krun-vmm/src/linux/tee/amdsnp/launch/mod.rs deleted file mode 100644 index 25621a8fd..000000000 --- a/vendor/krun-vmm/src/linux/tee/amdsnp/launch/mod.rs +++ /dev/null @@ -1,381 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! Everything one needs to launch an AMD SEV encrypted virtual machine. -//! -//! This module contains types for establishing a secure channel with the -//! AMD Secure Processor for purposes of attestation as well as abstractions -//! for navigating the AMD SEV launch process for a virtual machine. - -#[cfg(target_os = "linux")] -mod linux; - -pub mod error; -pub mod firmware; -pub(crate) mod util; - -use super::error::FirmwareError; - -#[cfg(target_os = "linux")] -use linux::{ioctl::*, snp::*}; - -use std::{fmt::Display, marker::PhantomData, os::unix::io::AsRawFd, result::Result}; - -use bitfield::bitfield; -use bitflags::bitflags; - -/// Launcher type-state that indicates a brand new launch. -pub struct New; - -/// Launcher type-state that indicates a SNP in-progress. -pub struct Started; - -/// Facilitates the correct execution of the SEV launch process. -pub struct Launcher { - vm_fd: U, - sev: V, - state: PhantomData, -} - -impl AsRef for Launcher { - /// Give access to the vm fd to create vCPUs or such. - fn as_ref(&self) -> &U { - &self.vm_fd - } -} - -impl AsMut for Launcher { - /// Give access to the vm fd to create vCPUs or such. - fn as_mut(&mut self) -> &mut U { - &mut self.vm_fd - } -} - -impl Launcher { - /// Begin the SEV-SNP launch process by creating a Launcher and issuing the - /// KVM_SNP_INIT ioctl. - pub fn new(vm_fd: U, sev: V) -> Result { - let mut launcher = Launcher { - vm_fd, - sev, - state: PhantomData, - }; - - let init = Init2::init_default_snp(); - - let mut cmd = Command::from(&launcher.sev, &init); - - INIT2 - .ioctl(&mut launcher.vm_fd, &mut cmd) - .map_err(|_| cmd.encapsulate())?; - - Ok(launcher) - } - - /// Initialize the flow to launch a guest. - pub fn start(mut self, start: Start) -> Result, FirmwareError> { - let launch_start = LaunchStart::from(start); - let mut cmd = Command::from(&self.sev, &launch_start); - - SNP_LAUNCH_START - .ioctl(&mut self.vm_fd, &mut cmd) - .map_err(|_| cmd.encapsulate())?; - - let launcher = Launcher { - vm_fd: self.vm_fd, - sev: self.sev, - state: PhantomData, - }; - - Ok(launcher) - } -} - -impl Launcher { - /// Encrypt guest SNP data. - pub fn update_data( - &mut self, - mut update: Update, - gpa: u64, - gpa_len: u64, - ) -> Result<(), FirmwareError> { - loop { - let launch_update_data = LaunchUpdate::from(update); - let mut cmd = Command::from(&self.sev, &launch_update_data); - - // Register the encryption region - KvmEncRegion::new(update.uaddr).register(&mut self.vm_fd)?; - - // Set memory attributes to private - KvmSetMemoryAttributes::new(gpa, gpa_len, KVM_MEMORY_ATTRIBUTE_PRIVATE) - .set_attributes(&mut self.vm_fd)?; - - // Perform the SNP_LAUNCH_UPDATE ioctl call - match SNP_LAUNCH_UPDATE.ioctl(&mut self.vm_fd, &mut cmd) { - Ok(_) => { - // Check if the entire range has been processed - if launch_update_data.len == 0 { - break; - } - - // Update the `update` object with the remaining range - update.start_gfn = launch_update_data.start_gfn; - update.uaddr = unsafe { - std::slice::from_raw_parts( - launch_update_data.uaddr as *const u8, - launch_update_data.len as usize, - ) - }; - } - Err(e) if e.raw_os_error() == Some(libc::EAGAIN) => { - // Retry the operation if `-EAGAIN` is returned - continue; - } - Err(_) => { - // Handle other errors - return Err(cmd.encapsulate()); - } - } - } - - Ok(()) - } - - /// Complete the SNP launch process. - pub fn finish(mut self, finish: Finish) -> Result<(U, V), FirmwareError> { - let launch_finish = LaunchFinish::from(finish); - let mut cmd = Command::from(&self.sev, &launch_finish); - - SNP_LAUNCH_FINISH - .ioctl(&mut self.vm_fd, &mut cmd) - .map_err(|_| cmd.encapsulate())?; - - Ok((self.vm_fd, self.sev)) - } -} - -/// Encapsulates the various data needed to begin the launch process. -#[derive(Default, Clone, Debug, PartialEq, Eq)] -pub struct Start { - /// Describes a policy that the AMD Secure Processor will enforce. - pub(crate) policy: GuestPolicy, - - /// Hypervisor provided value to indicate guest OS visible workarounds.The format is hypervisor defined. - pub(crate) gosvw: [u8; 16], - - /// Indicates that this launch flow is launching an IMI for the purpose of guest-assisted migration. - pub(crate) flags: u16, -} - -impl Start { - /// Encapsulate all data needed for the SNP_LAUNCH_START ioctl. - pub fn new(policy: GuestPolicy, gosvw: [u8; 16]) -> Self { - Self { - policy, - gosvw, - flags: 0, - } - } -} - -/// Encoded page types for a launch update. See Table 58 of the SNP Firmware -/// specification for further details. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -#[repr(C)] -#[non_exhaustive] -pub enum PageType { - /// A normal data page. - Normal = 0x1, - - /// A VMSA page. - Vmsa = 0x2, - - /// A page full of zeroes. - Zero = 0x3, - - /// A page that is encrypted but not measured - Unmeasured = 0x4, - - /// A page for the firmware to store secrets for the guest. - Secrets = 0x5, - - /// A page for the hypervisor to provide CPUID function values. - Cpuid = 0x6, -} - -/// Encapsulates the various data needed to begin the update process. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct Update<'a> { - /// guest start frame number. - pub(crate) start_gfn: u64, - - /// The userspace of address of the encrypted region. - pub(crate) uaddr: &'a [u8], - - /// Encoded page type. - pub(crate) page_type: PageType, -} - -impl<'a> Update<'a> { - /// Encapsulate all data needed for the SNP_LAUNCH_UPDATE ioctl. - pub fn new(start_gfn: u64, uaddr: &'a [u8], page_type: PageType) -> Self { - Self { - start_gfn, - uaddr, - page_type, - } - } -} - -bitflags! { - #[derive(Default, Copy, Clone, Debug, PartialEq, Eq)] - /// VMPL permission masks. - pub struct VmplPerms: u8 { - /// Page is readable by the VMPL. - const READ = 1; - - /// Page is writeable by the VMPL. - const WRITE = 1 << 1; - - /// Page is executable by the VMPL in CPL3. - const EXECUTE_USER = 1 << 2; - - /// Page is executable by the VMPL in CPL2, CPL1, and CPL0. - const EXECUTE_SUPERVISOR = 1 << 3; - } -} - -/// Encapsulates the data needed to complete a guest launch. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct Finish<'a, 'b> { - /// The userspace address of the encrypted region. - pub(crate) id_block: Option<&'a [u8]>, - - /// The userspace address of the authentication information of the ID block. - pub(crate) id_auth: Option<&'b [u8]>, - - /// Opaque host-supplied data to describe the guest. The firmware does not interpret this - /// value. - pub(crate) host_data: [u8; KVM_SEV_SNP_FINISH_DATA_SIZE], -} - -impl<'a, 'b> Finish<'a, 'b> { - /// Encapsulate all data needed for the SNP_LAUNCH_FINISH ioctl. - pub fn new( - id_block: Option<&'a [u8]>, - id_auth: Option<&'b [u8]>, - host_data: [u8; KVM_SEV_SNP_FINISH_DATA_SIZE], - ) -> Self { - Self { - id_block, - id_auth, - host_data, - } - } -} - -bitfield! { - /// The firmware associates each guest with a guest policy that the guest owner provides. The - /// firmware restricts what actions the hypervisor can take on this guest according to the guest policy. - /// The policy also indicates the minimum firmware version to for the guest. - /// - /// The guest owner provides the guest policy to the firmware during launch. The firmware then binds - /// the policy to the guest. The policy cannot be changed throughout the lifetime of the guest. The - /// policy is also migrated with the guest and enforced by the destination platform firmware. - /// - /// | Bit(s) | Name | Description > - /// |--------|-------------------|--------------------------------------------------------------------------------------------------------------------> - /// | 7:0 | ABI_MINOR | The minimum ABI minor version required for this guest to run. > - /// | 15:8 | ABI_MAJOR | The minimum ABI major version required for this guest to run. > - /// | 16 | SMT | 0: Host SMT usage is disallowed.
    1: Host SMT usage is allowed. > - /// | 17 | - | Reserved. Must be one. > - /// | 18 | MIGRATE_MA | 0: Association with a migration agent is disallowed.
    1: Association with a migration agent is allowed > - /// | 19 | DEBUG | 0: Debugging is disallowed.
    1: Debugging is allowed. > - /// | 20 | SINGLE_SOCKET | 0: Guest can be activated on multiple sockets.
    1: Guest can only be activated on one socket. > - /// | 21 | CXL_ALLOW | 0: CXL cannot be populated with devices or memory.
    1: CXL can be populated with devices or memory. > - /// | 22 | MEM_AES_256_XTS | 0: Allow either AES 128 XEX or AES 256 XTS for memory encryption.
    1: Require AES 256 XTS for memory encryption. > - /// | 23 | RAPL_DIS | 0: Allow Running Average Power Limit (RAPL).
    1: RAPL must be disabled. > - /// | 24 | CIPHERTEXT_HIDING | 0: Ciphertext hiding may be enabled or disabled.
    1: Ciphertext hiding must be enabled. > - /// | 25 | PAGE_SWAP_DISABLE | 0: Disable Guest access to SNP_PAGE_MOVE, SNP_SWAP_OUT and SNP_SWAP_IN commands. > - /// | 63:25 | - | Reserved. MBZ. > - /// - #[repr(C)] - #[derive(Default, Clone, Copy, Eq, PartialEq, PartialOrd, Ord)] - pub struct GuestPolicy(u64); - impl Debug; - /// ABI_MINOR field: Indicates the minor API version. - pub abi_minor, set_abi_minor: 7, 0; - /// ABI_MAJOR field: Indicates the minor API version. - pub abi_major, set_abi_major: 15, 8; - /// SMT_ALLOWED field: Indicates the if SMT should be permitted. - pub smt_allowed, set_smt_allowed: 16; - /// MIGRATE_MA_ALLOWED field: Indicates the if migration is permitted with - /// the migration agent. - pub migrate_ma_allowed, set_migrate_ma_allowed: 18; - /// DEBUG_ALLOWED field: Indicates the if debugging should is permitted. - pub debug_allowed, set_debug_allowed: 19; - /// SINGLE_SOCKET_REQUIRED field: Indicates the if a single socket is required. - pub single_socket_required, set_single_socket_required: 20; - /// CXL_ALLOW field: (1) can populate CXL devices/memory, (0) cannot populate CXL devices/memory - pub cxl_allowed, set_cxl_allowed: 21; - /// MEM_AES_256_XTS field: (1) require AES 256 XTS encryption, (0) allows either AES 128 XEX or AES 256 XTS encryption - pub mem_aes_256_xts, set_mem_aes_256_xts: 22; - /// RAPL_DIS field: (1) RAPL must be disabled, (0) allow RAPL - pub rapl_dis, set_rapl_dis: 23; - /// CIPHERTEXT_HIDING field: (1) ciphertext hiding must be enabled, (0) ciphertext hiding may be enabled/disabled - pub ciphertext_hiding, set_ciphertext_hiding: 24; - /// Guest policy to disable Guest access to SNP_PAGE_MOVE, SNP_SWAP_OUT, and SNP_SWAP_IN commands. If this policy - /// option is selected to disable these Page Move commands, then these commands will return POLICY_FAILURE. - /// 0: Do not disable Guest support for the commands. - /// 1: Disable Guest support for the commands. - pub page_swap_disabled, set_page_swap_disabled: 25; -} - -impl Display for GuestPolicy { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - r#"Guest Policy (0x{:x}): - ABI Major: {} - ABI Minor: {} - SMT Allowed: {} - Migrate MA: {} - Debug Allowed: {} - Single Socket: {} - CXL Allowed: {} - AEX 256 XTS: {} - RAPL Allowed: {} - Ciphertext hiding: {} - Page Swap Disable: {}"#, - self.0, - self.abi_major(), - self.abi_minor(), - self.smt_allowed(), - self.migrate_ma_allowed(), - self.debug_allowed(), - self.single_socket_required(), - self.cxl_allowed(), - self.mem_aes_256_xts(), - self.rapl_dis(), - self.ciphertext_hiding(), - self.page_swap_disabled() - ) - } -} - -impl From for u64 { - fn from(value: GuestPolicy) -> Self { - // Bit 17 of the guest policy is reserved and must always be set to 1. - let reserved: u64 = 1 << 17; - - value.0 | reserved - } -} - -impl From for GuestPolicy { - fn from(value: u64) -> Self { - // Bit 17 of the guest policy is reserved and must always be set to 1. - let reserved: u64 = 1 << 17; - - GuestPolicy(value | reserved) - } -} diff --git a/vendor/krun-vmm/src/linux/tee/amdsnp/launch/util/impl_const_id.rs b/vendor/krun-vmm/src/linux/tee/amdsnp/launch/util/impl_const_id.rs deleted file mode 100644 index ad8ec8867..000000000 --- a/vendor/krun-vmm/src/linux/tee/amdsnp/launch/util/impl_const_id.rs +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! A simple const generics substitute. - -#[doc(hidden)] -#[macro_export] -macro_rules! impl_const_id { - ( - $(#[$outer:meta])* - $visibility:vis $trait:ident => $id_ty:ty; - $( - $iocty:ty = $val:expr - ),* $(,)* - ) => { - $(#[$outer])* - $visibility trait $trait { - $(#[$outer])* - const ID: $id_ty; - } - - $( - impl $trait for $iocty { - const ID: $id_ty = $val; - } - )* - }; -} - -#[cfg(test)] -mod tests { - struct A; - struct B; - struct C; - - impl_const_id! { - Id => usize; - A = 1, - B = 2, - C = 3, - } - - #[test] - fn test_const_id_macro() { - assert_eq!(A::ID, 1); - assert_eq!(B::ID, 2); - assert_eq!(C::ID, 3); - } -} diff --git a/vendor/krun-vmm/src/linux/tee/amdsnp/launch/util/mod.rs b/vendor/krun-vmm/src/linux/tee/amdsnp/launch/util/mod.rs deleted file mode 100644 index 09ca59ed2..000000000 --- a/vendor/krun-vmm/src/linux/tee/amdsnp/launch/util/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -pub mod impl_const_id; diff --git a/vendor/krun-vmm/src/linux/tee/amdsnp/mod.rs b/vendor/krun-vmm/src/linux/tee/amdsnp/mod.rs deleted file mode 100644 index 1474a0be8..000000000 --- a/vendor/krun-vmm/src/linux/tee/amdsnp/mod.rs +++ /dev/null @@ -1,399 +0,0 @@ -pub(crate) mod launch; - -use std::{ - os::unix::io::{AsRawFd, RawFd}, - slice, -}; - -use crate::vstate::MeasuredRegion; -use arch::x86_64::layout::*; - -use launch::{error::FirmwareError, firmware::Firmware, *}; - -use kvm_bindings::{kvm_enc_region, CpuId, KVM_CPUID_FLAG_SIGNIFCANT_INDEX}; -use kvm_ioctls::VmFd; -use vm_memory::{ - Bytes, GuestAddress, GuestMemory, GuestMemoryMmap, GuestMemoryRegion, GuestRegionMmap, -}; - -#[derive(Debug)] -pub enum Error { - CpuIdWrite, - CpuIdFull, - CreateLauncher(FirmwareError), - GuestMemoryWrite(vm_memory::GuestMemoryError), - GuestMemoryRead(vm_memory::GuestMemoryError), - LaunchStart(FirmwareError), - LaunchUpdate(FirmwareError), - LaunchFinish(FirmwareError), - MemoryEncryptRegion, - OpenFirmware(std::io::Error), -} - -const COUNT_MAX: usize = 80; - -fn as_u32_le(array: &[u8; 4]) -> u32 { - (array[0] as u32) - + ((array[1] as u32) << 8) - + ((array[2] as u32) << 16) - + ((array[3] as u32) << 24) -} - -/// An entry in the SNP CPUID Page -#[repr(C)] -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] -pub struct CpuidFunctionEntry { - /// function - pub eax_in: u32, - /// index - pub ecx_in: u32, - /// register state when cpuid is called - pub xcr0_in: u64, - /// register state when cpuid is called - pub xss_in: u64, - /// cpuid out - pub eax: u32, - /// cpuid out - pub ebx: u32, - /// cpuid out - pub ecx: u32, - /// cpuid out - pub edx: u32, - reserved: u64, -} - -#[repr(C)] -#[derive(Copy, Clone, Debug)] -struct CpuidPageEntry { - count: u32, - reserved_1: u32, - reserved_2: u64, - functions: [CpuidFunctionEntry; COUNT_MAX], -} - -pub struct AmdSnp { - fw: Firmware, -} - -impl AmdSnp { - pub fn new() -> Result { - let fw = Firmware::open().map_err(Error::OpenFirmware)?; - - Ok(AmdSnp { fw }) - } - - pub fn vm_prepare( - &self, - vm_fd: &VmFd, - guest_mem: &GuestMemoryMmap, - ) -> Result, Error> { - let vm_rfd = vm_fd.as_raw_fd(); - let fw_rfd = self.fw.as_raw_fd(); - - let launcher = Launcher::new(vm_rfd, fw_rfd).map_err(Error::CreateLauncher)?; - - for region in guest_mem.iter() { - // It's safe to unwrap because the guest address is valid. - let host_addr = guest_mem.get_host_address(region.start_addr()).unwrap(); - let enc_region = kvm_enc_region { - addr: host_addr as u64, - size: region.len(), - }; - - vm_fd - .register_enc_memory_region(&enc_region) - .map_err(|_| Error::MemoryEncryptRegion)?; - } - - let mut policy = GuestPolicy(0); - policy.set_smt_allowed(true); - - let start = Start::new(policy, [0; 16]); - - let launcher = launcher.start(start).map_err(Error::LaunchStart)?; - - Ok(launcher) - } - - fn write_cpuid_page( - &self, - cpuid: CpuId, - guest_mem: &GuestMemoryMmap, - ) -> Result { - let mut cpuid_entry = CpuidPageEntry { - count: 0, - reserved_1: 0, - reserved_2: 0, - functions: [CpuidFunctionEntry::default(); COUNT_MAX], - }; - - for (i, kvm_entry) in cpuid.as_slice().iter().enumerate() { - // GET_CPUID2 returns bogus entries at the end with all zero set - if kvm_entry.function == 0 && kvm_entry.index == 0 && i != 0 { - continue; - } - - if kvm_entry.function == 0xFFFFFFFF { - break; - } - - // range check, see: - // SEV Secure Nested Paging Firmware ABI Specification - // 8.14.2.6 PAGE_TYPE_CPUID - if !((0..0xFFFF).contains(&kvm_entry.function) - || (0x8000_0000..0x8000_FFFF).contains(&kvm_entry.function)) - { - continue; - } - - let mut snp_cpuid_entry = CpuidFunctionEntry { - eax_in: kvm_entry.function, - ecx_in: { - if (kvm_entry.flags & KVM_CPUID_FLAG_SIGNIFCANT_INDEX) != 0 { - kvm_entry.index - } else { - 0 - } - }, - xcr0_in: 0, - xss_in: 0, - eax: kvm_entry.eax, - ebx: kvm_entry.ebx, - ecx: kvm_entry.ecx, - edx: kvm_entry.edx, - ..Default::default() - }; - - // Expose HYPERVISOR. - if snp_cpuid_entry.eax_in == 0x1 { - snp_cpuid_entry.ecx |= 1 << 31; - } - - // Disable extended features, not supported by SNP guests. - if snp_cpuid_entry.eax_in == 0x7 { - snp_cpuid_entry.ebx &= !(1 << 1); - snp_cpuid_entry.edx = 0; - } - - // Disable virt_ssbd, not supported by SNP guests. - if snp_cpuid_entry.eax_in == 0x8000_0008 { - snp_cpuid_entry.ebx &= !(1 << 25); - } - - // Fix XSAVE entry. - if snp_cpuid_entry.eax_in == 0xD { - if snp_cpuid_entry.ecx_in == 0x1 { - snp_cpuid_entry.xcr0_in = 0x1; - } - if snp_cpuid_entry.ecx_in == 0x0 || snp_cpuid_entry.ecx_in == 0x1 { - snp_cpuid_entry.ebx = 576; - } - } - - // Indicate the guest is running with SNP enabled. - if snp_cpuid_entry.eax_in == 0x8000_001F { - snp_cpuid_entry.eax = 0x1a; - snp_cpuid_entry.ebx = 51 | (1 << 6); - snp_cpuid_entry.ecx = 0; - snp_cpuid_entry.edx = 0; - } - - if cpuid_entry.count as usize >= COUNT_MAX { - return Err(Error::CpuIdFull); - } - - cpuid_entry.functions[cpuid_entry.count as usize] = snp_cpuid_entry; - cpuid_entry.count += 1; - } - - // Expose the KVM hypervisor signature. - let snp_cpuid_entry = CpuidFunctionEntry { - eax_in: 0x40000000, - ecx_in: 0, - xcr0_in: 0, - xss_in: 0, - eax: 0x40000001, - ebx: as_u32_le(b"KVMK"), - ecx: as_u32_le(b"VMKV"), - edx: as_u32_le(b"M\0\0\0"), - ..Default::default() - }; - - cpuid_entry.functions[cpuid_entry.count as usize] = snp_cpuid_entry; - cpuid_entry.count += 1; - - // Expose the KVM hypervisor flags. - let snp_cpuid_entry = CpuidFunctionEntry { - eax_in: 0x40000001, - ecx_in: 0, - xcr0_in: 0, - xss_in: 0, - eax: 0xff, - ebx: 0, - ecx: 0, - edx: 0, - ..Default::default() - }; - - cpuid_entry.functions[cpuid_entry.count as usize] = snp_cpuid_entry; - cpuid_entry.count += 1; - - let data = unsafe { - std::slice::from_raw_parts( - &cpuid_entry as *const _ as *const u8, - std::mem::size_of::(), - ) - }; - guest_mem - .write(data, GuestAddress(0x6000)) - .map_err(Error::GuestMemoryWrite)?; - - Ok(cpuid_entry) - } - - fn check_cpuid_page( - &self, - guest_mem: &GuestMemoryMmap, - old_cpuid: CpuidPageEntry, - ) -> Result<(), Error> { - let mut data: [u8; 4096] = [0; 4096]; - guest_mem - .read(&mut data, GuestAddress(0x6000)) - .map_err(Error::GuestMemoryRead)?; - - let new_cpuid_p = data.as_ptr() as *const CpuidPageEntry; - let new_cpuid = unsafe { *new_cpuid_p }; - - for (i, entry) in old_cpuid.functions.iter().enumerate() { - if *entry != new_cpuid.functions[i] { - debug!("cpuid entry: {i} differs"); - debug!("provided {entry:?}"); - debug!("expected: {:?}", new_cpuid.functions[i]); - } - } - - Ok(()) - } - - fn add_region( - &self, - guest_mem: &GuestMemoryMmap, - region: MeasuredRegion, - launcher: &mut Launcher, - page_type: PageType, - ) -> Result<(), Error> { - let ga = GuestAddress(region.guest_addr); - - /* - * Use the guest's address to obtain its GuestRegionMmap, and then - * convert this region to a slice. Basically, we are taking an - * entire slice of a guest memory region. - */ - let gr: &GuestRegionMmap = guest_mem.find_region(ga).unwrap(); - - let region_addr = gr.to_region_addr(ga).unwrap(); - let bytes = gr.get_slice(region_addr, region.size).unwrap(); - let ptr = bytes.ptr_guard().as_ptr(); - let slice: &[u8] = unsafe { slice::from_raw_parts(ptr, region.size) }; - - let update = Update::new(region.guest_addr >> 12, slice, page_type); - - launcher - .update_data(update, region.guest_addr, region.size as u64) - .map_err(Error::LaunchUpdate) - } - - pub fn vm_measure( - &self, - cpuid: CpuId, - guest_mem: &GuestMemoryMmap, - measured_regions: Vec, - mut launcher: Launcher, - ) -> Result<(), Error> { - for region in measured_regions { - self.add_region(guest_mem, region, &mut launcher, PageType::Normal)?; - } - - // Inital LIDT - self.add_region( - guest_mem, - MeasuredRegion { - guest_addr: SNP_LIDT_START, - host_addr: guest_mem - .get_host_address(GuestAddress(SNP_LIDT_START)) - .unwrap() as u64, - size: 0x1000, - }, - &mut launcher, - PageType::Zero, - )?; - - // Secrets page - self.add_region( - guest_mem, - MeasuredRegion { - guest_addr: SNP_SECRETS_START, - host_addr: guest_mem - .get_host_address(GuestAddress(SNP_SECRETS_START)) - .unwrap() as u64, - size: 0x1000, - }, - &mut launcher, - PageType::Secrets, - )?; - - // CPUID page - let old_cpuid = self.write_cpuid_page(cpuid, guest_mem)?; - if let Err(e) = self.add_region( - guest_mem, - MeasuredRegion { - guest_addr: SNP_CPUID_START, - host_addr: guest_mem - .get_host_address(GuestAddress(SNP_CPUID_START)) - .unwrap() as u64, - size: 0x1000, - }, - &mut launcher, - PageType::Cpuid, - ) { - // The PSP fixes the tables itself, so a second attempt should succeed. - warn!("PSP rejected the CPUID page ({e:?}). Trying again."); - - self.check_cpuid_page(guest_mem, old_cpuid)?; - if let Err(e) = self.add_region( - guest_mem, - MeasuredRegion { - guest_addr: SNP_CPUID_START, - host_addr: guest_mem - .get_host_address(GuestAddress(SNP_CPUID_START)) - .unwrap() as u64, - size: 0x1000, - }, - &mut launcher, - PageType::Cpuid, - ) { - error!("PSP rejected the CPUID page fixed by itself: {e:?}"); - } - } - - // FW stack and initial page tables - self.add_region( - guest_mem, - MeasuredRegion { - guest_addr: SNP_FWDATA_START, - host_addr: guest_mem - .get_host_address(GuestAddress(SNP_FWDATA_START)) - .unwrap() as u64, - size: SNP_FWDATA_SIZE, - }, - &mut launcher, - PageType::Zero, - )?; - - let finish = Finish::new(None, None, [0; 32]); - - let (_vmfd, _fwfd) = launcher.finish(finish).map_err(Error::LaunchFinish)?; - - Ok(()) - } -} diff --git a/vendor/krun-vmm/src/linux/tee/inteltdx.rs b/vendor/krun-vmm/src/linux/tee/inteltdx.rs deleted file mode 100644 index dc12f8cc3..000000000 --- a/vendor/krun-vmm/src/linux/tee/inteltdx.rs +++ /dev/null @@ -1,55 +0,0 @@ -use kvm_ioctls::VmFd; -use tdx::launch::{self, Launcher}; - -use std::os::unix::io::AsRawFd; - -#[derive(Debug)] -pub enum Error { - GetCapabilities(launch::Error), - InitVm(launch::Error), - InitMemoryRegions(launch::Error), - FinalizeVm(launch::Error), -} - -type Result = std::result::Result; - -pub struct IntelTdx {} - -impl IntelTdx { - pub fn new() -> Self { - Self {} - } - - pub fn vm_prepare(&self, vm_fd: &VmFd, cpuid: kvm_bindings::CpuId) -> Result { - let mut launcher = Launcher::new(vm_fd.as_raw_fd()); - let caps = launcher - .get_capabilities() - .map_err(Error::GetCapabilities)?; - launcher.init_vm(&caps, cpuid).map_err(Error::InitVm)?; - Ok(launcher) - } - - pub fn configure_td_memory( - &self, - launcher: &mut Launcher, - regions: &Vec, - ) -> Result<()> { - for region in regions { - let mem_region = tdx::launch::MemRegion::new( - region.guest_addr, - (region.size / 4096) as u64, - (arch::FIRMWARE_START == region.guest_addr).into(), - region.host_addr, - ); - launcher - .init_mem_region(mem_region) - .map_err(Error::InitMemoryRegions)?; - } - - Ok(()) - } - - pub fn finalize_vm(&self, mut launcher: Launcher) -> Result<()> { - launcher.finalize().map_err(Error::FinalizeVm) - } -} diff --git a/vendor/krun-vmm/src/linux/tee/mod.rs b/vendor/krun-vmm/src/linux/tee/mod.rs deleted file mode 100644 index 572856408..000000000 --- a/vendor/krun-vmm/src/linux/tee/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -#[cfg(feature = "amd-sev")] -pub mod amdsnp; - -#[cfg(feature = "tdx")] -pub mod inteltdx; diff --git a/vendor/krun-vmm/src/linux/vstate.rs b/vendor/krun-vmm/src/linux/vstate.rs deleted file mode 100644 index 05e58fbd7..000000000 --- a/vendor/krun-vmm/src/linux/vstate.rs +++ /dev/null @@ -1,2043 +0,0 @@ -// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -// -// Portions Copyright 2017 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the THIRD-PARTY file. - -#[cfg(target_arch = "aarch64")] -use arch::ArchMemoryInfo; -use crossbeam_channel::{unbounded, Receiver, Sender, TryRecvError}; -use libc::{c_int, c_void, siginfo_t}; -use std::cell::Cell; -use std::fmt::{Display, Formatter}; -use std::io; -use std::ops::Range; - -use std::os::unix::io::RawFd; - -#[cfg(target_arch = "x86_64")] -use std::env; -use std::result; -use std::sync::atomic::{fence, Ordering}; -#[cfg(not(test))] -use std::sync::Barrier; -use std::thread; -#[cfg(target_arch = "x86_64")] -use std::time::Duration; - -use super::super::{FC_EXIT_CODE_GENERIC_ERROR, FC_EXIT_CODE_OK}; - -#[cfg(feature = "amd-sev")] -use super::tee::amdsnp::{AmdSnp, Error as SnpError}; - -#[cfg(feature = "tdx")] -use super::tee::inteltdx::{Error as TdxError, IntelTdx}; - -#[cfg(feature = "tee")] -use kbs_types::Tee; - -#[cfg(feature = "tee")] -use crate::resources::TeeConfig; -use crate::vmm_config::machine_config::CpuFeaturesTemplate; -#[cfg(target_arch = "x86_64")] -use cpuid::{c3, filter_cpuid, t2, VmSpec}; -#[cfg(target_arch = "x86_64")] -use kvm_bindings::{ - kvm_clock_data, kvm_debugregs, kvm_irqchip, kvm_lapic_state, kvm_mp_state, kvm_pit_state2, - kvm_regs, kvm_sregs, kvm_vcpu_events, kvm_xcrs, kvm_xsave, CpuId, MsrList, Msrs, - KVM_CLOCK_TSC_STABLE, KVM_IRQCHIP_IOAPIC, KVM_IRQCHIP_PIC_MASTER, KVM_IRQCHIP_PIC_SLAVE, - KVM_MAX_CPUID_ENTRIES, -}; -use kvm_bindings::{ - kvm_create_guest_memfd, kvm_userspace_memory_region, kvm_userspace_memory_region2, - KVM_API_VERSION, KVM_MEM_GUEST_MEMFD, KVM_SYSTEM_EVENT_RESET, KVM_SYSTEM_EVENT_SHUTDOWN, -}; -#[cfg(feature = "tee")] -use kvm_bindings::{kvm_enable_cap, KVM_CAP_EXIT_HYPERCALL, KVM_MEMORY_EXIT_FLAG_PRIVATE}; -#[cfg(not(target_arch = "riscv64"))] -use kvm_bindings::{kvm_memory_attributes, KVM_MEMORY_ATTRIBUTE_PRIVATE}; -use kvm_ioctls::{Cap::*, *}; -use utils::eventfd::EventFd; -use utils::signal::{register_signal_handler, sigrtmin, Killable}; -use utils::sm::StateMachine; -#[cfg(feature = "tee")] -use utils::worker_message::{MemoryProperties, WorkerMessage}; -use vm_memory::{ - Address, GuestAddress, GuestMemory, GuestMemoryError, GuestMemoryMmap, GuestMemoryRegion, - GuestRegionMmap, -}; - -#[cfg(feature = "amd-sev")] -use super::tee::amdsnp::launch as snp; - -/// Signal number (SIGRTMIN) used to kick Vcpus. -pub(crate) const VCPU_RTSIG_OFFSET: i32 = 0; - -/// Errors associated with the wrappers over KVM ioctls. -#[derive(Debug)] -pub enum Error { - #[cfg(target_arch = "x86_64")] - /// A call to cpuid instruction failed. - CpuId(cpuid::Error), - /// Unable to create a KVM guest_memfd. - CreateGuestMemfd(kvm_ioctls::Error), - #[cfg(target_arch = "x86_64")] - /// Error configuring the floating point related registers - FPUConfiguration(arch::x86_64::regs::Error), - /// Invalid guest memory configuration. - GuestMemoryMmap(GuestMemoryError), - #[cfg(target_arch = "x86_64")] - /// Retrieving supported guest MSRs fails. - GuestMSRs(arch::x86_64::msr::Error), - /// Hyperthreading flag is not initialized. - HTNotInitialized, - /// Unable to enable KVM hypercall exits. - #[cfg(feature = "tee")] - HypercallExitEnable(kvm_ioctls::Error), - /// Cannot configure the IRQ. - Irq(kvm_ioctls::Error), - /// The host kernel reports an invalid KVM API version. - KvmApiVersion(i32), - /// Cannot initialize the KVM context due to missing capabilities. - KvmCap(kvm_ioctls::Cap), - #[cfg(feature = "amd-sev")] - /// Cannot read the CPUID entries from KVM. - KvmCpuId(kvm_ioctls::Error), - #[cfg(target_arch = "x86_64")] - /// Cannot set the local interruption due to bad configuration. - LocalIntConfiguration(arch::x86_64::interrupts::Error), - #[cfg(feature = "tee")] - /// Missing TEE config - MissingTeeConfig, - #[cfg(target_arch = "x86_64")] - /// Error configuring the MSR registers - MSRSConfiguration(arch::x86_64::msr::Error), - /// The number of configured slots is bigger than the maximum reported by KVM. - NotEnoughMemorySlots, - #[cfg(target_arch = "aarch64")] - /// Error configuring the general purpose aarch64 registers. - REGSConfiguration(arch::aarch64::regs::Error), - #[cfg(target_arch = "riscv64")] - /// Error configuring the general purpose riscv64 registers. - REGSConfiguration(arch::riscv64::regs::Error), - #[cfg(target_arch = "x86_64")] - /// Error configuring the general purpose registers - REGSConfiguration(arch::x86_64::regs::Error), - /// Cannot set memory region attributes. - SetMemoryAttributes(kvm_ioctls::Error), - /// Cannot set the memory regions. - SetUserMemoryRegion(kvm_ioctls::Error), - /// Error creating memory map for SHM region. - ShmMmap(io::Error), - #[cfg(feature = "amd-sev")] - /// Error initializing the Secure Virtualization Backend (SNP). - SnpSecVirtInit(SnpError), - #[cfg(feature = "amd-sev")] - /// Error preparing the VM for Secure Virtualization (SNP). - SnpSecVirtPrepare(SnpError), - #[cfg(feature = "amd-sev")] - /// Error attesting the Secure VM (SNP). - SnpSecVirtAttest(SnpError), - #[cfg(feature = "tdx")] - /// Error preparing the VM for Trust Domain Extensions (TDX) - TdxSecVirtPrepare(TdxError), - #[cfg(feature = "tdx")] - /// Error initializing vCPU for Trust Domain Extensions (TDX) - TdxSecVirtInitVcpu, - #[cfg(feature = "tee")] - /// The TEE specified is not supported. - InvalidTee, - /// Failed to signal Vcpu. - SignalVcpu(utils::errno::Error), - #[cfg(target_arch = "x86_64")] - /// Error configuring the special registers - SREGSConfiguration(arch::x86_64::regs::Error), - #[cfg(target_arch = "aarch64")] - /// Error doing Vcpu Init on Arm. - VcpuArmInit(kvm_ioctls::Error), - #[cfg(target_arch = "aarch64")] - /// Error getting the Vcpu preferred target on Arm. - VcpuArmPreferredTarget(kvm_ioctls::Error), - /// vCPU count is not initialized. - VcpuCountNotInitialized, - /// Cannot open the VCPU file descriptor. - VcpuFd(kvm_ioctls::Error), - #[cfg(target_arch = "x86_64")] - /// Failed to get KVM vcpu debug regs. - VcpuGetDebugRegs(kvm_ioctls::Error), - #[cfg(target_arch = "x86_64")] - /// Failed to get KVM vcpu lapic. - VcpuGetLapic(kvm_ioctls::Error), - #[cfg(target_arch = "x86_64")] - /// Failed to get KVM vcpu mp state. - VcpuGetMpState(kvm_ioctls::Error), - #[cfg(target_arch = "x86_64")] - /// Failed to get KVM vcpu msrs. - VcpuGetMsrs(kvm_ioctls::Error), - #[cfg(target_arch = "x86_64")] - /// Failed to get KVM vcpu regs. - VcpuGetRegs(kvm_ioctls::Error), - #[cfg(target_arch = "x86_64")] - /// Failed to get KVM vcpu sregs. - VcpuGetSregs(kvm_ioctls::Error), - #[cfg(target_arch = "x86_64")] - /// Failed to get KVM vcpu event. - VcpuGetVcpuEvents(kvm_ioctls::Error), - #[cfg(target_arch = "x86_64")] - /// Failed to get KVM vcpu xcrs. - VcpuGetXcrs(kvm_ioctls::Error), - #[cfg(target_arch = "x86_64")] - /// Failed to get KVM vcpu xsave. - VcpuGetXsave(kvm_ioctls::Error), - /// Cannot run the VCPUs. - VcpuRun(kvm_ioctls::Error), - #[cfg(target_arch = "x86_64")] - /// Failed to set KVM vcpu cpuid. - VcpuSetCpuid(kvm_ioctls::Error), - #[cfg(target_arch = "x86_64")] - /// Failed to set KVM vcpu debug regs. - VcpuSetDebugRegs(kvm_ioctls::Error), - #[cfg(target_arch = "x86_64")] - /// Failed to set KVM vcpu lapic. - VcpuSetLapic(kvm_ioctls::Error), - #[cfg(target_arch = "x86_64")] - /// Failed to set KVM vcpu mp state. - VcpuSetMpState(kvm_ioctls::Error), - #[cfg(target_arch = "x86_64")] - /// Failed to set KVM vcpu msrs. - VcpuSetMsrs(kvm_ioctls::Error), - #[cfg(target_arch = "x86_64")] - /// Failed to set KVM vcpu regs. - VcpuSetRegs(kvm_ioctls::Error), - #[cfg(target_arch = "x86_64")] - /// Failed to set KVM vcpu sregs. - VcpuSetSregs(kvm_ioctls::Error), - #[cfg(target_arch = "x86_64")] - /// Failed to set KVM vcpu event. - VcpuSetVcpuEvents(kvm_ioctls::Error), - #[cfg(target_arch = "x86_64")] - /// Failed to set KVM vcpu xcrs. - VcpuSetXcrs(kvm_ioctls::Error), - #[cfg(target_arch = "x86_64")] - /// Failed to set KVM vcpu xsave. - VcpuSetXsave(kvm_ioctls::Error), - /// Cannot spawn a new vCPU thread. - VcpuSpawn(io::Error), - /// Cannot cleanly initialize vcpu TLS. - VcpuTlsInit, - /// Vcpu not present in TLS. - VcpuTlsNotPresent, - /// Unexpected KVM_RUN exit reason - VcpuUnhandledKvmExit, - /// Unsupported KVM_EXIT_HYPERCALL. - #[cfg(feature = "tee")] - VcpuUnsupportedHypercall, - /// Cannot open the VM file descriptor. - VmFd(kvm_ioctls::Error), - #[cfg(target_arch = "x86_64")] - /// Failed to get KVM vm pit state. - VmGetPit2(kvm_ioctls::Error), - #[cfg(target_arch = "x86_64")] - /// Failed to get KVM vm clock. - VmGetClock(kvm_ioctls::Error), - #[cfg(target_arch = "x86_64")] - /// Failed to get KVM vm irqchip. - VmGetIrqChip(kvm_ioctls::Error), - #[cfg(target_arch = "x86_64")] - /// Failed to set KVM vm pit state. - VmSetPit2(kvm_ioctls::Error), - #[cfg(target_arch = "x86_64")] - /// Failed to set KVM vm clock. - VmSetClock(kvm_ioctls::Error), - #[cfg(target_arch = "x86_64")] - /// Failed to set KVM vm irqchip. - VmSetIrqChip(kvm_ioctls::Error), - /// Cannot configure the microvm. - VmSetup(kvm_ioctls::Error), - /// Failed to enable split IRQCHIP in vm - VmSplitIrqchip(kvm_ioctls::Error), - /// Failed to set vm APIC bus clock rate (in nanoseconds) - VmApicBusClockRate(kvm_ioctls::Error), -} - -impl Display for Error { - fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { - use self::Error::*; - - match self { - #[cfg(target_arch = "x86_64")] - CpuId(e) => write!(f, "Cpuid error: {e:?}"), - CreateGuestMemfd(e) => write!(f, "Unable to create KVM guest_memfd: {e:?}"), - GuestMemoryMmap(e) => write!(f, "Guest memory error: {e:?}"), - #[cfg(target_arch = "x86_64")] - GuestMSRs(e) => write!(f, "Retrieving supported guest MSRs fails: {e:?}"), - HTNotInitialized => write!(f, "Hyperthreading flag is not initialized"), - #[cfg(feature = "tee")] - HypercallExitEnable(e) => write!(f, "Unable to enable KVM hypercall exits: {e}"), - KvmApiVersion(v) => { - write!(f, "The host kernel reports an invalid KVM API version: {v}") - } - KvmCap(cap) => write!(f, "Missing KVM capabilities: {cap:?}"), - #[cfg(feature = "amd-sev")] - KvmCpuId(e) => write!(f, "Cannot read CPUID entries from KVM: {e}"), - VcpuCountNotInitialized => write!(f, "vCPU count is not initialized"), - VmFd(e) => write!(f, "Cannot open the VM file descriptor: {e}"), - VcpuFd(e) => write!(f, "Cannot open the VCPU file descriptor: {e}"), - VmSetup(e) => write!(f, "Cannot configure the microvm: {e}"), - VmSplitIrqchip(e) => write!(f, "Failed to enable split IRQCHIP: {e}"), - VmApicBusClockRate(e) => write!( - f, - "Failed to set vm APIC bus clock rate (in nanoseconds): {e}" - ), - VcpuRun(e) => write!(f, "Cannot run the VCPUs: {e}"), - NotEnoughMemorySlots => write!( - f, - "The number of configured slots is bigger than the maximum reported by KVM" - ), - #[cfg(target_arch = "x86_64")] - LocalIntConfiguration(e) => write!( - f, - "Cannot set the local interruption due to bad configuration: {e:?}" - ), - SetMemoryAttributes(e) => write!(f, "Cannot set memory region attributes: {e}"), - SetUserMemoryRegion(e) => write!(f, "Cannot set the memory regions: {e}"), - ShmMmap(e) => write!(f, "Error creating memory map for SHM region: {e}"), - #[cfg(feature = "amd-sev")] - SnpSecVirtInit(e) => write!( - f, - "Error initializing the Secure Virtualization Backend (SEV): {e:?}" - ), - - #[cfg(feature = "amd-sev")] - SnpSecVirtPrepare(e) => write!( - f, - "Error preparing the VM for Secure Virtualization (SNP): {e:?}" - ), - - #[cfg(feature = "amd-sev")] - SnpSecVirtAttest(e) => write!(f, "Error attesting the Secure VM (SNP): {e:?}"), - - SignalVcpu(e) => write!(f, "Failed to signal Vcpu: {e}"), - #[cfg(feature = "tdx")] - TdxSecVirtPrepare(e) => write!( - f, - "Error preparing the VM for Trust Domain Extensions (TDX): {e:?}" - ), - #[cfg(feature = "tdx")] - TdxSecVirtInitVcpu => write!( - f, - "Error initializing vCPU for Trust Domain Extensions (TDX)" - ), - #[cfg(feature = "tee")] - MissingTeeConfig => write!(f, "Missing TEE configuration"), - #[cfg(target_arch = "x86_64")] - MSRSConfiguration(e) => write!(f, "Error configuring the MSR registers: {e:?}"), - #[cfg(target_arch = "aarch64")] - REGSConfiguration(e) => write!( - f, - "Error configuring the general purpose aarch64 registers: {e:?}" - ), - #[cfg(target_arch = "riscv64")] - REGSConfiguration(e) => write!( - f, - "Error configuring the general purpose riscv64 registers: {e:?}" - ), - #[cfg(target_arch = "x86_64")] - REGSConfiguration(e) => { - write!(f, "Error configuring the general purpose registers: {e:?}") - } - #[cfg(target_arch = "x86_64")] - SREGSConfiguration(e) => write!(f, "Error configuring the special registers: {e:?}"), - #[cfg(target_arch = "x86_64")] - FPUConfiguration(e) => write!( - f, - "Error configuring the floating point related registers: {e:?}" - ), - Irq(e) => write!(f, "Cannot configure the IRQ: {e}"), - #[cfg(target_arch = "x86_64")] - VcpuGetDebugRegs(e) => write!(f, "Failed to get KVM vcpu debug regs: {e}"), - #[cfg(target_arch = "x86_64")] - VcpuGetLapic(e) => write!(f, "Failed to get KVM vcpu lapic: {e}"), - #[cfg(target_arch = "x86_64")] - VcpuGetMpState(e) => write!(f, "Failed to get KVM vcpu mp state: {e}"), - #[cfg(target_arch = "x86_64")] - VcpuGetMsrs(e) => write!(f, "Failed to get KVM vcpu msrs: {e}"), - #[cfg(target_arch = "x86_64")] - VcpuGetRegs(e) => write!(f, "Failed to get KVM vcpu regs: {e}"), - #[cfg(target_arch = "x86_64")] - VcpuGetSregs(e) => write!(f, "Failed to get KVM vcpu sregs: {e}"), - #[cfg(target_arch = "x86_64")] - VcpuGetVcpuEvents(e) => write!(f, "Failed to get KVM vcpu event: {e}"), - #[cfg(target_arch = "x86_64")] - VcpuGetXcrs(e) => write!(f, "Failed to get KVM vcpu xcrs: {e}"), - #[cfg(target_arch = "x86_64")] - VcpuGetXsave(e) => write!(f, "Failed to get KVM vcpu xsave: {e}"), - #[cfg(target_arch = "x86_64")] - VcpuSetCpuid(e) => write!(f, "Failed to set KVM vcpu cpuid: {e}"), - #[cfg(target_arch = "x86_64")] - VcpuSetDebugRegs(e) => write!(f, "Failed to set KVM vcpu debug regs: {e}"), - #[cfg(target_arch = "x86_64")] - VcpuSetLapic(e) => write!(f, "Failed to set KVM vcpu lapic: {e}"), - #[cfg(target_arch = "x86_64")] - VcpuSetMpState(e) => write!(f, "Failed to set KVM vcpu mp state: {e}"), - #[cfg(target_arch = "x86_64")] - VcpuSetMsrs(e) => write!(f, "Failed to set KVM vcpu msrs: {e}"), - #[cfg(target_arch = "x86_64")] - VcpuSetRegs(e) => write!(f, "Failed to set KVM vcpu regs: {e}"), - #[cfg(target_arch = "x86_64")] - VcpuSetSregs(e) => write!(f, "Failed to set KVM vcpu sregs: {e}"), - #[cfg(target_arch = "x86_64")] - VcpuSetVcpuEvents(e) => write!(f, "Failed to set KVM vcpu event: {e}"), - #[cfg(target_arch = "x86_64")] - VcpuSetXcrs(e) => write!(f, "Failed to set KVM vcpu xcrs: {e}"), - #[cfg(target_arch = "x86_64")] - VcpuSetXsave(e) => write!(f, "Failed to set KVM vcpu xsave: {e}"), - VcpuSpawn(e) => write!(f, "Cannot spawn a new vCPU thread: {e}"), - VcpuTlsInit => write!(f, "Cannot clean init vcpu TLS"), - VcpuTlsNotPresent => write!(f, "Vcpu not present in TLS"), - VcpuUnhandledKvmExit => write!(f, "Unexpected KVM_RUN exit reason"), - #[cfg(feature = "tee")] - VcpuUnsupportedHypercall => write!(f, "Unsupported KVM_EXIT_HYPERCALL"), - #[cfg(target_arch = "x86_64")] - VmGetPit2(e) => write!(f, "Failed to get KVM vm pit state: {e}"), - #[cfg(target_arch = "x86_64")] - VmGetClock(e) => write!(f, "Failed to get KVM vm clock: {e}"), - #[cfg(target_arch = "x86_64")] - VmGetIrqChip(e) => write!(f, "Failed to get KVM vm irqchip: {e}"), - #[cfg(target_arch = "x86_64")] - VmSetPit2(e) => write!(f, "Failed to set KVM vm pit state: {e}"), - #[cfg(target_arch = "x86_64")] - VmSetClock(e) => write!(f, "Failed to set KVM vm clock: {e}"), - #[cfg(target_arch = "x86_64")] - VmSetIrqChip(e) => write!(f, "Failed to set KVM vm irqchip: {e}"), - #[cfg(target_arch = "aarch64")] - VcpuArmPreferredTarget(e) => { - write!(f, "Error getting the Vcpu preferred target on Arm: {e}") - } - #[cfg(target_arch = "aarch64")] - VcpuArmInit(e) => write!(f, "Error doing Vcpu Init on Arm: {e}"), - - #[cfg(feature = "tee")] - InvalidTee => write!(f, "TEE selected is not currently supported"), - } - } -} - -pub type Result = result::Result; - -#[cfg(feature = "tee")] -#[derive(Debug)] -pub struct MeasuredRegion { - pub guest_addr: u64, - pub host_addr: u64, - pub size: usize, -} - -/// Describes a KVM context that gets attached to the microVM. -/// It gives access to the functionality of the KVM wrapper as -/// long as every required KVM capability is present on the host. -pub struct KvmContext { - kvm: Kvm, - max_memslots: usize, -} - -impl KvmContext { - pub fn new() -> Result { - let kvm = Kvm::new().expect("Error creating the Kvm object"); - - // Check that KVM has the correct version. - if kvm.get_api_version() != KVM_API_VERSION as i32 { - return Err(Error::KvmApiVersion(kvm.get_api_version())); - } - - // A list of KVM capabilities we want to check. - #[cfg(target_arch = "x86_64")] - let capabilities = [Irqchip, Ioeventfd, Irqfd, UserMemory, SetTssAddr]; - - #[cfg(target_arch = "aarch64")] - let capabilities = [Irqchip, Ioeventfd, Irqfd, UserMemory, ArmPsci02]; - - #[cfg(target_arch = "riscv64")] - let capabilities = [Irqchip, Ioeventfd, Irqfd, UserMemory]; - - // Check that all desired capabilities are supported. - match capabilities - .iter() - .find(|&capability| !kvm.check_extension(*capability)) - { - None => { - let max_memslots = kvm.get_nr_memslots(); - Ok(KvmContext { kvm, max_memslots }) - } - - Some(c) => Err(Error::KvmCap(*c)), - } - } - - pub fn fd(&self) -> &Kvm { - &self.kvm - } - - /// Get the maximum number of memory slots reported by this KVM context. - pub fn max_memslots(&self) -> usize { - self.max_memslots - } -} - -/// A wrapper around creating and using a VM. -pub struct Vm { - fd: VmFd, - next_mem_slot: u32, - - // X86 specific fields. - #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] - supported_cpuid: CpuId, - #[cfg(target_arch = "x86_64")] - supported_msrs: MsrList, - - #[cfg(feature = "amd-sev")] - tee: Option, - - #[cfg(feature = "tdx")] - tdx: Option, - - #[cfg(feature = "tee")] - pub tee_config: Tee, - - pub guest_memfds: Vec<(Range, RawFd)>, -} - -impl Vm { - /// Constructs a new `Vm` using the given `Kvm` instance. - #[cfg(not(feature = "tee"))] - pub fn new(kvm: &Kvm) -> Result { - //create fd for interacting with kvm-vm specific functions - let vm_fd = kvm.create_vm().map_err(Error::VmFd)?; - - #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] - let supported_cpuid = kvm - .get_supported_cpuid(KVM_MAX_CPUID_ENTRIES) - .map_err(Error::VmFd)?; - #[cfg(target_arch = "x86_64")] - let supported_msrs = - arch::x86_64::msr::supported_guest_msrs(kvm).map_err(Error::GuestMSRs)?; - - Ok(Vm { - fd: vm_fd, - next_mem_slot: 0, - #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] - supported_cpuid, - #[cfg(target_arch = "x86_64")] - supported_msrs, - guest_memfds: Vec::new(), - }) - } - - #[cfg(feature = "amd-sev")] - pub fn new(kvm: &Kvm, tee_config: &TeeConfig) -> Result { - //create fd for interacting with kvm-vm specific functions - let vm_fd = kvm - .create_vm_with_type(4 /* KVM_X86_SNP_VM */) - .map_err(Error::VmFd)?; - - let supported_cpuid = kvm - .get_supported_cpuid(KVM_MAX_CPUID_ENTRIES) - .map_err(Error::VmFd)?; - - let supported_msrs = - arch::x86_64::msr::supported_guest_msrs(kvm).map_err(Error::GuestMSRs)?; - - let cap = kvm_enable_cap { - cap: KVM_CAP_EXIT_HYPERCALL, - flags: 0, - args: [1 << 12 /* KVM_HC_MAP_GPA_RANGE */, 0, 0, 0], - ..Default::default() - }; - - vm_fd.enable_cap(&cap).map_err(Error::HypercallExitEnable)?; - - let tee = match tee_config.tee { - Tee::Snp => Some(AmdSnp::new().map_err(Error::SnpSecVirtInit)?), - _ => return Err(Error::InvalidTee), - }; - - Ok(Vm { - fd: vm_fd, - next_mem_slot: 0, - supported_cpuid, - supported_msrs, - tee, - tee_config: tee_config.tee, - guest_memfds: Vec::new(), - }) - } - - #[cfg(feature = "tdx")] - pub fn new( - kvm: &Kvm, - tee_config: &TeeConfig, - _sender: crossbeam_channel::Sender, - ) -> Result { - // create fd for interacting with kvm-vm specific functions - let vm_fd = kvm - .create_vm_with_type(tdx::launch::KVM_X86_TDX_VM) - .map_err(Error::VmFd)?; - - let supported_cpuid = kvm - .get_supported_cpuid(KVM_MAX_CPUID_ENTRIES) - .map_err(Error::VmFd)?; - - let supported_msrs = - arch::x86_64::msr::supported_guest_msrs(kvm).map_err(Error::GuestMSRs)?; - - let mut cap = kvm_enable_cap { - cap: KVM_CAP_EXIT_HYPERCALL, - flags: 0, - args: [1 << 12 /* KVM_HC_MAP_GPA_RANGE */, 0, 0, 0], - ..Default::default() - }; - vm_fd.enable_cap(&cap).map_err(Error::HypercallExitEnable)?; - - cap.cap = kvm_bindings::KVM_CAP_SPLIT_IRQCHIP; - cap.args[0] = 24; - vm_fd.enable_cap(&cap).map_err(Error::VmSplitIrqchip)?; - - cap.cap = 237; // KVM_CAP_X86_APIC_BUS_CYCLES_NS - cap.args[0] = 40; - vm_fd.enable_cap(&cap).map_err(Error::VmApicBusClockRate)?; - - Ok(Vm { - fd: vm_fd, - next_mem_slot: 0, - supported_cpuid, - supported_msrs, - tdx: Some(IntelTdx::new()), - tee_config: tee_config.tee, - guest_memfds: Vec::new(), - }) - } - - /// Returns a ref to the supported `CpuId` for this Vm. - #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] - pub fn supported_cpuid(&self) -> &CpuId { - &self.supported_cpuid - } - - /// Returns a ref to the supported `MsrList` for this Vm. - #[cfg(target_arch = "x86_64")] - pub fn supported_msrs(&self) -> &MsrList { - &self.supported_msrs - } - - /// Initializes the guest memory. - pub fn memory_init( - &mut self, - guest_mem: &GuestMemoryMmap, - kvm_max_memslots: usize, - ) -> Result<()> { - if guest_mem.num_regions() > kvm_max_memslots { - return Err(Error::NotEnoughMemorySlots); - } - - for region in guest_mem.iter() { - self.memory_region_set(guest_mem, region)?; - } - - #[cfg(target_arch = "x86_64")] - self.fd - .set_tss_address(arch::x86_64::layout::KVM_TSS_ADDRESS as usize) - .map_err(Error::VmSetup)?; - - Ok(()) - } - - pub fn guest_memfd_get(&self, gpa: u64) -> Option<(RawFd, u64)> { - for (range, rawfd) in self.guest_memfds.iter() { - if range.contains(&gpa) { - return Some((*rawfd, range.start)); - } - } - None - } - - #[allow(unused_mut)] - fn memory_region_set( - &mut self, - guest_mem: &GuestMemoryMmap, - region: &GuestRegionMmap, - ) -> Result<()> { - let host_addr = guest_mem.get_host_address(region.start_addr()).unwrap(); - let start = region.start_addr().raw_value(); - let end = start + region.len(); - - // GuestMemfd is generally intended for either of two purposes: - // * sharing the memory with out-of-process components, and conversely, - // * hiding the memory completely from the VMM process (Confidential Computing). - // - // We only use it for the second use case currently, so don't even try to use it - // outside of TEE builds. Software-protected VMs are only available on x86_64 and - // are marked with strongly-worded warnings about them being for development only, - // as of late 2025. Also, on other architectures like aarch64, guest_memfd in - // general is unstable for now, so don't try to use it without a reason. - - if cfg!(not(feature = "tee")) { - let memory_region = kvm_userspace_memory_region { - slot: self.next_mem_slot, - guest_phys_addr: start, - memory_size: region.len(), - userspace_addr: host_addr as u64, - flags: 0, - }; - - // Safe because we mapped the memory region, we made sure that the regions - // are not overlapping. - unsafe { - self.fd - .set_user_memory_region(memory_region) - .map_err(Error::SetUserMemoryRegion)?; - }; - } else { - if !self.fd.check_extension(GuestMemfd) { - return Err(Error::KvmCap(GuestMemfd)); - } - - // Create a guest_memfd and set the region. - let guest_memfd = self - .fd - .create_guest_memfd(kvm_create_guest_memfd { - size: region.size() as u64, - flags: 0, - reserved: [0; 6], - }) - .map_err(Error::CreateGuestMemfd)?; - - let memory_region = kvm_userspace_memory_region2 { - slot: self.next_mem_slot, - flags: KVM_MEM_GUEST_MEMFD, - guest_phys_addr: start, - memory_size: region.len(), - userspace_addr: host_addr as u64, - guest_memfd_offset: 0, - guest_memfd: guest_memfd as u32, - pad1: 0, - pad2: [0; 14], - }; - - // Safe because we mapped the memory region, we made sure that the regions - // are not overlapping. - unsafe { - self.fd - .set_user_memory_region2(memory_region) - .map_err(Error::SetUserMemoryRegion)?; - }; - - let attr = kvm_memory_attributes { - address: start, - size: region.len(), - attributes: KVM_MEMORY_ATTRIBUTE_PRIVATE as u64, - flags: 0, - }; - - self.fd - .set_memory_attributes(attr) - .map_err(Error::SetMemoryAttributes)?; - - self.guest_memfds.push((Range { start, end }, guest_memfd)); - } - - self.next_mem_slot += 1; - - Ok(()) - } - - #[cfg(feature = "tdx")] - pub fn tdx_secure_virt_prepare(&self) -> Result { - match &self.tdx { - Some(t) => t - .vm_prepare(&self.fd, self.supported_cpuid.clone()) - .map_err(Error::TdxSecVirtPrepare), - None => Err(Error::InvalidTee), - } - } - - #[cfg(feature = "tdx")] - pub fn tdx_secure_virt_init_vcpus(&self, launcher: &mut tdx::launch::Launcher) -> Result<()> { - match &self.tdx { - Some(_) => { - launcher.init_vcpus(0).unwrap(); - Ok(()) - } - None => Err(Error::InvalidTee), - } - } - - #[cfg(feature = "tdx")] - pub fn tdx_secure_virt_prepare_memory( - &self, - launcher: &mut tdx::launch::Launcher, - regions: &Vec, - ) -> Result<()> { - match &self.tdx { - Some(t) => t - .configure_td_memory(launcher, regions) - .map_err(Error::TdxSecVirtPrepare), - None => Err(Error::InvalidTee), - } - } - - #[cfg(feature = "tdx")] - pub fn tdx_secure_virt_finalize_vm(&self, launcher: tdx::launch::Launcher) -> Result<()> { - match &self.tdx { - Some(t) => t.finalize_vm(launcher).map_err(Error::TdxSecVirtPrepare), - None => Err(Error::InvalidTee), - } - } - - #[cfg(feature = "amd-sev")] - pub fn snp_secure_virt_prepare( - &self, - guest_mem: &GuestMemoryMmap, - ) -> Result> { - match &self.tee { - Some(s) => s - .vm_prepare(&self.fd, guest_mem) - .map_err(Error::SnpSecVirtPrepare), - None => Err(Error::InvalidTee), - } - } - - #[cfg(feature = "amd-sev")] - pub fn snp_secure_virt_measure( - &self, - cpuid: CpuId, - guest_mem: &GuestMemoryMmap, - measured_regions: Vec, - launcher: snp::Launcher, - ) -> Result<()> { - match &self.tee { - Some(s) => s - .vm_measure(cpuid, guest_mem, measured_regions, launcher) - .map_err(Error::SnpSecVirtAttest), - None => Err(Error::InvalidTee), - } - } - - /// Gets a reference to the kvm file descriptor owned by this VM. - pub fn fd(&self) -> &VmFd { - &self.fd - } - - #[allow(unused)] - #[cfg(target_arch = "x86_64")] - /// Saves and returns the Kvm Vm state. - pub fn save_state(&self) -> Result { - let pitstate = self.fd.get_pit2().map_err(Error::VmGetPit2)?; - - let mut clock = self.fd.get_clock().map_err(Error::VmGetClock)?; - // This bit is not accepted in SET_CLOCK, clear it. - clock.flags &= !KVM_CLOCK_TSC_STABLE; - - let mut pic_master = kvm_irqchip { - chip_id: KVM_IRQCHIP_PIC_MASTER, - ..Default::default() - }; - self.fd - .get_irqchip(&mut pic_master) - .map_err(Error::VmGetIrqChip)?; - - let mut pic_slave = kvm_irqchip { - chip_id: KVM_IRQCHIP_PIC_SLAVE, - ..Default::default() - }; - self.fd - .get_irqchip(&mut pic_slave) - .map_err(Error::VmGetIrqChip)?; - - let mut ioapic = kvm_irqchip { - chip_id: KVM_IRQCHIP_IOAPIC, - ..Default::default() - }; - self.fd - .get_irqchip(&mut ioapic) - .map_err(Error::VmGetIrqChip)?; - - Ok(VmState { - pitstate, - clock, - pic_master, - pic_slave, - ioapic, - }) - } - - #[allow(unused)] - #[cfg(target_arch = "x86_64")] - /// Restores the Kvm Vm state. - pub fn restore_state(&self, state: &VmState) -> Result<()> { - self.fd - .set_pit2(&state.pitstate) - .map_err(Error::VmSetPit2)?; - self.fd.set_clock(&state.clock).map_err(Error::VmSetClock)?; - self.fd - .set_irqchip(&state.pic_master) - .map_err(Error::VmSetIrqChip)?; - self.fd - .set_irqchip(&state.pic_slave) - .map_err(Error::VmSetIrqChip)?; - self.fd - .set_irqchip(&state.ioapic) - .map_err(Error::VmSetIrqChip)?; - Ok(()) - } -} - -#[allow(unused)] -#[cfg(target_arch = "x86_64")] -/// Structure holding VM kvm state. -pub struct VmState { - pitstate: kvm_pit_state2, - clock: kvm_clock_data, - pic_master: kvm_irqchip, - pic_slave: kvm_irqchip, - ioapic: kvm_irqchip, -} - -/// Encapsulates configuration parameters for the guest vCPUS. -#[derive(Debug, Eq, PartialEq)] -pub struct VcpuConfig { - /// Number of guest VCPUs. - pub vcpu_count: u8, - /// Enable hyperthreading in the CPUID configuration. - pub ht_enabled: bool, - /// CPUID template to use. - pub cpu_template: Option, - /// Enable nested virtualization in the CPUID configuration. - pub nested_enabled: bool, -} - -// Using this for easier explicit type-casting to help IDEs interpret the code. -type VcpuCell = Cell>; - -/// A wrapper around creating and using a kvm-based VCPU. -pub struct Vcpu { - fd: VcpuFd, - id: u8, - mmio_bus: Option, - #[allow(dead_code)] - #[cfg_attr(all(test, target_arch = "aarch64"), allow(unused))] - exit_evt: EventFd, - - #[cfg(target_arch = "x86_64")] - io_bus: devices::Bus, - #[cfg(target_arch = "x86_64")] - cpuid: CpuId, - #[cfg(target_arch = "x86_64")] - msr_list: MsrList, - #[cfg(target_arch = "x86_64")] - kernel_enomem_workaround: bool, - - #[cfg(target_arch = "aarch64")] - mpidr: u64, - - // The receiving end of events channel owned by the vcpu side. - event_receiver: Receiver, - // The transmitting end of the events channel which will be given to the handler. - event_sender: Option>, - // The receiving end of the responses channel which will be given to the handler. - response_receiver: Option>, - // The transmitting end of the responses channel owned by the vcpu side. - response_sender: Sender, - - #[cfg(feature = "tee")] - pm_sender: Sender, -} - -impl Vcpu { - thread_local!(static TLS_VCPU_PTR: VcpuCell = const { Cell::new(None) }); - - /// Associates `self` with the current thread. - /// - /// It is a prerequisite to successfully run `init_thread_local_data()` before using - /// `run_on_thread_local()` on the current thread. - /// This function will return an error if there already is a `Vcpu` present in the TLS. - fn init_thread_local_data(&mut self) -> Result<()> { - Self::TLS_VCPU_PTR.with(|cell: &VcpuCell| { - if cell.get().is_some() { - return Err(Error::VcpuTlsInit); - } - cell.set(Some(self as *mut Vcpu)); - Ok(()) - }) - } - - /// Deassociates `self` from the current thread. - /// - /// Should be called if the current `self` had called `init_thread_local_data()` and - /// now needs to move to a different thread. - /// - /// Fails if `self` was not previously associated with the current thread. - fn reset_thread_local_data(&mut self) -> Result<()> { - // Best-effort to clean up TLS. If the `Vcpu` was moved to another thread - // _before_ running this, then there is nothing we can do. - Self::TLS_VCPU_PTR.with(|cell: &VcpuCell| { - if let Some(vcpu_ptr) = cell.get() { - if std::ptr::eq(vcpu_ptr, self) { - Self::TLS_VCPU_PTR.with(|cell: &VcpuCell| cell.take()); - return Ok(()); - } - } - Err(Error::VcpuTlsNotPresent) - }) - } - - /// Runs `func` for the `Vcpu` associated with the current thread. - /// - /// It requires that `init_thread_local_data()` was run on this thread. - /// - /// Fails if there is no `Vcpu` associated with the current thread. - /// - /// # Safety - /// - /// This is marked unsafe as it allows temporary aliasing through - /// dereferencing from pointer an already borrowed `Vcpu`. - unsafe fn run_on_thread_local(func: F) -> Result<()> - where - F: FnOnce(&mut Vcpu), - { - Self::TLS_VCPU_PTR.with(|cell: &VcpuCell| { - if let Some(vcpu_ptr) = cell.get() { - // Dereferencing here is safe since `TLS_VCPU_PTR` is populated/non-empty, - // and it is being cleared on `Vcpu::drop` so there is no dangling pointer. - let vcpu_ref: &mut Vcpu = &mut *vcpu_ptr; - func(vcpu_ref); - Ok(()) - } else { - Err(Error::VcpuTlsNotPresent) - } - }) - } - - /// Registers a signal handler which makes use of TLS and kvm immediate exit to - /// kick the vcpu running on the current thread, if there is one. - pub fn register_kick_signal_handler() { - extern "C" fn handle_signal(_: c_int, _: *mut siginfo_t, _: *mut c_void) { - // This is safe because it's temporarily aliasing the `Vcpu` object, but we are - // only reading `vcpu.fd` which does not change for the lifetime of the `Vcpu`. - unsafe { - let _ = Vcpu::run_on_thread_local(|vcpu: &mut Vcpu| { - vcpu.fd.set_kvm_immediate_exit(1); - fence(Ordering::Release); - }); - } - } - - register_signal_handler(sigrtmin() + VCPU_RTSIG_OFFSET, handle_signal) - .expect("Failed to register vcpu signal handler"); - } - - /// Constructs a new VCPU for `vm`. - /// - /// # Arguments - /// - /// * `id` - Represents the CPU number between [0, max vcpus). - /// * `vm_fd` - The kvm `VmFd` for the virtual machine this vcpu will get attached to. - /// * `cpuid` - The `CpuId` listing the supported capabilities of this vcpu. - /// * `msr_list` - The `MsrList` listing the supported MSRs for this vcpu. - /// * `io_bus` - The io-bus used to access port-io devices. - /// * `exit_evt` - An `EventFd` that will be written into when this vcpu exits. - #[cfg(target_arch = "x86_64")] - pub fn new_x86_64( - id: u8, - vm_fd: &VmFd, - cpuid: CpuId, - msr_list: MsrList, - io_bus: devices::Bus, - exit_evt: EventFd, - #[cfg(feature = "tee")] pm_sender: Sender, - ) -> Result { - let kvm_vcpu = vm_fd.create_vcpu(id as u64).map_err(Error::VcpuFd)?; - let (event_sender, event_receiver) = unbounded(); - let (response_sender, response_receiver) = unbounded(); - - let kernel_enomem_workaround = if env::var_os("KRUN_ENOMEM_WORKAROUND").is_some() { - debug!("Enabling ENOMEM workaround"); - true - } else { - false - }; - - // Initially the cpuid per vCPU is the one supported by this VM. - Ok(Vcpu { - fd: kvm_vcpu, - id, - mmio_bus: None, - exit_evt, - io_bus, - cpuid, - msr_list, - kernel_enomem_workaround, - event_receiver, - event_sender: Some(event_sender), - response_receiver: Some(response_receiver), - response_sender, - #[cfg(feature = "tee")] - pm_sender, - }) - } - - /// Constructs a new VCPU for `vm`. - /// - /// # Arguments - /// - /// * `id` - Represents the CPU number between [0, max vcpus). - /// * `vm_fd` - The kvm `VmFd` for the virtual machine this vcpu will get attached to. - /// * `exit_evt` - An `EventFd` that will be written into when this vcpu exits. - /// * `create_ts` - A timestamp used by the vcpu to calculate its lifetime. - #[cfg(target_arch = "aarch64")] - pub fn new_aarch64(id: u8, vm_fd: &VmFd, exit_evt: EventFd) -> Result { - let kvm_vcpu = vm_fd.create_vcpu(id as u64).map_err(Error::VcpuFd)?; - let (event_sender, event_receiver) = unbounded(); - let (response_sender, response_receiver) = unbounded(); - - Ok(Vcpu { - fd: kvm_vcpu, - id, - mmio_bus: None, - exit_evt, - mpidr: 0, - event_receiver, - event_sender: Some(event_sender), - response_receiver: Some(response_receiver), - response_sender, - }) - } - - /// Constructs a new VCPU for `vm`. - /// - /// # Arguments - /// - /// * `id` - Represents the CPU number between [0, max vcpus). - /// * `vm_fd` - The kvm `VmFd` for the virtual machine this vcpu will get attached to. - /// * `exit_evt` - An `EventFd` that will be written into when this vcpu exits. - /// * `create_ts` - A timestamp used by the vcpu to calculate its lifetime. - #[cfg(target_arch = "riscv64")] - pub fn new_riscv64(id: u8, vm_fd: &VmFd, exit_evt: EventFd) -> Result { - let kvm_vcpu = vm_fd.create_vcpu(id as u64).map_err(Error::VcpuFd)?; - let (event_sender, event_receiver) = unbounded(); - let (response_sender, response_receiver) = unbounded(); - - Ok(Vcpu { - fd: kvm_vcpu, - id, - mmio_bus: None, - exit_evt, - event_receiver, - event_sender: Some(event_sender), - response_receiver: Some(response_receiver), - response_sender, - }) - } - - /// Returns the cpu index as seen by the guest OS. - pub fn cpu_index(&self) -> u8 { - self.id - } - - /// Gets the MPIDR register value. - #[cfg(target_arch = "aarch64")] - pub fn get_mpidr(&self) -> u64 { - self.mpidr - } - - /// Sets a MMIO bus for this vcpu. - pub fn set_mmio_bus(&mut self, mmio_bus: devices::Bus) { - self.mmio_bus = Some(mmio_bus); - } - - #[cfg(target_arch = "x86_64")] - #[allow(unused_variables)] - /// Configures a x86_64 specific vcpu and should be called once per vcpu. - /// - /// # Arguments - /// - /// * `machine_config` - The machine configuration of this microvm needed for the CPUID configuration. - /// * `guest_mem` - The guest memory used by this microvm. - /// * `kernel_start_addr` - Offset from `guest_mem` at which the kernel starts. - pub fn configure_x86_64( - &mut self, - guest_mem: &GuestMemoryMmap, - kernel_start_addr: GuestAddress, - vcpu_config: &VcpuConfig, - kernel_boot: bool, - ) -> Result<()> { - let cpuid_vm_spec = VmSpec::new( - self.id, - vcpu_config.vcpu_count, - vcpu_config.ht_enabled, - vcpu_config.nested_enabled, - ) - .map_err(Error::CpuId)?; - - filter_cpuid(&mut self.cpuid, &cpuid_vm_spec).map_err(|e| { - error!("Failure in configuring CPUID for vcpu {}: {:?}", self.id, e); - Error::CpuId(e) - })?; - - if let Some(template) = vcpu_config.cpu_template { - match template { - CpuFeaturesTemplate::T2 => { - t2::set_cpuid_entries(&mut self.cpuid, &cpuid_vm_spec).map_err(Error::CpuId)? - } - CpuFeaturesTemplate::C3 => { - c3::set_cpuid_entries(&mut self.cpuid, &cpuid_vm_spec).map_err(Error::CpuId)? - } - } - } - - self.fd - .set_cpuid2(&self.cpuid) - .map_err(Error::VcpuSetCpuid)?; - - if kernel_boot { - arch::x86_64::msr::setup_msrs(&self.fd).map_err(Error::MSRSConfiguration)?; - arch::x86_64::regs::setup_regs(&self.fd, kernel_start_addr.raw_value(), self.id) - .map_err(Error::REGSConfiguration)?; - arch::x86_64::regs::setup_fpu(&self.fd).map_err(Error::FPUConfiguration)?; - arch::x86_64::regs::setup_sregs(guest_mem, &self.fd, self.id) - .map_err(Error::SREGSConfiguration)?; - arch::x86_64::interrupts::set_lint(&self.fd).map_err(Error::LocalIntConfiguration)?; - } - Ok(()) - } - - #[cfg(target_arch = "aarch64")] - /// Configures an aarch64 specific vcpu. - /// - /// # Arguments - /// - /// * `vm_fd` - The kvm `VmFd` for this microvm. - /// * `guest_mem` - The guest memory used by this microvm. - /// * `kernel_load_addr` - Offset from `guest_mem` at which the kernel is loaded. - pub fn configure_aarch64( - &mut self, - vm_fd: &VmFd, - mem_info: &ArchMemoryInfo, - kernel_load_addr: GuestAddress, - ) -> Result<()> { - let mut kvi: kvm_bindings::kvm_vcpu_init = kvm_bindings::kvm_vcpu_init::default(); - - // This reads back the kernel's preferred target type. - vm_fd - .get_preferred_target(&mut kvi) - .map_err(Error::VcpuArmPreferredTarget)?; - // We already checked that the capability is supported. - kvi.features[0] |= 1 << kvm_bindings::KVM_ARM_VCPU_PSCI_0_2; - // Non-boot cpus are powered off initially. - if self.id > 0 { - kvi.features[0] |= 1 << kvm_bindings::KVM_ARM_VCPU_POWER_OFF; - } - - if vm_fd.check_extension(kvm_ioctls::Cap::ArmPtrAuthAddress) { - kvi.features[0] |= 1 << kvm_bindings::KVM_ARM_VCPU_PTRAUTH_ADDRESS; - } - if vm_fd.check_extension(kvm_ioctls::Cap::ArmPtrAuthGeneric) { - kvi.features[0] |= 1 << kvm_bindings::KVM_ARM_VCPU_PTRAUTH_GENERIC; - } - - self.fd.vcpu_init(&kvi).map_err(Error::VcpuArmInit)?; - arch::aarch64::regs::setup_regs(&self.fd, self.id, kernel_load_addr.raw_value(), mem_info) - .map_err(Error::REGSConfiguration)?; - - self.mpidr = arch::aarch64::regs::read_mpidr(&self.fd).map_err(Error::REGSConfiguration)?; - - Ok(()) - } - - #[cfg(target_arch = "riscv64")] - /// Configures an riscv64 specific vcpu. - /// - /// # Arguments - /// - /// * `vm_fd` - The kvm `VmFd` for this microvm. - /// * `guest_mem` - The guest memory used by this microvm. - /// * `kernel_load_addr` - Offset from `guest_mem` at which the kernel is loaded. - pub fn configure_riscv64( - &mut self, - _vm_fd: &VmFd, - guest_mem: &GuestMemoryMmap, - kernel_load_addr: GuestAddress, - ) -> Result<()> { - arch::riscv64::regs::setup_regs(&self.fd, self.id, kernel_load_addr.raw_value(), guest_mem) - .map_err(Error::REGSConfiguration)?; - Ok(()) - } - - /// Moves the vcpu to its own thread and constructs a VcpuHandle. - /// The handle can be used to control the remote vcpu. - pub fn start_threaded(mut self) -> Result { - let event_sender = self.event_sender.take().unwrap(); - let response_receiver = self.response_receiver.take().unwrap(); - let (init_tls_sender, init_tls_receiver) = unbounded(); - let vcpu_thread = thread::Builder::new() - .name(format!("fc_vcpu {}", self.cpu_index())) - .spawn(move || { - self.init_thread_local_data() - .expect("Cannot cleanly initialize vcpu TLS."); - - init_tls_sender - .send(true) - .expect("Cannot notify vcpu TLS initialization."); - - self.run(); - }) - .map_err(Error::VcpuSpawn)?; - - init_tls_receiver - .recv() - .expect("Error waiting for TLS initialization."); - - Ok(VcpuHandle::new( - event_sender, - response_receiver, - vcpu_thread, - )) - } - - #[allow(unused)] - #[cfg(target_arch = "x86_64")] - fn save_state(&self) -> Result { - /* - * Ordering requirements: - * - * KVM_GET_MP_STATE calls kvm_apic_accept_events(), which might modify - * vCPU/LAPIC state. As such, it must be done before most everything - * else, otherwise we cannot restore everything and expect it to work. - * - * KVM_GET_VCPU_EVENTS/KVM_SET_VCPU_EVENTS is unsafe if other vCPUs are - * still running. - * - * KVM_GET_LAPIC may change state of LAPIC before returning it. - * - * GET_VCPU_EVENTS should probably be last to save. The code looks as - * it might as well be affected by internal state modifications of the - * GET ioctls. - * - * SREGS saves/restores a pending interrupt, similar to what - * VCPU_EVENTS also does. - * - * GET_MSRS requires a pre-populated data structure to do something - * meaningful. For SET_MSRS it will then contain good data. - */ - - // Build the list of MSRs we want to save. - let num_msrs = self.msr_list.as_fam_struct_ref().nmsrs as usize; - let mut msrs = Msrs::new(num_msrs).unwrap(); - { - let indices = self.msr_list.as_slice(); - let msr_entries = msrs.as_mut_slice(); - assert_eq!(indices.len(), msr_entries.len()); - for (pos, index) in indices.iter().enumerate() { - msr_entries[pos].index = *index; - } - } - let mp_state = self.fd.get_mp_state().map_err(Error::VcpuGetMpState)?; - let regs = self.fd.get_regs().map_err(Error::VcpuGetRegs)?; - let sregs = self.fd.get_sregs().map_err(Error::VcpuGetSregs)?; - let xsave = self.fd.get_xsave().map_err(Error::VcpuGetXsave)?; - let xcrs = self.fd.get_xcrs().map_err(Error::VcpuGetXcrs)?; - let debug_regs = self.fd.get_debug_regs().map_err(Error::VcpuGetDebugRegs)?; - let lapic = self.fd.get_lapic().map_err(Error::VcpuGetLapic)?; - let nmsrs = self.fd.get_msrs(&mut msrs).map_err(Error::VcpuGetMsrs)?; - assert_eq!(nmsrs, num_msrs); - let vcpu_events = self - .fd - .get_vcpu_events() - .map_err(Error::VcpuGetVcpuEvents)?; - Ok(VcpuState { - cpuid: self.cpuid.clone(), - msrs, - debug_regs, - lapic, - mp_state, - regs, - sregs, - vcpu_events, - xcrs, - xsave, - }) - } - - #[allow(unused)] - #[cfg(target_arch = "x86_64")] - fn restore_state(&self, state: VcpuState) -> Result<()> { - /* - * Ordering requirements: - * - * KVM_GET_VCPU_EVENTS/KVM_SET_VCPU_EVENTS is unsafe if other vCPUs are - * still running. - * - * Some SET ioctls (like set_mp_state) depend on kvm_vcpu_is_bsp(), so - * if we ever change the BSP, we have to do that before restoring anything. - * The same seems to be true for CPUID stuff. - * - * SREGS saves/restores a pending interrupt, similar to what - * VCPU_EVENTS also does. - * - * SET_REGS clears pending exceptions unconditionally, thus, it must be - * done before SET_VCPU_EVENTS, which restores it. - * - * SET_LAPIC must come after SET_SREGS, because the latter restores - * the apic base msr. - * - * SET_LAPIC must come before SET_MSRS, because the TSC deadline MSR - * only restores successfully, when the LAPIC is correctly configured. - */ - self.fd - .set_cpuid2(&state.cpuid) - .map_err(Error::VcpuSetCpuid)?; - self.fd - .set_mp_state(state.mp_state) - .map_err(Error::VcpuSetMpState)?; - self.fd.set_regs(&state.regs).map_err(Error::VcpuSetRegs)?; - self.fd - .set_sregs(&state.sregs) - .map_err(Error::VcpuSetSregs)?; - unsafe { - self.fd - .set_xsave(&state.xsave) - .map_err(Error::VcpuSetXsave)?; - } - self.fd.set_xcrs(&state.xcrs).map_err(Error::VcpuSetXcrs)?; - self.fd - .set_debug_regs(&state.debug_regs) - .map_err(Error::VcpuSetDebugRegs)?; - self.fd - .set_lapic(&state.lapic) - .map_err(Error::VcpuSetLapic)?; - self.fd.set_msrs(&state.msrs).map_err(Error::VcpuSetMsrs)?; - self.fd - .set_vcpu_events(&state.vcpu_events) - .map_err(Error::VcpuSetVcpuEvents)?; - Ok(()) - } - - /// Runs the vCPU in KVM context and handles the kvm exit reason. - /// - /// Returns error or enum specifying whether emulation was handled or interrupted. - fn run_emulation(&mut self) -> Result { - // This is a workaround for a kernel bug in the Linux - // kernel (6.12 and 6.13). - // https://github.com/containers/libkrun/issues/314#issuecomment-2818154193 - #[cfg(target_arch = "x86_64")] - { - if self.kernel_enomem_workaround { - thread::sleep(Duration::from_millis(5)); - } - } - - match self.fd.run() { - Ok(run) => match run { - #[cfg(feature = "tee")] - VcpuExit::Hypercall(hypercall) => { - if hypercall.nr != 12 - /* KVM_HC_MAP_GPA_RANGE */ - { - return Err(Error::VcpuUnsupportedHypercall); - } - - let gpa = hypercall.args[0]; - let size = hypercall.args[1] * 0x1000; /* TARGET_PAGE_SIZE */ - let attributes = hypercall.args[2]; - - let private = !matches!(attributes, 0); - - let mem_properties = MemoryProperties { gpa, size, private }; - - let (response_sender, response_receiver) = unbounded(); - self.pm_sender - .send(WorkerMessage::ConvertMemory( - response_sender.clone(), - mem_properties, - )) - .unwrap(); - if !response_receiver.recv().unwrap() { - error!("Unable to convert memory with properties: gpa: 0x{gpa:x} size: 0x{size:x} to_private: {private}"); - return Err(Error::VcpuUnhandledKvmExit); - } - Ok(VcpuEmulation::Handled) - } - #[cfg(target_arch = "x86_64")] - VcpuExit::IoIn(addr, data) => { - self.io_bus.read(0, u64::from(addr), data); - Ok(VcpuEmulation::Handled) - } - #[cfg(target_arch = "x86_64")] - VcpuExit::IoOut(addr, data) => { - self.io_bus.write(0, u64::from(addr), data); - Ok(VcpuEmulation::Handled) - } - #[cfg(feature = "tee")] - VcpuExit::MemoryFault { gpa, size, flags } => { - if flags & !kvm_bindings::KVM_MEMORY_EXIT_FLAG_PRIVATE as u64 != 0 { - println!("KVM_EXIT_MEMORY_FAULT: Unknown flag {flags}"); - Err(Error::VcpuUnhandledKvmExit) - } else { - let private = (flags & (KVM_MEMORY_EXIT_FLAG_PRIVATE as u64)) != 0; - let mem_properties = MemoryProperties { gpa, size, private }; - let (response_sender, response_receiver) = unbounded(); - self.pm_sender - .send(WorkerMessage::ConvertMemory( - response_sender.clone(), - mem_properties, - )) - .unwrap(); - if !response_receiver.recv().unwrap() { - error!("Unable to convert memory with properties: gpa: 0x{gpa:x} size: 0x{size:x} to_private: {private}"); - return Err(Error::VcpuUnhandledKvmExit); - } - Ok(VcpuEmulation::Handled) - } - } - VcpuExit::MmioRead(addr, data) => { - if let Some(ref mmio_bus) = self.mmio_bus { - mmio_bus.read(0, addr, data); - } - Ok(VcpuEmulation::Handled) - } - VcpuExit::MmioWrite(addr, data) => { - if let Some(ref mmio_bus) = self.mmio_bus { - mmio_bus.write(0, addr, data); - } - Ok(VcpuEmulation::Handled) - } - VcpuExit::Hlt => { - info!("Received KVM_EXIT_HLT signal"); - Ok(VcpuEmulation::Stopped) - } - VcpuExit::Shutdown => { - info!("Received KVM_EXIT_SHUTDOWN signal"); - Ok(VcpuEmulation::Stopped) - } - // Documentation specifies that below kvm exits are considered - // errors. - VcpuExit::FailEntry(reason, vcpu) => { - error!("Received KVM_EXIT_FAIL_ENTRY signal: reason={reason}, vcpu={vcpu}"); - Err(Error::VcpuUnhandledKvmExit) - } - VcpuExit::InternalError => { - error!("Received KVM_EXIT_INTERNAL_ERROR signal"); - Err(Error::VcpuUnhandledKvmExit) - } - VcpuExit::SystemEvent(event, _reason) => { - match event { - KVM_SYSTEM_EVENT_SHUTDOWN => info!("Received KVM_SYSTEM_EVENT_SHUTDOWN"), - KVM_SYSTEM_EVENT_RESET => info!("Received KVM_SYSTEM_EVENT_RESET"), - _ => error!("Received an unexpected System Event: {event}"), - } - Ok(VcpuEmulation::Stopped) - } - r => { - // TODO: Are we sure we want to finish running a vcpu upon - // receiving a vm exit that is not necessarily an error? - error!("Unexpected exit reason on vcpu run: {r:?}"); - Err(Error::VcpuUnhandledKvmExit) - } - }, - // The unwrap on raw_os_error can only fail if we have a logic - // error in our code in which case it is better to panic. - Err(ref e) => { - match e.errno() { - libc::EAGAIN => Ok(VcpuEmulation::Handled), - libc::EINTR => { - self.fd.set_kvm_immediate_exit(0); - // Notify that this KVM_RUN was interrupted. - Ok(VcpuEmulation::Interrupted) - } - _ => { - error!("Failure during vcpu run: {e}"); - Err(Error::VcpuUnhandledKvmExit) - } - } - } - } - } - - /// Main loop of the vCPU thread. - /// - /// Runs the vCPU in KVM context in a loop. Handles KVM_EXITs then goes back in. - /// Note that the state of the VCPU and associated VM must be setup first for this to do - /// anything useful. - pub fn run(&mut self) { - // Start running the machine state in the `Paused` state. - StateMachine::run(self, Self::paused); - } - - // This is the main loop of the `Running` state. - fn running(&mut self) -> StateMachine { - // This loop is here just for optimizing the emulation path. - // No point in ticking the state machine if there are no external events. - loop { - match self.run_emulation() { - // Emulation ran successfully, continue. - Ok(VcpuEmulation::Handled) => (), - // Emulation was interrupted, check external events. - Ok(VcpuEmulation::Interrupted) => break, - // If the guest was rebooted or halted: - // - vCPU0 will always exit out of `KVM_RUN` with KVM_EXIT_SHUTDOWN or - // KVM_EXIT_HLT. - // - the other vCPUs won't ever exit out of `KVM_RUN`, but they won't consume CPU. - // Moreover if we allow the vCPU0 thread to finish execution, this might generate a - // seccomp failure because musl calls `sigprocmask` as part of `pthread_exit`. - // So we pause vCPU0 and send a signal to the emulation thread to stop the VMM. - Ok(VcpuEmulation::Stopped) => return self.exit(FC_EXIT_CODE_OK), - // Emulation errors lead to vCPU exit. - Err(_) => return self.exit(FC_EXIT_CODE_GENERIC_ERROR), - } - } - - // By default don't change state. - let mut state = StateMachine::next(Self::running); - - // Break this emulation loop on any transition request/external event. - match self.event_receiver.try_recv() { - // Running ---- Pause ----> Paused - Ok(VcpuEvent::Pause) => { - // Nothing special to do. - self.response_sender - .send(VcpuResponse::Paused) - .expect("failed to send pause status"); - - // TODO: we should call `KVM_KVMCLOCK_CTRL` here to make sure - // TODO continued: the guest soft lockup watchdog does not panic on Resume. - - // Move to 'paused' state. - state = StateMachine::next(Self::paused); - } - Ok(VcpuEvent::Resume) => { - self.response_sender - .send(VcpuResponse::Resumed) - .expect("failed to send resume status"); - } - // Unhandled exit of the other end. - Err(TryRecvError::Disconnected) => { - // Move to 'exited' state. - state = self.exit(FC_EXIT_CODE_GENERIC_ERROR); - } - // All other events or lack thereof have no effect on current 'running' state. - Err(TryRecvError::Empty) => (), - } - - state - } - - // This is the main loop of the `Paused` state. - fn paused(&mut self) -> StateMachine { - match self.event_receiver.recv() { - // Paused ---- Resume ----> Running - Ok(VcpuEvent::Resume) => { - // Nothing special to do. - self.response_sender - .send(VcpuResponse::Resumed) - .expect("failed to send resume status"); - // Move to 'running' state. - StateMachine::next(Self::running) - } - // All other events have no effect on current 'paused' state. - Ok(_) => StateMachine::next(Self::paused), - // Unhandled exit of the other end. - Err(_) => { - // Move to 'exited' state. - self.exit(FC_EXIT_CODE_GENERIC_ERROR) - } - } - } - - #[cfg(not(test))] - // Transition to the exited state. - fn exit(&mut self, exit_code: u8) -> StateMachine { - self.response_sender - .send(VcpuResponse::Exited(exit_code)) - .expect("failed to send Exited status"); - - if let Err(e) = self.exit_evt.write(1) { - error!("Failed signaling vcpu exit event: {e}"); - } - - // State machine reached its end. - StateMachine::next(Self::exited) - } - - #[cfg(not(test))] - // This is the main loop of the `Exited` state. - fn exited(&mut self) -> StateMachine { - // Wait indefinitely. - // The VMM thread will kill the entire process. - let barrier = Barrier::new(2); - barrier.wait(); - - StateMachine::finish() - } - - #[cfg(feature = "tdx")] - pub fn tdx_secure_virt_prepare(&self, launcher: &mut tdx::launch::Launcher) { - use std::os::fd::AsRawFd; - launcher.add_vcpu_fd(self.fd.as_raw_fd()); - } - - #[cfg(test)] - // In tests the main/vmm thread exits without 'exit()'ing the whole process. - // All channels get closed on the other side while this Vcpu thread is still running. - // This Vcpu thread should just do a clean finish without reporting back to the main thread. - fn exit(&mut self, _: u8) -> StateMachine { - // State machine reached its end. - StateMachine::finish() - } -} - -impl Drop for Vcpu { - fn drop(&mut self) { - let _ = self.reset_thread_local_data(); - } -} - -#[cfg(target_arch = "x86_64")] -/// Structure holding VCPU kvm state. -pub struct VcpuState { - cpuid: CpuId, - msrs: Msrs, - debug_regs: kvm_debugregs, - lapic: kvm_lapic_state, - mp_state: kvm_mp_state, - regs: kvm_regs, - sregs: kvm_sregs, - vcpu_events: kvm_vcpu_events, - xcrs: kvm_xcrs, - xsave: kvm_xsave, -} - -// Allow currently unused Pause and Exit events. These will be used by the vmm later on. -#[allow(unused)] -#[derive(Debug)] -/// List of events that the Vcpu can receive. -pub enum VcpuEvent { - /// Pause the Vcpu. - Pause, - /// Event that should resume the Vcpu. - Resume, - // Serialize and Deserialize to follow after we get the support from kvm-ioctls. -} - -#[derive(Debug, Eq, PartialEq)] -/// List of responses that the Vcpu reports. -pub enum VcpuResponse { - /// Vcpu is paused. - Paused, - /// Vcpu is resumed. - Resumed, - /// Vcpu is stopped. - Exited(u8), -} - -/// Wrapper over Vcpu that hides the underlying interactions with the Vcpu thread. -pub struct VcpuHandle { - event_sender: Sender, - response_receiver: Receiver, - // Rust JoinHandles have to be wrapped in Option if you ever plan on 'join()'ing them. - // We want to be able to join these threads in tests. - vcpu_thread: Option>, -} - -impl VcpuHandle { - pub fn new( - event_sender: Sender, - response_receiver: Receiver, - vcpu_thread: thread::JoinHandle<()>, - ) -> Self { - Self { - event_sender, - response_receiver, - vcpu_thread: Some(vcpu_thread), - } - } - - pub fn send_event(&self, event: VcpuEvent) -> Result<()> { - // Use expect() to crash if the other thread closed this channel. - self.event_sender - .send(event) - .expect("event sender channel closed on vcpu end."); - // Kick the vcpu so it picks up the message. - self.vcpu_thread - .as_ref() - // Safe to unwrap since constructor make this 'Some'. - .unwrap() - .kill(sigrtmin() + VCPU_RTSIG_OFFSET) - .map_err(Error::SignalVcpu)?; - Ok(()) - } - - pub fn response_receiver(&self) -> &Receiver { - &self.response_receiver - } -} - -enum VcpuEmulation { - Handled, - Interrupted, - Stopped, -} - -#[cfg(test)] -mod tests { - use crossbeam_channel::unbounded; - use std::sync::{Arc, Barrier}; - - use super::*; - #[cfg(target_arch = "aarch64")] - use crate::builder::create_guest_memory; - #[cfg(target_arch = "aarch64")] - use crate::builder::Payload; - #[cfg(target_arch = "aarch64")] - use crate::resources::VmResources; - use devices; - #[cfg(target_arch = "x86_64")] - use devices::legacy::KvmIoapic; - - use utils::signal::validate_signal_num; - - // In tests we need to close any pending Vcpu threads on test completion. - impl Drop for VcpuHandle { - fn drop(&mut self) { - // Make sure the Vcpu is out of KVM_RUN. - self.send_event(VcpuEvent::Pause).unwrap(); - // Close the original channel so that the Vcpu thread errors and goes to exit state. - let (event_sender, _event_receiver) = unbounded(); - self.event_sender = event_sender; - // Wait for the Vcpu thread to finish execution - self.vcpu_thread.take().unwrap().join().unwrap(); - } - } - - // Auxiliary function being used throughout the tests. - fn setup_vcpu(mem_size: usize) -> (Vm, Vcpu, GuestMemoryMmap) { - let kvm = KvmContext::new().unwrap(); - let gm = GuestMemoryMmap::from_ranges(&[(GuestAddress(0), mem_size)]).unwrap(); - let mut vm = Vm::new(kvm.fd()).expect("Cannot create new vm"); - #[cfg(target_arch = "x86_64")] - let _kvmioapic = KvmIoapic::new(&vm.fd()).unwrap(); - assert!(vm.memory_init(&gm, kvm.max_memslots()).is_ok()); - - let exit_evt = EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap(); - - let vcpu; - #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] - { - vcpu = Vcpu::new_x86_64( - 1, - vm.fd(), - vm.supported_cpuid().clone(), - vm.supported_msrs().clone(), - devices::Bus::new(), - exit_evt, - ) - .unwrap(); - } - #[cfg(target_arch = "aarch64")] - { - vcpu = Vcpu::new_aarch64(1, vm.fd(), exit_evt).unwrap(); - } - - (vm, vcpu, gm) - } - - #[test] - fn test_set_mmio_bus() { - let (_, mut vcpu, _) = setup_vcpu(0x1000); - assert!(vcpu.mmio_bus.is_none()); - vcpu.set_mmio_bus(devices::Bus::new()); - assert!(vcpu.mmio_bus.is_some()); - } - - #[ignore] - #[test] - #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] - fn test_get_supported_cpuid() { - let kvm = KvmContext::new().unwrap(); - let vm = Vm::new(kvm.fd()).expect("Cannot create new vm"); - let cpuid = kvm - .kvm - .get_supported_cpuid(KVM_MAX_CPUID_ENTRIES) - .expect("Cannot get supported cpuid"); - assert_eq!(vm.supported_cpuid().as_slice(), cpuid.as_slice()); - } - - #[test] - fn test_vm_memory_init() { - let mut kvm_context = KvmContext::new().unwrap(); - let mut vm = Vm::new(kvm_context.fd()).expect("Cannot create new vm"); - - // Create valid memory region and test that the initialization is successful. - let gm = GuestMemoryMmap::from_ranges(&[(GuestAddress(0), 0x1000)]).unwrap(); - assert!(vm.memory_init(&gm, kvm_context.max_memslots()).is_ok()); - - // Set the maximum number of memory slots to 1 in KvmContext to check the error - // path of memory_init. Create 2 non-overlapping memory slots. - kvm_context.max_memslots = 1; - let gm = GuestMemoryMmap::from_ranges(&[ - (GuestAddress(0x0), 0x1000), - (GuestAddress(0x1001), 0x2000), - ]) - .unwrap(); - assert!(vm.memory_init(&gm, kvm_context.max_memslots()).is_err()); - } - - #[cfg(target_arch = "x86_64")] - #[test] - fn test_configure_vcpu() { - let (_vm, mut vcpu, vm_mem) = setup_vcpu(0x10000); - - let mut vcpu_config = VcpuConfig { - vcpu_count: 1, - ht_enabled: false, - cpu_template: None, - nested_enabled: false, - }; - - assert!(vcpu - .configure_x86_64(&vm_mem, GuestAddress(0), &vcpu_config, true) - .is_ok()); - - // Test configure while using the T2 template. - vcpu_config.cpu_template = Some(CpuFeaturesTemplate::T2); - assert!(vcpu - .configure_x86_64(&vm_mem, GuestAddress(0), &vcpu_config, true) - .is_ok()); - - // Test configure while using the C3 template. - vcpu_config.cpu_template = Some(CpuFeaturesTemplate::C3); - assert!(vcpu - .configure_x86_64(&vm_mem, GuestAddress(0), &vcpu_config, true) - .is_ok()); - } - - #[cfg(target_arch = "aarch64")] - #[test] - fn test_configure_vcpu() { - let kvm = KvmContext::new().unwrap(); - let vm_resources = VmResources::default(); - let (guest_memory, arch_memory_info, _shm_manager, _payload_config) = - create_guest_memory(128, &vm_resources, &Payload::Empty).unwrap(); - let mut vm = Vm::new(kvm.fd()).expect("new vm failed"); - assert!(vm.memory_init(&guest_memory, kvm.max_memslots()).is_ok()); - - // Try it for when vcpu id is 0. - let mut vcpu = Vcpu::new_aarch64( - 0, - vm.fd(), - EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap(), - ) - .unwrap(); - - assert!(vcpu - .configure_aarch64(vm.fd(), &arch_memory_info, GuestAddress(0)) - .is_ok()); - - // Try it for when vcpu id is NOT 0. - let mut vcpu = Vcpu::new_aarch64( - 1, - vm.fd(), - EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap(), - ) - .unwrap(); - - assert!(vcpu - .configure_aarch64(vm.fd(), &arch_memory_info, GuestAddress(0)) - .is_ok()); - } - - #[test] - fn test_vcpu_tls() { - let (_, mut vcpu, _) = setup_vcpu(0x1000); - - // Running on the TLS vcpu should fail before we actually initialize it. - unsafe { - assert!(Vcpu::run_on_thread_local(|_| ()).is_err()); - } - - // Initialize vcpu TLS. - vcpu.init_thread_local_data().unwrap(); - - // Validate TLS vcpu is the local vcpu by changing the `id` then validating against - // the one in TLS. - vcpu.id = 12; - unsafe { - assert!(Vcpu::run_on_thread_local(|v| assert_eq!(v.id, 12)).is_ok()); - } - - // Reset vcpu TLS. - assert!(vcpu.reset_thread_local_data().is_ok()); - - // Running on the TLS vcpu after TLS reset should fail. - unsafe { - assert!(Vcpu::run_on_thread_local(|_| ()).is_err()); - } - - // Second reset should return error. - assert!(vcpu.reset_thread_local_data().is_err()); - } - - #[test] - fn test_invalid_tls() { - let (_, mut vcpu, _) = setup_vcpu(0x1000); - // Initialize vcpu TLS. - vcpu.init_thread_local_data().unwrap(); - // Trying to initialize non-empty TLS should error. - vcpu.init_thread_local_data().unwrap_err(); - } - - #[test] - fn test_vcpu_kick() { - Vcpu::register_kick_signal_handler(); - let (vm, mut vcpu, _mem) = setup_vcpu(0x1000); - - let mut kvm_run = - KvmRunWrapper::mmap_from_fd(&vcpu.fd, vm.fd.run_size()).expect("cannot mmap kvm-run"); - let success = Arc::new(std::sync::atomic::AtomicBool::new(false)); - let vcpu_success = success.clone(); - let barrier = Arc::new(Barrier::new(2)); - let vcpu_barrier = barrier.clone(); - // Start Vcpu thread which will be kicked with a signal. - let handle = std::thread::Builder::new() - .name("test_vcpu_kick".to_string()) - .spawn(move || { - vcpu.init_thread_local_data().unwrap(); - // Notify TLS was populated. - vcpu_barrier.wait(); - // Loop for max 1 second to check if the signal handler has run. - for _ in 0..10 { - if kvm_run.as_mut_ref().immediate_exit == 1 { - // Signal handler has run and set immediate_exit to 1. - vcpu_success.store(true, Ordering::Release); - break; - } - std::thread::sleep(std::time::Duration::from_millis(100)); - } - }) - .expect("cannot start thread"); - - // Wait for the vcpu to initialize its TLS. - barrier.wait(); - // Kick the Vcpu using the custom signal. - handle - .kill(sigrtmin() + VCPU_RTSIG_OFFSET) - .expect("failed to signal thread"); - handle.join().expect("failed to join thread"); - // Verify that the Vcpu saw its kvm immediate-exit as set. - assert!(success.load(Ordering::Acquire)); - } - - #[test] - fn test_vcpu_rtsig_offset() { - assert!(validate_signal_num(sigrtmin() + VCPU_RTSIG_OFFSET).is_ok()); - } -} diff --git a/vendor/krun-vmm/src/macos/mod.rs b/vendor/krun-vmm/src/macos/mod.rs deleted file mode 100644 index ab962865c..000000000 --- a/vendor/krun-vmm/src/macos/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod vstate; diff --git a/vendor/krun-vmm/src/macos/vstate.rs b/vendor/krun-vmm/src/macos/vstate.rs deleted file mode 100644 index 92db6496b..000000000 --- a/vendor/krun-vmm/src/macos/vstate.rs +++ /dev/null @@ -1,731 +0,0 @@ -// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -// -// Portions Copyright 2017 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the THIRD-PARTY file. - -use std::cell::Cell; -use std::collections::HashMap; -use std::fmt::{Display, Formatter}; -use std::io; -use std::result; -use std::sync::Arc; -use std::thread; -use std::time::Duration; - -use super::super::{FC_EXIT_CODE_GENERIC_ERROR, FC_EXIT_CODE_OK}; -use crate::vmm_config::machine_config::CpuFeaturesTemplate; - -use arch::ArchMemoryInfo; -use crossbeam_channel::{unbounded, Receiver, RecvTimeoutError, Sender}; -use devices::legacy::VcpuList; -use hvf::{HvfVcpu, HvfVm, VcpuExit, Vcpus}; -use utils::eventfd::EventFd; -use vm_memory::{ - Address, GuestAddress, GuestMemory, GuestMemoryError, GuestMemoryMmap, GuestMemoryRegion, -}; - -/// Errors associated with the wrappers over KVM ioctls. -#[derive(Debug)] -pub enum Error { - /// Invalid guest memory configuration. - GuestMemoryMmap(GuestMemoryError), - /// The number of configured slots is bigger than the maximum reported by KVM. - NotEnoughMemorySlots, - /// Error configuring the general purpose aarch64 registers. - REGSConfiguration(arch::aarch64::regs::Error), - /// Cannot set the memory regions. - SetUserMemoryRegion(hvf::Error), - /// Failed to signal Vcpu. - SignalVcpu(utils::errno::Error), - /// Error doing Vcpu Init on Arm. - VcpuArmInit, - /// Error getting the Vcpu preferred target on Arm. - VcpuArmPreferredTarget, - /// vCPU count is not initialized. - VcpuCountNotInitialized, - /// Cannot run the VCPUs. - VcpuRun, - /// Cannot spawn a new vCPU thread. - VcpuSpawn(io::Error), - /// Cannot cleanly initialize vcpu TLS. - VcpuTlsInit, - /// Vcpu not present in TLS. - VcpuTlsNotPresent, - /// Unexpected KVM_RUN exit reason - VcpuUnhandledKvmExit, - /// Cannot configure the microvm. - VmSetup(hvf::Error), -} - -impl Display for Error { - fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { - use self::Error::*; - - match self { - GuestMemoryMmap(e) => write!(f, "Guest memory error: {e:?}"), - VcpuCountNotInitialized => write!(f, "vCPU count is not initialized"), - VmSetup(e) => write!(f, "Cannot configure the microvm: {e:?}"), - VcpuRun => write!(f, "Cannot run the VCPUs"), - NotEnoughMemorySlots => write!( - f, - "The number of configured slots is bigger than the maximum reported by KVM" - ), - SetUserMemoryRegion(e) => write!(f, "Cannot set the memory regions: {e:?}"), - SignalVcpu(e) => write!(f, "Failed to signal Vcpu: {e}"), - REGSConfiguration(e) => write!( - f, - "Error configuring the general purpose aarch64 registers: {e:?}" - ), - VcpuSpawn(e) => write!(f, "Cannot spawn a new vCPU thread: {e}"), - VcpuTlsInit => write!(f, "Cannot clean init vcpu TLS"), - VcpuTlsNotPresent => write!(f, "Vcpu not present in TLS"), - VcpuUnhandledKvmExit => write!(f, "Unexpected KVM_RUN exit reason"), - VcpuArmPreferredTarget => write!(f, "Error getting the Vcpu preferred target on Arm"), - VcpuArmInit => write!(f, "Error doing Vcpu Init on Arm"), - } - } -} - -pub type Result = result::Result; - -/// A wrapper around creating and using a VM. -pub struct Vm { - hvf_vm: HvfVm, -} - -impl Vm { - /// Constructs a new `Vm` using the given `Kvm` instance. - pub fn new(nested_enabled: bool) -> Result { - let hvf_vm = HvfVm::new(nested_enabled).map_err(Error::VmSetup)?; - - Ok(Vm { hvf_vm }) - } - - pub fn hvf_vm(&self) -> &HvfVm { - &self.hvf_vm - } - - /// Initializes the guest memory. - pub fn memory_init(&mut self, guest_mem: &GuestMemoryMmap) -> Result<()> { - for region in guest_mem.iter() { - // It's safe to unwrap because the guest address is valid. - let host_addr = guest_mem.get_host_address(region.start_addr()).unwrap(); - debug!( - "Guest memory host_addr={:x?} guest_addr={:x?} len={:x?}", - host_addr, - region.start_addr().raw_value(), - region.len() - ); - self.hvf_vm - .map_memory( - host_addr as u64, - region.start_addr().raw_value(), - region.len(), - ) - .map_err(Error::SetUserMemoryRegion)?; - } - - Ok(()) - } - - pub fn add_mapping( - &self, - reply_sender: Sender, - host_addr: u64, - guest_addr: u64, - len: u64, - ) { - debug!("add_mapping: host_addr={host_addr:x}, guest_addr={guest_addr:x}, len={len}"); - if let Err(e) = self.hvf_vm.unmap_memory(guest_addr, len) { - error!("Error removing memory map: {e:?}"); - } - - if let Err(e) = self.hvf_vm.map_memory(host_addr, guest_addr, len) { - error!("Error adding memory map: {e:?}"); - reply_sender.send(false).unwrap(); - } else { - reply_sender.send(true).unwrap(); - } - } - - pub fn remove_mapping(&self, reply_sender: Sender, guest_addr: u64, len: u64) { - debug!("remove_mapping: guest_addr={guest_addr:x}, len={len}"); - if let Err(e) = self.hvf_vm.unmap_memory(guest_addr, len) { - error!("Error removing memory map: {e:?}"); - reply_sender.send(false).unwrap(); - } else { - reply_sender.send(true).unwrap(); - } - } -} - -/// Encapsulates configuration parameters for the guest vCPUS. -#[derive(Debug, Eq, PartialEq)] -pub struct VcpuConfig { - /// Number of guest VCPUs. - pub vcpu_count: u8, - /// Enable hyperthreading in the CPUID configuration. - pub ht_enabled: bool, - /// CPUID template to use. - pub cpu_template: Option, -} - -// Using this for easier explicit type-casting to help IDEs interpret the code. -type VcpuCell = Cell>; - -/// A wrapper around creating and using a kvm-based VCPU. -pub struct Vcpu { - id: u8, - boot_entry_addr: u64, - boot_receiver: Option>, - boot_senders: Option>>, - fdt_addr: u64, - mmio_bus: Option, - #[cfg_attr(all(test, target_arch = "aarch64"), allow(unused))] - exit_evt: EventFd, - - #[cfg(target_arch = "aarch64")] - mpidr: u64, - - #[allow(unused)] - event_receiver: Receiver, - // The transmitting end of the events channel which will be given to the handler. - event_sender: Option>, - // The receiving end of the responses channel which will be given to the handler. - response_receiver: Option>, - // The transmitting end of the responses channel owned by the vcpu side. - response_sender: Sender, - - vcpu_list: Arc, - nested_enabled: bool, -} - -impl Vcpu { - thread_local!(static TLS_VCPU_PTR: VcpuCell = const { Cell::new(None) }); - - /// Associates `self` with the current thread. - /// - /// It is a prerequisite to successfully run `init_thread_local_data()` before using - /// `run_on_thread_local()` on the current thread. - /// This function will return an error if there already is a `Vcpu` present in the TLS. - fn init_thread_local_data(&mut self) -> Result<()> { - Self::TLS_VCPU_PTR.with(|cell: &VcpuCell| { - if cell.get().is_some() { - return Err(Error::VcpuTlsInit); - } - cell.set(Some(self as *const Vcpu)); - Ok(()) - }) - } - - /// Deassociates `self` from the current thread. - /// - /// Should be called if the current `self` had called `init_thread_local_data()` and - /// now needs to move to a different thread. - /// - /// Fails if `self` was not previously associated with the current thread. - fn reset_thread_local_data(&mut self) -> Result<()> { - // Best-effort to clean up TLS. If the `Vcpu` was moved to another thread - // _before_ running this, then there is nothing we can do. - Self::TLS_VCPU_PTR.with(|cell: &VcpuCell| { - if let Some(vcpu_ptr) = cell.get() { - if std::ptr::eq(vcpu_ptr, self) { - Self::TLS_VCPU_PTR.with(|cell: &VcpuCell| cell.take()); - return Ok(()); - } - } - Err(Error::VcpuTlsNotPresent) - }) - } - - /// Registers a signal handler which makes use of TLS and kvm immediate exit to - /// kick the vcpu running on the current thread, if there is one. - pub fn register_kick_signal_handler() { - /* - extern "C" fn handle_signal(_: c_int, _: *mut siginfo_t, _: *mut c_void) { - // This is safe because it's temporarily aliasing the `Vcpu` object, but we are - // only reading `vcpu.fd` which does not change for the lifetime of the `Vcpu`. - unsafe { - let _ = Vcpu::run_on_thread_local(|_vcpu| { - vcpu.fd.set_kvm_immediate_exit(1); - fence(Ordering::Release); - }); - } - } - */ - - //register_signal_handler(sigrtmin() + VCPU_RTSIG_OFFSET, handle_signal) - // .expect("Failed to register vcpu signal handler"); - } - - /// Constructs a new VCPU for `vm`. - /// - /// # Arguments - /// - /// * `id` - Represents the CPU number between [0, max vcpus). - /// * `vm_fd` - The kvm `VmFd` for the virtual machine this vcpu will get attached to. - /// * `exit_evt` - An `EventFd` that will be written into when this vcpu exits. - pub fn new_aarch64( - id: u8, - boot_entry_addr: GuestAddress, - boot_receiver: Option>, - exit_evt: EventFd, - vcpu_list: Arc, - nested_enabled: bool, - ) -> Result { - let (event_sender, event_receiver) = unbounded(); - let (response_sender, response_receiver) = unbounded(); - - Ok(Vcpu { - id, - boot_entry_addr: boot_entry_addr.raw_value(), - boot_receiver, - boot_senders: None, - fdt_addr: 0, - mmio_bus: None, - exit_evt, - mpidr: id as u64, - event_receiver, - event_sender: Some(event_sender), - response_receiver: Some(response_receiver), - response_sender, - vcpu_list, - nested_enabled, - }) - } - - /// Returns the cpu index as seen by the guest OS. - pub fn cpu_index(&self) -> u8 { - self.id - } - - /// Gets the MPIDR register value. - pub fn get_mpidr(&self) -> u64 { - self.mpidr - } - - /// Sets a MMIO bus for this vcpu. - pub fn set_mmio_bus(&mut self, mmio_bus: devices::Bus) { - self.mmio_bus = Some(mmio_bus); - } - - pub fn set_boot_senders(&mut self, boot_senders: HashMap>) { - self.boot_senders = Some(boot_senders); - } - - /// Configures an aarch64 specific vcpu. - /// - /// # Arguments - /// - /// * `guest_mem` - The guest memory used by this microvm. - pub fn configure_aarch64(&mut self, mem_info: &ArchMemoryInfo) -> Result<()> { - self.fdt_addr = mem_info.fdt_addr; - - Ok(()) - } - - /// Moves the vcpu to its own thread and constructs a VcpuHandle. - /// The handle can be used to control the remote vcpu. - pub fn start_threaded(mut self) -> Result { - let event_sender = self.event_sender.take().unwrap(); - let response_receiver = self.response_receiver.take().unwrap(); - let (init_tls_sender, init_tls_receiver) = unbounded(); - - let vcpu_thread = thread::Builder::new() - .name(format!("fc_vcpu {}", self.cpu_index())) - .spawn(move || { - self.init_thread_local_data() - .expect("Cannot cleanly initialize vcpu TLS."); - - self.run(init_tls_sender); - }) - .map_err(Error::VcpuSpawn)?; - - init_tls_receiver - .recv() - .expect("Error waiting for TLS initialization."); - - Ok(VcpuHandle::new( - event_sender, - response_receiver, - vcpu_thread, - )) - } - - /// Returns error or enum specifying whether emulation was handled or interrupted. - fn run_emulation(&mut self, hvf_vcpu: &mut HvfVcpu) -> Result { - let vcpuid = hvf_vcpu.id(); - - match hvf_vcpu.run(self.vcpu_list.clone()) { - Ok(exit) => match exit { - VcpuExit::Breakpoint => { - debug!("vCPU {vcpuid} breakpoint"); - Ok(VcpuEmulation::Interrupted) - } - VcpuExit::Canceled => { - debug!("vCPU {vcpuid} canceled"); - Ok(VcpuEmulation::Handled) - } - VcpuExit::CpuOn(mpidr, entry, context_id) => { - debug!("CpuOn: mpidr=0x{mpidr:x} entry=0x{entry:x} context_id={context_id}"); - if let Some(boot_senders) = &self.boot_senders { - if let Some(sender) = boot_senders.get(&mpidr) { - sender.send(entry).unwrap() - } - } else { - error!("CpuOn request coming from an unexpected vCPU={}", self.id); - } - Ok(VcpuEmulation::Handled) - } - VcpuExit::HypervisorCall => { - debug!("vCPU {vcpuid} HVC"); - Ok(VcpuEmulation::Handled) - } - VcpuExit::MmioRead(addr, data) => { - if let Some(ref mmio_bus) = self.mmio_bus { - debug!("vCPU {vcpuid} MMIO read 0x{addr:x}"); - mmio_bus.read(vcpuid, addr, data); - } - Ok(VcpuEmulation::Handled) - } - VcpuExit::MmioWrite(addr, data) => { - if let Some(ref mmio_bus) = self.mmio_bus { - mmio_bus.write(vcpuid, addr, data); - } - Ok(VcpuEmulation::Handled) - } - VcpuExit::PsciHandled => { - debug!("vCPU {vcpuid} PSCI"); - Ok(VcpuEmulation::Handled) - } - VcpuExit::SecureMonitorCall => { - debug!("vCPU {vcpuid} SMC"); - Ok(VcpuEmulation::Handled) - } - VcpuExit::Shutdown => { - info!("vCPU {vcpuid} received shutdown signal"); - Ok(VcpuEmulation::Stopped) - } - VcpuExit::SystemRegister => { - debug!("vCPU {vcpuid} accessed a system register"); - Ok(VcpuEmulation::Handled) - } - VcpuExit::VtimerActivated => { - debug!("vCPU {vcpuid} VtimerActivated"); - self.vcpu_list.set_vtimer_irq(vcpuid); - Ok(VcpuEmulation::Handled) - } - VcpuExit::WaitForEvent => { - debug!("vCPU {vcpuid} WaitForEvent"); - Ok(VcpuEmulation::WaitForEvent) - } - VcpuExit::WaitForEventExpired => { - debug!("vCPU {vcpuid} WaitForEventExpired"); - Ok(VcpuEmulation::WaitForEventExpired) - } - VcpuExit::WaitForEventTimeout(duration) => { - debug!("vCPU {vcpuid} WaitForEventTimeout timeout={duration:?}"); - Ok(VcpuEmulation::WaitForEventTimeout(duration)) - } - }, - Err(e) => panic!("Error running HVF vCPU: {e:?}"), - } - } - - /// Main loop of the vCPU thread. - pub fn run(&mut self, init_tls_sender: Sender) { - let mut hvf_vcpu = - HvfVcpu::new(self.mpidr, self.nested_enabled).expect("Can't create HVF vCPU"); - let hvf_vcpuid = hvf_vcpu.id(); - - init_tls_sender - .send(true) - .expect("Cannot notify vcpu TLS initialization."); - - let (wfe_sender, wfe_receiver) = unbounded(); - self.vcpu_list.register(hvf_vcpuid, wfe_sender); - - let entry_addr = if let Some(boot_receiver) = &self.boot_receiver { - boot_receiver.recv().unwrap() - } else { - self.boot_entry_addr - }; - - hvf_vcpu - .set_initial_state(entry_addr, self.fdt_addr) - .unwrap_or_else(|_| panic!("Can't set HVF vCPU {hvf_vcpuid} initial state")); - - loop { - match self.run_emulation(&mut hvf_vcpu) { - // Emulation ran successfully, continue. - Ok(VcpuEmulation::Handled) => (), - // Emulation was interrupted by a breakpoint. - Ok(VcpuEmulation::Interrupted) => self.wait_for_resume(), - // Wait for an external event. - Ok(VcpuEmulation::WaitForEvent) => { - self.wait_for_event(hvf_vcpuid, &wfe_receiver, None) - } - Ok(VcpuEmulation::WaitForEventExpired) => (), - Ok(VcpuEmulation::WaitForEventTimeout(timeout)) => { - self.wait_for_event(hvf_vcpuid, &wfe_receiver, Some(timeout)) - } - // The guest was rebooted or halted. - Ok(VcpuEmulation::Stopped) => { - self.exit(FC_EXIT_CODE_OK); - break; - } - // Emulation errors lead to vCPU exit. - Err(_) => { - self.exit(FC_EXIT_CODE_GENERIC_ERROR); - break; - } - } - } - } - - fn wait_for_event( - &mut self, - hvf_vcpuid: u64, - receiver: &Receiver, - timeout: Option, - ) { - if self.vcpu_list.should_wait(hvf_vcpuid) { - if let Some(timeout) = timeout { - match receiver.recv_timeout(timeout) { - Ok(_) => {} - Err(e) => match e { - RecvTimeoutError::Timeout => {} - RecvTimeoutError::Disconnected => panic!("WFE channel closed unexpectedly"), - }, - } - } else { - receiver.recv().unwrap(); - } - } - } - - fn wait_for_resume(&mut self) {} - - fn exit(&mut self, exit_code: u8) { - self.response_sender - .send(VcpuResponse::Exited(exit_code)) - .expect("failed to send Exited status"); - - if let Err(e) = self.exit_evt.write(1) { - error!("Failed signaling vcpu exit event: {e}"); - } - } -} - -impl Drop for Vcpu { - fn drop(&mut self) { - let _ = self.reset_thread_local_data(); - } -} - -// Allow currently unused Pause and Exit events. These will be used by the vmm later on. -#[allow(unused)] -#[derive(Debug)] -/// List of events that the Vcpu can receive. -pub enum VcpuEvent { - /// Pause the Vcpu. - Pause, - /// Event that should resume the Vcpu. - Resume, - // Serialize and Deserialize to follow after we get the support from kvm-ioctls. -} - -#[derive(Debug, Eq, PartialEq)] -/// List of responses that the Vcpu reports. -pub enum VcpuResponse { - /// Vcpu is paused. - Paused, - /// Vcpu is resumed. - Resumed, - /// Vcpu is stopped. - Exited(u8), -} - -/// Wrapper over Vcpu that hides the underlying interactions with the Vcpu thread. -pub struct VcpuHandle { - event_sender: Sender, - response_receiver: Receiver, -} - -impl VcpuHandle { - pub fn new( - event_sender: Sender, - response_receiver: Receiver, - _vcpu_thread: thread::JoinHandle<()>, - ) -> Self { - Self { - event_sender, - response_receiver, - } - } - - pub fn send_event(&self, event: VcpuEvent) -> Result<()> { - // Use expect() to crash if the other thread closed this channel. - self.event_sender - .send(event) - .expect("event sender channel closed on vcpu end."); - // Kick the vcpu so it picks up the message. - /* - self.vcpu_thread - .as_ref() - // Safe to unwrap since constructor make this 'Some'. - .unwrap() - .kill(sigrtmin() + VCPU_RTSIG_OFFSET) - .map_err(Error::SignalVcpu)?; - */ - Ok(()) - } - - pub fn response_receiver(&self) -> &Receiver { - &self.response_receiver - } -} - -enum VcpuEmulation { - Handled, - Interrupted, - Stopped, - WaitForEvent, - WaitForEventExpired, - WaitForEventTimeout(Duration), -} - -#[cfg(test)] -mod tests { - #[cfg(target_arch = "x86_64")] - use crossbeam_channel::RecvTimeoutError; - use std::sync::Arc; - #[cfg(target_arch = "x86_64")] - use std::time::Duration; - - use super::*; - use arch::aarch64::layout::DRAM_MEM_START_EFI; - use devices::legacy::VcpuList; - use vm_memory::{GuestAddress, GuestMemoryMmap}; - - // Auxiliary function being used throughout the tests. - // Does NOT create a real HVF VM — Vcpu::new_aarch64 and most vcpu methods - // work without one, keeping tests free from the one-VM-per-process limit. - fn setup_vcpu(mem_size: usize) -> (Vcpu, GuestMemoryMmap) { - let gm = GuestMemoryMmap::from_ranges(&[(GuestAddress(0), mem_size)]).unwrap(); - let exit_evt = EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap(); - let vcpu_list = Arc::new(VcpuList::new(1)); - let vcpu = Vcpu::new_aarch64(1, GuestAddress(0), None, exit_evt, vcpu_list, false).unwrap(); - (vcpu, gm) - } - - #[test] - fn test_set_mmio_bus() { - let (mut vcpu, _) = setup_vcpu(0x1000); - assert!(vcpu.mmio_bus.is_none()); - vcpu.set_mmio_bus(devices::Bus::new()); - assert!(vcpu.mmio_bus.is_some()); - } - - #[test] - fn test_vm_memory_init() { - let mut vm = Vm::new(false).expect("Cannot create new vm"); - - // Use a realistic guest physical address; hv_vm_map rejects GPA 0. - let gm = GuestMemoryMmap::from_ranges(&[( - GuestAddress(DRAM_MEM_START_EFI), - 0x20_0000, // 2 MB - )]) - .unwrap(); - vm.memory_init(&gm).expect("memory_init failed"); - } - - #[test] - fn test_configure_vcpu() { - // configure_aarch64 only sets fdt_addr — no HVF VM needed. - let mem_info = arch::ArchMemoryInfo::default(); - - // Try it for when vcpu id is 0. - let vcpu_list = Arc::new(VcpuList::new(1)); - let mut vcpu = Vcpu::new_aarch64( - 0, - GuestAddress(0), - None, - EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap(), - vcpu_list, - false, - ) - .unwrap(); - assert!(vcpu.configure_aarch64(&mem_info).is_ok()); - - // Try it for when vcpu id is NOT 0. - let vcpu_list = Arc::new(VcpuList::new(2)); - let mut vcpu = Vcpu::new_aarch64( - 1, - GuestAddress(0), - None, - EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap(), - vcpu_list, - false, - ) - .unwrap(); - assert!(vcpu.configure_aarch64(&mem_info).is_ok()); - } - - #[test] - fn test_vcpu_tls() { - let (mut vcpu, _) = setup_vcpu(0x1000); - - // Reset should fail before TLS is initialized. - assert!(vcpu.reset_thread_local_data().is_err()); - - // Initialize vcpu TLS. - vcpu.init_thread_local_data().unwrap(); - - // Reset vcpu TLS. - assert!(vcpu.reset_thread_local_data().is_ok()); - - // Second reset should return error. - assert!(vcpu.reset_thread_local_data().is_err()); - } - - #[test] - fn test_invalid_tls() { - let (mut vcpu, _) = setup_vcpu(0x1000); - // Initialize vcpu TLS. - vcpu.init_thread_local_data().unwrap(); - // Trying to initialize non-empty TLS should error. - vcpu.init_thread_local_data().unwrap_err(); - } - - #[cfg(target_arch = "x86_64")] - // Sends an event to a vcpu and expects a particular response. - fn queue_event_expect_response(handle: &VcpuHandle, event: VcpuEvent, response: VcpuResponse) { - handle - .send_event(event) - .expect("failed to send event to vcpu"); - assert_eq!( - handle - .response_receiver() - .recv_timeout(Duration::from_millis(100)) - .expect("did not receive event response from vcpu"), - response - ); - } - - #[cfg(target_arch = "x86_64")] - // Sends an event to a vcpu and expects no response. - fn queue_event_expect_timeout(handle: &VcpuHandle, event: VcpuEvent) { - handle - .send_event(event) - .expect("failed to send event to vcpu"); - assert_eq!( - handle - .response_receiver() - .recv_timeout(Duration::from_millis(100)), - Err(RecvTimeoutError::Timeout) - ); - } -} diff --git a/vendor/krun-vmm/src/resources.rs b/vendor/krun-vmm/src/resources.rs deleted file mode 100644 index 2d3e5ef2b..000000000 --- a/vendor/krun-vmm/src/resources.rs +++ /dev/null @@ -1,517 +0,0 @@ -// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -//#![deny(warnings)] - -#[cfg(feature = "tee")] -use std::fs::File; -#[cfg(feature = "tee")] -use std::io::BufReader; -use std::os::fd::RawFd; -use std::path::PathBuf; - -#[cfg(feature = "tee")] -use serde::{Deserialize, Serialize}; - -#[cfg(feature = "blk")] -use crate::vmm_config::block::{BlockBuilder, BlockConfigError, BlockDeviceConfig}; -use crate::vmm_config::external_kernel::ExternalKernel; -use crate::vmm_config::firmware::FirmwareConfig; -#[cfg(not(feature = "tee"))] -use crate::vmm_config::fs::*; -#[cfg(feature = "tee")] -use crate::vmm_config::kernel_bundle::{InitrdBundle, QbootBundle, QbootBundleError}; -use crate::vmm_config::kernel_bundle::{KernelBundle, KernelBundleError}; -use crate::vmm_config::kernel_cmdline::{KernelCmdlineConfig, KernelCmdlineConfigError}; -use crate::vmm_config::machine_config::{VmConfig, VmConfigError}; -#[cfg(feature = "net")] -use crate::vmm_config::net::{NetBuilder, NetworkInterfaceConfig, NetworkInterfaceError}; -use crate::vmm_config::vsock::*; -use crate::vstate::VcpuConfig; -#[cfg(feature = "gpu")] -use devices::virtio::display::DisplayInfo; -#[cfg(feature = "tee")] -use kbs_types::Tee; -#[cfg(feature = "gpu")] -use krun_display::DisplayBackend; - -type Result = std::result::Result<(), E>; - -// Re-export TsiFlags from devices crate -pub use devices::virtio::TsiFlags; - -/// Errors encountered when configuring microVM resources. -#[derive(Debug)] -pub enum Error { - /// JSON is invalid. - InvalidJson, - /// Boot source configuration error. - KernelCmdline(KernelCmdlineConfigError), - /// Error opening TEE config file. - #[cfg(feature = "tee")] - OpenTeeConfig(std::io::Error), - /// Error parsing TEE config file. - #[cfg(feature = "tee")] - ParseTeeConfig(serde_json::Error), - /// microVM vCpus or memory configuration error. - VmConfig(VmConfigError), - /// Vsock device configuration error. - VsockDevice(VsockConfigError), -} - -#[cfg(feature = "tee")] -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TeeConfig { - pub workload_id: String, - pub cpus: u8, - pub ram_mib: usize, - pub tee: Tee, - pub tee_data: String, - pub attestation_url: String, -} - -#[cfg(feature = "tee")] -impl Default for TeeConfig { - fn default() -> Self { - Self { - workload_id: "".to_string(), - cpus: 0, - ram_mib: 0, - tee: Tee::Sev, - tee_data: "".to_string(), - attestation_url: "".to_string(), - } - } -} - -pub struct SerialConsoleConfig { - pub input_fd: RawFd, - pub output_fd: RawFd, -} - -pub struct DefaultVirtioConsoleConfig { - pub input_fd: RawFd, - pub output_fd: RawFd, - pub err_fd: RawFd, -} - -pub enum VirtioConsoleConfigMode { - Autoconfigure(DefaultVirtioConsoleConfig), - Explicit(Vec), -} - -pub enum PortConfig { - Tty { - name: String, - tty_fd: RawFd, - }, - InOut { - name: String, - input_fd: RawFd, - output_fd: RawFd, - }, -} - -/// Configuration for the vsock device -#[derive(Debug, Default, Clone, Eq, PartialEq)] -pub enum VsockConfig { - /// Default behavior - vsock created implicitly with heuristics-based TSI - #[default] - Implicit, - /// Explicit configuration with specified TSI features - Explicit { tsi_flags: TsiFlags }, - /// Vsock device disabled - Disabled, -} - -/// A data structure that encapsulates the device configurations -/// held in the Vmm. -#[derive(Default)] -pub struct VmResources { - /// The vCpu and memory configuration for this microVM. - vm_config: VmConfig, - /// The firmware to be loaded into the microVM. - pub firmware_config: Option, - /// The kernel command line for this microVM. - pub kernel_cmdline: KernelCmdlineConfig, - /// The parameters for the kernel bundle to be loaded in this microVM. - pub kernel_bundle: Option, - /// The path to an external kernel, as an alternative to KernelBundle. - pub external_kernel: Option, - /// The parameters for the qboot bundle to be loaded in this microVM. - #[cfg(feature = "tee")] - pub qboot_bundle: Option, - /// The parameters for the initrd bundle to be loaded in this microVM. - #[cfg(feature = "tee")] - pub initrd_bundle: Option, - /// The fs device. - #[cfg(not(feature = "tee"))] - pub fs: Vec, - /// The vsock device. - pub vsock: VsockBuilder, - /// The virtio-blk device. - #[cfg(feature = "blk")] - pub block: BlockBuilder, - /// The network devices builder. - #[cfg(feature = "net")] - pub net: NetBuilder, - /// TEE configuration - #[cfg(feature = "tee")] - pub tee_config: TeeConfig, - /// Flags for the virtio-gpu device. - pub gpu_virgl_flags: Option, - pub gpu_shm_size: Option, - #[cfg(feature = "gpu")] - pub display_backend: Option>, - #[cfg(feature = "gpu")] - pub displays: Vec, - #[cfg(feature = "input")] - pub input_backends: Vec<( - krun_input::InputConfigBackend<'static>, - krun_input::InputEventProviderBackend<'static>, - )>, - #[cfg(feature = "snd")] - /// Enable the virtio-snd device. - pub snd_device: bool, - /// File to send console output. - pub console_output: Option, - /// SMBIOS OEM Strings - pub smbios_oem_strings: Option>, - /// Whether to enable nested virtualization. - pub nested_enabled: bool, - /// Whether to enable split irqchip - pub split_irqchip: bool, - /// Do not create an implicit console device in the guest - pub disable_implicit_console: bool, - /// The console id to use for console= in the kernel cmdline - pub kernel_console: Option, - /// Serial consoles to attach to the guest - pub serial_consoles: Vec, - /// Virtio consoles to attach to the guest - pub virtio_consoles: Vec, - /// Enable the embedded dhcp client in init.c - pub dhcp_client: bool, -} - -impl VmResources { - /// Returns a VcpuConfig based on the vm config. - pub fn vcpu_config(&self) -> VcpuConfig { - // The unwraps are ok to use because the values are initialized using defaults if not - // supplied by the user. - VcpuConfig { - vcpu_count: self.vm_config().vcpu_count.unwrap(), - ht_enabled: self.vm_config().ht_enabled.unwrap(), - cpu_template: self.vm_config().cpu_template, - #[cfg(target_os = "linux")] - nested_enabled: self.nested_enabled, - } - } - - /// Returns the VmConfig. - pub fn vm_config(&self) -> &VmConfig { - &self.vm_config - } - - /// Set the machine configuration of the microVM. - pub fn set_vm_config(&mut self, machine_config: &VmConfig) -> Result { - if machine_config.vcpu_count == Some(0) { - return Err(VmConfigError::InvalidVcpuCount); - } - - if machine_config.mem_size_mib == Some(0) { - return Err(VmConfigError::InvalidMemorySize); - } - - let ht_enabled = machine_config - .ht_enabled - .unwrap_or_else(|| self.vm_config.ht_enabled.unwrap()); - - let vcpu_count_value = machine_config - .vcpu_count - .unwrap_or_else(|| self.vm_config.vcpu_count.unwrap()); - - // If hyperthreading is enabled or is to be enabled in this call - // only allow vcpu count to be 1 or even. - if ht_enabled && vcpu_count_value > 1 && vcpu_count_value % 2 == 1 { - return Err(VmConfigError::InvalidVcpuCount); - } - - // Update all the fields that have a new value. - self.vm_config.vcpu_count = Some(vcpu_count_value); - self.vm_config.ht_enabled = Some(ht_enabled); - - if machine_config.mem_size_mib.is_some() { - self.vm_config.mem_size_mib = machine_config.mem_size_mib; - } - - if machine_config.cpu_template.is_some() { - self.vm_config.cpu_template = machine_config.cpu_template; - } - - Ok(()) - } - - /// Set the guest kernel cmdline configuration. - pub fn set_kernel_cmdline( - &mut self, - kernel_cmdline_cfg: KernelCmdlineConfig, - ) -> Result { - self.kernel_cmdline = kernel_cmdline_cfg; - Ok(()) - } - - pub fn kernel_bundle(&self) -> Option<&KernelBundle> { - self.kernel_bundle.as_ref() - } - - pub fn set_kernel_bundle(&mut self, kernel_bundle: KernelBundle) -> Result { - // Safe because this call just returns the page size and doesn't have any side effects. - let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) as usize }; - - if kernel_bundle.host_addr == 0 || (kernel_bundle.host_addr as usize) & (page_size - 1) != 0 - { - return Err(KernelBundleError::InvalidHostAddress); - } - - if (kernel_bundle.guest_addr as usize) & (page_size - 1) != 0 { - return Err(KernelBundleError::InvalidGuestAddress); - } - - self.kernel_bundle = Some(kernel_bundle); - Ok(()) - } - - pub fn external_kernel(&self) -> Option<&ExternalKernel> { - self.external_kernel.as_ref() - } - - pub fn set_external_kernel(&mut self, external_kernel: ExternalKernel) { - self.external_kernel = Some(external_kernel); - } - - pub fn set_firmware_config(&mut self, firmware_config: FirmwareConfig) { - self.firmware_config = Some(firmware_config); - } - - #[cfg(feature = "tee")] - pub fn qboot_bundle(&self) -> Option<&QbootBundle> { - self.qboot_bundle.as_ref() - } - - #[cfg(feature = "tee")] - pub fn set_qboot_bundle(&mut self, qboot_bundle: QbootBundle) -> Result { - if qboot_bundle.size != 0x10000 { - return Err(QbootBundleError::InvalidSize); - } - - self.qboot_bundle = Some(qboot_bundle); - Ok(()) - } - - #[cfg(feature = "tee")] - pub fn initrd_bundle(&self) -> Option<&InitrdBundle> { - self.initrd_bundle.as_ref() - } - - #[cfg(feature = "tee")] - pub fn set_initrd_bundle(&mut self, initrd_bundle: InitrdBundle) -> Result { - self.initrd_bundle = Some(initrd_bundle); - Ok(()) - } - - #[cfg(not(feature = "tee"))] - pub fn add_fs_device(&mut self, config: FsDeviceConfig) { - self.fs.push(config) - } - - #[cfg(feature = "blk")] - pub fn add_block_device(&mut self, config: BlockDeviceConfig) -> Result { - self.block.insert(config) - } - - /// Sets a vsock device to be attached when the VM starts. - pub fn set_vsock_device(&mut self, config: VsockDeviceConfig) -> Result { - self.vsock.insert(config) - } - - pub fn set_gpu_virgl_flags(&mut self, virgl_flags: u32) { - self.gpu_virgl_flags = Some(virgl_flags); - } - - pub fn set_gpu_shm_size(&mut self, shm_size: usize) { - self.gpu_shm_size = Some(shm_size); - } - - #[cfg(feature = "snd")] - pub fn set_snd_device(&mut self, enabled: bool) { - self.snd_device = enabled; - } - - pub fn set_console_output(&mut self, console_output: PathBuf) { - self.console_output = Some(console_output); - } - - /// Sets a network device to be attached when the VM starts. - #[cfg(feature = "net")] - pub fn add_network_interface( - &mut self, - config: NetworkInterfaceConfig, - ) -> Result { - self.net.insert(config) - } - - #[cfg(feature = "tee")] - pub fn tee_config(&self) -> &TeeConfig { - &self.tee_config - } - - #[cfg(feature = "tee")] - pub fn set_tee_config(&mut self, filepath: PathBuf) -> Result { - let file = File::open(filepath.as_path()).map_err(Error::OpenTeeConfig)?; - let reader = BufReader::new(file); - let tee_config: TeeConfig = - serde_json::from_reader(reader).map_err(Error::ParseTeeConfig)?; - - // Override VmConfig with TeeConfig values - self.set_vm_config(&VmConfig { - vcpu_count: Some(tee_config.cpus), - mem_size_mib: Some(tee_config.ram_mib), - ht_enabled: Some(false), - cpu_template: None, - }) - .map_err(Error::VmConfig)?; - - self.tee_config = tee_config; - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - #[cfg(feature = "gpu")] - use crate::resources::DisplayBackendConfig; - use crate::resources::VmResources; - use crate::vmm_config::kernel_cmdline::KernelCmdlineConfig; - use crate::vmm_config::machine_config::{CpuFeaturesTemplate, VmConfig, VmConfigError}; - use crate::vmm_config::vsock::tests::{default_config, TempSockFile}; - use crate::vstate::VcpuConfig; - use utils::tempfile::TempFile; - - fn default_kernel_cmdline() -> KernelCmdlineConfig { - KernelCmdlineConfig { - prolog: None, - krun_env: None, - epilog: None, - } - } - - fn default_vm_resources() -> VmResources { - VmResources { - vm_config: VmConfig::default(), - firmware_config: None, - kernel_cmdline: default_kernel_cmdline(), - kernel_bundle: Default::default(), - external_kernel: None, - fs: Default::default(), - vsock: Default::default(), - #[cfg(feature = "blk")] - block: Default::default(), - #[cfg(feature = "net")] - net: Default::default(), - gpu_virgl_flags: None, - gpu_shm_size: None, - #[cfg(feature = "gpu")] - display_backend: None, - #[cfg(feature = "gpu")] - displays: Vec::new(), - #[cfg(feature = "input")] - input_backends: Vec::new(), - #[cfg(feature = "snd")] - snd_device: false, - console_output: None, - smbios_oem_strings: None, - nested_enabled: false, - split_irqchip: false, - disable_implicit_console: false, - serial_consoles: Vec::new(), - virtio_consoles: Vec::new(), - kernel_console: None, - dhcp_client: false, - } - } - - #[test] - fn test_vcpu_config() { - let vm_resources = default_vm_resources(); - let expected_vcpu_config = VcpuConfig { - vcpu_count: vm_resources.vm_config().vcpu_count.unwrap(), - ht_enabled: vm_resources.vm_config().ht_enabled.unwrap(), - cpu_template: vm_resources.vm_config().cpu_template, - #[cfg(target_os = "linux")] - nested_enabled: vm_resources.nested_enabled, - }; - - let vcpu_config = vm_resources.vcpu_config(); - assert_eq!(vcpu_config, expected_vcpu_config); - } - - #[test] - fn test_vm_config() { - let vm_resources = default_vm_resources(); - let expected_vm_cfg = VmConfig::default(); - - assert_eq!(vm_resources.vm_config(), &expected_vm_cfg); - } - - #[test] - fn test_set_vm_config() { - let mut vm_resources = default_vm_resources(); - let mut aux_vm_config = VmConfig { - vcpu_count: Some(32), - mem_size_mib: Some(512), - ht_enabled: Some(true), - cpu_template: Some(CpuFeaturesTemplate::T2), - }; - - assert_ne!(vm_resources.vm_config, aux_vm_config); - vm_resources.set_vm_config(&aux_vm_config).unwrap(); - assert_eq!(vm_resources.vm_config, aux_vm_config); - - // Invalid vcpu count. - aux_vm_config.vcpu_count = Some(0); - assert_eq!( - vm_resources.set_vm_config(&aux_vm_config), - Err(VmConfigError::InvalidVcpuCount) - ); - aux_vm_config.vcpu_count = Some(33); - assert_eq!( - vm_resources.set_vm_config(&aux_vm_config), - Err(VmConfigError::InvalidVcpuCount) - ); - aux_vm_config.vcpu_count = Some(32); - - // Invalid mem_size_mib. - aux_vm_config.mem_size_mib = Some(0); - assert_eq!( - vm_resources.set_vm_config(&aux_vm_config), - Err(VmConfigError::InvalidMemorySize) - ); - } - - #[test] - fn test_set_vsock_device() { - let mut vm_resources = default_vm_resources(); - let tmp_sock_file = TempSockFile::new(TempFile::new().unwrap()); - let new_vsock_cfg = default_config(&tmp_sock_file); - assert!(vm_resources.vsock.get().is_none()); - vm_resources - .set_vsock_device(new_vsock_cfg.clone()) - .unwrap(); - let actual_vsock_cfg = vm_resources.vsock.get().unwrap(); - assert_eq!( - actual_vsock_cfg.lock().unwrap().id(), - &new_vsock_cfg.vsock_id - ); - } -} diff --git a/vendor/krun-vmm/src/signal_handler.rs b/vendor/krun-vmm/src/signal_handler.rs deleted file mode 100644 index fabe3855b..000000000 --- a/vendor/krun-vmm/src/signal_handler.rs +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -use std::os::unix::io::RawFd; -use std::sync::atomic::{AtomicI32, Ordering}; - -use libc::{_exit, c_int, c_void, siginfo_t, SIGBUS, SIGINT, SIGSEGV, SIGSYS, SIGWINCH}; -use utils::signal::register_signal_handler; - -// The offset of `si_syscall` (offending syscall identifier) within the siginfo structure -// expressed as an `(u)int*`. -// Offset `6` for an `i32` field means that the needed information is located at `6 * sizeof(i32)`. -// See /usr/include/linux/signal.h for the C struct definition. -// See https://github.com/rust-lang/libc/issues/716 for why the offset is different in Rust. -const SI_OFF_SYSCALL: isize = 6; - -const SYS_SECCOMP_CODE: i32 = 1; - -static CONSOLE_SIGWINCH_FD: AtomicI32 = AtomicI32::new(-1); -static CONSOLE_SIGINT_FD: AtomicI32 = AtomicI32::new(-1); - -/// Signal handler for `SIGSYS`. -/// -/// Increments the `seccomp.num_faults` metric, logs an error message and terminates the process -/// with a specific exit code. -extern "C" fn sigsys_handler(num: c_int, info: *mut siginfo_t, _unused: *mut c_void) { - // Safe because we're just reading some fields from a supposedly valid argument. - let si_signo = unsafe { (*info).si_signo }; - let si_code = unsafe { (*info).si_code }; - - // Sanity check. The condition should never be true. - if num != si_signo || num != SIGSYS || si_code != SYS_SECCOMP_CODE { - // Safe because we're terminating the process anyway. - unsafe { _exit(i32::from(super::FC_EXIT_CODE_UNEXPECTED_ERROR)) }; - } - - // Other signals which might do async unsafe things incompatible with the rest of this - // function are blocked due to the sa_mask used when registering the signal handler. - let syscall = unsafe { *(info as *const i32).offset(SI_OFF_SYSCALL) as usize }; - error!("Shutting down VM after intercepting a bad syscall ({syscall})."); - // Safe because we're terminating the process anyway. We don't actually do anything when - // running unit tests. - #[cfg(not(test))] - unsafe { - _exit(i32::from(super::FC_EXIT_CODE_BAD_SYSCALL)) - }; -} - -/// Signal handler for `SIGBUS` and `SIGSEGV`. -/// -/// Logs an error message and terminates the process with a specific exit code. -extern "C" fn sigbus_sigsegv_handler(num: c_int, info: *mut siginfo_t, _unused: *mut c_void) { - // Safe because we're just reading some fields from a supposedly valid argument. - let si_signo = unsafe { (*info).si_signo }; - let si_code = unsafe { (*info).si_code }; - - // Sanity check. The condition should never be true. - if num != si_signo || (num != SIGBUS && num != SIGSEGV) { - // Safe because we're terminating the process anyway. - unsafe { _exit(i32::from(super::FC_EXIT_CODE_UNEXPECTED_ERROR)) }; - } - - error!("Shutting down VM after intercepting signal {si_signo}, code {si_code}."); - - // Safe because we're terminating the process anyway. We don't actually do anything when - // running unit tests. - #[cfg(not(test))] - unsafe { - _exit(i32::from(match si_signo { - SIGBUS => super::FC_EXIT_CODE_SIGBUS, - SIGSEGV => super::FC_EXIT_CODE_SIGSEGV, - _ => super::FC_EXIT_CODE_UNEXPECTED_ERROR, - })) - }; -} - -extern "C" fn sigwinch_handler(num: c_int, info: *mut siginfo_t, _unused: *mut c_void) { - // Safe because we're just reading some fields from a supposedly valid argument. - let si_signo = unsafe { (*info).si_signo }; - - // Sanity check. The condition should never be true. - if num != si_signo || num != SIGWINCH { - // Safe because we're terminating the process anyway. - unsafe { _exit(i32::from(super::FC_EXIT_CODE_UNEXPECTED_ERROR)) }; - } - - let val: u64 = 1; - let console_fd = CONSOLE_SIGWINCH_FD.load(Ordering::Relaxed); - let _ = unsafe { libc::write(console_fd, &val as *const _ as *const c_void, 8) }; -} - -extern "C" fn sigint_handler(num: c_int, info: *mut siginfo_t, _unused: *mut c_void) { - // Safe because we're just reading some fields from a supposedly valid argument. - let si_signo = unsafe { (*info).si_signo }; - - // Sanity check. The condition should never be true. - if num != si_signo || num != SIGINT { - // Safe because we're terminating the process anyway. - unsafe { _exit(i32::from(super::FC_EXIT_CODE_UNEXPECTED_ERROR)) }; - } - - let val: u64 = 1; - let console_fd = CONSOLE_SIGINT_FD.load(Ordering::Relaxed); - let _ = unsafe { libc::write(console_fd, &val as *const _ as *const c_void, 8) }; -} - -pub fn register_sigwinch_handler(console_fd: RawFd) -> utils::errno::Result<()> { - CONSOLE_SIGWINCH_FD.store(console_fd, Ordering::Relaxed); - - register_signal_handler(SIGWINCH, sigwinch_handler)?; - - Ok(()) -} - -pub fn register_sigint_handler(sigint_fd: RawFd) -> utils::errno::Result<()> { - CONSOLE_SIGINT_FD.store(sigint_fd, Ordering::Relaxed); - - register_signal_handler(SIGINT, sigint_handler)?; - - Ok(()) -} - -/// Registers all the required signal handlers. -/// -/// Custom handlers are installed for: `SIGBUS`, `SIGSEGV`, `SIGSYS`. -pub fn register_signal_handlers() -> utils::errno::Result<()> { - // Call to unsafe register_signal_handler which is considered unsafe because it will - // register a signal handler which will be called in the current thread and will interrupt - // whatever work is done on the current thread, so we have to keep in mind that the registered - // signal handler must only do async-signal-safe operations. - register_signal_handler(SIGSYS, sigsys_handler)?; - register_signal_handler(SIGBUS, sigbus_sigsegv_handler)?; - register_signal_handler(SIGSEGV, sigbus_sigsegv_handler)?; - - Ok(()) -} diff --git a/vendor/krun-vmm/src/terminal.rs b/vendor/krun-vmm/src/terminal.rs deleted file mode 100644 index a0721172b..000000000 --- a/vendor/krun-vmm/src/terminal.rs +++ /dev/null @@ -1,27 +0,0 @@ -use nix::sys::termios::{cfmakeraw, tcgetattr, tcsetattr, LocalFlags, SetArg, Termios}; -use std::os::fd::BorrowedFd; - -#[must_use] -pub struct TerminalMode(Termios); - -// Enable raw mode for the terminal and return the old state to be restored -pub fn term_set_raw_mode( - term: BorrowedFd, - handle_signals_by_terminal: bool, -) -> Result { - let mut termios = tcgetattr(term)?; - let old_state = termios.clone(); - - cfmakeraw(&mut termios); - - if handle_signals_by_terminal { - termios.local_flags |= LocalFlags::ISIG; - } - - tcsetattr(term, SetArg::TCSANOW, &termios)?; - Ok(TerminalMode(old_state)) -} - -pub fn term_restore_mode(term: BorrowedFd, restore: &TerminalMode) -> Result<(), nix::Error> { - tcsetattr(term, SetArg::TCSANOW, &restore.0) -} diff --git a/vendor/krun-vmm/src/vmm_config/block.rs b/vendor/krun-vmm/src/vmm_config/block.rs deleted file mode 100644 index 788be5a39..000000000 --- a/vendor/krun-vmm/src/vmm_config/block.rs +++ /dev/null @@ -1,76 +0,0 @@ -use std::collections::VecDeque; -use std::fmt; -use std::sync::{Arc, Mutex}; - -use devices::virtio::{ - block::{ImageType, SyncMode}, - Block, CacheType, -}; - -#[derive(Debug)] -pub enum BlockConfigError { - /// Failed to create the block device. - CreateBlockDevice(std::io::Error), -} - -impl fmt::Display for BlockConfigError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use self::BlockConfigError::*; - match *self { - CreateBlockDevice(ref e) => write!(f, "Cannot create block device: {e:?}"), - } - } -} - -type Result = std::result::Result; - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct BlockDeviceConfig { - pub block_id: String, - pub cache_type: CacheType, - pub disk_image_path: String, - pub disk_image_format: ImageType, - pub is_disk_read_only: bool, - pub direct_io: bool, - pub sync_mode: SyncMode, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct BlockRootConfig { - pub device: String, - pub fstype: Option, - pub options: Option, -} - -#[derive(Default)] -pub struct BlockBuilder { - pub list: VecDeque>>, -} - -impl BlockBuilder { - pub fn new() -> Self { - Self { - list: VecDeque::>>::new(), - } - } - - pub fn insert(&mut self, config: BlockDeviceConfig) -> Result<()> { - let block_dev = Arc::new(Mutex::new(Self::create_block(config)?)); - self.list.push_back(block_dev); - Ok(()) - } - - pub fn create_block(config: BlockDeviceConfig) -> Result { - devices::virtio::Block::new( - config.block_id, - None, - config.cache_type, - config.disk_image_path, - config.disk_image_format, - config.is_disk_read_only, - config.direct_io, - config.sync_mode, - ) - .map_err(BlockConfigError::CreateBlockDevice) - } -} diff --git a/vendor/krun-vmm/src/vmm_config/external_kernel.rs b/vendor/krun-vmm/src/vmm_config/external_kernel.rs deleted file mode 100644 index a3b5b29fc..000000000 --- a/vendor/krun-vmm/src/vmm_config/external_kernel.rs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2024, Red Hat Inc. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -use std::path::PathBuf; - -#[derive(Clone, Debug, Default)] -pub enum KernelFormat { - // Raw image, ready to be loaded into the VM. - #[default] - Raw, - // ELF image, need to locale sections be loaded. - Elf, - // Raw image compressed with GZIP, embedded into a PE file. - PeGz, - // ELF image compressed with BZIP2, embedded into an Image file. - ImageBz2, - // ELF image compressed with GZIP, embedded into an Image file. - ImageGz, - // ELF image compressed with ZSTD, embedded into an Image file. - ImageZstd, -} - -/// Data structure holding the attributes read from the `libkrunfw` kernel config. -#[derive(Clone, Debug, Default)] -pub struct ExternalKernel { - pub path: PathBuf, - pub format: KernelFormat, - pub initramfs_path: Option, - pub initramfs_size: u64, - pub cmdline: Option, -} diff --git a/vendor/krun-vmm/src/vmm_config/firmware.rs b/vendor/krun-vmm/src/vmm_config/firmware.rs deleted file mode 100644 index cdb9b522a..000000000 --- a/vendor/krun-vmm/src/vmm_config/firmware.rs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2025, Red Hat Inc. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -use std::path::PathBuf; - -#[derive(Clone, Debug, Default)] -pub struct FirmwareConfig { - pub path: PathBuf, -} diff --git a/vendor/krun-vmm/src/vmm_config/fs.rs b/vendor/krun-vmm/src/vmm_config/fs.rs deleted file mode 100644 index 8a8bde706..000000000 --- a/vendor/krun-vmm/src/vmm_config/fs.rs +++ /dev/null @@ -1,21 +0,0 @@ -#[cfg(not(feature = "aws-nitro"))] -use devices::virtio::fs::passthrough::PermissionSemantics; -#[cfg(not(feature = "aws-nitro"))] -use devices::virtio::fs::virtual_entry::VirtualDirEntry; -#[cfg(not(feature = "aws-nitro"))] -use devices::virtio::fs::OverlayConfig; - -#[derive(Clone, Debug)] -pub struct FsDeviceConfig { - pub fs_id: String, - /// Host directory to pass through. None means a virtual-only filesystem - /// (NullFs + AugmentFs, no host directory). - pub shared_dir: Option, - pub semantics: PermissionSemantics, - pub shm_size: Option, - pub read_only: bool, - #[cfg(not(feature = "aws-nitro"))] - pub overlay: Option, - #[cfg(not(feature = "aws-nitro"))] - pub virtual_entries: Vec, -} diff --git a/vendor/krun-vmm/src/vmm_config/instance_info.rs b/vendor/krun-vmm/src/vmm_config/instance_info.rs deleted file mode 100644 index c1adc68e6..000000000 --- a/vendor/krun-vmm/src/vmm_config/instance_info.rs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -/// The strongly typed that contains general information about the microVM. -#[derive(Debug)] -pub struct InstanceInfo { - /// The ID of the microVM. - pub id: String, - /// Whether the microVM has been started. - pub started: bool, - /// The version of the VMM that runs the microVM. - pub vmm_version: String, - /// The name of the application that runs the microVM. - pub app_name: String, -} diff --git a/vendor/krun-vmm/src/vmm_config/kernel_bundle.rs b/vendor/krun-vmm/src/vmm_config/kernel_bundle.rs deleted file mode 100644 index 52b6c1a05..000000000 --- a/vendor/krun-vmm/src/vmm_config/kernel_bundle.rs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2020, Red Hat Inc. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -use std::fmt::{Display, Formatter, Result}; - -/// Data structure holding the attributes read from the `libkrunfw` kernel config. -#[derive(Debug, Default)] -pub struct KernelBundle { - pub host_addr: u64, - pub guest_addr: u64, - pub entry_addr: u64, - pub size: usize, -} - -/// Structure used to specify the parameters for the `libkrunfw` kernel bundle. -#[derive(Debug)] -pub enum KernelBundleError { - /// Guest address is not page-aligned. - InvalidGuestAddress, - /// Host address is zero or not page-aligned. - InvalidHostAddress, - /// Kernel size is zero or not a multiple of the page size. - InvalidSize, -} - -impl Display for KernelBundleError { - fn fmt(&self, f: &mut Formatter) -> Result { - use self::KernelBundleError::*; - match *self { - InvalidGuestAddress => write!(f, "Guest address is not page-aligned"), - InvalidHostAddress => write!(f, "Host address is zero or not page-aligned"), - InvalidSize => write!(f, "Kernel size is zero or not a multiple of the page size"), - } - } -} - -/// Data structure holding the attributes read from the `libkrunfw` qboot config. -#[derive(Debug, Default)] -pub struct QbootBundle { - pub host_addr: u64, - pub size: usize, -} - -/// Structure used to specify the parameters for the `libkrunfw` qboot bundle. -#[derive(Debug)] -pub enum QbootBundleError { - /// Qboot binary is not 64K long. - InvalidSize, -} - -impl Display for QbootBundleError { - fn fmt(&self, f: &mut Formatter) -> Result { - use self::QbootBundleError::*; - match *self { - InvalidSize => write!(f, "qboot binary is not 64K long."), - } - } -} - -/// Data structure holding the attributes read from the `libkrunfw` initrd config. -#[derive(Debug, Default)] -pub struct InitrdBundle { - pub host_addr: u64, - pub size: usize, -} diff --git a/vendor/krun-vmm/src/vmm_config/kernel_cmdline.rs b/vendor/krun-vmm/src/vmm_config/kernel_cmdline.rs deleted file mode 100644 index 94bd77522..000000000 --- a/vendor/krun-vmm/src/vmm_config/kernel_cmdline.rs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -use std::fmt::{Display, Formatter, Result}; - -#[cfg(target_os = "linux")] -pub const DEFAULT_KERNEL_CMDLINE: &str = "reboot=k panic=-1 panic_print=0 nomodule console=hvc0 \ - rootfstype=virtiofs rw quiet no-kvmapf"; -#[cfg(target_os = "macos")] -pub const DEFAULT_KERNEL_CMDLINE: &str = "reboot=k panic=-1 panic_print=0 nomodule console=hvc0 \ - rootfstype=virtiofs rw quiet no-kvmapf"; - -/// Strongly typed data structure used to configure the boot source of the -/// microvm. -#[derive(Debug, Default, Eq, PartialEq)] -pub struct KernelCmdlineConfig { - pub prolog: Option, - pub krun_env: Option, - pub epilog: Option, -} - -/// Errors associated with actions on `KernelCmdlineConfig`. -#[derive(Debug)] -pub enum KernelCmdlineConfigError { - /// The kernel command line is invalid. - InvalidKernelCommandLine(String), -} - -impl Display for KernelCmdlineConfigError { - fn fmt(&self, f: &mut Formatter) -> Result { - use self::KernelCmdlineConfigError::*; - match *self { - InvalidKernelCommandLine(ref e) => { - write!(f, "The kernel command line is invalid: {}", e.as_str()) - } - } - } -} diff --git a/vendor/krun-vmm/src/vmm_config/machine_config.rs b/vendor/krun-vmm/src/vmm_config/machine_config.rs deleted file mode 100644 index ff991f4e6..000000000 --- a/vendor/krun-vmm/src/vmm_config/machine_config.rs +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -use std::fmt; - -/// Firecracker aims to support small scale workloads only, so limit the maximum -/// vCPUs supported. -pub const MAX_SUPPORTED_VCPUS: u8 = 32; - -/// Errors associated with configuring the microVM. -#[derive(Debug, Eq, PartialEq)] -pub enum VmConfigError { - /// The vcpu count is invalid. When hyperthreading is enabled, the `cpu_count` must be either - /// 1 or an even number. - InvalidVcpuCount, - /// The memory size is invalid. The memory can only be an unsigned integer. - InvalidMemorySize, -} - -impl fmt::Display for VmConfigError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use self::VmConfigError::*; - match *self { - InvalidVcpuCount => write!( - f, - "The vCPU number is invalid! The vCPU number can only \ - be 1 or an even number when hyperthreading is enabled.", - ), - InvalidMemorySize => write!(f, "The memory size (MiB) is invalid.",), - } - } -} - -/// Strongly typed structure that represents the configuration of the -/// microvm. -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct VmConfig { - /// The number of vCPUs. - pub vcpu_count: Option, - /// The memory size in MiB. - pub mem_size_mib: Option, - /// Enables or disabled hyperthreading. - pub ht_enabled: Option, - /// A CPU template that it is used to filter the CPU features exposed to the guest. - pub cpu_template: Option, -} - -impl Default for VmConfig { - fn default() -> Self { - VmConfig { - vcpu_count: Some(1), - mem_size_mib: Some(128), - ht_enabled: Some(false), - cpu_template: None, - } - } -} - -impl fmt::Display for VmConfig { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let vcpu_count = self.vcpu_count.unwrap_or(1); - let mem_size = self.mem_size_mib.unwrap_or(128); - let ht_enabled = self.ht_enabled.unwrap_or(false); - let cpu_template = self - .cpu_template - .map_or("Uninitialized".to_string(), |c| c.to_string()); - - write!(f, "{{ \"vcpu_count\": {vcpu_count:?}, \"mem_size_mib\": {mem_size:?}, \"ht_enabled\": {ht_enabled:?}, \"cpu_template\": {cpu_template:?} }}") - } -} - -/// Template types available for configuring the CPU features that map -/// to EC2 instances. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum CpuFeaturesTemplate { - /// C3 Template. - C3, - /// T2 Template. - T2, -} - -impl fmt::Display for CpuFeaturesTemplate { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - CpuFeaturesTemplate::C3 => write!(f, "C3"), - CpuFeaturesTemplate::T2 => write!(f, "T2"), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_display_cpu_features_template() { - assert_eq!(CpuFeaturesTemplate::C3.to_string(), "C3".to_string()); - assert_eq!(CpuFeaturesTemplate::T2.to_string(), "T2".to_string()); - } - - #[test] - fn test_display_vm_config_error() { - let expected_str = "The vCPU number is invalid! The vCPU number can only \ - be 1 or an even number when hyperthreading is enabled."; - assert_eq!(VmConfigError::InvalidVcpuCount.to_string(), expected_str); - - let expected_str = "The memory size (MiB) is invalid."; - assert_eq!(VmConfigError::InvalidMemorySize.to_string(), expected_str); - } -} diff --git a/vendor/krun-vmm/src/vmm_config/mod.rs b/vendor/krun-vmm/src/vmm_config/mod.rs deleted file mode 100644 index d324f54e9..000000000 --- a/vendor/krun-vmm/src/vmm_config/mod.rs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -/// Wrapper for configuring the Block devices attached to the microVM. -#[cfg(feature = "blk")] -pub mod block; - -/// Wrapper for configuring the firmware. -pub mod firmware; - -/// Wrapper for configuring the kernel command line. -pub mod kernel_cmdline; - -/// Wrapper for configuring an external kernel to be loaded in the microVM. -pub mod external_kernel; - -/// Wrapper for configuring the Fs devices attached to the microVM. -#[cfg(not(feature = "tee"))] -pub mod fs; - -/// Wrapper over the microVM general information attached to the microVM. -pub mod instance_info; - -/// Wrapper for configuring the kernel bundle to be loaded in the microVM. -pub mod kernel_bundle; - -/// Wrapper for configuring the memory and CPU of the microVM. -pub mod machine_config; - -/// Wrapper for configuring the vsock devices attached to the microVM. -pub mod vsock; - -/// Wrapper for configuring the network devices attached to the microVM. -#[cfg(feature = "net")] -pub mod net; diff --git a/vendor/krun-vmm/src/vmm_config/net.rs b/vendor/krun-vmm/src/vmm_config/net.rs deleted file mode 100644 index 444692d8f..000000000 --- a/vendor/krun-vmm/src/vmm_config/net.rs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -use std::collections::VecDeque; -use std::fmt; -use std::result; -use std::sync::{Arc, Mutex}; - -use devices::virtio::net::device::VirtioNetBackend; -use devices::virtio::Net; - -pub struct NetworkInterfaceConfig { - /// ID of the guest network interface. - pub iface_id: String, - /// Backend to transport data to/from the host. - pub backend: VirtioNetBackend, - /// MAC address. - pub mac: [u8; 6], - /// virtio-net features for the network interface. - pub features: u32, -} - -/// Errors associated with `NetworkInterfaceConfig`. -#[derive(Debug)] -pub enum NetworkInterfaceError { - /// Could not create Network Device. - CreateNetworkDevice(devices::virtio::net::Error), - /// Couldn't find the interface to update (patch). - DeviceIdNotFound, -} - -impl fmt::Display for NetworkInterfaceError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use self::NetworkInterfaceError::*; - match *self { - CreateNetworkDevice(ref e) => write!(f, "Could not create Network Device: {e:?}"), - DeviceIdNotFound => write!(f, "Invalid interface ID - not found."), - } - } -} - -type Result = result::Result; - -/// Builder for a list of network devices. -#[derive(Default)] -pub struct NetBuilder { - pub list: VecDeque>>, -} - -impl NetBuilder { - /// Creates an empty list of Network Devices. - pub fn new() -> Self { - NetBuilder { - // List of built network devices. - list: VecDeque::new(), - } - } - - pub fn insert(&mut self, config: NetworkInterfaceConfig) -> Result<()> { - let net_dev = Arc::new(Mutex::new(Self::create_net(config)?)); - self.list.push_back(net_dev); - Ok(()) - } - - /// Creates a Net device from a NetworkInterfaceConfig. - pub fn create_net(cfg: NetworkInterfaceConfig) -> Result { - // Create and return the Net device - Net::new(cfg.iface_id, cfg.backend, cfg.mac, cfg.features) - .map_err(NetworkInterfaceError::CreateNetworkDevice) - } -} diff --git a/vendor/krun-vmm/src/vmm_config/vsock.rs b/vendor/krun-vmm/src/vmm_config/vsock.rs deleted file mode 100644 index 1e3d6300d..000000000 --- a/vendor/krun-vmm/src/vmm_config/vsock.rs +++ /dev/null @@ -1,161 +0,0 @@ -// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -use std::collections::HashMap; -use std::fmt; -use std::path::PathBuf; -use std::sync::{Arc, Mutex}; - -use devices::virtio::{TsiFlags, Vsock, VsockError}; - -type MutexVsock = Arc>; - -/// Errors associated with `NetworkInterfaceConfig`. -#[derive(Debug)] -pub enum VsockConfigError { - /// Failed to create the vsock device. - CreateVsockDevice(VsockError), -} - -impl fmt::Display for VsockConfigError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use self::VsockConfigError::*; - match *self { - CreateVsockDevice(ref e) => write!(f, "Cannot create vsock device: {e:?}"), - } - } -} - -type Result = std::result::Result; - -/// This struct represents the strongly typed equivalent of the json body -/// from vsock related requests. -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct VsockDeviceConfig { - /// ID of the vsock device. - pub vsock_id: String, - /// A 32-bit Context Identifier (CID) used to identify the guest. - pub guest_cid: u32, - /// An optional map of host to guest port mappings. - pub host_port_map: Option>, - /// An optional map of guest port to host UNIX domain sockets for IPC. - pub unix_ipc_port_map: Option>, - /// TSI feature flags - pub tsi_flags: TsiFlags, -} - -struct VsockWrapper { - vsock: MutexVsock, -} - -/// A builder of Vsock from 'VsockDeviceConfig'. -#[derive(Default)] -pub struct VsockBuilder { - inner: Option, - tsi_flags: TsiFlags, -} - -impl VsockBuilder { - /// Creates an empty Vsock. - pub fn new() -> Self { - Self { - inner: None, - tsi_flags: TsiFlags::empty(), - } - } - - /// Inserts a Vsock in the store. - /// If an entry already exists, it will overwrite it. - pub fn insert(&mut self, cfg: VsockDeviceConfig) -> Result<()> { - self.tsi_flags = cfg.tsi_flags; - self.inner = Some(VsockWrapper { - vsock: Arc::new(Mutex::new(Self::create_vsock(cfg)?)), - }); - Ok(()) - } - - /// Provides a reference to the Vsock if present. - pub fn get(&self) -> Option<&MutexVsock> { - self.inner.as_ref().map(|pair| &pair.vsock) - } - - pub fn tsi_flags(&self) -> TsiFlags { - self.tsi_flags - } - - /// Creates a Vsock device from a VsockDeviceConfig. - pub fn create_vsock(cfg: VsockDeviceConfig) -> Result { - Vsock::new( - u64::from(cfg.guest_cid), - cfg.host_port_map, - cfg.unix_ipc_port_map, - cfg.tsi_flags, - ) - .map_err(VsockConfigError::CreateVsockDevice) - } -} - -#[cfg(test)] -pub(crate) mod tests { - use super::*; - use utils::tempfile::TempFile; - - // Placeholder for the path where a socket file will be created. - // The socket file will be removed when the scope ends. - pub(crate) struct TempSockFile { - path: String, - } - - impl TempSockFile { - pub fn new(tmp_file: TempFile) -> Self { - TempSockFile { - path: String::from(tmp_file.as_path().to_str().unwrap()), - } - } - } - - impl Drop for TempSockFile { - fn drop(&mut self) { - let _ = std::fs::remove_file(&self.path); - } - } - - pub(crate) fn default_config(_tmp_sock_file: &TempSockFile) -> VsockDeviceConfig { - let vsock_dev_id = "vsock"; - VsockDeviceConfig { - vsock_id: vsock_dev_id.to_string(), - guest_cid: 3, - host_port_map: None, - unix_ipc_port_map: None, - tsi_flags: TsiFlags::empty(), - } - } - - #[test] - fn test_vsock_insert() { - let mut store = VsockBuilder::new(); - let tmp_sock_file = TempSockFile::new(TempFile::new().unwrap()); - let mut vsock_config = default_config(&tmp_sock_file); - - store.insert(vsock_config.clone()).unwrap(); - let vsock = store.get().unwrap(); - assert_eq!(vsock.lock().unwrap().id(), &vsock_config.vsock_id); - - let new_cid = vsock_config.guest_cid + 1; - vsock_config.guest_cid = new_cid; - store.insert(vsock_config).unwrap(); - let vsock = store.get().unwrap(); - assert_eq!(vsock.lock().unwrap().cid(), new_cid as u64); - } - - #[test] - fn test_error_messages() { - use super::VsockConfigError::*; - use std::io; - - let err = CreateVsockDevice(devices::virtio::VsockError::EventFd( - io::Error::from_raw_os_error(0), - )); - let _ = format!("{err}{err:?}"); - } -} diff --git a/vendor/krun-vmm/src/worker.rs b/vendor/krun-vmm/src/worker.rs deleted file mode 100644 index d0131b994..000000000 --- a/vendor/krun-vmm/src/worker.rs +++ /dev/null @@ -1,157 +0,0 @@ -use std::io; -use std::sync::{Arc, Mutex}; - -#[cfg(feature = "tee")] -use utils::worker_message::MemoryProperties; -use utils::worker_message::WorkerMessage; - -use crossbeam_channel::Receiver; -#[cfg(feature = "tee")] -use crossbeam_channel::Sender; -#[cfg(feature = "tee")] -use kvm_bindings::{kvm_memory_attributes, KVM_MEMORY_ATTRIBUTE_PRIVATE}; -#[cfg(feature = "tee")] -use libc::{fallocate, madvise, FALLOC_FL_KEEP_SIZE, FALLOC_FL_PUNCH_HOLE, MADV_DONTNEED}; -#[cfg(feature = "tee")] -use std::ffi::c_void; -#[cfg(feature = "tee")] -use vm_memory::{ - guest_memory::GuestMemory, Address, GuestAddress, GuestMemoryRegion, MemoryRegionAddress, -}; - -pub fn start_worker_thread( - vmm: Arc>, - receiver: Receiver, -) -> io::Result<()> { - std::thread::Builder::new() - .name("vmm worker".into()) - .spawn(move || loop { - match receiver.recv() { - Err(e) => error!("error receiving message from vmm worker thread: {e:?}"), - #[cfg(target_os = "macos")] - Ok(message) => vmm.lock().unwrap().match_worker_message(message), - #[cfg(target_os = "linux")] - Ok(message) => vmm.lock().unwrap().match_worker_message(message), - } - })?; - Ok(()) -} - -impl super::Vmm { - fn match_worker_message(&self, msg: WorkerMessage) { - match msg { - #[cfg(target_os = "macos")] - WorkerMessage::GpuAddMapping(s, h, g, l) => self.add_mapping(s, h, g, l), - #[cfg(target_os = "macos")] - WorkerMessage::GpuRemoveMapping(s, g, l) => self.remove_mapping(s, g, l), - #[cfg(target_arch = "x86_64")] - WorkerMessage::GsiRoute(sender, entries) => { - let mut routing = kvm_bindings::KvmIrqRouting::new(entries.len()).unwrap(); - let routing_entries = routing.as_mut_slice(); - routing_entries.copy_from_slice(&entries); - sender - .send(self.vm.fd().set_gsi_routing(&routing).is_ok()) - .unwrap(); - } - #[cfg(target_arch = "x86_64")] - WorkerMessage::IrqLine(sender, irq, active) => { - sender - .send(self.vm.fd().set_irq_line(irq, active).is_ok()) - .unwrap(); - } - WorkerMessage::ConvertMemory(_sender, _properties) => - { - #[cfg(feature = "tee")] - self.convert_memory(_sender, _properties) - } - } - } - - #[cfg(feature = "tee")] - fn convert_memory(&self, sender: Sender, properties: MemoryProperties) { - let Some((guest_memfd, region_start)) = self.kvm_vm().guest_memfd_get(properties.gpa) - else { - error!( - "unable to find KVM guest_memfd for memory region corresponding to GPA 0x{:x}", - properties.gpa - ); - sender.send(false).unwrap(); - return; - }; - - let attributes: u64 = if properties.private { - KVM_MEMORY_ATTRIBUTE_PRIVATE as u64 - } else { - 0 - }; - - let attr = kvm_memory_attributes { - address: properties.gpa, - size: properties.size, - attributes, - flags: 0, - }; - - if self.kvm_vm().fd().set_memory_attributes(attr).is_err() { - error!("unable to set memory attributes for memory region corresponding to guest address 0x{:x}", properties.gpa); - sender.send(false).unwrap(); - return; - } - - let region = self - .guest_memory() - .find_region(GuestAddress(properties.gpa)); - if region.is_none() { - error!( - "guest memory region corresponding to GPA 0x{:x} not found", - properties.gpa - ); - sender.send(false).unwrap(); - return; - } - - let offset = properties.gpa - region_start; - - if properties.private { - let region_addr = MemoryRegionAddress(offset); - - let Ok(host_startaddr) = region.unwrap().get_host_address(region_addr) else { - error!( - "host address corresponding to memory region address 0x{:x} not found", - region_addr.raw_value() - ); - sender.send(false).unwrap(); - return; - }; - - let ret = unsafe { - madvise( - host_startaddr as *mut c_void, - properties.size.try_into().unwrap(), - MADV_DONTNEED, - ) - }; - - if ret < 0 { - error!("unable to advise kernel that memory region corresponding to GPA 0x{:x} will likely not be needed (madvise)", properties.gpa); - sender.send(false).unwrap(); - } - } else { - let ret = unsafe { - fallocate( - guest_memfd, - FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, - offset as i64, - properties.size as i64, - ) - }; - - if ret < 0 { - error!("unable to allocate space in guest_memfd for shared memory (fallocate)"); - sender.send(false).unwrap(); - } - } - - sender.send(true).unwrap(); - } -} diff --git a/vendor/libkrun/.cargo-ok b/vendor/libkrun/.cargo-ok deleted file mode 100644 index 5f8b79583..000000000 --- a/vendor/libkrun/.cargo-ok +++ /dev/null @@ -1 +0,0 @@ -{"v":1} \ No newline at end of file diff --git a/vendor/libkrun/.cargo_vcs_info.json b/vendor/libkrun/.cargo_vcs_info.json deleted file mode 100644 index 5c726ae99..000000000 --- a/vendor/libkrun/.cargo_vcs_info.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "git": { - "sha1": "654e4a6045858d5ced90efae106e91d0eca3469f" - }, - "path_in_vcs": "src/libkrun" -} \ No newline at end of file diff --git a/vendor/libkrun/Cargo.lock b/vendor/libkrun/Cargo.lock deleted file mode 100644 index 34c74918f..000000000 --- a/vendor/libkrun/Cargo.lock +++ /dev/null @@ -1,1921 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "annotate-snippets" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4" -dependencies = [ - "anstyle", - "unicode-width", -] - -[[package]] -name = "anstream" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys", -] - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bincode" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" -dependencies = [ - "bincode_derive", - "unty", -] - -[[package]] -name = "bincode_derive" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" -dependencies = [ - "virtue", -] - -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "annotate-snippets", - "bitflags 2.11.0", - "cexpr", - "clang-sys", - "itertools", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn", -] - -[[package]] -name = "bitfield" -version = "0.19.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21ba6517c6b0f2bf08be60e187ab64b038438f22dd755614d8fe4d4098c46419" -dependencies = [ - "bitfield-macros", -] - -[[package]] -name = "bitfield-macros" -version = "0.19.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f48d6ace212fdf1b45fd6b566bb40808415344642b76c3224c07c8df9da81e97" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bumpalo" -version = "3.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" - -[[package]] -name = "bzip2" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" -dependencies = [ - "bzip2-sys", -] - -[[package]] -name = "bzip2-sys" -version = "0.1.13+1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" -dependencies = [ - "cc", - "pkg-config", -] - -[[package]] -name = "caps" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd1ddba47aba30b6a889298ad0109c3b8dcb0e8fc993b459daa7067d46f865e0" -dependencies = [ - "libc", -] - -[[package]] -name = "cc" -version = "1.2.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom 7.1.3", -] - -[[package]] -name = "cfg-expr" -version = "0.20.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6b04e07d8080154ed4ac03546d9a2b303cc2fe1901ba0b35b301516e289368" -dependencies = [ - "smallvec", - "target-lexicon", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - -[[package]] -name = "convert_case" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "cookie-factory" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "env_filter" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys", -] - -[[package]] -name = "filetime" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" -dependencies = [ - "cfg-if", - "libc", - "libredox", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "imago" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae7cfee876c698a1a2ed9c705ab18f21acbed82110f19b51cc458de73426fe2c" -dependencies = [ - "async-trait", - "bincode", - "cfg-if", - "libc", - "miniz_oxide", - "nix 0.30.1", - "page_size", - "rustc_version", - "tokio", - "tracing", - "windows-sys", -] - -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "iocuddle" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8972d5be69940353d5347a1344cb375d9b457d6809b428b05bb1ca2fb9ce007" - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "jiff" -version = "0.2.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" -dependencies = [ - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", -] - -[[package]] -name = "jiff-static" -version = "0.2.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "kbs-types" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2119e8aaa1382675e879f0cbc95fc948d28284d134896f57676b8ef2c90212e" -dependencies = [ - "base64", - "serde", - "serde_json", - "sha2", - "sm3", - "strum", - "thiserror 2.0.18", -] - -[[package]] -name = "krun-arch" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e408be4923e881a3fb6536322d569a845e55fb0f5af4a9af3202ed97becbb192" -dependencies = [ - "krun-arch-gen", - "krun-smbios", - "krun-utils", - "kvm-bindings", - "kvm-ioctls", - "libc", - "tdx", - "vm-memory", - "vmm-sys-util 0.14.0", -] - -[[package]] -name = "krun-arch-gen" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e903977e89dbb2f77008307ce9953281f681a099e647b79e9220169278ce1970" - -[[package]] -name = "krun-aws-nitro" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17c86aee916b21b598f5330d37a70ae2553bdb8a6bf7f95eaddc41e048384015" -dependencies = [ - "krun-devices", - "libc", - "log", - "nitro-enclaves 0.6.1", - "nix 0.30.1", - "signal-hook", - "tar", - "vsock", -] - -[[package]] -name = "krun-cpuid" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e0bda6161895cc919dff67cf5d51d03085a93e2f2022aa52da406d758a566a" -dependencies = [ - "kvm-bindings", - "kvm-ioctls", - "vmm-sys-util 0.14.0", -] - -[[package]] -name = "krun-devices" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78dba5c05da51af1b53fc3eb7cb324f7929707d72641ece0da21216ee7d13f0f" -dependencies = [ - "bitflags 1.3.2", - "caps", - "crossbeam-channel", - "imago", - "krun-arch", - "krun-display", - "krun-hvf", - "krun-input", - "krun-polly", - "krun-rutabaga-gfx", - "krun-utils", - "kvm-bindings", - "kvm-ioctls", - "libc", - "libloading", - "log", - "lru", - "nix 0.30.1", - "pipewire", - "rand", - "thiserror 2.0.18", - "virtio-bindings", - "vm-fdt", - "vm-memory", - "zerocopy", -] - -[[package]] -name = "krun-display" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e13751337ed633505118ddea0182350eb2d0dbfffca299da641fe36c50e8e93" -dependencies = [ - "bindgen", - "bitflags 2.11.0", - "log", - "static_assertions", - "thiserror 2.0.18", -] - -[[package]] -name = "krun-hvf" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f7e78f0c5431195ca36aded1886024872699a6a56f0a600f619aeb7ef2161bc" -dependencies = [ - "crossbeam-channel", - "krun-arch", - "libloading", - "log", -] - -[[package]] -name = "krun-init-blob" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c90831a5abcb1c887fcd56b76c9970e1a6e36e6892e74fb672a731448473cd3b" - -[[package]] -name = "krun-input" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f67de5755f01ea31499764a4a850613e21ec209ad207b8e93156491e53c2d3" -dependencies = [ - "bindgen", - "bitflags 2.11.0", - "libc", - "log", - "static_assertions", - "thiserror 2.0.18", -] - -[[package]] -name = "krun-kernel" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48953b0f707aafee40684fdf45fcb0ea068745aaa9500f672c1a39da113e97a" -dependencies = [ - "krun-utils", - "vm-memory", -] - -[[package]] -name = "krun-polly" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d2c61890357072d5751ef813aea744b93a67bfc3b820f36061f9706f72af84d" -dependencies = [ - "krun-utils", - "libc", -] - -[[package]] -name = "krun-rutabaga-gfx" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cb903397f5798f49d0aa8e481e54a79ef163e0621f1020a363e798081210991" -dependencies = [ - "anyhow", - "cfg-if", - "libc", - "log", - "nix 0.30.1", - "pkg-config", - "remain", - "thiserror 1.0.69", - "vmm-sys-util 0.14.0", - "winapi", - "zerocopy", -] - -[[package]] -name = "krun-smbios" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01462ad735097a9a9650564e7f7ba082db720a41696f94ec9fb56ecaf072c744" -dependencies = [ - "vm-memory", -] - -[[package]] -name = "krun-utils" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8315551f3fd0c86191ff14318ac595a1e62d9c1e0690d30355d3a4e0ac741c87" -dependencies = [ - "bitflags 1.3.2", - "crossbeam-channel", - "kvm-bindings", - "libc", - "log", - "nix 0.30.1", - "vmm-sys-util 0.14.0", -] - -[[package]] -name = "krun-vmm" -version = "0.1.0-1.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b146a23c59ec63b4a6ca05fee35f41a3cba8a9f6539a25fd513bfb07e7939f0" -dependencies = [ - "bitfield", - "bitflags 2.11.0", - "bzip2", - "crossbeam-channel", - "flate2", - "iocuddle", - "kbs-types", - "krun-arch", - "krun-arch-gen", - "krun-cpuid", - "krun-devices", - "krun-display", - "krun-hvf", - "krun-input", - "krun-kernel", - "krun-polly", - "krun-utils", - "kvm-bindings", - "kvm-ioctls", - "libc", - "linux-loader", - "log", - "nix 0.30.1", - "serde", - "serde_json", - "tdx", - "vm-memory", - "vmm-sys-util 0.15.0", - "zstd", -] - -[[package]] -name = "kvm-bindings" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a537873e15e8daabb416667e606d9b0abc2a8fb9a45bd5853b888ae0ead82f9" -dependencies = [ - "vmm-sys-util 0.14.0", -] - -[[package]] -name = "kvm-ioctls" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8f7370330b4f57981e300fa39b02088f2f2a5c2d0f1f994e8090589619c56d" -dependencies = [ - "bitflags 2.11.0", - "kvm-bindings", - "libc", - "vmm-sys-util 0.14.0", -] - -[[package]] -name = "libc" -version = "0.2.183" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" - -[[package]] -name = "libkrun" -version = "1.19.3" -dependencies = [ - "crossbeam-channel", - "env_logger", - "krun-aws-nitro", - "krun-devices", - "krun-display", - "krun-hvf", - "krun-init-blob", - "krun-input", - "krun-polly", - "krun-utils", - "krun-vmm", - "kvm-bindings", - "kvm-ioctls", - "libc", - "libloading", - "log", - "nitro-enclaves 0.5.0", - "once_cell", - "vm-memory", -] - -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link", -] - -[[package]] -name = "libredox" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" -dependencies = [ - "bitflags 2.11.0", - "libc", - "plain", - "redox_syscall", -] - -[[package]] -name = "libspa" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6b8cfa2a7656627b4c92c6b9ef929433acd673d5ab3708cda1b18478ac00df4" -dependencies = [ - "bitflags 2.11.0", - "cc", - "convert_case", - "cookie-factory", - "libc", - "libspa-sys", - "nix 0.30.1", - "nom 8.0.0", - "system-deps", -] - -[[package]] -name = "libspa-sys" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "901049455d2eb6decf9058235d745237952f4804bc584c5fcb41412e6adcc6e0" -dependencies = [ - "bindgen", - "cc", - "system-deps", -] - -[[package]] -name = "linux-loader" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de72cb02c55ecffcf75fe78295926f872eb6eb0a58d629c58a8c324dc26380f6" -dependencies = [ - "vm-memory", -] - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "lru" -version = "0.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" -dependencies = [ - "hashbrown", -] - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "memoffset" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" -dependencies = [ - "autocfg", -] - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "nitro-enclaves" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b5b539a76e3f555fb143c3e67d5e05fa1d5fece02a515f6ecf41b3f1a081f58" -dependencies = [ - "bitflags 2.11.0", - "libc", - "nix 0.26.4", - "rand", - "vsock", -] - -[[package]] -name = "nitro-enclaves" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6436c562bcdb6f192e0e59f627bff5b0b88f2e1c48264079f4f1d6da42bec2d" -dependencies = [ - "bitflags 2.11.0", - "libc", - "nix 0.26.4", - "vsock", -] - -[[package]] -name = "nix" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "libc", - "memoffset 0.7.1", - "pin-utils", -] - -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags 2.11.0", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset 0.9.1", -] - -[[package]] -name = "nix" -version = "0.31.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" -dependencies = [ - "bitflags 2.11.0", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset 0.9.1", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "nom" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "page_size" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pipewire" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9688b89abf11d756499f7c6190711d6dbe5a3acdb30c8fbf001d6596d06a8d44" -dependencies = [ - "anyhow", - "bitflags 2.11.0", - "libc", - "libspa", - "libspa-sys", - "nix 0.30.1", - "once_cell", - "pipewire-sys", - "thiserror 2.0.18", -] - -[[package]] -name = "pipewire-sys" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb028afee0d6ca17020b090e3b8fa2d7de23305aef975c7e5192a5050246ea36" -dependencies = [ - "bindgen", - "libspa-sys", - "system-deps", -] - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "plain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" - -[[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - -[[package]] -name = "portable-atomic-util" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "redox_syscall" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" -dependencies = [ - "bitflags 2.11.0", -] - -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" - -[[package]] -name = "remain" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7ef12e84481ab4006cb942f8682bba28ece7270743e649442027c5db87df126" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags 2.11.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_spanned" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" -dependencies = [ - "serde_core", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" -dependencies = [ - "libc", - "signal-hook-registry", -] - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "simd-adler32" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" - -[[package]] -name = "sm3" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebb9a3b702d0a7e33bc4d85a14456633d2b165c2ad839c5fd9a8417c1ab15860" -dependencies = [ - "digest", -] - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strum" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "system-deps" -version = "7.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7" -dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml", - "version-compare", -] - -[[package]] -name = "tar" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" -dependencies = [ - "filetime", - "libc", - "xattr", -] - -[[package]] -name = "target-lexicon" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" - -[[package]] -name = "tdx" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad59e5bf374211a1fdd8e7439a07d5a5e617fe97f5cf21d03bcd1bf8c82b73af" -dependencies = [ - "bitflags 2.11.0", - "iocuddle", - "kvm-bindings", - "kvm-ioctls", - "libc", - "uuid", - "vmm-sys-util 0.12.1", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio" -version = "1.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" -dependencies = [ - "pin-project-lite", -] - -[[package]] -name = "toml" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "toml_writer", - "winnow", -] - -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow", -] - -[[package]] -name = "toml_writer" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "unty" -version = "0.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "uuid" -version = "1.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "version-compare" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "virtio-bindings" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091f1f09cfbf2a78563b562e7a949465cce1aef63b6065645188d995162f8868" - -[[package]] -name = "virtue" -version = "0.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" - -[[package]] -name = "vm-fdt" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e21282841a059bb62627ce8441c491f09603622cd5a21c43bfedc85a2952f23" - -[[package]] -name = "vm-memory" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f39348a049689cabd3377cdd9182bf526ec76a6f823b79903896452e9d7a7380" -dependencies = [ - "libc", - "thiserror 2.0.18", - "winapi", -] - -[[package]] -name = "vmm-sys-util" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1435039746e20da4f8d507a72ee1b916f7b4b05af7a91c093d2c6561934ede" -dependencies = [ - "bitflags 1.3.2", - "libc", -] - -[[package]] -name = "vmm-sys-util" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d21f366bf22bfba3e868349978766a965cbe628c323d58e026be80b8357ab789" -dependencies = [ - "bitflags 1.3.2", - "libc", -] - -[[package]] -name = "vmm-sys-util" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "506c62fdf617a5176827c2f9afbcf1be155b03a9b4bf9617a60dbc07e3a1642f" -dependencies = [ - "bitflags 1.3.2", - "libc", -] - -[[package]] -name = "vsock" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b82aeb12ad864eb8cd26a6c21175d0bdc66d398584ee6c93c76964c3bcfc78ff" -dependencies = [ - "libc", - "nix 0.31.2", -] - -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "winnow" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" - -[[package]] -name = "xattr" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" -dependencies = [ - "libc", - "rustix", -] - -[[package]] -name = "zerocopy" -version = "0.8.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/vendor/libkrun/Cargo.toml b/vendor/libkrun/Cargo.toml deleted file mode 100644 index 0a4cc0fae..000000000 --- a/vendor/libkrun/Cargo.toml +++ /dev/null @@ -1,164 +0,0 @@ -# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO -# -# When uploading crates to the registry Cargo will automatically -# "normalize" Cargo.toml files for maximal compatibility -# with all versions of Cargo and also rewrite `path` dependencies -# to registry (e.g., crates.io) dependencies. -# -# If you are reading this file be aware that the original Cargo.toml -# will likely look very different (and much more reasonable). -# See Cargo.toml.orig for the original contents. - -[package] -edition = "2021" -name = "libkrun" -version = "1.19.3" -authors = ["The libkrun Authors"] -build = "build.rs" -autolib = false -autobins = false -autoexamples = false -autotests = false -autobenches = false -description = "A dynamic library providing Virtualization-based process isolation capabilities" -readme = false -license = "Apache-2.0" -repository = "https://github.com/containers/libkrun" - -[features] -amd-sev = [ - "blk", - "tee", - "vmm/amd-sev", - "devices/amd-sev", -] -aws-nitro = [ - "vmm/aws-nitro", - "devices/aws-nitro", - "dep:aws-nitro", - "dep:nitro-enclaves", -] -blk = [ - "devices/blk", - "vmm/blk", -] -default = ["init-blob"] -efi = [ - "blk", - "net", - "vmm/efi", - "devices/efi", -] -gpu = [ - "vmm/gpu", - "devices/gpu", - "krun_display", -] -init-blob = ["dep:init-blob"] -input = [ - "krun_input", - "vmm/input", - "devices/input", -] -net = [ - "devices/net", - "vmm/net", -] -snd = [ - "vmm/snd", - "devices/snd", -] -tdx = [ - "blk", - "tee", - "vmm/tdx", - "devices/tdx", -] -tee = [ - "vmm/tee", - "devices/tee", -] -virgl_resource_map2 = ["devices/virgl_resource_map2"] - -[lib] -name = "krun" -crate-type = [ - "cdylib", - "lib", -] -path = "src/lib.rs" - -[dependencies.crossbeam-channel] -version = ">=0.5.15" - -[dependencies.devices] -version = "=0.1.0-1.19.3" -package = "krun-devices" - -[dependencies.env_logger] -version = "0.11" - -[dependencies.init-blob] -version = "=0.1.0-1.19.3" -optional = true -package = "krun-init-blob" - -[dependencies.krun_display] -version = "0.1.0" -features = ["bindgen_clang_runtime"] -optional = true -package = "krun-display" - -[dependencies.krun_input] -version = "0.1.0" -features = ["bindgen_clang_runtime"] -optional = true -package = "krun-input" - -[dependencies.libc] -version = ">=0.2.39" - -[dependencies.libloading] -version = "0.8" - -[dependencies.log] -version = "0.4.0" - -[dependencies.once_cell] -version = "1.4.1" - -[dependencies.polly] -version = "=0.1.0-1.19.3" -package = "krun-polly" - -[dependencies.utils] -version = "=0.1.0-1.19.3" -package = "krun-utils" - -[dependencies.vmm] -version = "=0.1.0-1.19.3" -package = "krun-vmm" - -[target.'cfg(target_os = "linux")'.dependencies.aws-nitro] -version = "=0.1.0-1.19.3" -optional = true -package = "krun-aws-nitro" - -[target.'cfg(target_os = "linux")'.dependencies.kvm-bindings] -version = "0.12" -features = ["fam-wrappers"] - -[target.'cfg(target_os = "linux")'.dependencies.kvm-ioctls] -version = "0.22" - -[target.'cfg(target_os = "linux")'.dependencies.nitro-enclaves] -version = "0.5.0" -optional = true - -[target.'cfg(target_os = "linux")'.dependencies.vm-memory] -version = "0.17" -features = ["backend-mmap"] - -[target.'cfg(target_os = "macos")'.dependencies.hvf] -version = "=0.1.0-1.19.3" -package = "krun-hvf" diff --git a/vendor/libkrun/Cargo.toml.orig b/vendor/libkrun/Cargo.toml.orig deleted file mode 100644 index e3613a712..000000000 --- a/vendor/libkrun/Cargo.toml.orig +++ /dev/null @@ -1,54 +0,0 @@ -[package] -name = "libkrun" -version = "1.19.3" -authors = ["The libkrun Authors"] -edition = "2021" -description = "A dynamic library providing Virtualization-based process isolation capabilities" -build = "build.rs" -license = "Apache-2.0" -repository = "https://github.com/containers/libkrun" - -[features] -default = ["init-blob"] -init-blob = ["dep:init-blob"] -tee = ["vmm/tee", "devices/tee"] -amd-sev = ["blk", "tee", "vmm/amd-sev", "devices/amd-sev"] -tdx = ["blk", "tee", "vmm/tdx", "devices/tdx"] -net = ["devices/net", "vmm/net"] -blk = ["devices/blk", "vmm/blk"] -efi = ["blk", "net", "vmm/efi", "devices/efi"] -gpu = ["vmm/gpu", "devices/gpu", "krun_display"] -snd = ["vmm/snd", "devices/snd"] -input = ["krun_input", "vmm/input", "devices/input"] -virgl_resource_map2 = ["devices/virgl_resource_map2"] -aws-nitro = ["vmm/aws-nitro", "devices/aws-nitro", "dep:aws-nitro", "dep:nitro-enclaves"] - -[dependencies] -crossbeam-channel = ">=0.5.15" -env_logger = "0.11" -libc = ">=0.2.39" -libloading = "0.8" -log = "0.4.0" -once_cell = "1.4.1" -krun_display = { package = "krun-display", version = "0.1.0", path = "../display", optional = true, features = ["bindgen_clang_runtime"] } -krun_input = { package = "krun-input", version = "0.1.0", path = "../input", optional = true, features = ["bindgen_clang_runtime"] } - -devices = { package = "krun-devices", version = "=0.1.0-1.19.3", path = "../devices" } -init-blob = { package = "krun-init-blob", version = "=0.1.0-1.19.3", path = "../init_blob", optional = true } -polly = { package = "krun-polly", version = "=0.1.0-1.19.3", path = "../polly" } -utils = { package = "krun-utils", version = "=0.1.0-1.19.3", path = "../utils" } -vmm = { package = "krun-vmm", version = "=0.1.0-1.19.3", path = "../vmm" } - -[target.'cfg(target_os = "macos")'.dependencies] -hvf = { package = "krun-hvf", version = "=0.1.0-1.19.3", path = "../hvf" } - -[target.'cfg(target_os = "linux")'.dependencies] -kvm-bindings = { version = "0.12", features = ["fam-wrappers"] } -kvm-ioctls = "0.22" -aws-nitro = { package = "krun-aws-nitro", version = "=0.1.0-1.19.3", path = "../aws_nitro", optional = true } -nitro-enclaves = { version = "0.5.0", optional = true } -vm-memory = { version = "0.17", features = ["backend-mmap"] } - -[lib] -name = "krun" -crate-type = ["cdylib", "lib"] diff --git a/vendor/libkrun/build.rs b/vendor/libkrun/build.rs deleted file mode 100644 index 633dc9aff..000000000 --- a/vendor/libkrun/build.rs +++ /dev/null @@ -1,13 +0,0 @@ -fn main() { - #[cfg(target_os = "linux")] - println!( - "cargo:rustc-cdylib-link-arg=-Wl,-soname,libkrun.so.{}", - std::env::var("CARGO_PKG_VERSION_MAJOR").unwrap() - ); - #[cfg(target_os = "macos")] - println!( - "cargo:rustc-cdylib-link-arg=-Wl,-install_name,libkrun.{}.dylib,-compatibility_version,{}.0.0,-current_version,{}.{}.0", - std::env::var("CARGO_PKG_VERSION_MAJOR").unwrap(), std::env::var("CARGO_PKG_VERSION_MAJOR").unwrap(), - std::env::var("CARGO_PKG_VERSION_MAJOR").unwrap(), std::env::var("CARGO_PKG_VERSION_MINOR").unwrap() - ); -} diff --git a/vendor/libkrun/src/lib.rs b/vendor/libkrun/src/lib.rs deleted file mode 100644 index ef00e8eba..000000000 --- a/vendor/libkrun/src/lib.rs +++ /dev/null @@ -1,3274 +0,0 @@ -#[macro_use] -extern crate log; - -use crossbeam_channel::unbounded; -#[cfg(feature = "blk")] -use devices::virtio::block::{ImageType, SyncMode}; -#[cfg(not(any(feature = "tee", feature = "aws-nitro")))] -use devices::virtio::fs::passthrough::PermissionSemantics; -#[cfg(not(any(feature = "tee", feature = "aws-nitro")))] -use devices::virtio::fs::OverlayConfig; -#[cfg(feature = "gpu")] -use devices::virtio::gpu::display::DisplayInfo; -#[cfg(feature = "net")] -use devices::virtio::net::device::VirtioNetBackend; -#[cfg(feature = "blk")] -use devices::virtio::CacheType; -use env_logger::{Env, Target}; -#[cfg(feature = "gpu")] -use krun_display::DisplayBackend; - -#[cfg(not(any(feature = "tee", feature = "aws-nitro")))] -use devices::virtio::fs::virtual_entry::{VirtualDirEntry, VirtualEntry, VirtualEntryContent}; -use libc::{c_char, c_int, size_t}; -use once_cell::sync::Lazy; -use polly::event_manager::EventManager; -use std::collections::hash_map::Entry; -use std::collections::HashMap; -use std::convert::TryInto; -use std::env; -use std::ffi::CString; -use std::ffi::{c_void, CStr}; -use std::fs::File; -use std::io::IsTerminal; -#[cfg(target_os = "linux")] -use std::os::fd::AsRawFd; -use std::os::fd::{BorrowedFd, FromRawFd, RawFd}; -use std::path::PathBuf; -use std::slice; -use std::sync::atomic::{AtomicI32, Ordering}; -use std::sync::LazyLock; -use std::sync::Mutex; -use utils::eventfd::EventFd; -use vmm::resources::{ - DefaultVirtioConsoleConfig, PortConfig, SerialConsoleConfig, TsiFlags, VirtioConsoleConfigMode, - VmResources, VsockConfig, -}; -#[cfg(feature = "blk")] -use vmm::vmm_config::block::{BlockDeviceConfig, BlockRootConfig}; -#[cfg(not(feature = "tee"))] -use vmm::vmm_config::external_kernel::{ExternalKernel, KernelFormat}; -#[cfg(not(feature = "tee"))] -use vmm::vmm_config::firmware::FirmwareConfig; -#[cfg(not(feature = "tee"))] -use vmm::vmm_config::fs::FsDeviceConfig; -use vmm::vmm_config::kernel_bundle::KernelBundle; -#[cfg(feature = "tee")] -use vmm::vmm_config::kernel_bundle::{InitrdBundle, QbootBundle}; -use vmm::vmm_config::kernel_cmdline::{KernelCmdlineConfig, DEFAULT_KERNEL_CMDLINE}; -use vmm::vmm_config::machine_config::VmConfig; -#[cfg(feature = "net")] -use vmm::vmm_config::net::NetworkInterfaceConfig; -use vmm::vmm_config::vsock::VsockDeviceConfig; - -#[cfg(feature = "aws-nitro")] -use aws_nitro::enclave::NitroEnclave; - -#[cfg(feature = "gpu")] -use devices::virtio::display::{DisplayInfoEdid, PhysicalSize, MAX_DISPLAYS}; -#[cfg(feature = "input")] -use krun_input::{InputConfigBackend, InputEventProviderBackend}; - -// Value returned on success. We use libc's errors otherwise. -const KRUN_SUCCESS: i32 = 0; -// Maximum number of arguments/environment variables we allow -const MAX_ARGS: usize = 4096; - -// krunfw library name for each context -#[cfg(all(target_os = "linux", not(feature = "tee")))] -const KRUNFW_NAME: &str = "libkrunfw.so.5"; -#[cfg(all(target_os = "linux", feature = "amd-sev"))] -const KRUNFW_NAME: &str = "libkrunfw-sev.so.5"; -#[cfg(all(target_os = "linux", feature = "tdx"))] -const KRUNFW_NAME: &str = "libkrunfw-tdx.so.5"; -#[cfg(target_os = "macos")] -const KRUNFW_NAME: &str = "libkrunfw.5.dylib"; - -#[cfg(feature = "aws-nitro")] -static KRUN_NITRO_DEBUG: Mutex = Mutex::new(false); - -// Path to the init binary to be executed inside the VM. -const INIT_PATH: &str = "/init.krun"; - -#[cfg(all( - feature = "init-blob", - not(any(feature = "tee", feature = "aws-nitro")) -))] -const DEFAULT_INIT_PAYLOAD: &[u8] = init_blob::INIT_BINARY; - -#[cfg(all( - feature = "init-blob", - not(any(feature = "tee", feature = "aws-nitro")) -))] -fn init_virtual_entry() -> VirtualDirEntry { - VirtualDirEntry { - name: CString::new("init.krun").unwrap(), - entry: VirtualEntry { - mode: 0o755, - one_shot: true, - content: VirtualEntryContent::File { - data: DEFAULT_INIT_PAYLOAD, - }, - }, - } -} - -static KRUNFW: LazyLock> = - LazyLock::new(|| unsafe { libloading::Library::new(KRUNFW_NAME).ok() }); - -pub struct KrunfwBindings { - get_kernel: libloading::Symbol< - 'static, - unsafe extern "C" fn(*mut u64, *mut u64, *mut size_t) -> *mut c_char, - >, - #[cfg(feature = "tee")] - get_initrd: libloading::Symbol<'static, unsafe extern "C" fn(*mut size_t) -> *mut c_char>, - #[cfg(feature = "tee")] - get_qboot: libloading::Symbol<'static, unsafe extern "C" fn(*mut size_t) -> *mut c_char>, -} - -impl KrunfwBindings { - fn load_bindings() -> Result { - let krunfw = match KRUNFW.as_ref() { - Some(krunfw) => krunfw, - None => return Err(libloading::Error::DlOpenUnknown), - }; - Ok(unsafe { - KrunfwBindings { - get_kernel: krunfw.get(b"krunfw_get_kernel")?, - #[cfg(feature = "tee")] - get_initrd: krunfw.get(b"krunfw_get_initrd")?, - #[cfg(feature = "tee")] - get_qboot: krunfw.get(b"krunfw_get_qboot")?, - } - }) - } - - pub fn new() -> Option { - Self::load_bindings().ok() - } -} - -#[derive(Clone)] -#[cfg(feature = "net")] -enum LegacyNetworkConfig { - VirtioNetPasst(RawFd), - VirtioNetGvproxy(PathBuf), -} - -#[derive(Default)] -struct ContextConfig { - krunfw: Option, - vmr: VmResources, - workdir: Option, - exec_path: Option, - env: Option, - args: Option, - rlimits: Option, - #[cfg(feature = "net")] - legacy_net_cfg: Option, - #[cfg(feature = "net")] - legacy_mac: Option<[u8; 6]>, - net_index: u8, - tsi_port_map: Option>, - vsock_config: VsockConfig, - #[cfg(feature = "blk")] - block_cfgs: Vec, - #[cfg(feature = "blk")] - root_block_cfg: Option, - #[cfg(feature = "blk")] - data_block_cfg: Option, - #[cfg(feature = "blk")] - block_root: Option, - #[cfg(feature = "tee")] - tee_config_file: Option, - unix_ipc_port_map: Option>, - shutdown_efd: Option, - gpu_virgl_flags: Option, - gpu_shm_size: Option, - enable_snd: bool, - console_output: Option, - vmm_uid: Option, - vmm_gid: Option, - #[cfg(all( - feature = "init-blob", - not(any(feature = "tee", feature = "aws-nitro")) - ))] - disable_implicit_init: bool, -} - -impl ContextConfig { - fn set_workdir(&mut self, workdir: String) { - self.workdir = Some(workdir); - } - - fn get_workdir(&self) -> String { - match &self.workdir { - Some(workdir) => format!("KRUN_WORKDIR={workdir}"), - None => "".to_string(), - } - } - - fn set_exec_path(&mut self, exec_path: String) { - self.exec_path = Some(exec_path); - } - - fn get_exec_path(&self) -> String { - match &self.exec_path { - Some(exec_path) => format!("KRUN_INIT={exec_path}"), - None => "".to_string(), - } - } - - #[cfg(all(feature = "blk", not(feature = "tee")))] - fn set_block_root(&mut self, device: String, fstype: Option, options: Option) { - self.block_root = Some(BlockRootConfig { - device, - fstype, - options, - }); - } - - fn get_block_root(&self) -> String { - #[cfg(feature = "blk")] - match &self.block_root { - Some(block_root) => { - let mut res = format!("KRUN_BLOCK_ROOT_DEVICE={}", block_root.device); - if let Some(fstype) = &block_root.fstype { - res += &format!(" KRUN_BLOCK_ROOT_FSTYPE={fstype}"); - } - if let Some(options) = &block_root.options { - res += &format!(" KRUN_BLOCK_ROOT_OPTIONS={options}"); - } - res - } - None => "".to_string(), - } - #[cfg(not(feature = "blk"))] - "".to_string() - } - - fn set_env(&mut self, env: String) { - self.env = Some(env); - } - - fn get_env(&self) -> String { - match &self.env { - Some(env) => env.clone(), - None => "".to_string(), - } - } - - fn set_args(&mut self, args: String) { - self.args = Some(args); - } - - fn get_args(&self) -> String { - match &self.args { - Some(args) => args.clone(), - None => "".to_string(), - } - } - - fn set_rlimits(&mut self, rlimits: String) { - self.rlimits = Some(rlimits); - } - - fn get_rlimits(&self) -> String { - match &self.rlimits { - Some(rlimits) => format!("KRUN_RLIMITS={rlimits}"), - None => "".to_string(), - } - } - - #[cfg(feature = "blk")] - fn add_block_cfg(&mut self, block_cfg: BlockDeviceConfig) { - self.block_cfgs.push(block_cfg); - } - - #[cfg(feature = "blk")] - fn set_root_block_cfg(&mut self, block_cfg: BlockDeviceConfig) { - self.root_block_cfg = Some(block_cfg); - } - - #[cfg(feature = "blk")] - fn set_data_block_cfg(&mut self, block_cfg: BlockDeviceConfig) { - self.data_block_cfg = Some(block_cfg); - } - - #[cfg(feature = "blk")] - fn get_block_cfg(&self) -> Vec { - // For backwards compat, when cfgs is empty (the new API is not used), this needs to be - // root and then data, in that order. Also for backwards compat, root/data are setters and - // need to discard redundant calls. So we have simple setters above and fix up here. - // - // When the new API is used, this is simpler. - if self.block_cfgs.is_empty() { - [&self.root_block_cfg, &self.data_block_cfg] - .into_iter() - .filter_map(|cfg| cfg.clone()) - .collect() - } else { - self.block_cfgs.clone() - } - } - - #[cfg(feature = "net")] - fn set_net_mac(&mut self, mac: [u8; 6]) { - self.legacy_mac = Some(mac); - } - - fn set_port_map(&mut self, new_port_map: HashMap) -> Result<(), ()> { - if self.net_index != 0 { - return Err(()); - } - - self.tsi_port_map.replace(new_port_map); - Ok(()) - } - - #[cfg(feature = "tee")] - fn set_tee_config_file(&mut self, filepath: PathBuf) { - self.tee_config_file = Some(filepath); - } - - #[cfg(feature = "tee")] - fn get_tee_config_file(&self) -> Option { - self.tee_config_file.clone() - } - - fn add_vsock_port(&mut self, port: u32, filepath: PathBuf, listen: bool) { - if let Some(ref mut map) = &mut self.unix_ipc_port_map { - map.insert(port, (filepath, listen)); - } else { - let mut map: HashMap = HashMap::new(); - map.insert(port, (filepath, listen)); - self.unix_ipc_port_map = Some(map); - } - } - - fn set_gpu_virgl_flags(&mut self, virgl_flags: u32) { - self.gpu_virgl_flags = Some(virgl_flags); - } - - fn set_gpu_shm_size(&mut self, shm_size: usize) { - self.gpu_shm_size = Some(shm_size); - } - - fn set_vmm_uid(&mut self, vmm_uid: libc::uid_t) { - self.vmm_uid = Some(vmm_uid); - } - - fn set_vmm_gid(&mut self, vmm_gid: libc::gid_t) { - self.vmm_gid = Some(vmm_gid); - } -} - -#[cfg(feature = "aws-nitro")] -impl TryFrom for NitroEnclave { - type Error = i32; - - fn try_from(ctx: ContextConfig) -> Result { - let vm_config = ctx.vmr.vm_config(); - - let Some(mem_size_mib) = vm_config.mem_size_mib else { - error!("memory size not configured"); - return Err(-libc::EINVAL); - }; - - let Some(vcpus) = vm_config.vcpu_count else { - error!("vCPU count not configured"); - return Err(-libc::EINVAL); - }; - - let rootfs = if let Some(path) = &ctx.vmr.fs.first() { - path.shared_dir.clone() - } else { - error!("rootfs path required"); - return Err(-libc::EINVAL); - }; - - let Some(exec_path) = ctx.exec_path else { - error!("exec path not specified"); - return Err(-libc::EINVAL); - }; - - let Some(exec_env) = ctx.env else { - error!("execution env not specified"); - return Err(-libc::EINVAL); - }; - - let Some(exec_args) = ctx.args else { - error!("execution args not specified"); - return Err(-libc::EINVAL); - }; - - let net_unixfd = { - let mut list = ctx.vmr.net.list; - let len = list.len(); - match len { - 0 => None, - 1 => { - let device = list.pop_front().unwrap(); - let device = device.lock().unwrap(); - - let fd = match device.cfg_backend { - VirtioNetBackend::UnixstreamFd(fd) => RawFd::from(fd), - _ => return Err(libc::EINVAL), - }; - - Some(fd) - } - _ => { - error!( - "more than one network interface configured (max 1 allowed, found {len})" - ); - return Err(-libc::EINVAL); - } - } - }; - - let Some(output_path) = ctx.console_output else { - error!("console output path not specified"); - return Err(-libc::EINVAL); - }; - - let debug = KRUN_NITRO_DEBUG.lock().unwrap(); - - Ok(Self { - mem_size_mib, - vcpus, - rootfs, - exec_path, - exec_args, - exec_env, - net_unixfd, - output_path, - debug: *debug, - }) - } -} - -// TODO: Use this everywhere instead of the manual match -#[allow(dead_code)] -fn with_cfg(ctx_id: u32, f: impl FnOnce(&mut ContextConfig) -> i32) -> i32 { - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => f(ctx_cfg.get_mut()), - Entry::Vacant(_) => -libc::ENOENT, - } -} - -static CTX_MAP: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); -static CTX_IDS: AtomicI32 = AtomicI32::new(0); - -fn log_level_to_filter_str(level: u32) -> &'static str { - match level { - 0 => "off", - 1 => "error", - 2 => "warn", - 3 => "info", - 4 => "debug", - _ => "trace", - } -} - -#[no_mangle] -pub extern "C" fn krun_set_log_level(level: u32) -> i32 { - let filter = log_level_to_filter_str(level); - env_logger::Builder::from_env(Env::default().default_filter_or(filter)) - .format_timestamp_micros() - .init(); - - #[cfg(feature = "aws-nitro")] - { - // Notify krun-awsnitro to enable debug for log level. - if level == 4 { - let mut debug = KRUN_NITRO_DEBUG.lock().unwrap(); - - *debug = true; - } - } - - KRUN_SUCCESS -} - -mod log_defs { - pub const KRUN_LOG_STYLE_AUTO: u32 = 0; - pub const KRUN_LOG_STYLE_ALWAYS: u32 = 1; - pub const KRUN_LOG_STYLE_NEVER: u32 = 2; - pub const KRUN_LOG_OPTION_NO_ENV: u32 = 1; -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub unsafe extern "C" fn krun_init_log(target: RawFd, level: u32, style: u32, options: u32) -> i32 { - let target = match target { - ..-1 => return -libc::EINVAL, - -1 => Target::default(), - 0 /* stdin */ => return -libc::EINVAL, - 1 /* stdout */ => Target::Stdout, - 2 /* stderr */ => Target::Stderr, - fd => Target::Pipe(Box::new(File::from_raw_fd(fd))), - }; - - let filter = log_level_to_filter_str(level); - - let write_style = match style { - log_defs::KRUN_LOG_STYLE_AUTO => "auto", - log_defs::KRUN_LOG_STYLE_ALWAYS => "always", - log_defs::KRUN_LOG_STYLE_NEVER => "never", - _ => return -libc::EINVAL, - }; - - let use_env = match options { - 0 => true, - log_defs::KRUN_LOG_OPTION_NO_ENV => false, - _ => return -libc::EINVAL, - }; - - let mut builder = if use_env { - env_logger::Builder::from_env( - Env::new() - .default_filter_or(filter) - .default_write_style_or(write_style), - ) - } else { - let mut builder = env_logger::Builder::new(); - builder.parse_filters(filter).parse_write_style(write_style); - builder - }; - builder.format_timestamp_micros().target(target).init(); - - KRUN_SUCCESS -} - -#[no_mangle] -pub extern "C" fn krun_create_ctx() -> i32 { - let shutdown_efd = if cfg!(target_arch = "aarch64") && cfg!(target_os = "macos") { - Some(EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap()) - } else { - None - }; - - let ctx_cfg = { - ContextConfig { - krunfw: KrunfwBindings::new(), - shutdown_efd, - ..Default::default() - } - }; - - let ctx_id = CTX_IDS.fetch_add(1, Ordering::SeqCst); - if ctx_id == i32::MAX || CTX_MAP.lock().unwrap().contains_key(&(ctx_id as u32)) { - // libkrun is not intended to be used as a daemon for managing VMs. - panic!("Context ID namespace exhausted"); - } - CTX_MAP.lock().unwrap().insert(ctx_id as u32, ctx_cfg); - - ctx_id -} - -#[no_mangle] -pub extern "C" fn krun_free_ctx(ctx_id: u32) -> i32 { - match CTX_MAP.lock().unwrap().remove(&ctx_id) { - Some(_) => KRUN_SUCCESS, - None => -libc::ENOENT, - } -} - -#[no_mangle] -pub extern "C" fn krun_set_vm_config(ctx_id: u32, num_vcpus: u8, ram_mib: u32) -> i32 { - let mem_size_mib: usize = match ram_mib.try_into() { - Ok(size) => size, - Err(e) => { - warn!("Error parsing the amount of RAM: {e:?}"); - return -libc::EINVAL; - } - }; - - let vm_config = VmConfig { - vcpu_count: Some(num_vcpus), - mem_size_mib: Some(mem_size_mib), - ht_enabled: Some(false), - cpu_template: None, - }; - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - if ctx_cfg.get_mut().vmr.set_vm_config(&vm_config).is_err() { - return -libc::EINVAL; - } - } - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -#[cfg(not(any(feature = "tee", feature = "aws-nitro")))] -pub unsafe extern "C" fn krun_set_root(ctx_id: u32, c_root_path: *const c_char) -> i32 { - let root_path = match CStr::from_ptr(c_root_path).to_str() { - Ok(root) => root, - Err(_) => return -libc::EINVAL, - }; - - let fs_id = "/dev/root".to_string(); - let shared_dir = root_path.to_string(); - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - cfg.vmr.add_fs_device(FsDeviceConfig { - fs_id, - shared_dir: Some(shared_dir), - semantics: PermissionSemantics::LinuxComplete, - // Default to a conservative 512 MB window. - shm_size: Some(1 << 29), - read_only: false, - overlay: None, - virtual_entries: { - #[allow(unused_mut)] - let mut v = Vec::new(); - #[cfg(feature = "init-blob")] - if !cfg.disable_implicit_init { - v.push(init_virtual_entry()); - } - v - }, - }); - } - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -#[cfg(not(any(feature = "tee", feature = "aws-nitro")))] -pub unsafe extern "C" fn krun_add_virtiofs( - ctx_id: u32, - c_tag: *const c_char, - c_path: *const c_char, -) -> i32 { - krun_add_virtiofs4(ctx_id, c_tag, c_path, 0, false, 0) -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -#[cfg(not(any(feature = "tee", feature = "aws-nitro")))] -pub unsafe extern "C" fn krun_add_virtiofs2( - ctx_id: u32, - c_tag: *const c_char, - c_path: *const c_char, - shm_size: u64, -) -> i32 { - krun_add_virtiofs4(ctx_id, c_tag, c_path, shm_size, false, 0) -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -#[cfg(not(any(feature = "tee", feature = "aws-nitro")))] -pub unsafe extern "C" fn krun_add_virtiofs3( - ctx_id: u32, - c_tag: *const c_char, - c_path: *const c_char, - shm_size: u64, - read_only: bool, -) -> i32 { - krun_add_virtiofs4(ctx_id, c_tag, c_path, shm_size, read_only, 0) -} - -/// Add a copy-on-write virtio-fs device without creating a host FUSE mount. -/// -/// `lower_paths` are ordered from highest to lowest precedence. Guest writes -/// are stored in `upper_path`; whiteouts use the portable `.wh.*` format. -#[allow(clippy::missing_safety_doc, clippy::too_many_arguments)] -#[unsafe(no_mangle)] -#[cfg(not(any(feature = "tee", feature = "aws-nitro")))] -pub unsafe extern "C" fn krun_add_virtiofs_overlay( - ctx_id: u32, - c_tag: *const c_char, - lower_paths: *const *const c_char, - lower_count: usize, - c_upper_path: *const c_char, - c_work_path: *const c_char, - c_preimage_path: *const c_char, - excluded_paths: *const *const c_char, - excluded_count: usize, - shm_size: u64, -) -> i32 { - if c_tag.is_null() - || c_upper_path.is_null() - || lower_paths.is_null() - || lower_count == 0 - || (excluded_count > 0 && excluded_paths.is_null()) - { - return -libc::EINVAL; - } - let parse = |pointer: *const c_char| { - if pointer.is_null() { - return Err(()); - } - CStr::from_ptr(pointer) - .to_str() - .map(str::to_owned) - .map_err(|_| ()) - }; - let tag = match parse(c_tag) { - Ok(value) => value, - Err(()) => return -libc::EINVAL, - }; - let upper_dir = match parse(c_upper_path) { - Ok(value) => value, - Err(()) => return -libc::EINVAL, - }; - let work_dir = if c_work_path.is_null() { - None - } else { - match parse(c_work_path) { - Ok(value) => Some(value), - Err(()) => return -libc::EINVAL, - } - }; - let preimage_dir = if c_preimage_path.is_null() { - None - } else { - match parse(c_preimage_path) { - Ok(value) => Some(value), - Err(()) => return -libc::EINVAL, - } - }; - let lowers = slice::from_raw_parts(lower_paths, lower_count) - .iter() - .map(|pointer| parse(*pointer)) - .collect::, _>>(); - let lower_dirs = match lowers { - Ok(value) => value, - Err(()) => return -libc::EINVAL, - }; - let excluded = if excluded_count == 0 { - Vec::new() - } else { - match slice::from_raw_parts(excluded_paths, excluded_count) - .iter() - .map(|pointer| parse(*pointer)) - .collect::, _>>() - { - Ok(value) => value, - Err(()) => return -libc::EINVAL, - } - }; - let shm_size = if shm_size == 0 { - None - } else { - match shm_size.try_into() { - Ok(value) => Some(value), - Err(_) => return -libc::EINVAL, - } - }; - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - #[allow(unused_mut)] - let mut virtual_entries = Vec::new(); - #[cfg(feature = "init-blob")] - if tag == "/dev/root" && !cfg.disable_implicit_init { - virtual_entries.push(init_virtual_entry()); - } - cfg.vmr.add_fs_device(FsDeviceConfig { - fs_id: tag, - shared_dir: None, - semantics: PermissionSemantics::LinuxComplete, - shm_size, - read_only: false, - overlay: Some(OverlayConfig { - lower_dirs, - upper_dir, - work_dir, - preimage_dir, - excluded_paths: excluded, - semantics: PermissionSemantics::LinuxComplete, - }), - virtual_entries, - }); - } - Entry::Vacant(_) => return -libc::ENOENT, - } - KRUN_SUCCESS -} - -#[allow(clippy::missing_safety_doc)] -#[unsafe(no_mangle)] -#[cfg(not(any(feature = "tee", feature = "aws-nitro")))] -pub unsafe extern "C" fn krun_add_virtiofs4( - ctx_id: u32, - c_tag: *const c_char, - c_path: *const c_char, - shm_size: u64, - read_only: bool, - semantics: u32, -) -> i32 { - let semantics = match PermissionSemantics::try_from(semantics) { - Ok(semantics) => semantics, - Err(_) => return -libc::EINVAL, - }; - - if c_tag.is_null() { - return -libc::EINVAL; - } - - let tag = match CStr::from_ptr(c_tag).to_str() { - Ok(tag) => tag, - Err(_) => return -libc::EINVAL, - }; - - // NULL path means NullFs (virtual-only filesystem, no host directory). - let path = if c_path.is_null() { - None - } else { - match CStr::from_ptr(c_path).to_str() { - Ok(path) => Some(path), - Err(_) => return -libc::EINVAL, - } - }; - - let shm = if shm_size > 0 { - match shm_size.try_into() { - Ok(s) => Some(s), - Err(_) => return -libc::EINVAL, - } - } else { - None - }; - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - #[allow(unused_mut)] - let mut virtual_entries = Vec::new(); - #[cfg(feature = "init-blob")] - if tag == "/dev/root" && !cfg.disable_implicit_init { - virtual_entries.push(init_virtual_entry()); - } - cfg.vmr.add_fs_device(FsDeviceConfig { - fs_id: tag.to_string(), - shared_dir: path.map(|p| p.to_string()), - semantics, - shm_size: shm, - read_only, - overlay: None, - virtual_entries, - }); - } - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -#[cfg(not(feature = "tee"))] -pub unsafe extern "C" fn krun_set_mapped_volumes( - _ctx_id: u32, - _c_mapped_volumes: *const *const c_char, -) -> i32 { - -libc::EINVAL -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -#[cfg(feature = "blk")] -pub unsafe extern "C" fn krun_add_disk( - ctx_id: u32, - c_block_id: *const c_char, - c_disk_path: *const c_char, - read_only: bool, -) -> i32 { - let disk_path = match CStr::from_ptr(c_disk_path).to_str() { - Ok(disk) => disk, - Err(_) => return -libc::EINVAL, - }; - - let block_id = match CStr::from_ptr(c_block_id).to_str() { - Ok(block_id) => block_id, - Err(_) => return -libc::EINVAL, - }; - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - let block_device_config = BlockDeviceConfig { - block_id: block_id.to_string(), - cache_type: CacheType::auto(disk_path), - disk_image_path: disk_path.to_string(), - disk_image_format: ImageType::Raw, - is_disk_read_only: read_only, - direct_io: false, - #[cfg(not(target_os = "macos"))] - sync_mode: SyncMode::Full, - #[cfg(target_os = "macos")] - sync_mode: SyncMode::Relaxed, - }; - cfg.add_block_cfg(block_device_config); - } - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -#[cfg(feature = "blk")] -pub unsafe extern "C" fn krun_add_disk2( - ctx_id: u32, - c_block_id: *const c_char, - c_disk_path: *const c_char, - disk_format: u32, - read_only: bool, -) -> i32 { - let disk_path = match CStr::from_ptr(c_disk_path).to_str() { - Ok(disk) => disk, - Err(_) => return -libc::EINVAL, - }; - - let block_id = match CStr::from_ptr(c_block_id).to_str() { - Ok(block_id) => block_id, - Err(_) => return -libc::EINVAL, - }; - - let format = match ImageType::try_from(disk_format) { - Ok(format) => format, - Err(_) => return -libc::EINVAL, - }; - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - let block_device_config = BlockDeviceConfig { - block_id: block_id.to_string(), - cache_type: CacheType::auto(disk_path), - disk_image_path: disk_path.to_string(), - disk_image_format: format, - is_disk_read_only: read_only, - direct_io: false, - #[cfg(not(target_os = "macos"))] - sync_mode: SyncMode::Full, - #[cfg(target_os = "macos")] - sync_mode: SyncMode::Relaxed, - }; - cfg.add_block_cfg(block_device_config); - } - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -#[cfg(feature = "blk")] -pub unsafe extern "C" fn krun_add_disk3( - ctx_id: u32, - c_block_id: *const c_char, - c_disk_path: *const c_char, - disk_format: u32, - read_only: bool, - direct_io: bool, - sync_mode: u32, -) -> i32 { - let disk_path = match CStr::from_ptr(c_disk_path).to_str() { - Ok(disk) => disk, - Err(_) => return -libc::EINVAL, - }; - - let block_id = match CStr::from_ptr(c_block_id).to_str() { - Ok(block_id) => block_id, - Err(_) => return -libc::EINVAL, - }; - - let format = match ImageType::try_from(disk_format) { - Ok(fmt) => fmt, - Err(_) => return -libc::EINVAL, - }; - - let sync_mode = match SyncMode::try_from(sync_mode) { - Ok(mode) => mode, - Err(_) => return -libc::EINVAL, - }; - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - let block_device_config = BlockDeviceConfig { - block_id: block_id.to_string(), - cache_type: CacheType::auto(disk_path), - disk_image_path: disk_path.to_string(), - disk_image_format: format, - is_disk_read_only: read_only, - direct_io, - sync_mode, - }; - cfg.add_block_cfg(block_device_config); - } - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -#[cfg(feature = "blk")] -pub unsafe extern "C" fn krun_set_root_disk(ctx_id: u32, c_disk_path: *const c_char) -> i32 { - let disk_path = match CStr::from_ptr(c_disk_path).to_str() { - Ok(disk) => disk, - Err(_) => return -libc::EINVAL, - }; - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - let block_device_config = BlockDeviceConfig { - block_id: "root".to_string(), - cache_type: CacheType::auto(disk_path), - disk_image_path: disk_path.to_string(), - disk_image_format: ImageType::Raw, - is_disk_read_only: false, - direct_io: false, - #[cfg(not(target_os = "macos"))] - sync_mode: SyncMode::Full, - #[cfg(target_os = "macos")] - sync_mode: SyncMode::Relaxed, - }; - cfg.set_root_block_cfg(block_device_config); - } - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -#[cfg(feature = "blk")] -pub unsafe extern "C" fn krun_set_data_disk(ctx_id: u32, c_disk_path: *const c_char) -> i32 { - let disk_path = match CStr::from_ptr(c_disk_path).to_str() { - Ok(disk) => disk, - Err(_) => return -libc::EINVAL, - }; - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - let block_device_config = BlockDeviceConfig { - block_id: "data".to_string(), - cache_type: CacheType::auto(disk_path), - disk_image_path: disk_path.to_string(), - disk_image_format: ImageType::Raw, - is_disk_read_only: false, - direct_io: false, - #[cfg(not(target_os = "macos"))] - sync_mode: SyncMode::Full, - #[cfg(target_os = "macos")] - sync_mode: SyncMode::Relaxed, - }; - cfg.set_data_block_cfg(block_device_config); - } - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -/* - * Send the VFKIT magic after establishing the connection, - * as required by gvproxy in vfkit mode. - */ -#[cfg(feature = "net")] -const NET_FLAG_VFKIT: u32 = 1 << 0; -#[cfg(feature = "net")] -const NET_FLAG_DHCP_CLIENT: u32 = 1 << 1; -#[cfg(feature = "net")] -const NET_FLAG_ALL: u32 = NET_FLAG_VFKIT | NET_FLAG_DHCP_CLIENT; - -/* Taken from uapi/linux/virtio_net.h */ -#[cfg(feature = "net")] -const NET_FEATURE_CSUM: u32 = 1 << 0; -#[cfg(feature = "net")] -const NET_FEATURE_GUEST_CSUM: u32 = 1 << 1; -#[cfg(feature = "net")] -const NET_FEATURE_GUEST_TSO4: u32 = 1 << 7; -#[cfg(feature = "net")] -const NET_FEATURE_GUEST_TSO6: u32 = 1 << 8; -#[cfg(feature = "net")] -const NET_FEATURE_GUEST_UFO: u32 = 1 << 10; -#[cfg(feature = "net")] -const NET_FEATURE_HOST_TSO4: u32 = 1 << 11; -#[cfg(feature = "net")] -const NET_FEATURE_HOST_TSO6: u32 = 1 << 12; -#[cfg(feature = "net")] -const NET_FEATURE_HOST_UFO: u32 = 1 << 14; -/* - * These are the flags enabled by default on each virtio-net instance - * before the introduction of "krun_add_net_*". They are now used in - * the legacy API ("krun_set_passt_fd" and "krun_set_gvproxy_path") - * for compatiblity reasons. - */ -#[cfg(feature = "net")] -const NET_COMPAT_FEATURES: u32 = NET_FEATURE_CSUM - | NET_FEATURE_GUEST_CSUM - | NET_FEATURE_GUEST_TSO4 - | NET_FEATURE_GUEST_UFO - | NET_FEATURE_HOST_TSO4 - | NET_FEATURE_HOST_UFO; -#[cfg(feature = "net")] -const NET_ALL_FEATURES: u32 = NET_FEATURE_CSUM - | NET_FEATURE_GUEST_CSUM - | NET_FEATURE_GUEST_TSO4 - | NET_FEATURE_GUEST_TSO6 - | NET_FEATURE_GUEST_UFO - | NET_FEATURE_HOST_TSO4 - | NET_FEATURE_HOST_TSO6 - | NET_FEATURE_HOST_UFO; - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -#[cfg(feature = "net")] -pub unsafe extern "C" fn krun_add_net_unixstream( - ctx_id: u32, - c_path: *const c_char, - fd: c_int, - c_mac: *const u8, - features: u32, - flags: u32, -) -> i32 { - let path = if !c_path.is_null() { - match CStr::from_ptr(c_path).to_str() { - Ok(path) => Some(PathBuf::from(path)), - Err(_) => None, - } - } else { - None - }; - - if fd >= 0 && path.is_some() { - return -libc::EINVAL; - } - if fd < 0 && path.is_none() { - return -libc::EINVAL; - } - let backend = if let Some(path) = path { - VirtioNetBackend::UnixstreamPath(path) - } else { - VirtioNetBackend::UnixstreamFd(fd) - }; - - let mac: [u8; 6] = match slice::from_raw_parts(c_mac, 6).try_into() { - Ok(m) => m, - Err(_) => return -libc::EINVAL, - }; - - if (flags & !NET_FLAG_DHCP_CLIENT) != 0 { - return -libc::EINVAL; - } - let enable_dhcp_client: bool = flags & NET_FLAG_DHCP_CLIENT != 0; - - if (features & !NET_ALL_FEATURES) != 0 { - return -libc::EINVAL; - } - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - create_virtio_net(cfg, backend, mac, features); - if enable_dhcp_client { - cfg.vmr.dhcp_client = true; - } - } - Entry::Vacant(_) => return -libc::ENOENT, - } - KRUN_SUCCESS -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -#[cfg(feature = "net")] -pub unsafe extern "C" fn krun_add_net_unixgram( - ctx_id: u32, - c_path: *const c_char, - fd: c_int, - c_mac: *const u8, - features: u32, - flags: u32, -) -> i32 { - let path = if !c_path.is_null() { - match CStr::from_ptr(c_path).to_str() { - Ok(path) => Some(PathBuf::from(path)), - Err(_) => None, - } - } else { - None - }; - - if fd >= 0 && path.is_some() { - return -libc::EINVAL; - } - if fd < 0 && path.is_none() { - return -libc::EINVAL; - } - - let mac: [u8; 6] = match slice::from_raw_parts(c_mac, 6).try_into() { - Ok(m) => m, - Err(_) => return -libc::EINVAL, - }; - - if (features & !NET_ALL_FEATURES) != 0 { - return -libc::EINVAL; - } - - if (flags & !NET_FLAG_ALL) != 0 { - return -libc::EINVAL; - } - let send_vfkit_magic: bool = flags & NET_FLAG_VFKIT != 0; - let enable_dhcp_client: bool = flags & NET_FLAG_DHCP_CLIENT != 0; - - let backend = if let Some(path) = path { - VirtioNetBackend::UnixgramPath(path, send_vfkit_magic) - } else { - VirtioNetBackend::UnixgramFd(fd) - }; - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - create_virtio_net(cfg, backend, mac, features); - if enable_dhcp_client { - cfg.vmr.dhcp_client = true; - } - } - Entry::Vacant(_) => return -libc::ENOENT, - } - KRUN_SUCCESS -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -#[cfg(all(target_os = "linux", feature = "net"))] -pub unsafe extern "C" fn krun_add_net_tap( - ctx_id: u32, - c_tap_name: *const c_char, - c_mac: *const u8, - features: u32, - flags: u32, -) -> i32 { - let tap_name = match CStr::from_ptr(c_tap_name).to_str() { - Ok(tap_name) => tap_name.to_string(), - Err(e) => { - debug!("Error parsing tap_name: {e:?}"); - return -libc::EINVAL; - } - }; - - let mac: [u8; 6] = match slice::from_raw_parts(c_mac, 6).try_into() { - Ok(m) => m, - Err(_) => return -libc::EINVAL, - }; - - if (features & !NET_ALL_FEATURES) != 0 { - return -libc::EINVAL; - } - - if features & (NET_FEATURE_GUEST_TSO4 | NET_FEATURE_GUEST_TSO6 | NET_FEATURE_GUEST_UFO) != 0 - && features & NET_FEATURE_GUEST_CSUM == 0 - { - debug!("Network tap backend requires GUEST_CSUM to be requested if any of GUEST_TSO4, GUEST_TSO6 and/or GUEST_UFO are required"); - return -libc::EINVAL; - } - - if (flags & !NET_FLAG_DHCP_CLIENT) != 0 { - return -libc::EINVAL; - } - let enable_dhcp_client: bool = flags & NET_FLAG_DHCP_CLIENT != 0; - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - create_virtio_net(cfg, VirtioNetBackend::Tap(tap_name), mac, features); - if enable_dhcp_client { - cfg.vmr.dhcp_client = true; - } - } - Entry::Vacant(_) => return -libc::ENOENT, - } - KRUN_SUCCESS -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -#[cfg(all(not(target_os = "linux"), feature = "net"))] -pub unsafe extern "C" fn krun_add_net_tap( - _ctx_id: u32, - _c_tap_name: *const c_char, - _c_mac: *const u8, - _features: u32, - _flags: u32, -) -> i32 { - -libc::EINVAL -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -#[cfg(feature = "net")] -pub unsafe extern "C" fn krun_set_passt_fd(ctx_id: u32, fd: c_int) -> i32 { - if fd < 0 { - return -libc::EINVAL; - } - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - // The legacy interface only supports a single network interface. - if cfg.net_index != 0 { - return -libc::EINVAL; - } - cfg.legacy_net_cfg = Some(LegacyNetworkConfig::VirtioNetPasst(fd)); - } - Entry::Vacant(_) => return -libc::ENOENT, - } - KRUN_SUCCESS -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -#[cfg(feature = "net")] -pub unsafe extern "C" fn krun_set_gvproxy_path(ctx_id: u32, c_path: *const c_char) -> i32 { - let path_str = match CStr::from_ptr(c_path).to_str() { - Ok(path) => path, - Err(e) => { - debug!("Error parsing gvproxy_path: {e:?}"); - return -libc::EINVAL; - } - }; - - let path = PathBuf::from(path_str); - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - // The legacy interface only supports a single network interface. - if cfg.net_index != 0 { - return -libc::EINVAL; - } - cfg.legacy_net_cfg = Some(LegacyNetworkConfig::VirtioNetGvproxy(path)); - } - Entry::Vacant(_) => return -libc::ENOENT, - } - KRUN_SUCCESS -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -#[cfg(feature = "net")] -pub unsafe extern "C" fn krun_set_net_mac(ctx_id: u32, c_mac: *const u8) -> i32 { - let mac: [u8; 6] = match slice::from_raw_parts(c_mac, 6).try_into() { - Ok(m) => m, - Err(_) => return -libc::EINVAL, - }; - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - cfg.set_net_mac(mac); - } - Entry::Vacant(_) => return -libc::ENOENT, - } - KRUN_SUCCESS -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub unsafe extern "C" fn krun_set_port_map(ctx_id: u32, c_port_map: *const *const c_char) -> i32 { - let mut port_map = HashMap::new(); - let port_map_array: &[*const c_char] = slice::from_raw_parts(c_port_map, MAX_ARGS); - for item in port_map_array.iter().take(MAX_ARGS) { - if item.is_null() { - break; - } else { - let s = match CStr::from_ptr(*item).to_str() { - Ok(s) => s, - Err(_) => return -libc::EINVAL, - }; - let port_tuple: Vec<&str> = s.split(':').collect(); - if port_tuple.len() != 2 { - return -libc::EINVAL; - } - let host_port: u16 = match port_tuple[0].parse() { - Ok(p) => p, - Err(_) => return -libc::EINVAL, - }; - let guest_port: u16 = match port_tuple[1].parse() { - Ok(p) => p, - Err(_) => return -libc::EINVAL, - }; - - if port_map.contains_key(&guest_port) { - return -libc::EINVAL; - } - for hp in port_map.values() { - if *hp == host_port { - return -libc::EINVAL; - } - } - port_map.insert(guest_port, host_port); - } - } - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - if cfg.vsock_config == VsockConfig::Disabled { - return -libc::ENODEV; - } - if cfg.set_port_map(port_map).is_err() { - return -libc::EINVAL; - } - } - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub unsafe extern "C" fn krun_set_rlimits(ctx_id: u32, c_rlimits: *const *const c_char) -> i32 { - let rlimits = if c_rlimits.is_null() { - return -libc::EINVAL; - } else { - let mut strvec = Vec::new(); - - let array: &[*const c_char] = slice::from_raw_parts(c_rlimits, MAX_ARGS); - for item in array.iter().take(MAX_ARGS) { - if item.is_null() { - break; - } else { - let s = match CStr::from_ptr(*item).to_str() { - Ok(s) => s, - Err(_) => return -libc::EINVAL, - }; - strvec.push(s); - } - } - - format!("\"{}\"", strvec.join(",")) - }; - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - ctx_cfg.get_mut().set_rlimits(rlimits); - } - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub unsafe extern "C" fn krun_set_workdir(ctx_id: u32, c_workdir_path: *const c_char) -> i32 { - let workdir_path = match CStr::from_ptr(c_workdir_path).to_str() { - Ok(workdir) => workdir, - Err(_) => return -libc::EINVAL, - }; - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - ctx_cfg.get_mut().set_workdir(workdir_path.to_string()); - } - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -unsafe fn collapse_str_array(array: &[*const c_char]) -> Result { - let mut strvec = Vec::new(); - - for item in array.iter().take(MAX_ARGS) { - if item.is_null() { - break; - } else { - let s = CStr::from_ptr(*item).to_str()?; - strvec.push(format!("\"{s}\"")); - } - } - - Ok(strvec.join(" ")) -} - -#[allow(clippy::format_collect)] -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub unsafe extern "C" fn krun_set_exec( - ctx_id: u32, - c_exec_path: *const c_char, - c_argv: *const *const c_char, - c_envp: *const *const c_char, -) -> i32 { - let exec_path = match CStr::from_ptr(c_exec_path).to_str() { - Ok(path) => path, - Err(e) => { - debug!("Error parsing exec_path: {e:?}"); - return -libc::EINVAL; - } - }; - - let args = if !c_argv.is_null() { - let argv_array: &[*const c_char] = slice::from_raw_parts(c_argv, MAX_ARGS); - match collapse_str_array(argv_array) { - Ok(s) => s, - Err(e) => { - debug!("Error parsing args: {e:?}"); - return -libc::EINVAL; - } - } - } else { - "".to_string() - }; - - let env = if !c_envp.is_null() { - let envp_array: &[*const c_char] = slice::from_raw_parts(c_envp, MAX_ARGS); - match collapse_str_array(envp_array) { - Ok(s) => s, - Err(e) => { - debug!("Error parsing args: {e:?}"); - return -libc::EINVAL; - } - } - } else { - env::vars() - .map(|(key, value)| format!(" {key}=\"{value}\"")) - .collect() - }; - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - cfg.set_exec_path(exec_path.to_string()); - cfg.set_env(env); - cfg.set_args(args); - } - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -#[allow(clippy::format_collect)] -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub unsafe extern "C" fn krun_set_env(ctx_id: u32, c_envp: *const *const c_char) -> i32 { - let env = if !c_envp.is_null() { - let envp_array: &[*const c_char] = slice::from_raw_parts(c_envp, MAX_ARGS); - match collapse_str_array(envp_array) { - Ok(s) => s, - Err(e) => { - debug!("Error parsing args: {e:?}"); - return -libc::EINVAL; - } - } - } else { - env::vars() - .map(|(key, value)| format!(" {key}=\"{value}\"")) - .collect() - }; - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - cfg.set_env(env); - } - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -#[cfg(feature = "tee")] -pub unsafe extern "C" fn krun_set_tee_config_file(ctx_id: u32, c_filepath: *const c_char) -> i32 { - let filepath = match CStr::from_ptr(c_filepath).to_str() { - Ok(f) => f, - Err(_) => return -libc::EINVAL, - }; - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - cfg.set_tee_config_file(PathBuf::from(filepath.to_string())); - } - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub unsafe extern "C" fn krun_add_vsock_port( - ctx_id: u32, - port: u32, - c_filepath: *const c_char, -) -> i32 { - krun_add_vsock_port2(ctx_id, port, c_filepath, false) -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub unsafe extern "C" fn krun_add_vsock_port2( - ctx_id: u32, - port: u32, - c_filepath: *const c_char, - listen: bool, -) -> i32 { - #[cfg(feature = "aws-nitro")] - if listen { - return -libc::EINVAL; - } - - let filepath = match CStr::from_ptr(c_filepath).to_str() { - Ok(f) => PathBuf::from(f.to_string()), - Err(_) => return -libc::EINVAL, - }; - - if listen { - match filepath.try_exists() { - Ok(true) => return -libc::EEXIST, - Err(_) => return -libc::EINVAL, - _ => {} - } - } - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - if cfg.vsock_config == VsockConfig::Disabled { - return -libc::ENODEV; - } - cfg.add_vsock_port(port, filepath, listen); - } - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub unsafe extern "C" fn krun_set_gpu_options(ctx_id: u32, virgl_flags: u32) -> i32 { - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - cfg.set_gpu_virgl_flags(virgl_flags); - } - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub unsafe extern "C" fn krun_set_gpu_options2( - ctx_id: u32, - virgl_flags: u32, - shm_size: u64, -) -> i32 { - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - cfg.set_gpu_virgl_flags(virgl_flags); - cfg.set_gpu_shm_size(shm_size.try_into().unwrap()); - } - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -#[cfg(not(feature = "gpu"))] -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub extern "C" fn krun_set_display_backend( - _ctx_id: u32, - _features: u32, - _vtable: *const c_void, - _vtable_size: usize, -) -> i32 { - -libc::ENOTSUP -} - -#[cfg(feature = "gpu")] -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub extern "C" fn krun_set_display_backend( - ctx_id: u32, - vtable: *const c_void, - vtable_size: usize, -) -> i32 { - if vtable_size < size_of::() { - return -libc::EINVAL; - } - - // SAFETY: We have checked the vtable size is fine, otherwise we have to trust the user. Just - // to be extra careful, this uses read_unaligned, but we could probably get away with ptr::read. - let display_backend: DisplayBackend = - unsafe { std::ptr::read_unaligned(vtable as *const DisplayBackend) }; - - if !display_backend.verify() { - return -libc::EINVAL; - } - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - cfg.vmr.display_backend = Some(display_backend); - } - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -#[cfg(not(feature = "input"))] -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub extern "C" fn krun_add_input_device( - _ctx_id: u32, - _config_backend: *const c_void, - _config_backend_size: size_t, - _event_provider_backend: *const c_void, - _event_provider_backend_size: size_t, -) -> i32 { - -libc::ENOTSUP -} - -#[cfg(feature = "input")] -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub extern "C" fn krun_add_input_device_fd(ctx_id: u32, input_fd: i32) -> i32 { - use devices::virtio::input::passthrough::PassthroughInputBackend; - use krun_input::{IntoInputConfig, IntoInputEvents}; - - if input_fd < 0 { - return -libc::EINVAL; - } - // TODO: currently we let the fd (and it's Box allocation) live forever, we should eventually fix - // this - let input_fd = unsafe { - // SAFETY: The user provided fd should be valid. Its lifetime is 'static because it will - // exist until libkrun _exits the process - BorrowedFd::borrow_raw(input_fd) - }; - let borrowed_fd: &'static BorrowedFd<'static> = Box::leak(Box::new(input_fd)); - - let config_backend = PassthroughInputBackend::into_input_config(Some(borrowed_fd)); - let events_backend = PassthroughInputBackend::into_input_events(Some(borrowed_fd)); - - with_cfg(ctx_id, |cfg| { - cfg.vmr - .input_backends - .push((config_backend, events_backend)); - KRUN_SUCCESS - }) -} - -#[cfg(feature = "input")] -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub unsafe extern "C" fn krun_add_input_device( - ctx_id: u32, - config_backend: *const InputConfigBackend<'static>, - config_backend_size: size_t, - event_provider_backend: *const InputEventProviderBackend<'static>, - event_provider_backend_size: size_t, -) -> i32 { - if config_backend.is_null() || event_provider_backend.is_null() { - return -libc::EINVAL; - } - - if config_backend_size < size_of::() - || event_provider_backend_size < size_of::() - { - return -libc::EINVAL; - } - - let config_backend = unsafe { *config_backend }; - let events_backend = unsafe { *event_provider_backend }; - - if !config_backend.verify() || !events_backend.verify() { - return -libc::EINVAL; - } - - with_cfg(ctx_id, |cfg| { - cfg.vmr - .input_backends - .push((config_backend, events_backend)); - KRUN_SUCCESS - }) -} - -#[cfg(not(feature = "input"))] -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub unsafe extern "C" fn krun_add_input_device_fd(_ctx_id: u32, _input_fd: i32) -> i32 { - -libc::ENOTSUP -} - -#[cfg(feature = "gpu")] -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub unsafe extern "C" fn krun_add_display(ctx_id: u32, width: u32, height: u32) -> i32 { - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - if cfg.vmr.displays.len() >= MAX_DISPLAYS { - return -libc::ENOMEM; - } - - cfg.vmr.displays.push(DisplayInfo::new(width, height)); - (cfg.vmr.displays.len() - 1) as i32 - } - Entry::Vacant(_) => -libc::ENOENT, - } -} - -#[cfg(not(feature = "gpu"))] -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub unsafe extern "C" fn krun_add_display(_ctx_id: u32, _width: u32, _height: u32) -> i32 { - -libc::ENOTSUP -} - -#[cfg(feature = "gpu")] -#[no_mangle] -pub extern "C" fn krun_display_set_refresh_rate( - ctx_id: u32, - display_id: u32, - refresh_rate: u32, -) -> i32 { - with_cfg(ctx_id, |cfg| { - let Some(display_info) = cfg.vmr.displays.get_mut(display_id as usize) else { - return -libc::EINVAL; - }; - - let DisplayInfoEdid::Generated(ref mut edid_params) = display_info.edid else { - return -libc::EALREADY; - }; - - edid_params.refresh_rate = refresh_rate; - KRUN_SUCCESS - }) -} - -#[cfg(not(feature = "gpu"))] -#[no_mangle] -pub extern "C" fn krun_display_set_refresh_rate( - _ctx_id: u32, - _display_id: u32, - _refresh_rate: u32, -) -> i32 { - -libc::ENOTSUP -} - -#[cfg(feature = "gpu")] -#[no_mangle] -#[allow(clippy::missing_safety_doc)] -pub unsafe extern "C" fn krun_display_set_edid( - ctx_id: u32, - display_id: u32, - edid: *const u8, - size: size_t, -) -> i32 { - with_cfg(ctx_id, |cfg| { - let Some(display_info) = cfg.vmr.displays.get_mut(display_id as usize) else { - return -libc::EINVAL; - }; - - if edid.is_null() { - return -libc::EINVAL; - } - - let blob = unsafe { slice::from_raw_parts(edid, size) }; - - display_info.edid = DisplayInfoEdid::Provided(Box::from(blob)); - KRUN_SUCCESS - }) -} - -#[cfg(not(feature = "gpu"))] -#[no_mangle] -#[allow(clippy::missing_safety_doc)] -pub unsafe extern "C" fn krun_display_set_edid( - _ctx_id: u32, - _display_id: u32, - _edid: *const u8, - _size: size_t, -) -> i32 { - -libc::ENOTSUP -} - -#[cfg(feature = "gpu")] -#[no_mangle] -pub extern "C" fn krun_display_set_physical_size( - ctx_id: u32, - display_id: u32, - width_mm: u16, - height_mm: u16, -) -> i32 { - with_cfg(ctx_id, |cfg| { - let Some(display_info) = cfg.vmr.displays.get_mut(display_id as usize) else { - return -libc::EINVAL; - }; - let DisplayInfoEdid::Generated(ref mut edid_params) = display_info.edid else { - return -libc::EALREADY; - }; - edid_params.physical_size = PhysicalSize::DimensionsMillimeters(width_mm, height_mm); - KRUN_SUCCESS - }) -} - -#[cfg(not(feature = "gpu"))] -#[no_mangle] -pub extern "C" fn krun_display_set_physical_size( - _ctx_id: u32, - _display_id: u32, - _width_mm: u16, - _height_mm: u16, -) -> i32 { - -libc::ENOTSUP -} - -#[cfg(feature = "gpu")] -#[no_mangle] -#[allow(clippy::missing_safety_doc)] -pub extern "C" fn krun_display_set_dpi(ctx_id: u32, display_id: u32, dpi: u32) -> i32 { - with_cfg(ctx_id, |cfg| { - let Some(display_info) = cfg.vmr.displays.get_mut(display_id as usize) else { - return -libc::EINVAL; - }; - let DisplayInfoEdid::Generated(ref mut edid_params) = display_info.edid else { - return -libc::EINVAL; - }; - edid_params.physical_size = PhysicalSize::Dpi(dpi); - KRUN_SUCCESS - }) -} - -#[cfg(not(feature = "gpu"))] -#[no_mangle] -pub extern "C" fn krun_display_set_dpi(_ctx_id: u32, _display_id: u32, _dpi: u32) -> i32 { - -libc::ENOTSUP -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub unsafe extern "C" fn krun_set_snd_device(ctx_id: u32, enable: bool) -> i32 { - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - cfg.enable_snd = enable; - } - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -#[allow(unused_assignments)] -#[no_mangle] -pub extern "C" fn krun_get_shutdown_eventfd(ctx_id: u32) -> i32 { - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - if let Some(efd) = cfg.shutdown_efd.as_ref() { - #[cfg(target_os = "macos")] - return efd.get_write_fd(); - #[cfg(target_os = "linux")] - return efd.as_raw_fd(); - } else { - -libc::EINVAL - } - } - Entry::Vacant(_) => -libc::ENOENT, - } -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub unsafe extern "C" fn krun_set_console_output(ctx_id: u32, c_filepath: *const c_char) -> i32 { - let filepath = match CStr::from_ptr(c_filepath).to_str() { - Ok(f) => f, - Err(_) => return -libc::EINVAL, - }; - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - if cfg.console_output.is_some() { - -libc::EINVAL - } else { - cfg.console_output = Some(PathBuf::from(filepath.to_string())); - KRUN_SUCCESS - } - } - Entry::Vacant(_) => -libc::ENOENT, - } -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub unsafe extern "C" fn krun_set_nested_virt(ctx_id: u32, enabled: bool) -> i32 { - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - cfg.vmr.nested_enabled = enabled; - KRUN_SUCCESS - } - Entry::Vacant(_) => -libc::ENOENT, - } -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub unsafe extern "C" fn krun_check_nested_virt() -> i32 { - #[cfg(target_os = "macos")] - match hvf::check_nested_virt() { - Ok(supp) => supp as i32, - Err(_) => -libc::EINVAL, - } - - #[cfg(target_os = "linux")] - { - let paths = [ - "/sys/module/kvm_intel/parameters/nested", - "/sys/module/kvm_amd/parameters/nested", - ]; - if paths.iter().any(|path| { - std::fs::read_to_string(path).is_ok_and(|contents| { - let val = contents.trim(); - val == "1" || val.eq_ignore_ascii_case("Y") - }) - }) { - 1 - } else { - 0 - } - } - - #[cfg(not(any(target_os = "macos", target_os = "linux")))] - -libc::EOPNOTSUPP -} - -const KRUN_FEATURE_NET: u64 = 0; -const KRUN_FEATURE_BLK: u64 = 1; -const KRUN_FEATURE_GPU: u64 = 2; -const KRUN_FEATURE_SND: u64 = 3; -const KRUN_FEATURE_INPUT: u64 = 4; -const KRUN_FEATURE_EFI: u64 = 5; -const KRUN_FEATURE_TEE: u64 = 6; -const KRUN_FEATURE_AMD_SEV: u64 = 7; -const KRUN_FEATURE_INTEL_TDX: u64 = 8; -const KRUN_FEATURE_AWS_NITRO: u64 = 9; -const KRUN_FEATURE_VIRGL_RESOURCE_MAP2: u64 = 10; -const KRUN_FEATURE_INIT_BLOB: u64 = 11; - -#[no_mangle] -pub extern "C" fn krun_has_feature(feature: u64) -> c_int { - let supported = match feature { - KRUN_FEATURE_NET => cfg!(feature = "net"), - KRUN_FEATURE_BLK => cfg!(feature = "blk"), - KRUN_FEATURE_GPU => cfg!(feature = "gpu"), - KRUN_FEATURE_SND => cfg!(feature = "snd"), - KRUN_FEATURE_INPUT => cfg!(feature = "input"), - KRUN_FEATURE_EFI => cfg!(feature = "efi"), - KRUN_FEATURE_TEE => cfg!(feature = "tee"), - KRUN_FEATURE_AMD_SEV => cfg!(feature = "amd-sev"), - KRUN_FEATURE_INTEL_TDX => cfg!(feature = "tdx"), - KRUN_FEATURE_AWS_NITRO => cfg!(feature = "aws-nitro"), - KRUN_FEATURE_VIRGL_RESOURCE_MAP2 => cfg!(feature = "virgl_resource_map2"), - KRUN_FEATURE_INIT_BLOB => cfg!(feature = "init-blob"), - _ => return -libc::EINVAL, - }; - - supported as c_int -} - -/// Gets the maximum number of vCPUs supported by the hypervisor. -/// -/// Returns the maximum number of vCPUs that can be created by this hypervisor, -/// or a negative error code on failure. -#[cfg(any(target_os = "macos", target_os = "linux"))] -#[no_mangle] -pub extern "C" fn krun_get_max_vcpus() -> i32 { - #[cfg(target_os = "macos")] - { - use hvf::bindings::{hv_vm_get_max_vcpu_count, HV_SUCCESS}; - let mut max_vcpu_count: u32 = 0; - let ret = unsafe { hv_vm_get_max_vcpu_count(&mut max_vcpu_count as *mut u32) }; - if ret == HV_SUCCESS { - max_vcpu_count as i32 - } else { - error!("Error retrieving max vcpu count: {ret:?}"); - -libc::EINVAL - } - } - - #[cfg(target_os = "linux")] - { - use kvm_ioctls::Kvm; - match Kvm::new() { - Ok(kvm) => kvm.get_max_vcpus() as i32, - Err(e) => { - error!("Error retrieving max vcpu count: {e:?}"); - -libc::EINVAL - } - } - } -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub extern "C" fn krun_split_irqchip(ctx_id: u32, enable: bool) -> i32 { - if enable && !cfg!(target_arch = "x86_64") { - return -libc::EINVAL; - } - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - cfg.vmr.split_irqchip = enable; - KRUN_SUCCESS - } - Entry::Vacant(_) => -libc::ENOENT, - } -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub unsafe extern "C" fn krun_set_smbios_oem_strings( - ctx_id: u32, - oem_strings: *const *const c_char, -) -> i32 { - if oem_strings.is_null() { - return -libc::EINVAL; - } - - let cstr_ptr_slice = slice::from_raw_parts(oem_strings, MAX_ARGS); - - let mut oem_strings = Vec::new(); - - for cstr_ptr in cstr_ptr_slice.iter().take_while(|p| !p.is_null()) { - let Ok(s) = CStr::from_ptr(*cstr_ptr).to_str() else { - return -libc::EINVAL; - }; - oem_strings.push(s.to_string()); - } - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - ctx_cfg.get_mut().vmr.smbios_oem_strings = - (!oem_strings.is_empty()).then_some(oem_strings) - } - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -#[cfg(feature = "net")] -fn create_virtio_net( - ctx_cfg: &mut ContextConfig, - backend: VirtioNetBackend, - mac: [u8; 6], - features: u32, -) { - let network_interface_config = NetworkInterfaceConfig { - iface_id: format!("eth{}", ctx_cfg.net_index), - backend, - mac, - features, - }; - ctx_cfg.net_index += 1; - ctx_cfg - .vmr - .add_network_interface(network_interface_config) - .expect("Failed to create network interface"); -} - -#[cfg(all(target_arch = "x86_64", not(feature = "tee")))] -fn map_kernel(ctx_id: u32, kernel_path: &PathBuf) -> i32 { - let file = match File::options().read(true).write(false).open(kernel_path) { - Ok(file) => file, - Err(err) => { - error!("Error opening external kernel: {err}"); - return -libc::EINVAL; - } - }; - - let kernel_size = file.metadata().unwrap().len(); - - let kernel_host_addr = unsafe { - libc::mmap( - std::ptr::null_mut(), - kernel_size as usize, - libc::PROT_READ, - libc::MAP_SHARED, - file.as_raw_fd(), - 0_i64, - ) - }; - if std::ptr::eq(kernel_host_addr, libc::MAP_FAILED) { - error!("Can't load kernel into process map"); - return -libc::EINVAL; - } - - let kernel_bundle = KernelBundle { - host_addr: kernel_host_addr as u64, - guest_addr: 0x8000_0000, - entry_addr: 0x8000_0000, - size: kernel_size as usize, - }; - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => ctx_cfg - .get_mut() - .vmr - .set_kernel_bundle(kernel_bundle) - .unwrap(), - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -#[cfg(feature = "tee")] -#[allow(clippy::format_collect)] -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub unsafe extern "C" fn krun_set_kernel(_ctx_id: u32, _c_kernel_path: *const c_char) -> i32 { - -libc::EOPNOTSUPP -} - -#[cfg(not(feature = "tee"))] -#[allow(clippy::format_collect)] -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub unsafe extern "C" fn krun_set_kernel( - ctx_id: u32, - c_kernel_path: *const c_char, - kernel_format: u32, - c_initramfs_path: *const c_char, - c_cmdline: *const c_char, -) -> i32 { - let path = match CStr::from_ptr(c_kernel_path).to_str() { - Ok(path) => PathBuf::from(path), - Err(e) => { - error!("Error parsing kernel_path: {e:?}"); - return -libc::EINVAL; - } - }; - - let format = match kernel_format { - // For raw kernels in x86_64, we map the kernel into the - // process and treat it as a bundled kernel. - #[cfg(all(target_arch = "x86_64", not(feature = "tee")))] - 0 => return map_kernel(ctx_id, &path), - #[cfg(target_arch = "aarch64")] - 0 => KernelFormat::Raw, - 1 => KernelFormat::Elf, - 2 => KernelFormat::PeGz, - 3 => KernelFormat::ImageBz2, - 4 => KernelFormat::ImageGz, - 5 => KernelFormat::ImageZstd, - _ => { - return -libc::EINVAL; - } - }; - - let (initramfs_path, initramfs_size) = if !c_initramfs_path.is_null() { - match CStr::from_ptr(c_initramfs_path).to_str() { - Ok(path) => { - let path = PathBuf::from(path); - let size = match std::fs::metadata(&path) { - Ok(metadata) => metadata.len(), - Err(e) => { - error!("Can't read initramfs metadata: {e:?}"); - return -libc::EINVAL; - } - }; - (Some(path), size) - } - Err(e) => { - error!("Error parsing initramfs path: {e:?}"); - return -libc::EINVAL; - } - } - } else { - (None, 0) - }; - - let cmdline = if !c_cmdline.is_null() { - match CStr::from_ptr(c_cmdline).to_str() { - Ok(cmdline) => Some(cmdline.to_string()), - Err(e) => { - error!("Error parsing kernel cmdline: {e:?}"); - return -libc::EINVAL; - } - } - } else { - None - }; - - let external_kernel = ExternalKernel { - path, - format, - initramfs_path, - initramfs_size, - cmdline, - }; - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => ctx_cfg.get_mut().vmr.set_external_kernel(external_kernel), - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -#[cfg(not(feature = "tee"))] -#[allow(clippy::format_collect)] -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub unsafe extern "C" fn krun_set_firmware(ctx_id: u32, c_firmware_path: *const c_char) -> i32 { - let path = match CStr::from_ptr(c_firmware_path).to_str() { - Ok(path) => PathBuf::from(path), - Err(e) => { - error!("Error parsing firmware_path: {e:?}"); - return -libc::EINVAL; - } - }; - - let firmware_config = FirmwareConfig { path }; - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => ctx_cfg.get_mut().vmr.set_firmware_config(firmware_config), - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -unsafe fn load_krunfw_payload( - krunfw: &KrunfwBindings, - vmr: &mut VmResources, -) -> Result<(), libloading::Error> { - let mut kernel_guest_addr: u64 = 0; - let mut kernel_entry_addr: u64 = 0; - let mut kernel_size: usize = 0; - let kernel_host_addr = unsafe { - (krunfw.get_kernel)( - &mut kernel_guest_addr as *mut u64, - &mut kernel_entry_addr as *mut u64, - &mut kernel_size as *mut usize, - ) - }; - let kernel_bundle = KernelBundle { - host_addr: kernel_host_addr as u64, - guest_addr: kernel_guest_addr, - entry_addr: kernel_entry_addr, - size: kernel_size, - }; - vmr.set_kernel_bundle(kernel_bundle).unwrap(); - - #[cfg(feature = "tee")] - { - let mut qboot_size: usize = 0; - let qboot_host_addr = unsafe { (krunfw.get_qboot)(&mut qboot_size as *mut usize) }; - let qboot_bundle = QbootBundle { - host_addr: qboot_host_addr as u64, - size: qboot_size, - }; - vmr.set_qboot_bundle(qboot_bundle).unwrap(); - - let mut initrd_size: usize = 0; - let initrd_host_addr = unsafe { (krunfw.get_initrd)(&mut initrd_size as *mut usize) }; - let initrd_bundle = InitrdBundle { - host_addr: initrd_host_addr as u64, - size: initrd_size, - }; - vmr.set_initrd_bundle(initrd_bundle).unwrap(); - } - - Ok(()) -} - -#[no_mangle] -pub extern "C" fn krun_setuid(ctx_id: u32, uid: libc::uid_t) -> i32 { - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - cfg.set_vmm_uid(uid); - } - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -#[no_mangle] -pub extern "C" fn krun_setgid(ctx_id: u32, gid: libc::gid_t) -> i32 { - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - cfg.set_vmm_gid(gid); - } - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -#[cfg(all(feature = "blk", not(any(feature = "tee", feature = "aws-nitro"))))] -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub unsafe extern "C" fn krun_set_root_disk_remount( - ctx_id: u32, - c_device: *const c_char, - c_fstype: *const c_char, - c_options: *const c_char, -) -> i32 { - let device = match CStr::from_ptr(c_device).to_str() { - Ok(device) => device.to_string(), - Err(e) => { - error!("Error parsing device path: {e:?}"); - return -libc::EINVAL; - } - }; - - let fstype = if !c_fstype.is_null() { - match CStr::from_ptr(c_fstype).to_str() { - Ok(fstype) => { - if fstype == "auto" { - None - } else { - Some(fstype.to_string()) - } - } - Err(e) => { - error!("Error parsing fstype: {e:?}"); - return -libc::EINVAL; - } - } - } else { - None - }; - - let options = if !c_options.is_null() { - match CStr::from_ptr(c_options).to_str() { - Ok(options) => Some(options.to_string()), - Err(e) => { - error!("Error parsing options: {e:?}"); - return -libc::EINVAL; - } - } - } else { - None - }; - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let ctx_cfg = ctx_cfg.get_mut(); - - if ctx_cfg.vmr.fs.iter().any(|fs| fs.fs_id == "/dev/root") { - error!("Root filesystem already configured"); - return -libc::EINVAL; - } - - if ctx_cfg.block_cfgs.is_empty() { - error!("No block devices configured"); - return -libc::EINVAL; - } - - // Boot from a block device: the virtiofs root only needs to - // serve init.krun and provide mount points for /dev, /proc, /sys. - // Use a NullFs (no host directory) with the inode overlay. - let mut virtual_entries = Vec::new(); - #[cfg(feature = "init-blob")] - if !ctx_cfg.disable_implicit_init { - virtual_entries.push(init_virtual_entry()); - } - // init.c needs these directories as mount points before - // pivoting to the block device root. - for name in ["dev", "proc", "sys", "newroot"] { - virtual_entries.push(VirtualDirEntry { - name: CString::new(name).unwrap(), - entry: VirtualEntry { - mode: 0o755, - one_shot: false, - content: VirtualEntryContent::Dir { - children: Vec::new(), - }, - }, - }); - } - - ctx_cfg.vmr.add_fs_device(FsDeviceConfig { - fs_id: "/dev/root".into(), - shared_dir: None, - semantics: PermissionSemantics::LinuxComplete, - // Default to a conservative 512 MB window. - shm_size: Some(1 << 29), - read_only: false, - overlay: None, - virtual_entries, - }); - - ctx_cfg.set_block_root(device, fstype, options); - } - Entry::Vacant(_) => return -libc::ENOENT, - }; - - KRUN_SUCCESS -} - -#[unsafe(no_mangle)] -#[cfg(all( - feature = "init-blob", - not(any(feature = "tee", feature = "aws-nitro")) -))] -pub extern "C" fn krun_disable_implicit_init(ctx_id: u32) -> i32 { - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - ctx_cfg.get_mut().disable_implicit_init = true; - } - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -#[unsafe(no_mangle)] -#[cfg(all( - not(feature = "init-blob"), - not(any(feature = "tee", feature = "aws-nitro")) -))] -pub extern "C" fn krun_disable_implicit_init(_ctx_id: u32) -> i32 { - KRUN_SUCCESS -} - -/// Resolve a path like "a/b/c" into parent directory children + leaf name. -/// Errors with a libc errno if any intermediate component is missing or not a Dir. -#[cfg(not(any(feature = "tee", feature = "aws-nitro")))] -fn resolve_overlay_path<'a>( - entries: &'a mut Vec, - path: &str, -) -> Result<(&'a mut Vec, CString), i32> { - let path = path.strip_prefix('/').unwrap_or(path); - let components: Vec<&str> = path.split('/').collect(); - let (leaf, parents) = components.split_last().ok_or(-libc::EINVAL)?; - if leaf.is_empty() { - return Err(-libc::EINVAL); - } - - let mut current = entries; - for component in parents { - let dir = current - .iter_mut() - .find(|e| e.name.as_c_str().to_bytes() == component.as_bytes()) - .ok_or(-libc::ENOENT)?; - match &mut dir.entry.content { - VirtualEntryContent::Dir { children } => current = children, - _ => return Err(-libc::ENOTDIR), - } - } - - let name = CString::new(*leaf).map_err(|_| -libc::EINVAL)?; - Ok((current, name)) -} - -/// Add a virtual overlay entry to a virtiofs device, resolving paths with `/`. -#[cfg(not(any(feature = "tee", feature = "aws-nitro")))] -fn fs_add_overlay_entry(ctx_id: u32, fs_tag: &str, path: &str, entry: VirtualEntry) -> i32 { - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - let fs_cfg = match cfg.vmr.fs.iter_mut().find(|fs| fs.fs_id == fs_tag) { - Some(fs) => fs, - None => return -libc::ENOENT, - }; - let (parent_children, name) = - match resolve_overlay_path(&mut fs_cfg.virtual_entries, path) { - Ok(v) => v, - Err(e) => return e, - }; - parent_children.push(VirtualDirEntry { name, entry }); - } - Entry::Vacant(_) => return -libc::ENOENT, - } - KRUN_SUCCESS -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -#[cfg(not(any(feature = "tee", feature = "aws-nitro")))] -pub unsafe extern "C" fn krun_fs_add_overlay_file( - ctx_id: u32, - c_fs_tag: *const c_char, - c_path: *const c_char, - data: *const u8, - data_len: size_t, - mode: u32, - one_shot: bool, -) -> i32 { - if c_fs_tag.is_null() || c_path.is_null() { - return -libc::EINVAL; - } - - let fs_tag = match CStr::from_ptr(c_fs_tag).to_str() { - Ok(s) => s, - Err(_) => return -libc::EINVAL, - }; - let path = match CStr::from_ptr(c_path).to_str() { - Ok(s) => s, - Err(_) => return -libc::EINVAL, - }; - - // SAFETY: The caller guarantees the memory remains valid for the VM - // lifetime (see the C header contract). - let payload: &'static [u8] = if data_len == 0 { - &[] - } else if !data.is_null() { - slice::from_raw_parts(data, data_len) - } else { - return -libc::EINVAL; - }; - - fs_add_overlay_entry( - ctx_id, - fs_tag, - path, - VirtualEntry { - mode, - one_shot, - content: VirtualEntryContent::File { data: payload }, - }, - ) -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -#[cfg(not(any(feature = "tee", feature = "aws-nitro")))] -pub unsafe extern "C" fn krun_fs_add_overlay_dir( - ctx_id: u32, - c_fs_tag: *const c_char, - c_path: *const c_char, - mode: u32, -) -> i32 { - if c_fs_tag.is_null() || c_path.is_null() { - return -libc::EINVAL; - } - - let fs_tag = match CStr::from_ptr(c_fs_tag).to_str() { - Ok(s) => s, - Err(_) => return -libc::EINVAL, - }; - let path = match CStr::from_ptr(c_path).to_str() { - Ok(s) => s, - Err(_) => return -libc::EINVAL, - }; - - fs_add_overlay_entry( - ctx_id, - fs_tag, - path, - VirtualEntry { - mode, - one_shot: false, - content: VirtualEntryContent::Dir { - children: Vec::new(), - }, - }, - ) -} - -#[allow(clippy::missing_safety_doc)] -#[unsafe(no_mangle)] -#[cfg(all( - feature = "init-blob", - not(any(feature = "tee", feature = "aws-nitro")) -))] -pub unsafe extern "C" fn krun_get_default_init( - data_out: *mut *const u8, - len_out: *mut size_t, -) -> i32 { - if data_out.is_null() || len_out.is_null() { - return -libc::EINVAL; - } - *data_out = DEFAULT_INIT_PAYLOAD.as_ptr(); - *len_out = DEFAULT_INIT_PAYLOAD.len(); - KRUN_SUCCESS -} - -#[allow(clippy::missing_safety_doc)] -#[unsafe(no_mangle)] -#[cfg(all( - not(feature = "init-blob"), - not(any(feature = "tee", feature = "aws-nitro")) -))] -pub unsafe extern "C" fn krun_get_default_init( - _data_out: *mut *const u8, - _len_out: *mut size_t, -) -> i32 { - -libc::ENOTSUP -} - -#[unsafe(no_mangle)] -pub extern "C" fn krun_disable_implicit_console(ctx_id: u32) -> i32 { - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - cfg.vmr.disable_implicit_console = true; - } - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -#[no_mangle] -pub extern "C" fn krun_disable_implicit_vsock(ctx_id: u32) -> i32 { - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - cfg.vsock_config = VsockConfig::Disabled; - } - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -#[no_mangle] -pub extern "C" fn krun_add_vsock(ctx_id: u32, tsi_features: u32) -> i32 { - let tsi_flags = match TsiFlags::from_bits(tsi_features) { - Some(flags) => flags, - None => return -libc::EINVAL, - }; - - if cfg!(target_os = "macos") && tsi_flags.contains(TsiFlags::HIJACK_UNIX) { - error!("TSI hijacking of UNIX sockets is not yet supported on macOS"); - return -libc::EINVAL; - } - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - if cfg.vsock_config != VsockConfig::Disabled { - return -libc::EEXIST; - } - cfg.vsock_config = VsockConfig::Explicit { tsi_flags }; - } - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub unsafe extern "C" fn krun_add_virtio_console_default( - ctx_id: u32, - input_fd: libc::c_int, - output_fd: libc::c_int, - err_fd: libc::c_int, -) -> i32 { - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - - cfg.vmr - .virtio_consoles - .push(VirtioConsoleConfigMode::Autoconfigure( - DefaultVirtioConsoleConfig { - input_fd, - output_fd, - err_fd, - }, - )); - } - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub unsafe extern "C" fn krun_add_virtio_console_multiport(ctx_id: u32) -> i32 { - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - let console_id = cfg.vmr.virtio_consoles.len() as i32; - - cfg.vmr - .virtio_consoles - .push(VirtioConsoleConfigMode::Explicit(Vec::new())); - - console_id - } - Entry::Vacant(_) => -libc::ENOENT, - } -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub unsafe extern "C" fn krun_add_console_port_tty( - ctx_id: u32, - console_id: u32, - name: *const libc::c_char, - tty_fd: libc::c_int, -) -> i32 { - if tty_fd < 0 { - return -libc::EINVAL; - } - - let name_str = if name.is_null() { - String::new() - } else { - match CStr::from_ptr(name).to_str() { - Ok(s) => s.to_string(), - Err(_) => return -libc::EINVAL, - } - }; - - if !BorrowedFd::borrow_raw(tty_fd).is_terminal() { - return -libc::ENOTTY; - } - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - - match cfg.vmr.virtio_consoles.get_mut(console_id as usize) { - Some(VirtioConsoleConfigMode::Explicit(ports)) => { - ports.push(PortConfig::Tty { - name: name_str, - tty_fd, - }); - KRUN_SUCCESS - } - _ => -libc::EINVAL, - } - } - Entry::Vacant(_) => -libc::ENOENT, - } -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub unsafe extern "C" fn krun_add_console_port_inout( - ctx_id: u32, - console_id: u32, - name: *const c_char, - input_fd: c_int, - output_fd: c_int, -) -> i32 { - let name_str = if name.is_null() { - String::new() - } else { - match CStr::from_ptr(name).to_str() { - Ok(s) => s.to_string(), - Err(_) => return -libc::EINVAL, - } - }; - - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - - match cfg.vmr.virtio_consoles.get_mut(console_id as usize) { - Some(VirtioConsoleConfigMode::Explicit(ports)) => { - ports.push(PortConfig::InOut { - name: name_str, - input_fd, - output_fd, - }); - KRUN_SUCCESS - } - _ => -libc::EINVAL, - } - } - Entry::Vacant(_) => -libc::ENOENT, - } -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub unsafe extern "C" fn krun_add_serial_console_default( - ctx_id: u32, - input_fd: c_int, - output_fd: c_int, -) -> i32 { - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - cfg.vmr.serial_consoles.push(SerialConsoleConfig { - input_fd, - output_fd, - }); - } - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -#[allow(clippy::missing_safety_doc)] -#[no_mangle] -pub unsafe extern "C" fn krun_set_kernel_console(ctx_id: u32, console_id: *const c_char) -> i32 { - let console_id = match CStr::from_ptr(console_id).to_str() { - Ok(id) => id.to_string(), - Err(_) => return -libc::EINVAL, - }; - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - cfg.vmr.kernel_console = Some(console_id); - } - Entry::Vacant(_) => return -libc::ENOENT, - } - - KRUN_SUCCESS -} - -#[no_mangle] -#[allow(unreachable_code)] -pub extern "C" fn krun_start_enter(ctx_id: u32) -> i32 { - #[cfg(target_os = "linux")] - { - let prname = match env::var("HOSTNAME") { - Ok(val) => CString::new(format!("VM:{val}")).unwrap(), - Err(_) => CString::new("libkrun VM").unwrap(), - }; - unsafe { libc::prctl(libc::PR_SET_NAME, prname.as_ptr()) }; - } - - #[cfg(feature = "aws-nitro")] - return krun_start_enter_nitro(ctx_id); - - let mut event_manager = match EventManager::new() { - Ok(em) => em, - Err(e) => { - error!("Unable to create EventManager: {e:?}"); - return -libc::EINVAL; - } - }; - - let mut ctx_cfg = match CTX_MAP.lock().unwrap().remove(&ctx_id) { - Some(ctx_cfg) => ctx_cfg, - None => return -libc::ENOENT, - }; - - if ctx_cfg.vmr.external_kernel.is_none() - && ctx_cfg.vmr.kernel_bundle.is_none() - && ctx_cfg.vmr.firmware_config.is_none() - && cfg!(not(feature = "efi")) - { - if let Some(ref krunfw) = ctx_cfg.krunfw { - if let Err(err) = unsafe { load_krunfw_payload(krunfw, &mut ctx_cfg.vmr) } { - eprintln!("Can't load libkrunfw symbols: {err}"); - return -libc::ENOENT; - } - } else { - eprintln!("Couldn't find or load {KRUNFW_NAME}"); - return -libc::ENOENT; - } - } - - #[cfg(feature = "blk")] - for block_cfg in ctx_cfg.get_block_cfg() { - if ctx_cfg.vmr.add_block_device(block_cfg).is_err() { - error!("Error configuring virtio-blk for block"); - return -libc::EINVAL; - } - } - - /* - * Before krun_start_enter() is called in an encrypted context, the TEE - * config must have been set via krun_set_tee_config_file(). If the TEE - * config is not set by this point, print the relevant error message and - * fail. - */ - #[cfg(feature = "tee")] - if let Some(tee_config) = ctx_cfg.get_tee_config_file() { - if let Err(e) = ctx_cfg.vmr.set_tee_config(tee_config) { - error!("Error setting up TEE config: {e:?}"); - return -libc::EINVAL; - } - } else { - error!("Missing TEE config file"); - return -libc::EINVAL; - } - - let kernel_cmdline = KernelCmdlineConfig { - prolog: Some(format!("{DEFAULT_KERNEL_CMDLINE} init={INIT_PATH}")), - krun_env: Some(format!( - " {} {} {} {} {}", - ctx_cfg.get_exec_path(), - ctx_cfg.get_workdir(), - ctx_cfg.get_block_root(), - ctx_cfg.get_rlimits(), - ctx_cfg.get_env(), - )), - epilog: Some(format!(" -- {}", ctx_cfg.get_args())), - }; - - if ctx_cfg.vmr.set_kernel_cmdline(kernel_cmdline).is_err() { - return -libc::EINVAL; - } - - #[cfg(feature = "net")] - { - if let Some(legacy_net_cfg) = ctx_cfg.legacy_net_cfg.clone() { - let backend = match legacy_net_cfg { - LegacyNetworkConfig::VirtioNetGvproxy(path) => { - VirtioNetBackend::UnixgramPath(path, true) - } - LegacyNetworkConfig::VirtioNetPasst(fd) => VirtioNetBackend::UnixstreamFd(fd), - }; - let mac = ctx_cfg - .legacy_mac - .unwrap_or([0x5a, 0x94, 0xef, 0xe4, 0x0c, 0xee]); - create_virtio_net(&mut ctx_cfg, backend, mac, NET_COMPAT_FEATURES); - } - } - - match &ctx_cfg.vsock_config { - VsockConfig::Disabled => (), - VsockConfig::Explicit { tsi_flags } => { - let vsock_device_config = VsockDeviceConfig { - vsock_id: "vsock0".to_string(), - guest_cid: 3, - host_port_map: ctx_cfg.tsi_port_map, - unix_ipc_port_map: ctx_cfg.unix_ipc_port_map.clone(), - tsi_flags: *tsi_flags, - }; - ctx_cfg.vmr.set_vsock_device(vsock_device_config).unwrap(); - } - VsockConfig::Implicit => { - // Implicit vsock configuration - use heuristics - // Check if TSI should be enabled based on network configuration - #[cfg(feature = "net")] - let enable_tsi = ctx_cfg.vmr.net.list.is_empty() && ctx_cfg.legacy_net_cfg.is_none(); - #[cfg(not(feature = "net"))] - let enable_tsi = true; - - let has_ipc_map = ctx_cfg.unix_ipc_port_map.is_some(); - - if enable_tsi || has_ipc_map { - let (tsi_flags, host_port_map) = if enable_tsi { - (TsiFlags::HIJACK_INET, ctx_cfg.tsi_port_map) - } else { - (TsiFlags::empty(), None) - }; - - let vsock_device_config = VsockDeviceConfig { - vsock_id: "vsock0".to_string(), - guest_cid: 3, - host_port_map, - unix_ipc_port_map: ctx_cfg.unix_ipc_port_map.clone(), - tsi_flags, - }; - ctx_cfg.vmr.set_vsock_device(vsock_device_config).unwrap(); - } - } - } - - if let Some(virgl_flags) = ctx_cfg.gpu_virgl_flags { - ctx_cfg.vmr.set_gpu_virgl_flags(virgl_flags); - } - if let Some(shm_size) = ctx_cfg.gpu_shm_size { - ctx_cfg.vmr.set_gpu_shm_size(shm_size); - } - - #[cfg(feature = "snd")] - ctx_cfg.vmr.set_snd_device(ctx_cfg.enable_snd); - - if let Some(console_output) = ctx_cfg.console_output { - ctx_cfg.vmr.set_console_output(console_output); - } - - if let Some(gid) = ctx_cfg.vmm_gid { - if unsafe { libc::setgid(gid) } != 0 { - error!("Failed to set gid {gid}"); - return -std::io::Error::last_os_error().raw_os_error().unwrap(); - } - } - - if let Some(uid) = ctx_cfg.vmm_uid { - if unsafe { libc::setuid(uid) } != 0 { - error!("Failed to set uid {uid}"); - return -std::io::Error::last_os_error().raw_os_error().unwrap(); - } - } - - let (sender, _receiver) = unbounded(); - - let _vmm = match vmm::builder::build_microvm( - &ctx_cfg.vmr, - &mut event_manager, - ctx_cfg.shutdown_efd, - sender, - ) { - Ok(vmm) => vmm, - Err(e) => { - error!("Building the microVM failed: {e:?}"); - return -libc::EINVAL; - } - }; - - #[cfg(target_os = "macos")] - if ctx_cfg.gpu_virgl_flags.is_some() { - vmm::worker::start_worker_thread(_vmm.clone(), _receiver).unwrap(); - } - - #[cfg(target_arch = "x86_64")] - if ctx_cfg.vmr.split_irqchip { - vmm::worker::start_worker_thread(_vmm.clone(), _receiver.clone()).unwrap(); - } - - #[cfg(any(feature = "amd-sev", feature = "tdx"))] - vmm::worker::start_worker_thread(_vmm.clone(), _receiver.clone()).unwrap(); - - loop { - match event_manager.run() { - Ok(_) => {} - Err(e) => { - error!("Error in EventManager loop: {e:?}"); - return -libc::EINVAL; - } - } - } -} - -#[cfg(feature = "aws-nitro")] -#[no_mangle] -fn krun_start_enter_nitro(ctx_id: u32) -> i32 { - let ctx_cfg = match CTX_MAP.lock().unwrap().remove(&ctx_id) { - Some(ctx_cfg) => ctx_cfg, - None => return -libc::ENOENT, - }; - - let Ok(enclave) = NitroEnclave::try_from(ctx_cfg) else { - return -libc::EINVAL; - }; - - match enclave.run() { - Ok(ret) => ret, - Err(e) => { - error!("Error running nitro enclave: {e}"); - - -libc::EINVAL - } - } -} - -#[cfg(all(test, feature = "init-blob", not(feature = "tee")))] -mod test_disable_implicit_init { - use super::*; - - #[test] - fn test_disable_implicit_init() { - let ctx = krun_create_ctx() as u32; - unsafe { - krun_disable_implicit_init(ctx); - krun_set_root(ctx, c"/tmp".as_ptr()); - } - - let ctx_map = CTX_MAP.lock().unwrap(); - let cfg = ctx_map.get(&ctx).unwrap(); - assert_eq!(cfg.vmr.fs.len(), 1); - assert!( - cfg.vmr.fs[0].virtual_entries.is_empty(), - "root virtiofs should not inject init.krun after krun_disable_implicit_init()" - ); - drop(ctx_map); - - assert_eq!(krun_free_ctx(ctx), KRUN_SUCCESS); - } - - #[test] - fn test_add_virtiofs_overlay_validates_and_records_config() { - let ctx = krun_create_ctx() as u32; - let tag = CString::new("workspace").unwrap(); - let lower = CString::new("/lower").unwrap(); - let upper = CString::new("/upper").unwrap(); - let work = CString::new("/work").unwrap(); - let preimages = CString::new("/preimages").unwrap(); - let excluded = CString::new(".stage").unwrap(); - let lowers = [lower.as_ptr()]; - let excluded_paths = [excluded.as_ptr()]; - assert_eq!( - unsafe { - krun_add_virtiofs_overlay( - ctx, - tag.as_ptr(), - lowers.as_ptr(), - lowers.len(), - upper.as_ptr(), - work.as_ptr(), - preimages.as_ptr(), - excluded_paths.as_ptr(), - excluded_paths.len(), - 0, - ) - }, - KRUN_SUCCESS - ); - let ctx_map = CTX_MAP.lock().unwrap(); - let overlay = ctx_map[&ctx].vmr.fs[0].overlay.as_ref().unwrap(); - assert_eq!(overlay.lower_dirs, ["/lower"]); - assert_eq!(overlay.upper_dir, "/upper"); - assert_eq!(overlay.preimage_dir.as_deref(), Some("/preimages")); - assert_eq!(overlay.excluded_paths, [".stage"]); - drop(ctx_map); - assert_eq!(krun_free_ctx(ctx), KRUN_SUCCESS); - - let ctx = krun_create_ctx() as u32; - assert_eq!( - unsafe { - krun_add_virtiofs_overlay( - ctx, - std::ptr::null(), - lowers.as_ptr(), - lowers.len(), - upper.as_ptr(), - work.as_ptr(), - std::ptr::null(), - std::ptr::null(), - 0, - 0, - ) - }, - -libc::EINVAL - ); - assert_eq!(krun_free_ctx(ctx), KRUN_SUCCESS); - } -} From 29e455e65d11bd417973d3e360d26331a931cf1a Mon Sep 17 00:00:00 2001 From: Reiase Date: Fri, 2 Oct 2026 07:59:02 +0800 Subject: [PATCH 2/4] Fix S3 CI by building pinned MinIO tools from source --- .github/workflows/ci.yml | 40 ++++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5845d3c88..d85a344cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -251,28 +251,41 @@ jobs: with: shared-key: ci-pchronicle-s3 + - uses: actions/setup-go@v5 + with: + go-version: "1.24.2" + cache: false + + # Upstream no longer distributes the public container images or binaries. + - name: Build pinned MinIO tools from source + env: + GOBIN: ${{ runner.temp }}/minio-bin + run: | + go install github.com/minio/minio@RELEASE.2025-07-23T15-54-02Z + go install github.com/minio/mc@RELEASE.2025-08-13T08-35-41Z + echo "$GOBIN" >> "$GITHUB_PATH" + - name: Start pinned MinIO server + env: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin123 run: | - docker run --detach --name persisting-minio-contract \ - --publish 9000:9000 \ - --env MINIO_ROOT_USER=minioadmin \ - --env MINIO_ROOT_PASSWORD=minioadmin123 \ - quay.io/minio/minio:RELEASE.2025-07-23T15-54-02Z \ - server /data + nohup minio server "$RUNNER_TEMP/minio-data" --address 127.0.0.1:9000 \ + > "$RUNNER_TEMP/minio.log" 2>&1 & + echo $! > "$RUNNER_TEMP/minio.pid" for attempt in $(seq 1 30); do if curl --fail --silent http://127.0.0.1:9000/minio/health/live >/dev/null; then exit 0 fi sleep 1 done - docker logs persisting-minio-contract + cat "$RUNNER_TEMP/minio.log" exit 1 - name: Create isolated test bucket run: | - docker run --rm --network host --entrypoint /bin/sh \ - quay.io/minio/mc:RELEASE.2025-08-13T08-35-41Z -c \ - 'mc alias set local http://127.0.0.1:9000 minioadmin minioadmin123 && mc mb --ignore-existing local/persisting-tests' + mc alias set local http://127.0.0.1:9000 minioadmin minioadmin123 + mc mb --ignore-existing local/persisting-tests - name: Run real S3 storage contract env: @@ -288,11 +301,14 @@ jobs: - name: Show MinIO logs after failure if: failure() - run: docker logs persisting-minio-contract + run: cat "$RUNNER_TEMP/minio.log" - name: Stop MinIO if: always() - run: docker rm --force persisting-minio-contract || true + run: | + if [ -f "$RUNNER_TEMP/minio.pid" ]; then + kill "$(cat "$RUNNER_TEMP/minio.pid")" || true + fi python-test: name: Python Test From 801bf4c269fd3bc95be9813da03b1653ab9359d4 Mon Sep 17 00:00:00 2001 From: Reiase Date: Fri, 2 Oct 2026 08:52:26 +0800 Subject: [PATCH 3/4] Fix Clippy macro warnings and benchmark CPU consistency --- .github/workflows/pchronicle-benchmark.yml | 18 ++++++------------ .../src/capture/step_table_writer.rs | 4 ++++ crates/persisting-events/src/control.rs | 4 ++++ crates/persisting-overlaynet/src/server.rs | 4 ++++ .../examples/langfuse_backend_feasibility.rs | 4 ++++ 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/.github/workflows/pchronicle-benchmark.yml b/.github/workflows/pchronicle-benchmark.yml index 6783ab4cb..0ff9c3d60 100644 --- a/.github/workflows/pchronicle-benchmark.yml +++ b/.github/workflows/pchronicle-benchmark.yml @@ -16,14 +16,10 @@ permissions: jobs: benchmark: - name: Benchmark (${{ matrix.ref }}) + name: Benchmark (main and candidate) runs-on: ubuntu-latest - timeout-minutes: 45 - strategy: - fail-fast: false - max-parallel: 2 - matrix: - ref: [main, candidate] + timeout-minutes: 60 + # Both revisions must run on the same machine for comparable CPU timings. steps: - name: Checkout candidate uses: actions/checkout@v4 @@ -43,10 +39,9 @@ jobs: - uses: Swatinem/rust-cache@v2 with: - shared-key: pchronicle-benchmark-${{ matrix.ref }} + shared-key: pchronicle-benchmark - name: Resolve main baseline - if: matrix.ref == 'main' id: baseline shell: bash run: | @@ -71,7 +66,7 @@ jobs: fi - name: Benchmark main - if: matrix.ref == 'main' && steps.baseline.outputs.supported == 'true' + if: steps.baseline.outputs.supported == 'true' shell: bash run: | set -euo pipefail @@ -84,7 +79,6 @@ jobs: --output "${RUNNER_TEMP}/pchronicle-benchmark/main" - name: Benchmark candidate - if: matrix.ref == 'candidate' shell: bash run: | set -euo pipefail @@ -99,7 +93,7 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: pchronicle-benchmark-part-${{ matrix.ref }}-${{ github.run_id }} + name: pchronicle-benchmark-part-${{ github.run_id }} path: ${{ runner.temp }}/pchronicle-benchmark retention-days: 30 if-no-files-found: warn diff --git a/crates/persisting-dlcapt/src/capture/step_table_writer.rs b/crates/persisting-dlcapt/src/capture/step_table_writer.rs index 3d1caf995..09aab7cb7 100644 --- a/crates/persisting-dlcapt/src/capture/step_table_writer.rs +++ b/crates/persisting-dlcapt/src/capture/step_table_writer.rs @@ -8,6 +8,10 @@ pub use crate::capture::writers::lance_crate::LanceCrateWriter; /// 单表 session_steps 追加写;一步一行。 #[async_trait] +#[allow( + clippy::double_must_use, + reason = "async_trait adds must_use to futures" +)] pub trait StepTableWriter: Send + Sync { async fn append(&self, record: &StepRecord) -> Result<()>; } diff --git a/crates/persisting-events/src/control.rs b/crates/persisting-events/src/control.rs index 46760157d..e934d9a71 100644 --- a/crates/persisting-events/src/control.rs +++ b/crates/persisting-events/src/control.rs @@ -222,6 +222,10 @@ pub enum ChronicleControlResponse { } #[async_trait] +#[allow( + clippy::double_must_use, + reason = "async_trait adds must_use to futures" +)] pub trait ChronicleControl: Send + Sync { fn root_uri(&self) -> &str; diff --git a/crates/persisting-overlaynet/src/server.rs b/crates/persisting-overlaynet/src/server.rs index 441e671a0..5c482d4ac 100644 --- a/crates/persisting-overlaynet/src/server.rs +++ b/crates/persisting-overlaynet/src/server.rs @@ -38,6 +38,10 @@ pub struct OverlayRequestContext { /// always delivered to the configured sink. OverlayNet is independent of any /// concrete sink; an implementation may also compose several downstream sinks. #[async_trait] +#[allow( + clippy::double_must_use, + reason = "async_trait adds must_use to futures" +)] pub trait OverlaySink: Clone + Send + Sync + 'static { type RequestContext: Clone + Send + Sync + 'static; diff --git a/crates/persisting-pchronicle/examples/langfuse_backend_feasibility.rs b/crates/persisting-pchronicle/examples/langfuse_backend_feasibility.rs index c85040fb9..cadab1e38 100644 --- a/crates/persisting-pchronicle/examples/langfuse_backend_feasibility.rs +++ b/crates/persisting-pchronicle/examples/langfuse_backend_feasibility.rs @@ -140,6 +140,10 @@ struct BackendHealth { } #[async_trait(?Send)] +#[allow( + clippy::double_must_use, + reason = "async_trait adds must_use to futures" +)] trait LangfuseAnalyticsBackend { async fn append(&mut self, rows: &[LogicalRow]) -> Result; async fn point(&self, project_id: &str, logical_id: &str) -> Result>; From 128282bbac68e967f5070fec90e70d1f8fed3de2 Mon Sep 17 00:00:00 2001 From: Reiase Date: Fri, 2 Oct 2026 09:05:41 +0800 Subject: [PATCH 4/4] Center documentation on pChronicle and remove retired product structure --- AGENTS.md | 4 +- README.md | 3 +- crates/persisting-agentctl/README.md | 20 +- crates/persisting-agentctl/src/client.rs | 4 +- crates/persisting-agentctl/src/lib.rs | 5 +- crates/persisting-agentctl/src/protocol.rs | 24 +- crates/persisting-agentctl/src/runtime.rs | 26 +- crates/persisting-agentctl/src/supervisor.rs | 4 +- crates/persisting-events/src/control.rs | 2 +- crates/persisting-events/src/lib.rs | 2 +- crates/persisting-gateway/src/config.rs | 10 +- .../persisting-gateway/src/gateway/state.rs | 2 +- .../src/runtime/in_process.rs | 6 +- .../src/runtime/run_config.rs | 2 +- crates/persisting-gateway/tests/README.md | 2 +- crates/persisting-overlaynet/README.md | 18 +- crates/persisting-overlaynet/src/lib.rs | 2 +- crates/persisting-overlaynet/src/vm.rs | 2 +- crates/persisting-pchronicle-cli/README.md | 12 +- .../persisting-pchronicle-cli/src/control.rs | 2 +- crates/persisting-pchronicle/README.md | 15 +- .../src/store/attempt_registry.rs | 4 +- .../src/store/run_control.rs | 2 +- docs/archive/README.md | 18 +- docs/archive/legacy-nav/design/agentvisor.md | 4 - .../legacy-nav/design/agentvisor.zh.md | 4 - docs/archive/legacy-nav/design/cli-ppilot.md | 4 - docs/archive/legacy-nav/design/cli-pvisor.md | 4 - docs/archive/legacy-nav/design/gateway.md | 4 - docs/archive/legacy-nav/design/overlaynet.md | 4 - docs/archive/legacy-nav/design/ppilot.md | 4 - .../legacy-nav/design/pvisor-isolation.md | 4 - docs/archive/legacy-nav/guide/capture.md | 4 - docs/archive/legacy-nav/guide/capture.zh.md | 4 - docs/archive/legacy-nav/guide/index.md | 4 - docs/archive/legacy-nav/guide/index.zh.md | 4 - docs/archive/legacy-nav/guide/orchestrate.md | 4 - .../legacy-nav/guide/orchestrate.zh.md | 4 - docs/archive/legacy-nav/guide/overlaynet.md | 4 - .../archive/legacy-nav/guide/overlaynet.zh.md | 4 - .../legacy-nav/guide/pvisor-execution.md | 4 - .../legacy-nav/guide/pvisor-execution.zh.md | 4 - docs/archive/legacy-nav/guide/review-apply.md | 4 - .../legacy-nav/guide/review-apply.zh.md | 4 - docs/archive/legacy-nav/quickstart.md | 4 - docs/archive/legacy-nav/quickstart.zh.md | 4 - .../diagrams/agentvisor/agentvisor-stack.svg | 141 ---------- .../diagrams/agentvisor/effect-governance.svg | 108 -------- .../agentvisor/execution-continuum.svg | 114 -------- .../diagrams/persisting/libkrun-executor.svg | 22 -- .../persisting/pchronicle-product.svg | 2 +- .../diagrams/persisting/system-products.svg | 105 -------- .../pvisor/agentvisor-architecture.svg | 135 ---------- docs/src/assets/logos/pvisor-icon.png | Bin 135697 -> 0 bytes docs/src/assets/logos/pvisor-with-text.png | Bin 224004 -> 0 bytes docs/src/assets/system-products.svg | 105 -------- docs/src/en/installation.md | 1 - docs/src/en/pchronicle/design/architecture.md | 2 +- docs/src/en/pchronicle/design/index.md | 2 +- .../pchronicle/design/trajectory-storage.md | 18 +- docs/src/en/pchronicle/guides/index.md | 2 - .../src/en/pchronicle/guides/serve-gateway.md | 12 +- docs/src/en/pchronicle/index.md | 6 +- docs/src/en/project/index.md | 2 - docs/src/en/rfcs/0002-events-format.md | 6 +- docs/src/en/rfcs/0003-pchronicle-ownership.md | 6 +- .../en/rfcs/0006-pchronicle-vortex-backend.md | 6 +- ...0007-events-contract-pchronicle-sidecar.md | 76 ++---- docs/src/en/roadmap.md | 6 +- docs/src/en/system-design/architecture.md | 245 ++++-------------- .../src/en/system-design/design-principles.md | 47 ++-- docs/src/en/system-design/index.md | 72 ++--- docs/src/en/system-design/local-to-fleet.md | 30 --- .../src/en/system-design/security-evidence.md | 54 ---- docs/src/en/why-persisting.md | 13 +- docs/src/zh/installation.md | 2 +- docs/src/zh/pchronicle/design/architecture.md | 2 +- docs/src/zh/pchronicle/design/index.md | 2 +- .../pchronicle/design/trajectory-storage.md | 14 +- docs/src/zh/pchronicle/guides/index.md | 2 - .../src/zh/pchronicle/guides/serve-gateway.md | 10 +- docs/src/zh/pchronicle/index.md | 5 +- docs/src/zh/project/engineering.md | 2 +- docs/src/zh/project/index.md | 2 - docs/src/zh/rfcs/0002-events-format.md | 6 +- docs/src/zh/rfcs/0003-pchronicle-ownership.md | 6 +- .../zh/rfcs/0006-pchronicle-vortex-backend.md | 6 +- ...0007-events-contract-pchronicle-sidecar.md | 76 ++---- docs/src/zh/roadmap.md | 6 +- docs/src/zh/system-design/architecture.md | 214 +++------------ .../src/zh/system-design/design-principles.md | 34 ++- docs/src/zh/system-design/index.md | 60 +---- docs/src/zh/system-design/local-to-fleet.md | 26 -- .../src/zh/system-design/security-evidence.md | 48 ---- docs/src/zh/why-persisting.md | 8 +- .../diagrams/agentvisor/agentvisor-stack.svg | 141 ---------- .../diagrams/agentvisor/effect-governance.svg | 108 -------- .../agentvisor/execution-continuum.svg | 114 -------- .../diagrams/persisting/libkrun-executor.svg | 22 -- .../persisting/pchronicle-product.svg | 2 +- .../diagrams/persisting/system-products.svg | 105 -------- .../pvisor/agentvisor-architecture.svg | 135 ---------- docs/static/img/logos/pvisor-icon.png | Bin 135697 -> 0 bytes docs/static/img/logos/pvisor-with-text.png | Bin 224004 -> 0 bytes docs/zensical.toml | 2 +- 105 files changed, 352 insertions(+), 2425 deletions(-) delete mode 100644 docs/archive/legacy-nav/design/agentvisor.md delete mode 100644 docs/archive/legacy-nav/design/agentvisor.zh.md delete mode 100644 docs/archive/legacy-nav/design/cli-ppilot.md delete mode 100644 docs/archive/legacy-nav/design/cli-pvisor.md delete mode 100644 docs/archive/legacy-nav/design/gateway.md delete mode 100644 docs/archive/legacy-nav/design/overlaynet.md delete mode 100644 docs/archive/legacy-nav/design/ppilot.md delete mode 100644 docs/archive/legacy-nav/design/pvisor-isolation.md delete mode 100644 docs/archive/legacy-nav/guide/capture.md delete mode 100644 docs/archive/legacy-nav/guide/capture.zh.md delete mode 100644 docs/archive/legacy-nav/guide/index.md delete mode 100644 docs/archive/legacy-nav/guide/index.zh.md delete mode 100644 docs/archive/legacy-nav/guide/orchestrate.md delete mode 100644 docs/archive/legacy-nav/guide/orchestrate.zh.md delete mode 100644 docs/archive/legacy-nav/guide/overlaynet.md delete mode 100644 docs/archive/legacy-nav/guide/overlaynet.zh.md delete mode 100644 docs/archive/legacy-nav/guide/pvisor-execution.md delete mode 100644 docs/archive/legacy-nav/guide/pvisor-execution.zh.md delete mode 100644 docs/archive/legacy-nav/guide/review-apply.md delete mode 100644 docs/archive/legacy-nav/guide/review-apply.zh.md delete mode 100644 docs/archive/legacy-nav/quickstart.md delete mode 100644 docs/archive/legacy-nav/quickstart.zh.md delete mode 100644 docs/src/assets/diagrams/agentvisor/agentvisor-stack.svg delete mode 100644 docs/src/assets/diagrams/agentvisor/effect-governance.svg delete mode 100644 docs/src/assets/diagrams/agentvisor/execution-continuum.svg delete mode 100644 docs/src/assets/diagrams/persisting/libkrun-executor.svg delete mode 100644 docs/src/assets/diagrams/persisting/system-products.svg delete mode 100644 docs/src/assets/diagrams/pvisor/agentvisor-architecture.svg delete mode 100644 docs/src/assets/logos/pvisor-icon.png delete mode 100644 docs/src/assets/logos/pvisor-with-text.png delete mode 100644 docs/src/assets/system-products.svg delete mode 100644 docs/src/en/system-design/local-to-fleet.md delete mode 100644 docs/src/en/system-design/security-evidence.md delete mode 100644 docs/src/zh/system-design/local-to-fleet.md delete mode 100644 docs/src/zh/system-design/security-evidence.md delete mode 100644 docs/static/img/diagrams/agentvisor/agentvisor-stack.svg delete mode 100644 docs/static/img/diagrams/agentvisor/effect-governance.svg delete mode 100644 docs/static/img/diagrams/agentvisor/execution-continuum.svg delete mode 100644 docs/static/img/diagrams/persisting/libkrun-executor.svg delete mode 100644 docs/static/img/diagrams/persisting/system-products.svg delete mode 100644 docs/static/img/diagrams/pvisor/agentvisor-architecture.svg delete mode 100644 docs/static/img/logos/pvisor-icon.png delete mode 100644 docs/static/img/logos/pvisor-with-text.png diff --git a/AGENTS.md b/AGENTS.md index 206c07b77..bf714448b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,8 +26,8 @@ This exclusion covers, in particular: workflows The default active scope is pChronicle, its trajectory CLI and Web UI, and -the Gateway, Control, events, and OverlayNet libraries it depends on. pVisor -and pPilot are maintained in external repositories. `persisting-dlcapt` is a separate standalone component; excluding it +the Gateway, Control, events, and OverlayNet libraries it depends on. +`persisting-dlcapt` is a separate standalone component; excluding it does not exclude Gateway trajectory capture or pChronicle capture storage. Enter an excluded subsystem only when: diff --git a/README.md b/README.md index e2827e2f9..eddc01e8c 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,7 @@ Persisting logo Persisting contains **pChronicle**, which captures, browses, queries, exchanges, -and serves durable Agent trajectory Datasets. pVisor and pPilot have moved to -external repositories and are no longer built or distributed here. +and serves durable Agent trajectory Datasets. pChronicle diff --git a/crates/persisting-agentctl/README.md b/crates/persisting-agentctl/README.md index 9d5c11367..26eb1e6c2 100644 --- a/crates/persisting-agentctl/README.md +++ b/crates/persisting-agentctl/README.md @@ -1,17 +1,14 @@ # persisting-agentctl -pVisor and pPilot are maintained in external repositories. This repository -ships pChronicle and the internal libraries needed for capture and history. - **Agent control contracts, policies, the versioned AgentCtl v1 protocol, and its synchronous client SDK.** Owns the runtime control state machine, the wire protocol, and -`AgentCtlClient`. AgentCtl is an optional, cooperative channel between pVisor +`AgentCtlClient`. AgentCtl is an optional, cooperative channel between a control server and a Run-local runtime client. It is not a sandbox, does not discover processes or external effects, and is never enforcement evidence by itself. -pVisor owns the Run-scoped server and credential injection. OverlayNet and +Integrations provide the Run-scoped server and credential injection. OverlayNet and Gateway apply policy decisions; they do not own this protocol. ```text @@ -22,7 +19,7 @@ Requested -> Allowed / Denied -> Applied / Failed - `ControlController` evaluates policy and returns the authorization transition. - `ControlMachine` validates transitions and retains the state/history. - `protocol` is the dependency-light request/response schema shared with - pVisor's server. + the control server. - `AgentCtlClient` discovers the authenticated Unix endpoint from the environment and drives Session creation, periodic state synchronization, and checkpoint quiescence. @@ -31,13 +28,13 @@ An `Applied { effect: Deny }` state means the driver successfully blocked an operation. It does not mean that a proxy-based driver is non-bypassable. The protocol has two requests: `Hello` authenticates and opens a Session; -`Sync` exchanges the client's current state for pVisor's current directive. -Clients report `active`, `idle`, or `quiesced { checkpoint_id }`. pVisor replies +`Sync` exchanges the client's current state for the control server's current directive. +Clients report `active`, `idle`, or `quiesced { checkpoint_id }`. The server replies with `continue`, `quiesce { checkpoint_id, deadline_unix_ms? }`, or `shutdown { reason? }`. A checkpoint succeeds only after every Session frozen into that checkpoint reports the matching quiesced state. -pVisor injects four Run-local variables: `PERSISTING_AGENTCTL_ENDPOINT`, +Integrations provide four Run-local variables: `PERSISTING_AGENTCTL_ENDPOINT`, `PERSISTING_AGENTCTL_TOKEN`, `PERSISTING_AGENTCTL_VERSION` (exactly `1`), and `PERSISTING_AGENTCTL_TRANSPORT` (currently `unix`). New integrations use only `PERSISTING_AGENTCTL_*`. A future interactive login or terminal will use a @@ -53,7 +50,7 @@ examples, state semantics, typed errors, and safety boundary. use persisting_agentctl::{AgentCtlClient, AgentCtlClientConfig, AgentState}; let Some(config) = AgentCtlClientConfig::from_current_environment("worker-1")? else { - return Ok(()); // not running under pVisor + return Ok(()); // no control endpoint configured }; let mut client = AgentCtlClient::new(config); let directive = client.connect()?; @@ -68,7 +65,4 @@ just test persisting-agentctl ## Links -- pVisor isolation architecture (external repository) -- OverlayNet architecture (external repository) - [System architecture](../../docs/src/en/system-design/architecture.md) -- `persisting-pvisor` (external repository) diff --git a/crates/persisting-agentctl/src/client.rs b/crates/persisting-agentctl/src/client.rs index 0f63729ad..e674e1289 100644 --- a/crates/persisting-agentctl/src/client.rs +++ b/crates/persisting-agentctl/src/client.rs @@ -1,4 +1,4 @@ -//! Synchronous client for pVisor's cooperative, low-frequency AgentCtl protocol. +//! Synchronous client for the cooperative, low-frequency AgentCtl protocol. use crate::{ AGENTCTL_ENDPOINT_ENV, AGENTCTL_MAX_FRAME_BYTES, AGENTCTL_TOKEN_ENV, AGENTCTL_TRANSPORT_ENV, @@ -143,7 +143,7 @@ impl AgentCtlClient { } } - /// Report cooperative state and return pVisor's current directive. + /// Report cooperative state and return the server's current directive. pub fn sync(&mut self, state: AgentState) -> anyhow::Result { let session_id = self .session_id diff --git a/crates/persisting-agentctl/src/lib.rs b/crates/persisting-agentctl/src/lib.rs index 038560048..6b560013f 100644 --- a/crates/persisting-agentctl/src/lib.rs +++ b/crates/persisting-agentctl/src/lib.rs @@ -3,9 +3,8 @@ //! Drivers such as OverlayNet and Capture submit typed resources to a //! [`ControlController`]. Authorization is represented as a state transition; //! the driver then records whether the authorized operation was applied or -//! failed. [`AgentCtlClient`] implements pVisor's optional cooperative -//! AgentCtl protocol. Supervisor messages are shared wire contracts rather than -//! types owned by either pPilot or pVisor. +//! failed. [`AgentCtlClient`] implements the optional cooperative AgentCtl +//! protocol. Supervisor messages are shared wire contracts for integrations. mod client; pub mod protocol; diff --git a/crates/persisting-agentctl/src/protocol.rs b/crates/persisting-agentctl/src/protocol.rs index 7dbb0a59b..01605b95b 100644 --- a/crates/persisting-agentctl/src/protocol.rs +++ b/crates/persisting-agentctl/src/protocol.rs @@ -1,8 +1,8 @@ //! AgentCtl v1 Control-plane wire contract. //! -//! AgentCtl is an optional, cooperative channel between pVisor and runtime +//! AgentCtl is an optional, cooperative channel between a control server and runtime //! clients inside one Run. A client authenticates once with [`AgentRequest::Hello`] -//! and then periodically exchanges its [`AgentState`] for pVisor's current +//! and then periodically exchanges its [`AgentState`] for the server's current //! [`AgentDirective`] through [`AgentRequest::Sync`]. Each bounded, //! newline-delimited JSON connection carries exactly one request and one //! response. @@ -15,7 +15,7 @@ //! client must observe before admitting work. A successful `Sync` refreshes //! Session liveness and returns the latest directive. //! -//! Outside a checkpoint, pVisor considers a Session stale after it misses +//! Outside a checkpoint, the server considers a Session stale after it misses //! three recommended Sync intervals. A later `Hello` may replace a stale //! Session. Live duplicates and capacity conflicts are rejected. Staleness is //! diagnostic and lifecycle state; it is not proof that the client process @@ -23,17 +23,17 @@ //! //! # Checkpoint protocol //! -//! When pVisor publishes [`AgentDirective::Quiesce`], every runtime Session +//! When the server publishes [`AgentDirective::Quiesce`], every runtime Session //! that was live at checkpoint start must stop accepting work, drain in-flight //! work, and report [`AgentState::Quiesced`] with the same checkpoint ID. -//! pVisor freezes that participant set, rejects every new or replacement +//! The server freezes that participant set, rejects every new or replacement //! `Hello`, and never expires a participant until the checkpoint completes or //! is abandoned. Each checkpoint attempt requires a fresh matching report; //! an acknowledgement retained from an earlier attempt cannot satisfy it. //! //! A new `Quiesced` report whose ID does not match the active checkpoint is a //! conflict. Repeating the exact accepted report is idempotent, including -//! after pVisor publishes `Continue` or `Shutdown`, so a still-quiesced client +//! after the server publishes `Continue` or `Shutdown`, so a still-quiesced client //! can learn that new directive. Reporting `Active` or `Idle` while draining //! removes any current acknowledgement. Clients remain quiesced until they //! observe [`AgentDirective::Continue`] or [`AgentDirective::Shutdown`]. A @@ -53,7 +53,7 @@ //! //! Client states are Agent declarations. They are not enforcement evidence, //! an authoritative process inventory, or proof that unreported external -//! effects do not exist. pVisor obtains authoritative process facts from its +//! effects do not exist. Integrations obtain authoritative process facts from the //! execution provider. //! //! # Wire examples @@ -96,7 +96,7 @@ pub const AGENTCTL_VERSION_ENV: &str = "PERSISTING_AGENTCTL_VERSION"; /// Environment variable naming the transport, currently `unix`. pub const AGENTCTL_TRANSPORT_ENV: &str = "PERSISTING_AGENTCTL_TRANSPORT"; -/// A request sent by a runtime client to pVisor. +/// A request sent by a runtime client to the server. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum AgentRequest { @@ -104,12 +104,12 @@ pub enum AgentRequest { Hello { /// Protocol version spoken by the client. version: u32, - /// Run-scoped bearer token injected by pVisor. + /// Run-scoped bearer token injected by the server. token: String, /// Non-empty client identity, unique among live Sessions in the Run. client_id: String, }, - /// Report current cooperative state and obtain pVisor's current directive. + /// Report current cooperative state and obtain the server's current directive. Sync { /// Protocol version spoken by the client. version: u32, @@ -135,7 +135,7 @@ pub enum AgentState { }, } -/// Desired cooperative state published by pVisor. +/// Desired cooperative state published by the server. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum AgentDirective { @@ -171,7 +171,7 @@ pub enum AgentErrorCode { Conflict, } -/// A response sent by pVisor to a runtime client. +/// A response sent by the server to a runtime client. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum AgentResponse { diff --git a/crates/persisting-agentctl/src/runtime.rs b/crates/persisting-agentctl/src/runtime.rs index f338fbb62..2cd76a570 100644 --- a/crates/persisting-agentctl/src/runtime.rs +++ b/crates/persisting-agentctl/src/runtime.rs @@ -1,4 +1,4 @@ -//! Stable value types shared by pVisor, pPilot, capture, and storage. +//! Stable value types shared by capture, storage, and runtime integrations. //! //! The runtime and narrative dimensions are deliberately orthogonal: //! @@ -64,7 +64,7 @@ string_id!(RunId); string_id!(AttemptId); string_id!(StorylineId); -/// Connection material injected by pPilot into a RunSpec it launches. +/// Connection material supplied by a coordinator in a RunSpec. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SupervisorBootstrap { pub endpoint: String, @@ -72,8 +72,8 @@ pub struct SupervisorBootstrap { pub controller_epoch: u64, #[serde(default = "default_supervisor_connect_timeout_ms")] pub connect_timeout_ms: u64, - /// pChronicle root used by pVisor to publish durable Attempt liveness and - /// terminal results. This is optional for standalone pVisor Runs. + /// pChronicle root used to publish durable Attempt liveness and + /// terminal results. This is optional for standalone Runs. #[serde(default, skip_serializing_if = "Option::is_none")] pub attempt_registry_uri: Option, #[serde(default = "default_attempt_ttl_ms")] @@ -100,13 +100,13 @@ impl AgentRef { } } -/// One semantic Agent execution submitted to pVisor. +/// One semantic Agent execution described by a runtime integration. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RunSpec { #[serde(default = "runtime_schema_version")] pub schema_version: u32, pub run_id: RunId, - /// Monotonic pPilot ownership generation. Zero is reserved for callers that + /// Monotonic coordinator ownership generation. Zero is reserved for callers that /// do not use durable orchestration/fencing. #[serde(default)] pub lease_epoch: u64, @@ -122,8 +122,8 @@ pub struct RunSpec { pub runtime: RuntimeConfig, #[serde(default)] pub capabilities: CapabilitySet, - /// Optional pPilot control channel. Absence, connection failure, or later - /// disconnection never prevents standalone pVisor execution. + /// Optional coordinator control channel. Absence, connection failure, or later + /// disconnection never prevents standalone execution. #[serde(default, skip_serializing_if = "Option::is_none")] pub supervisor: Option, #[serde(default)] @@ -213,7 +213,7 @@ pub enum StdioMode { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RuntimeConfig { - /// Wall-clock limit for one Attempt. `None` means no pVisor deadline. + /// Wall-clock limit for one Attempt. `None` means no execution deadline. #[serde(default, skip_serializing_if = "Option::is_none")] pub timeout_ms: Option, /// Grace period between a cooperative process-tree termination request and @@ -382,7 +382,7 @@ pub struct NetworkBandwidthLimit { pub bytes_per_second: u64, } -/// Runtime-neutral network request presented to pVisor for authorization. +/// Runtime-neutral network request presented to a controller for authorization. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NetworkAccessRequest { #[serde(default, skip_serializing_if = "Option::is_none")] @@ -410,7 +410,7 @@ pub enum NetworkTransport { } /// Model invocation metadata. Request/response bodies intentionally stay in -/// Capture; pVisor receives only the information needed for policy and audit. +/// Capture; the controller receives only the information needed for policy and audit. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ModelCallRequest { #[serde(default, skip_serializing_if = "Option::is_none")] @@ -794,7 +794,7 @@ pub struct RunResult { pub warnings: Vec, } -/// The current pPilot execution owner for one logical Run. +/// The current execution owner for one logical Run. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RunLeaseRecord { pub run_id: RunId, @@ -809,7 +809,7 @@ pub struct RunLeaseRecord { } /// Immutable terminal commit request. `result_digest` binds the commit to the -/// durable pPilot completion record without embedding an arbitrarily large +/// durable completion record without embedding an arbitrarily large /// result in the control object. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RunCommitRequest { diff --git a/crates/persisting-agentctl/src/supervisor.rs b/crates/persisting-agentctl/src/supervisor.rs index 43df056c0..be081daf8 100644 --- a/crates/persisting-agentctl/src/supervisor.rs +++ b/crates/persisting-agentctl/src/supervisor.rs @@ -1,4 +1,4 @@ -//! Versioned pPilot Supervisor protocol shared by the control and execution planes. +//! Versioned Supervisor protocol shared by the control and execution planes. use crate::{AttemptId, NetworkBandwidthLimit, RunId}; use serde::{Deserialize, Serialize}; @@ -38,7 +38,7 @@ pub enum SupervisorClientMessage { Ack(SupervisorDirectiveAck), } -/// A time-bounded rate grant enforced locally by pVisor. +/// A time-bounded rate grant enforced locally by a runtime integration. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SupervisorNetworkQuotaGrant { pub grant_id: String, diff --git a/crates/persisting-events/src/control.rs b/crates/persisting-events/src/control.rs index e934d9a71..68e210810 100644 --- a/crates/persisting-events/src/control.rs +++ b/crates/persisting-events/src/control.rs @@ -1,7 +1,7 @@ //! Lightweight, versioned control-plane client for the standalone pChronicle process. //! //! This optional module contains no storage engine. pChronicle implements the -//! durable operations; orchestrators such as pPilot depend only on these +//! durable operations; local integrations depend only on these //! contracts and the long-lived process transport. use crate::{EventRecord, unix_now_ms}; diff --git a/crates/persisting-events/src/lib.rs b/crates/persisting-events/src/lib.rs index 6d43fc6c5..4fb40eb5c 100644 --- a/crates/persisting-events/src/lib.rs +++ b/crates/persisting-events/src/lib.rs @@ -1,6 +1,6 @@ //! Storage-independent runtime event contracts shared by Persisting components. //! -//! Producers such as pVisor and Gateway emit these records. Consumers such as +//! Producers such as Gateway emit these records. Consumers such as //! pChronicle decide how to persist, query, and project them. //! The optional `control` feature also carries the lightweight, versioned //! sidecar protocol so callers do not need another protocol-only package. diff --git a/crates/persisting-gateway/src/config.rs b/crates/persisting-gateway/src/config.rs index 053dac85c..e4ab20377 100644 --- a/crates/persisting-gateway/src/config.rs +++ b/crates/persisting-gateway/src/config.rs @@ -31,13 +31,13 @@ pub struct ProxyConfig { /// Harbor-aligned egress policy for forward-proxy traffic (`CONNECT` + absolute-URI). #[serde(default)] pub network: NetworkConfig, - /// Optional embedded OverlayFS mount for the Attempt (consumed by pVisor). + /// Optional overlay configuration for integrations; Gateway does not mount it. #[serde(default)] pub overlay: OverlayConfig, pub models: Vec, } -/// Filesystem overlay settings (same capture TOML; applied by pVisor). +/// Filesystem overlay settings retained in capture TOML for integrations. /// /// Model: **target** (read-only base / apply destination) + **staging** (upper /// holds deltas). The Agent sees `merged`; changes do **not** touch `target` @@ -45,7 +45,7 @@ pub struct ProxyConfig { #[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct OverlayConfig { - /// When true, pVisor mounts its embedded OverlayFS for the Attempt. + /// Whether the integration requests an overlay for the Attempt. #[serde(default)] pub enabled: bool, /// Target filesystem: primary lower layer and destination for `apply`. @@ -70,7 +70,7 @@ pub struct OverlayConfig { /// operation log (overrides `{storage}/.overlay/jujutsu`). #[serde(default)] pub jujutsu_store_path: Option, - /// Jujutsu workspace/fork name (defaults to the pVisor session id). + /// Jujutsu workspace/fork name for the integration. #[serde(default)] pub jujutsu_workspace: Option, /// Writable upper directory (overrides `{stage_dir}/upper` when set). @@ -83,7 +83,7 @@ pub struct OverlayConfig { #[serde(default)] pub merged_dir: Option, /// If true, apply staging onto `target` automatically when the Attempt ends. - /// Default false — review then `pvisor apply` or `pvisor drop`. + /// Default false; effects require an explicit apply or discard decision. #[serde(default)] pub auto_apply: bool, /// If true, discard staging automatically when the Attempt ends. diff --git a/crates/persisting-gateway/src/gateway/state.rs b/crates/persisting-gateway/src/gateway/state.rs index 7e36d78b8..b54740e3b 100644 --- a/crates/persisting-gateway/src/gateway/state.rs +++ b/crates/persisting-gateway/src/gateway/state.rs @@ -136,7 +136,7 @@ pub async fn serve_with_listeners_and_shutdown( /// Gateway sink with an injected runtime control state controller. /// -/// pVisor injects the controller; Gateway and OverlayNet apply model/network +/// The caller supplies the controller; Gateway and OverlayNet apply model/network /// transitions while retaining HTTP adaptation and trajectory extraction. pub async fn serve_with_runtime_control( config: ProxyConfig, diff --git a/crates/persisting-gateway/src/runtime/in_process.rs b/crates/persisting-gateway/src/runtime/in_process.rs index 696063ad9..6b419516a 100644 --- a/crates/persisting-gateway/src/runtime/in_process.rs +++ b/crates/persisting-gateway/src/runtime/in_process.rs @@ -1,4 +1,4 @@ -//! In-process Gateway for one pVisor Attempt (no forked daemon). +//! In-process Gateway for one Attempt (no forked daemon). use std::net::TcpStream; use std::path::PathBuf; @@ -22,14 +22,14 @@ pub struct InProcessCapture { } /// Attempt-scoped network services shared by Gateway and other interception -/// drivers owned by pVisor. +/// drivers supplied by the caller. #[derive(Clone)] pub struct InProcessRuntime { pub controller: Arc, pub interception_metrics: InterceptionMetrics, pub bandwidth_registry: BandwidthRegistry, pub attempt_id: Option, - /// Disable LLM dispatch for pVisor runs that only need the network proxy. + /// Disable LLM dispatch for Runs that only need the network proxy. pub gateway_enabled: bool, } diff --git a/crates/persisting-gateway/src/runtime/run_config.rs b/crates/persisting-gateway/src/runtime/run_config.rs index f8b11c6b8..5f30e7d1d 100644 --- a/crates/persisting-gateway/src/runtime/run_config.rs +++ b/crates/persisting-gateway/src/runtime/run_config.rs @@ -33,7 +33,7 @@ pub fn snapshot_run_proxy_config( } /// Write an already-resolved proxy configuration into the session snapshot. -/// pVisor uses this after merging its TOML and CLI configuration, so Gateway +/// Callers use this after resolving their configuration, so Gateway /// runtime setup never needs to know where configuration originated. pub fn snapshot_proxy_config( storage: &Path, diff --git a/crates/persisting-gateway/tests/README.md b/crates/persisting-gateway/tests/README.md index cc4247bf5..f22d0f272 100644 --- a/crates/persisting-gateway/tests/README.md +++ b/crates/persisting-gateway/tests/README.md @@ -29,4 +29,4 @@ cargo nextest run -p persisting-gateway --test model_api_forwarding --locked ## Links - [`persisting-gateway`](../README.md) -- [Capture trajectories](../../../docs/src/pvisor/guides/capture.md) +- [Capture trajectories](../../../docs/src/en/pchronicle/guides/serve-gateway.md) diff --git a/crates/persisting-overlaynet/README.md b/crates/persisting-overlaynet/README.md index 050f7c953..992e77a38 100644 --- a/crates/persisting-overlaynet/README.md +++ b/crates/persisting-overlaynet/README.md @@ -1,8 +1,5 @@ # persisting-overlaynet -pVisor and pPilot are maintained in external repositories. This repository -ships pChronicle and the internal libraries needed for capture and history. - **Network interception and egress-policy data planes for trajectory capture.** Owns the proxy data plane: request classification, HTTP `CONNECT`, @@ -10,26 +7,15 @@ absolute-URI forwarding, access enforcement through [`persisting-agentctl`](../persisting-agentctl/README.md), request accounting, shared proxy header safety, and dispatch to one caller-supplied `OverlaySink`. -Also owns the libkrun VM driver: a non-bypassable virtio-net path whose -in-process smoltcp stack serves DHCP and synthetic DNS, then terminates and -re-originates policy-authorized IPv4 TCP. Gateway capture is reachable through -the guest's virtual router; Gateway and ordinary VM egress share the Attempt -controller, metrics, and bandwidth buckets. - Does not own LLM protocol adaptation, upstream selection, session correlation, capture events, WAL, or pChronicle writes. [`persisting-gateway`](../persisting-gateway/README.md) implements `OverlaySink` -for those. pVisor owns Run configuration, executor selection, and the recorded -interception profile. +for those. The explicit-proxy profile is deliberately marked `cooperative`: policy decisions over intercepted requests are not non-bypassable enforcement. `no-network` and `allowlist` mean "for traffic that reached this proxy"; direct sockets and clients that remove proxy variables remain ambient. -`InterceptionProfile::vm_smoltcp()` records the VM's implemented non-bypassable -TCP/DNS surface. General UDP, IPv6, ICMP, QUIC, inbound forwarding, link-local, -and other reserved destinations fail closed in the MVP. Accepted Linux-host -netns and seccomp designs remain future independent drivers. ## Develop @@ -39,7 +25,5 @@ just test persisting-overlaynet ## Links -- OverlayNet architecture (external repository) -- Network control (external repository) - [`persisting-gateway`](../persisting-gateway/README.md) - [`persisting-agentctl`](../persisting-agentctl/README.md) diff --git a/crates/persisting-overlaynet/src/lib.rs b/crates/persisting-overlaynet/src/lib.rs index be637f20e..3f1f783b3 100644 --- a/crates/persisting-overlaynet/src/lib.rs +++ b/crates/persisting-overlaynet/src/lib.rs @@ -1,4 +1,4 @@ -//! Network interception and egress policy drivers for pVisor. +//! Network interception and egress policy drivers for trajectory capture. //! //! Host/container execution uses the cooperative HTTP proxy. libkrun VM //! execution uses a non-bypassable virtio-net/smoltcp IPv4 TCP and DNS data diff --git a/crates/persisting-overlaynet/src/vm.rs b/crates/persisting-overlaynet/src/vm.rs index 059530d7c..e5e2f7ab6 100644 --- a/crates/persisting-overlaynet/src/vm.rs +++ b/crates/persisting-overlaynet/src/vm.rs @@ -1014,7 +1014,7 @@ fn synthetic_address(address: Ipv4Addr) -> bool { /// Hard VM destinations that cannot be enabled by Public mode or by a DNS /// rebinding result. RFC1918 and loopback remain available intentionally for -/// explicit host/LAN services; pVisor's own virtual and special-purpose ranges +/// explicit host/LAN services; the virtual router and special-purpose ranges /// do not. fn forbidden_host_address(address: IpAddr) -> bool { let IpAddr::V4(address) = address else { diff --git a/crates/persisting-pchronicle-cli/README.md b/crates/persisting-pchronicle-cli/README.md index bf3a2e5b8..a908841e3 100644 --- a/crates/persisting-pchronicle-cli/README.md +++ b/crates/persisting-pchronicle-cli/README.md @@ -1,20 +1,16 @@ # pChronicle CLI -pVisor and pPilot are maintained in external repositories. This repository -ships pChronicle and the internal libraries needed for capture and history. - **Standalone `pchronicle` CLI for onboarding, browsing, querying, importing, exporting, and serving trajectory Datasets.** Owns the `pchronicle` binary, Warehouse HTTP, the write-capable -`--control` plane used by pPilot and pVisor, optional Gateway ingest/forwarding +`--control` plane for local integrations, optional Gateway ingest/forwarding flags, and the embed of staged `pchronicle-web` assets at build time. Does not own trajectory models, Lance storage, Catalog, or the query engine — those live in [`persisting-pchronicle`](../persisting-pchronicle/README.md). Does not own the Web UI source — -[`pchronicle-web`](../../pchronicle-web/README.md) does. Does not start, schedule, -or isolate Agent Runs (pVisor / pPilot). +[`pchronicle-web`](../../pchronicle-web/README.md) does. Current commands include `onboard`, `dataset` (pin/unpin/list/show/set/rename), `list`/`ls`, `stats`, bounded read-only `query`, built-in `stats` reports, assisted @@ -28,8 +24,8 @@ Storyline Lance Dataset at `--to`. Provide at least one destination. With `--input-format compact-jsonl`, only `--mirror` is valid. Use `--once` for a finite run. -`pchronicle serve --control 127.0.0.1:0 URI` is normally launched by pPilot or -pVisor. `serve --listen` is the read-only Warehouse. Public bind addresses are +`pchronicle serve --control 127.0.0.1:0 URI` exposes the authenticated local +Control protocol for event producers. `serve --listen` is the read-only Warehouse. Public bind addresses are rejected. Small deterministic Datasets live in [`../../examples/data`](../../examples/data). diff --git a/crates/persisting-pchronicle-cli/src/control.rs b/crates/persisting-pchronicle-cli/src/control.rs index 1021a1e78..37039cff8 100644 --- a/crates/persisting-pchronicle-cli/src/control.rs +++ b/crates/persisting-pchronicle-cli/src/control.rs @@ -1,7 +1,7 @@ //! pChronicle's write-capable control plane. //! //! The Warehouse HTTP server remains read-only. This long-lived JSONL/stdin -//! protocol is intended for trusted local orchestrators such as pPilot. +//! protocol is intended for trusted local integrations. use anyhow::{Context, Result}; use fs2::FileExt; diff --git a/crates/persisting-pchronicle/README.md b/crates/persisting-pchronicle/README.md index a94eeb4cb..fade90bdc 100644 --- a/crates/persisting-pchronicle/README.md +++ b/crates/persisting-pchronicle/README.md @@ -1,8 +1,5 @@ # pChronicle -pVisor and pPilot are maintained in external repositories. This repository -ships pChronicle and the internal libraries needed for capture and history. - **Persisting 的结构化轨迹与 Dataset 数据层。** 拥有轨迹领域模型、磁盘格式、Lance 持久化、数据源发现、DataFusion 查询、格式交换 @@ -13,7 +10,7 @@ ships pChronicle and the internal libraries needed for capture and history. [`persisting-pchronicle-cli`](../persisting-pchronicle-cli/README.md) 拥有 `pchronicle` 命令、loopback API 与嵌入式静态资源。 [`pchronicle-web`](../../pchronicle-web/README.md) 拥有浏览前端。 -pVisor / Gateway 生产 canonical events;pPilot 编排多个 Run。 +Gateway 生产 canonical events。 Canonical Event 与 Storyline 分别在事实层和交换/分析层保持权威,关系是单向投影: `events.lance` 是 append-only 运行时事实源;`StorylineDocument` 是与 ATIF v1.7 @@ -56,9 +53,9 @@ just proptest pchronicle ## Links -- [pChronicle overview](../../docs/src/en/pchronicle/index.zh.md) -- [产品架构](../../docs/src/en/pchronicle/design/architecture.zh.md) -- [记录数据、视图与版本](../../docs/src/en/pchronicle/concepts/facts-and-projections.zh.md) -- [pChronicle CLI](../../docs/src/en/pchronicle/reference/cli.zh.md) -- [RFC-0003 ownership](../../docs/src/rfcs/0003-pchronicle-ownership.md) +- [pChronicle overview](../../docs/src/zh/pchronicle/index.md) +- [产品架构](../../docs/src/zh/pchronicle/design/architecture.md) +- [记录数据、视图与版本](../../docs/src/zh/pchronicle/concepts/facts-and-projections.md) +- [pChronicle CLI](../../docs/src/zh/pchronicle/reference/cli.md) +- [RFC-0003 ownership](../../docs/src/zh/rfcs/0003-pchronicle-ownership.md) - [`persisting-pchronicle-cli`](../persisting-pchronicle-cli/README.md) diff --git a/crates/persisting-pchronicle/src/store/attempt_registry.rs b/crates/persisting-pchronicle/src/store/attempt_registry.rs index f79b6d281..4a1cb13fe 100644 --- a/crates/persisting-pchronicle/src/store/attempt_registry.rs +++ b/crates/persisting-pchronicle/src/store/attempt_registry.rs @@ -1,7 +1,7 @@ -//! Durable pVisor Attempt liveness and terminal-result registry. +//! Durable Attempt liveness and terminal-result registry. //! //! One CAS-managed record exists per Run. A newer lease epoch fences every -//! update from an older Attempt, while heartbeat expiry lets pPilot distinguish +//! update from an older Attempt, while heartbeat expiry lets a coordinator distinguish //! a live remote Attempt from an orphan after coordinator restart. use super::cas_store::{CasStore, Mutation, unix_now_ms}; diff --git a/crates/persisting-pchronicle/src/store/run_control.rs b/crates/persisting-pchronicle/src/store/run_control.rs index 64be65d7e..b01317318 100644 --- a/crates/persisting-pchronicle/src/store/run_control.rs +++ b/crates/persisting-pchronicle/src/store/run_control.rs @@ -70,7 +70,7 @@ impl RunControlStore { } /// Replace a lease only after the caller has established that its attempt - /// is absent or stale (for example through pPilot reconciliation). + /// is absent or stale (for example during coordinator reconciliation). pub async fn takeover_lease( &self, run_id: &RunId, diff --git a/docs/archive/README.md b/docs/archive/README.md index b082c2922..29aff255e 100644 --- a/docs/archive/README.md +++ b/docs/archive/README.md @@ -1,17 +1,11 @@ # Archived documentation -This directory preserves historical design notes that are no longer part of -the published documentation navigation. The current product architecture is -documented under [`docs/src/system-design/`](../src/system-design/) and the -product-specific design sections. - -Archived notes are retained for historical context only. They may describe -queue-era components or proposed integrations that are not part of the -current pVisor → pChronicle product path. +This directory preserves historical notes and redirect stubs outside the +published documentation. Current architecture is documented under +[`docs/src/en/system-design/`](../src/en/system-design/), with a +[Chinese edition](../src/zh/system-design/). ## legacy-nav/ -Redirect stubs from the pre-restructure `/design/`, `/guide/`, `/dev/`, and -`/quickstart` URL space. The current navigation under `pvisor/`, `pchronicle/`, -`project/`, and `system-design/` has absorbed their targets, so the -stubs no longer serve external links. Archived 2026-08-30; kept outside the Docusaurus source trees because they are not published. +Remaining redirect stubs refer to trajectory history, project engineering, and +legacy APIs. They are retained for historical context and are not published. diff --git a/docs/archive/legacy-nav/design/agentvisor.md b/docs/archive/legacy-nav/design/agentvisor.md deleted file mode 100644 index de2efaf77..000000000 --- a/docs/archive/legacy-nav/design/agentvisor.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -template: redirect.html -location: ../../pvisor/concepts/agentvisor/ ---- diff --git a/docs/archive/legacy-nav/design/agentvisor.zh.md b/docs/archive/legacy-nav/design/agentvisor.zh.md deleted file mode 100644 index de2efaf77..000000000 --- a/docs/archive/legacy-nav/design/agentvisor.zh.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -template: redirect.html -location: ../../pvisor/concepts/agentvisor/ ---- diff --git a/docs/archive/legacy-nav/design/cli-ppilot.md b/docs/archive/legacy-nav/design/cli-ppilot.md deleted file mode 100644 index 55595d609..000000000 --- a/docs/archive/legacy-nav/design/cli-ppilot.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -template: redirect.html -location: ../../pvisor/reference/ppilot-cli/ ---- diff --git a/docs/archive/legacy-nav/design/cli-pvisor.md b/docs/archive/legacy-nav/design/cli-pvisor.md deleted file mode 100644 index 958696d28..000000000 --- a/docs/archive/legacy-nav/design/cli-pvisor.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -template: redirect.html -location: ../../pvisor/reference/cli/ ---- diff --git a/docs/archive/legacy-nav/design/gateway.md b/docs/archive/legacy-nav/design/gateway.md deleted file mode 100644 index 2a752daab..000000000 --- a/docs/archive/legacy-nav/design/gateway.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -template: redirect.html -location: ../../pvisor/design/gateway/ ---- diff --git a/docs/archive/legacy-nav/design/overlaynet.md b/docs/archive/legacy-nav/design/overlaynet.md deleted file mode 100644 index 646991922..000000000 --- a/docs/archive/legacy-nav/design/overlaynet.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -template: redirect.html -location: ../../pvisor/design/overlaynet/ ---- diff --git a/docs/archive/legacy-nav/design/ppilot.md b/docs/archive/legacy-nav/design/ppilot.md deleted file mode 100644 index 371c4854c..000000000 --- a/docs/archive/legacy-nav/design/ppilot.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -template: redirect.html -location: ../../pvisor/design/orchestration/ ---- diff --git a/docs/archive/legacy-nav/design/pvisor-isolation.md b/docs/archive/legacy-nav/design/pvisor-isolation.md deleted file mode 100644 index a9b006cfd..000000000 --- a/docs/archive/legacy-nav/design/pvisor-isolation.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -template: redirect.html -location: ../../pvisor/design/isolation/ ---- diff --git a/docs/archive/legacy-nav/guide/capture.md b/docs/archive/legacy-nav/guide/capture.md deleted file mode 100644 index e258a77e9..000000000 --- a/docs/archive/legacy-nav/guide/capture.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -template: redirect.html -location: ../../pvisor/guides/capture/ ---- diff --git a/docs/archive/legacy-nav/guide/capture.zh.md b/docs/archive/legacy-nav/guide/capture.zh.md deleted file mode 100644 index e258a77e9..000000000 --- a/docs/archive/legacy-nav/guide/capture.zh.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -template: redirect.html -location: ../../pvisor/guides/capture/ ---- diff --git a/docs/archive/legacy-nav/guide/index.md b/docs/archive/legacy-nav/guide/index.md deleted file mode 100644 index d57e35f72..000000000 --- a/docs/archive/legacy-nav/guide/index.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -template: redirect.html -location: ../pvisor/guides/ ---- diff --git a/docs/archive/legacy-nav/guide/index.zh.md b/docs/archive/legacy-nav/guide/index.zh.md deleted file mode 100644 index d57e35f72..000000000 --- a/docs/archive/legacy-nav/guide/index.zh.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -template: redirect.html -location: ../pvisor/guides/ ---- diff --git a/docs/archive/legacy-nav/guide/orchestrate.md b/docs/archive/legacy-nav/guide/orchestrate.md deleted file mode 100644 index 257f229e4..000000000 --- a/docs/archive/legacy-nav/guide/orchestrate.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -template: redirect.html -location: ../../pvisor/guides/orchestrate/ ---- diff --git a/docs/archive/legacy-nav/guide/orchestrate.zh.md b/docs/archive/legacy-nav/guide/orchestrate.zh.md deleted file mode 100644 index 257f229e4..000000000 --- a/docs/archive/legacy-nav/guide/orchestrate.zh.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -template: redirect.html -location: ../../pvisor/guides/orchestrate/ ---- diff --git a/docs/archive/legacy-nav/guide/overlaynet.md b/docs/archive/legacy-nav/guide/overlaynet.md deleted file mode 100644 index c29c1e068..000000000 --- a/docs/archive/legacy-nav/guide/overlaynet.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -template: redirect.html -location: ../../pvisor/guides/network/ ---- diff --git a/docs/archive/legacy-nav/guide/overlaynet.zh.md b/docs/archive/legacy-nav/guide/overlaynet.zh.md deleted file mode 100644 index c29c1e068..000000000 --- a/docs/archive/legacy-nav/guide/overlaynet.zh.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -template: redirect.html -location: ../../pvisor/guides/network/ ---- diff --git a/docs/archive/legacy-nav/guide/pvisor-execution.md b/docs/archive/legacy-nav/guide/pvisor-execution.md deleted file mode 100644 index 888e10726..000000000 --- a/docs/archive/legacy-nav/guide/pvisor-execution.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -template: redirect.html -location: ../../pvisor/guides/execution/ ---- diff --git a/docs/archive/legacy-nav/guide/pvisor-execution.zh.md b/docs/archive/legacy-nav/guide/pvisor-execution.zh.md deleted file mode 100644 index 888e10726..000000000 --- a/docs/archive/legacy-nav/guide/pvisor-execution.zh.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -template: redirect.html -location: ../../pvisor/guides/execution/ ---- diff --git a/docs/archive/legacy-nav/guide/review-apply.md b/docs/archive/legacy-nav/guide/review-apply.md deleted file mode 100644 index a5cc71862..000000000 --- a/docs/archive/legacy-nav/guide/review-apply.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -template: redirect.html -location: ../../pvisor/guides/review-apply/ ---- diff --git a/docs/archive/legacy-nav/guide/review-apply.zh.md b/docs/archive/legacy-nav/guide/review-apply.zh.md deleted file mode 100644 index a5cc71862..000000000 --- a/docs/archive/legacy-nav/guide/review-apply.zh.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -template: redirect.html -location: ../../pvisor/guides/review-apply/ ---- diff --git a/docs/archive/legacy-nav/quickstart.md b/docs/archive/legacy-nav/quickstart.md deleted file mode 100644 index 546fe6cdb..000000000 --- a/docs/archive/legacy-nav/quickstart.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -template: redirect.html -location: ../pvisor/get-started/ ---- diff --git a/docs/archive/legacy-nav/quickstart.zh.md b/docs/archive/legacy-nav/quickstart.zh.md deleted file mode 100644 index 546fe6cdb..000000000 --- a/docs/archive/legacy-nav/quickstart.zh.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -template: redirect.html -location: ../pvisor/get-started/ ---- diff --git a/docs/src/assets/diagrams/agentvisor/agentvisor-stack.svg b/docs/src/assets/diagrams/agentvisor/agentvisor-stack.svg deleted file mode 100644 index 768eac65f..000000000 --- a/docs/src/assets/diagrams/agentvisor/agentvisor-stack.svg +++ /dev/null @@ -1,141 +0,0 @@ - - The AgentVisor category in the Agent infrastructure stack - Multiple Agents share a pool of compute, filesystem, network, model, tool, and credential resources through an AgentVisor. Each Agent Run receives an isolated virtual execution environment with its own identity, state, authority, effects, and failure boundary. - - - - - - - - - - - - - - - - - - - - - - - The AgentVisor category - The hypervisor for Agent execution - - - - Independent Agent Runs - Agent A · Agent B · Agent C · different frameworks and goals - - - request an Agent virtual execution environment - - - - AgentVisor - maps shared resources into isolated Agent virtual execution environments - - - - Agent environment A - identity · workspace · authority - Isolated state and effect boundary - - - - Agent environment B - identity · workspace · authority - Isolated state and effect boundary - - - - Agent environment C - identity · workspace · authority - Isolated state and effect boundary - - - - Resource multiplexing - compute · storage · network - Shared pool, bounded consumption - - - - Lifecycle + placement - create · place · suspend · migrate - Stable Run across physical substrates - - - - Continuity + evidence - checkpoint · lineage · outcomes - State and accountability follow the Run - - - - - Identity - human · workload - organization - Policy - intent and constraints - - - - - - Evidence - audit · lineage - attestation - History - durable accountable facts - - - - - capability-aware Attempt placement - - - - Host sandbox - local process boundary - - - Container - shared-kernel isolation - - - MicroVM - dedicated guest kernel - - - Confidential - attested environment - - - Fleet - scheduled remote provider - - - - - - Models · tools · files · data · networks · services · people · physical systems - - Multiple Agents share the infrastructure pool while identity, state, authority, effects, and failures remain isolated. - diff --git a/docs/src/assets/diagrams/agentvisor/effect-governance.svg b/docs/src/assets/diagrams/agentvisor/effect-governance.svg deleted file mode 100644 index 8d23ad997..000000000 --- a/docs/src/assets/diagrams/agentvisor/effect-governance.svg +++ /dev/null @@ -1,108 +0,0 @@ - - The AgentVisor effect governance loop - An Agent action moves through intent, admission, execution, observation, containment, and a decision to promote, reject, or compensate. Evidence closes the loop. Reversible, transactional, compensatable, and irreversible effects require different controls. - - - - - - - - - - - - - - - - - - - The effect governance loop - Autonomy is useful only when consequences have identity, state, policy, and evidence - - - - - - - - - - Intent - Agent proposes - an action - - - Admit - authority + context - - - Execute - allow · deny - or transform - - - Observe - capture actual - outcome - - - Contain - isolate or hold - when possible - - - - - - Resolve the consequence - promote · reject · expire - rollback · compensate · escalate - The decision depends on reversibility - - - - - - Evidence + accountability - intent · decision · mechanism - outcome · lineage · terminal state - Durable facts close the loop - - - - future admission learns from evidence - - Different effects require different controls - - - Reversible - stage · review · promote · discard - - - Transactional - reserve · validate · commit atomically - - - Compensatable - commit + durable counter-action - - - Irreversible - strong admission · minimal authority - - - Observation proves that something happened. It does not, by itself, make the consequence controllable or reversible. - diff --git a/docs/src/assets/diagrams/agentvisor/execution-continuum.svg b/docs/src/assets/diagrams/agentvisor/execution-continuum.svg deleted file mode 100644 index 7e29c352e..000000000 --- a/docs/src/assets/diagrams/agentvisor/execution-continuum.svg +++ /dev/null @@ -1,114 +0,0 @@ - - The AgentVisor execution continuum - A stable Agent Run identity, delegated authority, semantic checkpoint, effect frontier, lineage, and evidence can continue from a personal device through a team environment to a secure fleet, while the physical execution substrate and operational controls change. - - - - - - - - - - - - - - - - One Agent Run across an execution continuum - Semantic portability preserves authority and accountability while infrastructure changes - - - - What remains stable - - Run identity + intent - - Delegated authority - - Semantic checkpoint - - Effect frontier - - Lineage - - Enforcement + outcome evidence - - - - - promote artifacts and checkpoints - schedule with stronger guarantees - - - - Personal device - Low friction · same-owner trust - - Local AgentVisor - Autonomy inside; controlled promotion outside - - Process - sandbox - - Container - local OCI - - MicroVM - local VM - - - - - Team environment - Shared policy · review · artifacts - - Organizational AgentVisor - Identity, budgets, promotion, and common evidence - - Workstation - managed local - - Team pool - shared compute - - - - - Secure fleet - Multi-tenant · attested · reconciled - - Fleet AgentVisor - Fenced ownership, placement, and node evidence - - MicroVM - tenant pool - - Confidential - attested - - Remote - specialized - - - - - Portable artifacts and accountable history - content digests · checkpoints · effect records · evidence bundles · causal lineage - - - - - - Local-to-fleet is semantic continuity first. Live process-memory or VM migration is an optional provider capability, not the category definition. - diff --git a/docs/src/assets/diagrams/persisting/libkrun-executor.svg b/docs/src/assets/diagrams/persisting/libkrun-executor.svg deleted file mode 100644 index 31d906867..000000000 --- a/docs/src/assets/diagrams/persisting/libkrun-executor.svg +++ /dev/null @@ -1,22 +0,0 @@ - - pVisor libkrun execution path - The host Run controller prepares an OCI or explicit root filesystem and a separate workspace, passes a RunnerSpec to the self-executed pVisor runner and libkrun, then mounts the merged root through virtio-fs into a minimal Linux guest that runs the Agent. - - pVisor libkrun execution pathThe VM replaces the executor; pVisor retains Run ownership and finalization - HOST PVISOR - Run controllerRunId · AttemptId · lease - OCI registry/cacheverified image layersor explicit rootfs - Rootfs OverlayFSimmutable lowerper-Run temporary upper - Host workspaceseparate durable mount - VmExecutorRunnerSpec + GuestSpecCPU · RAM · cwd · env - self-exec pVisor runnerLinux confinement or macOS HVF · watchdog · cancellation - - LIBKRUN VMM - libkrunvCPU · RAM · init - virtio-fsguest root + workspace - - MINIMAL LINUX GUEST - embedded initmount · env · exec - Agentguest process tree - exit · cancel · watchdog → Run controller - diff --git a/docs/src/assets/diagrams/persisting/pchronicle-product.svg b/docs/src/assets/diagrams/persisting/pchronicle-product.svg index 1dfd76521..8f263c3ff 100644 --- a/docs/src/assets/diagrams/persisting/pchronicle-product.svg +++ b/docs/src/assets/diagrams/persisting/pchronicle-product.svg @@ -38,7 +38,7 @@ Canonical event Sources - Gateway · pVisor lifecycle · native writers + Gateway · event producers · native writers diff --git a/docs/src/assets/diagrams/persisting/system-products.svg b/docs/src/assets/diagrams/persisting/system-products.svg deleted file mode 100644 index 271928944..000000000 --- a/docs/src/assets/diagrams/persisting/system-products.svg +++ /dev/null @@ -1,105 +0,0 @@ - - Persisting workflows and optional integration - pVisor runs one Agent with a reviewable execution boundary, while pChronicle queries trajectory Datasets. Execution and history are independent starting points that can be connected by configured capture. - - - - - - - - - - - - - - - - - - - - - - - - Persisting product paths and optional integration - pPilot plans many Runs; pVisor governs each Attempt; pChronicle stores history - - Inputs - - - Agent command / task - - - - Pinned external Sources - - - - - - - - EXECUTION PATH - pVisor: controlled execution - - - pVisor: one governed Run per Agent - pPilot adds planning, leases, retry, and reconciliation - - - Host · OCI container · libkrun VM - provider boundary and Evidence recorded with the Run - - - - - HISTORY PATH - pChronicle: trajectory Datasets - - - local/S3 files · Storyline Sources · @alias - - - Snapshot · normalize · query · analyze · exchange - - - - - OPTIONAL CAPTURE - Gateway trajectory events - pVisor lifecycle records - recorded execution context - - - configured capture - - - - - Shared principle - keep reusable state durable; keep completed work inspectable - - diff --git a/docs/src/assets/diagrams/pvisor/agentvisor-architecture.svg b/docs/src/assets/diagrams/pvisor/agentvisor-architecture.svg deleted file mode 100644 index adb179d61..000000000 --- a/docs/src/assets/diagrams/pvisor/agentvisor-architecture.svg +++ /dev/null @@ -1,135 +0,0 @@ - - pVisor AgentVisor architecture - An Agent sends one Run contract into pVisor. pVisor governs lifecycle, capabilities, Effects, checkpoints, lineage, Evidence, and runtime drivers. pPilot orchestrates many Runs, while host, container, VM, and future fleet providers execute Attempts. Configured pChronicle capture receives Gateway trajectory events, pVisor lifecycle records, and their event-carried Evidence; the full Run Bundle and its Artifact, lineage, Effect, and broader Evidence inventory remain local. - - - - - - - - - - - - - - - - - - - - - - - pVisor is an AgentVisor - One governed Agent Run across local and clustered execution providers - - - - - - Autonomous Agent - CLI · coding Agent · evaluation worker - - - RunSpec + capability intent - - - - pVisor - AgentVisor control and containment layer - - - - Lifecycle - Run · Attempt · cancel · terminal - Stable identity above process - - - - Capabilities - read · write · network · tools - Evidence per requested dimension - - - - Effects - observe · stage · apply · drop - Control what becomes real - - - - Checkpoint - quiesce · snapshot · fork · lineage - Logical Agent + workspace state - - - - Evidence - Run Bundle · events · metrics - What ran, changed, and enforced - - - - Drivers - AgentCtl · Gateway · Overlay* - One policy context per Attempt - - - - - pPilot - plan · lease · schedule - retry · reconcile - Many Runs - - - - - - pChronicle - Configured event handoff - Dataset · history - Event-carried facts only - - - Gateway trajectory events - pVisor lifecycle records + carried Evidence - - - Attempt placement - - - - Safe host - Linux · macOS - - - Container - Docker · Podman - - - libkrun VM - KVM · HVF - - - Fleet provider - Product gate - - - AgentVisor defines Agent semantics; execution providers define process, container, VM, and node mechanics. - diff --git a/docs/src/assets/logos/pvisor-icon.png b/docs/src/assets/logos/pvisor-icon.png deleted file mode 100644 index 44e0a74323a6a6722f0e845860735d8f16994ce9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 135697 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
    gU_QwHowcwBLV--w4tY) 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�{eF$>btq6rR5*t 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 diff --git a/docs/src/assets/logos/pvisor-with-text.png b/docs/src/assets/logos/pvisor-with-text.png deleted file mode 100644 index 28b3ae58eb3da926228b9d1df0a9c57abea419d5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 224004 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 z aC*uo{_!Thx3=(M1}hmg-(fNk$_*8$^e7a=7%t5kvVn`)*lg2{%1>l|(UrN>{( z^v=F}?37iOc*|CZf4o_64OfO}){b8x$ja#A0HcbHh~(QpmJfzsGf3Y~QZIZ@bfNBJ zq1^Ftiz_}ONhoJ?y8ihPpoI?mood?7rU(}*TX)euD&^9m<|Yp+9>dP7{VrP&ycA36 z3Ech+AI^hqu2tBgMGs+Wi@VgL>EqDA;g23(P++aB!CWl=r^s*y_9ZQ7O? zEGD&};`w=3NA#R8G3V-qAV$QDdDu6F{6^wgQkv#YI`-hKXjh1G$kl)xQ8U4_Y^SaF za;(i#NZ_SAOLodyNu*Up#MjAz*ypntG{dIeKHT2P;B-ru9^t!}*9jKTTO=TKm?mc< z@Ro?An|VL|)sF$;fhk`3iPMQbLK*+x!zXv)uu*~Chm1Rpdf?7MlD9wy$N{S&ssrmx zFRZR$nR6$#^Y)(8Mx*NKsOQ!2{Rj=J`2p}bDmCu_M?$&#XQ`x`vO(+PvKOhJ_y?7+0DbprkjOUe zRSwVSn)j4ar0A2DGY57xJ4TXs>uHXTy^|aQD2KeVEjnl4fC77X{L6rP`sv|~t48ua zKYnQh95d;If(9a0U%q5kAxpjc;7>z_o_)z)bZ%e6*^Cn?Use1?5s;?D<6=GQcfZnK zyvBP3Awj~o2@TH3q{!dM4t}<82It?TPu<{1SJ!t&dK!N-=3?}nkAGa&7sj|J&KJ5b zH1q`L@Bpc<)*WYccHJ&>hS`aJF@NIu`)}@;d;UQ9CH*e4zt^Gr{%#Y}tPy;i@geeM z>$>KCG#N{8+LCWSs^+He`uJOoqVFlI0V=2%br=;MEoA}KWVlh_c|Ok{lKH^o$f~{F z6*oSbCd`7Jg>NaHK=qJF7YU>lAfLjViWay3N_);{A!sR7Dh|sgoG!PL(8O8ga{d&z zXw*e^k8@{^t~%~SNWA3*SQc6)aqKfD)#tCh9}bcnaZfmeaq8OsH1k3I&w-R=f>`v#rx zi_pPEtll`|aHd%s!}nJ6*Xn1tWa*1K(-}+o&K|6|Jjfg8@?t*laY1NHJJM9sw{v^s zd&t4JED!YXGP8V|?O}8i&P>5bkM3<8a8&4H%ftJfqov&wkr?ZVkR~cPsk6q5?q0eHnBXJ;w26efh@pgiwK33nUkUR#y?W?>`u* z+DD1Fu=|D!ztba5i#Ov0Th(E4)L{37qVTzmQuFt3v?u&All{R-jCT*0{v7XI;0xPyXJdYzx5T@SY^a}Dp4;jdtY2VLrvXD@Hhlc-TCCa z4aP*1mRO=U^apg~Krl4L?2O!v3nhdM2H8E@UU0L5QhR8y0~>0r`m-K!){0v}r{}|> zNj3r!0wqbwScZtSKL#se>^J|ZXk76;|&%P3Lj8zh~w4dfUdn0yin zQvnTG(h;JW!->Ic*PMFEKNx;ShL%!ztMxPQ=af6`9&o1b?3W7Gnsz)VTI=4~Q$)eF z03N2gQ9;Mb)M>uGaVF2XXy|hz+c& zRS-{o5tVLBzC`PrEmZYgAk%?0?%ZK(&`Q9=%bm2io<7Wm`ndjr18 zPEN3E#s9fm(U!C8XmjN`A=S=>!ZrN&e_4g(f}p7U&f4!ny|>qkU3YjF?`xKCpDFI; z%>O2EPs|`=!t2ctIs{-UHCth}Nl^EZ`ZX(BXG#dedX-3PD6>|#??xg#n3BTxt9Y)N z9hH^oef#SZ7e{ccFK+tvQBmb1*eFExqhmFpUmZx#p8Cz!8I3TXi)(y7<|;gEk?E9L zn{E*D$D545@*g{+p_AxS;)}3GLB;OjuVbMWHC+_2a%RmTOGQ#)*pdeIbyNICE!ns2 ziv4WdiyNw^QCJ!tl&pg~qQrktp)5Q{uuCPr!CXQnwNA>gUk!LoBmFX{KeE zG$E`wG$#Qj0Y7|C%qmeFzlu`r%rHX3+x&Oef3uhUfS1s7gw4MvUz?}l3J`g zytf8)CZc5e6_M;<7GlaCeMTvGBU}@w!iokCH2< zzeXD)S`08y9?9h!`Cgy4Z9zMMpB+UE^+qFyQ7epGL#?<>zS=x|B}cGc-W%Yeo^{e5 zM@d95vp+mw)`9O%-w5enB_6{>QUDb;cL`vA7VAqe5lr|j3Sv*N!&%d3I1bx=!@Wmg z9{wQ-<2~b_AHINg##~*)6i!U>q&|(Dk_|I>#_BmJq5nEejEt&k9N)!f?4y=#&0DU^ z;=fW}=RJfijxGDo(v@#$3H#H~421Gv(4#U02)~_};z5qqWPX7?IEL3gqb_ODbeGH+ zjd-T~Lr*fB087Ul?n(kYDxb7nee)zURBd7^e)aa*Tsmkb5ZN@rS78T&eSx6(eaJ#W z+@~-6oZ#k!i0yTiAev>f_ff5I9e=S5lPsV3b6BhQU3o*Q{mQkeq5r>{d+5_jsC(`M zPHct`Bl*++o;nOHK*#Oq?swE#&%5jc@IK+AuC;Y$WEgof`~iJZ(6|PmwM#6T%-e}t z)gbc?dsK8rylR3|k5qL|405mhggq|)^gWN9@lwGE68VeHt8jH+LUG$XCiccJJMJuH z!;^&P2En1ZhEdtPWc$CQ7g~y?xAoVS76t^?>X50mG#L7dVVTrxNgS59_py37k~RUq!4H_p$iu zS^I8+eBo*?1Q%j1&#%Iqi1-?}XH$_x7UmW(xQ=SJz*Yu2WZDpHjZCL~b#LI7$Zx#TwUH zin+{YYDU!Xt*ed{tDmUVqulj~dqt;XZ)8fB3Q@4@5G`H_T#8SGc>+?LW0GOPzwyY3TQ%~V=Iq%} zB({5QEzpl#`y{M3mx)Z>V@G$yflfewHXMO_`=ZN(n5`~?yF@?ksh))X6bih(7WySZ z4w6Z*-u$Dv2j5JQKq1klRvm^#l861i>|X9mk)a!g#A4ebo#e2mk)GgX8-@l1j~>m} zE`<89o|7j^tPKxzu+!^D43o86F(G8`umShQ)VCqAA~#3Nak_QOg~}b7UIG2mgCk2D zsfH5js`lEJf=X!C1Z{QKkS`sRA70z=^0SRtKYMV>3zG76HO%k7fm&yeq!?oiFE1pt zf}qq%8Ve*bQfLay_%iRV{ljTWP1~i`w@v$Q_hDH*^C+Z`yP)oj4{JNA#-S6{*zoT8 z;`Isax6ipLWS>Ek7AZbrn;(c8ABoBYWK{UDza!G0|7pvNOI#quL&uVZON@&K()u{k zXv6_W#Zikt;c{u7Z;kdET^Hk88T?T($HCo&0txaXyA4!QU%752r0k{03@c)g1yxp-ev5=A{u@gRjSAy6m9riaPT zV*3M4sc|HKeH;3->?)>G<6AH?)1iovE?Ro823@>JuM#WY<{yWLb4BqKE84RBL+I`W z^n8rNK{q`@0&|gC+jQm6;o<3NzqKPF8G6c}-u0~`GmO+#7aXkDA%bY5@tN`Z3@x&` zNQ~KTR6yQ*ypc#$%!6>7BIs2&tJ~9o1DfKQ1ZXnUMA#?_Kz~7K-D_hO-HUumj>4)D z(+}5r;Y{I!fhf4VrR61dwj4X{ z&vZIZXJq~F-d=>B83FvLf4ZIOmOnB92mjNPPVis>D+q*|>B)rtJBK@A8VZwgiA6LS z)V*Jm;J>nxLHL&3nV;0Viux>0i*%S^L<7vF*xLPc79Ia*?I|!Zc*$GzJ zyo$7Bb*A_IW=?sHJ}%DM^Fmu9)Az+Ix0K9O`qiww)Nlti1q+s{s>hxe`uD5 z%&u%7C{?j@?>kS=8{y{rp()z>a*npLHh*>Cp%DX$cId2o*Ov$0wpg$8tiUUA(NQ@z zj`e1kszq|?&|Wl{mZk{21`5o{8uJ7j2ERRYRU78i%ra)Y)EJu0!VdG``)5q)N`6#g z(kL$zi%h)|0`A|2K#&Q4tlDd+okW^@Dox=;{`hbC2{|;1AX=+yo)phE{9}~0rE?X9 zZUvJH0uMp|FVM)k?(ZR0ifmME7z*v86AsFyt};F&C-Sd1g`4E<8`~fNG)05~AbpjE zilcS#jX zhi_TM(RN>bf0Gg@wA6oYNW+rR-^|mgxV0GGE=hSZ%hO zY+w3VS#*X3|JzwuALC%PqWg@W)cbbJV;cX5Q7tqJZ`YLZki3-PuL&!9eC+8`<31Lu zObnNkwaNtC-{`d190hB+9@e}ZviO{;%%@$$_w@3DW?PoQalAAL zayhl5Wf&*~wth{%#D1jAPK%;6DQRqoklg^h{qHOV_6fY9mQSNh+Zull%&$8X6olcu zm=iZ!9hU{PWjyIt7$TIU4ZlXLDZa9AVIk)&vX|3rn0pQBr=kHg4S5Nnmy&aZ#Pp=EEu5@dU^1DqVVY4KI?=Vc`1!{W9dA@uYZY9TMTet$3XxA`Kk2 zy#ZnjPlvj-j0Ly2%B#;Ib_Sv$!o-EG1i~THFTF&9hs1#Xs*Wb;F25+YQ06wTViYZa z^)Hjh4N0@-PU(=9Udx8)?w3wn_DrAJSvPf@lxA~EqEp@onp_@No`qr_26>q!x2 zXm@d!RZik2Kd5aDTN#a=I?|zumY7P=;1&FTovmlbKy2wtJpB`v1C-jy0oM6b>2={UJsvzQ7(%hzrhOBSLO3G6}C zyG($$k4Adn@CbYIP@n*a30Vq#TWkKlK}+__En1CH^AGszH$5lb5&D^rrRr({PG_0g zz;9`({|YWvB*0?Y>QkE*k^lY?Y2!{wK7u!OeS>NFa+cp=XGocQjc%GtiuQZUZUnl; ze4T3~Z*=b2ZaeR>FTcu8@txuu`9Nb~!JzD0P$ORRr(5~^-u&UF?A-;?v=+MHj6tTp z>4@a>9y|$~6|~XbNgS9*l%ykAV8KD6cQPp%mhLO98DM`1n-Mc7ZhZaALe#VClzt4t zoKgOv&d*|x$Hz_>5^Vufcn^Nkm){8gEzP4Iu;lR%pnc2Gbtl3=`TPht3VXja0O}fX%+0bw*Y{smVTpi+lJvAYM#gI6}M{*=sVMk0D-j=&ITy>H}-d~ zS#hgMV1K(~3ojL>Kn5upL3-I5jEFJv%D1&B&t&V$Xa&&Ut1$rX%0>^&!tOoAZzVOGc z>U>b~P~SrsUFhRmxWHnRV8Q{MELGA~E+v+x;J~iJxHDFDcu(UV1y*ZwF8q>aWJh;I z0j-jV(NUYRUszCoN-0%y@GibS+^@Lw9|sTLWeb|#(<@d`(B*w5NWDX68tdMl{i4v& z?+6y;2XRS~x{Q)w#U)0yf^yKL<1Vv@mT*2G`<+L+&ky%adpR>#frcWwKoYO%?gOkL zRA99CqOqjkuLZiUE zG3OTkV#(-#Au5?(T-Hx8$lo4;_8g-IJrxDxH>dE&~t(nPdZ zK^OTq1Z*&VdWL`qU|HgLd;W|fQdiG$mp*(Sb#tgE)HJAK9OY)MGq&{FuY9{r$K5aJ zf)o6w^|6L`!Nkf$)e!{uvMa@+Ysv1yC9jxK0ezl35_9tNKAbmp#bEOUN(w41$_^AI zTf6sFjw4^C^0Ru4Bl)*q(Dc^g>hR*ngAQLZ%xTYkq-gqg6MTuZ*euHxPIxeWVV-Al#q!Y!(2JvXJl#jJ>fABdiJv~q)}y`JNp_O?x8 zF?NqzBSxn`Hjj=*gLPwVan!HGlwwEdnp%vFJMu-xM&RwBltn29j^bc14;>%oAtG%> z57VNk8q;M<>Wx!l61~0`yLEb?ug4`MhZ0qNH~K^ z!p__Jyc^e_RP{4gx0lo-u#S7WPhBHYc|_1;0l<=q8hr0Jc}IiU!?v zd1@nneMT_rObxm4_+ab?iCYqOKX#|Bp42mnU7VhS*Q;q5p1}R z6$HJs11@7f`P+d_1p+E@KMRCD!~g+cKi@=&nx)dmtrgoLxmSHe0de}5SI!tOktVUy z1IE{}RU+%9vfo?XSg&Lmm;jacE=l1+UOCMO`4MXQzB5t< z5EJaE2(JAzrM%}?^E|^pyAfHs?jG~Oc9t|@}d0Nu8iLJl94#{4?j!J-$ z3H;-@qsN+P2C#{7i89P%78&8dV#n*!c-Us`hs&Rexr+2_2{#&Q*7%O6^l@=@F-(My z{ZXPvZ!9~{joxxb*RAhm3>}VsJ@!(6xRi)IuN9j+Y{5VP9XxQ4!aN`BqVn{^jP9OT z;pse?$$PtvEkD+&Ymx?;KSd|MJ2cV#Sz!(y&%Et|bq_{TG0UoSMjm^(K&tKXLB|+I zlXo-7cE!EJR76@wbkWUHXt0(WjU}AGgZx6j*bD;_bE~e&YIu{lwH2bB$>xYq_=^a? z%8A4H`vpbkZ1|2U5ZV0`;*wfB~_DJdc+{!+92QlIS8UQj_V6fO&k9?fhh zXBv^Id0N^4S)%aY1=sL?By9bXfB*8`VSLoN<#ddt|xolP1Y~> zIa5S@Y=&MzVDCZOzsdb65E@|fSl7V!ZQ4}n?X1N5&uw(RQND7M-gpIKo(qk>!{KR* zk(I+<&sm01pkov$aE+jRY};5(FjkPXFfK6o2zw%(g?;pth;))sffNMYp8AO}Vbw3D ztNeNba!BKB~{TXWZvNJ0w_9<(=B`&`M=l3-&1p|?j;jn`RmE?muws_Z)(^uBm z*E1=}QOq^Dkex9K9r6s7NF|O(G>~N31DZ}x_4}1d@%$4JwSm)12E{EZ{pgV9E$G+L z^Z;=}{O&wWv$zm16XUlRD=IG>eO5JFxc(N6`89hS=*1m*F}LB^J=qd?z;7dr3c0Hi z&m&od&#GS8kICJ2qOP)tl1Kt7(29{Hk`%>@?qr-)aoO;e!}*Pj{C||4EoGy zUUPJU^L*v@CF-PLMJ{r^ZuvB0FY@_B?G zuXT#1Xg|K)ERfA=kRBVvp?w;h{~CmS_l*mX(m0Po@NJNL{!pVHMs#PUOxj;HS)3hO zw{jg3ryN3Dbfv9)7*cenPM^o`0ufR|ws4uTbl*l<{ScDsQ_mnsvP2(K#6>lY3GkKT zSGbw3`N^BO-yAUo89te)d^I$!@2i^Gs|J#L7DRwlS}<=>S+2$+ z1Ypp66zwS*Fs$!c;9z_d6wahXYxdQ3h$<|C+3^8CVR?7T4j0e1>}$_)=Gb|h_Xi!; zkv~~riS@7ZIbn))e)ne=VUif>yICwSunm35AraS;rXi2pu*0)*=v6Y@ALR`XH}+u~ z{VL!wb;!JI3zOY5X}r9)Tmj3)mfX6d!ZthRhUaQprjdT{;HfFNHudj`+FyO*h>YId z7+@~yv!4^K!Myoh%nh{FsTJGrw9f5VwJ=iP?O$3FzfD>9CT2rL6)ft4f06Xv1n`?> z*3fStf))-0#}Nh1wRf6q^^;?Ixb!fj>(&4hdTRwS-<%G)!>v|Nq@A@XY}n#iEkb(R zu!g!oKUG(%hgI!bQhAFLe!a$a&4h_!p1)|vn50x&-6nU1&V&B+C_Qu5AhHpt4fvL6 zp)g^!saIVR2H+E}hkSuOot<@=IeAd3OgC`!>7z~?&k=FmPuMcLn#Qis%C>$4v_yj zU~_r((+8uVw74XJ>C8N7f)RA^tNX2AhsizBcbPzi^(=Y#P6E@s1_IL0G6AOIrcB$5 zu|pa`8Z$rGn>s%`Q3MWmewuaHUBkct|FTMmuiwq8^4mDLbucgDP7$|v&?yocOF%eY zx=jYPZhPg}?kKo8_CVL?+p5obd`<&Kj>cS#CLz|Dusw`+H3s_V@5H>a(EOo=X4_44 z#-{0E8<)WdVWjM}ib)LC<$40x{uCjjTT4(I>!#dr481V+Uc8?&m8F4Z)2M0F^olJEOu$kyWl ziPvF1NWkxU_I7+6L4Nj@&de#6#qY!9QU`@}K=kNmuW;7ZCf>dR1yhD6^HU1g141mG ztS{&)cYRkWHvU|*WTFtcFdjC}qd%V(f(|p70O^45^Xz4x7pDz^r>F!UL)Hp^vEDkLsfV1OACJF89x5jCk@LOnq+toGvR+u;K?uT`#~C4O z#qm-1!=gl7*GqrIPUleu{x3J%Wq2D>@e^sNXvY>~Vj-1#qaT--5md+bK z`TOn9darNN# z1e0B1isz(@4OFBaFEQoVrn(Fma4VR4qgdhS?Aokb~iM|-9n?=Dpm@vN$uTheQB^Lw^jCI%F#G2V$5P7Nx~DE`(} zN1vLlUZG%CC{Ja+5sudn8Zj;az=(kW8Ru%xecka{J}h?K?|Ay{^Xh^CELcW~s7BXF zJ|Kk$+>cY}>5u}SQY}`Ijtw+aWV(Y)&eV`_y5VH0fdWQgx%sMs(CrKa-;|vdOra zWm!17p**X-tv|;QI-V5k?t=j%H;3CxA#p57a37|BIA3Zy_pVJ>7Ao=2z!<~VKW_yK zY)37#c;4g$7BJ0mqEbf#15{I8AFVmjo{w8wvBd5VBJY+DU}t9g`nzFh(16y|#001o zM|zivw$66o5H4C-==v^NNCM*-OIrt4R%FTWU3m%Zhvq zg9g)@T{_M?jn7erpa7A|*s^S)jAzw}QDJ6%p2+6?YMK-_(*lwzYHKu~YMCKKR6#C#o> zyxNHHm3n(}^2RV5RNMYJ-f!BX-b0l5nFBq`O9&Gb#Fiu3&Q>OfnfV@lIARpLot}aV z_>Bi8IvXu5K@d0C;&pbszR#Q$Mz~N7WV1j0$j%YI+nBFxe~r@BtO33x#I15pMeO^%gdYk|)yFR_;y=;RfCMNyj8r>j}@5AAi3S`E|>mhNg z4~AN~bZv(xd<-=13KGP_t1EPjkEPBP35Ikt$R?DSRr8mf*n<`ilKgYI#A%2VcWhcp z(%DTZxQ^s%c}VMPGh6EUQC8*?8}ICz*V;Z#A7^=epG--!9<~++zGx)lytR9v7ykE< z{0=aB03#{un!la6qvGHWcD7yIe`7zqRyJ9PSIp@yN+Wy1994^FyCQg;B?+m55x87e zYCW!d0V%2|(tdYEgXZ8cjckWlZ5D$?=&p3Og6`({Ax1$ZsI5l^P}6TScW(5&+284W zpIz^9xQcsL2RiJ{>nfeDwS(c9jk{0v9dg8-MgF!aQ4xF5IPh!|0064bHKOIbe%n=1 zBXYK|GlAC~R^r`N^_HR`jUO6WZHih~xsmFfT&y2b!J>ci`5qHXp5jq4+^(A6RcVW& zynO}k@mcS;P7t28)57Pf@gz*@10%QC*PWhakU~h%l@%Kh=q_Jxbq)yV&en#IvYzga z!e#&0KzV|J1Z2$V8n~Y>UZts*kYFwvWGkgTm-5L^zAumxHW49@Fr{ZQpZ1W9$8hDt z-Ea(_U~eyV#ArmeGXGv^85|p!?_a}GNp}DM$~raV9Cn*mZ9>Y)C5w8DrZF;~M;U8= zRHfPbVts$VXr0~!f(#m;ly80BEoFrAl+kh`3wD{NDBFA?wgBo=Le7EJURN;|_p3Tq zgQka5<7ldVh7WwgIH6gW)%Dhlnh?9D5EkfoLN2^sOgC4NyC30a2E_|=G5p4@P=jK_ z{tI53?6_%E{&EH@Wm)A5aKf6@sQo()=_7j~xH z#@U!4jd2?xE%$7{4nxjr8M?-_y4)1!kr()LR%Zb_&&omm+qvF;wzv2?f<{|S`poqC z^&Mb{__a_098c@zgk2vk3DC#F-j4)Kq7Jp?51(g^Vy#JHGURae3r))qJfJ`aCkA+pIqjI=dV4af?iG+s6nKZL~2SpQdvZG)`BC0{mF_W}dFnNe5 zf`By9WICStO2MUF8J!>MbOpZOm(C2mLvey@0+Fa`29pOPcYV42yWr*MzI@ak#kWEV zTbL(3D%{Q$d$_FKP#TE{Sh%58r-60!I#eWkOC@)ox#Js+Cf*nm-3bH#S`&%^vC|x`zu8) zh>)#1ZYG*IVyl92f~G+uy>mw8tSd#$lI%AaQYn%ByhLOamX8S+!5)x_aeNk#4R*iS zLj!(g0KMHBzTK6Vt=PA}th*Px9rvqQw@!9x#j!6T=3ga^-hhbe33%Vu!&H(-~- zu#!{2Dlvs-hIp!iv_oXzJte{X5g^-v$%eV8S9}HQ|-!$hIp}3sF?E-&>DQrz!iKjim2w2)})$P zBdExjA(_xx;3=t920KYMN)cPgF!*UC39W~0c_747aIU68*S@{cpwVeAj|oY-yXicY z@qXha9~F5ugNA)NKEn>&nPhG~E`P7qZx4k-$zHvEVqN{%pD9%`)YX`ahxa~*udQ-u zV!{YOcG8C;w8*b_QYg}f1^zUR!V(Mrh?^!$N0Os}PL1N@Pj^iweGfYu3)yoPSNk)u zLk$<)4Yw;{Q;yLlQ8keseGx132aN+uA}kfC8Yl%OqJGp@2dE zCjZah-%B3)R4)?e?tH?O6q3+SQ$G` z+OK!CHopxt3-fVv-WU)~eoRcg-}!@E@cE(oF@^@hS&`S-zY5pyuG8Ycb^DC}>!TXb z>v^}Vc|1oLvD0rOzLA`Za+qz)Gr9(xVM0T+(i2q%-$-z?>VzfsJiA^uu_ebPEd&4< z#nitH7kl838WX>Bp-f+i*6NYRo`s+NjjE;>J#RtUVIl1RYj0iLu`Y*$I^P>g`=ZGq zHQ(r8z~k;GF*%-R;M6>01_Jv~0RKgy-pOxZ?*CmmGAGFUd6Dzv4;gv_`pp)x0ni>} zFKkyj2|f-vRK5qEAsHJv_VzcKwtf*Vs%lg!sZ@8>ILAmwOIahu*Oaf49o8&7U|V}T zo4cDH;Y#-%HLFgG?TPt^i=X`^@q%r}6E64~eFzOQ*s2NsGh#4MYa9Y=+%No`>%qGf z))_Z0V-5MdcK@&v0q<{@-)EB>W2*XBVvB2;k4Nax>d6f51o98+ig;BE!7=-1wM3vPd=HBtl^#P(%E_?_e)_9}%bJdQae3 z$h?R&?iUe>x_0WL7b0bRPqi^s{v`_4-(zYU7ntQ<40Z%@jvk`| z08I-3L%g%4AV!0SGvIAh7%Iu@NNJv*QyVY0`KRF@LWE3lB6987CfJ^(CM&3J=}9il_WY%x#R?5Z`cjy z$1kYm@-5Bk|1P!T=`jYgs~s619)yKY#!v??Y*`^E*jZQ}7{1oB3fJNTVw3Y=z_?Tk zFr;HNWC$R7bD3Yi5Hr%%f*)hIc9MaXaU6nY-1I!+OIcE})4}DeN|Tv7oF!k0MArPE zZ}ZAdS}I7P{{-!L(>2?;Ibd6=2Iv#l?SC9l`a4Wr#k~!Xiq)Zj=@*DlJ@;q6d=5q~OXj?G9mo6n{vXp}B+pqx zvBWuLFP-e@vu5-vUoe{U*qSc<2!=+r$}InW>%DN~H$N_n9v%PUN}afI#8gN`0yKHms;gU%tqbI33mC{`Ay1pw=;L=Cm&n|H;=y@Hb zYLVu@JeXt}XoF7<$-lAct$h7ztEyrKN_lj9=}_?pVHTF zCCZnuc5aXQH(Ja+qZ)v5V5F-ra3GL2G<0a-bm9v>e@!v2@;+ z8}hpxZa$C#{Thz%N(Z5p(H*eAl5`>*)Aievq4xcDp<$G@+J#jIR$p7i#A7!fa@(Qq zTv7CYJF~nfno9JPKu5F`C%SnJ&RRL)CtMcv3e!T-9@KpZX~BgS=^+mJiPuW$>V}9I zMYGLa=oaG-zZ`BI z;B2M&0R`My@tHoWX>;0pt|H~TUhHWm1-^6LmXv;iKmH-sw*>!J3Qr_C@7D)&&q3d- zp3bn!t~#kK+z-s?Y6yx3s?OvG3RMT~5=;+kzUpx3p2O`sQ@*QAG(y@Lnn>cBL<3Qp zw!O2>FGibTH%qdl1>=-XJP$$!r=!uchI&BSd9jL_?`Q2M%ZDvPOo6@)A*4{OVL~acg zp{=9PeMFZQpkN-5dDUy=Xun^_Skrp3d;)sSZSK0t>Ap$;yWAXktMgYFM3G6PS{=v$ zzwZ_{=70{HzpY;GK`kZ|%XSz3+tWi`4;iLPs@*{IoM8~%DWN58&OGdmjZchr+Ux{K7$Xx4z;k#kLhy)vTDbzQ#|Ubsly$P z`K?$qGkr|HS3s8+NVJh1ZFSrT=;$k3iZmYd1pib#e?J=t`=ZvE$NN>(lnr8P3_21PfS`3z+sjj;{ zYb5fm#$)VpYF;koW42HIy?F#NgnTYE6!>y6oJ4IL;_Gcb#;OkXqn38=_d5OA@3Q~OR8g3FhAe@+ZnnB9TI>h80?#SwXs`jA z-WGzr)#Z?bF&%8bnO|hcXlNcC@j^O93W-wfUGh(OM&?K(9tZFh>_kbCND&N9{CYW1 z(jgnXB%n?U*&1pdR7L1HU7;PGrF1hba71yeP5d>JX&2ef#^lrVW^b?1mxmHJ64*ni zQ~Kl*@E)f~u-5nQ%&K$dJxX%hXJ+kBF#`&E4#oS^x%%E#t)-tL4yh(nK_gk6_KwP0Zz);^zGwd3 zck^lQv;OaIsD3QY@?h`9)>3U@AXNJx?}pDoD0oz7-F^2@4=KOvwd7o0CWWN#l}t36 z1;M3UvSN4wuBtK~YoJN;hG%W^=N;+4pSXC9ar*H`k_rG(6tmvYod@iCk*JHt9ISdc ztJ1XRXb8UOp_0GnTV;BNv>|7%GM~JCJ7jsvaK#No878=>zE)=GW9&!T`_{lt&lmOt z62}bH@V^|EDcY}fc}ZbQLHse3igfC+?z&u@$$GiFmH!n;PNBh+Bbly(>pew1CX_Kr zB`TW7M;R{Sk2dH(aDNHi6C7j|{+(_ARh>V7s)Y~{anNkh7A25-*N$i(B^;mp`v)@3;Skqa%%Slt-i+lIWiQ&AR4%Xh&a4f<71<1b5+HEgV zk@vlKOCqlG<+e<%WZ{B0ps5LPxZi^w58}p_h5ixVNcY5-5U1leOCEpKrBED!NKUgu zGC;K^J0{oOLiG~YcIzmcj3Cur1Cq*mrLD$+NuWW|Zmkk<^G2QHtjpJG9&0LlDtQVdAmy68EeHy(jc@r9iR|i=&3goz` zm_vn*eq@q5W=+oB?IT37~s>v$6lWyV6loqT_qQEx;PEBxY6;t z2L>TGK1qR@DW-56zWe=U<=`)2^drZLQevGr{O{9|D8HMs`*VQ@7NWX&J!raJme|yg zJPwsjiAe#Ei<~e{!i<51{cq%is;U+}2awkAAhH9Kj<7Q+y~4$@O|{1uxKy_xuE&^I<3}h zxIcn}#_8=5X9+L&96(i3Qqm}T<8MDIH#0SB@c|X=LM6R&lq+f6 z+bTaB9@+UEet|_WHn%@JFs^{ zpO~ct$5MFc00a>PHh23gr{e zb!@D|sbq@l1fkph8goHCue}auncQkx2Ht1+%xR#fGT=u|_eaG10Qq)5nB4jM)Pcff zp_qq>b90`luYh~zH}m7Nm?mK=8oEsCNop_Q$mdV zKY!~PB1{EMWOJ`)2kr6-Ye*wjqBP7EIKL8Af31HmXKc3rs#Vzr4hhu%m7K~DfQ+s3 zIpif9>Ar!`c!me!h>kj;Ap3AO#wtSws%@FNbA&}tahJ9bS!iqdNJhX^$HJ)Gt8@Cbwcg@n-fn2VQvajg|Gm`z(om;LBN*Ik4L)0Ozk9gY z06y4#Bcjum?=bxdY|B#ZrK*DAb`I4;3K2v4Lx*39O_>tj- zH;`Kx;xuEEua_ACW0!@~*PJF!qc$>yGB~B&5Wb+ViS8+^Y>@09#`G0Nf>bh}fHIAp zY6tr|busfjJqGV>Bi%<3M}#~e#J6BgQ2KcqlfXB*m;IM|LSP37=gd%d-ZiSBepizyeNrvKnm2#HtbCkp-z2g#m0(&PA7CPO+pxlmrv zyaKQPr%Uk5sQgUvQ1_>v`us{|fO}~wYnEPAM~edD+2cZ5)}wkv-6sjplt-vJCK|W4};#yDv?oVv;o=7OQ9ZpIkh0zfRg>XgJj=(Sc zPV8Mm6KdUqf`UuN!rl$X`>kw2#B-n12C%74(F)$B1OLzHsE2o&qLtf+&8TIdKi5b; zR{GGs-=LO67oc-3W#0flB{mX}e_fn_9~A=CHt}N>vnT4tAZC#UvcL^lXNIdV#YR&p ztL>wP$9Dae(eV8`FWb5FCp@@{{M7Dtc1*h1?*6CC-J`hlQoqsGN=Bw z3>lxb`9(js|L-p;73rQE9TFAsVY&tX{$gtZj%y`^Ue0zTocEjJRy57gZp0+}5Xb7r z2!~}B+H12w5qXaN)UPx8lKDzBiSH`m-jdG9nTn+S2yJ*ql90c^3;OI=e|_@`K>!mK zM8OiLlNeZgh0F7_Xd{@=<0L}XNV3R=CPd2HE`aZUE=oK17|9{|J<(4kH8K$2aA^F*wf`JD}?}c6%)VV zUG(YjLMy4pygeP7K(adu6+Xxg}q zBL%yI1IsKzdoPtvT~^CA}1F+%x9dcl&0dQK5WSNz`|lSuSog1 zg_mbGZE@MlBt>4!zpm#G@?gJvB{~FyHO7y4nY7|7^Nf)`Y||m-)`z(6<|b`2^SGh zIQR7#REIg0YmQ3>f&|$Tyg^Z+qQsEuRwUKb&OlpUFA={-VFIJsZrIHX0d zeBrcJhs9Z2hK7yi2au7s>e)0}vw((HHTeKyrTO9*_@EpIFfjy{%(Fz&?G>}pkhrxwV56} zZ2s0GB)}_39o}kEC_eNkx#OujFy)EwnWmW8SGZ`A->qX_X2WUYh7_0m$-gFjET!El zD7JYvKBw)u&BlJI(MC(7EfAa&asNSZrkHyO@S}%1r;Yy9FhI>O3-K_@^L;;6@>TFi z7^p#y>buKVXMv8MQoWJKw#f*f(l0(71r@rQuhC~3vMZ<0aR#TOe=jSyaO5*Ibe^~i zE3wpk+s}Io^M7l|qEbT#Uk<1y5U75q>+HqvI`lJ%*W+6psn@CQzNO{oJ9Sd#c-b9p z(_I@`k%22&nO|QzcqH~)cgdS-wGm!SoI?6B;Z@KI?KpBf{)#}S@`k^P5-dv&%p`uJ zwL6&c@=2`i#Ji@Fg6Wi#%}VSwAFftnNdDx&gF`u$Zjj#6-=O?K-7b=7MF+V{&iXF6 zU<=v1K0bSFIuxtuu*DGRRQnGN>1qGTe?5*AxFqO$%{1Fd(7GRaFG^6UTf)RB{}Rno z5zx`}3Wt82l^1@hD>9dR#=RWsna>W3b8wI~)?{Dx!g=>G=Rc>Irzc&7CFL1=2`&NK z{jV}vMV^ZvfcJ-epv=4+LM=64GQf_ZJNr1A*)(S3k918%C zzo09>aRPw>eQ>A~LO0`_I&$V6+HOWqrm_mQKuJL5(CI-Qgi& zPZI1=I$uJTmOT7Io7BS6jT7N>N-!Y&{OG>@JpOx@2E9bH9aU7h z9(oW-QU7kG7~GjmY_b zPUEE3Tb&YrJsL(gqQ6B{8g}3k&ius`my8wqqYhm-9j`fF`L)|(B2#Y2Q%dDOncC>( z{4G1$vokx6_#!cZ!i|=X;ABJy^uj1+|2J;bKh7KbTGQO^!7h3t$weV zI{j#*MG=12@9PQhQu9TGx5^qwoiu;sjju)2FYQqUy^th@fJfguCHJ^{r>6I=R@AaZ>B z6T#chq47Kko$N8?+aju<*GE^;2pg`CiFTcgAIK*^dWk&RFl0iVBQgixraT3Pf8h!= zYS|1w(-=0H!%kev80m!m5B-(veYo11)WSz}pUJL*pa2+>^}S|+MEcl)W`1sU zhc=DY&(7fn8^XjGwZ6QaziX{OH>_}~g9UCqJ_m5SZ06p}LP^N) zkvZxHvFZFSJpP+aPv?LTm-=Wc8_84ScYrPFF5WgluOYudGa6wTo8GtWx*RN^MU_IyAVS0j3=y|y$5y^2X=9ouK|+y4A%}f;r!jR z+RTTYS6^FPU#{>mGWe+p2MOr9PN~UWZw75_yZd}>_^kz@0w1SPNqt^pq7b9{%Bz|4 zt48W-18wSEqK$xLH}NU%I5h9yk!~+d4G8u|v>+bY=26JiZLPlQ<9y3RFeUi`8Sj5_ zBIZ+scz_+kR9qoClc{7Y*M;tUjX0KR5Fob!UCcO!MLw{^A2Qa?ir`B`5kUA29^Pwb z+QE}o0gNW(`giVI!*FmWw~g2ix#uD47JgOs(uH@K`Jy9wnPv?{Rjrt4YZJl_S&Xis z`tf9ebkI7B{(dO0n?pdbKilmfiSRZQ;7Lh-&CUt|>mUgk<$f2epiYBk8*s-7{Es{S z{p0t?Az;^~-Nr*M$nP;9&i`b%XUMSe7aOqhn<-jP0|i!R7zRgGkMo|W%T5S(N0c)>q0EEY}Nj>+on9B z)F~!`lX3l$j?l=AtR6UgxZk%IR>FJ>I|Z@(V5G7niFZihQoqu@hFm7}P_$65F~jb~ z6@+bLeV5Nnz9nEqlK1?CW;^rSitRK87V%&H|4&*4Y#Lpf>*-}z#WR_Qif(!EBa>KW z&lIH`=@28fT=&U%2}JFMjCfesM+RlbV5dr9?~kJAlM^UTjL*)@Fp78%q}r_$F6-!m z880P2aPvVvEA1PPi$SRG^9ez58(n#Ori^k|g^pCVOD)Wq@Dq|k3BsmE2H+@>VWTXB1oH1o{~zo1#Jt{CyS zxYBcqZK}Jws*j%^EH-s>|0uMAwO6(ppFPG>k@u@OR-cm<|HlD!pv*;yO_Iuaz_E(4cs)%N8yfsi9Fs_dOfwD>lO$qw{Gd;W}!idP$wDekrFe{_6S*6~4Od-jA`^(DkBneEDb4geE0B_u<)RdOX6U{gYk7_3aImaMJhC`) zW^GwT*&L&m?|xlH^<$p5|HoNO=pI-==m7`(`fkuhR|v1)RQtyBCHR@Glq>W+8{@Xh zq{&S{VZ#wdI+{m^&3V`-OwWWV!;NafF$poufewryLD-PpF%*lL_K zwr#Vq?c|=m_XnI0&zzY(d)8hc?c3PU(j*soX9qS#^8ev~02{8C#)MH!s2z0TEO*3w z`FH59Zi@gzBBL-R5SjN6g){K3#)yIVI#cz{Y6Lw^{1pVemh=~AxGxhQ4tG-=jDPlM zaljVZy%YG+gZAsLPfy?9chUqr?vGWt*39`T&MH9t$CwBuaw?htNGcB|^6=$)loKzh zkk-j!)G*%``Tb23veTyvk59~z`*CZo-9U$;U++3HyCf&}S>@PVe zW(gTB9btfsr}A$k>u_I?7=i!`-!;Y=u6qZ2?{>r^;(TtKU zn6HRhtbqgYBU^Akr1(BBA3JporftVHdNINTZQur}jC?7*?LyJ%B2@E5_SfAv(i<55 zx{01Y&&Y#3OSQi+6W~lQ0$3nth!q`jn?-VpL)&52ou9emMwU{AzpEzR4@3SsI!g_H zmq)!I(G-8wWpRSk%B7mRJm00D6vFflAqbhR~pbOR*^ay2+Z&V~M{- z#w;2Sf_bxYkR+0^F2lEF%$!*wdL90frmf1&(goNd>o-1xXAt}HWA~L=WwvoGR$4pn z9A^FCb0GXErXJG>1M*$m+efK+u2zx=4xq13o}%n|lraeDm(7 zShpug7&%(i5uuVzPGYMWkQHdzqtwd~4lF!oyI|Et4I&915n6!^)fJOQgt{CR6a~`6 zFAmZS0t$QgzPvBY<|U>}yMY$^ku2_;t{Og9okaTYQ||8r?Oo41Sbh%?{b9{3B{++& zb|a|G>wGUaz1|- zQORCLIl2w!UkFS--u2m&Db>+9cD3OoO+hL96#c4@;nfX?kNiXLm^qptUeh)~9uBcU zWUC?OCxZCBkwZ&~LRZm{f~rAIPh16;0-L(JSYi@1VJg-jsK58He_*%koOqN|fL^ojuL@0u&URAr#Fnkrr0dg-2KfZdUZdcbcBizG)WCoU9xYm(JvbYqM(Hf)S_8E{P; z054LZSl|iW7RX{O5yXlIm>@qGCJ22D2yRrckM(OMpoA&RYtv2B`Gf%Q^dXV{HP`L` zIhF!of4pJZOo<0`GdjKukJWMmJGiRn<*f(~CpV{(@`ya>1&0K2g;Z8T|EIUbf;53| zdPCseFz~jALT>1R5Q3Ld8KD^dPDhBv%*_LPpb zp%3J)LFGo_`3~jAhd^iVi?s7g0JcyqE586_#u5=8WnoG~2L-U!9F6j5*s&{~?t9^F z#Esi?o$m+su}EjgM|kj&%+35~V0Z_Il4-QxVww`bsf$@?9rtng*5(klN60|ZRl4zO zr7m3r$WB4|4@`%>H!AnER2#+KbAB&onvW~&O@vS%-4^bPVQ;JGSI;w!95e2$`^C`G zT-Ry@g!~Z>3*R0uf9b>)iULV}&`iQ%_OCfl9s2>(8&5V8NJL$T$dGDg@l`205)oX9 zG6b|9#~Yr$tHwr2yDqBZ64-vk28G80nZzCg_-o{KgTW3h)AN;Db$j)Si7MTHSz%gA z)rJ6w^8Fr-uXNU@N=$s$ihOxBzS7gfzT+xrD|egQwCkD;Q!DEID6388N>2nLOeP`2||oP3nmc`i8~p6 z&$oxFy@Y)tMSDO;B|%y58~>8CYDp5ZAg%94+MULT?1mZ;DOk5K6mErQG=Qc9##&1N zN&ULi$V2|3dsjaT?*CS~nR_=X(h&HL_Djox)%z@vte9uaVqw9ICI$<`;?TaNd~hHS z6*@L}eekF#JbZ8RdZ3^(?j1vQ;Yk2GPc7=Xe~5Mh8D+a%oi7sIUp+_P-vUZ=y-uYC z8!TyqmN?Txw?i-Z{QnZ8xj9CbJGbrtgVvPBDCCq~*Qi9M^M6z?COA`8M!jv;}0bqkwRXKFx*Eup2g3 zq{dn&ii2j=q!KO%79%r8ocLLh`gyie2dzYN;aZA=cW$8rdP}`JO^#A4I)Sb%nbtt? zZ?W^q*YY~_%JXU46J%*tGhFV z8iXNMT<^k!S?vj)FIXmC7uMkr-~l@i63l$V3-yDW`qN~;uoyO+R?r4840x!JV{W(^ zWY*WU-K-`awng3dK;5aAl`cru3;dAYjf+3G_7yhbAsJby$$VKHIYk4~{Kb+mXvUnO zDEvD0EPrgY4i2-U3@A3W(nA+=kTo+pl;Fa}JcOmBqKg%0Onwe~DOOphuOs3msAy~E zXYAr4;8s|xOf%--eXj*>m;8UvmgQjU;n?=ctCy#XBQE=OjmG6v`646j7g7E1$HoGVg6DD|1&LkEn{i7g) zEN!o^Uu#i=Rgaqtj9hMR0wR$CK+W6C1VPwI6H`FU?^m_-c3Y|mPDt9^_>quj?dUCW zL9YEPf^2{x2*_xoRERbNyuT0g{8>PmRY=wLe$taHD;n~$(JQ|}h6m(fHDbhpsrcXf zr`iGG=(_@){s-gG`{x$YErp!HbZ>iLD?|Pt$^iiasHh;d+++8&qH{I=(xF5m8~={s zjgiO4o-$}9>##u~v_c`nW2!}B+KZ~xw3~>~P+3|UOQE4L=JG8ay*TP0(Yw`to4;_m z^Wc}e-^(3R$5lpY?v^Pnn;;km3QmiHW%jlcOsFWf_%&)Ok+5{Ptmaab+{=jiigIz# z0lw%DJ~>pJ;(5dX>9?Q8v1OGkQV?d;$tLI+$I`ud(s@2%VEQ?h;=592&$-mNb9i5w zPfOSD42RS~ITZ(Il8vV*1EHk(dAm+*7d|pha?`#WM^lhQwPfvCf${;NoW zhh|)C=~~j-SEi432H6S!H`W(pu?UWzHmB(mkUbxTr1y{V;$LEgJlnWx-~Y~Us#7w+ z+%^#9AE8Ft#qdXCUQLrc*uo34E}Np)rrlJL(L7XnK032J11IzJfl$Zw<$fcPppY84 zI2UV8QI9}EgV({?_9q$?(fjQs+HlUQ@?vMcBk6(+4yHiH2vakd1ONYyZtg_bYv`L9VM;?>PSVoy(+Y{B7NbQxx^@_{){pX&36qi`bFd&>$!g zW)m22GAJPMPs=cAx?NmhEEj(8_n_!6+=(?B^!*{fRwURnXk>5?-hn=$2r4Nf^5ozu z4|(-bhFDOwr?2jP_fwm-tv~Q)EHTYxf2TwbG?zH;w;*50MwgIG1*4v+7 zWV#!cglX5=8!o;NxVX_dLUc$q=)${g3dhb~uqmei>r;b~3C`2kiFlu9dvkb0mb1;PGR%D!A&~A+vwjDrh6SI^z|+9^bCGWXW{R0q*Gr9A6W1bU~wbI zV=~ExgENTLTRc2RsPD3advVa!XF)gN#v6)CoZ2K}tYHZyYS~?s8E6Uq)b2AOc#RKJ zXyM`z==WSfnYS|mvLht_V@Ck0nxIox=w0?;id)60NEu>luZW6ak1&3O;-~fWBSO_0eQ38E^=sdHeoI{xiY#!cosUq|bbYhgdjISXL;T!K zqKf^^+UW&=q&r&*1_dCm9k0^MaJ=Y{V6r{ae{3ZQ_r!BUCbDYMi)riT6Q=uQMiE_N zB$HsDSG}Wl)%g0UH2+d-7mEQAoDUsR=dYdQiyoSa&veC*7pB$dDjW&}0fb=-0NYmJ8jq z1>B#`-fuI@@8vk3YRmifZ$!oEqMe(r%&~?W>go;$nh;vNXu)gRT$pdDP^cM!%;V=@ z0W{2Pr{ev7D=Q#n%lGy5xlYymR@7>D zn{$Is2n67x)DRvF31{x}tQ3D3?epg)$HqABq=Ga#N)x|hIdzcWshMn=dN9cFP<4#l z;wDit?=wgXC`{4G;k1$~_6)ys;x*uZd5*ZwGDTW0bCivX0TcH5D7nTRH)*zDwc^zh2@rv+iRGysL_KZ_C{|_`oMRwsl#Wk zoh8(>j6Lj+l&U0^rZ)=HUY#OB!iKbX-F!~!5lB&kf>1yzaV=di{g--9*ZnZP+&j=P z>q4#B{Rx*0HxqTv#7ycLzuU--5RX$`ND`tKPHUyqI;8%=nVhE9)BpZsi< zZ#cQljItnw*_Ka8D!7SNe5dRCG^|Ci0RdXpEVBUrNI3j|ckgbVAz3KSjc}_jO>JzY zs(D37HF#u%RJz{Gr!ev1C5fhmQk3z8oM=4G_n(`nkZ|PMta9@^Yp#_Ssf$_XyT3cS z(GN(?B#;chJmyO-=ze#d9B#%5ypDJ){NmHs`0$Dj^)lULN3E+PK|x(rmUpzo#~xUd zL*8z84A^VksWsv)TkjL0Df^E}n+Ba-D#=lS1pNLVlg)PPCBJ=dV!9oGwe{bDxoNw6 z8VXm#5TzDGb|K$XyL`i5Iu$S{DVRtIW8Ak6n~#jcxD=4>b2eF_kx5fu3k_amd<*_L zQaSysEuStTWQYMP9{x6WT`;SViNjn1$;$(U$=P#HAb@l~cNG*M#=K9mFa+R$PKDPL ztjh!yL)cp(cx-@CqmD4XA~xN(;0}0V>K4BAh_#FJIe)_;*}}S)d1uLpCLSB`q6db< z(k*+mN#EI7du;A~pc;JZwKMd&sR!P{rvE1>8)c#2+W2~`sN#C0ynT!RBJHb<6jGab zT2sos-$7PQ`DQa{+hH3ku+5D&f{iafoqpKSE}@hRJS{9ev0md%8t+6=!*DXqWY_g8QZ zyS7FRv6W%ri-#vc2`qNLyX$)AH!v!t85``I!ZqA}uBddAx9f1)Wa9rk?icVpy&uKs zJUKxDERf=Joui#n=nxhYHEJC-&W(17-Rzj%B{6LSL?YTL%UB})PqVRP^{X%5{$@J(g zq;jDNz#DLK=y%_4pRxj`Krkx*N5BXSmhhKrN^1_?4=`jwht)z1--sKVPW^+o8kygB z&*2^*P3D&PvO|Efm0D+*zkJpofmVvh$k}gX0VJ?V{UhFUwLL7^>%q`@e_h`U6MmGl zl`5!xl%KHVRae$()Vju zYu`t_?79p!!CyzSvpX&;o5YUPeu#lHk_oNe8f6Zz&{@mKUV@7O_Pyp?nNKU9h zge!jT-gVV#5Osh+GEn~#0zG7ii9QlXUO#j7v^$Z(c0|bM>+oL1Bdx^9>2?WBnTgZC zE8(gRQ|mPF*&Er-<#9e#{msR(35X{-L-2xiMBu|ug9fNDQ3$2`S`VXjH?xJB4T<^u zZT2-nQ(}}-)KsDVOtKN`jYA4I@|L?lBN@C`_qMDgKw0|&P`Bc;5V)q>QhZl$t6)_! z?@c*+gmm=|7FNq=fu8JxDO2A7oEf};FmrW5(8n0H)iFcxmz7x$(vo7A*7hxWkl7_# z@!<;!zbWC{4t7nAd`<)Wg#i6e2o&+1DY|5iKZfeEHt+AdM3LOl6Mhm{wo$U{ElZb6 z%Dv~_08=wFw?y1B3;sW?c0g9zhaa$%t_`R8VUO&hs;467jC%i+SRivJw&Z3qj4trLv*k(iM5dN#~G&a`)bB_ z>0q;Up=g-RKDCZ?2qKz2s(IkEUy#`HFemhcqowa~`Gd2^)joLF5};xdjC}3Jua?62|j;etPF%2kH52q&H!Ph{rVf>_KW&lqOCIA zRbnC3Ar0W7T}&@%No8B?AFSSxV-GO(d_}p!eE)*q|3mC|Ej2t-2r|egu`u1R4mXrS z`{YZ0#=|}K5wm}IQ5>34XJU}9lk$?n?K%}kmz-)ONBraML%lu4ZQaJ^t~p)T4GUcV z-B;M~dAzqOd;4JVw_6TQR4_;5FqcwHRW(I-x)n4wp7uW1&FEc?gx&;`gI;y$e*Iuu zO=S|5ErXsmCz<200}zpq=AyPJ2Qa(5IA!?XnrT47qp~}f8hYKG8GdkmU~;bOgY4h% z-2(u5P^q-Fmj2l-i?5rEF#hk&X_s&|0av#)Qs#&zXi}j!JkeH3BL!A>IZ$aeyi`l# zx?~$}#`7^Dcu8~AHC1fzJZIteP);WCHFW;RS@p>a0kBAJs)5d}+<4=Te8)m@euEdH z{LJXcQ18^wr9YCIxFQ*oc>#0!{Q$#SBw&={q1SXLim+XiscX6zOIGqmn;J2+)ifW7 z1wd!M%yV}>JOJNl-LPpz8e zE*vee0RTABU{gE1M*4o6!pJ8z*)!syQ_+L87DzemVZLf3R*?NrW2qtigp*ALVS3jX z)fgZLYWAP`;2*<02+W5}Z0KJ6u}|YDzN8nv*65T#L>b!2N~!e2Kf0-fO_HS1Fds_{ z(_NBco&ohfZ#AC*U{Ik~9?RY>zL%d0emlqcWCHB&msQo?{BQt_bu{#(w6AxAvVnGL z)^4|Rs#UE8PZ^j)l3xz1E+e!Efz|$#{J8_b$6QYZ5+r@};Z*Pa5?5b^{rhlj?F|n$ zV|EB`sec|QDca-5-|wPzhypO)Q#R3PT^4hV4*kXhQD;P4aQ>zsz)T@kZgtOI@Y(qd z*AJiy^-n*A>j^L*gc8`6PyHVfVxF}`uCpeNiA2T>yOD^i1=ID%q_j!ygBisH_3cEF z3SthumP$Fd=<3HMGkRCj9ZMbxB>6_t_C|NgAm#}Y0PtUT2eR0?Yu%rliJl|ua$nA| zd@n`|C44P?e=IF^0*Nf$l<0N%Bt#_T$IOdjeGfELB?IZgEWHS$i}8#D>acOO<}BoY z@{zBJ$b5cPC5^85w>HCS^Fo$}it^g>nT%6>$WtD77LqS@fu^@Xf<^h?SvWkT*!Oa5 zKl^Ixx^t`yIy1-k0EDqsAY!|{Oi9p2 zqW<@WfoGj!mt+kZ(0%$TrE#xPuu?kt)oWp1Mi+{QJ}VPx{5EGR&O6_RGZK?Gz>#P^ z4GyR+Gx`&mSnIP~vlYU8(I& zSA$v$O7)mwlm-{ z0DgA6@6K?upW7Ff@7wWgp}m2wrsZiHD5*6T39?bA2%D6ckdQ_s;mpev*{yH-MhE+Df+ z$eO<(Fg+hSWALckbbi4Y_50@JJj16kk<)HQmdtEfOujQZ(vul;H^UqAXP^10ryO(@ z1Us!b1_Z$UVr_A~-Ti7uuHEH13}3kr;B#*fS|&xRT%Wv zPZa*trSB|QrANVBNcTEf8t^ zeFK3Wakl<9&Bx62)ad8IQ;@A@ZEp##lM?PL^hQczdgk>Gvk|`$c&bj*Uq6+-*z)AX zp-mBjZ45C_^?y&49PbhBsL&bkpfFy}F>m&T;yjwAWAFCdbx(5~wC1e?_9x5~s{?HN2WjF*~A0 zpt!+rBO+vx&ad%j;tekzpL!~m-$|oOm;$UF5ldu=%2)(V`!KnEujsdbl5>`e_Y4qp z>r<|W{GI5QOGu!;rL*pLmH}hA`7SSN0+p1CaTMfL)o&`cs%EZQGhji&W39ecst&}9 z(f*rm*1C~_dV`3_6X$w>nG_%@@Ej5JUm7b%aR$eP$i2j~-3K_>$|=^`{vtV`uV*KT zSij{naKf$d?Z_0wjSu<>g=#SD z>AvK05a?cE2u#^<4@k_9iFE}n2x^pkRv6^3-$iO7!Emfnx#x92z7OiEY|U%t;2UL_ zFA?Jz8oJ}Koh_ma`+%^I3ERguJ|;oZlv((IYJE&~PzF0I&=%%SSi~7^bK%jmMaGr-C$5BCG{WEAiSd(m;gc-^%M5fkHjvF4F-PFgB5x&H`&|#_ ztwmZdd(z?W2RRkn-Zqr68QGf%*>8N2pAXYOmd*#kz|+E zr4f|m`Lp99IyY~x5ZC_Sy^J8h&rEU;5fu6%)?)yWTr;e)wQzl z6M+Gn`RR2>Gea(sNm%7T9p}cRBheND_%VtrMGaX z_J=H?@j%vwM_k1v{S+@aYQ?j^^{g^JIIAs$-Amg5H$L!#y&1wjC4Z!vq+MkAoME~- zZWF3n88y6Vo!btpf?9#vx`;7dps1jR_5a7a>obfdAx2B1mSYB|eu+UPi~qvEz|iFp zq_27#bN3~t$HviHlCJ+1L_LaSg!d3bi-p$uuYKrz+T!(iYxli$&HZElbp@12ULO?h z8UnvNv}*8q&V~N<-T)-{Nx!3nu_bODL+qG3ajW}yEJ+}lzi#(dIoV7AiQCMc1gvQpJ`@sFW_iBsBlJ8? zuxRWx?Vq9z3_-?reX&CEUVZCwUdYqOe}OqD|7NekifR}*J&=9$EO#;FyEe!jP!UwD zD8;Dyu`Qb8C8!Z+Lt1C#HCP{kWV@PDO|)NiA5#Eq0*mCR$cuKj2gLJD-uKw6?zgN< zV2ZZi(akX81vXOXo$GWX>4Mz02N+Rj4*)f1a%Y#EvEb(%k)Sk}xZ@=g`fgzkb(^q9 zHxb!S?o#9=CrwbAU#nr*;Bf}1f_s5o(bz*Y6@Lo_o}c%%jRKh8ru_VwpmxN;LIt~ls z28(qEqF{>gSjwr*a=XLcGbF$0S||__Fgww5vP_Lh`Exk7zDB5sRy5AxC-8v!Td^42 z@W|VZd24=?VZrec6QYX_K59m8Rm~yMuUr9Gl9H|S8o5KeQAD^O)0U zMpm~CANT)sv|Jq@dcWW10-(NDfs%6X8xOwB-`;9+OQiK$6t{a-_nFCk*1Rp&`L%6m zFMkF^Nh14LzbtkI9U(^aEl9ZLPX%ive`zHD#xoO;BqC}OMx`ZY|8X@9E4vtUbIp$3 zfWz5+j+O7X&TEtdFB=m&^V`cm*#G#bYp+Nv3z0UX^{z$8h0U!}LCMC2CSpA7s3k%; zy@{#tCk9giNxPYTRhT2zsxt_1=Ja_$-W+O=rXI^aWi#AxXD-Z_^81CoJQs_?gx;3g z;2(Vo4tT@fZV2!26$r9Z62mT8)_jAY{xs8x3M0p`eZV}}n%nat#5Bf?Me4#$LO=np zGB0RNOnTJ4{^_2o)Wz~clJWx+xomTG03<5r7{IDU(0zdZvuaO3i~?Kk#RzGGbB#HzZqbpu4@En(=dzI-H!Z|p z^grM*yQw^XTH=M&8b!8nW|>hCpIrtHO5Xv^i2v)%@xwj)AZg!+s$si6!=^)-Xx_(2 z(IZKIPNc&UY8;xvq^i8z2QFhoNDkv1KupKBUtL~o`%NGJ68MB;;;;ScLL%UQA7Bu5 z>s=@@z5xHkbB1e zOSKV3Z?=4S5KnWKT)N-2KlQGUPWpD(P^<|{5u>7GW)&zq_xs|1g7>=~{@k+jd%^O% zna<(99k`HWXXbbu;k?x2ASp+dVQ1*G?Ef3*kx|5s0QucfqkxXxL!CrAv~`;-5npcw zT!VV>V|<8UHv?`i`AsPUU`!MyyJ=;w#QGx{luwUEGk^tSB}8RLr-@*x(+i1xxf>D_ z3p0P2*l;mYQ5f`^G?_Yh57a2@TPks+q%Xx~Cxqz-_PtFRT8RlG3NNJ><)1M2an^A$ zGA5#Ro)%-S2`%sg#)_EH?t*Sbd&-%NA&{ zucO`{_#}LKeB!{P1l@!J6!lQN-DOL-IbKO3)@9hLE7vTn*tuLmNcm0zlZBOaQagCo9Q&o?EMqv{cr z7)68~yo=z~zhVX?UPEKf;f_LYR^cbXDSoLHTtt~Q$dIr9k*vMOXFZr8B{sEUNiGAnLD5*$_IW#khea)rHbGkp=WC$OaT69IWrp304kJo9fW} z8#t$2re5-aY&5DbmUr~@{_~W2yZU2s-!JfqbooBs{oL@iJxdk)Q$%eMPA$fVREI?? z=vMB`mp7c@_rw7n5hycKG=rT;AJ=8k9(Gb=q=M>;Y_m{Cu=T5*Y@d~HRvdBiU-)z^ zZgL3O_zK|(954*fnRl3U{T8sL?#pGvpObXJ2n3K<1YwBd`u%!>g1kmRs&_Vtcywzl zZFLkp=$s8Dh7f@|LG9(D0J)k9%B1prL_T^@Bjg#v&SUDh`ssT+t8cRy!# zzmLa#P9^k*8K^yZgH_4qhQpJBqGaZQv#kD9zO1B%z|Ddq(h3${=_@izf(esAtKd?hidfpA%gE71t>c z(|pb|COXTswDy*rRP8lB#EGrL38eDJBRFL>)338)UY4={NB~I#aqi_zuc4MME~tpk zhsBds5>#@n9BO@t(dfcW>%MVL_nD@vZSIP++Rif)o<{JpUNk(( z<1flAOmM~~ddVt)HH-dRz_NU&;?bSi(wd3IBtqS4w6X82345X~D`suaLi0^06Mao8vhdls>157v*%Cn<|7bhN=f>Y75U#)wfa*eIQ1hbm0v-I$b^ zL<2EKHdRhsv`|&nfEN+3DH4@m^Mm%`A5n|8YK=<|qY7A6 zsB+QoN%9W`pDeHN^T3sZ<_9+V*j^)_G+E6W#=iWKsC9Y3t99Q#s{8CnGx2f7kl_=O ziOHw+S-0(AuSc%+-$d220!jN;Wsn`c(^rY$G@5Mzz&%pKEzto*9xSISOsw0eTOFv> zMnALDa#i!rXLieTtb!%b^5MI}##q+nDupz{uLIw)5tnuGCIot9Qru?_?^PqA~O`u7Z=H2{!2C$? zeJg?aUA0BJCt1oos}f7g(A7UwwYODV`;p7Oh-cHjFq}*3bxEbf@F#L*5yYt?dU76)|r+puIQy8Ygm0o%}1_rn4%pEAxYty;4PBr&ef zxEJfmWD||96`NyjD$@6ado*cSA(#Dn62_S1WLh+cL9NG8^kk*HKSPP~TsvK~1Zp{# zbMWB-&yxb5vkE!fj@ww5ylz+SOmb<qe{>Aqh=Z}qRSBpiGs^qa(HvHeSw{s0l6LnjGJ7-iiQAES*)Lc$=W$x@#~L zjm^tJBEz!VcOIDJe)rn${Ca=(kw`0*k--Bfg!yEkpU5`M#UvFN*X=d*uK|;*c)%r* zs(n0MvgbU=1AmWXTDdKFa4HT!wy`tl0*JGfd&^8j&Nzmm7|fQgFk(lQxx2d4VJ1ys zPnTE>_`^20?+A4thx`JYI;V$+{;1J&$p}s#g8?q~+nMe~zbm^Hxi?T9Lf zOfxg_u+-4*1^+x+EvbF@rw{M4Y**5%RpIZ z32E4edtyqN+WN!>#rK)zJ2j%OP$gJQaT}NxOqoH-Y+~RQd2Wpy2@u0m%EIS z&=K5FlqHc!E3C`Ux>O#kFW0^e3WVr!eZpmToxf-bxMRvpk~elF__a;Lln8Z()rJ^; zFP;RYNM%1$_0?}LkiL0oEjnH8Xh2h>h`_`TlW>lJq_(JOFztuHGU%Tbp?t4#AY{;( z?%of*1Gyf8UKYEB8f__+1{Pb$@@UHxS}gs7$2n6$*4`=jB5jOB@TxZD1GlB%k{Pg? z8SHUnC6>sn)w!<5bY%UTlF@++bU28gXg+JqW?42!zNlg7&G6b-+52_mamlWd|AKH?2yx$NlW^z9Y68gq;R zJj)S;fQNd7fQQ2;Xn3qH3(4KQ5$(sz!SXOZF6yGRN}ttwzu|}jGagbL4<i3ER1j)QWKrwmr#Nk+So2@N<7f$N4Bh?m<6O?WK(55PV~itZ&xF!!#BIfUf1Lf zD`)NlZ678tqUh9f66$r#l`2)sZ{gF%{@Vp+N!#wl6&0O%lm~*1h9V!QQ3+YPQoGL3 z=lMZ!3CeiGYba+;>pPnD!pK&fYKvvnJBti0WAie&00RHVCi|_gakaEV>O%YJ$_8l(3hlgUd{Zr_O;R%XMQrga)9 zG{ya-qB>Iw)eiO>b&mmFw{R5wPLieKs9@=^Xy|gUK#(krclV$jTxfM{&3vpFqfvLO zzgH8?YTysfWivwB43vHZB})38>`CgfZTQ>ev;E9p3K%B<^%JV%jo>~>b>J~U{cnNl z>-rO6MRldhIV`xCdtg^(7gQ zgpTByI-avY)IpPd*Hjs)r3wEt1KHbsG86S;=@c1#S~Bh8u@H=2 z&!T(vvl3ty6h`eFtfo%GVq9A+eXmr0jJ|U=RtXq#@dBcgR&7q2i9U7@A2T9h_UqR7 z`jonIWi#GlkQoE_&B<|%PQz)7O99_)JE}OM{a?IpY=t_gfw6<#fNZx(gud*KqX=*b zoJVOunIqfG4}{&ezp`VHd!Edhu}Rn!l8;K1NYGNW7wb^>I9C5Rd zkcli-RL2$yAPtxrUAG#w`=JIy883)4tFH8Nss7(vR0f}t>J}W0i7y8G&rgW=O{%ip zSLD16E}k1M6+zy_Qkm-~O7tihMhyq{_o15IxEH2h7W1--_21PYFa97dQ z=VC>M`*+E9NvSeGc|lt5=G_{20Tnj_+K7Ih=>W&hczkD z=^NA*Iwja%)F$)PJ|iB=b|iDt5-9wZN`#1iLZmivVkC9oa~;~dN@|!nqqdzQU1j+t8DeKg19nglO>BuDAIe?hB`wWVRXL!tMpK7EykCUk zY22bD*Hv8NWWV)#31UyaxDrly18Zi^iSnc}a0$}lKeXbC0<+Z#)u&Em9=$OabNhaG zOyM0lk2ne?tS>}f+0(l-e^&nZqsC}%1>(}OaU1bV2Cp5M8g;YZ?R>j3`@`%gwnRc8 zVglPSNx2$4#QPp*rgh>usn0*U?yY>sl-ZW3QK8HpOiO--)eLvy#9h!UqwJLdYsm4+ zcFWcYR%QCPE z+>eP|mWG|`%%Ln5R(^dTWD-U^9rzPr)Wn(fDvvdxTkZsfq z0dcD*YsX-5FgOYq{`+4W8yHSA{+`UB$SQB9O^U$6QOx>pJA~c)C{UX&%T|EkJhA5T zFyU4-acRK{Cz^1MHUsv))&%@7 zEs}9cE={lvC)_hezSkMQrU9SUpRN?CHt6-UD!JaN3PRS8`lU*Xu()w*CwpYImo~Oi zwUA&RrTWu=eO|kCV)vkl;v&8RuZi~;u#WPJGO!hAyq13@HT-{#rwm-re8|g0qhbw} zAi2laBm19#o8E{6sCAL0XT2hT7!7EU-}8Vs5x?_mPA0G8J0}sZr<3nd_2$9@m~h4* zJ9Q4^o>g1gKC;No>dJlKjf*C4WtH%D_tB*fAWoxM)WB+n4c%bRvC)ORy8W*^v*&}t z-bAGYPljQeWoR$2BypgW{f~RPEO;7+ z>)tY(D0J;od&NmHEB3oqMS9oo3s|NFG%6^(Hh_L<34rR69bvNbL>KOWn zz}*ZurMmm&D&NU#lx1tU#b}^ZzuB;X=z!&mz?nHQpLQQ?-OnC?!q`n5Vr51lD6FSA zsW-N+(4XAwzwvwK6B)w0b0@lQ*j>m7zt1+@Y=35=w5i<&p}~N|TySO!&htX6f2>>? z-Tr=awBwNIU><=$zt6Ib=p#=@Sh&Ine-P_uYEAw@WM=rnvKEa{D>Kw!gg=(6Yv!vK z<>J9;wnw&H%1o|_>9o{2x!PPpN5iWdh`3m$IyXmMaNp`SSDWZhyW_7sdg&ZxH=3S% z(CKcxS>^@+&KhAV-+`5bbHIe1KMfeHbFh(|J^P6VaU zgRS0q>D-I_K{hqT$+US4j1>ok`0<3h7OC`V%v zOOh9r!3!!z=!=3L{XfAuK^ubka?B*>2a9AbI@r7AWb99Uh;FbVBZ95}+#)3*#ty@w zMogT-Ea7mMMNy_r76QXRrE`JrUqbkO|DhTrm2!;v?W6v zT^oaR08y2L_z?+aQX-fOrQ(=f30|bQ_9wSI-y? zuZe2LwSN`{e~rtGL}NUtqKby^lQen=2o8WXE)gqrOmgQF2$6_cYZhk*58&F5=U+u)nh^+iw9AVmgankvC^#}Ev zJ*#VP4=1;q`5BQd#ha;&4E}VCD?AUIde5~hZmn1XSy;=^mCSMoA>8nZ6%xlSV`!w* zJItKt!UyJ%;8Q=FTMAmx&W_7ckAA}rFhZbfVQHeji>$*aJlLd$B%?`%k=a>8D#3dT z;G73wF0H>Kjdniln60TXX#u;p({yd%4TOW%a(Y&SX-#zzUh;<J_MbvMdCY&AkJX0gdxLl``FsMCJ705*~ck~Ae)^ryBWdT;Z!avvp+Ns(DT z6Z{rOi-pHT6KN$|8KWJe?co|8h$Tn4l0w1jWPF~Qu9%-A^V?IiJtb~2t3K*E|7uRb zr0B1=VQ={6WXLpq8kj1jBDkObfn%yG{@+VBa0a@H1%q7bdhU~n4*J+z$i!)-kkn|V zq>D>q)r&}Ygp>K?T&cu5E>WxM`Q||0@OFCNi`_rYE53Q{2ljAzUCk1Cp8lbnsyg3l zTX64^p0VJEa-_{@g6}X}+a>0RPS4D)1k*Eh7BFA-7*BMok$qa7$OHI)JH=f@z>1?G zW!CsDV0Vh$;0Fz1{;5I7EQrpwktgs3QLNqtmD2V1ZpGsh*`V7CYuuOC;`h*oTsfIw z!*u2F9E&Yk;2#(!B(j-{*^?3kNbJnY1~#%k77O#G;=_tlYgNn99*NBF9cjA4EloSL zq-j4P8iqHq`kK^Au5THd%hvsT9^f9lV=vq_t@mF`BbY!78iyL!z5@rqdX4|udyY|) zTMq?U5f$`R0*oLfGmhyvL26m@S-Ae<#z+oBZ26fZ@d4MthYw;?liP;V=99CCH zsazG56Eu&MF|byD!`aYjE`UpoNGhHEJT~iB>&W{CsYd&uYwNk3f6$Bj2gsp$PQTLj zubJGvVJ=+wE(C$v?mv)%NL*fWit+Hy-_LHAu2!5r{Cf6UkKvW>^=u`0b97^cu|n3M2`&O3Bo_&{kVK zW(t3sJxg=JlnMfER*=aSiq&7te(Ii0UK*6PYOF&`>`RneSZa2(s~3*$6f75bx7^i) z$+EyVGq#=29!f3+9*#z?_+A0dM^oABb=J#HoH?Y!yTl4k>83I#%zs;};%_6;2t_54 zA5p3n(Ig(J^9lzC^m0xEeeL4-7LaT_N7NLISL@|;2PuwG;ZX^;AQp1_pbn3+2dB)h z2C{wqBZ15R!t5TX>1Hto*&tYyyoJXwimeiTPa;RJVI}bK`ZPaHmd%v*#LBb4(xr=e zIE?Mgf&*-@Yt3jryyaMv%WVsB4owyuU@D)jWY1)z$5$NL%K@w!KBH|a+2tG)eJic; z5a2U`;41!$KlW#omhNbfWkBugA81RJiU39b>vrV&MRom9^yt~5t&;!zfs@czn2EK#v=-5!0c$l*nSX%#^e8GTL*Kx!{{QE7`sLl1YcZ#yW_cfNi8(Es6ffBax= zZGGf+zE-#4dIv(OLzNUiCwr2Pz*^fnikE6s%{J{Ut((7EM#oqy+SD}ktR~utCd(fh$7j;S_kPX(%Hm^9p=yOdy7#a=>_)(_dH;QUt|Zi zxfkgD6Ba?*GcD5C8Ei=KiER4q#FE%JrBPOsY3+Hb1e*-<(ysGPlH0)3bk~9i>VCWs zcnDEO&VG$BmbiM0i+Xrypyg3ZHf!Mi6G!%oFDZ7G7jA8bwEuJ)Onkh-?a&X$jmf+C z099#v&O%Qj<67!#@f$2|%3*FpBHB-72;xYtd;}{zlqDvCCoVU?<#mVEj4ufDnpUK{ z2g*8GWfV?>#+}$uRa-L^h61_^!&ZV(P?MsXG+2h;#hwh4T&9sI;B8#q?TPV*RaC(~ z88@`l!yc^16+9?1i8>jkmmKe={;Bk-2$Ted5yENYD1dgPU8Yad%lHQSh~*L*p62?` zZ$LrID|vY{HWlx#867`5o$hTliKfAJr5X<7qQKNyBZ>WrAQl>E)zCe z_)GKDs_NZa>`v&Eiw;6_Hv#0$Q(QXs@jgP%JmG{Nsv+(7(K!B3g%~Q#JF#6O4<3yw z^X|5)<50mt$|1&}=o+*?)4zchFN`~-ihJYf$NFl4&tZ9|=-U^mSHu1=`HE}~T)mQ# zV>}nDv73&UF2`@EA9vnjMR!kG)DH$G8x?UN({~Yj;sTK6#rG@2=g|5+N8DnfyB#0d zmTLNEtGg(^J_MQLq141vDPKCtzGV<{W&6H6X8U@k0Qqa(`01PHs?Szd7M+4xd$n=H zw5?1gQjE46hvum1%mL_Mn1n>WSR4Br1cN%Dtwl+9L^Nec_8vd)Bf+NPh@plihd(N_DU)qN$0836%WftP zj}z;@vy&OMrVFJ@Z)u!N`1aVxVoB#LaknTR!x>oA1-O#9t2(92w)_NTQcyO#xM_Ra#WbfC-NYza;8)Gs+*8wy9M@tnOV*kTSw zfVJ6aOzJ;ODdYYe7I`F?3Z-53GS`IcDQMqF#0#QATiV7hym_#iszjFW+lH{FYI$bQ zpz2!BgKtasB@jySce~mQ)t~BJ9doHCxDBeOlaqT1i>N?4M~|*^YM{S)p?;36e7=K) z{4u;=WNaf;su2r7N`~Tsjh?KL3kWZ~nm9SlGZv}MFV+P+tT^V>0y>1gP=I}kx0)`_i3L8)Rj+9GzSz&Dl1Qg2u{jO88DVD3n*{@cDj9zHuGMD}k|1F4B zgG{M>56@B3ST5OyRc&*;x#Oo(1ZYtOCfpA`fw%O6r>!UZtM0Q5M&sdCx3y}Q58iku zj9w)yJ8>0<-%3e|*;Phxit^Jq-X5{oDPZK>Phe1t^*^FaK_04bvL%upjpMY-K*a}o ziglsd+3(Nzs=bCNYu^xa`VvxqsZSgJ!84FI$d(M!p#ecx+QK}^ZE!e&y?IGY!VX=n zq5c9~$xXHP%tpgoTMx5~l1vel@zFAv3F|{s7X)r?SAdq$hL##wP$f=op=$#l`Ffi3 zhC5pVs5d7@S*j&H($0`%JF2{;pKPp1s(?j`G3sTI)$IRI$*51t&EGB z9V#ZeymkyMTdasqjiU3{qi+JMT1sK3UB%6jrB%7g%iAAb5P{ExocB6!q|F!g(5sh2 znUeW~hKokczulDi;%$TVi=lgyg&6lB=?GtEYh)#r*gMbcTS03$ zw=T)}Me#?D#|K~N<+FR}W!qVij128jI_$-McS0v|aLK?M`r*V zLwiMkkpb7hksM9)l3&0WIH#+yD1+;$qEJjeI;my6=pk&H6qD&m49N@&TBNztGZ*nL zg*$LWo8m#rdq3zhq}TDiA3b^hBpc_`&9I>WO?+d)ihowxPdx!v51N$b|9yFUb{itm zn4RHx8NT&m^N&C7MR@!_20R>QBl@K0ESE}jJvn+Q!r`!`x`I)N_8J9~XtvWw!+i!i z_}{1Yo;L1kkhgu0B7n=;o8Weny3JA8O|pXrEvQ`%xui$(VMR)1P^a$=Jcy(1p!#u6 zsMtfYAnLn3CYa4CHb@wvVeLyfR-u<8!*ocI>%~Ix8l8MD5#c3siy@~KejmxSo`woU zC8_(PDYzi6y+Bbc3_?q4H9V7!wMQ)T0PQ^rgNh(|am z7_o1SVTbmU!werjAF+Sx&@x-3kb2n?`PQ*486RgkK>plx24>#>L6t&FOH1h?p1<;g zxqG%~3=*3IR#9kA;~jrDY!)9l--~H?=SMX9rBD-9UNRpQ4t_%;Y?V&xi6KLIZASjw z|NWHpOmx-u7L9$e(dAA!NShxM0X7lG@63Zv2!j}>#I8Zw6g;n1Bkd-JTe791@zMcq z*$zQE@whr&DcbFc1*64OeVW2o4(?$`LEp*~TCLx|So4kmqdKJlA`dViK1OdyNhIV12dZm7KvW!EY2Mi0LLkRhIy^Gu5 zN+B3=Ed9D%Up*TXb3I?>kEUl?v5qg7md_Ola9On;&7xS2|p zB4h2Mq?460*)ciU17$#uk#J;2?R0hY+|qf_3@C(L*+yLl*jd7@jxl(u&tk3=l)md9 z%Ft6pr;NBitiY>1L>hoGjsbE-HQ=8Y&e~V-(~dizf^V2Wi|_4aWcx%;xIq$Z6Hgn@ zd7>->`+2Qv@PV&(h5(rX^8YbKKqQbo$c&WNe{AeeBaNtORQM!!mYWbdW2Hu1DUFtz zY9cIvBgQgI$ViRribvHcJlfLswD5hj7>GE(sF`Z~nz(Oo@jn(Rkhlbw!)z;Tn{h>Kze-s5fkVzXtL^&p7;TQ^#qj!2R zkXaGUj8WO^LNIwWGiJnYVi98oC0*msaFQ3}{QhzlIZQ9!A;O8b#AB1(bSVUHa%^JF z#p1m9=Pd^1aQ+?qzKa+t*N&exE@%rkdXHv_ipY@T!(O?snfq*6z9FHi` zGSB)?3gCVG5AtgkdG(XuC;$y(2Sv#%a$RuhSx(OqYL85+-eE_XOZHKg95c$X=Gkvi zSA>Ha&{7QD92gdX`%~_q@}d?Da&6nPx%9pwclEU*Qu6J&-}h;&q0@Ug+mAos=XaWg z?_(*cRaR31)&-Le0dUwns1AliEHUoFg*wpMyx*z0-G+-%Se;Qt6Py1OzlC_&bkCU7 z8cCaIt~c&Hj0lZyN6^}}A|?<$J7((U82`utWozAgq(Lsh>A{LRAeqI9;MG2rUKK{$ zqn}XI`zVDE(`E}kWrz392CgpN&%u7;Y`|1n8PKHTl2OoQ`%ZShhjElz0@2 zR4fF(1DK@w@9us7M>sh39dNKsekG-u9q3z2p&K-j{83c`h~(Y2l6Uejp zPyR|-WWg*>z;G@aamLQWKL>^gHRj|jCh%(~JDZm~0a=J7Vpr;6q8$21y-GIjqFv%@ zwj(uVd~-N%bTsU&c8lcL zrQ57xl1U99AOS-SYU>{77FN8^4cZly>1LB9AxnVB43q$JO^p zqA?Z*c~0?vNlE#-g8sTp`PTaNoDxmQw{f-UjoWVLo6m%%ofwa;MCUonjP9kvPSGK; z6Uf>#SX$qmS-A3c!~mvo60%N|ttvNEexL`bubPABFrQDHyX^s%khT5W{NfGl6}tx# z|Exme)cfbQ!4~K7EL;F)f!gfHqI&gXyt!XxqF^@c?8RNY({LwNA41ZRk{+K)Oww;2 zOgjc&UD5cM`piq7z9_L1+YDQ`Z^U%QJ0+-xjzXgAV!ce9sEKE~WM$^?MW0Cj&Eo&W zr4iCjU7^NIxoE)yrbpfTB4)@#Y>0(Q_mI=8FkuP#V}Pd_>Mlj)@K0A`?}%NAL#R|# zan%tT)kqYOt)8!hj<2Vlwx-7O~{NSVj9Ser4_eo+Nm*n)uyNr#z@Q71>O)r9cwFjw;pzm--a;_qd7 zvr(F&jSqEl=Ik$=1t zkg;K#EI`mH+ug8+WFU-yQFo_geWDDjBS{9qU?dX8T z#2>-MqqD$N9}6NZ;JC`EK=1*yhTrelXY#t8-)E0AW=xr#(W8o8xfn)-bd36O4jts! z=4N_K@QMWW|H5>z$PrW(vrA=;&a&!pfj#yF$$0#O1z?{h#s8gjPFZx!wib~7?XE$= z=p|fzSCjJ-bSe8`Ktor1hy_M4f*$Kg|T58|G3Pu&w!P2FQpQSO=+- z0VpZJ1pE<3!LJjkoZBb@d2k;l7F7-$I20!?-dbewin~Y3;(aQLn%ic_BzQ za>}9m=;An~e@zNGC9l!wNXWKnYPN^4#Ndy#HnsuzE&JaKt`BU9|FlH)h;wX+p}k3h zhxw{T0uS7HZBy_WJ*4SPG*SdzfY(lA;ZAjv_J!T4VlCIsZN51ysw5P~GBHFck?1udXy=McQK<2A0+ULSDIyYJ$E_u^hsl(% z&=d>;Ttjp&nA2DZ+SF_FAVsJ9Lh7+3=O4Lv)Qwhwe^d*On4aBh(aZLpU__=+?9GZh zA>szNlBK<1Ae{kI=F*naJ(R^+GnDtSXTNZ>!XWx*wyqSKQbxIvGTi@AKycLXz6O|Q`qhdGE``?YlKc0h~V zsg@Vsf5P5Xia)bY)S}->jv$~1rpP$Uck$gL0dxkU_A}y6NqAAdhV0FdDa4n`#$};@ zV5iT@hw1^T2;c8Ta{T`OU3!o0Z@l_U6ig%4Z-hriSB4X@6^ry>R{9s;iGe zWWd*te=p2vZnJ5_ISu=6dk)5&ijk_cEDyoNDSNQP_RUV+Y%o$}5}rV_srajif*vpO z7qqP%msF3;+Jq)mvg%w79Kk=a{QpL}BrrPyb3>|9>Nq|*O$6E9Kn&xYtmfaPoW)r} z#mxPck!5gnTGi^kaN2t=FPK4UxCuaE#r2WC&(+_KZkPA&J^O79zPkm$R1)E)4;G2# zYV90HsqN2S*yq#pm8s{)Mkidnalfi$ zUz2AbrSNc{mv@q4ICQ*$*p#FF;@&b1~&bb!!gG z=rR4WC8S|PD7-C=byxEGBD*e#r9|#cmJ+5R7ZJ*|8-uL~bJ)}jhQC%aa_>r(hzJaCuFv!VdE<(DTHbv~4@(EmXSPxSOD@mn*B({>iu18{Rom}J6 z(Wsv&0vxcD!AD#uh(hnJCRzZc>TQK_mN(ScTc7?L*Z1;}wVPT`HFv6EonKFo>vCm9o$YL)qWN9P+Wz z)JL@RLroO%8O1=!gMxe2nOe$3v(zlzp{=9MAX>Xuw5jtpu+HWX>ZW5JC}(uZLzosX z{a7D#G!=ojZ_feH&=rvviwmZr)KQ>SKRE*OnBd}Ur4;12udC~OnfrZFabTaDb{KP# znV>RrjOyu_Fh(Qp61vdKuHyjP&f>1AH^V(){JUms)gnalXhR0(I0U zF!2IxT%I61>J*Xpj70yj$TWh$c?~Gz+jlFOdMH#SX3oSHX4Sgj8!DSajWzP=-z4`- zYKj-pjE+w-AsPTI(=0M<`Y(dnLp62H&TXaa$Ib>KVqe&asAANMYv?FCmq z@=6x6C8p|A=q6xy`LPjcb=sZkYGv@z3KV59m%b}A0V2 zT`J6Lk01pC$L(KPGq*e;Uz-r=Py<^frh||YK-_(%x$meMIvw+A%Q^s$iX;vRerYR$ ze=F^n96d)nqnkN69j;iYNn?=McZz(nIT$&8Ch0*)Q)#Qh?-^9>ji7E-Eo>N8zfIm_ zDPX6giN{y3C5Jrmu0B*)tg=zV=MXk1rJ8_^2c9?H-+6w>0XZ@0|GWnpL6Xi$$-0pU zC2AW8V|z2!#Ft_-$c>0uan1FfGTgKW!AUF{TbQap{Zd83#Se>xw$+OY;MicgUMN5R zR>W+1-^+v|6TQ~G44+TT-MUCDyNZftf|bD`)+B2L`sM3{#^`lXWUK3}{h%yuq=Ihl z_i+SpmT9BnDi5iXw}6!tYg14Qh&wP`Wsbn0Gm$du7dHInvF{07J;ihD+1YG8r2m48 zVZ0LS=?*ltni=OS+89h6HD_&3wq?j@229`vp+-SwsAt@-&Q!C|s12WvRb!OIWNs}? zG~mI4cgymbX?FtPPj9WzIvnH23p(t!>>1tfBljon+eOSgB%v+hU~oD&lG*63Tv7;vKI)y%KnGU{S!wGeHTK8x#39!@@3aSw zocaFr^sVjf1wB2}y5u6d`%F0?2_u{0oNyrr+68-YMRb_!Qt@OteFOtV!fUw*Jf=S( zuAR~Ldy=YLAs7_77{6{&&R+-s+YU)pY$#NnAL^h{(4z7vTVqx!s7FZD1N*t}O=zw8 zw*B7Np%dE~A%z_L1n$OnV!g?LI$NIHvE9K8MT;C$@Ug92RGn-5<`Y%6hq6^+rzgl^ zPAFQq5w84Jg0>=Vu*7j&VQUC&M04k)!|m&JCt{f#G}Pnm>f2lS9X9Y;tmmO7W5RAJ z&c%JxfBHyShBoAJ+bFiw-Z>i z;oGlUG20%ygR}nr&zJW6{+^zeguT++51>b7T$FKwF0Bz=K(t)O(q6|$_Pxv`m^mWH zHLx2RZ-L51GTc@shAT z=BXPySHIb@G55+sI$UL_cn@43>;m`?!CLtv!t--a9+_c z*SP`c4+9>0!b{p=)BQrI+4GRWVdP4YEeXIuDc@xCLN!)FsiL{&p4)Tf9PILD<<_86 zq`5>dezKbwySRRXqHi0*s*A^!uq=YTr(v~Pm6_CHfd~-hAjWuvR&FD975#{Phi7=fLseQ+=9yW%ys@aZn z2wt@F>)YOEJTaT@=l-30SD%mReBKP}$8UCN!Dxo{Q-OtoDNol8_ljfphJzjORt98m z$dicO#g&!$CV^Hz-wBX0%IO-(7c2CU!E0>@|Umk)YKK zdfF^K+XhD$yH057<9@Z)m-PbIH|u#THZtrO%wIV zZIKeEgc??m*OdcjRvVgnVtPi?GcqXOEvG8I4}<#;8Ok{ zDqgnj_%vSaN*RBTeyCQrOF1-7Yy0x;2N63NvzQv^EHxa-e41748Fv#a^^46FdnTlz z!(^4gJz5-;V9!+f00;|PkpMU5v9zLq;r_6 zd?kgxgXHgVTKQMBDQd0Wxx*_g47DQH%T^9pG5faIW@9F+VL!=l93rvg)^pQxK;3y9 zCM*1r$7^1>Gq*q}d_yg%Dvu01syOh9+`|c-{7auF&|Sjxht$rRF`*>urs{0wYO41i ztQ}f6bJC&4^Z)Z3DajM^-2zDGk!mQ zWJHF34x4m0uCwjm;8?0*>4!D_CDt^6TlKI#%DnkMO7CCe&2UxWf#No7A7pP-;K%z~^i^+8cD);d%g+2KU_VfFr+W47mFh ztUAA7epIe&^wnb8#pVtk+9aCpj+UM)&PP{l3nru3QjpYnFhV!^hb>4GXmHU`U6XP_ zomeKGsYXnQjA)^|2Q%G<$~;A_@DJ2q#lXmc8@OQ+UJa?*zmgmZyz}#R{}@U1mK8EI zv2++iEK(xao)9##W`LBFM$p^H!2^)F^T0}3yjfNoL$*p4sVZ_SUQBdw62x@-y1Xf$ zX7y{2b}=~9N|q@w< zd_?SxL}zdM4q;x{bo0Y{N>+Nw$Dzs)$h}r;3gI|l-<^v$OGqlr4lgDUFJi_9q9c8S zDa2ngU>Tx?R={NMI;2kz#emnk+wa znGaGX+o{+i6bB!u`F=BejQ>$aQ#*ODi1 zt=(1xJ1qNhqs84a`ckG6R$)aOE_{Oq=;4rC{E@vn2XbuR{W9R)F>-wt!*_cvAmF$? z%L;rgF>At%)%F~t(Cl8dXl5j|ub0)09Ku0ZbPPFVv;Ld+ugInIlnP-Lp%5Uz5$`2bF}**nbE#}EM!y;t-V4eenxg%FFMA1J+HP2i>Gf`4pP z&0OGZZ})JJy~Sl?5{$*mW{j0IS!^x-#;MOy45)SE6flmqR^xPD*WaO<3RAZT;e#6} z5TDmA2xZcgZKqyI z`>ywAQsl2k5oG@B)dWiU%+0Q&+agcSg2JGZT6Kc7q*yDGOI8_~u;7E=8z#B>pQ5oe zB6SjL2TKhBsI{mQU~1I`Fpg^CszrH(A^2y3M-04muo;hE+o{uxTDo+{x||;^L#jR{ z&ejlO{)QfXelL*@ZTO)BB7Z{z_hyHA9h+di5>KnQ)i30sTaM=|vqsUPSrxMsG1}@R zp8EThLU%KKS!Dl^O|mH7G=>P*LtxlXFSGj0VrDFjqJJhiw8Czra^U1==ve9+DXz&K z+i>YzTgh`Maq?9S!O&W)$-`6THdZG*c~D%ptKWCRsstnWm8bt+MH|icK4be& zWQ$)SpUKtWJ-v0(scj#;J%Q!csafS)H4?8@O*okx&;O&e;Ae#sWz&XiHw0|akqv5V zB#Mdk_-bM!NTU|YTE4B;wGHNiUWEDPv-z>!D}UxXNb*bpV$@+sJda(S2=_i+LbRz` zB3Xkq5`p3{Vc=t9uSg|0IfTv+DE6XYC}k*}@!SVnd$FH(i$`qP{ zQLeqzZdF5B)brE81*b zYLeaLrBeeVpd%Pkrm0yP8>D*C+(wK;ReV?B{6S2W!>Wkp-ag4ZmJQ_fsrv~#+jUvJ z$M3D|P2sa2yhNVA>8QSo$LtOfj$+QPeSqAjr#)mJclM{Hfo4+< zRj_Xgo}g0P&GLDaJhbr`)31w&4V%a2^paomtX2gV^~&#-qt-%vudCc_GSdT#HD6#$ z*-u`THLe;YxhK9uc`4se94!(&Rh&}|dtrwHi{Dj+%VbOSSPX`j6(dEIm5%090TM$r zeQOW^uxBG#{Y_g*~*#jGr3)PMWmU^Ejt6@jHGi+xq*P6qcsKw<)sIjbJ zrB_yD5C2q^|CK~3be2C(LYAne2iCg+)2*YqArbqvl4Z%_TxBOo<>G{i+Zwf#;WAoWFc^j{ zOmHL6+`NxRFVcGJ{z$|3k+!y*c?1nc4jTPc3nb;7zKPw#rZjXQfB`a{8q6w%RVk%1 zhOw6mG)T*Zx?`JEl91W(=8ap9pS7wO5cvy5W`F-XJxC*8JPJo0!fs<^Q4 z3uWOy*a~7;s5ff?Sb<(_zy=JL1TN^?>&HL)H*nke#ut#fdAdxWoUmN{i zrcI+(MXpv63HM~el5NoF@sCn^XeJC zlK<$fu8w&C4dj%&wwhUiYL$ugc0(~BJWf-am%xM!+M?R!k&f)k^1~)K8KVv{gP!e9YaKu@Sz|x%Spt)HMj5b z#^_$v*imLi#@-FJjls9ZnvQjeb6wBXWee)$JqfoNj8O(~5Gndf&3qwkhmlz(V5sJo zM@t?SVB~Fk^(FJRCJ)ij#rO2M5@%;UKyXX#KTk)LWofwf~eZbV-}RiRU)26&T!y;_I*blbG70ysN`*l7*B{FF6N36Kn|N6U=lmIwuxqnjzhLU;r|5%&Y>NATKGHKCc z>hhAC5nb&|F?JRCaEi1*LlCedDQW88N;!PVn`zT!)y#pioC0hr3-;V|Q47d9&kpqV-Q4#Gt9QY}52 zX4vE?E0Vah4dTEhf?1EOdXcn5M@OBkdP$~AX+2Yft136y)>M<;-;b93hS~AWZGxg! z6lCT%v(Rs(J0P^K?@e0mJC1#`FlGLm^DtEjUfBK@ndOq)xWfub&Fy^hFgHKhBaedJBu$;_ZWo6u-ap9 zLz^58>U`ihqjs(+eZk3akjQi1y`5WMDBi`=?v%4v^BlR!h%|e2s2C^QV<%tUe*DEA zTC5x&TM>5Q2Tujjb0(-0AkYjwx7VhxgYK}Q zU?2WOXCQtU6CC^CN08ci|0i4X5$wWCM>(%;5Xe2z%d>gUlePQSOYlG_a+kNU>qXfC z@O^^mqdN!?)D(Fgsq;h^>2Uta_$it(6EJtIca2$Yq=apwa02|(2FX6mJUH?ydhg39 zw1m@~F$905zZua*8FQ9O^Z9)Q4Gk45cYK_?;lBRi^R()@>#2s}urr`L6an;60Js`Z z`RPs)Ly4jD?IIX)RsDKFF$VW?HnDWXVnL=YbHT|kw}xR4CkGq92USRS^(a*1 zonj_#zs11|1levFn%ZAp&6}mnvM8i{$x$GD1C9yIDYcA5t5i^eQlj8!gm)^MVLu^r zkC;zvhz%jw4CvdKMkISPbku>7DWm+~P=p1vQ`|Vb;LwzK$*9TtRMGtWyJ4eNdIG)(BE&F(7oHcx%p<_#njTAGb4DMBw*hL z-B2Znn^sBlM{9r}3FtvOsT92^79UK%b2*6);mwc7Qy;62r3o2S_adePRHkrn$SO{Y zvVrpryvOcqES-nPp8k`A@xD7m+0Yq15r2FTIatwPLz(04<}k%*FOeUP(V<#S##HLq;yfN;SQB8xm82c#H*9uW8 zDU^rxrfZI)P9o?V@zd9>XXp+3*zG6#{Od$C`u?A+hmUmXKv-G&KL8LYW@x7G$V3lN zgiSCLopn{a{k0Z}{rBJ0sTWRQO$MqLmd35w)s(bFO$zzb;DjCyRDRBqG$Z)3(y300 z?dK!WOYrt*V9&?e_UBmE_Va6owpF%G3T4MzwXvxGM(LIKX>=nCUXqHCw5jd z^VA%kh;jXAp>yB)qZ=-_fLB}!Hc)qQR|PR;>Ki4KHH`8-HblpjRW|lSV!4T9K8E%6#)un2ZdGF zCKH!&?s1dvl9kM5O9xkzV7av2I(+UKE4N}7>@Z1mVU0o&^(2M9g-#0$(l<)cTiT$O z!222hU+%j|$W5k(Zy-J8ttp+WI| zm0YlU)AoB!(Ysshd0YQFvG#v!;%vUSA2Tc4Ty!TyPj3WuiiH5h!M&6Gp`-va*3Xrr zJCNv056-d}A9pVSAK{KjGV%*cS1K;MmkOM?tQP?>ARRyVFQN;+b7klCUhDg?z~HJI zY=Rv)ealWas<^E5?tLtSQA1%q7#C=N_WHZU*7NN}bB>jQ!>PmHZWgFGXKE(2qvew; zh!F}qmWIA#gGnrcbh}EZ!^O0~nK>@7wmMOE4(Pmg0d7*O%LTv26S0~$Np&n4cU5cN z>9$y@$;YphQ>Xs9g}cB6O`cPGVc^jO{HK`7Qi`sURQw2|i%GqQ*6OE8qDybnL6iNY zaEMfYO`OtH%I?&{)KvDit-XvnAzeHE%EjoAT&$|^LK_t+&e;BF;3VuioAG~rUH*L3 z~w6~wr$(CZFX$4W83bqW81cEeY>yw`NsPf z&awAetE!HgGme5LLl+}(AIC6ZubT_OFid-13k^2uq-sL7KG{|He7C-Y33Q%)fzmeAP8=&7Uk3o~=7Z;}O$mtFD7P>^< zQ*tH&%G0FrR3|BnC!|l=HCPY~yu0UWxMV<`T3j*)6r~F}hMxF}(1=B}_C~`J2GI}N zW7%=#z7j_xY=2#)17(%`RmCS`<)3utjjNYJJ%V2H6ewdl+Ek9! z%YBErTIE|#R_z)l$41Cm8~WbHT%sp=O78Y}%3THgM3?ac0QUgN|7t274PS=L7Gs2L zEn@!Tklfp%0R*Y?&FoT`=EHYi?xUPKObOHMIus#1%y|@45PL+{Hn)!sD)bOUpzh0- z^~~j-N0943&)tRJX|?{3_BCtu4c2gQ*Jtm9TPjim2AJ^UtETUZ$tbbsJB>M`DnA3$ z+Ym`vSo!=2PAKU8*o*Apvyujf#ZI5AuBZdUTy(9h=YeGT2W);j96t8IdF=zf<0=U# zsdb-j(zWh_O&)Om*{hrF!;T(0TOuk648i_*3<5uM)G7^^V0ivGY@M&D2}tP=!iFA)v)i{A@dnJ5 z>P2n;uG6ZECy+j}fQvX9q20Si239ya+Yp$p&iyG6n?o%D_thsMs?b3Z0`mC&`n>$U z+)?Mh`D+~a{R$AHW?!tdTZ2DPZ#?q~upBhev?FkdYNR@AS!WJ5MmlJr!Ztsz&?AFP z9DqOm0dknIQYO%>FJ@Rr9gb$7EmPtG_KY`{!XS*!oKep)_FdXNJNHL_Fm5&S{nN*U z@fYr`8@O<^69>N*5)b8*XQ-o}cb3WI%~dFV>phFeVLAGMf!}i*S>BTK7{fCaq*L~m z7wi(xhErYtDm`8kq&~+(!ufYgOfi#qo9KNkF11MGuYq(VDc!q>>06`<<(h9_INDe$G{LY|5vbDy zs6n3rB+rAJ@AqrJr(1FS*CcO*?RkKYxzhN`9f&7DVHgLk0rK!vNu!#uUCy#(7g@M{ z2PxaT+MQ8;%r)Y6?(*kREJwW%@CDPkEAANW&hkWxMy<0TI;GfruhY;;H*MK&uHWRxANG57HMK?OZ4nOwz+=J%I zQJ9V|q_ap32JTJxBkY-hTH)flA@mMqLvay=tiGPnWfc@dU~9kL8cjjNT4NS3cx1$~ zyVvvCg5Nopje0IqCWQz&vpmk9)!(S$(d<>8fE0GRr{mSt5UV=tdSkC2LZi{2BmmTt zo|)ZNTNA)`s8Ctn_(!x{YcVxC)7-#7|0YYr$w0fBY*Oc1=cHC;KShWLtcTZ{Acn(8 za!6{MXO#*3S?%Nq1$<)Pwi}9T{+0WEsT{}WaTa&Y?c&~Qy?m83OAGO94i-ZY2jU*- z0(y+k>o&6f;M+tgG~Hx@V>?9I=(<6S@>j2141FrQ5y4$=n2?^UWLX z^sYB!j>cJoG2S)6TyE&%5$#zSFMS|gzInraGRc#H0~+C;ZzwEFtj2W{7woh6PP&%G zwscKPaj;?D8`>;-f|H6Rug%=Eh_(n0Bda^-u!O@t`$>_eZLNkH`UwWzM`A#d7cBWJ z#Ja(^LG;Zz-(x1HGl0I09c1x|Es~()2oVtfj!gV-D%p4F2DBZVE&wDICNbJGALq(a zQhIq#L^5>MHpheDKwu3xE9V>X4|SeV$5yV52Xkm_yq z|GsK0W%GONgOK~W`W%j{zFzBW&6qZqz9lldRM5;6nszvA#+fQ>zA(4hR?k540aI#Qg&YnTQt?Y1-$hn~bTiys|uiLurb>6y>7vyil zFI~pq4xfF1^TO*|^ySxx2`sy;l0T|K<*6c6flJ`SNSl|DZE?-|q3=TZcM zVhHl$!Q1k1jgF-NRVCeR{zkvBpW{w$4V8QkE236dF`zmxw3N)beKGDN7?{z)T?lr^ zc-8-YZU0-GBf2BD@jH{2)M%2_qZ|(DROJlWCtD_nVmk`QQ?hbXasY)oKDw&@3I5sN zs7lwy0`#(Z-TipiEdI9bdlRVKy6Jz>-Mr~dZ+(2fnh1}9ZG+~ywnkx-q91FQgSGtH zmFxuvgCSyg&14=R)@||&eKihi|8>w((bkO$#H-j3U4lLKD)AHi=o6mjnAIQjqnE$8 zy%}#DG|zh0F^bQl0$WsnZ-F9SrwCQo{{kK2Q94)U+4V7tX9Pr9n?ck@C_SS*yszY- zzsX|)TUL}Ta@@F5t?DkEFz^@&S}BT~t+MTV)h`(xu&ul)r`W%n zt~HGrXyksZqu`u@(%7k07kNhF(@>NDR3z?<@L&BZNCo)$nZA?EqYTO_f+;kWEtc7! zRBsh&fXvowGSf=_jFQqcg5|K3nIkggPICzb^Y*d`y%40WK!0R){wevd<6YgV_jS|0 z=>OLHS?ag{GV48k)qQa{I^<)!XZd$z2V}AM#1Y%;|rdkDWkTql-h9;`wMAPdONn+ zb%=>ElM@Q^u&U=E*&`~Q#arSQcek%6+S8{6w~fgr`^@3QClgufkOG|Xi}eWLe zehl8JT$T^J+q7Mg#HG%K4U!ZKyElx8mHyPPV@j=ySC`-0;deeQRsc|7jE~WYmn#$nkK<8iZR#SEYEFOr1w1OM1Sw(wib?Ha= zaJzKOaP}D3p6fg~{}*eKqEZFKT0+u9>e~<*w3_P$#hno&X}@!hcLX|jjy{5v!&w3x zxN_nSG|`Yn3o8iLi9z$eN6h&<&tI11Kb9g2pTCljhhwt=(>zn^##Jrmyr^VVH8Psz z{63<`6vSzmPOK@`(PSJwsSj_LG*;NE4q+*gg>Uu zc8ZVpGF!hmr+lUn4EJrqO2FEzXYN6HGf{en3h{y`TVJx%h7vcD)GjjJ<6KS6s&adU>F4z8;ar|4b~(Z9$WJB4I#$mY9p zHBbjcAx3T?-{Q^rj29X(K8;6&)p1T@j}cZDT$y z+pdA#?4o&U1teXTebG5@Ire%dN7d!QLp+eMW;lkd>H=Lqz8rw! zIcPR7?nw`9tjm%7H$k}`{~(p7^bq%HzcCjw_d&>x@fMrtRijYG0fgFPFi!oD6F8+BJQFn>!?;b+pB+Lpb7cmJ1 zR1yk$U^Axu>#%I9aUm4_h-Uk9BYSjJ7y3s8bq+iZ9M@7O0K8ZYB_U3vVohYO1T6HPM?Dzkst)q(Fj7A zvFww%Qn25UF&H(D+!hevj3ACS(a1Q;paz&;I`~hiMM>U1`x@vT~9nmA@>1Gn2(U*VurJz_C~Su{EL` z<~Uh={{=mZsEXuVtlKvc=G?WlDQ|hpJW@?rJH<7YuyEU! zI~OkvECu=txI_|9>X}$f0@AwvZ41~=L$P0V_+qu%@#y+Ny znWL!(iTl-5nl!({1Rgrog~bsI@|sv2cURPL^0x}ZoTC=PzsGF5$+~vSb0pu!GcF~d z^s6!&tR5}WQ&)4c!zg_6(u&67?_SE`AnZ3eD}0AS#M?zQ#>EejK}kv%%fPx`y-uR0 zd?Nc=|JEeT+8OLW{9ehW3x_2rtcNT#ht8+&fihh6EHq}eF$fTWZ+3cp0CtM%|6!SK zKsH&#Kv=WoTga0@fj|$FLM2j)x1ErR>8PT_GU~fi2EoY&mr~{B1|rr6UEDC{VJ#{u z$H0(&*jWQTT)&LwzVGgn$9;x*Bj9&iUT&Oa6sH^>+VR=LHWQzD8sU-FSWXDSAk~#I zTbez*wEvM? z^4u_`)Y>IMQ23Lr35AtiOhRs&o>DALn0^E;86YZ_`nOD3O#ENbwj0t>gB?Z9s8cs+ z{Ki3j&HuADBl(9WjZL`)_mhy~fe$lBPxDv^%8w`Z-pfz!E%$Rc@i^SiDDSKG<6!`y zCgXI>#B^&itM@BmH5TW)O(H$h(^y-Qr?7zq6m`8lY|V7fnf8wCt>RqEQ7|Qj3=5oQ z&dWJvmCp2Ygym$WI(Oiq0sE$^^^2UR{aMSU5z|cp;VcmTvW{Fuq2q@sGzQA1O4Q|q zn1LXD)i#B7>oV-<*#^XxqPRYr@d!3x<%Z!+ujbgQ1flW2qHS&1DR<&dO&MD^8g1ptV`hOI95S*T zEAV)NfRnF%;m`=E;L%j&P$|h~(%G$Ft9T%+ia;%E?-wc5DGCNCS;KpwSb8`lG0S+O zQCUU0lo$jum$d;=c8d1ZDLZBi)iz!Jx?N{X0LUiylmEvMGCBTB?p61Fjk@BrIr;b{ zAZ05;V5UapfVMhRPw0gUT|zW#hkTCbi8c2kR$VnWxBkPlu-A$RDWovPIx*=eFZvi3 zK~hAq;P1>~G&(4Jk-hLLE!V{Ia=pvtWZT~GH*Sv3YLwj)+*KQHzA#AatRw0di?8W# zG^jBSk+H+sEhZNBx{->VhwRW&-9uZMWlJ?zgA8Z;C|byjk|Sl6HoZ7^Eo{P&m?`9< zZ_F`veD2lG@>DCHq5iR07r|Ovxp95t`+nJ{#!_ZrdD1^N$gwmO9L;g%lO%%%s_{j@?bxxPO0dI(P!C;_l=M z9i=&l=e35pI!bv;c3A$G0$Vx*bruI&6gHz%IZ>~WO($)IK+(z7gcWJk?rNd7OSX>B zr}cO3t@}p%&g+xeZ;#>Sp0}V;6Q`Qra;cp)r7VKU$Rx-7!iU^tMNn~bh5MvtF6m%J zY$Gv33{fZYZs)ESOuxHHDRz-Fja$6nJ9D3tRvu%g_eXI%XJ>GvL0#_A?&Ax<>TON% z)8-6@=`xQINWr#a%WdC*dSxm~75tx8+C38<01M73BaHU%YgC>mJ+TQEl%?jUKYV9^ zdAA{aRr3q(5;4wYM{zimVwpLj_Z}Lm#r0MZ` z>&o&LPA!U`O5-=aSMA440gJokw{IKhNvPrVYK1m`!Jk;z5-%<+MPvvx z@n@I^gp5jnqyYh(G8tQ~_y*+%lev^o*E1@nr|>(RqK1VmrNo+&)O7}h3M;Ae*hQ_` z7-~WF)u@U!oaef?<$EBZ_k_!bE8l89>3FjJhQuLl1mb>;Vcxz|y~lN-vsIcRKSEZt z5)k;HM!hV|7&##20pEXXJ;h^BKW3|gCZHuMb(ol>P8eeNAPbN@F3H+X8+bw87bU2T zGO|$^p>U7{B%<@uv(}5p1dEHoeO(9M-~ol5IryL3-}oOl}9s8 z07#QSvIZX+nLNp$Z_Z(iiJ9gp1tmC7YI#|-A~p#0&$bmL+qcBJAs;tMK%JogvwaD*J?UX6qTVkR7vzUd7N8;=)p8u(NaZsf!HqCRE$G+SS|#j{iI7E0$fvM z1-ckg*8-T3Dw z3v;rTuv=|HUsWBWFJ7dGLS9*KmV^bB^n9-4;YdaQT_j3i=$>zAZvK@jo}rRg&dhBFVsW43)SR5oB`7$u!fZ;%}kGXVS_i_ifmbYR{J19%w7)*aW!yOSYB&}vy z)<=!if<8(>k{zMkF2bT}q8!@*^?R5O91Tkb@&X4$`bS$TvIwU#yd25XruA4!R0JAp z!FvV}Xog~)*SGvsXb3~y;sob2qsiwyML5tWy=}k-T_)k8PSJZqt^9Wx6QL6$R?`<#rA3=W z>E4|k3Pv_r>v!GJ!3 ztO)n`-yPhJ0{XeQ&OHm{?jwS%ELpexS>~g+nPA+`G({|OCi)zj$=FT368G}zaA2Xx&2#3 zD|brxFpc1`A(BXt5uwe}|+{+0e7rNAc@a z617y_>JxtW20f!yBWOj30pS%0$ANCIJ&6B>6?Mub{h>9%2w3G`tbIYf=-6fUFxtv1 z#azvfD&_Rcq+9uc?iOqqfs67^XbjQJ6nmp8!%MYk!qE{H zyS)P{o*k2_%^`_-c|ICU$1{E@dK#oie=-~BI9ht$DKW?RJu(+B%M+&hEs++8GHSk=i~t?DlUm0o z+0Ax>pkfyRiIOyouu~yVZEF2KS*&<2%m71O^3&-Y`ef&G)KthN3?Ki+&Y*woaES1Q z1|Wvqd)O=)NJY2cuWeDIHJJ?E`Y=rxV=2>+rn2pU`-ZK!kDw;%Y8c_-CZ;eA(AWlj zDqE6Fcbwu@-o{pWv+&gUT_@|om+PRa$^Nd7QVn9JWWdwqlYRcdz1)^L=_g8Sd`;W< zv{IwgS8A1v!sRY62-Ky+0M~Y_D8|$IyvoM18inB&IiLOa{K8()2XbEa7t_zP+`=xL zR&zBFHT(q$)zo#cv;V?Bl5{1AYmb*IhjS^AvrckGBtg;O`2wfMTY9zEP?>S+J20(9 zLn+cnw+P0x<#*M9LhwDu_g$?6x77b}0ni#eeyJ~PGJd8z>v#o?IAydVAM8riN}!`S zEkYBWN=ON5bj2nR5}e<(p-tDeVHRJ>C^~r$!ZA~TO&2pK%%xz>3!MpK%)0J9WcR`Q zjQhmNStFqNS$GOH0hgXNdusrULl>s_YD5g~0WFo+Rg)mZPI}3bTM{7v7A~HJc`rBw zPO($9PMf>$E%v87)bBJyRg7bRRZf?@Rm{@9W6tIA?Rg^&Uz(6_zM+^-Q-8kG z~#pU6c#kDB{uPWUimCVh@f)`tl)8Z(^& z?1Pck2{Lw(=lD<~hv$2|g#+5BoM=HKcv%1{tt%ETAj(#iQ)^$F&T*mKEODe+c-L`c z%%0iYg80(y607vKapRzuwfOm&NXI-f8id$Dmh{bV(=EwW^6d=RqJ7TC@U%HCcV4&Y zx8c4Oe?P~$?DlGsJYcgOaPPK_=m4-??IX-t=`a`SPQ6p^6VL>n3;Dh7x6GbvN$8}Qy+Gf(E=h~V71SXZPq;=Q-iYW? z*btl}P@uBZxub?b0#lKEgK>O0+H2xhV_a70Af{)x@BWBvQRNhTNZ5)UutI(LD!aFR z#2`&GD?@G}J;kM%&Pl+#oVLaBo`HSEW)@4Xk?$-#sLWhefU~UfQ;P5rn>UT3(dECLz?R79h z&-XGj>Swwv2WTP2(4{Ao&)6kINK0-H$}$C!jwLASARrY*+C~uVn}lVP5goApvQ|}5 zY%lHY&(P_}tblIDh@S*bXtZMIP8)O(o=0T3o_+Mb*;xJt?l|8HwW}V4x2j)gC9o2X zF^r}xc{lKsYlgALA@a`|tmgPcWf3vN+3Q)6CaQz*PhbqmP*`*?+Yvm~6)IYhUW~P| zn-K6Cn)Ej}Tt>mi?r!wF|6573vSfq1`V66gh^aW@DqVp;Mu7NCe*P@yQrv7Qj1)XN zL1-#dz#UO|Zr8m;DsEvY5z457Yzzy&KWp9mo6rjj`(RQ4q33D-&uFN?(}O^4CO?xQbIukKCaznx!Fy8k*iKP7@P)L6=yaKbakU+g(v z?f0Zo7cKcj1(YdBI*qM`2B=GFYcuXq)>`ei4|)SZHoNh^XQ-!Za^EWf2+;Q&aXi15 zjVayEv^URf=ux1-07_LCn>VRp<}oK~q!hbifi#bCq>MC!BKbR}DYFRAL2<{$Nrm#z zi#0Rg^56#9JU=)Kw_Q&5590}aADAU}EE+~bCVT;rX_N7Y-f5m62Z$o&yQ2&$X9=&F zycgw}vTc*V3<>Vt(F8o^lwtHH%LIKc@@N)d1Kk(cbS{U2$NNMk!2^En*S3Nw0w*Ed1?m3-_7( zjO$X87j-){jVV8c>105o8x7;!(b3*}zP5kWMdtF|6#*vfxc?eEbsAJbW|U+g&8+GH zd7w|&{6uQDzz_{&P(SHt^dH;c{{WK8iF&!mI@J0&Dftc*7U4AC%gYBJRE&4L!c%|X z1a&3!eI6Ets3Gw8PR2%pe)OK8+q+aG{(8FJ-p1kaaQK>f{&T1|m5NeC1T|rp)9iO zV=&6h4mvM%8C0Arx_8-lJVz0%cDw^;X9{55tb_$OsX=>6;JBuLK+Ld(G4-2= zq%@AeGrlCFBgYM&plY4(tJpm1a^gjI_xilBUf$bl6nSa{btA%jK2X%tN(m8M5!oY_ zwjIyX)dq~D0g>u|TOVwa^lLFKb=sF@*)_Bo~p1K1T9|Yx!DRIyxGRkDGFzi)J%&c?}C$ z6=nobEw7~BgP0%J#AjW`K7>a5ZF8<4oLjP)vuR})rlV97iT@t;{<^clZ8|q?+dKLW zpPzmww3#4+#7&>^gq1s3X>I8A@_G?7@O({=_J;qUF#amr**c9Z@riZ<5%J^$-9S0V z%P_w_)7C&2m9r6JIfY$^Pu*RH;LreX zP6vPnV-x@FfEodx?_&@!NqF6TY;ImWOgSvBInRUQM^YCbO9p7;lQo${4t#WFWe4}rV6r@(9fh@ zW~XPOtGFf}bGPytm}!51VYw+Ia24Oy@5`kQ(iS@@ zUPn#Dv@!^7!3b%c#^8XS)m%U#6#nCQwEZvGpLla4Sp;hjsh{p^sM6wutQ|77O@2w} zJ>EYTlPJ;g>gs$VWJ!el7W@3YaUP4ud3PO$$Mvxm1Ne}qV{aSHtoNadFqcj)9rz1*>_@R z@&&`ljH_6>M+*|n7Sy0rVjfx~w9mZICr(#*>N@Ry5j?b_x6|(jX`KyN%X(%WC;X#(pPJjwC*0aaNWqb1DUf)#C|O06DNWQQG_SahV+g0WbT{qc8sZ z?R(tsk3xXS`9AU0r<+YzEAvSgKiont!TA^1p22`bp$<4gY(zb*I7v9mE$4-%%f)YZ zFq%kXUn1_ZEW*q(&a$bL@G{u0G6U8-0}CpLvSv%Ki6y+@In>%gR{PIZ51ZxYJkGia zI6$Cl5;2_upT1|x*QMqG$1{}C6SU~lttwKoeq@Cwtl?WNMLw{63S2~gm?65*&$SM^ z#3X#Fs;5G>!bc2ND5ZjZTu2u`2C+XVVXr}YC;mc9rncc=bXPq>AHD-B57NniS5QZ9 zmPk(3xj?_CzE-9MH1<69zCOvopbA5+p_{54Y|E9sxZZGbC((u84uG-`<~H{v@jTe@`L9kPq~MAr#4S(cO=fy{MuM z0ONgufK|0>ksBUZSttTLo&t@?f=uyHYo_{kNG)RSV!bu)pXnua ze@q`0OSF_1#n_PwLx`nrv{ia|5_h-oI%ox-Hi4&o{mE>#o#ZW$%lb+PgDJWnxrSmBoxcy91*rL5wJ~^ z{(9x8g0-J0plaSBGDD*->7u*vHn7D!F|j5i*rGnKLwzJH$|2t3m01_*mK7Gt!h*(AKW0Flr5;NyHYc z$&wB(LY$0h(tchbvgVZlXM)?p++47@@gI}v+?Vw9?|0S?by+hWqS?wPK9N+cRi#m_ zo5Fx_J`$`vY2+!UhbK5rIe=xul*%M{@tPc$b@>k3VsyLbV2t<*QbRncm43DIk$3f>7$qU>- zP@REIo=!EqDR=dH+HY0?%8~A$N9p5{n1<~PexNf9Rmf)y+Uvbn$5+DuBte=-)hbvH zu(H%8`X3!&h2SJu=$Rc@GZ`5@W%#T9LC(hgG#QcG*I%AFlf>eRf8>oBK!u$+`K1R& ztbqUeCFmbDv%>c3GcCuoylP`HNQm*TTftSQ9%hSdHiHV51lz~!2%A}^dDO$ zP1{#Oq?r_Dnt2HXWD<%CBiPG*I*QeshC@c560RhO%gEZ|TUn;pF{O%K(ymmrwr%j# z@$(?PJV#o7YG;G8-?@eZH$NCib5lV9Hc|fX>&C%)i?j!>!ygonNTJl?F*C#!r%~u( zp$Mix>TOOOq`>FWcIQ1X&e({_=x_n-RZs%0-=+MY2v=R_*#0grH@x&3daLzQ>GW`Y z>R9Z1cba9@O!LBK%-E|;6hPAt?j%;_Uj{n;!x+svXfvW1`lXn(DJumofstA;(2Ba) zhId$it-37Z=Vw3IPWum>92>U+U}LFhF|e~mU#*pDBn~>9Wku`8Ez~#J*U^ap6{~R9 zM-`q-qHO5EhDI-_Q3---J(ogik*ocOiF=L<$X3mpv5@-bbFF1&w0Fz;=wJn3KL6pf zkb*9RhNa*B$_=J?k*D6=WWtL&IcRis58XQRzlB1swEmQ`^!OjNWdkf>S$eQQ8xGrx z+yALm{yhiXpq@#l^;IC1_ijjnnZ&T7NQ#v@X_!Jbg#=GwmQJfh`Cvz9Q>cxUmL;x- zjng-fielc{VP#Z&r-6Qbe!~DmbbRvPXI+1G_Plpa<9EMb9RsAj&9jABpu-Z!ELbE{ z?K;4n)*Lj5NNx(a@L(P#;jBr>SA}(c95gH${&JN;!7bU1yux<9b6m!|4;NTTDP}($ z%0vFT9gcpx90u;%ln>aztvn{`4*}+eL#t*GWri{q5_lvAjIl~Of2xLB^NNNU;{;7m zjv7z;0fNRhABup!!O?76FI}zdfEYY(X z0vj?(=hl4qWj)**O{CqSKoe^l3~kOC0-Y9`U;ZU-iSBuLV@6U5LZGC)(^^L84V*6C zCSji8`?Yot1Udr}q5fD-*;2_r7+#OlC4=U~s?lF?OM*xA zjk!6jFS)+J5dnQ1UaWY9lw;9|uP3?B0%QXm$HV_w8UK6I{rgh=YuexOWrml2rQY~h zZ2JCm$G4nyFx=+NWuk4WFNnZ#*i>Cib4*&Gx;DBZQmW34mNwaX4BFP^He*0~W}+JMEv=V$qlhTc)E0qWNYS7o_bE*FTy zD?Kg-ZLIofUPY!6|0KtYZ)4=Bk+xX*$w_Kf#Ss(onQlYlebiLM@Uz7C0!^D6c2yBy z#uJrBrMt!({0XPr;9<@DQHlnRFk9^I>MMu&m_%RIm?}6jGUg_R&c!|r8DGSRsgUtF zM54|#RL#0>_bJ8D(ZM=lk+WK-l*XqCV)=3F-VETq4(mio9R@f|8y&J_p4DIs(G5*h zYL!!c)So0$=x$%jEV-7{fGNCQQ5<33;ISkJIc>Vrb z{vJk0@V&n}S981AI4QRe%O@s`Ovcv55-7faR+z6BU>QbQR>1fk(Z}9wM|l+uDoqAS z0fJ#!1%|8FQ&oU=uu?a0)=X`k)=y{~I0>>I`nBjPY>7NkU%#}|aQ&?+qxa0Lv3U=G}(N+NbPG$cKMgC{t6hXF|qi>f*wd^%AvXucx7-Zt4w1~)dq8FoOx?ES{ zBv*p!E1nh=kG5rU0l@?p5M0?c9=Ie$B(0z6IVbN`KU&s(F7)m`e((8S?*0O7sNu`u z@OD_69#6%3vXQbhK-nlvqNSv)qe;y2iMgk!@azRxCO;HpwHR^9O35LUMuntE2XHtM zUfj`ktcWU=2=Zz!gO)Lb0I?f`qk$WOoMYqGyyUMldM=7P+Qrl@Jfyqz>8b)}!A4_& zZN@^J*N?r{g&ZUC4<;(lfw*j5Sh>fbuY?QOY-G_ zOU)WbagvO&uYHECdn+tF_?w1K3Un%H9FdT6{(}J)C{yN!PEE8#FTP7-bEJ(>_MOC; zMfxnC;2GBPBMTM^+)j8!iXDEnFulhr#Zj_#bj@KT=*tGFTDsXa!)-C_Sv<1HW_>pe zQJR42{f3?;3{haCDd}g_f6SQwX=%{)3ec8E;yxjv7@nBiCYD z-Rjs(rfx~u*)QbvTx5LJ)bG0h)NT4-XTHmP?;Yr$eeBmSL-zPR&m%R5xZE?(_mV40 zlnd!Zbj&Whf%_P@N!@kYVA`#~h%&^`tS3Rv;!ZKew82bk;H?yk{Al2+moU-AllAp2 zAnM;c*8mr#KUU>EI5HyHJsQaDM*6vO<=)q_2ztBL{jr%%70-T=H|G^tZjH+l1TSoA zptMGjLZnRkAS@;?E1hBd{FI`v{tx$#&O6>l>bB<0K|vz*L@u0GavQcUa9!;o#|>c@ zVB-wjnes9x&5ITtIu9H05#Mn^3giJWS}NFFf0v|5Lq)kYr~A(}<`k6p`_wA4j*r_< z@_(5t(EsHrvtDBS<=~=2ZOw*-1##ue)c8;vBuLE0vuc9DHYm9YY+jL76bOlshXTm# z!iC&`#hf*_XOoWk1?15gLA*9ymuYd|#?+U3u7{ug7+haj^SNK|3Say0d?lTabGtS9 z^4pstDQ)D+d?0EwP+{XFAfrVXNk!w1FRYXPG^mWot2oX@$|E|sI0#aqbJIODJz%x^ zLqd7!$=ntiGHV|$*cIBHyOXK@$wtQi0-U{S!2pb~8t;{-Zi67*`A2bxNe5~-w(Zwj z331D^UQ$PS?*2!H@ulq!I9&=i;k4^@X+OI+r#5}@a*%s6c=b8ZZz{N@izYNF$& zN<~39kPOrAA2(=OBBb_H>hR!dw%ndbCp{(C4Gst^sV1H z$jblZotM@PQOd3bh#2#NJxy8-f~{yP9SOtFh?2;{Ux-1| z>+Cl_)MqGgFmdP7W12B@du9!FD%m)@ReCadgmwaTH{S*#pG28Uq-D6)nef7xgH|2E zu_=ZPmhGImEDjMb35v8YDCO6P5}_E~{ixqOFqygi%@pCW2Qmb;JDlSvRjl=fdm}9A zN5lLKsL9J7nhkJ9iUJyhhgM%IQ~Of~!Ifm`bpO3q|Mi|*6lJ6&&MVAOLM-azP$0?K zez8asrFy~;Lhwo|B7kDQl2Pa#V3g0kh!>?$TQ`#Xf0hk>P9#AJ>E93Ke(aX!c;nTx5|sxzoy7~-fSw@|lIHQ&q0KA6_o@!ws5PFOJwtCWTLG(D!7X5IVfpHyZj}}} zB6cBGXMA307!Y7^W`E^OzLb2vBKI$H+kN=NZt^_gWlQ@Zrn^^#5($a2Oa~{NI#l$y zktnojOBbGL`TllH&)E(qfJb45j2X!zOWUj};B&Ycs4eO#YN-8W&{%lTl=Y;Vt}EeV zb@DnL+)W}6LfL_T)OcE-GBVNGH^$L5mn@x?jDhz@2Npy1Pn*BbkQ5v5nEU%u7Fd84 z1~$9oD(G1Dr>~!neQ&UDfyXoAg9*@LHcj}i0qPbJmESKG^J@t)l%M9T`#_PA&Q_{Y z;@kZqpg<-@D)=YDP=Bk)U(3Uuyow|3ha5|y%=K~oZhxm?T!^LZryPW@liPRv*TVN} z?%R&XM`!H$V#-HcEk!2?S=j<*%#c-sx&&UUDPCl=n&$qDa{r~7{BfP$IZ;$SAVI0c z!Ylb5$qmU#yw16Pr5zEJI~-UhkOi0CAuf;8PRB#+?(oL#PvP`hc?Meubsce5)0+G> zUIO5hps+x-w!t>x(aZTdnNu#Q-wc7Ve*;kXy}t)XwqhHULE_0wnHxq32V_O-5+YSc zl>d0jtfJ^dtj^ib?i6d%;u^CeoS;xs_C1LC59ps4{yUUa4l?ir0Fr~!Bo13jO8l`cTS|mD$PM!`moR`cPvf=u z=eBU@$CpPx)whE8?}vH?Qs7SuMR{gIb{dz>Np|_`w*B6r$yT3-$LCzH1Nk_7?x&8& z=Eay%7b)H8%lN58D2vboFc~wf1Z1tO*50VdhD^#|e6N*8eQ2&zR4Z1t$tUn6;MCPd zvu-c~1Pi^4fry0B@&++jKyYiR0*$OPYI*_Ib|2E&zI~4x>*;yaX)07cUI13dk+VKob%uA>iZ0jzQ=x_ z8;f6guMWMf*>v#acb`*;*{9S|nWU(S*jXk_Ri|SPO2KK&o$Pz%{>FVw-q4GZb2nT? zZdZdCj$jpG`<;I_8`-3M#|eHB$U!*VP}pdX6%87^o;q9m8;rIGNPYX|*yE0K+0Yn7 zqX7IeTL4b;Sz=o~By;kssjX&|*R+Eg6G)}-?~~@kKBp9xA{LpX>i~Ru{HKs#I%@r7 z2yg!zZ{D!T(Hv{+WT6ZbU!AhP3DTvnAMWAFLX{3e6=!vv2hkB(%YM?2!c$}6@4Xgb zu%h=};WMt|OPYY$3E9qH*==0rW*W(tvcP|$8h4E&Nl}ndlD%X) zLn;1;(Meaxt&^I+^H;$F+_U1tX#QRa=Q{n7Q4SkY*sq$-*P~Hf2zZ;WcR(Ex@g9f& zz2AA={cd`0ZEZb&`U@~9{SgyQf|_5Vioy)0tv)9Ah|?Ym4K(Uu*D3cBYpbc@PtnQl z1p+Oy+x#kNf{6sjFDYW7m8b2E)U+Ss@udBYyp(fqG=l?xQKUb?X<5yrBNlhqTJ1WW z1}!`g{WP5s+|~29P#Krkr)aT8Cgs75zzHA8vV-L}UZ%BrE_lVmWqUqjJdb8Yj5T)- z&V(aGnTkD}c8QtDDjFXPTxK|M^Ofq1`lRNiA&${#Vj)?xXKH?dd1NA9DjX!ZWWuTD zgQ`q&`2UM+6PVL1n*;Cr z9bs*dcyf|`n}_$|kiNF(+c@|2)_6GXdsf$d<8kkC(fNG7c(m!dGw}5w8It)2!x77n zqx@mI6`Hg8*s~xE=_D4aEFzOQ+oYHXI?fR}6E-;h;3Dd&4amvfH_SN&gK>Wa6q3TX ziEhZ(@SCtUCS054Obc8`jOzbK(>X9^)^yD}o{4SSwr$(i#LmP{Cbq3R#>BR5+qNb? z$@6~aFYMmEy4UKey6QdkW3D{zeEVNm=|O&XL;aKn4pN$fk>Wcs546eh*k);-?I~N} zx9}ZRXFku{>o#|?H%+O*&4x}$QsM8d(P0E5U4)!njeAUQ6lf6EFY{Li-k0inXq6Tg znM~5El2-%$U0q?cXF%m*1wlv0@t$nlL8kdIhDG-FXyon`>|Iu&VbUWvN=#fQZNJsv}3daE&T0xxiMoa6H!6R<{Rtue{g%(;oMI~4@OsNwShrADcjgI zZJHnG{PKeev(sP#LP?Zv0iudOTkXPau^`Jd=!8)&e;q6}ph*y^$#wGDgI|vW6*%90 z)#$FbuLb%Hg!g^H)qj0q@3h|^-Z^vYtl5++zGy$|*PWMWw43uW04LpTFSjT}F=UBs zg!3_(_e@qg&m<34s78zzV02hy*4L#>ZrD9B*9kWoU^zaE*c#yW#1!)1ape?a^VxgwVO8Us(82ErvY9Qr98wop7B(MGLUqJnzX__Vs z6|>Fn9X9Z%C1HUz)xMTW?9jY+^s0C$7*@FArt9c0y?V#C@x$Ai6G3?%WR@7SFUiXs zz3=nb+12ZC@#bAaTj%4Kv83}E>GJ68!g=+PQT=C%*8x-zXzbx6fM4NqIiI>X9!>P6 z-W;h!bG|abuK9-`+_j>s(89^$SrIb6##7_TOl4NzXhYNXXwwJy;*(BPQH~+tqMJ9b z0F7}HS-HDVDNt#OmD^I$evtxCHCb993aKyg!O;4G9^r>a_>oO^97qUlJvkBBMXSs} zgxFfTC*oQJ(ipc^e?S<0yxrrQ+8{l(!HsMM{lgK8^c8(gU+ih#TLm+l!*kH+r)4)I ztYE8`P%h6=v8@T;@ZrjQjGxUfkUY~Am__gDpwbWDQt;Ud^Iq)lY_u(h|9v=eNN-D^ zlNAsmUsI#{pJ^_fwPt#vQkXLrEk@g6!$_#H2*U1|r))x~hG~iBLbr~qUA~{+5*BRy zx0bE9u+PV&u8+4F;-}7%G!|?y%a_jLzvwnRL{(_GrWBU8?g;Vy{Gz|5Bzuc*wI2;( zgUVT7zjA0*9{Q8Z8#(gFWB|&uHsP3~z$FSTvGMneP*yQOEc1EVxlj2=_8VXV_DI9W|Y=iFiz9W#$bCa$D}}8 zwpyW!PJlSyFP5}4y787$p-LjUWIeZOn-8550%7QYm2^7~b90NW-(}NZ<>L?MQ@$c? z5+|1CpT5dxtO7^lS!UZs&J{768L@Aty0rNJoO4ny7LB6LubTvfE z4&0&pTA^jgY(rv`%Hxg6CX?=6-ut-RkhR$6!PTpQRL&Sz6JaAsoRIYEao!{LVmA{B za*9McRG5Gio{Z!2`4Bkrlw5yN8ADW(G_XJ25~zQ~FLp3&->0M@_97zLb$~azEazn) zpOMTw&~&ap^~U~+oV(=_?jhjLu0%-ryt&yu%otv`x@=XzZKLZPHZz(fxN(R;Z=1%i}EEU)G;S;pOm5YL2HwGJW~97N=fx!sxSyN zsK6Wx4R^dVi?mZ7Z|*|*_%S{Pf?_lUc*8L9#^HAGFx8}H0&|3~%9o!xR-)X(4jVK# zn~Ozu9#@oH@;m)L%sosoz2)9!&qtvUD6Bpcyh?3 zmG58dMYB+l$X;Yk4z2r69A~0$Q+KROlk9ZWKb@2#$w(23`DfbplVxnmT1K+hroYR3 z;|@Djj`;1Q-AQN#7BK!g30KaiN)W7JJLO2#DEgW8GlUvJwtmLU4IR*YfJ+u=E2j!{MeR0Ha$=mZ5&{TeMF~VnwG^iV^=H`{yvEW;d#zUkH%3fiZ6L=__C0 zM)eCR&!BW_zW7{2U#Oy$8tsrjtPSfR=FoVY)=e+2#pu|B>gY_aObgOkn%tBDS5D)R zQY!qyCCsnG0&`FGfW^aj2?vI~JlT zvz^D+dKQ_>DH*)jry>+XIZ5V05C3TWk`BQS1wB3r-QnZG^pl7ia>Ts>cs`6W_m@=h zjBy1%y1_>igTnw0w!TuFydgf6g2Enfw7bx)$(UBoceTjWseY@&lbeX|s&Li(Ke3y{ zS|ce6qXFN+7#oQLdy2J^qk`1(;mdvNR}z;Lo}C!}Sg&R>DQKP|;=V(>Gh&$YwLAOy zmcwo}e8tB#eAl68GM(oo`WMZ6stwQMm}C`TF~!Sn{4$r1NR=Hi%Gfeu=({CBqi_p# z0@64n>n!L^0~H~epBPgFb{vlh1~N>F4_gA9?t^+4g}3yvU2SmiqThpjhn7(FV;XLp zlIZN9Sx^p9fYh#NXb=F`oE;8g^B8G4Z8$VjXA6qp>_<=7)?3xmo0?UPAwvR0TvNEB zCGixO?$ZV{xT+|TRXf6wA~%#>Yk8!mXIz1H1j3xA)o)>cOEfgp%7Od=6!nw^HKkl5 zrc@KH3D7Id=Mc0z45IR6mHBp%{|+*87Q__@o?QSl9fb7L$&JRGm&(QH1H4lbsVZHP9!?T zcFd}2Ox51_&JiD`&x{Z(4!+cb$?$kQe?8tibG#@GKZeAwcyAYn@VuLIXk;-X$)GU; z_1sa(0tIn?XX*5BPi#Y@A2Eu(GXT|;ydC~1)M5P*mjQajej-5acxC1;kv~-xE+WKu zL{#hoaO?aF*GEMt6bP}+>b!we<+ETu^D!;IcW)h#%!Tlim;JbuvCO7M5=EDpHepcV zG2RGEN}p=PNl+6&nPZ|fhnr$XroH1R1dLK7pnMuCWo`teHc-|rjOEP?zo}i10SBOh^@m3O2!!~Ey68qWeI08jWV3?97Ug#F&1u(Nm@8> zEAYJaEH-m_y5Ezeb7_9=+`sN|f$eI5Vbs>n8%w!OK71Er@F-%v#o3%`6=kkH@rC-{ z!rCouKKf7J#7L#q4~jcVccEiusxIGL7|w5JK|rhcH;`6V;vP%6D9nEU(=2>wc$X*)~{4?ygKQZ!PpRAhDlqRjT2y z61Qy6EEO}F7BX6pK(3kng>9?q{T*)e%R&=qbt|g588QNwA2vgJ^FrNnd_9{k0*wnB zrY;SJ6t<)0gCsA`$~77dL(QwhWM79-pW#C2t}`l}5-@{>84ZvxBzPa)G>n~%jfC#B zSTPLjA!zmDZ#EflV=*SdSj>1g7Jrea52!Q#uHt1J)%@_A!&P>ySn;g82->MA%)P}T zv!cej&hcdu3Y=V__Y*|LGtQ4Kadby3IhXDzap2>TPm3ME>M~)|vOi6twL(W3y{0N! z)#xqgH;+JC!X+C@qAnYS(I+))Eb79X)c zCcNu`jMSE+;wUWp`d`QE?Vg65f7kTu*Z&IS-!q7xa6B#&cz{qooyi9-5Jm6)rLsF0pe9^7NYNNFAy5>f0_2W4 z9M_&lsQ`0UE&;5r#2zgBJ6$tgSxbHjFC?HRkEMEl@y{DA&pL=4H283ts;rBEbQ(>s zz6+w2F~;mURY)TAK%B1XVPPZyE)mt=kP_AQr$pfqQ)947+Nw$Mnn;Go8f{cDjbI3G z35HF>0f8qwnxKXo9;*Qx1jIYdKo)}K1ZtM~TxgO1g3p8!2fDAC%wLL6Y(no_GYE_L z596(WRTltsb<=df0W>qSoU@FMD9R%lw1WY_ac2N9mJ#zXFjqfc__Jc{y~T- zx4RaX4&sN;8D}6FgM0Kk#P*dhQ^M?*c1(Tib1uj0Zf1}C^EjLP%I6?$PTp0TdhFN} zttvD$vawV=uwbxa-ac7@soVZ91C+6$&Kr~B4ZIh8pQq3$gmMzMW2+0F6Avx|>nz)qwS~78{jNG(? z?&%1VUVTm2?(Hvmrri(AX4R@evK=>ml}9ahP8}fa5MwNDC^XpOK4a|mz2&&deh8t! zH0>)Q>{rQF8DK9ojH2~K1*?#Gjg>1LkZ0pUNAQUkiv0(xgTRAV9GS8ZqtU~+MrmICj;1Bq!_y6WR;$QO8I=fAWCF-ecA4|548X5=v zxe5~7?&uQiEu^QSL@#f(a!&>UDmt(W<&zM*x|#cV6o&BPBM9OJREII;@nsocc_q1bH!vk$qhf7Lch)EHs*bzUBC`Pkj-pj8WnKNgZ*g6ihU&b1UTwe@l<)@PpV9A{+zaPX=H z&$B(J<9?<0?55;hjc72?s0U;zj#f+1p%{i&uS%VLRpT$V&3_rRvc2OSre3+J4LoJ$ zli0@vTz)}|-C4cn6kr#R4YARXDJiRQs|l9mG|kufu^HcN^v*W`ePzejO!I3@9KH2a zec5?7nUcxtWY*YTD;57FCC@r#uEq8;Ze+wCzPaD$V+ zt@TBK`i4txCQm4VSm3cD)0jZD1rxdmMgY@iNepa32lEPv`*4~3 z6{F6=c+!%J&B-Lku<v+~?FAq% z?oPz(6IxK~!9U1;#@b=-jq4~Pjf~jl!0wkWnz8ka?U`tlYfOYoedB0WP1+A!$(Ee> z(#Jbo%*De~?9s3c08U3UiU&vj0Tr4_=MP*IGyyslm^fU^{OdVvPnP zw(f*rtNphGop;{&pEsUx8&6+eP%f{H73O00WVPfBymkBPL~&WqFzG6zxkySNC_;?Z|34Nt$cM~*T>H)En#fA)`@q9_Z)35Jn9|wqu+q>gc zY`&O}HvOUZiasV#*{vX3R!{`eWR#pDgA2>!8eAbL$jV4#1wY76Z%a3dsGDWx1YFcN z{nl(p@J(ICUm=%5a3HLUSng=jrf_4XI>jk9JE9lC4h!f4=PA)^b(%(};gxd3({yK| zNW>5k%KoiO4YD59DGC%MyB>Byf+I!7lWm=fBN0v471+=c?$LFvyIhBSiDg&~2n5BV z!dQ`TgZ^)FlLd<}JdL7upjiQ?B*TjVIk9gh6U5)}T7v(26==GA2=HOyy)`qPASnVd zlkvMb(rmFBWEC|N7Ps(G4I=JdJ<^^wzATV^FPg$!ZPR|)oqxf}u3MejDW5OZpB@jm z{0}$U(Kugoqu*ODPcBE3*;(=_DQkGYJT9ZiD_o;#hLU#~g8H@+$N8vERGR5$m50eG zlnoLxj8_Ya0|OK+-c(Ug#yTasQA_snXui@ee+Q2HCsiet0wV@4;SV_v-QBFSzjLx> ze{xmWIBT4E%Dnb4%{oQ+EZa*XA(Q?stf|z3Oz9TJyJldz1{gN@!E;I+;)M;whsk*T zGwX_Z&>#G@uQ?`IrFuT}=;;{QtK(tWjw{$ek@f>*ZkPdXJ}xf$tlN(Ik*=igF1y;; znE57(p_@2sF0WW}D!~V~%xWKT`V51yT)~pO{3gXQ%7Gj$9%3?0TgJVJe}74Ill6BX za6ruzax0Uo`kDE3@-fNdWJ@Cip+$y*4X##%dS+t$c&Gim3ecEsAx{+DuZ4gfg zQFVnhG_}!U`BGt9IF^`vT|;P23itlUSd0681XU)%<m#uO=z0 zENEQfeBvK5DF6g4@zp83Ppx$(EU-P<7Si_clG&#P&z@8@UlSHb7s@;XNDdZ7Mu*>ZMAe#!kv zWR%S=%iX+}4#T_`o{{jDuQ*Wd{3M}=_CpK-^@wOxYd$qI+;Av68KnDf8@p(|s1ZGQ zJ&c9=i<7*;bFKPbIQCa$Q|m22BWHpUQlLOM5H5ib5CNg$PB1gzy&^(X70NpcgnU@K zA|=A0S_Y~P@VLuWJ@DKwkUy%;eL?!kYD%ktR-4GQ*#0m7uV0*^2JQioqgK6rLMzTt)Lv0)B1Lo9u$HCk7GoA8Q32Q31B;4OhfO)_r;#Ur8M$r5upwN|roq|?N zy71SKx{T1x(?%V^-YN+68VVgQnrGw&8@e6asKzT&yTH- zvueDKr{M*gwej>ui_|m;kmlnWSzG(GzYtu(v7!}8N(bj`NL~vG8UyJ&hpmMrPD*-ngyp1lX`}O3J+O`e5V(g&lZHcen~`H%_wJdSuN%ka*cwF; z^FPq1hYidqD3gztN^GN);!Kf==dCGCS9v7&A^K|n^tWgm6<&KW*LAem(xh$hM4jAW z>d*pVuNb95bPju!u^prCfQFamu?)FTlTbpP7<)|p3Rq44R38UO)0*m}Z>+znC)rSs zq^mi^g*Z5jrI|pU2-x5my`(GYyR4VbnCk-aMi#-_Iz~F#50fxXO+B+PoA{&+()p5S ztQsfhUgQ+!ixHy?{$d1j*uE0HcMy0~TX$+1)vl`y}xM6xdfU9)&Q zV1UUYXN9)(l5L$5&0wYHe23oVMZmi(bU+nB!w8T?jW(pZ?Lu^k0*w+*nDw8xQf~WT345 z9WD1;?Qwc`-t7W)nfNj#2rDgCI_MX9{K=htZ#1JblsSbL|IYRc&rV=F<3By*l}9%` zmi(G$%*JU@H39s5`#SbIjUqqsU`RG6XpWf>;q+xeI0*+4WD= zCdjdN+92&I9=C{YmqJtHacMa0gI(P%&x(l(Kdj zC4@E&1}xJRngWaJVzZ6q1;fj~NNLlC^!D?almG!n;=+sxTUw4uDW&2<-a!jr^K2m! z0Ij6$63sPL@mAr-U$vc8gU4rg_RC(kSa&mYxHK$8-A?EJpgSqi{jV8nY3J}-m^Eg= z*mh%FS-0vc!j3TI^Tzr(>FC^#u<$9eQXtKWN29aG82`rhc9es(2=(-Ar!NbOciYJ1 zfCT8Sr_DMOg0J8*#%TXO!BfI{ag-IyhwX5|h|00vhyXGc#yb3qj~?PTAmjhAlpRJQ z$Z5ItRlV?SzcBSu%x4e>WMsKILkkRskFh8bgTdPZP5Hnd6?WGzmWkKg+&C6GO5$MORzvy!PUIElTUXz2|UyP2u9Z@9+Fs9hsfYEU|-umtE~?$qj}U zkbF`^3BY!|?dk;+n^DHuJwnZxK~@(5+skWLIzPXyrhXvBV0uiMF9<4#8D6{L*jry4JOU?ResGqXJ2zS(TLGbr$}41QU;V#=dL4W z>s0N~ygII6%~`d#t2D05Vo;n}tdlb8^kKWic?Rw;BWFT}?lS@8oHmA~7k7ph18{5G zoeeIZJYV@j?)eAe2;;h;Df}kMuCM-6>3@1#MK80P9r(5_%g=~PP@BSaPcEh^r5y?8 zB*OfNR`L-cSw?eKmc#<1Wz;x`Txwt*HjjZV?Vmu8Uoiz<*JJv6ZO?u9zMBQbcwYoD zIB%y<xt)V@ND&{U3m?25^E)Hdp^DQ(d=4H}N?3&^0UeIoG1}dC^ zSkpG$Wr=-|_VI1W>oiO!RcjC!86v$-9$6k0=RI{^=mmn-@f9?T$^UMR{aDqkINde= zH2|Wl_Cqp*dB?ZJNzOAVV{G^vg5@-bQz+~CG1gMYI@mmOYj%4ww9Scnpw_{vbYl!{tt6&D?52&^+b0a1t0UxkMl7Qlpt)wf98 zXV()Ak}Ikc3s9X{pI=V?$&lgT(zn;4$e3+B0jpOjBv4hFE+ zXo4~+>33+Ov43a7q{RN;L%L#zOvt{#774b8(iSm#-aq&xMI!Y-V^wOSu9YRoR2p@} zz@Tz@_CM240%ve&8%_Ng!xTqKitUP7mh^q!NA*7+^m9Bvzlg)FUC)C{e7|=)UpE=_ z^>dn}rN<-HF8ms-wL}C_Hjr2T&LGM_ub~3F6CRJ}Taf?)e*bXxi&afP2n;>$7KBnS zYUl>xOSmipC5G+vs)$5qcx{vlcWiG0Rsh!gQ_ zCi^n`S$1HsVTj2WZ+-Y4vpW;1f4EIM*#vzS5CW zj%K^UxT~Dal0q`k-rv~?zvr}_H0G&Rk1x8y9@>&6O{fm`gj+Lvji&U83liLkCfS@V zu7xiBZ&&D*Pq-1Ny{qH@_)FPh|GQsGr_+QLd;&!>!kFH9YElqw6yB5n92K)dL?!x= zr|d3rsj9@%5EU4}x%dZr^V=-uSDoEfC!pHvfcl8pbIjyuv=jKEqBp zuz=qU=`qErKoQ>k85^GRlUgRjZ(1kO2wy63qkxPyOx>2%QG%S}_!e^!dAhWd7+iAZ zva)nefq*5Ad-feR-3*dNUzB~zzg+KuJN50DzyIVv_L4O1^ckm`w^^MtSP+qjEUAs! zOb7xwDPE+~kjmEkG1>G)(Ax|J+9~`&oH^-j860Y8)l-eh_Y=eyQ~ zyaMaY(c}=A7|?g9g5)iFk~8}X`qC=G+IlvMR8upo<1(FpMH1|HWGUu%xkrCB0;t-W zx0In2uUZ{Jlx9Uw^M`owvHaRF-F)EI#e4+7DQ2IJNf{d3z3ZLW*;g8BmNf)zsi#T9 z9QuFjn#Qh>ppI`v^nH8PnI*n^&x2m*CY!PTd2{k{_*aZ>Npoo(=|^5;{1P`!J>ywd zFclLE1SppiddPPl?qDF6uh$9R=k1Weth8XcU@c9BDZ=!lBU(F%lVas~sGdN0;cG$vy>-ks5yh!R*v{u~UgcdH7% zz|A;|T@~GgE~@sBZ=zBbGcYiFwFQ0mLKU%DY>ISa@g4pi!JmBkA!#)EKDce-{{k8V6n z#~1!JOck1=RX)Co{SCINy3qfRBeS{{ZxnG4m=>12fKu6*5T`}4!&3AHsr%b3w*WTm zc>(`D;Y0`iN8a-a5HWwQUHu+iTmj&9KfV9DoWIJP&B<1+I8s_zmP#7ijzXV(KN6=% zZs8n7G;O>_X=<-+k|D@}w+#R22buof1%4zepJyV$Y1evf-Ep~)?|P2nYvMXXL%uPlHO&jsUqNs|5>vhy-! z$qgh-zm-svdLVP8ZPf=;YrNMrSp+lI8!3b zfBc*B+V6T>`h08IBbW1HM`R;QKpS6otagX2o1(XIp>mB%ggQ`8E9o>l5=ZqFyS?4B zHV`{XRqmIsWZ~i?spbqA%~dXrcso=9r`#O~?3IOO(Dy~Dzvt0;IP{IUG`;E31L*mz78C8u{HcsfvEUBtM5RXNuOH)t^u75&a<=X_ zbDp!fJ6|5;`R}hm)#ZyXB1>#iJ;Q-jN%ToXtZK1h6)iPi()Aa;zkkNZNa%rH*s53_>&M{>H81`n9_Q;3k%VR^oW>|urPmxnE zeK51%L+B0O-T4Ees`{9iu`Ij2m7F6&U|^My)KC7As@a4}i-NMX9mNOj7;)pYl4#at zzZ1$XmI5$SgwlVfp|C5&>5}YG*$BCw zA&qCAN@jpshzq>C)~AW^L3tEJ709U68ks~Y!%gPZ@$+`MYk6~!wTYv*HLZe2avs-A zA{A)~Gmx+JUI47^vnF+r@*I091PyA)MT>+tsYnvc&yxV`J-wO_;&O4uK)sc|3fV#H zhH-YU^)CT$rPGhi1rH+DrNjEmay<04Ynoi-d#olkNPb74(2Sx=`|m;Ue`MvgAWEC5 z!1oQjskbDde?_l4A0B-LV^W1DAq0?oY^k?t}BxikH6xjqvGon$ysCu zO0?pi29GoX6-yVRFyzSjjc7}rx}jUmE8>6Bo_oEmJB2|BbEs*vY;Cil%0hKAv|6V# zIo=rDbgzUwM>f7;D|bF%cTt>0PGWsv_lSOj-rixTQ7_gN+#rx06Y}f=Dw+$MLDC=@ zSkjLdY}*r+n!mFGtpogK@6>?M2S5|i;y|||&>Y#=6e4<1?K~7Uax$ZGTZ`IpT*4Yv zGoSj{V#GWK%hD?ll)J+EFMxKg<05ECbcQLJjFpcu+U`ap{KDUtQp9sK*gG!I4GPS)w@~_+-{`T6>+43G*@_pa= zf!|({=h`7CJPtVn-3`eBBZ9=Uc)Fcfs?MfT#A^-=XrcBZfg}SXVxV+M7g`PLPCUl+ zdmo*B+wn2WdNmgKN8=f?GKHb(8b<--1fHykDh87ei3P$o$?Jb$W!KNj{z9~`G zV@0A-@WuST4S5g>I+ke3pH(mtEkBKp8o{d2J-yJ)HiYmv4sR$8vN{*ecI?cER(W-% zTaJXcfO4o^4eeL|h*}vReVZ0V zFfn1EDYcm#PR2r8MKt(2d=2zN=mi(iMXgi&4Hc{9aT_o3Q$M*?2J|ZerS=B4BI^KW z;e(2g@1|JlJDUA~C_dFl@w3W_Lnwfs#z@xRCSW$dL2`DPL47?w9T^qATILux*+D-S z1Wl?Znte)fSH1myq0Ln`+xyqu_PxAQj8L*i@tByV2kdKE%dO>Q zv0?=Ygq+r*`_P!eo#hDFWgtYARAGhadUu;Y{2J)7unU{!8+ zUHvCnQ-Vz%aMCO;K}itFhO+KZ!RO-4wvV>l!?$U7#;x>;r;Tbql>OgWzoxD_Xk|Yw zpw@^~#aY!7|0FCJB^-~Or#fd)QS#Z{%O|J+_Iw#;%z97R-`l7Ez|7aGoC4Re(p8DKe3 zIdIOQ60U1!t1j~P?WPlU!CwhRr~l>WD{ z0qI2ARMci2n~&l({01^`h@!_02~b=A2FBO^4;K`VXWe^cCn;9Hgi$wLLp7pRX?rUH zP4TN#bf8C%B%^=XmGI4Gt?}aME~;~nH-^vkvZe3U?6ViD{wtHo?)lq4Y}a-WNYDE_ zn&0;+>$mrI%@CfhWeOQC>--~*swrexdEC};i5P`|SkBxL6NH$d^X!Hmi@-Q2bn7*d z#YjmiVHCxPoWl+oB%$S70-_zcUx{cRc4}p%(NqL~=$PG$aOUQ-_4v}H(I~RL_;H#w z6=s(bX(bu?V300%WFpZ8hh;xy=M7d{yCc_;7ZO++?PNqXjZSV)CCL6P2dscB`8X13 zn!9##ZFFHDphk6|C@c}0@(ef>xIigGGm5$NbF6NAvA5R&Cj&c7#j}&WXlc?t zOd6Wxpg#sG)gn;xBdF}if3_qhN!wD~!w#v$V!ApTq8d+pjwZ0xXC@alruLQFM=_6{ zuexk2o?$hb!(h=*-L{JUjt6h~+o)ae|7#fAn??O-sp*+k@>UEA21}TW@J#UBZk&DH z%ketT(E4(C%QMi*QgMPb~ko$KCl6^~C%5uVBvAeKxT9!jr&e(PXYDF)#s~;@~s(BDl_v#U7LF%0TIU z05xHyL_?|bM2-PL#Gq*Rc<=@(GdY(4-A4u5{0Wqi`v0i#6!Zc{kxZRiAhl-W__?6)K)Nn_-7Y`=rV_y_l z&|S-n$Li6WZM-ypz%WVL>mPH`YWO)NUiMo>M#&1kj84FtI9C_ZSWOVP#fz z+cz7N3`#oG+1F)$^`GxeK~x1+#7u9q(HX+@PNtq#cK%?gb^`{&G?Me7|DkjK(BG$(+HO7+b*UM@=?va&0 zx2S1IIl6m0%6b*%C>q|gAwGW7zj43x3q>(J*WH5$o{v7;pAN@%dQM*^jmy?EIr-j3 zKe3L$^qu9gV)iYsyZ!Ih(9Y*f+~@r2vgJGlI!MuqXP;Ck4o;Q~delAw>h)zfOk0G;a_0f8e6u)3M@sXeR*dhu zn-*8&Hk~lz+@~11A{kH0lV%5V7l2FD&^XA0MT6}!QTOH`40gW{a_hFAIz!; z32YCe1HjLDdW>N;znM<$IWj}Ja8jV-T9jRzE#tN-`i?pz3qj`g?=?B;*~sd6CP^~I zGNdG>=v^$4for@9Ewbq26vf!JwRYvbU*@=fTW>vdb-k%p`ySL7U+Hjr{bShjIG&Jx zeY(m4eNBPK;b7o)K1T2>IJ@#X;zswq`SUrM<8`|t?)wrpg!h?nhQ8Q1p3DW27lFu9 zHF`AngG893Ua9R3HNrTawbWBG41bl^u*bhMlSWMRpNrB%q#l&Q!(>%2S(ng13C#vJ z+Nx0THBeCC8-|`szXBy4w1yRTa>0f_JweE3+niLm?CRr z8ZyhMzJgR9pj?x{??4`75^*(|X z!i%Mz10~}@(fo^7yV3}QCO~VCB!U>;NQo(dsXkCv^GSRqo(tYJGuYh+rJU% zX?u{1w7f)zQ`xgRVIWy0=y3iLDY>16NHxq6oO7x2V1i&xw$>@gUI|)KTHeJVyrigz zW1`#YsOICoIOMIRS#qp19+b2LI^F9ph^nvpye?tWEgiEs1wb7+Yd|n->;JSp2jq;NLm8}zN#tz+ad`K9et*Jz_T+uJ?62N&n#6xMoAN%*-*L3{HqQ-IQIsei z0|Jz+?>&?OXT8>PSB?7|we{g|9*y7CdBu*l;RRs^#%3s;WTl%BiKRI@mOt?z2{z!R z2r?Hbl3bEWxe~DOPYg8*e9FY31IbwpB*8V5%O9Zr37dI@>U?&V8cZsJ_ZW0oV5F2hZg#|2-g@+p#8St;kxU9#%V)oyK28)* z$`jB#>{!4~SgE#OFiL;6bnFo~NdscK%Om#FRHesR$qz$xmP#FzTm7E3r7u=Oirl-t z!K~&@*+h=$#ZFHIV++&dNI)tH)cCB9q-JDB!89sRoI1;ZVP3*&Eweqcbl538^6zK01o?x>uf!wvmL`Y8xOK4hTI~s9{H`wh~In{-^ zzc({!TnT&hEMFpe47woJhwdoowCV8QC<5>W3AxN31M(MGSUV=-jh*B@YxqQqNpk83 z!jU1t$`=AAQ+Cl(-eGNNXP`wJLqR3QDby5*CTd`kq+ zzexH9YT0kC)gmb9VeARTMX%8N3E~v2GsVuEjIVm|2S(Z3G0U-?f}&iU4Wlu{2UJz~ z6FA~pYR5^`Xt0?Pht-hHiGieECyNr*UhS!2Pd!DV-qVf3E#!>LEURi?Oog=A%jzw> zTL?j!_Ci$l*QxY-W@chz$N3VPb-dga9)HWN(I~6qD_>L}%H=axbjH1?6;Nf}dq|hd zg_V?vQgpB>ZQ_TdEdVDSL`wDA-YcHH)1ZNzm#OFFGm7(&Sp$*d1Lb%FX=H;ko<`%6 zqZAWsYI$ap2N980>Mt`zowI1$YqI3l6R5=u89||2T!74*F*$MIsI&dAVD=|j@j&B3 zVQfqmx?9wXT{lb7(?ahdue!K84rDXije!N$kAfLXT3qlNUz2&xh6|w^!;FoMdA+Te zqZjv$U{7Z9zk}YbSnsW;L6ftiMJRVYN;)8es;f1K3MU!esfFiE0v^Dqo+Igj0&M!4zz*$_9((jv<)3UZO`&l8eoJgdlLkW5eT}*uozuq3+*8 zUl`NRy$K^g+Xj(l%R)UdR=XT~$X?Df+Z($M5v%dnRTl3nAme3lh4HYaH3aU@o>UOB z;-5Vxe}9Gshe|EQ2;FF>oVo!t(#;%oTJfS^5{WD zId=(+ zK|(zE7m9KBl+xfC?Yw1=ZaSh1`3xJeE(+E;HR@GRxN-UKFa5l{gHW}NLA5OK`A#uj zvz1G7ltmFh5IVl0s7-AMwbvvnJ(DBV1KO+2!RCw@GzEPxO0U$egRMx1YW5FPX^YN6 zmVb)d?qH-%Pnqf~S83?rgJ1epeY8)c`+#u)%K;x5x@RiPaK;?kWTGt^_>TJA{|D?q z6TiDqEeueh($#+QL^MkS)MAmH6BUVUSsfu7SjG&c{KplVw0BqT@2+O9cyClyAVaT9 zQR*3GNoF3c&x70oA_7GOj>8fp5!Ro61dHPz!)(44aX6DI512F#wMCus$^`2{UfM;- z_e-SX@~r%;A-8ViICbJ)Ys-RHiE8;e zX}NkSJ?kRWR)Q2N5~?!S77+W8vD!!%KL8CYBWEI*72E3IctP5VqEgoSIT)q7Wn3gh zPK^AfPyHYf8axMXNndL%t5~NDxfV*F46ajj`b6Jk#Vx5S)~?WGqT1T6!iWR7KUd#xjrF9A7h;(bZROf4ma$psvb-G5Ny&Kf$rQt=n#3ksxd@nlXE+4hikUDEs)=C!FYM&h+*=2boqs#!}aEwxq^H_i4qloJ#C@f#a zG4}dhU*xIjKC2eMRzgKM&+86j*<9XT`Q1^GQO=Qa%R5ePFp$Mb;w%}iTK5qZ%^k;1 zLLCkV{P-4!OkEJXne}=_v-;REL70dkwI_?l-he#o-jv`!WC4sv8 zH#skY%y5u$C+IU#8djFN979-|zI^SmUemRSGOMFH#{RHq~eQ4Nt{@o(YTD4Dsv-RHhsHKW z?E;rAA?Zv;*2(~~C81^EU}34#d-iG@5NairNLd(kWyc)aTQrAzD)N$8HLqM7<+8b0 zo=y0JRwi_~8yqqb&agtm#8d`T$2LsJDAEuoes!pTy0%^V(WMe$(a zE}E}UaqeV*I#7sMTsV%|p4VW zmGGv_#;c&%jaV#7C~halBMDb#Yr!vbx+wVB^?0lciE5Q^oLzVILYF3g`ozmpsv;oO zJl3Bx7nEKVdD*O7E61*sdQY;u)&hbU!*cNls{KM%YPF=)p;=tcUAite<_}O-rIMt{ z-Q=IPM&Di$JknO)m;k`eP1^3PAcpKM}oB=Ta-n zBC7_bv_7E#U5YiKJ`15LMgNFH?Hg%YHB)kd)W$Yx!E;!9r^+l)&K_*ZPU>=5`Lr7Z zo8=3IRb7z=CXtF#?mV)YFsRmHh7bZaE}X`et6z_6UjLI=T=O!-)eBgO1mX_N-9SK- z8Gf|9wneA3b1R%orZj00#7^))qiFX zcGZy+Q=&>b$*$^q>MT()0Bz_q+ty4)t2j7SM=P4x`Y68>c+!F@Um>93p$!PNHi3SpzN~Fiq_#O8f6Bxvd7&>q~gZ0OL2edc=?q&dDSr{NL`6m85 z@s88uut8L-=cc6JT7;QsFPV|G$vo9Fv;1A^W6vR+{33eB;c$Gd!7ZW=$5$M80k{br zb@>V_6``Shvery;I<@#M&hbnNVp=V4**7ZXcD$@31?9YTc|uLSB!X%R)EgVe>Xc(h zvK6Vas+s^yiS#IDl+uy7ryHHN2A0Rl{!tbiK8w5Cc)W{Z8=`8Y>tDo1Ss85GP|Pxf z2oA);-8p#(4!Y{Fhb@jI6uiLV%qeWS@q4iATmLcE_T7wl;T%>*D{4#!M#ycp^1MG8 zI7jWdHS9rHK++)-oB?WBrk(Fp*#vbU zqAaU%@Y~w3v4&18V#qzZd49016zgDTKt5818@~_f;SH&lmK)cp+Ef;)*g8m!hAU|)X0q)L%wApEYGzDxwa!I zHK4pc%Mn5Onbwe(-WxVyy=)@psF@DU`lNaQk(rj4mg3Ist9bh9maH5O$CU)Ph&mjX z0WW;v3whV>UHy1ugVlhi)}o6gsW!bBDyvt%Cl8{a^25YBM88bbQaBs?G_@f^*DYCM z7{(>i?19P(pxT#-tO&}KDY9*;la@(kA+p?hQ~-;OPt+uP*GfV=ZI!5MM?!H_kBRp4 zpx6#gOTET9;wD!Qq;>;cm?7{wHZGjUwio;$c7Mk|!G)`C#@gxy%xDHiK;!@>LZmRf z?j?z`5mIwSsog0?9kpu<$z@Yp>!=<-$u(GYZW&9dKc~zkId{_*mBP5)+Fm2&Gaz%W zGbA*-T^HAyV^(Gjci73swKB@|-*lOLF3FpVSarfY=w(ljw@STgKZVkU`kE!(Ms_JH zGCyc|3M82|%E^71y+TN34YeQrIiQk}*`k`$mA&FD7Eq+1?K=RYAL_6xJAwj%=mIt# z`2ZHDKZkC<0&uj&+og+oU^LbUrXkgE%E;nlBsVHnJL#uYPP+~>GqDO?1 zM^b3We3wP08)P05^-Kz7QKrc#JW?@E&zR;#9TYUtT&t&Xxh#asf+?x3e^y$RzCNC$ zV|baBK8yZAw$uoL3zkDAbM-vRIUIuchcE~A=MY!du;ayl1zW%UA7XXqjffj(v4Rf7 z0X@dRM1r}5v82#+?-tBS>b`2|k$5IlO}3}1+A`RrRW9VhZ$e3qHXcUR``W(6UQ$*L<8DJ^YR?acBZP>3Lk z=$7Wd+Hsuw{I39AH|*I?o)6Uq(cg=M;E)TYB*}@y>HahBf1AV?;Sxh8<Sjp!|S5 zMndHyoo-2UA%f&OCAF-RSn(dYE05i?+5jnwEB~uNBJ@^0}+plaEji zUDSLyNsqZ4(NM{1x{f5pER{j#(nY9LH#Rwl4#;(=7+ogbGEWB3&FA2avskPL>^ks6 zSo)?P#)bJ#gtc>6q8S1h3j{<-PI9&_?VPK?LZwXE3Na>RqfIXr2l1iCdZD033FcEI zs5I<+vucUWF0G6dj^0J4u{P;GR-lGi-TWPhg!;${@8*NTImf zNSdAC_GqdQ}bMk$Bl?y;5QXaID>g^BYLL=B9~U94&^@ zN=j)V!?cZn+Wl6%v{YcN#@i*&7EV@U)=m=#)y()=JZLVsVi7ql0FER`?rBBydbOG9` zxkP*^iKcD&0Ee?lcfLlZzj;}m;&z?Q}#PMg&j+P+9K?MVl z?r`?l2&->3h(}q$C!vg_ARc7}T(6JU`BYcf5WsT0K>olfuuHB>u^=d`glML;c8WF; zn57dksbD+BKEMzmohVl_1)7nhAz64Rx1$}*LtDUngL`25V07QQ}s>ttRsb!dXHr5n=)CBd5iayeQ)Z(JT`!t%_{XN>+6m8 zZdy7DW4j4+a2K(5|A(>q*n82f>_X&TEJw-lK%@dni~P0cLjA5Tax9Hh0ip8z5ceG<@iHW)_|Q@m zvx})z24S_MO=@9rNoIoKahB9@Ae!SUdbP`qr2@2x`e8VAW>o6B;zE`Arx4Gaqfj6f zQ6OES*owJn<(efRU27M=-)XV6px*xZ8)l-}*lSum1BmAD0m} z*3gC7$dtxNry`h3%8YGI8pqBSnmyf)YRjckiZ_3jWdA|wr1Qu<_xp2qO;g_yLnfr@+S!e*{_>qX!d)n zVWC;8iek(Sxm^^ZZTi`IhjM=Hr0w9c(fLhbh@~9MsvCG*j1lhnjoy>nzbS1_Ukbpv zs@m45>Z)ABw4b|h0I*2=0)|lp>=NY#qVr}>Rn@===%k> ztgOVf-RCd;kB!6O_?m?iQHSFz3kEQu81|cxe2Q z0fTY@jflDz`-{)4_oOLRrP?cmN(EG&h+K5CWJ`Mh-tTeEuYQFh;ysUUz z`GT;L*cKrv_}p;sl`>L$dN?Wn1k?YN7BeydWJE+WyaeeqLo^Xmv3)tS zk5(XTb?iopqFjtqrA>-Z>t&eqp-?kjscL4oS159QY~Eyka^;e_l}i26jON_l)*zrZ znUV!q?}QkEm`Gq97_Ce}Q7_r%vCdH)bTuI)vC^Oy9ppaRF&Z{U@DjlDHC#CMUaUXy z+n8_N4#af?yD@uoUCj$5sk14KG?}y!0yno)5^M90V{IXB?b9DAuX?6#JLOf7G9JpF z-6|?J0SH5bD0H}RVf752ti|kbIIcXnYp%obl?UB(&prKZFaD;F_Z$7`5P17|c%|HQ z$cht)Kk8XON&0C^>r4e%ty`pMZM^~v88u2dE0dTZU~Si7qsiBr)1*GC#35`PLJ==6 zz2oIr8pbLQyE5NUH$@wYOEFhXkm*^R4~Q&RQNCb9Rp1Y>Qf5nzNskVo5U_FXIObbl zh^xN!F3^kKg!5}Xx*iDBA@&Roq)DM}o*QaOoIGu|D8GedY2U}( z&lNdKWKb&RwiKOSR@N%hnnFwKSG(R5978;m+D2+Dq~Wb)Llx1n?zoAVRU9C*WINtO zUlbm!+uB&@NQ_c-eID2Kyvx!T{ig{d~&btRK*mZhg_5j0;y|Lo^*?vp=_*}Ovp1Kg*9 zP+n;!41gF_DWsY7jMcHWD&i>fH3Q8Aq-#UIH^f*av51#~WXTtUA+f3%AViL|G+(BN z9(wdM{pY&aej_*>jw=aHL>-Q=JQx5@JoU`lV%C8I7~o(uC2H+U%ZvKuJ4xk=m1dD) z^+h$j;`ui|AP1ndaMax=^;@Yp5BljP>Xw!T3NkuP0Md06iB>Y8l40X1dG}&^FjwE2 zb5i6zl^feViKc{10+YPvOi*|2_E3 z$I)%Q3Vpv`Mjz)aL?zN&J-HB2O(sQsBypcic8O^3y|$yHV)gn@O`{HJT1PNH!W?ftwqWaS^%{i%|!v#%2K)_?$~gY@&qI#O>%O@k$Ii&C1dLvbq~ z++5jM*)`LU3kpQ?*@j!;NSs*oZ=O)ey;n9+H8#0}3fFgNm*6sQgy(g|WX$QZbwO$_ zQy%}NlUjX1Y^vTG@8a1~v>bwhSjlVBC0mdQGHJ`$rZVxXO~X3hA@CNd%B7J}ev_#; zBb^DA?9D}9tmbWgKA*!}#z?78dWp(eOQIuGnC>iZbIPr#jgYY2s~!MC1p2{UJ1i5< zKk)lF|G9sS*~)grxKIOzZAU65H+g#Pa96yOTM3z2chf{3!}F9qUh5 z$!645B5-7^Y?7r$WF;lr=~5^1x;;rLpB6H$lbD-Q;MYq#$?wQuj9vvJV}BI4qn z*ih~um7dXDs{Yj1(P%eQVlrQ;%nh16Zr+0OrLj1!b1+S^hAjJ;%mrnXN zfA)E9R4uvL6?R$?-a_aZHFZnhMy0jYAe;aN**d^aRX^x!0`PNnOG_QW1e=1GYYd! zb&v#4CT$Y2i5{Q0tO96>v?}&jA}UoTt@+uwCKxQWpPHy6tMRO=xXHw1`Kc8$w>j@> zGNZOSH_Di%NM-)PNY0MdazX5AEh|rIza<2js^-*Y6J1yxC+?0zKt?@gH4ampqpB>q zXLk%rG&r{m`?kY;8S(gIIP?2IgE@|a(ILjZlDjqx3u*DB@T??tqteI8&l7Waf~^iy zK=!D;m&KvhzbuFW?itJTIp#~r9>7Hphf#S&)11ux|NY%CllB>^mC znR35nl4ad+RiGP9Bt*_lEjL>p8H%dvp_LpwsTm3;AL}DWsB(R_By^LhOtpVvb10K- zw=qVgt)A+bAlVu{yR;2GEmI8Nn6wmCfokt?q5Xx~mq^M|=~wmV^h_xmVvO5^n`BTj zIoYJKQ$H`xy3KPzda&DNBPz#yP{}B{w6pA!bnKB3zH%sFC93i^$^Dhi%q?S?Rc8uE zVl@?nS4Y`_nZ=sDH`H9w!L|}<=5a-z!kB;0!15g6HJtv~FQb3v2$py3LBCj-k%96? zB4eQ)b@NtdlnmK!mJD2^9I7WFDxsEDtwX9 zhzpq-ZxrQ3{NY#XuL>X{R5W7@Nj$~U5uK{$^;ZAaoXsfVl3B}ZT%|PhdHO&B&_ITw zBn3HuQLzpwJ;`puwOb5PnyE>s5SlqYQs}=@krmxa#?&zo*wD?uaTSXT>)5&fFm`?G z{}<7qEnRQN#&g4!Rzg8sU@`>%7-56V0Lw=Y60P){JHd7uFW|qU;MYDtk=q zH?)p-uRpxMp-Cl zO*O#~hrGA00+)S+;+m}ot1ndAsVZR)5upjS@Hy%=@*GC-JPXT1<;pgmmui*dvQ9v% z*HHB3@z(I#rQviIg!vrcb6BjdV&}{LJa)eEAL8teTd;n96)Ryryrh{x=>a4~TnCI& zVR)2IXr&|0LX;NDV5DdI-%6^|I|-Hi<+_TXp5+-d#!j}NS!G&&mqehoKoLw^YGy*_ zb3WEWHj_(@OL`Lbpe&Ad31`w2+b?zWGs*%)Z3UO4g3Zj^q)`}; zQMr`DFQtwojtU}CuI6(ZwFh|l#_i|ocVQymwYYQ9F^>Bv8EIW8fI-xwTj{Xz>|;1{ z??1zgp9XbHNO5^47emyZT-{D2rLLH`>|5aBcp@Fglzpnz_^1oUfIY|OCt75^XX{kFf4 z)#d9E*H*DiGf<>#dC096C(;lL`HkYty1VTDOrBV5LmsK6ayBTGoM6+1NF=Do|gl_#b&VA@7z^6Wp`OrPTenI~L6g>qWi=d|vq z6yS!us#-V2ZA)k)lPnViyA`!MtW?iPn=DK~(LJq7dAKIjio0K?__={p2QqHOazLZSs$85G$!o+do=;jX~ z^5_a4dvv@&hzo?}IYPIFvmdz|8xQ>=<||teV-(9DBHOC4%HKe;`)VFH+hds@H!fZ( zsO4pFDOXgDlx!n&T-mZrOUp|S?hY5uj<(`Gb(Xj1+7b zkaqn`GPwu=Rc+L^kQ6v47r}jz6Z@a%;yp3KK4sE^QpVBw6r`r>n_xLTm^{|{V$=< zZq@I0)l?JYs?=Y+y113Te7=;xUuEpJ*QJ=`nXAIBUNwObW5n{x61MH!e*FB#(kdRC zF52O6Tsd%ysKaqta1R5ZC!ad~Xy5mbhS_X3Zm=Q|vJohHuK)|8RKprgrtUW(&PYhg zH&l9Unvi5p79bV(s_NodZ3noBi6gt!rW~f2u~3>DO{uV)-uJ)MsvY34Kid~+_7kSx(Kc0)Q-Gc|FH zFq?_$#Gpv~E(MvY$C#bpjm?~m7O44a9#wAmji>F{3&fZOMqoym1c z{L~XqfBfSg|M=RaJn3*azV_in)Zw^1U;y;0{jWZ~W$U)1ODjvW7-LK^R$BWji!Icw zKC{%qE8MuY@>VBdTVY-!Meul?}PtlT6VY*@l zII_IcKwT=?55xdPMP!*v?*SdU`3xK9pGCLwQe6GIAIH*x@5dr;V76FAp!wkPNAe8M znv~I0`QymR`~Iw1f-L)}TQmHtqZScqH{hmxxZysT$diodtZP18jtu5ll-j&USXism zEw*wfc~4FQW^*mIRV5nw-t4U=^KbPBlf{w>o3SIwE@!?e&Q#qb3XPf+4R?ICU?i+i zo6L>6O;Q3>j?zr7=Hj=WuwghS%Fecl{f)thx>=lb;Galt6dr2Z3M@B?I*t=`{`L%NwJ^`Sg zsIkN0xWeE>)ZzHbLePf8bjKYZ@B59#vH5I?Vx&lEN*W^ykySLc#>7eo5WzVVAK>N> zq{3iac=eCn3rfPpOXlBD@MPmH5l&}|%7;eRxg2z(p=>EDg;L~f0VS`jV9AFo)@)>y zByLE64gR4kmilhA3WpBV0sYx0uw}=~arL+V6W~SPiPgnA=8HAVs6&iEnl3C!FlwVy=Dm^2jZ!- z5-SLbG8Qh)q7RcxZy|8yY)eHk$7)!DZTq1zr)7c5cS{9xgpqtS2bU7EFY~&zK}$}1 zW280@KPl9g^jc7pH&awrS6Z~xj%BWL8W>b5lK^yx=r{r)qT9BNwTC~6V~KQMf(JJc}eqx#Ly~E|pvO#DYe1?oYAcl4>~O zI#4&8pV+dr^2F47mc!w=^58_&;kax#z<1qsq`z?f{6}`|*l})H{jL?sgtDtKtEB11 zML=z>MkSH-g~Z-Nv{ks4i=o7*8K?b4q&{2K(ONcizKVETuUS>8h7ls`)TA#|A2%0b z3Rl+9Q26&D|Bt(R4fhoqI ziF&mzw>ZTY1tVD2J=V;Ys$?ax87j>YBh!g;S=^2J~N>Ei25mRklHmi|pakV#5BoZ$9mDuDBdrqn%f~?b86w|1# zpWvA+&rqqJ-=wi)M)hlt<;cm^Dt5f-u{<4e`liIQjm6 zh+&`oQSQMAjb+-ILlJ=)`+s4-s4nnk1dVUWz1mauwZ zar7*fo_-FMa5x-aGHV%P>17szr+y z4vHLA3PN>0<`^)8Rd5RJx>{D%OMDF?tpI7gG6j(#p|hmG97X|7Zge8hNDisO+5Vv{ zI+)lZYYev8QozX~AdF3wRY)AApvk3thI2n;*R?FK(Rq5@@5dYm5*pU9e)bf$-tb-6 z^@bnA>eVksJa-x^SOL&seLRyiDz{Ur+h{FXT-C57qtb=RHj>R}Rz^8nR?X3a#tg1s2L-jEbzQ2~-wD zii`O8T<#kL8u*`pPfl3goG|p3d^E10rS=F-MO+0KBpl>rXy{)9?RBh$r5U`N}TDeqGp3^|}g+_imE+C|@QbQpci* z_i5QmD#kpCxn*SD`a4isQ-e7LxRURd^bX_ZJR~{3Wn=A;GxWb9cb8Jx3>f2;p+clfm7A8{|!h4BEFOzgI2(;b*hA)Ff zC3G`@BG%5F#*Ul62m9XePqDV=rPw%o4s%`t_dwqRJp%f)IarhwfO6iVY}ZsfO`4`C z$o$xc8>=9j2}3R-RQF)gWa2cLt1e=~q{}5ixsZxb3_z4)uSvb%);(X-LM&v)JkYFW zT{evryuE` zDItrr<1R0pjBPTV#k>6*oywi2k`9g`~$2#{cg;b zcYyna9P#5gix93Lb-SC>!A|tXlb0}mn%4A*@lkbUiUhrBRq7YTNT2JqoWT{Fmgmc- z*XQxqaqOMz&+X0Ya5%n};Y8HoxU4vg?|ADq7k2F0d1TAhElaWIo`^P4o#dGZ((X`O z9e6y5y;!qGHH!!olFBtIvEq!pL6746i1b#JI%tl7Nm+Lj>BdBnMIfN06*kmv#qwG= z%)PNN$^lFaJ7bhUE)f2@4Z@`Nd&gf-W05kGO>tJnlB z_XJM`!Vwi5eg%N+o?b3xmg7S77*mK0>62aNIhWt9ajVq+aq)r(fdYs^^_Z-8MgD{w z2*`4f?P|SdAPHys+;|@(!mKlnBMCYB2))jKu6 zd}X)T@h3YW4HJDo?F*GsZ1$4aKUy-4Lw?qszuLz=Xyzia|Adx4K18vZc0@vv#s>EA$~)ULc5-!xOf%4O1b9(nlq(mb3i2$ z!>M$b2;kDFw@6p&lw>qg7Sd(eVbKf}+$2?H-jt(Z#ez(WKs98RF52|^DNdFmRh^*^ zNR>y`+EdDbMM)D70jEe_G*u__OuAV5KaI3a%7SbQ)k4m%783bs3oHZ80c=mIm6gts zh^0kTxR}e=Q3>uPYXDVO%A{(vlr9#arGReBGA>VA`kTk!2S)PIA5Ca z?p@p8`0DHv6mMa5%mqIIwinP1wGD?^VBj|ASw6>1;M63OGg0zVvB^juQZETtQQ;EBN@TStYC7l#bMZDSM6so$Q2Eyq5E6S; z6H%mclsh-Im84{n&|0A=Y}(TXl@pjtMHwiZNM~>Oe6|gyJo25P%E{78>A=K*Fq?xH zCqe5o?0WfM#ss!04eHA4) zE~(`Bo^G!$DviFfiXhrz_!L+DZy@?1kyLOA;zP2vb`fC{zAR03g?^3wm8@fOojULB zEtO(o@w6#R?#H%!h?Trpj88#C^@5Oj8AxJH;@Ozn79fta5#zBQ4Z~4Er7FV-T8trr zHR3wvTjroS;ryq68Rzc(3B=W7=;nJ6`vnMbtXd+XVlhLuT)tb&5Y8=Db3_ZW9(`gi zr7_X6kmlr#%&B^&WSS{`W8bRHdwfF0wlfPJyjb*$J-hd;-E!+q-}B-3{n`ie3(Dbe zINT!Ya6BJyH2&c~{B38hx_b8~0#P?;HA$h75iX>XE69&EiY1z7G=Wo4zP0H` zFx{NYFiLS<9UmZEwKy-uKZSX>j2HuY?vM_PWmUr* z(WJJb`MeaEe5ve`OLs`=dOIo6f#Zc5q z*0~xZiJ(a3xMp4Xx!kAYkRdr%a|X(i!!o0)?%IV{>FY@3L5zJ-HbeescSdb?6~tBk zSju3!E!T@>Nr?#Q`H*A*GgpO=buO>=&&#p ztFCHA%B{&^nd+1&bvZw`1Q8Mo1frnk9$dX|B1=I3F8!<)VOXXbgBT^^+-L(p<#u_c zh*3&owYl55W9MTJ9R0kzxoOs`z zIRCMqK-WJFn(s#6FF>pfdg2{yWUJQ|YNWwUva{=a>sD_H2I?1^$-d6Jb|p@vm`Rh+ z!com%9Ch>*2tW*7=yvYi`N<#nfkUgK&gs@rhr^NK$G12f&kykNr_a3is;jPg_Tfh! z+0%915I2`V*%g+hdw60g2cON#6s$gWLy%^fGV#JMLAt~R4wmprNvO-d6 zR~lN`mqlsyKU-tVbr=OS8-J6PrL=-YYcz}zL7p)h#z#dygS~JRJVzS!sB|<*Kjb7w_~U76oP+(yK5gun%`VkxkL1|dS&W-) zvmDd1IMzdxLfeyud|GGb#6Z(AE$KKPAyMMSIN?I3a$m6QTP}G+4oKmysh!h{91G(d zlS!(@4H*f7mQ93g^moqUD;6X%5@mnHp=yz%82PFO4R$N2v@)$GE1OAAM)hZ1Kj_rz z3#WN9&iCYkn*~E=TtatwKK{n`OSY(MaoQMVDGzYj=AEqOt)Y&g z9mxRQA!3FFs0S7i*x0rO)B$VvzYpg=`p>cP#DBtUWh+425#z=X%^KqUlh~Tr^NV5~ z^eZCl?j;&0x4_X#JU4u_+H6H$lbvf~~G5N^BvWsjd(JbC{kk3D)vj2sCR zxab+x-a8caW>ktsyU%bmT_z8joM=*aaZsfq>bt@lOk9IfC(VN@mL!kS9xZ9K-*ubw zm2`XsWlIFxGZHngkda>|q(GSr7ehx@1cjnZj4btRKuauK28%U zn#8BPhmT}IVm&}Esi^PuQib{`7AaI2G9!68*V_n#VtkR>5Q`r55MR;XA*Rn5T zkSlk6Sh))U>-8$ChJadc$nqNs3jvrh5Ml(+fUs>F`U`8g@acbzwNL&F;QVK>v}GS6 z2pku==2J%tSxlVXtKDof~qiMHWk7< zye#`ZhCRFXJoD6JPrny!{=2)e-D-ojKELq8G1iHdX29?#;k{XqxsiRF-u2^Gqb-(dBjm>iGst;|u*z|Mh(K=0i&-OvJ*IPVS{L?#ah-2^i;P|+VljacC=SO{fj~XbcMI^2EdT|qJ@g5j{lx!`^@rb% z8O|fDT#XnPQfy-ZQAt*IO;z0{F{ErZOaiovc6rQV6qH(6Go=XYr`^}dG9UHW=-I(y zKWJmY$24Xvf*bPIHDX2|x)65m+V%O_?o*FG{-DUf;cz%c_#RP*JqA_5=|p_9eRZu5qp!WCu>xHZq{CGN(-Z9IZ%384ss6N9NaHJyc0WK`ClQt z`mCdq%4Z50Bz3>yu59}o zxo@SlJ0FcBxT?&fq!Z2Iw!o9(%V`6eZh}$3&9k}PCHfDUkzz?sRR!kJO|@Z8mfss% zHmSzK^iCmwrP~_P;7T4+nzCJGQ71i|-!j5!fz`Y!r36IGxW_C;5H>(tBjV02SiSHxPJZ}q zoP6(pgT;eChxvQ~TG~0D5t?R4EWIlSm>n-f&0$sKSSn7{*x)Ley4zsoQi>DPE!UaF zGdA7CC@lA7XS5132iNv}#I`M4C=k8-zBj({9B?>HR?p#ZT(NMAsKfERz&*pE47|#OzlcD>C^^en69@9u`n6>CAU7K6l$n@sJ`WS#svQhWT zW;ar9fT=Xj&CzbV@{kW@MHt)6#q~Em->l!XrKRn57cg!PBM>8W9Tw+KVaE&p99CZX zeOSc?mKHr`^BH0s3l3uk#H3Xgt8R)KT$4+&7t9b5GA_+9{ibWJkLW3qv7iVqjDt<97ng)R7fb9#2&Of!)(V^^y}ww{62M>3qWmxutXr>k@S3SmpcH`RhzXeVlk zd{j5H<+O2RjL^fRf|<(|M!ceYiZZgMTGa2P;hHU?)uEw@;b!GaMT69KwdATBM)znA z00jaT8<=-{uzc%xfVb|(eDxHTx@ANTU7UX z(jB<0I3(9P%BLD;#Xm(|mHR}CSTu3OXn84|-u6k-NX4T;wxtv>{_ zxyVw*|d`IsW|_!jhWwDf{C0VOp?a)(#zLY&v<+MKOT{Pf&O z*Bas7#FS~|FU~rhS_6``$geCNGHNU_#t=fcW7p14#WRn58n}~yyW(UM91h173n!uu z$MXhvfdIbcnP-3Ns=a$3fAGPFubVG*9C=h*i79J6V((hLs4m>xTHAgU5wYvqGB=S) z%GV$(tzutbwNhSL@<`uT+1m1L8>x}hC_>z-tFBRUa^osZYHD)rqmo$&LtDcP{l*z= z+4E||t6u`?8hM7ff$W0n*$0k2Vq7m69ggP*X!t{YXSn+6#~)dv&%b*%o4uLA>i~5r z$;gGEbVRG%$&1o$7a|=Ci;j%wDX+Jb4zb@+a)|*!kr!$x(z2MOaJA2)lM1`m+FB)idY-krN^fjjMA!}%1c)8WDOi1O=Dw<73z6Wcyro)tmP^B zk=zok7Y0bBEzKxn60jsF-4m0uTJ3>&p7Y{|0U}7qP&=*M@#Z4`mKDRK41n_#ZA2G1V}6#huzV;7<_@g2gX z+q+KjS9rLzV;|yo@puTs;OcXfd~-8abNPF?7htqcAAM! zpJoq56GW&&=s#@LBs*ats^8zNfk?yKRxkI9jTeh70bl+JbN%m0#dv8u40MKG^T^OVI)&gXOmKxJ$p`x8`ALW0`4577^k}d1+@+ zC1O9oQjPb%$BW-7QMI6k6B+aD*qV{~#w(*Yb+%Ta44M|T+HQIV3~k*~85+ueBRc zT+K!XR7yB^wb%3OYA;{uuJ$g&leB2%|2xW1{yWNG@{ycSG1sM=HeX{lTdXh6QH8Eg z*?+vtg}2m5QJF@T%IGP%`HL(0$b^ z{kft!yg!D5K`9*~z0&Vb>N2kJ-mZ`yR;ZXWBRyPJp7(=M$m<&7n1*6t%bq{CRQ*Gspio`J-!2*_(W@~kXcLb-(J~e{@=8;P}9o=2t%yw&4 zXjSG~vNZ6gS<>9GD?U$&o4GAtt;nz!=Fx6RQGtYoYD@(IV+((BK+330AQ$VH$X|xk zM3|7CfgZ5%xoeQQyqXq&iljF?$H~n{>^)A796(Fd4GxXon_5luVV|Lx|?LNz6kGu22PFJIG z4Gh-m?;#xl#EGfQOSJ|d%_``s<7zS+8vv>t2iBcwi*sutEp}OfwQ`Lbz3r2T4}{{_ z-zm_`%9@jK?E4y|>^8JC#%QK%o;d*`nD0M_g#|h6BRK_N>yLm&Fr zOHPXsGCi4d=CJEDG)``{nTcmc1JORk6L(5WVC2AsXffo-SXC&x?w&@Zj|JlLeo=0u zLCGIpsZGH$6tRj!I-Q9i&`E<34v73(Myr(4+xaciS+Rm@tR$w*_&r@xzHIb^0*V(M-*e2;W5+C#gYJv* z;F=B0BnoSMulop-^2%MES$2M;PG+bz_apbj{T%8?J-zUIg)!Cig8o{4-B8Wr<#W$p z$tMID$v>y1nw>kXHuo>XxWv9UciM;K{`PaGv-TyBg~Up>cY%;r@7nJAuwXaQ3OQXk z8F-n4NsAYZK)cwSgtY=p@Y-y`XmJGwlFiIFMKdv7ZAt8T@n-3=4)0Pu>CPe3L2)o% zFbFQ`ZaTU)+{}S)2*nX<@e~N3pM2xn=l1(zIrRDFKoh;ypGlih>+t1@1A8^6;*2v$ zAHAIJw(+uU4;P7cr$6VIruIwl9uq<0JQ|Mr1Ld$~Z+y@iJhqLrX#H{;bc%OJn-wQI zvw|`!`_dMkD>2DJypo)5H01QiFdVs^tomX)Y_Jo~ACN#ufUU6zTbgREpD1aa5lsi$ zSEz`=B0zKPQUVh09q24H%R%;F!_mKqz57N+t`r-CkOCiMkI0dJu&CPA84?Z39$E>9 zulf1ta+v*>^l^toSkDh5mm=ul^VEzRjATRyA_k=-LHR;l{njWK{snT${kge!`FW5E zQ*;)_h*_rV17No%y!dNpe*q^4qXtC1>k9dx3-!Tg`6@(Now|c`mUP6qTxyf~FSKNX z3H<{y++wb1;hck3?#Ta@6R0!U4+y0ER~B7wmw^|rTV1-Ir{R86?3M7_RVqKk)Ud=0 zxadi(sMi)$q-mwTvFg#S#NFjOE34bafhu&q?5~t1@Pp7j64%Y=$c-qmTCWl%3!@Ti zwTQ{^VI>Jujjg^R^0Bpw8Q08+f9{_7%s~9)ruVl$vgRfIjoR|*^Pk6uo>~l;ZqYyF zy?FfyA*V?CXta71@!TTLw>Id^VW94e>r8%$Jswo{MFT~-$*$E0w2!BcB8Lb`( z%{Q=>8Wl2`sx;-H7L^p~-ZCTILq{NEc&`tC4u?PZ4o%Xwf_P|q7qoS)m?!iC%azrE zdihrp%;Sdabc@fzI5pm6(s)MT|AFUj%a?-NA33!?lGt-ZwjBlx^5T;2N8V zOEbA>Aws4D&`E<|LYc+-kz)0sbmFyi_2(`#Vt0bAvIs(njL_XS#g<+5_(go3cubbc-^g8L!bC98es~ zpHGOL-XsKBT0KlXr2w4{6I@(Ic&}8VFC5OC+F~qtt!i^>GL=?K#JDKWG#;CvK$|SX zy^A3yMjNmPok}m~?&A}`kvSQ0NTPZg;dV#5ENzw7oDDb(-c`TNrd)KG)#wfhU&aJ- zGaL6Ybm#vf1}sOGl&Y3x@b8PDPdGQ`avawi+{pE|%~^Ib?541KLIWfSLT+di*2-WX zv5z}w>(uX>8}oRY6Jf%0nvQ_HRSV9(;{LN2m!V(4Rw~3T#L-@5AJRf;@qx`TII&g@ zihkH1tTog{3Hry!)3BoZzIpuyxD`8)LvrAyv_(S32`EFa>8*s zLw>vkb+1VGUn^2%d0}17y&Nj~y}w*D*=n#rd#XbaL#SgsG}jHWyCLjxB1`dPh*VaC zJD*}!{*mU2o&4n@noznB)YUH%O4$YsD_S)7=n4B6aU&=H_f`Yu(6tw!v(awm!XN39 zFY9}xy#5>6h!NWiLe-(Cv0U9VmeXm-;0~?wyB-#FH?QK~o^9S}lx^mCpr>ibf0tjl z9QrxWjgQbRIM(mEI)vpU75=L*bsldE8-=>3oH(K+y|%`FuyJBS@rqg=lk~iJF-CfC zZ5@-Q7#3qhLOzHM9~9*%jAakv zyT_&t_%_&~_xVxNzflh+kJs4L|4Er_-$_UZD5k@MHyW#UYkWB-OW@?3U8qOXvY4du z2bZZUcr3yhzxTVi9Fc@4nj_W@Dd+n6CFSp9o?1?;`NV~_cXXNn1~W9{Qc0lKE9v3a z;%}nC^AhL*E#g7EXZn~-t77j&;Pd>^q!{X@MC9|4c@N^T{gPg2#yS9bHXGzoGnB&A zw7ZV_dlqVc;_rxMwn^dcEDd6}J#RxZq!@69wdp@5tn{yllHNOTR?qL69|mLa2GQDO zogzJa%Fqq%+{_tR!M`~vM#qJMZx_429QZfv95V9gsuYNe*?^}yvodmOep7~jKH4UrmzEb}ZGbhQ(Y#TA|&M75Z^pmpzrce1v_l4N@ zVmXJ8;*P7H*G?b<$b!f6YF=k?D^p<;b%#d ze^837>|rkv#cZA@=IR#DO;rAeUG}e{i>%zw`&xjw2I>TdV${1R;ZzngRXSDxemNyu z>6}|}Qfmb9U#hmx z`}~q(=l8yMA-%^45&aV^g9HZWvm@3MLTb-Z%F?tL;C`ISCyUK%AIL>{M;K%lT~|m0 zVlhj>Gl5aX`}I3&f1ET=is~v6+Y_P63JUW_>DG&5w-<^YNhsbm2a|_p$JFrPK*UQFf6{oO@5e>QCDDkrq8H9|mc1kCZ*h*&Q^whqbzg#J24a$sh(ngm`CTV^+Z zp1;Nlq5S{4`w!uDKvQxG=^Heje#_?G_}O<4O;@4N=T2v3pGD&r)p{2-AOm3TweHp9 zGB+RS>o=JbV=~0QmCiM=PUQGl9k)wq<9KVu%8|B<>|{PH_fb<`dqmvVBmsEOjaljX zrBE4BUIuq{3yN1qN{55tXzp>I{6-qIC-4m+ke)pD%MK3?{t)f3e6lyrN1IT~93{NE z=$-;iPmAz`)LlBbAs5N2*k%9JTUY6N>M&Z{lt7@0Uc5>#}C8X3l1vF*tGDiP#@}P{Nqy+0hy;$bH zWxFe~bKKafU8~DdlA&dhdO5;Sj+HWPso4IpbC$*nA9leGX+w5)Y+6&q1X~z|$-C4W03U(a+EbNs^8Aa% zr?ao_V@rusXbfjN zV_0#>I=RsDQCfREI+C~6exo)?ecEZ#d?hd~e+O1}qA;RNclO}dNm{M1#$r8MbXh)E z;EIJg)>+O;sknoTJy6^1uS%i{7m2h4{-FHIXR(>dI4Y%ZXEj*)lx~QO1SelbJC|~U zYIkAYE^axY?uSemNDB3VSM+mvknyze{w6tf;+ySA+;R#w)wO?uFbloxKJO}CTU_7% z&o4bbDC6cLa-Was+R+Oq!&{409mY>J&T-sPq6U@+uXGw?_LDOyK_PX^i%2Ik1$7Ol z93A}e$LSjm+w)XylWQ7Y>2{YMBEV8To0%_*Yu3Y3O+NZN->?>O1=hW<8Dj-*Z%Ui= zdvyn(?IEmxl-@~2g!$J}|Dg+g0k^)px4n=r4nNOCQ3cz5xU$(@D3xqNLC=)KtABEz zb;a-L(CjNkVyzj{ZH(EMGHd03)#sV%V=U8} zi2G2-@w27)b{f?!EZj$vVRQ~1lz8hS(Y6O+>Diin(z}U}1p~{o704Ar-V9OE`PJ7% zc8U6<;&8ZZhEcLq1_YJFht|?2&Y&-X`6smHlvG3W&Gj>M?I~u|9?!eAm}jz8C6xm4 zys%aLT*bp`jz!ej$$9dKB0GNzu8PQI?v#XeRX^Ki?|@yof`>Wdt>H={aI3>HOd(?c zaT1{ma3Kr>#@_wlxjui{W0s0VlbIa$c zYLCe4?sb<&@a79@Z0i3XL~OlO1yDs;GPWeSW6jfChvY`vkI5`U-L%QCEcdxVSj&X! zL6X*GOpLTd18LR-R?>fvh$o%C17S=0k<1ch`v`)T(M4Q5I9c@5f>4g-RZ5ZkG zTyvrISQzAq+64IvF$XvC3e4f7rrvnEA!I+#_|gufu%x2h3HFF~mcX|icwDkB(+76z z&WzOjF5)^Phyam<$Nc$M34>%u-c9eSE3k)}+??E|ywH#xaD0nwPZx$a8P}?YZwdfe~`Ca-fG&ZKnR4 z!#xMH#H|sd?zlfL)x|!OaIbsM-fz4;Ogf}8Gu8(0c;)MXH7!Qzs*HMfr0N26 zsESnz3B~JM!pC;S2Xu;DvyGiVRn_Tq58BHZI<{Oo02?t#mzJb+y0h09RaFB^3yI7q znKu7SK1!iN1GTulJdP~Dc+5v_CGk`Uf+WJ_hvAkK`YkQbg~E(-9{7!JG}>#Kn!JwW ziQZvTz0c7*DdHo$CN9oQGZh@-=u@5##c%49T0kgOqN_XtYX30;&ahMWc-C0Q25r7s zW~~jNlC&xKLjjEQY(P^u%JDJE;*Iz3GQk4t0(~L*)^Mkyd`JX;b31AURl92lntQ2E z+L>sqs;6Nhbt2pFehI@`glRmN-~kgx{Jbe=sqplbppyra#d$t;CVPWTKCBfm*6PHZ zH)0^}w>SEZpmLeB#+eH9ABp55-fuM_bk zfNMD4xTvS^NCz6#j@p(R6OkC`xhs>&xe$g-S) z?O1OUk60HJxvJ*YnHq;rOs|67$mWHdo4-|1P~HvpGK`A8S{tms5{=Rnq<>)&ZuKKx=udWsJ*X zYWE6gmy!MnA3>w!2&wa!*YIuVyDk4vAs+P582-1~{C-An&5~Pmd~U~`@X(^-u?y;` zZK!W-BnTHV-S^})Wa-pFq!)IHsM76}{#bH&OjTAiuM1^f9JQHNbKY!-SxcP`L`WSt zL0sNrDp!=kp$CNYA?B)^%16pRH9!hiwe+k!=Gt`&@}(A8>c?hyMx9*7D+}>(oh8C^ zT;>|t&0i?AGmMoAi>O2|dDFoa8soPe{C82A<+a;?@U(ei+#*F^==N(*oGM1m;-UQg z526D+!N_Gd)@UwYJ1S8N_anSYGea(AUrR>g&Gwvn8DJI&2I+y(&IGX^2!?V|ELOtHPN~Rup=^p>gPTs+EU%%DU0#%k+sihGkCH>Agb3LdC&hh>7aO}g zGR3MDOQD!R|1&$#rm6nS{9v?qhV=dbrYYz)nLh-!#T-N)E>x&G?KeIe{Lu+W7&X+< zlo8&db7Op3?f(SO2rsZy31o^*iM++n>EG!gevk^>wlBKn-3a1ojM**0{(4rK zBxUxJ(eS|{lc~-&AB`)|imQ@h`5B1t1z*<0nR)DvNncaFQtW$0``9f5iKw2KBd)QN z)XS1F0i!G>NnEy?nHV3&gZUjP;=>=7aKBNlxPC?=jdTg|7w*tthIuz_HMe!*EAq7r z&tx4RBbD ztFE~XBvm>a&uI7~ZC6!C$L_a$Tw#5j8DxUy7evi^4p`x0sD>C+u;&$0gjoHvvR=i}Sv3h>AX!p8UEZgEJp9jlV?z(^Muhb!P3DXX zhtAuD*T-~w{^x)nFEd~Hzr{me&MN<*@yg+MB3=bv;wid|g&w`KS3B&6{QPbk7PB^& zv?9f@ROci=S)WQKq>2!PrcoA}BQlJm(}hl>$3*oxTrt(f+s*HoSZ67?UErpa{9CN4AXT@^7ayWfAQ90N55)3NoZ9A zdzq6{u!TeCT(xAusnZ^!^r{Ds$~px5y*+8JD*-!x4py5`URVZv&_0A@eWA%$9_!+& zVCp=S-z#3LAeK4>0YC&Iq%$Ye&kOjLxBRJ6;@nH+9u$E3E#VR3j$am#IMbhsCiK;DUJBb(+eCZ^Y*!DvYckq~g${mD?V{O+SiNOm zmN(jk@YZo`#(#Q$1+s!sKcdPlMeLw-Ftep}9`uNUsIaSBkU6+{%iWnKzRhPc-Z36G zgpkFmY!cea+S=`3Wgx&6)|TwM9=Q;RPCA>gerv2J;7%8Au+z)g!cZzU%}8>Dh-j^| zI)AP$X{~}UzO1&?h|e5L@BaYQZ!gCkUAY$8%y-6;xh9->e#G7ZbaX}S{5&gi*ri)B z8I`#(-do8iq>pvaUGo)~l;US*C!4o}#xBd(JUl)x}Cc0tN4{`pjRSflq@RqZuF%2k!aR!c_}U4F*zqO84lrBLYv7(JhUuV}-PgJJs-!4I~T8Up7J}${58CEPa!>EHI;1u+MbtIRmJh2sp=(Z zXsy2z4sqe8DKgvLS*_};PH3C;XhyL|pSZ#?md?1k+1)YV_}d=PRqHmoB|O-S04*<+ z95jxk=2$Svm0i(GqpTSr1Z^#3OxWttS3L9T_CUkbXy;X?YVO}1t78HF*3+e~Uq;%^ zjONRvebraJYAYlx_V2N+e;AFkk3QFVBn-O}RaaMTe{5QJhpHlM-e%xqpRZOWZ7eq{ z;-F<4Km0#i;@@L&zujT&$Ej&l0kF*`G|1_7#U@+Fun{f7DNf(o1MjeI%U}O!i>YbsSKlJ1~ zUugKxq$Zl9B|cB>;|lk4x8IL9nCvI`p=^PHbz3fG8G|eAd5GczwR9X?jp@CPI8Evq zp#3qAF<8QkOdF60DC_2qi&v6SA3Io?FwyDzg|!s1GIpj3uqI$n^XP}9l~$Ui>xe|6 zNEzaqb9hzPVi1fb&D*h7i7)YCiE8W+W-+`=5>cYlat|sCD-z%@JrYV5`YJ;F7`Hlp z>vIWim?BTl6wqza!g7JL4*1@>v64Bf<}j8Lv{%I%T4u96oR-}qwFWYa>1Nm&mWd4+ zmnP*d38kbKpLw**kA%NKpP4w{ttd8fmrZu@mnY5>EJK}Hv#zS*)M6*i3>clFant_@ zP;D^MQl&hijyS0le;0jYdu@=<^gyIymUmIk7v-bzD`})9HLRUMbC@B$B(&I=#4D#* zv;FGJ#(geD*X6!w==sm*;dqMe3}R?G`*&vRAKbMZSGrG&kk8ff?)B|PR_Ng>*>Am? zaq>V)H@OzyOm$wrs?%ou*I6n0c56q0P?`(DtabYmJr`Q8r$APaGi5mws__AnMauJxxee} znV4L4Ngt&QtWTEG@#@8YmE{4?Y_BZS+lQe!8w(ujI&E;=tEG=u)SR`kcxmkVZ}DrT zdyE*7L?-LJZK|X9GNKC<%69X`nV#2=N2}EOCbb+%Rb>V0>&fu9eb?ER(>4iNl654{wUF@#1=-z%VhPw$cco>jNT zbDps`k}(-n{*zf>-R>rI?K^IL`^E8!ns|yj2+v|%t!iotvI~@)RS^@C@bnU*jMtE9 zv987VCGW-aZW>@^N9^75?+U^Uk&skAzDHk-h73TVx}RTCe-#buYL5`X;009f1xNg1 z;6F+ecF!qS{g9w!j~lb%c zEhRMQ$2cepHmr}ZT+v%#f%=y=0W+k3J4#YKR~{7<#rfILC0%Mc5c<1j^mQyuxCvC4 zgg(BfDAUbkJm|H~q9BfauGN~m-B^}jN7})eEPg-iyjBD@fkBA=B+Bt>$_wa;Z0U8D zO3aZr^SevYOryY?_)7@EvH6y<=m0rPI8oinx|&hI_%gZom-&L46UqyNMUS$wzN8;f z4&nz^r?bA(yW^tuLJG>*WluOT#fJ$F!u1t|J%@PEBtOe-i%;=>IV}w)MEjgJQO9e< z`6htpY5z0&vjCpRRzt(5=A#AOET3gr!`H5^w_(4(lpR|iM-{$7n+PNv|5}TNn9WV_ z27+(v_pd_POHDsEb*noc(?T^g7x|9Odl`(d?g~oSg4ojV4V8-sq>y-M{Ca*>m?eG{ zDaDK^|JYxb^C-D&MEZn0j^B@l9bVhK z3?8qN-*#^o4LiW#&cOf?hGXL~9RpdSxgaUf2Xnjq(et__E#b>y6WEz#q*vY9^CuWSy$3s=BT#?FUwKwBZjGSu+zLW%lH(^68rmjn6_MzZM6;m|> zzpct|cTCzufukDzkFZ1W)xj)0n+oe0c$PvG9O>#yqm^6z=T*mBT zP#WgO1(yb9DVPNaPZ*|$S8J&1XoN-~%<04DH4!7&3+u?c0>vcc~ z8osH}dcWbd*Etngt;Y*h?oss`Laia1LjC-LgrAAqh1R?_ecC$jz24S@9@g()CbGZ! z?4R0pa^&Of{PaBhnS}^e9tjpn(pOxy;DtbO|N0Y>cBZltGhPy3C6M%iXDQp$DtDH1 zIarM0*N6~vd|al;L$Y}KKj~j|4yzK)f>sxcmFQBgY*nbje(SXVd`+0_3Hbbu z@&f^w-?^}dWSF)=#l12`{MJ`YnJf9R@mIAJl~=9b;H9u-QwJexEq*i`i;p;OElQai zy4p}|iX13UQN=#}79ARA!O}PaNQ+I1o2%p$<9SKfmv>t?bEm6};k2u)c^t-p$jL6Y zkChL0Jjd?&!@^2{4Qiz{WDDJp!+%=$ViP|QB@#xA@>?=2g?Z0L#2>pZqdu6Kj@ryM z>d?}ea%HoY0SuX~3Yo>T8ot=4F<&v3v37c$*zzKa*KGi92y<4}5}q$NsI~v*E`5QU0c%{cZ0` zMf$i0i)gU&&^FzoE`CoPc6gn`j!8nEKWY*d zv-t!%6=h7HW42z?O68W$oA;r4bwz|r3AeQA36hHQR97B2wTgOd&`xEU5^u}V6D9w`9<1+%qCB)VjM_Iha!_v07p6SCsLnob^DIR)jM z$V-jp)MVZvIb;#|7{UM^uiu{xpL;UB?q8ZN-lDVpva@laVYQuWj*EJsJ2t4u?cTlz znLn}wAO2GK-QTS`wEZySGBmKg@!`#mOOdb(f=l_vjVT5zVU@SNlinn|cR@ZZRr||n zc{An1Ou?2^AXUT9+ZjBL!x_!d+R||XA1o=_q`b#qmM{`4P#s&XI2Hx~`wr{77!JpR znk^dRrYuouy%dM&UzQ{aF3Eeti2wzug%PAcadBtTEaR5<0M?R@ih_!2Hd0yOK^6_wq?s7Yvad&zG6$snXhSy`9KL~eonBC%-Z?R#4nPEp4PdHO0nsVQY& zo{LbN_tuaXfNsMZ(Cs@RMo;qTQt2^8m@EN?l~#g>EUoIVae)>gd4dRPo5v{#BT#9>1B7rFGI&fl>2REX?h%>b;cFaQ3j!egTmcf z6oTqiKjn)^av^Y~yWUsnhJF4;NzJVcyZy1$Tntv1Nhj8}^T#j5?T^P_Uhkg#b`Caw z98Tq?{u|t1?hO{6{Dki52PB&Xye^Uy7|BJ$w?U!TOd5skr=us5=C^@zn=fC;f)wbs zoUO8LR<%bxjaBIuCs$aM8h?UoqJ(WA9=C#-_^Cclpc&dQNv3(zyiYVf>k)uNDhB3; zV7XM|1a%4Rl$q!h6fn-7Ue^i+O>ADNKALPyA_a%+cx^$#Gip##n;Bzz+2H;vHO>ef zZqEo?NmV6zZ(2gRuaT+&c{Zr%e5J}-GK@i?CEmI%*{YV%fNzE&@(D7&q;&*71kmr+UlvH6`!yZUvT2HF1f-c@Vf|=Laot6e76|XAYr?o)Y{2nY(2xpKvFG=6XZSZfTflLvduBRclAFte7b;d7B0>0PzW2V(=gN5hE~aXy zJD_m$b^Ptm`H6kY`Ng{PD2kz0+W0;zIXNt3pMt5_!Eu7jika;>Og$Nb4oD0yF9c8z zNCgM8&TAe6e=jDjQIDoA1&T|1sYXSYZzX9XYA_|L&@r7Q#>>j`XXNF4b~Ep3WR%9J zOcdbsEz*fsYhZAr8wx{co7U>WxaC>oPd1sbP3TDwYguUEhSx)10L^q7+f=lCP4}DK z0@hAF+5tqAp7<4l5CB-8N}}BR@?2S~%J_FL>D)*XT3FuqVU#>JmD#=UC#!xlWg92r z(Iu{1(>A9Vmd`?Uw~Juk1HV+ANN&66-&3<<#5c-jv1QMMdXfz#g^!7hNYxT{nU4Jlzy#6pcq{dQ0wVFJgr%R`k&=ULaanRh&QFj2$b-rY5 zmEYw3%ko9XTTkd~u_TG#mDx$;K;8*0vM@XMatl>7P|rP9y%Dp)b8>$khzA!gMULi0 z7*(K93mEyGE^LPl|9&r2@Ksz}RKvjElUS3Xi5I}mGXXjyzfPOq(DyCeJ zfD318Gqa1ZHB4x-sqZpAZ}@kDyZy4#;pyOQ_uw;Ov&+%pbQ?|z|Jpyk#OwF}I+ix5 zGL!tCH;03-FHgySFPd++U+r6N_uq~mPpTeHqbzZ}#IS+tr<;($_rhB|x+T_BYXC?u zA$R3blAS5w*oK;t@#NFWqCCJXX=RQ&*A5mpbgCR1f9%UCH8R^cdGQDnores{oK{Gh zwvs4j)fZ3jorc&Gd1dK!IYqrQ{oXJ~bEj6PeB-x>>!BpgDgsLqc+Np8a z(lr3bswjedWp{tqjQvXIeSti&nzknQc%z4`yacDy3VOx0M*%N(c*dz>J_e*+$Vu&U zj)7KXaPWelotw}PO_v67{VlmAW+ufxa0B}<$`!jP)r$JAQy(*Pu|G959L`beS^90+ zf0G=Z?nr!Ah2Cg{?vt9H$DW}p#r0ovvYL|VNU;sNNz35RHoN?emcS-9o41m_f7-hK z{MiwL;=&c%>o0Im^l{B8UhoBUL3=dlUG)&dF-{o^Vtey)QD<3`4oT^Q)NEqul6vE% z9rz%Tlv?@+j_n@mSvsm)=5)EMYr10NTY8*sJyfhu#tlEz26*ErW_+Cw8CMCAkwE6Wlab-d*3vo&bu6wV zA3p!GJ9o}+FG&_3N6vj3!16@AHryI%dMVezA|bX6)Yp{z3B%%0n^_dNDCquY9{%mT`gOI<&-X3e_vgdXS3hUFyd?N2G^;tb;~n{@N#*Yp7$MOPPDqPM@oi|oJ8Qy=s_)8;hhd>x+ZVYjD%cO?B7)7X91F zs(sfBXjJyP>cT|DgC{$gRDs-|oNtX(E?WI1bc}FH3oA0AK_{y(#L%o zkQO#j0I+^I9aok!n4zpSna$M7^ht72Zjo!m1-ZUg;{o?&7_UN%s~M=WFb@V4eIN(Q zf{9}%F=BmWG9KlKD}jCP=*f*mCc-M^s;?Z=;>dL=ph0meN|jpS`1k~AV*J#_vW*4u zVIfmfQmOCcL*$U^h@Cz*f2`Q77qdn=$0=~>4ms*FcO)V5u!-Wn#>JVvQX3(-ykJU% z_G)t50*puX>*vTjJ2&m?*mgmwJn76;8-Z4e_Am zE*?JN0XOu*AGz?}`SG&U_12R8H<-KQNPblEqQwIGnCNp#bsYIr4gCdd;9Q|q*&eiU zRPj2{b=Bv0sG2Qsw*W8r)=gwFS$XAc6`pE>cav9uCCykvw5EvUQ|WD;Xs2rQo=#C&d9mrjuu?X>i0uLWaYTXT ztS!8edPTxF@oSZON>+;9XN=TxR>j^ISG@{hfxDTn5+nT5l0aXlEhn$4qJhuS`?sUQ zt$jK(1~`avz8VJ6zb&ZDlRHh($`i7QO+K0%mJ`z)5wV{)E-fu>Jw&zIRJHvEFZw+F zc%EDJeS|V%fA@tZpF3edpBGH*|LQP3kF9@;O8l?)U?=nhy726=YkD5X?K~8JGkQu& zv=H|h7 zck6=BywaUPku@-2+(R!rS_d_h?NMw(^Mv2lp?_+j*GuRHH92i$6K*)Da%qwDahw|$1>hZ!R| zlOwi&j!1Aj2V4v8)Ti^GA3i>&VAg@3 zqP;vUDiNl{k<@y&VbJ8Qku{hb4$Nby4jaJP^-SMUofAs%EB`K@I+qwQlYy}#I@3EZ z$SIy3YO=pg9aZ0hQzu(lvko-Kj#DWe!qHstV>2w<`fhBoho>PGb7Ngiee|lvq3WJJ zR$2h0>(~FnBSkzQsuq%(8LG2UaIR)DTc6(|p-gOi8|6y})cP|Vr2kZq3}Zf9Ih~mv z(5z*M+V|$qL zEXek^PHLNM1Zm|vb}JFyq%3MQKk<+?Uz5Up!eC79cXyL9p&D$?$y{x6WA-{)d||2{^E>pp_#3?0 zb-Tb_&Hs06vQj=u?BCEKqVM0mWo9x(;?Uvt5!!Wp|NRxlZ_nZN(C_u^{$(CSqp&te zxY+AW?7tMPqcp8$0~5l-7KCqIoxDIAIx8ED!7#4Lfc=9>gWAkY87FNF7OGwdvGfd7 zSN0FyrvED{sFsgJ&9+7lr;D+yq@J{|DDqprmX%T$HXYp!3)|F3_54WgE<;5T%&geJ zP6V%tNnNs~RNzZT4hkzl(>bP*k5SztSRl;ZrRmL#QBfX>hy%k#vdN@JsvW;%s3_1G z5q}8|4GWbS`$87o>k{0owZ{JSr*^V7j?QTcvxzXK{%VBY;ZI3I#r(amuO@dj)T1`7 zpiswJyR7R_H9yK(mbL3(_n6%Q}|B^a0;k!>94Ba zgJjozVAqq^+u?4hg@Nz>DR0_jD+kA5rro7`%lU zpWTdpV`|U!Y2>8e9L_EKrt#UROf6}q&L~6_n4ez7yh!931j8tPVcvb6ugGvw-lHdn zh`bKIE3h7!(V-e{Y-F`Vk}qx0sYX&*;IVSE`OJ>bjZ1l<6ieeC$?x z#jQjh5^@>1=?nHV?M#|>nUd6s?+H_JzkGqB=RYkPc&)20>i;;I?0j4A`g{L=3;J}0 zE-dkS%EU1AWyOTbuLYHabeG@`K-YA@;qf<#!RwE=D?gthqRqc3en%&r=ZSAd_l;*K z-?NyxaL|LSnOAc-=rQq5Y=gPXr4zwAYy}uqEeGZjuTao8cUTpTpkfYV^$ZI0V%p7X%Yz(M6;<7N`Dk8% zeIYs|!Q_UhtPPfN3Xr9!A7PvQ?zS}qAm`_9skjo(64U+KF^RPuw(uLG89Vv{?YJpZ zx4J~2e9i*tmdxwCg|ejl%qUP19Fb(=B3o7X-Ac)B1h4EQcP((Lw#SQvR*2)UQ0i(a9cqd0o0Un1Z7bba?N_9+nclb509qM_@LxU;0t&qYD3NZ6@$&y(DTKvZJwk2Tk^ zZx3CULf*Uges9b|*Zs3kIE36H2mxVXyeAj=F=lsPxhfcuZ4S#|depWuR#TEUDAN)h z4skqn9VX-#As$Mp)3;nhlp#uHDlyIMl-RUV&_=Kd{F?R7oXsNT}NnsfwxT zRzoC>euEzN^N?3LHeDU@WK~s&M(aGgY|SWsH?^U2C4_#4-Kj zOf$nimar;8nCB_ZaiRRaDd*3&*>ye`sOt6bSmlhupcL){uCcKAFAO6d(LU6kkkeeT ztQ?qmxa|BAXMQu4AN2z>Bf{*FvsIERb92x$wF2Bj$$iL8DRG{5(@npJhQBc8)jLZa z99n!w0zYH0z28|rXF6dXZQf4E=d!*&dw~E|Z17&WuLI_JRXaTPN1HyL);FFKH+bDH z>2EwQ+IXYB{>u0Xto1k`|4f`A)}B&BA>w@pPB8I&TyMEpe}wZrvwN$X@Op0VxVt2c zZasYb6^|ceI~BwL-!hn^*dF0e-(I=QnrLf-+7;mZW9ToDihECOwJ%X2e@L80+H4SK z8XH-;o{U0#U(cJ;y7U`S}J%mb(-OVUYliu>Z|(%v~=)LBvL0(wJbhecM#&ErCk zzZdEd;_5GjAsM1q5}58)6}!*>)(XonVKzTmBFY0N#c7kvzoww+)kL0(D;Ld>W|z*h z+}b365E`Luob~kA=~gMQ%m%R_(duA3RTB01jo~YeQ6U|hixt=z`+|)XzBKIxp|0EZ zFtuXmJH_}0Yy(Z_1<7c=o7Yw+@Ve}Bxe|5;;#01ymgNh&pkart3#U3>~qQ8gkmWZRWh>y>{s)_nCdhiut#q<=?dQ zyW~-ol_;Jr+UV(M^K6(P5ntx{=h}jbW_JQdnqe_kFsL-a#uh2ZUvTHLw2U^DgBREk z12syJ)q52JmfT2pZzIusjS*%f;;lrjvE0Z~NksGhswT5I_50qql}lE6s4Eo9+uD(8 zenD{;Eb)39)Hll!;vK-SijS=`VahFNlmbvFNsBx)QwQ{RYw}fOZ?6h+SF^A_;PNXA zH5jebN2c4h?genJd#wn*M|ZrkcKiS)7sl6bvK}^pxtuntaUf{ea$En*Ol}ov3WMOP zuO$k;o4r5#-YkAY`dFI4e|hzTJHZWQzW)OTHd~y_ib`x)jzNdRorHj6S!6prnfK_g zMu)*~*|_kPs%D8GNf(lqL`usOZ_Ip2$28UKO;^#JI3>(~uNsc^_h}k!HHJr1S&c$Z zh#iX!CbqhWilzxm7Znm#6#F%-%War>Q)H<0^xQ%x)hh-H#(8Y%2MPuQXl!9))`-eBLd^pLSs6;H&{E@lv0dF>d&HE-w`Tao{-+xUp}6&1m0qy z28tS|MEYDnKe>3}9~UzqKp~B9_y;t;thaEs-^gw}-1*hNO;SetJV$5nJ3aSDE(YL) zGaxe~hx$+Mfumq7|7MdzNX}m>1TM-sT$<;x)gU!SC6~qSZX`QJC%Z{tNcO*1+nXHz{#kx{f!_8+wvxJSbh<|saIQ0SO(lluF4MAp#QKnBq{B#ou!hY z-jn)}mnA&1IX99fio}L6vuw5~WF~$2suj6=#f?A`W?jW#qO~wlJ;_v6$bpkR+ui9V zea~1E*I^4+th2W^PoRy_FX6z&3_1IVasMdudk=&?GIOD6%karg+Y-bcgV?uotH_MS&yNL;diW3tQ0~>` z$+pA1?YLTMI2&;?BR>lJvbG{77aveY4eHt8R_bkaS2{j|F4miN{${%!toYvRx~{vM zH6CPnngW4>ZX<0u5d2yO0K1}lu5^&^FMMZSKixKv&WE?7kL^e#@2#$mn?urQ?~$dN zgND1-uE0rGT`aED$Nrc!vR9iEFq}mZBNLN4Nek3yD{%Zj=x3CIebuekc4}0SBaH0P zbg>l0u_Sq*IaL`rabnMeCj82?ipcsdodG^Fe>9``bD0*3MZ~I5i^tfD<3wYU;xM1E zTn#W_#kC5x#8XlrW$Gs8HF{3_qzfNYWOnBjZDdP5#4p8cAn3+s&y$XIlyM}rQi;m$ z9MTAjc7h|q-6&eaO2{6-nuu!*F2;=locJTxDrzMn4C2JS!d^>2d$};m2TH5UrDnQS zd-&z~WWQ^q8l6=XMBbaWY2_}!%FlXt3 zMh9(-u8QWTQfS~yAHcP3-dl!?@GZE9dF6NRc22mW9*|NFbr z7z6IeRAiQBE9`gpV1_1u`a=mxNkrxO=kYx)sm{GM14z7MI2lY(sj-J}%spdb=rCm5 zpRe_gVG&UQicv5w_eEs8{kob5T$qe+CbE+SK>H}uq`sRY#&RP$^MyP_j$=;M8D~k0 z-wQn)A`U;!8F@ZinW69R1jfojtZlLTwN#vUDx!&9saA@jwe>aAq7pCeBlNPz3Q5k9 zBk_u8zi^|=WX^EQZ(FiG8fL5q0|VI!e3MqP-c|t0WB@4%#5Pmm2j?JDndWeHrDlwMk}khTGOjg zbd7;BhE>>52g@p(F02VkN3wY>fmc-DfdRQ-TfE*}aH0@uM6%40=o3+sD1l68w1`q` zp>itQjh;|Yt&i20PBRtvH``t07HZ`njXCOsNK`e()L+wC!I(4^1!fR(@bnMea~L~$ z@@l-JG5LH@o;=^A%#-=v1a%9^4X1&idgSyD1Io_mRguEIaR2R1>NLxN11p`@VXk=` zA1iSKKfSul=&F%w9J1>ijxwz>@|h`f@~B~)n+B9Fjdz*E$L#i?DWnhXAQg(N2}*@p zUVr;&Y+fk0BVj7cp=k}`O1+)$@jce+y&tZg+s()Gh~SIzg$ZQaD-_a1^tzM0V-K+V zG^)b#a~~U?9!>Mx%fmzzK5soFAf)}|vcUn`uKnYEqT}dxD4w(Q0oN~;6vY7=q)Oz) ze37oUMVBOJ`IuA}p7Z7_8fI?Z$n`B+smiw6SQtH&KUNAV#-MSmf6G3eNRJ5JRKbbk zbVfRdFh*Iz9fESlD52$hzKL}EfeqOWs_LfH zw>3*3NEO)-!%9t=8DQ^iT!+M*E<=7)E=d<5T}F`+%s)i;#oB%um7k8yzTOe$D@mqy zPP*#cj7D#sNy5)xL~J>x8NB82Xckf=VAzN%)b6nc~GpM?74nR3r#9ZrZb`bnD5w|ztp@^Z6^t|4lkC(IGy1|fI zuWpafIBl)1M@a?5J0`QU;5vXyny8V2`j9 zmts(p`AyQaYVRA#QB77+`m)%K1vEHnEyB}d{c^~Z8s-lalk}@)(~lA~Fj!QxiS&mL zl{FaA5^qou$jwQZErOo+sMJdyj8uK3&GZmYt^KfC~s?i)vquWzY7X?{E}P3PyACHlqaJ>W{qyNvf5;% z$*O$mp{e9S_v0q}X>h>jy|_@|i3|}x4WG|5>AdY>UDT)%=vNIafd8i`p`d|)3UQkV z_|-WAz-@={?Z&m#uJj|(Xn^1`i?vWrx1~x$I}C-N)DZyUCoDlG1P-m}DMCAMEQ^9X zFIa4xd)hH-5wWDS_p8>uLP36KAI{rR#@+N)C6sN%x`dU>3T?jk&e_2~R85Z_1w;Ed z&!i|Sz!D~djkiDy`!AK8vue_k;niQNCJOu_=t8>8*$^!z|MH}RYX{bJSdD7FSP>DE zzj5;!ose0Q3(museLG^PM^ZSNL!o;=uUd4N;--bHL=sC@7h_;&-qOd}L24FBSAtZM#splBD??J;q3!$8 zk%bRIg*4WyuTK#czJGMGbFMz#T=-1bz0qd)>~j6I2WC+0cL!q+Z5uzs+4%nqc`};w zva&Mh9m%5$pRhOO%tgI33t-O<&qC zrSU6BYAP)b=KAL;&G0o&ZY*skf2PGyoCXd@#zdG6q0LmDeHm5WT$+EGW)+}%5Ndj8 z7Q8{}k3So;`W%{bdg2^ibTs5rf(wQtWfy9EdX;tSaSj`ov~xO58BLvOLlXl$t9}|t z&GDfikF+KN2X!5}-mGq@{6gccI=;MAw5N1|32W_wpLv#L4fBuXKV&3Fj%V{LB=I?d z3}a-eNR=tC4J_`;rs@PUldxFn$CB2aNbuuGIN%SDFzHwPLG=6_*ScyWjSg{NO zMKzjJYU)SU7|AtTsrAe}b-iTKU)@}SvDcz<>#_0zS`~a)O{hc@2YH^#f*{a22Gj(t zOIY{7khQ{e$M`_n4a>m_3__TGcC2fSI7@38DKL|upN^foTBR5(G`iw6q*jp}(W-?B z9KXy}?Yefu2N+M{P3Q_sf`<~UZ1M9(7$VtpZxC?nz~e-H_m8EOsB@KZNtV+r(2z^y zdjZ7#DJ-X^HBnTf&-B_289rU~ocxt`531tAA--2ub|325!9I-lwjiIY?Z5mJ+X}Kz zvAxgc!T7YsAVh`>^qz>`D^?qo8W$hCLuiy8Tg8Q0tv+|%O*Y=UQxoxVTgbWPU$A@e zc2W$6v@_Wm=?y`XNS3UW)!DqIEwoTP6xy`3rD_YhIo>)opvcq08NM5~NbN145A}`r zEW4$E4bSH4Mhfnm|bI$wZKCu^qhDM%Be8Rozp? zvm8iaUWe$qM7cx=o)fkv^49LH(&_Oo%sy2|21Bws_sTrLi-Aqb0Nq|;o ztaNNyS3t~&LMH=LSh|p4#lxiGBY<>bsF(!Ddz4Rvg=k%WN42o4y-#_+k#=i|(uS1u&rg8@Jx5oCbNs_7LO3`uL$cgXuhg@-J)fWJ z=H|-y9}cnju|son7&P>+I}&@%ob-4s@QbY$NGQ-Mq=v@yUA2!vmjiQ$Tb_QBz?9uc z&?rxUQ>==(K!_3Q?6_({y`#J*P3$rLVngH*S<3+%D>RWMTVm-=f=+C)_m(Rr!=-mJ zAF``1v-q%i7QdSM#Q~G7tSIcx6r^r?!U&%?BaOkXnn*4?-|wh_s_6N?m;W2p)S}}# zM#3aKbeV2ItGLDqmqK!mg_vOGRBUgU({?sM37kO;W15Pd$Q}kl2EZk@Uqvt~L?q0a zK2g}f2&akIS{?(=~YnxxZ2M{Y5e=`vEXYp)~d(dgD#iTc@$-~`?V-#RCcR{k&%&A zheq~8Y|rnmzLYu}G4Ma9tGlBKG|58R5Z*`OeWicMcd&blJwqvg_FRhy*;ZCSTj zefGp!@?*xM5^bx?wyI6=Vj-eZNBwL*&(1 zurFzV5MCFwNDsv%GH5s5<@E4y)0uXIpu^gnx?rp4Mb?9$&ug*X>+Z|CJ9;Omd-f{? zkH>9MHW(1;{hJPqII!pM{QQf>`QOZ<-lfja%p(Y_Uojxmjd z8Iv4HwRPr@M!MttwD}d@kp5B{k1@u8kr&tXp%X9ZPeZc6Nn+=~#eo}X+L5=cwHA5n zrS`XXZ}l@z>y38)2cP@HDLwAzSki~WRwR5LHqYBB@fM?jDDvy0d3j(ejp@Je4Q0V8 z893fCBAy#hhQQnPd93)kCR!4>N1vC#O@Ut{ChZgKp| zLR(VX-HM20-6%H=oa*v>57Pi?k6SDyDhK0puZiPn0;rOEh=%)?eR8wNlehy zWqn=G$`pj%U~YM!8Qx_1J0`$-oJ1PDXoM$hyNCgx4_&-mCrBwJe;pp@)QJ8qV_R4D zU8Bq$d#LJgXYfe{CkFmSM@K8eJcnp#(|9|99AID_9fgPhPf#zjQ^h(8fS~vdMuq$nnMZ*L`ED~y-R$Pc zaAdx$gjSkX{8bk9 z8y*2p_c?=rm-_B{JX>68wNSpuv-_6be&+eT)n?^#hz9c5FINFeCEt_)RHSsNwXs5p)S!=$3DGXb#Z#e!PT7*zQdYtp?&&9UK(mi>Z@%3ItHs z5i2xQgr4E1dK{t^=2@Kq z>pWWIy^odg0eJi#Y_W1j9w+~*LOJ~YMaAZVyH1@B6}Z4Rj|#I9wd<|7oarKZ=Nf!w zTq*<}T`IV}w|D0$v%S0~A~`o+!|-`sSG}%#hja(dKN%#=rfEN|q+I(pJn?Q0L8~{k zuOD)LVcxAxFJemHaH_o~t~6&S=1xQAFOzf&DRjbP$ufEW*#odvR;_k0H+!=voK=Ev zf;)!>Bb|bYDQM7~wO`C4;fq+`s)`(%q>b#n(0FkFt=;U>Os5(+ePww9*>=DAHH$*I zFu&6564}o`mIidpFr*RHd82${>yq22Dg-<17~M>dhEZdiL^4B}q5kCWI#IiHL~1Q> zpa8^#h{PZ+w0Pir8!&YG7DJJ^{g&4LspGnCw0O;0{wT|DMMDMfIC1hAIc5ty!7gbA zgJ?%r(h@z07Pb*1oOeu&XYf7PxEzL2c&NQ1%sA{$DnviO`hjwx26VgCnpZ8};-(Im@;Ur2+N^Qf2UaCIL3T1F$iB^*zkd(yM`ni;L_(;DiB=Cm<7& z~vctBDV<8WqUj5=;R2F8In3vtyIQN*l}}lR-l86s8nuD9VCH z3dT9ezfALA%L(096bzF5Qi=$VrO=x$EE>(GZ%GZZ4IuSF#KcD(id>ViW#Z@u5-0(; z3FZ^nadKs)L%rXNDfghD2RtkOVj|k=2 z$4;SgHhOvH+KoS+QWmVIW>U(;$~S?_VfLN?IE3n+ev7D;SODa0tWNoR^n((?!be!q zI-xCwrMdGq1j{x$VcrF|G6sj z2D*E@rrlQu$Ukeqtq-;y+8RSqd9Kd+s021>?FJF7BfXGxg;}F?cHi@Nue|zQBRV!n z1;9FYa!V@?wr=i{B@mUgo?81n+k1I@iyJkveI!#H58ncUuQc0@y;3E=r_FRSRmASn zgF!Ey4FCxNzWbA&|M&5K`3SDO0J`U}|L$T65)E+!4d~43tTmsM?Xds70QSrQugGov zpMwF+T2bnEXWEhDd2??Sm%xa(=WZmJ0~0NS*_kPyBiDPbR3ScVT!4Uj6dC+BtA{yu zNH^qt?;N+h?LHqjoO}F-bDAMm!)TJJ({^H`&lZ=R`*o_(NrT;HmYrrF@X4)EARB&M zF-nwq|5$@B`(JnrI8P0R?-haPb{V^{0{cI|ov-m`oJot(;S6`<-XGW2eQ0Z~;rs)x(Cw)&T7Va>#0tIh@E54- zjSz?oUTTw>bBz!lcd0YKjo!Y`#&vOFz4OHTF;;zOS3i&C8k0!d^%l2k!KX?2wgXC^-4CeN!htPM|9D`E3;66T-+xjK zEIRU6e=q*H^taNxMiBf_u<`)*z&{O8BcPjcRk{eud=HQwUYQ#9e#J>&x3KBCuK zXjLN|dUa;31?#Hbiot7haX|2KLGb3{ebCI|5sQ~H_rIIz_!krghC05&Crt+UUJ16@ zKfrx|pV8y0x#1-gM8TxhKImos5eiOFmbJNs)%aTVWswRS9KJNp4|QWy`vu|0 z>dMEUH_BOWoljLXkR0k4*uIzT<+f1I&-tD^!B@4n5}mEB zWxXWjhb1GzcKQY$3hHOh6cWc0l%qp5j73dJ_T|+rwHab!`+9hrRPXm2F)+jde2atT|FE&1EwYf;<0a#djt`B8LV|YtyKIl=*`zsoX#+7hL_em+ z1r})1LU=ZhO;n-Y+-wFo&a7<{-n>8EvOW5^nRn17HG6%91nJ&0oNez&Z{C->5xg5x za$^4|X?(pD?EfU`k;SMonBaK|spIxEN!xc1O>fbCB*{kl@Y!d6tg*Kn!#5VY?LS>~cD1_c|e;r)hn=2@ER{ z_aH_1ey1q)5)HSmv}?=^vEJ4zy8M|wsDB?S+#atuN$S=L`)b)EiSli1+v{%Q$@6Ua> z(mHM0&}pSp0<_;(d||Iw>h(Iu9=P?0o{mL?|2YSH9fp6|BOt%P!T%&|J-W|#!qxun zVrwKeq6bvlsm93%Wf3@V%VTZmFm7;apTT zg!clz@ub}CUYM(;8Pkm;mx*U~->rd7i#-HQvuIanhnc?M9{h{EjRCt5Rs7-I7c`}2`o zt{Z62S(UZu%-O6__$MHAUrcyVYytqIe(5}TuRU<9FSz)X1oks!5#j`9tFvg&RxphO zhgk;BBM~+WB0Ang+iAHGp65p$d~;u-EQhk8AO{&=M+iTRtkm01&#VrPP9v(O#RI8Y zEpG@(2nG);2Wrke+_Q>m)32%GsFi&Hq{v2ol~Xodr_DFn?|MET=jyMzx_bZAH=ds4 z=a8C478F453S#Six_VOb$ACDYcTQnB7=-gCbA-$;({du@^w25@I%E$<@`)P`F(`D# zb_&a5{%CBf@TS3y9sn6-!4w1##GD3S;V@7#y7ina%2d(c7`gzQb9?{%}E0F`D3Snm|1HFqsV3O2vSBRgO zE~U8+=jKb1wsq6FNyo?He8=St*L48=>QJY4h}XIYCCH|2el7!eFSNp@3^H0Ioto_5 z(__92mcREAS~I-Ne);?=8ROk5+q@i;UkvINP~ia zF9dw$AhxdN=dg;CohYe#f_d;SQCoOJc&w!Usa>Odw>Se5hu=D;Y*Ou_)h{}B^!V;q zDn6RtfMu%rMi*Wu9tQq)5P(tYTfUt^opeI9r-X$&6b6;SBJU*0cnt^OM#?4GZ)vGt z@l0#)p{9ucBe&hnt&${U2eyy@VU?2pCs6*46KGp&G;K&=?4fn(7Hg`@y|5nd? z))yh*I2KL7=7iV&;fY)ZS5w1Db4@RQr}xCFIMMwmGbl6SbyVLfmspO;Q5{Wblo4Y9 zN*CJ?YzJe|08mo!0sw_DI0#7Znl(3r-O7DxnR>_J0mx-l$i1V33%%dZ=DIrsE$x%9 z0jZ7}3BPY$%<;lF?V6Qn{-WZ54LILYv1ST7-1BXAh5eK2#teH1 zpkrg`UD$nt@BNFWo`!^CR?s4$u(sdC-A`w?18T zumxU*$Y%=#Jxx1?#`5kH)7U;|`h=dCH|-pyf$UJH3lVKQMi$|gD`Ev_r#_~-?yv*> z$CRn465F?1sP{GBCxgvNc<~tlJ&Mh;h=%n(KxM0qQN0x}l`WBDn3a`mE*+9G&dveL zwfuF3tO`#i%HQrMM;J{uYu<)Kfom0-f))t6;*xHD;w7JH?dXDsT=nW_QE)@ zqHLx|p6`_&(2cEeAl0DrS!qH7@Vfsq;dS%;yo~>5=#9`B$1_1@bFn-1SemI5{yNZ} z|MfB-jrz{Dz$2_1QN8C9J%s!?X?*oEu0jx$r2TFZ&a`KJQiSr?0Dq1Kj2oT(heiZj zbgV~|M6m1Lo9s0lZT2z=%shvD(*Rq`-f%Y8YZS^MX;E&Ldh^VLR};@?cP!9m<^OA` zg8)u2cD)BYb~nL&SU!}m9iSjXmMF7mp}6gaQnM8Ep@Q|Y+0Kp(I9xV9usXndgNNAn zjq*`F>7sJ6&@(d5{6d?^$-Nv{zomRXM)-D2yT<=u4?zAH)%J1Tn33nZd5#~D56s^y zgM$p|<@fu#DEhuFGTxt>dBi+!h49z4H$+5-x&JS$KIFu~O{MFGi@4s6x8OBgXV+7} zW4ED3iNtxiOrzBzY4hvhTejfKZcPXOim-IR1yi((=noi%GYL;V@tic-{GG)S7r!DK z?`e60@YpHweiEhAZ>LEwtPlL&AEX`J^AiF3{~#`~pDl>i6}V9>`u($AfYA5R=wj7% zj6FJFia8P;ZL{lI)l(4_W1Hkx0f%-PG@J3M6G=tvw0~-G`q)0!couJmMrr%|(g0zL zw}gv)<{L)fw~#Cn!0jNZe4ATU`~BI$MCMNUiD{5EY|&YQP7-3uoTISZD7lGyDJ(u) z+!P?N6X2rT;I$RR$p6^#aiiCIzFMdE@l5VD;2PnVe=gO+>D2n0|KW9WsnOv%S34QB z58~l#R!etomoA;8IVlypXO>3e9_kSY1*oy8+#g>MSQPaNKBv>2k1csV{!^s8Th;x(**Y+g@n8#KPu_B{Pb4bOf4|6CR#CAg#;Ro$rhAnoN3F@xH23jvU4AXx3xb^UeOLcYsjPq_wm`%<3P@zA_x zMTr$Iz(i7wqZ>#{VZ|Lg=3& z*75mgcidJ-0NqB>{LdBlx@{M$Za!3IB(csft>Ov-ykF|2*is$EQ3>P*R5?OpDIj{8 z4clwkc&|>cvN{DTU(#$i5gHJ6V=qd+g&?{BF3mprqgQ==?p_}OdWjOezO3*{ZT=tWbn3(?UIcP%P@f+dlqr(;eE`zwTFXP*TB&v=S9?vP zKn{!4u@UPReKiY%cIMpPdXyJD>dV#(EG&-g?M@?k!KdS`{#e>6nzR6P?^n|7_x{Mb zyM_C=ek~&B`4o6symHaLu(BnevWGCl7Y^xJ!hLM8pd~MpJvjBX=jm)n6hXJKr%=!H zSlgy<203^I@Raw1j<;c_n#hoiA0sXwjGh2s+pQ$>^v6?CMMOcblFm#gVls1I%H8hiFC-3waU1u0x6&WVwsP=DWx6L82Lo*yZ6{5_@-P%nuOSY6v3p1Qk7h{%l% z1JC0!>{TH({F=u|66`7lezfSXR#93t-G0zfS@BS4#Pph6_V$Oqc2XFQzV`A^wkB+y63vRktFp&OdXS7;ny-_h+(jB3Ob&uEK*@D#+79i4jVbhaj8|@~T2W-vp+zgZ=-{-DXG?i}L>K(e zm}2zi2`7%>uyx9Gxw|Nepa5z-wiG|_w-RxN^+znELM3XnYDvoDgf(RDK{)+cftN`% zn?2nPY#l_KIiC5+YfeX+K=mfOO$t14dXlj(U-nV&taiPL*Z#5~lHuDguUJ_(E&5*c zW?{8M#(*RT*58NJ|08p<+v=Ad%b)TfOfB;$|4g~U$U@-Wb2ImgYZPM%kvKkD?3irW zUP_9w?InG%7XUwci8$BL0e9Z+wt_nd2uiHM7d3Z-_I`w&w1At$v2(j6J(}xHDC#e~ zl@~hwi@bm5q}DbwPSWl94aNQl|7P|neQzRuMHdqkQAL`LBn#?97%zZY$o2s?zfjR8 zB0yL6ab^>4FImR|dQR6XJ|v4}<=)o5Sl!3gbM;vw&Q5|}W1tz->VXU2Hu%SlR8M)U zex#`51dI4UYTte%@=NBf1;te#;@|PS_N_ng6HNCzy~l*H`SVHLpCB{0!9#zH!}#xDRnT9aYg_IW-D8 z8XWxGclX9DYg;TidTH+Adyd>!PujN3_ZR&3qD@*~M2y)%E0JT6yGGK{T;ve^Jg{?q z`Y5JkP!d%kfE4%(eO;eY0)PddosHbrK4kjh0xE^OHgG-2?1v7$KfI2zihCE%k>fH!Q*VZ<-0qf!d<*<4SlPH{B2FS$Tt5vK&KE=XmEb>R2xPHIp2mM#*jb!iV`KavDWM@~=n8zHHIi)7>Jq zq$E5x*)k6W8b0j>IAG8fo?t89rsxX@^7`Ea7T=umiqp;V<07Y=_CTqlBoBLfB6$uN zJm`eB*mUH*ba^sYkuP{}OeYg`BQb;{T{4ZntE=$K(upNfm&kkWQbw>I7yNN)J^_KH0ZG9{KLJB4V26KF#t)a=~7z|!LH5d1i^*jeNShx04(?PcXVmm zxD%GJ*bkirg&|Ekv70>-SL=h}62@V=hw|6wtQHpAm>gWeOV1Lio5b7KS3XwRZ*d94 zs)du&NA=kc1TM)dM77aY0&_JPzc2j5!>F}=>j4n}Xrn4{_;MVY1SG9!V-_65CVKI$ zB-|irv5pU&~oFHJdPc`cKzgb)HM3MT(3Q$sk_u-D^yQuh#Wr{nI=-GL%FiA zPy)iO3K9iIWXX;SKV^QwvGM{n7WF4_yd0q_xkEqcyW0cxbzeuL(TJv}ehPM{tPD)i z?5DZ;RK^)ZOy5sHKmVjehjtVNUsSiy_8sR05kbWcZ&4!`EDRYTUd3Xpj7i=VCC?O^ z-F*Lb*s@8&CXz;cE#sV*q6>rENfc;VrgfB=nc0S|Kh^_yp^7z3iO0Ue#UB>^%|V_NF}$P- zjk{?E%6s~s@Dm4m8h?LK0=jh&A$Hdr-~Nsse7J#_1*>WQdMMRvKhAmbIG{$q&@ovd z`TqB}ed7>{B(})pMgY6o--zYAqYwnewcxww2f3bPSD&Mcjk-%BK<6b>0KG2^pkZjC zgVObX&%w*QZ2a>`&>;bXW9f`K4f^U%Xv*QU6n;*{4<)5x8WIY%hlQnpVy@Ic{a7hr8TotcnqA}N-xkWk0>OyUvHQd7Z%U1bG#|%4jWnt` zgAS1hL4QmQ zRA5C6&a;gW?Kg%^YSS*EE-G|uL_iuB;}1V$xW-R=zi|8m6QIBGI+bV5O%fw645FXX z#ng$fU9SRiz6b~KjE`AuXNj-6pGYdQ`8rrL!NA=h$Zal=zM-TF-vdA01Gzg+mK?}o z6$;d02KHq5)rtePk9Pk!o|0V46Ns_42DdJ1cd}3i01{f~*&pj41RC)j@uFV}yw<%V z2Y77H&d=%MVgx8fB0-;)*u5%r8B5FU{Ju?t9k~rL-UVM4M_+8?+K@A{A?&QC>QQO+ z1#N5394(m1O*!+7f&8?6oumw**;}rwnVHc~p#X#7lPTKPP9=4;Fe*Spu!PKH+GVhC z*YTFFefTAWs-*_{0=01JE`0tf%EE2^mH3Fxq3({dJ^v@5fjBP&LBe~TPN;v#^v~;P z+lPWX*Sv|{nnxmD89Lri1IS`+cnKuj7y{E6Fql-jz@*8JUejc|KS2x7$Io4@>L&Cs z^XM~gxM59J!wREsaN|b9b)T9IQ_ixM4yfEKx!h@hM}Qo6!_i1-Bh-kEn)mRXa7J1FanH`i9Aaa z`3+)5?$7|6S(%I-OZAz~ZMMlfSJ=s9{-)9ASGWFK9Ky^VfA1AUrs?MYPz!pMhDyYd z5u2@sQJO|-yx3gEXDDkSIaF*gV34$#hI3-ZLBE-S-hzSbatpx%1dx~<0K`rBfH*+0 zVqnx)|95}wEO;X0cyv^S%OUgHzm#wZ?qp+tFzA@%A>-hM6S%MV^j1h90QA=*%3?Xu z6Xak(u)h&+u>yATd7ToVQuXEZ`OI$AxV&0OC(A zw@5#L+6tDVy&ysb_IGYBSa=`F{tYgf5N6v6JqKK^y`j(=SQkJEI;PN=W$Cv}*#bXz zS2fjlL%cxRkvZY>ZWMbCVNtL@$wJ_7H<%9KBUP%<^wB25QlP>NAKb{@ zyyzx0ZcdLU3frsEEP0hHnbv7xpjdBF`QR~9v#K907U2|@rWXa zcEP2|P!lc=i7p+*vyeeWXiS6!>^h#VaRt7+m0xluN|gl=$Nwy%%LuEXd_Hd=fFKXj zv3x<66- zABGp!n?^hKMV#Cq%kO z02%NAzrhj-l7du+qoiXxyY;g6g#LkhE4KxXrmWrYy~u&d5;k6lkd z^c#HJEvBIXKTVp@9Q3lm?c~>J=~$)nmGA&W&0IRJYa!}4^cg&o*y&&)=G|*ugxJ&KEfnCrbRimnA4DHT3kdN1O6)>tuMlK!geynk>&&y`Dl| z9xepY$Us1t=0fJlSu9o5IL*i{cV`1o=^MW8RcKyz-_5(Sii#w3QxB~0F2AVJecYz6 zJKlc`zn^qq@~hXy3-E^!=23a~jA2FbHJpt=4ItXq)0NPD?boe1hzUh?zc)z?{mUFW z3s-U$Rs=zdh$JRteAwPBq7w5nDvF`$=$6yxs^k3WA-Y1(*U9VQT={q)1r~zXB^<=b z6B`~x$OHiLqsxN};s<3j^nFCEQqNr? z2smIdlXitt<0mb+#LXWowAFHW{g+;&Y6Ysrsi)Jp6V{V`$^&(}1WYM2>qX~s-HLL9 zL7o`0U90#TY1UM#G$s_^f>voU()1=afxMV3MPBdu0E?nHe#yG-c9quUf~!7(hka%k zk-fsnRcZJTm04w(FEF__(`IRFo5Wj>R**fTtLz-3TLX@pLxsgR?Uj z3i8p>@$u+#&9)>L`jdW$hP4L2W~N5tuSyCjzB3-oGz$C3wnS$bd(o@C-qsL`ovuLa zekITnQ`ClnKJNLFi5jx2L4TE>&}ltXhX zU$>{Qz3AS^$Mcns8#qR)q#04k>ykOSA>(N}blAI_b;mWVdK=$+QzCs>r>iK(vgD#u zlprdUyG$tzK_$Wa*meDNa(0qq?Mnc+c#BebT4AZ4V70?c@kQGO)W#}gi~%KflNoEk24&ta?dOEE|plp7o-1k|iX83lW2Zg2W|! z&(V|w5Ru0k!$Y7->5o>?eOq*?>V(f-om;aB)3PZoEHpFFx7YuxRKA68?v&21XBE|x zFYf~`m&T*YLtfNM7x`NhmfttMH`K}sR~u3=VId0hHMkgrezM8uhaGC>K>#|L054k* z>TN@z+PKT8x(BBo?gy5()pLm4-ZRQIs^bwD5e60XfmlLcnS_L50C7c868f6+4CAAM z8LKa&ZvYEYDQh6sGZq8-=auOC0HOuE_Dl3&XMB`eNdr<8PHbdUR44%1LjM2Z=o}d1 z>eeVcv29PxMvaXL8@p+oG;C~}jcwa$Y}>XPHnwr+yT9O^Gqd%s_gQOW5xHQ=&MZqn zQVqWJ17h)0{HP%C$&ItQ;Q+ahleQNE?Pilj zjVkAVXJ=<`85&QNTD1w#o8;ihQAA__AgZFo_%M6TkR-^WoXJA+_u%Q$nfk-HJ01cD z03aR|V!;+4&o)IR!$6eGX_j86@)@yp-Z}0`#%=u6GfnF8W|6we1_y{8HFMe%+mhc89iKhBp~nssG@h1b#=mxVS(xp%PRmC|>oXV9k@xdF zf5E(QK@Kk@<%xLnQ!H%TWfq$EoUD)#l^l}hfm~bX-%mTr{!D_qc)PK!Qr!M^K1EW` ziM>?Q&1-X)#4OcX`{!_=E$m0uh3!}xSihq{EX@fF-a_%w}1tpYw3nm zpYz%3UQ#o9@J&@Nemjb~*i6YxPrHoYA2`>p8m}XP3fjR$pvHidk=Y^^0S(|oQoKZv zvlKR#V+^gAyDDL_N`KuAn-pzIXy9(**q!&)Tu8OSrAxc;wVB_Xj}KILpHl=V?B z+50h3fe*~wzXmq8*=a=Yz2TE62lgIHyo~FYQ@Fw8FM6Nb8|&@|#T9c&!DBFRlEaEs z%M3?qCyQN{jp-r*sA3}PxIMhG^c`Ua$oP;LGC%`(c;q+;LlZj+im_Y??@Nq-iftlu zS@7s__WM9;HWPU0Ij+f-Ov5x6imfK!?`B8!f}P{Hb7g{`Z(D&eiZKFwXj|HUjE7N+ zP<3kV`|DzW@__zBK zQ<=3B9|x>uxBVxI)j~_B0o*AfrB1G=_e1cWXk4gamaO@E?2j@V40MAd+XKTxJL)5`NOLoWtnwE z;HunFwDy_AZ9d+NjN4KJ!XsgDIp!Q1IaX04$>*=a8WGSlKMP7$YyCKvy zF3Am2x;UU9Mr|P_9L4-bL}clCxmS9|)Z6)9;Dm3^rfptfdT5ORIaT6&o6+C_&_M6} z9${p0cv=tlm>n$157om1+zT7QAb@~?GQrm#3xAfe#Gpc#0mO^!qst%!&7r-XF5>G> z+YiCxKRRsZ`+20H-Bp!4qh`Avs0zO>%!37REt&gS@_yAa(k8=xIaWB`kF9Oqg!RS8 zNjdovT3T8K7WHwGd+kPDS)=GZfPbQh7yuZX=m#H^T{N&BK;l8@$LGd+N3Z09^Xi42 z>iLK2#*`B>9+O~?`5zo4G!4%%CS*l}tLr6+4)2xM<;}q~AI@t@>ucA0i!ZovwswPj zI-T^!Q!Pt`D8t@wwz&`fsBYGI-)hF3S*%a^YQ}_pG@Z(l8Df@V`$2+WAObip9n@5U1+;Q5{692HnLVIwl zEBna9Gg1dTo7(bPyZl+NpUpGTgcc+hQZzg~ ztI5!LPjblz75w}$dVM`kQ)0%QIF@vv62V0QNnb=okKpYL9ox>S|0h>2GOzg}PfEHB zF-XAlI}1Va0fw^X7N3y(mKaivkhZep4=r|{=qTrPn+kq~W^x%wKZ5F90KhNrB@^3l zZTwc$@=VQ_OM|O9xB^K=2AV2LyA}8HOy_`D;3=|gCg^hBBHN`3%|$4Oo4NC$IKztb z2%Lg*^)}SH`j-KkpgtHtf$6so^fQ5#q7pmgQ_8yp{3GDnoO?awW04O(F+G6`xyC3S zx|72N`S~H^yE0z9=z!PpCS5qtuZqNj5W)6wqQ{GMUqdO+VOt%C^VggPp3mJq-HOkz z-F60JScwywq5vxW`PV~6+qXP|=BYX>7g~ns1|f3wj2yLqLU-m@rqQHAB|#`Q_&o7_ z8Ev8P`}s&n46_Y7OTIn^+Z=?4-F+Q!Y!IUC5 z3Igo#J-=xX-MC9OQTSCM*6-z}V}^XqT5 z8v5j|d;^5o{fe!AMnO*tyr_n0=7wt3<-)fSbu|V>e1Y|=fgA}OsT;b(REX{PxowdFMHyw2~**WKxOr0Tlw z@I#XPYUEg$9D!bgVIOc=y?E{e!lt5jC8AN9%S(CCQvVFd-(0|@br5u;yZd^L6%j`t zW|WYSkQKwnc|p>>BCu=oW7PDN7vQ#zLe^noY5GyX(%tg;1<4WVlsc@15=Qz_lqU=s zUmUO>=~Z%SCnO1g4q2m=RP40^LZ+ys+a=a_e*(RW+`>jyPuQAs$qEs!h`BE+uu24NK%Q@aEA>cZKZr|aa7*JT_ep#bd z@nCzoTK_unuk2Y*29j7xI>Z2qf^|q|lMdt2-DD&|M;toX!Oi3cY4CP_%^XeVo2;`E zE#9&HcSWq|pfJp^ktS*XCFnYM5)_xG9LTjN4zhenmwk6R*N9wPGE8-f&r+ zynM0M_&RrSF%_fdw#uJlb9(${^MR-M2F%&!LjlCf(NqNxfi6sLlni3!2xYkCB}sAt zcsd^!jBV}hLQV#kUE`#l`{7B>!wT@|2J6m$Gm$7i~ex)D$Yy#57uMr$D2T#P+4W`<}b& z#k%lAl+Hz$83aJ!;4pmWPlXoGI)n*KoL{RIwXq-OtfSsNt<+_rlGK`4(+zU;m=C7N zKOvEjxiZ9lc5YhCYjptYlH7GNSfj@-G_dlqdMCO3Tp_sK``Z2acFWRfZ0cfD6&obR z0}anbfoYc6ItC~5UKUJh5QdLzQX@Ta>k4?(VFwh766z}M!PZ*;jfNb9V~e>2%!KM8_8F9gi8%s0J`M3&RbZVP5O!uaYU zK1i=oZ|I@_!Q~S{A%xjO0sIQFT^dcrJM}Jo{sqrvv=H#WFUXg4997;NC3}AuLu8}Gq3(x#|esyaO%_C zCwd~XqRAHcU+e21dPF|?EhBv~05s$Y5F^GNtnX8*#Y42)n$?MG1l(gm z=$a_w=qqs+RKCsrMOh50{?f zzo&(_PZzA`z>SQH=E5Ya+ALNIiI7(JB(QA zAX}d=z&+FgcKqsJ{*X!z(>av;&mWK|JEQ2N*otwbI(busdQ%Lbp^6~82-TYI^Zeh; z9v8czWDk$wvcfy6LOC@D#XFANW05;^ilYOsALG^sV>h=D?}ya1aQnF$ByhcYePhP}e}gk6 z){M^*{YLtGhtsa8GgeesM7qWg>XbJ1neE->AQfQQvM4uQ@VFokJ7jFWN`*|h#vY5% z=UeObL(}}W!@Xy<`217K>t)BehF=34M`9Q~FI)=ZpB>ZrTrG-+&Z+9^(?ipvWb%L+ z`R@_(03xdurCbrEXaU>Bu}U4@Tn3O|BoWompN4rYmQJ552jb$(LY(oFOUZqfj|`BZ z50^)j0MtKE8L~)lb6-_eDR5&5RAjeF|BNgt3>Ii48Q@s?nflWV@<@u3ExvR)!9nkB zr0UHIU+v{=bqx{_wsFA%zG2WWXF;Uy+WOBG18#C^X}-#SrWx;t&g>t>G?vzp+vQ&| zeuDW!=#?agI_K0%c+T_WaGK^h+9U} z+7py4Z;|LPP9Xe|J-WJ6sz$41KJ)$oW|;D@pS2gMg3CFJ4B!D1oUP>K_W$6eWTj%y z!D^E0`NeAYXHW5m|3}7_$N8&|HbZiEyf5$--VLJMrDla@XMnO0S%n_;c{oc9Vi2rO zJ2T*3zCCqq!d2&FWt_VPMTxDTVmh{104NPa^^CdI5>L|6f#x!wR@Ts`i?&Uuz-K}_ zXUfjc@EzPJv&l}pPWvLBide+daVm|-AyWj265{u< zFK{E`@2!z<2}EA=pSF3Z_y-fB`M-@beWQ`dW!=F=frc$VGy-7w5a$R;p=FIN)o==K z@JNX(6)uM4fB~sLuN44(TR+E*T6Lz}AKQJMulg9b-zRBvd|ql?&sUtwzgRW_>5=E1 zxOL<3fT2{7?ynPc*bLG*tci=14R4&u*pP9Lu?@YRctu(fHce?m8{4*^9^@&Mz^~t{ zG*9cl&exgcc)h&6w?BBDoL|1L;I+d^E=NahsD3N4jeeGyNqmz)EO;tT7|JNBZ+OvK$nueeAgzCiS^m zo)$QbyzG7+d+h$2(%zUZTZB*bbqP~Quhsk2MQ`LFVm$fFI6?7008pyNKEOK^@t>$~ z4DCN_dC{loSf5;12QAN=H7)Y1ashHGf?~s^-$?4~)%vxM2PxA~fHBkh&^>cnWR)^( zC_w8!c{4q&GPNyUZhjPv36pA=9$R_!sx7yuq2o!PoZBJA9m_H-oCvLVe?MvTy~t&a zsvo!d{G=VO0%-#CIwvApk?P_2qp*z1K!WnV0wJzIw!gnku`!vPr)Gj3ySLrbvCKxH zd<@vr8UPLoLOG$$n|Ie}g7%;N`lPELXZ+s=pQk)JHI88MG94u^h@p253j9m|M!Y#G zE|`xhN29fd3k!!#r@xt9e8L(!9vl6_`e|5e%iOU1w18AF?6G`xv+8k+5vbj%SF>d4 zX7|hK9n9aq=Q&ipH8R>hElykWozB&}UUoI|33#PBt)Frx3A)iF7*c{8B#tbtADNR! zdOoMoS^S?SN6}d&3t5B-Dk~v|kj6F!1W7AaTxJLEW#U&L&yEI%tYS< z7ld~C;grlt)L*^bm^hLrf4IJHD`UndMzWc*4i!?B=vAv#k+7Weyn@H$M>`U8=k5>N zmg92nGf$o$GDS9Q8|GCtC;`wCa%4NDblD=`g*O)|D=UPLL<>|nT1X+t^Lwop3AHk* zM@y@q0R?Ee+2ht0-Dhk1&)aAMcQYj}=bNq?MH)>6P#J9dRMb%M@cSN`hJ}-&S!+(` z`URNWI+q+-O!50v2bhd0;BLkLX~J3)U_5cGo54_CDWaxe({(#60-KI#0OcEeW7lnb zZ2YRrAYg@J+hNsn%gey&(N)juqV?NZo9bRBl1*x6;i*FLDw4pr(rzUwNCaqm$Av`z z<=-Dlf|ivr%zAG7oGPP0Vk0aWTB8m`Tg*yZ5OUm);!*gbdL^d02eYOuL3_LDqpjQC zA3{mfM+De^oTnQd>>Ur3bCu{?FenIo_~Xop%qA4gu`nFo2NFT)fL!%VX$vX+kr1Ue zMASiPva20S_7=l|vwvj-RvxB@W6j_!X~C^H+WwBAj!wtQ4y86^h~*{AMnvuwnW#o* z(V0SWHR3}I#$>UB{Gel4dUj(P7Z=wqs<19(fEFEZr!L+UC-O7y%3a0ogh=7M7O}5?fh?T+~)1Lgmm=%%+`#ygt>IQt)B-xPVjA_ zX}YIXTkhF8uDKh;ZxzTCC=BQH!E!HACjJm!`3OASG~g9e-7q#gT?Z) zWObgS(R?a-_dUd>dSCcXJ%r#uBfMbYQn_ONq3`YOrtjhWvO1BWS@-iJ=3PRldl6z} z{dBoxuV(J8JWAi;Ql|8yu(7*j#6=J~ov}ZuxOeBSN3%yTKt@5D%Xme#ey|Lzp(DON zICmUGJbr8XZFaG6>N~VS$f{$mR;i2nAbZnkfjn)w*=$r`SK7z zH{mL{wPYNHlc)G?Bw05^`O5PYmsBAqwa0lI=hNuA-o^A!zQ=uSW8?c)Bj^EukT(JY6P-9gdX{8-y6e56#m zWPYe$nDZ8@Lc`h^nL6%H6`js}LHKF<2O7*j4JJ%ZiVnXu$+lSl0HZM0WMJ=bVj-~~ zeGA75&#YD0EV?dzm~7N!2<&v+`En`X?c(w>wz^8diacKw9QM22tpf43XKm}O*TVxC zoEWqu)_n=L2EdH&=K-KpYQK*3yLun-E{ zI;J7AKmQX?#cspe&zftqeBy|N^fc$lV&h}~dh2U{t**J{auC(+qMlbG5}J&d#LeRb(_xq2G|PD$G^N%-P7vKwO2q9=IK0zVqDg*`b!D$3I< z>Ez&l)Zo#1rG-2#6p2c4hdkpe`E%0&jdwsz@#I_Xq8dxTJlLONu6e5N>Gb(ApX2d2 zT-bZv8GIer&x&aQv&Rb^XYe0)yO>B4Ze+++zVk!vB;RKJ%CWG_JH6q0uGV$l^Qe&| zg$i0`Ni&`CVgJ_s_0R8`_xiZF6d!Ld2X-5~Qi4o!Oz1LWR^p}I_W)Sx&RK6oa;4$g zkIDcUMtB2|hzK&Wua+Zf0s)MT9-Y1pHZOlU3Q_!ialp-l!=9ZIrR>1^^6A4L$s-N6 zw*4fC?BIw-zspfdxDfha?FnWvfJw(i)=GWd+BPV3Sdy*)0%NJfY=B>@-mgJNj{v|; z65N&Be#y@bAjO|e+8QQd?7AQZV?ZJ=fWis5pc7;h;qfz&VCeM+g-sUe;j`D4&+Dtu z$HQuPcomDl`$A)q%B(>qZzRN{&V3J29`=M_VGB|@b4vtc^+ob&%x$T@sEPhBka96B z{^O<-k=fEn7hFvOxx{$6_H2HUc^Lme#0m&VP0lUXzmsWs@8NCf(NHabJ%2@|D_Jvf3P?37 zY;9Rsd$qFNv{Ap+){zr4-rh)+E2t6&dW~tYneC zAMG*}yx)VeZW|kyKe_D`V=5Qm*D}WdKjjxtNPj!)Id8)G)Yp2iOBENFpiH?(v6;r= zF_Q*dA2kzMJgP4L^0t}Bkxy626D>Q{%O(ffwjMTJ@kxMHIJ-)CTHjH5=$+Zd`QYO5 zh03pM66wT95zipOXf~)57m{Y2>}?KCL2qWjBClC;V!-n&XMm>q5>(PV{ZDNCuCe_2u{A&TkRL>8-JL1zjEvQ?&pP0$vHWHyuxU9@T<~c zpOmSctl<6e(nCcM+yBTQO@3adLV~G-}6ugFtO9ixEkPRr?lhD{Lw&iPTL zNkaM?qSYcwX%tI*)a-2ZFlMU7u^zymf)qMqzHJR=`hT>*eYAW-7jaRFaP2}H#+%WRE zy}*Ho{yd2+;J6ZRSubQ2w{r%vM4Q-af*Q!tjZE7@2}1fi1}f1@Kl@P1 zUlgExE0uT*8nn$}l*abR@H@&+tQC3`cQ~uSdpoZ-Nqexd)Rs3nO{*0}VGjfhwhIQG zTO$rjdE=tFCU7^%Gf}J;lYm_&=)hqVx6eM+(2hR%WcJTU-+n7oxj$+HfCVGJ5H@3` zvM`GMn?c;o!=Lg-<$u-8-x)o37XEV4DfMp@D96kA7K7NFCrhIV>XWgE8i@Wm`8E|zQ&fMtNKqXz|!U_mb-ST?iZE|AH$;c_v zns6Y|NJ1AUq)4tgB(Qq74>8P!TJW{v!50bW<-t&JasKk^!t83*sKxa6TGm{DKQpXw zz6DLAZtRI38Inx#yMB#mCcNI2v6eS1c>_rt$#pXWim zDLdkJQ!RTt$I}yJXq@8BodfjMY;@wI%MJ}6D+Z$qNAwU;dw5EIY$(Qb?oUuvkzv2( z9+xGKHC;Q)@M~9)e{*Mb=+=Odq0NSf&h%{%FHls zd|5-_fw$$b`08u@SS^_IMd0<0=p_1FE15iwOr7%@Tt6JYPERTsJIGE4GuIS> zdeDgE9SQ2TY?>8153}@(TgT*K&<^>@HVpYQHVfHeJD?BsV7eK?B0@lb0&*xe4R+X} z$n?_f`QSs4-}ODFdGqNsXY9b?n4UZ4RDiJv6s8R(0H46&gGf5+rV>W{=%6(LG3;Yt+lE+xXdMj(% zVpFFo%VY<%aei?G&K(;WUm+BjfyRq6XB*D2TQE2~V`+7y>1QLpS6cGlALY1vrxp}agJhXnY!(jpn-T&%KkpFS9 z9D47HWqIsO!58keJPI`3-CSl2ej3B*Ue*yXkvuQR*LGz4c!|#|C0$~p#TFC zjblOuoXw<0-ez%XCCW&l0qD?_-ep6*cor zw)L>%>Ft~51@Y=V-sPCrofjaNTCRHl!q1^K)A6I|@@?dJ<)fR(efK%ZWwH!E^Q%le z{kkAJcD3t3mVP)^Zo$9ea~h@BKV38AhQ`n>20B6d&wQHaTk_i#+wojZ`7z4vMa#5Q zGN!2;rTo$LX3G}?Bjdr_t%rhS&+T49EP~_5_8C@1*pboKw~AFX_;hDD zx&!xZap4alBvKS^^j4PRJrH%PyO1S$wllrAt}vZo@l+(?o{cI0p`AjwTtj)2?;xk5 zFbbd!yduh1CI%@zz;h=gYqfkIoP7qmr6# z2!r1Ok|_*vV9V;TWkJki4Y3<628`dGqe;-6R2@9r*p{h}i7X8Hog zT&xqZCezOAZ>K-V(zW9939|=}>3t8B9Wj8D2H*78i}RY1FG<|5ElrN`H6nv0<`;kX zGfZ9ni8E;J)w({7kfnGnl`fCNEONN|pO;Hx7-fzye+g9)(ev`--$)cn z926Lm*m3#2W&HenAb$V`MY-It@fF-pnuyeBpsV;M%S+3qXa6ukhsbGqsbbld+6iz> z@wpv0EkbA3G8KB?S*@*6to5B@fi1gzb7I^$mbbR7tBf}#ZNIe zCVefZUrxNL1U|LYDhR-Q!bml8=-K#qu*3QMPM4BJ57{On1nY}AhKEf}kFi_c-U{B- z#Dw%byS!~ql~G`rbl8WdZx}R1TE6ll8q%DDTi8Xt1OO!2*EGs5zqGvl7$EXm0q_Hh z7qec?1F=+4aEeLR+^`q3WO~5dxSQkmZ3>&}^bC`o0L2}S+fzXSbc0l={Q(rRk4BRjD zs&ZA5&e02WCW$z#`ZmF??vVcoTPu zl#Mk^R*0b#!Bya=eJ2`hZQXtMba`a8(U4EbP(fUdl5|eFFEixeQeq_(=ge7xQ1;ab zD|J5wttX_TD+vNLZ(*s<6~h}Vv%bl{e1}(Af_vBFOAE+FVr0E(&6TCLOR{||-_w%<^`-Bsbz%DZYn%)z@d@e--BNj5 zuI_OJ$6eZ&cv2A1xt7LD;D6I3FIwf1fuf8Yg0vwp@(c+bYz((zLCaoe#=fioyFQpN zeO^MBR0#xD+grML8Wl;X(3NrVhycQd+t4}Z?R{y!gB17wSL%o*+RS)L+;tJ4etp zp>gdt28?)B0PXvof*K~Hqnq$vG%KH*eLpAkiSKSaNv*c~onDTZz>>y4E8qK}&h|%} z)z-Zt7aIa~n_Mt$2Z}xp7et&4&3%Aqjo~uBb*~V;oH5BUTEQ%gvUwb*d2jrlQna&N z>-PI9{&?QjO)+X)rN0B~;r9-kr)?M&5WJxk3)I>wO6|6qeZHNX3g2z*fh1+%LjzTi z&2bTUczV#5n_3(staBtO)?3723Zw}#u}P7U2h1|V+l+V7mRbp&Yb6Z#{xQj_@QB#5L;?sA zX}C(ajnuTXGNJnSR-`H6%Xr12=C_UJu&#J_kz(+nTlgU%NA4c2`i)m+IF3p=ybN1| z(k|0d&T0k_0K4207FpToC)PaMfLzhSs#&vivMkhWJRJ<6)c|Kre3JH#$%i{0l^j*4 zr>6#KO^7)>YMi!V_-;V6MA*6ELlZD23Ca2yeo8>o2Xo+?g=-l}6*1UA0l)?mHvyHV z(%Ae-=avUbZ7G;r(|z7_JY{mEla0LWO6}QxAxOXuh}j2Z5IZSmX7+|v(C(6LM#u&L z;^No@KFcAs;krBYt}HYfrbGQ=HSeo{CB-O5DzjO%LXIqehNo-{q}Tn10z6Vx&~*(- z*Nor_Ww0sv*dU5@{G4s;#t6vxi$hT2PzOKmnY>3|lKnDyy8Y0bW5!O9J$|qg#`j1b z+jf8p_V4m)c0M*w5fKSqX|}sw=qQB!w86KhM}W|paVx#3nb}!X_2vTk7ZFl!vi^%-QC+ZWgFz))%G`MtPxX+|t?%<%%|A{?F54Gt91kad?XGgi|VrGoHp zG1PY0jwX5McR7Cv{axdAc@NfEV0jMx+z65VLo!$-@+;nE8S`uCKcM~ewigQ{{RYHl zXm;PQg4#4>c*N-VgTyJ$E+aCkaarZR#2jW7)r>O)-*0a8dj!4y!0_+kX}X>TTf7&(|zAMN8=+Lgk z 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 QD=!2@TSK3mr)J~-KN&IOga7~l diff --git a/docs/src/assets/system-products.svg b/docs/src/assets/system-products.svg deleted file mode 100644 index 271928944..000000000 --- a/docs/src/assets/system-products.svg +++ /dev/null @@ -1,105 +0,0 @@ - - Persisting workflows and optional integration - pVisor runs one Agent with a reviewable execution boundary, while pChronicle queries trajectory Datasets. Execution and history are independent starting points that can be connected by configured capture. - - - - - - - - - - - - - - - - - - - - - - - - Persisting product paths and optional integration - pPilot plans many Runs; pVisor governs each Attempt; pChronicle stores history - - Inputs - - - Agent command / task - - - - Pinned external Sources - - - - - - - - EXECUTION PATH - pVisor: controlled execution - - - pVisor: one governed Run per Agent - pPilot adds planning, leases, retry, and reconciliation - - - Host · OCI container · libkrun VM - provider boundary and Evidence recorded with the Run - - - - - HISTORY PATH - pChronicle: trajectory Datasets - - - local/S3 files · Storyline Sources · @alias - - - Snapshot · normalize · query · analyze · exchange - - - - - OPTIONAL CAPTURE - Gateway trajectory events - pVisor lifecycle records - recorded execution context - - - configured capture - - - - - Shared principle - keep reusable state durable; keep completed work inspectable - - diff --git a/docs/src/en/installation.md b/docs/src/en/installation.md index 35f64a39e..897afee5f 100644 --- a/docs/src/en/installation.md +++ b/docs/src/en/installation.md @@ -1,7 +1,6 @@ # Installation This repository distributes the `pchronicle` command and embedded Web UI. -pVisor and pPilot are maintained in external repositories. ## Install diff --git a/docs/src/en/pchronicle/design/architecture.md b/docs/src/en/pchronicle/design/architecture.md index fd13fc655..93243c1bb 100644 --- a/docs/src/en/pchronicle/design/architecture.md +++ b/docs/src/en/pchronicle/design/architecture.md @@ -2,7 +2,7 @@ This document explains how pChronicle stores Agent trajectories and exposes resource-limited read surfaces. User workflows belong to [Guides](../guides/index.md), -exact commands to [Reference](../reference/cli.md), and cross-product ownership to +exact commands to [Reference](../reference/cli.md), and component ownership to [System Design](../../system-design/architecture.md). ![pChronicle product boundary](../../../assets/diagrams/persisting/pchronicle-product.svg) diff --git a/docs/src/en/pchronicle/design/index.md b/docs/src/en/pchronicle/design/index.md index d76ba92f0..91991bc56 100644 --- a/docs/src/en/pchronicle/design/index.md +++ b/docs/src/en/pchronicle/design/index.md @@ -14,4 +14,4 @@ in the [terminology guide](../reference/terminology.md). | Three-table Storyline projection and content layer | [Storyline Lance](storyline-lance.md) | The [pChronicle Reference](../reference/index.md) describes current commands and -formats. Cross-product ownership belongs to [System Design](../../system-design/index.md). +formats. Component ownership belongs to [System Design](../../system-design/index.md). diff --git a/docs/src/en/pchronicle/design/trajectory-storage.md b/docs/src/en/pchronicle/design/trajectory-storage.md index bc1252f53..57a4e6901 100644 --- a/docs/src/en/pchronicle/design/trajectory-storage.md +++ b/docs/src/en/pchronicle/design/trajectory-storage.md @@ -27,8 +27,8 @@ owns: - materialize, revision lineage, and the standard query views. `persisting-events` owns the storage-independent logical event envelope. -Gateway and pVisor produce events. The CLI can call pChronicle in-process; -pVisor can also submit through the Control service of `pchronicle serve`. +Gateway produces events. The CLI can call pChronicle in-process; +local producers can also submit through the Control service of `pchronicle serve`. None of those producers define a second on-disk run format. ## 2. Logical coordinates @@ -74,7 +74,7 @@ broken. The physical schema lifts `event_id` into its own business column and normalizes `timestamp` to UTC `Timestamp(Millisecond)`. Newly written -Gateway and pVisor `EventRecord`s supply both an RFC3339 `timestamp` and +Gateway `EventRecord`s supply both an RFC3339 `timestamp` and `timestamp_unix_ms`; the two values must agree at millisecond precision. Admission still fills missing values for older producers or compatibility imports from the RFC3339 `timestamp` or the receive time. Storyline @@ -93,11 +93,9 @@ inspection, code review, and manual analysis. It omits protocol noise and allows missing or extended fields, so it is not a lossless substitute for the storage format or the raw HTTP events. -`pvisor run --record-format lance --record-destination WAREHOUSE` starts a -pChronicle sidecar that writes canonical Lance events. pVisor itself does -not open Lance. `--gateway-stream-markdown` can maintain live AgenticMD at -the same time. Markdown is a diagnostic projection. Dataset consumption -always goes through the pChronicle API and the `pchronicle` commands. +Gateway capture writes canonical events through pChronicle. Live AgenticMD is +a diagnostic projection. Dataset consumption uses the pChronicle API and the +`pchronicle` commands. ### Storyline three-table Lance @@ -220,7 +218,6 @@ formats are handled by `pchronicle import/export`. |---|---|---| | Gateway | protocol parsing, call lifecycle, capture order, live projection policy | generic store, format schema, offline conversion | | pChronicle | formats, paths, durability, reads, conversion, and revision lineage | network forwarding, Agent lifecycle | -| pVisor | Run lifecycle and Gateway / OverlayNet / OverlayFS assembly | long-lived run-data schema | ## 8. Related documents @@ -228,6 +225,5 @@ formats are handled by `pchronicle import/export`. - [Discover and query](../guides/discover-and-query.md) - [Snapshot](catalog.md) - [AgenticMD format](../reference/agenticmd.md) -- Gateway architecture (external repository) -- pVisor CLI (external repository) +- [Gateway capture](../guides/serve-gateway.md) - [`pchronicle` Dataset commands](../reference/cli.md) diff --git a/docs/src/en/pchronicle/guides/index.md b/docs/src/en/pchronicle/guides/index.md index 9dd6ddb66..47053ecb1 100644 --- a/docs/src/en/pchronicle/guides/index.md +++ b/docs/src/en/pchronicle/guides/index.md @@ -12,5 +12,3 @@ task-oriented workflow. The guides explain decisions and complete workflows. Use the [`pchronicle` reference](../reference/cli.md) for exact flags, and [Project examples](../../project/examples.md) for repository fixtures. - -pChronicle owns durable history. Execution control belongs to pVisor. diff --git a/docs/src/en/pchronicle/guides/serve-gateway.md b/docs/src/en/pchronicle/guides/serve-gateway.md index 4714a274a..e6116d4c6 100644 --- a/docs/src/en/pchronicle/guides/serve-gateway.md +++ b/docs/src/en/pchronicle/guides/serve-gateway.md @@ -68,9 +68,7 @@ remain read-only. Use this mode when an Agent or SDK already knows how to call an OpenAI-, Anthropic-, or Gemini-compatible base URL and you want to capture that traffic -without starting a pVisor Run. Use pVisor capture (external repository) -instead when the Gateway must share the lifecycle and isolation boundary of an -Agent execution. +in a Dataset. ## Configuration inputs @@ -182,8 +180,7 @@ closes the corresponding capture call. | `network` | No | `mode = "public"` | Policy for explicit forward-proxy traffic. | The shared Gateway schema also accepts an `[overlay]` table, but -`pchronicle serve` does not create or apply a filesystem overlay. Overlay -lifecycle belongs to pVisor (external repository). +`pchronicle serve` does not create or apply a filesystem overlay. ### Capture levels @@ -366,9 +363,8 @@ allowed_hosts = ["pypi.org", "files.pythonhosted.org", "*.github.com"] `public` is the default. `no-network` denies explicit proxy egress. `allowlist` requires a matching `allowed_hosts` entry or structured `[[network.rules]]` rule. Explicit `[[network.deny_rules]]` entries take -precedence over allows. Prefer pVisor when this policy must be a -non-bypassable boundary for an Agent process; `pchronicle serve` only controls -traffic that the client sends through the Gateway. +precedence over allows. `pchronicle serve` only controls traffic that the client +sends through the Gateway; this does not provide process-wide network isolation. ## Dataset and state selection diff --git a/docs/src/en/pchronicle/index.md b/docs/src/en/pchronicle/index.md index 21bc6aae4..4e0b6490b 100644 --- a/docs/src/en/pchronicle/index.md +++ b/docs/src/en/pchronicle/index.md @@ -10,7 +10,7 @@ Agent experience is the sum of everything an agent did. **pChronicle is an Agent trajectory storage engine**: it records that experience at the unit that matters — the Run — and makes every Run easier to understand and improve. Use it to browse, query, exchange, and serve run Datasets produced by Persisting or by -supported external formats; pChronicle does not require pVisor to run. +supported external formats. In Persisting, pChronicle stores and queries trajectory history. It can run as a local tool or be deployed as a service in front of many paths. @@ -70,10 +70,6 @@ When you already have a question, follow the matching path: - **Analyze with an Agent:** `pchronicle agent codex DATASET` - **Open the local UI and API:** [Serve a Dataset](guides/ui.md) -pChronicle reads and organizes run history. It does not execute or schedule -Agents. To run an Agent in a controlled workspace, start with -pVisor (external repository). - ## A useful reading order 1. [Explore your first Dataset](get-started.md) to complete a read-only query. diff --git a/docs/src/en/project/index.md b/docs/src/en/project/index.md index 9ba1ea4fb..c312d4a97 100644 --- a/docs/src/en/project/index.md +++ b/docs/src/en/project/index.md @@ -8,8 +8,6 @@ systems outside that current path. - [System overview](../system-design/index.md) - [End-to-end architecture](../system-design/architecture.md) -- [Local-to-fleet contracts](../system-design/local-to-fleet.md) -- [Security and evidence](../system-design/security-evidence.md) ## Build and release diff --git a/docs/src/en/rfcs/0002-events-format.md b/docs/src/en/rfcs/0002-events-format.md index 3a712793c..35f98b050 100644 --- a/docs/src/en/rfcs/0002-events-format.md +++ b/docs/src/en/rfcs/0002-events-format.md @@ -7,7 +7,7 @@ | **Date** | 2026-07-30 | | **Component** | `persisting-events` + Gateway + pChronicle | | **Implements** | `persisting-events::EventRecord` · `persisting-pchronicle` `formats/events.rs` / `EventRow` | -| **Related** | [RFC-0001 Storyline](0001-storyline-format.md) · [RFC-0007 Events/Sidecar 边界](0007-events-contract-pchronicle-sidecar.md) · Capture 管线 (external repository) · [轨迹存储](../pchronicle/design/trajectory-storage.md) | +| **Related** | [RFC-0001 Storyline](0001-storyline-format.md) · [RFC-0007 Events/Sidecar 边界](0007-events-contract-pchronicle-sidecar.md) · [轨迹存储](../pchronicle/design/trajectory-storage.md) | --- @@ -125,7 +125,7 @@ Persisting Gateway 的主入口是代理流量。`events` 应对齐这一现实 ## Schema:逻辑事件 `EventRecord` 编码:UTF-8 JSON object。`EventRecord` 由存储无关的 `persisting-events` 唯一定义; -Gateway、pVisor 与 pChronicle 直接使用同一类型。pChronicle 独占由该逻辑记录派生的 +Gateway 与 pChronicle 直接使用同一类型。pChronicle 独占由该逻辑记录派生的 物理 row schema 与存储实现。 一行事件 MUST 包含 `seq`、`source`、`kind`、`payload`。 @@ -154,7 +154,7 @@ Gateway、pVisor 与 pChronicle 直接使用同一类型。pChronicle 独占由 顶栏 **SHOULD NOT** 承载完整对话文本;正文在 `payload` 的 wire 字段中。 新写入事件必须同时提供 `timestamp` 与 `timestamp_unix_ms`。Gateway capture sink 和 -pVisor runtime 是 producer 侧的共同兜底;admission 仅为旧记录或兼容导入补齐缺失值。 +事件 producer 负责提供时间字段;admission 仅为旧记录或兼容导入补齐缺失值。 事件顺序仍由 `source + seq` 定义,时间戳用于关联和展示。 ### `payload`:HTTP-first wire 对象 diff --git a/docs/src/en/rfcs/0003-pchronicle-ownership.md b/docs/src/en/rfcs/0003-pchronicle-ownership.md index 72675a4c3..47dacccc2 100644 --- a/docs/src/en/rfcs/0003-pchronicle-ownership.md +++ b/docs/src/en/rfcs/0003-pchronicle-ownership.md @@ -68,7 +68,7 @@ Gateway 的 live Markdown 行为可以保留 producer-specific 策略,例如 - `persisting-gateway/src/session/` 维护 session 身份、路由、client metadata、索引与 snapshot。 - `persisting-gateway/src/projection/` 维护 Gateway 特有的可见文本解释、实时过滤、draft/upsert 和 reconcile。 - `persisting-gateway/src/engine/` 维护采集 actor、WAL、顺序状态机和 egress;这里的 “engine” 是 Gateway 内部编排器,不是轨迹存储层。 -- `persisting-overlaynet` 是 pVisor 当前的轻量显式代理网络层,负责 CONNECT、absolute-URI forward、header 规则和网络访问策略执行;Gateway 作为 `OverlaySink` 在其上解释并转发 LLM 流量、产出轨迹事件。OverlayNet 不依赖 Gateway,可配置其他 sink。 +- `persisting-overlaynet` 是轻量显式代理网络层,负责 CONNECT、absolute-URI forward、header 规则和网络访问策略执行;Gateway 作为 `OverlaySink` 在其上解释并转发 LLM 流量、产出轨迹事件。OverlayNet 不依赖 Gateway,可配置其他 sink。 ## 一致性与故障语义 @@ -87,14 +87,14 @@ Run lease epoch MUST 通过 `EventWriterFence` 进入 canonical event 提交协 ## 收敛结果 - 原 Engine 中的 Trajectory 适配、Lance、Markdown 和 Arrow row 实现全部迁入 pChronicle;Engine crate 删除。 -- Gateway、pVisor 与 pChronicle 直接使用 `persisting-events::EventRecord`;Gateway extension 仅承载实时 payload 解释。 +- Gateway 与 pChronicle 直接使用 `persisting-events::EventRecord`;Gateway extension 仅承载实时 payload 解释。 - Gateway 仅保留实时 payload 解释、live Markdown eligibility/upsert orchestration 与运行时 reconcile;格式解析、文件 I/O、frontmatter 契约与索引实现委托 pChronicle。 - 只生产事件或调用 control 协议的组件依赖 `persisting-events`;需要存储、查询或格式转换的调用方依赖 pChronicle。 - 旧 ATIF `sessions` / `steps` / `tool_calls`、`NormalizedStore`、内存联表视图及对应 Python 门面删除;ATIF 查询统一复用 Storyline 三表 schema。 - append 边界直接传递 `EventRecord` 批次,不保留 RON/event-lines 字符串适配层。 新代码 SHOULD 按能力选择依赖:事件 producer 使用 `persisting-events`,存储和读取调用方 -使用 pChronicle。Gateway 与 pVisor 不得为了构造 `EventRecord` 依赖 pChronicle。 +使用 pChronicle。事件 producer 不得为了构造 `EventRecord` 依赖 pChronicle。 ## 验收条件 diff --git a/docs/src/en/rfcs/0006-pchronicle-vortex-backend.md b/docs/src/en/rfcs/0006-pchronicle-vortex-backend.md index d91f53a5a..f7d49866a 100644 --- a/docs/src/en/rfcs/0006-pchronicle-vortex-backend.md +++ b/docs/src/en/rfcs/0006-pchronicle-vortex-backend.md @@ -135,7 +135,7 @@ Vortex projector 固定输入事实 snapshot 后异步构建。构建、上传 ### 3.6 依赖和构建默认隔离 -Vortex 不进入 pChronicle、orchestration layer、pVisor、Gateway 或 CLI 的默认依赖图。默认构建、测试和发布 +Vortex 不进入 pChronicle、Gateway 或 CLI 的默认依赖图。默认构建、测试和发布 二进制中不得出现 Vortex crate,除非消费者显式启用或构建独立实验 crate。 ## 4. 范围与非目标 @@ -1226,7 +1226,7 @@ Lance 的 merge、MVCC、maintenance、scalar index 和 Vortex 的 overlay、dir | workspace-wide CI 变慢 | 独立 job/cache;日常使用 targeted package commands | | test matrix 成倍增长 | core semantic tests共享 fixture;后端专项测试独立运行 | -验收要求:默认 `cargo build`、orchestration layer、pVisor、Gateway 和 pChronicle CLI 的依赖图中不出现任何 +验收要求:默认 `cargo build`、Gateway 和 pChronicle CLI 的依赖图中不出现任何 `vortex-*` package;只有显式构建实验 crate 时才增加编译成本。 ### 18.4 独立 helper 与同进程链接的权衡 @@ -1517,7 +1517,7 @@ codec。它适合作为 benchmark baseline,不作为目标设计。 - 锁定与 Arrow 58.3/DataFusion 54/object_store 0.13.2 兼容的 crates.io release; - 记录 clean build、增量 build、package graph、二进制和 target 增量; - 写最小 nested `TrajectoryBundle`,用 `vx`/Rust reader 检查 layout tree; -- 不修改 Gateway、orchestration layer、pVisor 或默认 CLI。 +- 不修改 Gateway 或默认 CLI。 ### Phase 1:标准 Vortex 单文件 codec prototype diff --git a/docs/src/en/rfcs/0007-events-contract-pchronicle-sidecar.md b/docs/src/en/rfcs/0007-events-contract-pchronicle-sidecar.md index 6e3b88ec7..f7abb68f6 100644 --- a/docs/src/en/rfcs/0007-events-contract-pchronicle-sidecar.md +++ b/docs/src/en/rfcs/0007-events-contract-pchronicle-sidecar.md @@ -4,34 +4,25 @@ |---|---| | **Status** | Accepted | | **Date** | 2026-08-16 | -| **Components** | `persisting-events` · pVisor · orchestration layer · pChronicle · Gateway | +| **Components** | `persisting-events` · pChronicle · Gateway | | **Amends** | [RFC-0002 Events](0002-events-format.md) · [RFC-0003 pChronicle ownership](0003-pchronicle-ownership.md) | -| **Related** | [端到端架构](../system-design/architecture.md) · orchestration architecture · [轨迹存储](../pchronicle/design/trajectory-storage.md) | +| **Related** | [端到端架构](../system-design/architecture.md) · [轨迹存储](../pchronicle/design/trajectory-storage.md) | ## 摘要 Persisting 将运行时事件的逻辑契约从存储实现中拆出,由唯一新增 crate -`persisting-events` 拥有。pVisor、Gateway、orchestration layer 与 pChronicle 共享该契约,但只有 +`persisting-events` 拥有。Gateway、事件 producer 与 pChronicle 共享该契约,但只有 pChronicle 拥有 Lance、DataFusion、对象存储、Catalog、查询与投影实现。 -pVisor 需要持久轨迹或 Attempt registry 时启动 -`pchronicle serve --control 127.0.0.1:0 DATASET`,通过带版本和认证令牌的 control 协议提交事件并等待 durable -acknowledgement。pVisor 默认构建不再链接 Lance/DataFusion,也不直接打开或写入 -`events.lance`。 +本地集成方可以启动 `pchronicle serve --control 127.0.0.1:0 DATASET`, +通过带版本和认证令牌的 Control 协议提交事件并等待 durable acknowledgement。 +存储实现由服务进程拥有,客户端只依赖逻辑事件与协议类型。 ## 动机 -此前 `EventRecord` 位于 pChronicle,pVisor 的默认持久化路径又以内嵌适配器链接 -pChronicle。这把“描述发生了什么”的稳定数据契约与“如何落盘、查询和投影”的实现绑在 -一起,导致单 Run 执行器携带不必要的存储依赖,也使 producer 难以在不依赖具体后端的 -情况下发布事件。 - -边界调整需要同时满足: - -1. producer 与 consumer 仍使用同一种事件类型,禁止复制同构 schema; -2. pVisor 不拥有存储格式,也不链接重型存储引擎; -3. pChronicle 仍是唯一的结构化轨迹持久化和读取层; -4. 不为少量进程协议再增加一个独立 client crate。 +逻辑事件描述“发生了什么”,物理存储实现决定“如何落盘、查询和投影”。将两者分离, +让 producer 可以发布事件而无需依赖具体存储后端,同时继续使用同一种公共事件类型。 +pChronicle 保留结构化轨迹的持久化和读取职责。 ## 决策 @@ -65,23 +56,14 @@ RFC-0003 中“pChronicle 拥有轨迹格式”的约束仍适用于物理 schem 其中“pChronicle 唯一定义 `EventRecord`”以及“所有调用方直接依赖 pChronicle 类型”的部分 由本 RFC 修订。 -### 3. pVisor 通过 sidecar 持久化 - -pVisor 只暴露两个面向用户的落盘选择:格式和目标位置。 - -| 选择 | 行为 | -|---|---| -| `--record-format json` + 本地目录 | pVisor 直接追加完整 `events.jsonl`,不启动 pChronicle | -| `--record-format json` + warehouse URI | 启动 pChronicle,由 sidecar 将 JSON 事件写入 warehouse | -| `--record-format lance` | 启动 `pchronicle serve --control 127.0.0.1:0 `,通过 control 协议写 canonical Lance | +### 3. 本地 producer 通过 Control 服务提交 -旧的 `--chronicle-mode`、`--chronicle-dir` 和 `--pchronicle-binary` 不再是 pVisor -CLI 参数。pVisor 库/配置仍可通过 `chronicle.binary` 选择 sidecar executable;orchestration layer -自己的 `--pchronicle-binary` 不属于 pVisor CLI。pVisor 管理自己启动的 child 生命周期; -child 退出、握手失败或协议版本不兼容都会显式使持久化路径失败。 +`pchronicle serve --control 127.0.0.1:0 DATASET` 提供本地持久化入口。 +`persisting-events` 的进程 client 负责启动子进程、握手与请求关联。 +子进程退出、握手失败或协议版本不兼容都使持久化请求显式失败。 -pVisor 与 Gateway producer 只构造 `EventRecord`。它们 MUST NOT 选择 Lance row、执行 -DataFusion query,或根据 storage URI 加载对象存储 SDK。 +使用此协议的 producer 提交 `EventRecord`;Lance row、DataFusion 查询与对象存储 +实现由 pChronicle 管理。 ### 4. Control 协议随事件契约发布 @@ -98,9 +80,8 @@ request ID 与令牌;client 校验响应版本和 request ID。frame 大小有 ### 5. ACK、背压与不确定性 -pVisor 到 sidecar 的 append 队列是有界的。入队使用 `try_send`:队列已满或 worker 已 -关闭时,事件被明确拒绝,调用方可以复用该 `seq`。成功入队后调用方等待 sidecar 响应; -只有 pChronicle 完成 append 并返回成功时,事件才对 pVisor 视为 durable。 +Gateway 到存储的 append 队列是有界的。提交被拒绝与提交后的结果不确定必须区分。 +只有 pChronicle 完成 append 并返回成功时,事件才视为 durable。 连接中断、写入错误或 ACK 丢失无法证明事件未提交,必须分类为 unknown。此时 producer 消耗该序号,不能把不确定写入伪装为“肯定未写”。事实层仍允许 at-least-once 与重复 @@ -112,7 +93,7 @@ pVisor 到 sidecar 的 append 队列是有界的。入队使用 `try_send`:队 ## 依赖边界 ```text -pVisor / Gateway / orchestration layer +Gateway / local event producers │ │ EventRecord + optional control protocol ▼ @@ -127,16 +108,13 @@ pVisor / Gateway / orchestration layer └── Catalog / query / projection ``` -默认 pVisor dependency graph MUST NOT 通过 Chronicle 写路径引入 Lance、Arrow、DataFusion -或云对象存储 SDK。其他 pVisor 组件若为了非存储的格式/投影 helper 暂时形成到 -pChronicle 的间接依赖,不改变本 RFC 的 ownership,但 SHOULD 在后续边界整理中消除; -不得借此让 pVisor 重新直接写存储。 +`persisting-events` 的默认 feature 和可选 `control` feature 均不得引入 Lance、Arrow、 +DataFusion 或云对象存储 SDK。物理存储与查询实现只由 pChronicle 维护。 ## 兼容与迁移 - `EventRecord` 的 JSON 顶层字段保持扁平,移动 crate 不改变既有 wire 形状; - pChronicle 对外 re-export 公共事件类型,允许调用方渐进迁移 import; -- pVisor 调用方应迁移到 `--record-format {json,lance}` 与 `--record-destination PATH|URI`;旧 Chronicle CLI 参数不再接受; - `persisting-pchronicle-client` 被删除,使用者改为 `persisting-events = { features = ["control"] }`; - pChronicle 的既有 Lance dataset、目录布局和 replay 语义不因这次 crate 拆分而变化。 @@ -145,18 +123,18 @@ pChronicle 的间接依赖,不改变本 RFC 的 ownership,但 SHOULD 在后 | 方案 | 原因 | |---|---| -| pVisor 继续内嵌 Lance adapter | 执行器与存储引擎生命周期、feature 和依赖重新耦合 | +| producer 内嵌另一套 Lance adapter | producer 与存储引擎生命周期、feature 和依赖重新耦合 | | `EventRecord` 继续由 pChronicle 定义 | producer 为使用基础事件信封被迫依赖存储产品 | | 单独保留 `persisting-pchronicle-client` | 协议包过碎;control 契约可以作为事件边界的可选 feature | -| pVisor 与 orchestration layer 各写一套 IPC client | 会产生协议漂移、重复认证与错误语义 | -| pVisor 写 JSONL,pChronicle 以后导入 | 缺少运行期 durable ACK、fencing 与统一 canonical append 语义 | +| 各集成方分别实现 IPC client | 会产生协议漂移、重复认证与错误语义 | +| 仅依赖离线导入而无实时 Control 协议 | 缺少运行期 durable ACK、fencing 与统一 canonical append 语义 | ## 验收条件 - Workspace 只新增 `persisting-events`,不存在 `persisting-pchronicle-client`; -- pVisor、Gateway、pChronicle 复用同一 `EventRecord`,没有同构公共事件 struct; -- pVisor 默认构建不直接依赖 pChronicle 存储 API,也不链接 Lance/DataFusion; -- `spawn` 模式能够启动 sidecar,持久写入有序事件并发布 Attempt 终态; +- Gateway、producer、pChronicle 复用同一 `EventRecord`,没有同构公共事件 struct; +- `persisting-events` 不直接依赖 pChronicle 存储 API,也不链接 Lance/DataFusion; +- 进程 client 能够启动 Control 服务,持久写入有序事件并发布 Attempt 终态; - 协议版本、认证、request correlation、frame limit、拒绝与 unknown 写入语义有测试; - pChronicle 的原有 replay、query 与物理存储测试继续通过。 @@ -164,4 +142,4 @@ pChronicle 的间接依赖,不改变本 RFC 的 ownership,但 SHOULD 在后 | Version | Date | Notes | |---|---|---| -| Accepted | 2026-08-16 | 拆出 `persisting-events`,移除 client crate,以 pChronicle sidecar 替代 pVisor 内嵌存储 | +| Accepted | 2026-08-16 | 拆出 `persisting-events`,移除 client crate,确立逻辑事件与 pChronicle 存储服务的边界 | diff --git a/docs/src/en/roadmap.md b/docs/src/en/roadmap.md index 2c176e094..bbfd0546e 100644 --- a/docs/src/en/roadmap.md +++ b/docs/src/en/roadmap.md @@ -10,12 +10,12 @@ source of truth for delivered behavior. lookup useful without a service or account. - Keep English and Chinese documentation paths aligned and examples runnable. -## Next: connect execution to durable history +## Next: improve capture and analysis - Preserve Run identity and lineage across capture, normalization, and query. - Improve comparison workflows for Runs, Sessions, and revisions. -## Later: move from one workstation to a fleet +## Later: share trajectory data across teams - Share Dataset catalogs and policies across teams without hiding provenance. @@ -27,4 +27,4 @@ source of truth for delivered behavior. An item is not complete because a design document exists. Look for a working CLI path, tests or examples, documented limitations, and a release entry before treating a capability as available. Proposed changes belong in an RFC when -they change a data contract, execution boundary, or public command. +they change a data contract, storage boundary, or public command. diff --git a/docs/src/en/system-design/architecture.md b/docs/src/en/system-design/architecture.md index 4512727b8..1a9d3517f 100644 --- a/docs/src/en/system-design/architecture.md +++ b/docs/src/en/system-design/architecture.md @@ -1,210 +1,65 @@ -# End-to-end architecture +# System architecture -> pVisor and pPilot are maintained in external repositories. This repository -> contains pChronicle and the libraries needed for capture and durable history. +pChronicle captures, stores, queries, and exchanges Agent trajectory history. +This page defines the boundaries between its engine, interfaces, and supporting +libraries. Physical layouts are documented in [pChronicle Design](../pchronicle/design/index.md). -This document defines the contracts between Persisting products. Provider -mechanisms belong to pVisor Design; storage layouts belong to pChronicle Design; -commands belong to each product's Reference. +## Component ownership -![Persisting product domains and integration](../../assets/diagrams/persisting/system-products.svg) +| Component | Responsibility | +|---|---| +| `persisting-pchronicle` | Trajectory models, storage, Source discovery, Dataset Snapshots, bounded queries, format conversion, and revision lineage | +| `persisting-pchronicle-cli` | The `pchronicle` command, local read-only Warehouse API, optional Control service, Gateway configuration, and embedded Web assets | +| `pchronicle-web` | Dataset browsing and query UI | +| `persisting-events` | Storage-independent `EventRecord` and optional versioned Control client/protocol | +| `persisting-gateway` | Model protocol adaptation, forwarding, session correlation, and trajectory capture | +| `persisting-overlaynet` | Proxy transport, request classification, and policy for intercepted traffic | +| `persisting-agentctl` | Shared control types, policy transitions, and cooperative client protocol | -## Product ownership - -| Product or layer | Owns | Does not own | -| --- | --- | --- | -| `persisting-events` contract | storage-independent `EventRecord` identity/envelope and the optional versioned pChronicle control protocol | storage rows, storage engines, query, or projection | -| pVisor | one Run, its Attempts, execution environment, capability admission, effects, and runtime evidence | many-Run scheduling or durable history queries | -| pChronicle | Agent trajectory storage engine: path identity, Snapshot, canonical events, projections, query, and exchange | starting, scheduling, or controlling a Run | -| Runtime provider | one physical execution mechanism | logical Run identity or product policy | - -Gateway, OverlayFS, and OverlayNet are pVisor runtime mechanisms. They do not -form independent control planes. - -## Runtime placement and platform boundary - -The logical Run contract is portable across providers, but the enforcement -boundary follows the selected platform: - -| Placement | Workload boundary | Workspace behavior | Security qualification | -| --- | --- | --- | --- | -| Linux host | private user/mount/PID namespaces plus Landlock | staged FUSE workspace | filesystem and network capabilities are reported separately; unavailable setup fails before execution | -| macOS host | Seatbelt where available, with staged macFUSE writes | staged host workspace | safe best-effort host isolation; host kernel and ambient reads remain visible in Evidence | -| Linux or Apple Silicon macOS VM | guest kernel with an OCI or prepared Linux rootfs | staged workspace inside the guest | stronger kernel boundary, while the macOS VMM still runs with the invoking user's host authority | -| native OCI container | OCI runtime and bundle selected by pVisor | bundle-mounted rootfs and staged paths | container isolation is recorded; it is not treated as a complete hostile multi-tenant boundary | - -The provider reports requested versus effective capability dimensions in the Run -Bundle. A successful process exit does not imply that the requested boundary -was installed, and a workspace stage remains reviewable independently of the -provider that produced it. See pVisor isolation design (external repository) -and the execution guide (external repository) for provider-specific -behavior and prerequisites. - -## Independent ingress paths - -```text -Configured runtime capture - Gateway trajectory events ─┐ - pVisor lifecycle records ──┴─> canonical event Source ──────────────┐ -Pinned external Sources │ - local/S3 ATIF, ACTF, OpenAI Messages files ──────────────────────────┼─> Snapshot - local/S3 Storyline Sources ──────────────────────────────────────────┘ - └─> normalized Dataset views -``` - -pVisor completes its standalone loop with a terminal RunResult, staged -Effects, and a private, versioned Run Bundle. Configured capture is not a pVisor -runtime prerequisite. External file and Storyline Sources are pinned and -normalized directly; they neither pass through pVisor nor become canonical -runtime events, and they do not acquire pVisor execution guarantees. - -## Stable objects - -```text -RunSpec - └── Run - ├── Attempt 1 - ├── Attempt 2 - └── Attempt finalization - ├── terminal RunResult - ├── private versioned Run Bundle - └── staged Effects → later review / apply / drop - -Optional configured event handoff - └── Gateway trajectory events + pVisor lifecycle records -``` - -The logical Run is portable. An Attempt is provider-specific. Infrastructure -retry creates another Attempt; a semantic retry creates a derived Run. A Run -may have multiple Attempts but only one visible terminal result. - -Where a Source carries it, the stable cross-product identity is `run_id`. -Session, Step, call, event, and Artifact identities remain scoped and retain -their Source lineage. A process ID, container ID, VM ID, or worker lease is -never a substitute for Run identity. - -## Single-Run path +## Capture and import ```text -User or Agent framework - → RunSpec - → pVisor admission - → capability-by-dimension provider selection - → Attempt execution - → terminal RunResult + private versioned Run Bundle + staged Effects - → later review / apply / drop +Agent / SDK requests + → Gateway protocol handling and capture + → EventRecord + → pChronicle canonical event storage + → Dataset Snapshot → bounded queries → CLI / Web / export + +Supported files and object-store Sources + → discovery and pinned Source versions + → Storyline normalization and query projections + → Dataset Snapshot → bounded queries → CLI / Web / export ``` -Admission compares requested capability dimensions with evidence the selected -provider can produce. A required dimension that cannot be enforced fails before -workload execution. Optional degradation is recorded explicitly in the Run -Bundle. +Gateway capture records traffic routed through it. Import reads supported +external formats directly. Both paths preserve available identities and source +provenance; neither supplies missing execution or isolation evidence. -Filesystem promotion is an Effect decision, not the Run terminal commit. -Selected paths can be applied more than once while the stage remains available. -Network requests and remote tool mutations are separate effect dimensions and -cannot be inferred from filesystem state. +## Write and read boundaries -When configured, pVisor publishes Gateway trajectory events plus `run.created`, -`run.state_changed`, and terminal lifecycle records to pChronicle. Those records -carry Run/Attempt identity, lifecycle facts, and available event-carried -Evidence. Artifact references, lineage, staged filesystem Effects, -AgentCtl/network/resource Evidence, and the full Run Bundle remain local unless -a separate adapter moves them. +The CLI may call the engine in process. Integrations can also use +`pchronicle serve --control 127.0.0.1:0 DATASET` through the authenticated, +versioned local protocol in `persisting-events`. Only a successful append ACK +confirms durability. A lost response leaves the outcome uncertain, and callers +must not treat it as proof that nothing was written. -## Dataset path - -Canonical runtime writers and pinned external Sources are independent Source -paths. They converge only at the Snapshot and normalized Dataset views: - -```text -configured Gateway and pVisor lifecycle writers - → canonical event Source ────────────────────────────────┐ -pinned local/S3 external Sources │ - → ATIF / ACTF / OpenAI Messages files ───────────────────┼─> Snapshot - → Storyline Sources ─────────────────────────────────────┘ ├─> normalized Run / Step / ToolCall views - └─> query / export / revision lineage -``` - -Canonical facts are append-oriented. Storyline and other normalized views are -rebuildable projections. Exchange files are interoperability boundaries, not a -replacement source of truth. Each read operation fixes a Snapshot; it -does not invent a global transaction across unrelated Sources. Pinning an -external file does not convert it into a canonical runtime event Source. - -## Source-specific guarantees - -| Source path | Supported claim | Explicit non-claim | -| --- | --- | --- | -| External file or imported Source | discovered content, pinned Source version, normalized representation, and recorded conversion lineage where implemented | completeness of an external task manifest or absence of unreported trajectories | -| Gateway capture | requests and responses observed and durably published through the configured Gateway path | absence of traffic that bypassed Gateway | -| pVisor Run | Run/Attempt identity, recorded terminal facts, installed mechanisms, observed Effects, and provider-specific Evidence | enforcement a selected provider did not supply | - -Ingestion preserves these boundaries. A normalized representation or Catalog -Snapshot does not upgrade the evidence supplied by its Source. - -The default pVisor build does not link Lance or DataFusion. Configured -Chronicle publication starts a pChronicle sidecar over authenticated loopback -IPC and treats only a successful sidecar acknowledgement as durable. The -legacy mode name `lance` is an alias for `spawn`; pVisor no longer writes Lance -itself. Sidecar flags and mode names belong to the -pVisor CLI reference (external repository) and -[RFC-0007](../rfcs/0007-events-contract-pchronicle-sidecar.md). - -## Failure and recovery - -| Failure | Owner | Required behavior | -| --- | --- | --- | -| Attempt exits or provider disappears | pVisor | finalize evidence; expose failure or create a fenced replacement Attempt | -| sidecar append queue is saturated or closed | pVisor/Gateway producer | reject before submission and report the failure; do not claim durability | -| append connection or acknowledgement is lost | producer and pChronicle writer | preserve the write as unknown because it may have committed; do not reuse its sequence as if definitely rejected | -| history publication conflicts | pChronicle writer | preserve the previously published Snapshot; surface or retry according to the writer contract | -| view generation fails | pChronicle | keep canonical facts readable; rebuild the derived view | - -Recovery never upgrades uncertainty into success. A missing terminal fact, a -lost callback, and an unenforced capability remain visible states. - -## Security and evidence chain - -Security is reported per capability dimension. pVisor records requested policy, -installed mechanism, provider identity, enforcement result, and observed -effects. Configured pChronicle capture stores lifecycle facts and only the -Evidence carried by Gateway or lifecycle event records; the broader Run Bundle -evidence inventory remains local unless moved separately. - -This produces a chain rather than a boolean label. The local Run evidence -chain does not mean every layer is automatically published into durable -history: - -```text -requested policy - → admission decision - → installed mechanism - → provider-bound evidence - → observed effects - → terminal result - -Optional configured persistence - Gateway trajectory events + pVisor lifecycle records - → event-carried Evidence only - → pChronicle durable history -``` +pChronicle owns the physical schema, writer fencing, manifest publication, and +maintenance. Producers submit logical records rather than defining parallel +storage layouts. See [RFC-0007](../rfcs/0007-events-contract-pchronicle-sidecar.md) +and [trajectory storage](../pchronicle/design/trajectory-storage.md). -See [Security and evidence](security-evidence.md) for evidence levels and -[Local to fleet](local-to-fleet.md) for portability requirements. +The Warehouse HTTP API and Web UI are read-only. The separately enabled Control +and Gateway capture paths can write. Public bind addresses are rejected by the +local server; the Control protocol is intended for trusted local processes. -## Public boundaries +## Facts, views, and versions -| Boundary | Contract owner | Detailed document | -| --- | --- | --- | -| logical runtime event and local Chronicle control protocol | `persisting-events` | [RFC-0007](../rfcs/0007-events-contract-pchronicle-sidecar.md) | -| Agent execution and Effect review | pVisor | pVisor concepts (external repository) and guides (external repository) | -| provider and runtime mechanisms | pVisor | pVisor design (external repository) | -| Dataset, facts, and projections | pChronicle | [pChronicle concepts](../pchronicle/concepts/index.md) | -| storage and Snapshot implementation | pChronicle | [pChronicle design](../pchronicle/design/index.md) | -| stable command syntax and formats | each product | pVisor reference (external repository) and [pChronicle reference](../pchronicle/reference/index.md) | -| normative ownership decisions | Project RFCs | [RFC index](../rfcs/index.md) | +Canonical events retain their original logical payload. Storyline provides the +normalized model for supported exchange formats; projections support analysis +without replacing the original facts. Queries read a pinned Snapshot, and +revision lineage identifies derived outputs. -This document changes only when a cross-product contract changes. Product -implementation status and roadmap details belong to their owning Design pages -or Project engineering notes. +Capture coverage is limited to the records a Source contains. Proxy policy +applies to traffic that reaches the proxy and does not establish process-wide +network isolation. See [Gateway capture](../pchronicle/guides/serve-gateway.md) +and [facts and projections](../pchronicle/concepts/facts-and-projections.md). diff --git a/docs/src/en/system-design/design-principles.md b/docs/src/en/system-design/design-principles.md index 39bf613dd..843e037ae 100644 --- a/docs/src/en/system-design/design-principles.md +++ b/docs/src/en/system-design/design-principles.md @@ -1,39 +1,32 @@ # Design principles -> pVisor and pPilot are maintained in external repositories. This repository -> contains pChronicle and the libraries needed for capture and durable history. +## Keep facts and projections distinct -These principles explain why Persisting has separate products and why the -documentation emphasizes reviewable steps. +Canonical events preserve recorded facts. Normalized Storyline views and query +projections remain traceable to their Sources; missing records stay visible as +limits on the answer. -## Boundaries are explicit +## Make ownership explicit -pVisor describes the execution boundary that was actually installed. pChronicle -describes the Source and Dataset that were actually observed. Neither product -silently upgrades a missing control or incomplete Source into a stronger claim. +Shared event contracts describe logical records. pChronicle owns storage, +query, and exchange, while Gateway owns capture and protocol adaptation. +The CLI and Web UI use those boundaries rather than defining new data models. -## Writes are reversible until reviewed +## Preserve provenance and versions -Agent Effects remain staged until a person or an explicit policy applies them. -Review is part of the workflow, not a report added after the write. +An answer should identify the Dataset, Source, Snapshot, and query that produced +it. Revision lineage keeps derived outputs connected to their inputs across +normalization and export. -## Evidence travels with the result +## Bound the work -A summary should point back to the Run, Dataset, Source, or query that produced -it. Lineage is useful only when it survives export, normalization, and later -inspection. +Query budgets, bounded capture queues, and explicit append acknowledgements +make resource use and write outcomes inspectable. A failed projection must not +be reported as a successful durable append. -## Execution and history stay composable +## Keep data portable -pVisor can run without pChronicle, and pChronicle can analyze external Sources -without pVisor. The integration is a narrow capture contract so each product -remains useful on its own. +Documented formats and the CLI make Datasets usable without a particular +viewer. Local files and object-store Sources share the Dataset model. -## Portable data beats a privileged viewer - -Datasets, query results, and Run records should remain inspectable through the -CLI and documented formats. A web view can improve discovery, but it should not -be the only way to recover an answer. - -See the [system overview](index.md) and the [roadmap](../roadmap.md) for how -these principles shape current delivery. +See the [system overview](index.md) and [roadmap](../roadmap.md). diff --git a/docs/src/en/system-design/index.md b/docs/src/en/system-design/index.md index 76e8a2a21..54bbdd6b7 100644 --- a/docs/src/en/system-design/index.md +++ b/docs/src/en/system-design/index.md @@ -1,64 +1,24 @@ -# System Design +# System design -> pVisor and pPilot are maintained in external repositories. This repository -> contains pChronicle and the libraries needed for capture and durable history. - -Persisting provides durable Agent trajectory history. This section describes -how pChronicle integrates with external execution components: - -- pVisor (external repository) virtualizes and governs one Agent Run; -- [pChronicle](../pchronicle/index.md) organizes durable trajectory Sources into - queryable Datasets. - -Gateway and OverlayNet support trajectory capture here; OverlayFS belongs to -the external execution components. Where -available, stable Run identity connects the domains, but each also has a -standalone entry path. - -![Persisting product domains and integration](../../assets/diagrams/persisting/system-products.svg) - -## Cross-product contract +Persisting centers on [pChronicle](../pchronicle/index.md), an Agent trajectory +storage engine. Gateway captures model traffic, shared event contracts carry +records into storage, and the CLI and Web UI expose Datasets for inspection. ```text -Configured pVisor capture - Gateway trajectory events ─┐ - pVisor lifecycle records ──┴─> canonical event Source ─┐ -Pinned external Sources │ - ATIF / ACTF / OpenAI Messages / Storyline ─────────────┴─> Snapshot - └─> normalized Dataset views +Gateway capture ── EventRecord ── pChronicle storage ─┐ +ATIF / ACTF / OpenAI Messages / Storyline Sources ────┴─> Dataset Snapshot + └─> query / exchange / Web UI ``` -Attempt finalization writes a private, versioned Run Bundle and leaves Effects -staged for later review/apply/drop without pChronicle. Configured capture sends -Gateway trajectory events and pVisor lifecycle records, including the Evidence -those records carry. The full Bundle and its Artifact, lineage, Effect, and -broader Evidence inventory remain local unless moved separately. - -External file and Storyline Sources are pinned and normalized directly without -passing through pVisor or becoming canonical events. Each path retains -source-specific guarantees; ingestion does not add Evidence that its Source did -not provide. - -The ownership boundary is deliberately simple: - -- **pVisor owns execution.** It defines one Run's boundary and its model, - network, and filesystem runtime drivers. Its private Run Bundle remains useful - even when no history is captured. -- **pChronicle owns history.** It records canonical events and terminal facts, - then provides Dataset queries, exchange, and revision lineage. - -This separation is a practical choice for users: start with execution or -history independently, and add the capture handoff only when the question you -need to answer crosses both domains. +Canonical events preserve recorded facts. Storyline normalization and query +projections make supported Sources usable through common views. A Snapshot pins +the Source versions used to answer a query; importing data does not add facts +that the Source did not provide. ## Continue by question -- [Complete architecture and target model](architecture.md) -- [Local-to-fleet continuity](local-to-fleet.md) -- [Security and evidence model](security-evidence.md) -- pVisor implementation boundaries (external repository) -- [pChronicle implementation boundaries](../pchronicle/design/index.md) - -Delivery state is reported in the product Design pages and -[Project Engineering Notes](../project/engineering.md). Target architecture is -not evidence that a capability is implemented. +- [Component ownership and data flow](architecture.md) +- [Design principles](design-principles.md) +- [Storage and query implementation](../pchronicle/design/index.md) +- [Gateway capture](../pchronicle/guides/serve-gateway.md) +- [Project engineering notes](../project/engineering.md) diff --git a/docs/src/en/system-design/local-to-fleet.md b/docs/src/en/system-design/local-to-fleet.md deleted file mode 100644 index a24a5def0..000000000 --- a/docs/src/en/system-design/local-to-fleet.md +++ /dev/null @@ -1,30 +0,0 @@ -# Local to fleet - -> pVisor and pPilot are maintained in external repositories. This repository -> contains pChronicle and the libraries needed for capture and durable history. - -The portable unit is a logical Run, not a live virtual machine. - -![The AgentVisor execution continuum](../../assets/diagrams/agentvisor/execution-continuum.svg) - -Across local and fleet placement, the following must remain stable: - -- Run identity and parent/child lineage; -- delegated authority and its generation; -- semantic checkpoint and effect frontier; -- artifact identity and durable evidence; -- terminal result ownership. - -The process, kernel, root filesystem, node, scheduler, and execution provider -may change. A provider is admissible only when it can satisfy the capability -dimensions requested by the Run. Unsupported guarantees must fail explicitly; -they must not silently become weaker after migration. - -On a personal device, the main experience is a staged workspace and reviewable -effects. In a fleet, the same model adds placement, tenant isolation, leases, -attestation, recovery, and reconciliation without redefining the Run. - -The stable identity model is defined in -Run, Attempt, and Effect (external repository). Provider admission -belongs to pVisor isolation (external repository). Fleet coordination—placement, leases, and reconciliation—belongs to the -deployment control plane and does not change the logical Run contract. diff --git a/docs/src/en/system-design/security-evidence.md b/docs/src/en/system-design/security-evidence.md deleted file mode 100644 index 48db3c4c8..000000000 --- a/docs/src/en/system-design/security-evidence.md +++ /dev/null @@ -1,54 +0,0 @@ -# Security and evidence model - -> pVisor and pPilot are maintained in external repositories. This repository -> contains pChronicle and the libraries needed for capture and durable history. - -Persisting does not compress security into one `safe` or `sandboxed` label. -Every Run reports guarantees by capability dimension. pVisor owns admission -and runtime enforcement. Placement and recovery mechanisms do not upgrade a -pVisor evidence level. -Configured pChronicle capture preserves lifecycle facts and only the Evidence -carried by Gateway or lifecycle event records. The full Run Bundle evidence -inventory remains local unless moved separately. - -| Dimension | Example mechanism | Evidence question | -| --- | --- | --- | -| Filesystem read | synthetic root, allowlisted projection | Which host paths were visible? | -| Filesystem write | staged OverlayFS, Landlock, Seatbelt | Where could the process tree write? | -| Network | private namespace, virtio-net, proxy policy | Could direct sockets bypass policy? | -| Process | namespace, sandbox profile, inherited-FD cleanup | Which descendants shared the boundary? | -| Credentials | Run-scoped delivery and expiry | Which identity received and used the secret? | -| Effects | stage, promotion decision, compensation record | Which consequences reached the real system? | - -Evidence has four useful levels: - -1. **Declared** — configuration requested a boundary. -2. **Mediated** — an Agent-facing path passed through a control point. -3. **Enforced** — bypass paths in the stated threat model were blocked. -4. **Attested** — enforcement evidence is bound to the exact Run and provider. - -A strong guarantee in one dimension does not upgrade another dimension. A -staged workspace is not proof of network isolation, and captured traffic is not -proof that unobserved sockets were impossible. - -The end-to-end chain is: - -```text -requested capability - → admission decision - → installed mechanism - → provider evidence - → observed Effect - → terminal result - → configured event-carried history -``` - -This final event path is narrower than the Run Bundle: it does not currently -publish the complete Artifact, lineage, filesystem Effect, -AgentCtl/network/resource Evidence, output, or metrics inventory. - -Read Capabilities and evidence (external repository) -for the user model, pVisor isolation design (external repository) and -OverlayNet (external repository) for mechanisms, and -[Facts and projections](../pchronicle/concepts/facts-and-projections.md) for the -history boundary. diff --git a/docs/src/en/why-persisting.md b/docs/src/en/why-persisting.md index 262996210..cb62a2c0f 100644 --- a/docs/src/en/why-persisting.md +++ b/docs/src/en/why-persisting.md @@ -19,24 +19,23 @@ Persisting focuses on durable Agent history: Every workflow should make three things easy to answer: -1. What was the Agent allowed to do? +1. Which Agent, model, and tool calls were recorded? 2. What actually changed or happened? 3. Which evidence and history support the answer? -Persisting does not claim that a successful command proves a perfect boundary. -It records the mechanisms, limitations, Effects, and evidence that were -actually available. +Answers remain tied to the recorded Sources and their versions. Missing +records remain a limit on what can be concluded. ## When Persisting fits Use pChronicle when trajectory history should remain useful after a terminal -session ends. pVisor and pPilot are maintained in external repositories. +session ends. If you only need a one-off script with no review or history requirement, Persisting may be more infrastructure than the task needs. ## The design direction -Persisting is built around explicit boundaries, inspectable evidence, reversible -writes, and portable data. These principles guide the [system design](system-design/index.md) +Persisting is built around explicit data ownership, inspectable Sources, +versioned snapshots, and portable data. These principles guide the [system design](system-design/index.md) and the current [roadmap](roadmap.md). diff --git a/docs/src/zh/installation.md b/docs/src/zh/installation.md index f8ed47778..a549391bf 100644 --- a/docs/src/zh/installation.md +++ b/docs/src/zh/installation.md @@ -1,6 +1,6 @@ # 安装 -本仓库发布 `pchronicle` 命令和内嵌 Web UI。pVisor 与 pPilot 已拆分到外部仓库。 +本仓库发布 `pchronicle` 命令和内嵌 Web UI。 ## 安装 diff --git a/docs/src/zh/pchronicle/design/architecture.md b/docs/src/zh/pchronicle/design/architecture.md index 089850de2..e5646977c 100644 --- a/docs/src/zh/pchronicle/design/architecture.md +++ b/docs/src/zh/pchronicle/design/architecture.md @@ -1,7 +1,7 @@ # pChronicle 架构 本文解释 pChronicle 如何存储 Agent 轨迹,并提供有资源限制的读取面。用户工作流属于 -[Guides](../guides/index.md),精确命令属于[Reference](../reference/cli.md),跨产品 ownership +[Guides](../guides/index.md),精确命令属于[Reference](../reference/cli.md),组件职责 属于 [System Design](../../system-design/architecture.md)。 ![pChronicle 产品边界](../../../assets/diagrams/persisting/pchronicle-product.svg) diff --git a/docs/src/zh/pchronicle/design/index.md b/docs/src/zh/pchronicle/design/index.md index 423d8ef3b..4b4369cc4 100644 --- a/docs/src/zh/pchronicle/design/index.md +++ b/docs/src/zh/pchronicle/design/index.md @@ -12,5 +12,5 @@ Snapshot,以及记录事实与派生视图的区分见 | Canonical event 与 projection ownership | [运行存储](trajectory-storage.md) | | Storyline 三表 projection 与内容层 | [Storyline Lance](storyline-lance.md) | -当前命令与格式见 [pChronicle Reference](../reference/index.md),跨产品 ownership 见 +当前命令与格式见 [pChronicle Reference](../reference/index.md),组件职责见 [System Design](../../system-design/index.md)。 diff --git a/docs/src/zh/pchronicle/design/trajectory-storage.md b/docs/src/zh/pchronicle/design/trajectory-storage.md index c36d94e83..c776d3d35 100644 --- a/docs/src/zh/pchronicle/design/trajectory-storage.md +++ b/docs/src/zh/pchronicle/design/trajectory-storage.md @@ -20,8 +20,8 @@ - events、Storyline、ATIF、ACTF、OpenAI messages、AgenticMD 之间的格式转换; - materialize、revision lineage 和标准查询视图。 -`persisting-events` 拥有存储无关的逻辑事件信封。Gateway 与 pVisor 负责产出事件;CLI -可以在进程内调用 pChronicle,pVisor 也可以通过 `pchronicle serve` 的 Control 服务提交。 +`persisting-events` 拥有存储无关的逻辑事件信封。Gateway 负责产出事件;CLI +可以在进程内调用 pChronicle,本地 producer 也可以通过 `pchronicle serve` 的 Control 服务提交。 这些 producer 都不定义第二套运行数据落盘格式。 ## 2. 逻辑坐标 @@ -59,7 +59,7 @@ Gateway 的 durable 微批写入每累计 8 个小 fragment 就 seal 一个 L0 s segment 数按层级增长而不是随事件线性增长;旧 version/file 仍按 maintenance 的保留期 vacuum,避免破坏已经固定旧快照的 reader。 物理 schema 把 `event_id` 提升为独立业务列,并把 `timestamp` 规范化为 UTC -`Timestamp(Millisecond)`。新写入的 Gateway 与 pVisor `EventRecord` 会同时提供 RFC3339 +`Timestamp(Millisecond)`。新写入的 Gateway `EventRecord` 会同时提供 RFC3339 `timestamp` 和 `timestamp_unix_ms`;两者必须在毫秒级一致。admission 仍会为旧 producer 或兼容导入根据 RFC3339 `timestamp` 或接收时间补齐缺失值。Storyline 投影也从 `timestamp_unix_ms` 生成 UTC 毫秒文本,输入文本时间戳保存在 `payload_json`。事实层不检查 @@ -73,9 +73,7 @@ AgenticMD 是面向人的 Markdown 调试视图。它保存可见对话块和会 代码审阅与人工分析。它会省略协议噪声,字段也允许缺失或扩展,因此不是存储格式或 原始 HTTP 事件的无损替代。 -`pvisor run --record-format lance --record-destination WAREHOUSE` 启动 pChronicle sidecar, -由 sidecar 写 canonical Lance events;pVisor 本身不打开 Lance。 -`--gateway-stream-markdown` 可同时维护 live AgenticMD。Markdown 是诊断投影,Dataset +Gateway 捕获通过 pChronicle 写入 canonical events。Live AgenticMD 是诊断投影,Dataset 消费统一使用 pChronicle API 和 `pchronicle` 命令。 ### Storyline 三表 Lance @@ -170,7 +168,6 @@ OpenAI msg ┘ |---|---|---| | Gateway | 协议解析、调用生命周期、采集顺序、live projection 策略 | 通用 store、格式 schema、离线转换 | | pChronicle | 格式、路径、落盘、读取、转换与 revision lineage | 网络转发、Agent 生命周期 | -| pVisor | Run 生命周期及 Gateway/OverlayNet/OverlayFS 装配 | 长期运行数据 schema | ## 8. 相关文档 @@ -178,6 +175,5 @@ OpenAI msg ┘ - [发现并查询](../guides/discover-and-query.md) - [Snapshot](catalog.md) - [AgenticMD 格式](../reference/agenticmd.md) -- Gateway 架构 (外部仓库) -- pVisor 命令 (外部仓库) +- [Gateway 捕获](../guides/serve-gateway.md) - [`pchronicle` Dataset 命令](../reference/cli.md) diff --git a/docs/src/zh/pchronicle/guides/index.md b/docs/src/zh/pchronicle/guides/index.md index 2ae881f03..a67e69e73 100644 --- a/docs/src/zh/pchronicle/guides/index.md +++ b/docs/src/zh/pchronicle/guides/index.md @@ -10,5 +10,3 @@ Guide 解释决策并完成工作流;精确参数查阅 [`pchronicle` 命令行指南](../reference/cli.md),仓库内 fixture 见[项目示例](../../project/examples.md)。 - -pChronicle 拥有持久历史;执行控制属于 pVisor。 diff --git a/docs/src/zh/pchronicle/guides/serve-gateway.md b/docs/src/zh/pchronicle/guides/serve-gateway.md index 72d39a784..da39ae2e3 100644 --- a/docs/src/zh/pchronicle/guides/serve-gateway.md +++ b/docs/src/zh/pchronicle/guides/serve-gateway.md @@ -53,9 +53,8 @@ span/秒和 p50/p95/p99 延迟。三个环境变量可以按硬件调整规模 选择 upstream,按需改写模型和 wire protocol,再以客户端协议返回响应,同时把 canonical capture events 追加到 CLI 指定的输出 Dataset。Dataset Web UI 和 API 仍然保持只读。 -当 Agent 或 SDK 已经能够调用 OpenAI、Anthropic 或 Gemini 兼容的 base URL,而你希望不 -启动 pVisor Run 就捕获这些流量时,可以使用这个模式。如果 Gateway 需要与 Agent 执行共享 -生命周期和隔离边界,应改用 pVisor 捕获 (外部仓库)。 +当 Agent 或 SDK 已经能够调用 OpenAI、Anthropic 或 Gemini 兼容的 base URL,而你希望把 +这些流量捕获到 Dataset 时,可以使用这个模式。 ## 配置输入 @@ -163,7 +162,7 @@ Capture metadata 会区分客户端请求模型和实际 upstream 模型,并 | `network` | 否 | `mode = "public"` | 显式 forward-proxy 流量的策略。 | 共享 Gateway schema 还接受 `[overlay]`,但 `pchronicle serve` 不会创建或 apply 文件系统 -overlay。Overlay 生命周期属于 pVisor (外部仓库)。 +overlay。 ### 捕获级别 @@ -332,8 +331,7 @@ allowed_hosts = ["pypi.org", "files.pythonhosted.org", "*.github.com"] `public` 是默认值;`no-network` 拒绝显式 proxy egress;`allowlist` 要求请求命中 `allowed_hosts` 或结构化 `[[network.rules]]`。显式 `[[network.deny_rules]]` 的优先级高于 -allow。若策略必须成为 Agent 进程不可绕过的边界,应使用 pVisor;`pchronicle serve` 只能 -控制客户端主动发送到 Gateway 的流量。 +allow。`pchronicle serve` 只能控制客户端主动发送到 Gateway 的流量,不提供进程级网络隔离。 ## Dataset 与状态目录选择 diff --git a/docs/src/zh/pchronicle/index.md b/docs/src/zh/pchronicle/index.md index daa609dff..d3f86826a 100644 --- a/docs/src/zh/pchronicle/index.md +++ b/docs/src/zh/pchronicle/index.md @@ -9,7 +9,7 @@ Agent 的经验,是它做过的一切。**pChronicle 是 Agent 轨迹存储引擎**:它以真正有意义的 单位——Run(运行)——记录这些经验,让每一次运行都更易于理解与改进。可用于浏览、查询、 交换和服务运行 Dataset;既可以读取 Persisting 产生的运行记录,也可以直接读取受支持的外部 -格式;不要求先运行 pVisor。 +格式。 在 Persisting 里,pChronicle 负责保存与查询轨迹历史;它可以作为本地工具使用,也可以在多条 path 前面以服务方式部署。 @@ -65,9 +65,6 @@ pchronicle query ./trajectory-data \ - **使用 Agent 分析:** `pchronicle agent codex DATASET` - **打开本地 UI 与 API:** [提供 Dataset 服务](guides/ui.md) -pChronicle 读取并组织运行历史,不执行或调度 Agent。要在受控工作区中运行 Agent,请从 -pVisor (外部仓库)开始。 - ## 推荐阅读顺序 1. [探索第一个 Dataset](get-started.md),先完成一次只读查询。 diff --git a/docs/src/zh/project/engineering.md b/docs/src/zh/project/engineering.md index 9a834209e..b3fd6bb3e 100644 --- a/docs/src/zh/project/engineering.md +++ b/docs/src/zh/project/engineering.md @@ -43,7 +43,7 @@ Rust 测试用 `cargo nextest` 做进程隔离和并行执行;用 或使用仓库 CI setup action。 本地和普通 CI 构建使用平台默认 linker。Linux wheel 使用 manylinux_2_28 -镜像(glibc 2.28),以便 rustc libstd 和 libkrun 能链接 `statx` / +镜像(glibc 2.28),以便 rustc libstd 能链接 `statx` / `copy_file_range`。 `just dev` 刻意限定在 runtime crate 以及无默认 feature 的 pChronicle 检查。 diff --git a/docs/src/zh/project/index.md b/docs/src/zh/project/index.md index 728077dc1..b61d58dee 100644 --- a/docs/src/zh/project/index.md +++ b/docs/src/zh/project/index.md @@ -7,8 +7,6 @@ Persisting 的公开产品是 pChronicle。这一节记录交付状态、稳定 - [系统概览](../system-design/index.md) - [端到端架构](../system-design/architecture.md) -- [从本地到集群](../system-design/local-to-fleet.md) -- [安全与 Evidence](../system-design/security-evidence.md) ## 构建与发布 diff --git a/docs/src/zh/rfcs/0002-events-format.md b/docs/src/zh/rfcs/0002-events-format.md index afcc75008..b7c75a683 100644 --- a/docs/src/zh/rfcs/0002-events-format.md +++ b/docs/src/zh/rfcs/0002-events-format.md @@ -7,7 +7,7 @@ | **Date** | 2026-07-30 | | **Component** | `persisting-events` + Gateway + pChronicle | | **Implements** | `persisting-events::EventRecord` · `persisting-pchronicle` `formats/events.rs` / `EventRow` | -| **Related** | [RFC-0001 Storyline](0001-storyline-format.md) · [RFC-0007 Events/Sidecar 边界](0007-events-contract-pchronicle-sidecar.md) · Capture 管线 (外部仓库) · [轨迹存储](../pchronicle/design/trajectory-storage.md) | +| **Related** | [RFC-0001 Storyline](0001-storyline-format.md) · [RFC-0007 Events/Sidecar 边界](0007-events-contract-pchronicle-sidecar.md) · [轨迹存储](../pchronicle/design/trajectory-storage.md) | --- @@ -125,7 +125,7 @@ Persisting Gateway 的主入口是代理流量。`events` 应对齐这一现实 ## Schema:逻辑事件 `EventRecord` 编码:UTF-8 JSON object。`EventRecord` 由存储无关的 `persisting-events` 唯一定义; -Gateway、pVisor 与 pChronicle 直接使用同一类型。pChronicle 独占由该逻辑记录派生的 +Gateway 与 pChronicle 直接使用同一类型。pChronicle 独占由该逻辑记录派生的 物理 row schema 与存储实现。 一行事件 MUST 包含 `seq`、`source`、`kind`、`payload`。 @@ -154,7 +154,7 @@ Gateway、pVisor 与 pChronicle 直接使用同一类型。pChronicle 独占由 顶栏 **SHOULD NOT** 承载完整对话文本;正文在 `payload` 的 wire 字段中。 新写入事件必须同时提供 `timestamp` 与 `timestamp_unix_ms`。Gateway capture sink 和 -pVisor runtime 是 producer 侧的共同兜底;admission 仅为旧记录或兼容导入补齐缺失值。 +事件 producer 负责提供时间字段;admission 仅为旧记录或兼容导入补齐缺失值。 事件顺序仍由 `source + seq` 定义,时间戳用于关联和展示。 ### `payload`:HTTP-first wire 对象 diff --git a/docs/src/zh/rfcs/0003-pchronicle-ownership.md b/docs/src/zh/rfcs/0003-pchronicle-ownership.md index 72675a4c3..47dacccc2 100644 --- a/docs/src/zh/rfcs/0003-pchronicle-ownership.md +++ b/docs/src/zh/rfcs/0003-pchronicle-ownership.md @@ -68,7 +68,7 @@ Gateway 的 live Markdown 行为可以保留 producer-specific 策略,例如 - `persisting-gateway/src/session/` 维护 session 身份、路由、client metadata、索引与 snapshot。 - `persisting-gateway/src/projection/` 维护 Gateway 特有的可见文本解释、实时过滤、draft/upsert 和 reconcile。 - `persisting-gateway/src/engine/` 维护采集 actor、WAL、顺序状态机和 egress;这里的 “engine” 是 Gateway 内部编排器,不是轨迹存储层。 -- `persisting-overlaynet` 是 pVisor 当前的轻量显式代理网络层,负责 CONNECT、absolute-URI forward、header 规则和网络访问策略执行;Gateway 作为 `OverlaySink` 在其上解释并转发 LLM 流量、产出轨迹事件。OverlayNet 不依赖 Gateway,可配置其他 sink。 +- `persisting-overlaynet` 是轻量显式代理网络层,负责 CONNECT、absolute-URI forward、header 规则和网络访问策略执行;Gateway 作为 `OverlaySink` 在其上解释并转发 LLM 流量、产出轨迹事件。OverlayNet 不依赖 Gateway,可配置其他 sink。 ## 一致性与故障语义 @@ -87,14 +87,14 @@ Run lease epoch MUST 通过 `EventWriterFence` 进入 canonical event 提交协 ## 收敛结果 - 原 Engine 中的 Trajectory 适配、Lance、Markdown 和 Arrow row 实现全部迁入 pChronicle;Engine crate 删除。 -- Gateway、pVisor 与 pChronicle 直接使用 `persisting-events::EventRecord`;Gateway extension 仅承载实时 payload 解释。 +- Gateway 与 pChronicle 直接使用 `persisting-events::EventRecord`;Gateway extension 仅承载实时 payload 解释。 - Gateway 仅保留实时 payload 解释、live Markdown eligibility/upsert orchestration 与运行时 reconcile;格式解析、文件 I/O、frontmatter 契约与索引实现委托 pChronicle。 - 只生产事件或调用 control 协议的组件依赖 `persisting-events`;需要存储、查询或格式转换的调用方依赖 pChronicle。 - 旧 ATIF `sessions` / `steps` / `tool_calls`、`NormalizedStore`、内存联表视图及对应 Python 门面删除;ATIF 查询统一复用 Storyline 三表 schema。 - append 边界直接传递 `EventRecord` 批次,不保留 RON/event-lines 字符串适配层。 新代码 SHOULD 按能力选择依赖:事件 producer 使用 `persisting-events`,存储和读取调用方 -使用 pChronicle。Gateway 与 pVisor 不得为了构造 `EventRecord` 依赖 pChronicle。 +使用 pChronicle。事件 producer 不得为了构造 `EventRecord` 依赖 pChronicle。 ## 验收条件 diff --git a/docs/src/zh/rfcs/0006-pchronicle-vortex-backend.md b/docs/src/zh/rfcs/0006-pchronicle-vortex-backend.md index d91f53a5a..f7d49866a 100644 --- a/docs/src/zh/rfcs/0006-pchronicle-vortex-backend.md +++ b/docs/src/zh/rfcs/0006-pchronicle-vortex-backend.md @@ -135,7 +135,7 @@ Vortex projector 固定输入事实 snapshot 后异步构建。构建、上传 ### 3.6 依赖和构建默认隔离 -Vortex 不进入 pChronicle、orchestration layer、pVisor、Gateway 或 CLI 的默认依赖图。默认构建、测试和发布 +Vortex 不进入 pChronicle、Gateway 或 CLI 的默认依赖图。默认构建、测试和发布 二进制中不得出现 Vortex crate,除非消费者显式启用或构建独立实验 crate。 ## 4. 范围与非目标 @@ -1226,7 +1226,7 @@ Lance 的 merge、MVCC、maintenance、scalar index 和 Vortex 的 overlay、dir | workspace-wide CI 变慢 | 独立 job/cache;日常使用 targeted package commands | | test matrix 成倍增长 | core semantic tests共享 fixture;后端专项测试独立运行 | -验收要求:默认 `cargo build`、orchestration layer、pVisor、Gateway 和 pChronicle CLI 的依赖图中不出现任何 +验收要求:默认 `cargo build`、Gateway 和 pChronicle CLI 的依赖图中不出现任何 `vortex-*` package;只有显式构建实验 crate 时才增加编译成本。 ### 18.4 独立 helper 与同进程链接的权衡 @@ -1517,7 +1517,7 @@ codec。它适合作为 benchmark baseline,不作为目标设计。 - 锁定与 Arrow 58.3/DataFusion 54/object_store 0.13.2 兼容的 crates.io release; - 记录 clean build、增量 build、package graph、二进制和 target 增量; - 写最小 nested `TrajectoryBundle`,用 `vx`/Rust reader 检查 layout tree; -- 不修改 Gateway、orchestration layer、pVisor 或默认 CLI。 +- 不修改 Gateway 或默认 CLI。 ### Phase 1:标准 Vortex 单文件 codec prototype diff --git a/docs/src/zh/rfcs/0007-events-contract-pchronicle-sidecar.md b/docs/src/zh/rfcs/0007-events-contract-pchronicle-sidecar.md index 6e3b88ec7..f7abb68f6 100644 --- a/docs/src/zh/rfcs/0007-events-contract-pchronicle-sidecar.md +++ b/docs/src/zh/rfcs/0007-events-contract-pchronicle-sidecar.md @@ -4,34 +4,25 @@ |---|---| | **Status** | Accepted | | **Date** | 2026-08-16 | -| **Components** | `persisting-events` · pVisor · orchestration layer · pChronicle · Gateway | +| **Components** | `persisting-events` · pChronicle · Gateway | | **Amends** | [RFC-0002 Events](0002-events-format.md) · [RFC-0003 pChronicle ownership](0003-pchronicle-ownership.md) | -| **Related** | [端到端架构](../system-design/architecture.md) · orchestration architecture · [轨迹存储](../pchronicle/design/trajectory-storage.md) | +| **Related** | [端到端架构](../system-design/architecture.md) · [轨迹存储](../pchronicle/design/trajectory-storage.md) | ## 摘要 Persisting 将运行时事件的逻辑契约从存储实现中拆出,由唯一新增 crate -`persisting-events` 拥有。pVisor、Gateway、orchestration layer 与 pChronicle 共享该契约,但只有 +`persisting-events` 拥有。Gateway、事件 producer 与 pChronicle 共享该契约,但只有 pChronicle 拥有 Lance、DataFusion、对象存储、Catalog、查询与投影实现。 -pVisor 需要持久轨迹或 Attempt registry 时启动 -`pchronicle serve --control 127.0.0.1:0 DATASET`,通过带版本和认证令牌的 control 协议提交事件并等待 durable -acknowledgement。pVisor 默认构建不再链接 Lance/DataFusion,也不直接打开或写入 -`events.lance`。 +本地集成方可以启动 `pchronicle serve --control 127.0.0.1:0 DATASET`, +通过带版本和认证令牌的 Control 协议提交事件并等待 durable acknowledgement。 +存储实现由服务进程拥有,客户端只依赖逻辑事件与协议类型。 ## 动机 -此前 `EventRecord` 位于 pChronicle,pVisor 的默认持久化路径又以内嵌适配器链接 -pChronicle。这把“描述发生了什么”的稳定数据契约与“如何落盘、查询和投影”的实现绑在 -一起,导致单 Run 执行器携带不必要的存储依赖,也使 producer 难以在不依赖具体后端的 -情况下发布事件。 - -边界调整需要同时满足: - -1. producer 与 consumer 仍使用同一种事件类型,禁止复制同构 schema; -2. pVisor 不拥有存储格式,也不链接重型存储引擎; -3. pChronicle 仍是唯一的结构化轨迹持久化和读取层; -4. 不为少量进程协议再增加一个独立 client crate。 +逻辑事件描述“发生了什么”,物理存储实现决定“如何落盘、查询和投影”。将两者分离, +让 producer 可以发布事件而无需依赖具体存储后端,同时继续使用同一种公共事件类型。 +pChronicle 保留结构化轨迹的持久化和读取职责。 ## 决策 @@ -65,23 +56,14 @@ RFC-0003 中“pChronicle 拥有轨迹格式”的约束仍适用于物理 schem 其中“pChronicle 唯一定义 `EventRecord`”以及“所有调用方直接依赖 pChronicle 类型”的部分 由本 RFC 修订。 -### 3. pVisor 通过 sidecar 持久化 - -pVisor 只暴露两个面向用户的落盘选择:格式和目标位置。 - -| 选择 | 行为 | -|---|---| -| `--record-format json` + 本地目录 | pVisor 直接追加完整 `events.jsonl`,不启动 pChronicle | -| `--record-format json` + warehouse URI | 启动 pChronicle,由 sidecar 将 JSON 事件写入 warehouse | -| `--record-format lance` | 启动 `pchronicle serve --control 127.0.0.1:0 `,通过 control 协议写 canonical Lance | +### 3. 本地 producer 通过 Control 服务提交 -旧的 `--chronicle-mode`、`--chronicle-dir` 和 `--pchronicle-binary` 不再是 pVisor -CLI 参数。pVisor 库/配置仍可通过 `chronicle.binary` 选择 sidecar executable;orchestration layer -自己的 `--pchronicle-binary` 不属于 pVisor CLI。pVisor 管理自己启动的 child 生命周期; -child 退出、握手失败或协议版本不兼容都会显式使持久化路径失败。 +`pchronicle serve --control 127.0.0.1:0 DATASET` 提供本地持久化入口。 +`persisting-events` 的进程 client 负责启动子进程、握手与请求关联。 +子进程退出、握手失败或协议版本不兼容都使持久化请求显式失败。 -pVisor 与 Gateway producer 只构造 `EventRecord`。它们 MUST NOT 选择 Lance row、执行 -DataFusion query,或根据 storage URI 加载对象存储 SDK。 +使用此协议的 producer 提交 `EventRecord`;Lance row、DataFusion 查询与对象存储 +实现由 pChronicle 管理。 ### 4. Control 协议随事件契约发布 @@ -98,9 +80,8 @@ request ID 与令牌;client 校验响应版本和 request ID。frame 大小有 ### 5. ACK、背压与不确定性 -pVisor 到 sidecar 的 append 队列是有界的。入队使用 `try_send`:队列已满或 worker 已 -关闭时,事件被明确拒绝,调用方可以复用该 `seq`。成功入队后调用方等待 sidecar 响应; -只有 pChronicle 完成 append 并返回成功时,事件才对 pVisor 视为 durable。 +Gateway 到存储的 append 队列是有界的。提交被拒绝与提交后的结果不确定必须区分。 +只有 pChronicle 完成 append 并返回成功时,事件才视为 durable。 连接中断、写入错误或 ACK 丢失无法证明事件未提交,必须分类为 unknown。此时 producer 消耗该序号,不能把不确定写入伪装为“肯定未写”。事实层仍允许 at-least-once 与重复 @@ -112,7 +93,7 @@ pVisor 到 sidecar 的 append 队列是有界的。入队使用 `try_send`:队 ## 依赖边界 ```text -pVisor / Gateway / orchestration layer +Gateway / local event producers │ │ EventRecord + optional control protocol ▼ @@ -127,16 +108,13 @@ pVisor / Gateway / orchestration layer └── Catalog / query / projection ``` -默认 pVisor dependency graph MUST NOT 通过 Chronicle 写路径引入 Lance、Arrow、DataFusion -或云对象存储 SDK。其他 pVisor 组件若为了非存储的格式/投影 helper 暂时形成到 -pChronicle 的间接依赖,不改变本 RFC 的 ownership,但 SHOULD 在后续边界整理中消除; -不得借此让 pVisor 重新直接写存储。 +`persisting-events` 的默认 feature 和可选 `control` feature 均不得引入 Lance、Arrow、 +DataFusion 或云对象存储 SDK。物理存储与查询实现只由 pChronicle 维护。 ## 兼容与迁移 - `EventRecord` 的 JSON 顶层字段保持扁平,移动 crate 不改变既有 wire 形状; - pChronicle 对外 re-export 公共事件类型,允许调用方渐进迁移 import; -- pVisor 调用方应迁移到 `--record-format {json,lance}` 与 `--record-destination PATH|URI`;旧 Chronicle CLI 参数不再接受; - `persisting-pchronicle-client` 被删除,使用者改为 `persisting-events = { features = ["control"] }`; - pChronicle 的既有 Lance dataset、目录布局和 replay 语义不因这次 crate 拆分而变化。 @@ -145,18 +123,18 @@ pChronicle 的间接依赖,不改变本 RFC 的 ownership,但 SHOULD 在后 | 方案 | 原因 | |---|---| -| pVisor 继续内嵌 Lance adapter | 执行器与存储引擎生命周期、feature 和依赖重新耦合 | +| producer 内嵌另一套 Lance adapter | producer 与存储引擎生命周期、feature 和依赖重新耦合 | | `EventRecord` 继续由 pChronicle 定义 | producer 为使用基础事件信封被迫依赖存储产品 | | 单独保留 `persisting-pchronicle-client` | 协议包过碎;control 契约可以作为事件边界的可选 feature | -| pVisor 与 orchestration layer 各写一套 IPC client | 会产生协议漂移、重复认证与错误语义 | -| pVisor 写 JSONL,pChronicle 以后导入 | 缺少运行期 durable ACK、fencing 与统一 canonical append 语义 | +| 各集成方分别实现 IPC client | 会产生协议漂移、重复认证与错误语义 | +| 仅依赖离线导入而无实时 Control 协议 | 缺少运行期 durable ACK、fencing 与统一 canonical append 语义 | ## 验收条件 - Workspace 只新增 `persisting-events`,不存在 `persisting-pchronicle-client`; -- pVisor、Gateway、pChronicle 复用同一 `EventRecord`,没有同构公共事件 struct; -- pVisor 默认构建不直接依赖 pChronicle 存储 API,也不链接 Lance/DataFusion; -- `spawn` 模式能够启动 sidecar,持久写入有序事件并发布 Attempt 终态; +- Gateway、producer、pChronicle 复用同一 `EventRecord`,没有同构公共事件 struct; +- `persisting-events` 不直接依赖 pChronicle 存储 API,也不链接 Lance/DataFusion; +- 进程 client 能够启动 Control 服务,持久写入有序事件并发布 Attempt 终态; - 协议版本、认证、request correlation、frame limit、拒绝与 unknown 写入语义有测试; - pChronicle 的原有 replay、query 与物理存储测试继续通过。 @@ -164,4 +142,4 @@ pChronicle 的间接依赖,不改变本 RFC 的 ownership,但 SHOULD 在后 | Version | Date | Notes | |---|---|---| -| Accepted | 2026-08-16 | 拆出 `persisting-events`,移除 client crate,以 pChronicle sidecar 替代 pVisor 内嵌存储 | +| Accepted | 2026-08-16 | 拆出 `persisting-events`,移除 client crate,确立逻辑事件与 pChronicle 存储服务的边界 | diff --git a/docs/src/zh/roadmap.md b/docs/src/zh/roadmap.md index dd2716649..94e4506ae 100644 --- a/docs/src/zh/roadmap.md +++ b/docs/src/zh/roadmap.md @@ -9,12 +9,12 @@ 或账号即可使用。 - 保持中英文文档路径一致,并让示例可以运行。 -## 下一步:把执行连接到持久历史 +## 下一步:完善捕获与分析 - 在捕获、规范化和查询之间保留 Run identity 与 lineage。 - 改进 Run、Session 和 revision 的比较流程。 -## 之后:从单机走向团队和集群 +## 之后:在团队间共享轨迹数据 - 在不隐藏 provenance 的前提下共享 Dataset catalog 和策略。 @@ -23,4 +23,4 @@ ## 如何理解路线图 有设计文档不等于功能已经完成。只有在 CLI 路径、测试或示例、限制说明和发布记录 -都具备后,才应把功能视为可用。改变数据契约、执行边界或公开命令的改动应先进入 RFC。 +都具备后,才应把功能视为可用。改变数据契约、存储边界或公开命令的改动应先进入 RFC。 diff --git a/docs/src/zh/system-design/architecture.md b/docs/src/zh/system-design/architecture.md index 4072c0522..bb4f4e6e2 100644 --- a/docs/src/zh/system-design/architecture.md +++ b/docs/src/zh/system-design/architecture.md @@ -1,187 +1,57 @@ -# 端到端架构 +# 系统架构 -> pVisor 与 pPilot 已拆分到外部仓库。本仓库保留 pChronicle 及捕获、持久历史所需的内部库。 +pChronicle 负责 Agent 轨迹历史的捕获、存储、查询与交换。本文定义引擎、入口和内部库 +之间的职责;物理布局见 [pChronicle 设计](../pchronicle/design/index.md)。 -本文只定义 Persisting 产品之间的契约。Provider 机制属于 pVisor Design,存储布局属于 -pChronicle Design,命令属于各产品 Reference。 +## 组件职责 -![Persisting 产品域与集成关系](../../assets/diagrams/persisting/system-products.svg) +| 组件 | 职责 | +|---|---| +| `persisting-pchronicle` | 轨迹模型、存储、Source 发现、Dataset Snapshot、有界查询、格式转换与版本血缘 | +| `persisting-pchronicle-cli` | `pchronicle` 命令、本地只读 Warehouse API、可选 Control 服务、Gateway 配置与内嵌 Web 资源 | +| `pchronicle-web` | Dataset 浏览与查询界面 | +| `persisting-events` | 存储无关的 `EventRecord` 及可选的带版本 Control 客户端与协议 | +| `persisting-gateway` | 模型协议适配、转发、会话关联与轨迹捕获 | +| `persisting-overlaynet` | 代理传输、请求分类与已拦截流量的策略执行 | +| `persisting-agentctl` | 共享控制类型、策略状态转换与协作式客户端协议 | -## 产品 Ownership - -| 产品或层次 | 拥有 | 不拥有 | -| --- | --- | --- | -| `persisting-events` 契约 | 存储无关的 `EventRecord` identity/envelope 与可选的版本化 pChronicle control 协议 | 物理 row、存储引擎、查询或 projection | -| pVisor | 一个 Run、Attempt、执行环境、capability admission、Effect 与运行时 Evidence | 多 Run 调度或持久历史查询 | -| pChronicle | Agent 轨迹存储引擎:path 身份、Snapshot、canonical event、projection、查询与交换 | 启动、调度或控制 Run | -| Runtime Provider | 一种物理执行机制 | 逻辑 Run identity 或产品策略 | - -Gateway、OverlayFS 和 OverlayNet 是 pVisor 运行时机制,不构成独立控制面。 - -## 运行位置与平台边界 - -逻辑 Run 契约可以跨 Provider 迁移,但 enforcement 边界取决于选中的平台: - -| 运行位置 | 工作负载边界 | Workspace 行为 | 安全声明 | -| --- | --- | --- | --- | -| Linux host | 私有 user/mount/PID namespace 与 Landlock | staged FUSE workspace | filesystem 与 network capability 分开报告;准备失败时在执行前失败 | -| macOS host | 可用时使用 Seatbelt,并用 staged macFUSE 处理写入 | staged host workspace | safe best-effort host 隔离;host kernel 与 ambient read 仍记录在 Evidence 中 | -| Linux 或 Apple Silicon macOS VM | guest kernel 加 OCI 或准备好的 Linux rootfs | guest 内 staged workspace | kernel 边界更强,但 macOS VMM 仍以调用用户的 host 权限运行 | -| 原生 OCI container | pVisor 选择的 OCI runtime 与 bundle | bundle 挂载的 rootfs 与 staged path | 记录 container isolation;不将其视为完整的敌对多租户边界 | - -Provider 会在 Run Bundle 中分别记录请求的 capability 与实际生效的维度。进程成功退出 -不代表请求的边界已经安装;workspace stage 也独立于产生它的 Provider,仍可 review。 -Provider 的行为与前置条件见 pVisor 隔离设计 (外部仓库) 和 -执行指南 (外部仓库)。 - -## 独立 Ingress 路径 - -```text -Configured runtime capture - Gateway trajectory events ─┐ - pVisor lifecycle records ──┴─> canonical event Source ──────────────┐ -Pinned external Sources │ - local/S3 ATIF, ACTF, OpenAI Messages files ──────────────────────────┼─> Snapshot - local/S3 Storyline Sources ──────────────────────────────────────────┘ - └─> normalized Dataset views -``` - -pVisor 可以用 staged Effect 与私有、带版本的 -Run Bundle 及 terminal RunResult 完成独立闭环。配置 capture 并不是 pVisor 运行时前置条件。 -外部文件与 Storyline Source 会被直接固定版本并规范化;它们既不经过 pVisor,也不会变成 -canonical runtime event,因此不会获得 pVisor 执行保证。 - -## 稳定对象 - -```text -RunSpec - └── Run - ├── Attempt 1 - ├── Attempt 2 - └── Attempt finalization - ├── terminal RunResult - ├── private versioned Run Bundle - └── staged Effects → later review / apply / drop - -Optional configured event handoff - └── Gateway trajectory events + pVisor lifecycle records -``` - -逻辑 Run 可以迁移,Attempt 与 Provider 绑定。基础设施重试创建新 Attempt;语义重试创建 -派生 Run。一个 Run 可以有多个 Attempt,但只能有一个可见终态结果。 - -Source 携带 `run_id` 时,它就是跨产品稳定身份。Session、Step、call、event 和 Artifact -identity 保留自己的 scope 与 Source lineage。进程 ID、Container ID、VM ID 或 worker lease -都不能代替 Run identity。 - -## 单 Run 路径 +## 捕获与导入 ```text -User or Agent framework - → RunSpec - → pVisor admission - → capability-by-dimension provider selection - → Attempt execution - → terminal RunResult + private versioned Run Bundle + staged Effects - → later review / apply / drop +Agent / SDK 请求 + → Gateway 协议处理与捕获 + → EventRecord + → pChronicle canonical event 存储 + → Dataset Snapshot → 有界查询 → CLI / Web / 导出 + +受支持的文件与对象存储 Source + → 发现并固定 Source 版本 + → Storyline 规范化与查询投影 + → Dataset Snapshot → 有界查询 → CLI / Web / 导出 ``` -Admission 比较请求的 capability 维度与选中 Provider 能提供的 Evidence。必需维度无法 -enforce 时,在 workload 执行前失败;可选降级必须明确写入 Run Bundle。 +Gateway 捕获经过它的流量,导入则直接读取受支持的外部格式。两条路径保留可用身份与 +来源信息,不会补出源数据缺失的执行或隔离证据。 -文件 promotion 是 Effect 决策,不是 Run 终态提交。只要 stage 仍存在,就可以多次 apply -不同路径。网络请求和远程工具修改属于不同 Effect 维度,不能从文件状态推断。 +## 写入与读取边界 -配置后,pVisor 会向 pChronicle 发布 Gateway 轨迹 event,以及 `run.created`、 -`run.state_changed` 和终态 lifecycle record。这些 record 携带 Run/Attempt identity、 -lifecycle fact 与其中可用的 Evidence。Artifact reference、lineage、staged filesystem Effect、 -AgentCtl/network/resource Evidence 和完整 Run Bundle 仍留在本地,除非由单独 adapter 搬运。 +CLI 可以在进程内调用引擎。集成方也可以启动 +`pchronicle serve --control 127.0.0.1:0 DATASET`,使用 `persisting-events` 中带认证、 +带版本的本地协议。只有成功的 append ACK 才确认持久化;响应丢失意味着结果不确定, +调用方不能据此认定事件没有写入。 -## Dataset 路径 - -Canonical runtime writer 与固定版本的外部 Source 是相互独立的 Source 路径;它们只在 -Snapshot 与规范化 Dataset 视图处汇合: - -```text -configured Gateway and pVisor lifecycle writers - → canonical event Source ────────────────────────────────┐ -pinned local/S3 external Sources │ - → ATIF / ACTF / OpenAI Messages files ───────────────────┼─> Snapshot - → Storyline Sources ─────────────────────────────────────┘ ├─> normalized Run / Step / ToolCall views - └─> query / export / revision lineage -``` - -Canonical fact 采用 append-oriented 模型。Storyline 等规范化视图是可重建 projection;交换 -文件是互操作边界,不替代事实源。每次读取固定一个 Snapshot,但不会虚构跨无关 -Source 的全局事务。固定外部文件不会把它转换为 canonical runtime event Source。 - -## Source 特定保证 - -| Source 路径 | 支持的主张 | 明确不主张 | -| --- | --- | --- | -| 外部文件或 imported Source | 已发现的内容、固定的 Source version、规范化表示,以及在已实现位置记录的 conversion lineage | 外部 task manifest 的完整性,或不存在未报告轨迹 | -| Gateway capture | 通过所配置 Gateway 路径观察到并持久发布的 request 与 response | 不存在绕过 Gateway 的流量 | -| pVisor Run | Run/Attempt identity、已记录终态事实、已安装机制、观察到的 Effect 与 Provider 特定 Evidence | 所选 Provider 未提供的 enforcement | - -Ingestion 保留这些边界。规范化表示或 Snapshot 不会升级 Source 提供的 Evidence。 - -pVisor 默认构建不链接 Lance/DataFusion。配置后的 Chronicle 发布会通过带认证的 -loopback IPC 启动 pChronicle sidecar,并且只把 sidecar 成功响应视为 durable -acknowledgement。旧模式名 `lance` 是 `spawn` 的兼容别名;pVisor 不再自行写 Lance。 -Sidecar 标志与模式名见 pVisor CLI (外部仓库) 与 -[RFC-0007](../rfcs/0007-events-contract-pchronicle-sidecar.md)。 - -## 故障与恢复 - -| 故障 | Owner | 必需行为 | -| --- | --- | --- | -| Attempt 退出或 Provider 消失 | pVisor | finalise Evidence;暴露失败或创建 fenced replacement Attempt | -| sidecar append queue 饱和或关闭 | pVisor/Gateway producer | 提交前明确拒绝并报告失败;不得声称已经 durable | -| append 连接或 ACK 丢失 | producer 与 pChronicle writer | 由于写入可能已经提交,保留 unknown 状态;不得按“明确拒绝”复用序号 | -| 历史发布冲突 | pChronicle writer | 保留已发布 Snapshot;按 writer contract 报错或重试 | -| 视图生成失败 | pChronicle | 保持 canonical fact 可读;重建派生视图 | - -恢复不能把不确定性升级为成功。缺失终态事实、丢失 callback、未 enforce 的 capability 都必须 -保持可见。 - -## 安全与 Evidence 链 - -安全性按 capability 维度报告。pVisor 记录请求策略、实际机制、Provider identity、 -enforcement 结果与观察到的 Effect。配置后的 pChronicle capture 会持久保存 Gateway 轨迹 event、pVisor lifecycle -record,以及这些 event 实际携带的 Evidence。完整 Artifact、lineage、filesystem Effect、 -AgentCtl/network/resource Evidence 和 Run Bundle 仍留在本地,除非由单独 publisher 或 -adapter 搬运。 - -下面保留 Evidence 的形成层级;它描述本地 Run 的证据链,不表示各层都会自动交接到持久历史: - -```text -requested policy - → admission decision - → installed mechanism - → provider-bound evidence - → observed effects - → terminal result - -Optional configured persistence - Gateway trajectory events + pVisor lifecycle records - → event-carried Evidence only - → pChronicle durable history -``` +pChronicle 拥有物理 schema、writer fencing、manifest 发布与维护。Producer 提交逻辑 +记录,不另行定义存储布局。详见 [RFC-0007](../rfcs/0007-events-contract-pchronicle-sidecar.md) +与[轨迹存储](../pchronicle/design/trajectory-storage.md)。 -Evidence 层级见[安全与 Evidence](security-evidence.md),可迁移要求见 -[从本地到集群](local-to-fleet.md)。 +Warehouse HTTP API 与 Web UI 只读,单独启用的 Control 和 Gateway 捕获路径可以写入。 +本地服务拒绝公共绑定地址,Control 协议面向可信本地进程。 -## 公共边界 +## 事实、视图与版本 -| 边界 | 契约 Owner | 详细文章 | -| --- | --- | --- | -| 逻辑运行事件与本地 Chronicle control 协议 | `persisting-events` | [RFC-0007](../rfcs/0007-events-contract-pchronicle-sidecar.md) | -| Agent 执行与 Effect review | pVisor | pVisor 概念 (外部仓库)与指南 (外部仓库) | -| Provider 与运行时机制 | pVisor | pVisor Design (外部仓库) | -| Dataset、事实与 Projection | pChronicle | [pChronicle 概念](../pchronicle/concepts/index.md) | -| 存储与 Snapshot 实现 | pChronicle | [pChronicle Design](../pchronicle/design/index.md) | -| 稳定命令语法与格式 | 各产品 | pVisor Reference (外部仓库)与 [pChronicle Reference](../pchronicle/reference/index.md) | -| 规范性 ownership 决策 | Project RFC | [RFC 索引](../rfcs/index.md) | +Canonical events 保留原始逻辑 payload。Storyline 为交换格式提供规范化模型,投影支持 +分析而不替代原始事实。查询读取固定的 Snapshot,版本血缘标识派生输出。 -只有跨产品契约变化时才修改本文。产品实现状态和 roadmap 属于对应 Design 页或 Project -工程说明。 +捕获覆盖范围取决于 Source 中实际存在的记录。代理策略仅作用于到达代理的流量,不能 +证明进程级网络隔离。详见 [Gateway 捕获](../pchronicle/guides/serve-gateway.md)和 +[事实与投影](../pchronicle/concepts/facts-and-projections.md)。 diff --git a/docs/src/zh/system-design/design-principles.md b/docs/src/zh/system-design/design-principles.md index 210a4535c..f22a7d793 100644 --- a/docs/src/zh/system-design/design-principles.md +++ b/docs/src/zh/system-design/design-principles.md @@ -1,30 +1,28 @@ # 设计原则 -> pVisor 与 pPilot 已拆分到外部仓库。本仓库保留 pChronicle 及捕获、持久历史所需的内部库。 +## 区分事实与投影 -这些原则解释了为什么 Persisting 拆分为两个产品,也解释了文档为什么强调可审查的步骤。 +Canonical events 保留已记录的事实。规范化的 Storyline 视图和查询投影可以追溯到 +Source;记录缺失应作为答案的限制明确呈现。 -## 边界必须明确 +## 明确职责 -pVisor 描述实际安装的执行边界;pChronicle 描述实际观察到的 Source 和 Dataset。任何一个产品 -都不会把缺失的控制或不完整的 Source 默认为更强的保证。 +共享事件契约描述逻辑记录。pChronicle 拥有存储、查询和交换,Gateway 拥有捕获与协议 +适配。CLI 和 Web UI 使用这些边界,不另行定义数据模型。 -## 审查前的写入应可逆 +## 保留来源与版本 -Agent 的 Effect 在人或明确策略应用前保持 staged。审查是工作流的一部分,不是写入完成后才补上的报告。 +答案应能定位到产生它的 Dataset、Source、Snapshot 和查询。版本血缘让派生输出在 +规范化和导出后仍然可以追溯到输入。 -## 结果必须带着证据走 +## 限制资源使用 -汇总应该能回到产生它的 Run、Dataset、Source 或 query。只有在导出、规范化和再次检查之后仍保留 lineage, -它才真正有用。 +查询预算、有界捕获队列和明确的 append ACK 让资源使用与写入结果可检查。投影失败 +不能被报告为已成功持久化。 -## 执行和历史保持可组合 +## 保持数据可移植 -pVisor 可以不依赖 pChronicle 运行,pChronicle 也可以分析外部 Source。集成采用窄化的 capture 契约, -让每个产品单独使用时仍然有价值。 +公开格式与 CLI 让 Dataset 不依赖特定查看器。本地文件和对象存储 Source 使用统一的 +Dataset 模型。 -## 可移植数据优先于特权查看器 - -Dataset、查询结果和 Run 记录应该可以通过 CLI 和公开格式检查。Web 界面可以改善发现,但不应成为恢复答案的唯一方式。 - -参见[系统概览](index.md)和[路线图](../roadmap.md)。 +继续阅读[系统概览](index.md)和[路线图](../roadmap.md)。 diff --git a/docs/src/zh/system-design/index.md b/docs/src/zh/system-design/index.md index 83853ed1d..d30761766 100644 --- a/docs/src/zh/system-design/index.md +++ b/docs/src/zh/system-design/index.md @@ -1,55 +1,21 @@ -# System Design +# 系统设计 -> pVisor 与 pPilot 已拆分到外部仓库。本仓库保留 pChronicle 及捕获、持久历史所需的内部库。 - -Persisting 提供持久 Agent 轨迹历史。本节描述 pChronicle 与外部执行组件的集成: - -- pVisor (外部仓库) 虚拟化并治理单个 Agent Run; -- [pChronicle](../pchronicle/index.md) 把持久轨迹 Source 组织为可查询 Dataset。 - -Gateway 与 OverlayNet 在本仓库支持轨迹捕获;OverlayFS 属于外部执行组件。 -存在稳定 Run identity 时,它会连接 -这些产品域,但各域也有独立入口。 - -![Persisting 产品域与集成关系](../../assets/diagrams/persisting/system-products.svg) - -## 跨产品契约 +Persisting 围绕 [pChronicle](../pchronicle/index.md) 构建 Agent 轨迹存储引擎。 +Gateway 捕获模型流量,共享事件契约将记录交给存储,CLI 和 Web UI 提供 Dataset 浏览与分析入口。 ```text -Configured pVisor capture - Gateway trajectory events ─┐ - pVisor lifecycle records ──┴─> canonical event Source ─┐ -Pinned external Sources │ - ATIF / ACTF / OpenAI Messages / Storyline ─────────────┴─> Snapshot - └─> normalized Dataset views +Gateway 捕获 ── EventRecord ── pChronicle 存储 ─┐ +ATIF / ACTF / OpenAI Messages / Storyline Source ┴─> Dataset Snapshot + └─> 查询 / 交换 / Web UI ``` -Attempt finalization 会写入私有、带版本的 Run Bundle,并保留 staged Effect,供之后执行 -review/apply/drop;这一过程不需要 pChronicle。配置后的 capture 会发送 Gateway 轨迹 event -与 pVisor lifecycle record,包括这些 record 携带的 Evidence。完整 Bundle 及其中的 Artifact、 -lineage、Effect 与更完整的 Evidence 清单仍留在本地,除非另行搬运。 - -外部文件与 Storyline Source 会被直接固定版本并规范化,无需经过 pVisor,也不会先变成 -canonical event。每条路径保留与 Source 对应的保证;ingestion 不会补充 Source 未提供的 -Evidence。 - -职责边界可以概括为两点: - -- **pVisor 负责执行。** 它定义单个 Run 的边界,以及模型、网络和文件系统 runtime driver。 - 即使没有捕获历史,私有 Run Bundle 也独立有效。 -- **pChronicle 负责历史。** 它记录 canonical event 和终态事实,并提供 Dataset 查询、交换与 - revision lineage。 - -这个拆分直接服务于使用者:可以先独立使用执行或历史,只有当问题同时跨越两个域时才配置 -capture 交接。 +Canonical events 保存已记录的事实;Storyline 规范化和查询投影将受支持的 Source +呈现为统一视图。Snapshot 固定查询使用的 Source 版本,导入不会补出源数据中不存在的事实。 ## 按问题继续阅读 -- [完整架构与目标模型](architecture.md) -- [从本地到集群的连续性](local-to-fleet.md) -- [安全与 Evidence 模型](security-evidence.md) -- pVisor 实现边界 (外部仓库) -- [pChronicle 实现边界](../pchronicle/design/index.md) - -交付状态以产品 Design 页面与[项目工程笔记](../project/engineering.md)为准。目标架构不能 -作为功能已经实现的证据。 +- [组件职责与数据流](architecture.md) +- [设计原则](design-principles.md) +- [存储与查询实现](../pchronicle/design/index.md) +- [Gateway 捕获](../pchronicle/guides/serve-gateway.md) +- [项目工程说明](../project/engineering.md) diff --git a/docs/src/zh/system-design/local-to-fleet.md b/docs/src/zh/system-design/local-to-fleet.md deleted file mode 100644 index 8eace50e5..000000000 --- a/docs/src/zh/system-design/local-to-fleet.md +++ /dev/null @@ -1,26 +0,0 @@ -# 从本地到集群 - -> pVisor 与 pPilot 已拆分到外部仓库。本仓库保留 pChronicle 及捕获、持久历史所需的内部库。 - -可移植单位是逻辑 Run,而不是一台正在运行的虚拟机。 - -![AgentVisor 执行连续体](../../assets/diagrams/agentvisor/execution-continuum.svg) - -从本地 placement 迁移到集群时,以下内容必须保持稳定: - -- Run identity 与父子 lineage; -- Delegated authority 及其 generation; -- Semantic checkpoint 与 effect frontier; -- Artifact identity 与持久 evidence; -- 终态结果 ownership。 - -进程、Kernel、root filesystem、Node、Scheduler 与 execution provider 可以变化。只有 -能够满足 Run 所请求 capability 维度的 Provider 才能通过 admission。不支持的保证必须 -显式失败,不能在迁移后静默弱化。 - -在个人设备上,主要体验是 staged workspace 与可审查 Effect;在集群中,同一模型增加 -placement、tenant isolation、lease、attestation、恢复与 reconciliation,而不重新定义 Run。 - -稳定 identity 模型见 Run、Attempt 与 Effect (外部仓库)。Provider -admission 属于 pVisor 隔离设计 (外部仓库);集群协调(placement、lease -与 reconciliation)属于部署控制面,不会改变逻辑 Run 契约。 diff --git a/docs/src/zh/system-design/security-evidence.md b/docs/src/zh/system-design/security-evidence.md deleted file mode 100644 index 32d9718ed..000000000 --- a/docs/src/zh/system-design/security-evidence.md +++ /dev/null @@ -1,48 +0,0 @@ -# 安全与 Evidence 模型 - -> pVisor 与 pPilot 已拆分到外部仓库。本仓库保留 pChronicle 及捕获、持久历史所需的内部库。 - -Persisting 不会把安全压缩成一个 `safe` 或 `sandboxed` 标签。每个 Run 都按 capability -维度报告保证。pVisor 拥有 admission 与 runtime enforcement。Placement 与恢复机制不会提升 -pVisor 的 Evidence 等级。配置后的 -pChronicle capture 保存 lifecycle fact,且只保存 Gateway 或 -lifecycle event record 实际携带的 Evidence。完整 Run Bundle Evidence 清单仍留在本地, -除非另行搬运。 - -| 维度 | 示例机制 | Evidence 问题 | -| --- | --- | --- | -| 文件读取 | synthetic root、allowlisted projection | 哪些 host path 可见? | -| 文件写入 | staged OverlayFS、Landlock、Seatbelt | 进程树可以写到哪里? | -| 网络 | private namespace、virtio-net、proxy policy | direct socket 能否绕过策略? | -| 进程 | namespace、sandbox profile、继承 FD 清理 | 哪些 descendant 共享边界? | -| 凭据 | Run-scoped delivery 与 expiry | 哪个 identity 获得并使用 Secret? | -| Effect | stage、promotion decision、compensation record | 哪些后果进入真实系统? | - -Evidence 可以分为四级: - -1. **Declared**:配置请求了一条边界。 -2. **Mediated**:Agent-facing 路径经过控制点。 -3. **Enforced**:声明 threat model 内的绕过路径被阻断。 -4. **Attested**:Enforcement evidence 与精确 Run 和 Provider 绑定。 - -一个维度的强保证不会自动升级其他维度。Staged workspace 不能证明网络已隔离,捕获到 -流量也不能证明未观测 socket 不可能存在。 - -端到端链路是: - -```text -requested capability - → admission decision - → installed mechanism - → provider evidence - → observed Effect - → terminal result - → configured event-carried history -``` - -最后一段 event 路径比 Run Bundle 更窄:当前不会发布完整的 Artifact、lineage、filesystem -Effect、AgentCtl/network/resource Evidence、output 或 metrics 清单。 - -用户模型见 Capability 与 Evidence (外部仓库),平台机制见 -pVisor 隔离设计 (外部仓库)与 OverlayNet (外部仓库), -历史边界见[事实与 Projection](../pchronicle/concepts/facts-and-projections.md)。 diff --git a/docs/src/zh/why-persisting.md b/docs/src/zh/why-persisting.md index 38990b968..24018b09f 100644 --- a/docs/src/zh/why-persisting.md +++ b/docs/src/zh/why-persisting.md @@ -17,17 +17,15 @@ Persisting 专注于持久 Agent 历史: 每条工作流都应该容易回答三个问题: -1. Agent 被允许做什么? +1. 记录了哪些 Agent、模型与工具调用? 2. 实际发生了什么或改动了什么? 3. 哪些证据和历史支持这个答案? -Persisting 不会把命令成功当成边界完美的证明,而是记录实际可用的机制、限制、 -Effect 和 Evidence。 +答案始终关联到已记录的 Source 及其版本;记录缺失仍然是结论的限制。 ## 什么时候适合使用 -当 Agent 能修改真实项目、Run 需要在人审查后才能合并,或轨迹需要在终端关闭后 -仍可使用时,Persisting 就适合介入。使用 pChronicle 查询持久历史;pVisor 与 pPilot 已拆分到外部仓库。 +当轨迹需要在终端关闭后仍可浏览、查询和交换时,使用 pChronicle 保存与分析持久历史。 如果只是运行一次无需审查、也无需留存历史的脚本,Persisting 可能不是必要的基础设施。 diff --git a/docs/static/img/diagrams/agentvisor/agentvisor-stack.svg b/docs/static/img/diagrams/agentvisor/agentvisor-stack.svg deleted file mode 100644 index 768eac65f..000000000 --- a/docs/static/img/diagrams/agentvisor/agentvisor-stack.svg +++ /dev/null @@ -1,141 +0,0 @@ - - The AgentVisor category in the Agent infrastructure stack - Multiple Agents share a pool of compute, filesystem, network, model, tool, and credential resources through an AgentVisor. Each Agent Run receives an isolated virtual execution environment with its own identity, state, authority, effects, and failure boundary. - - - - - - - - - - - - - - - - - - - - - - - The AgentVisor category - The hypervisor for Agent execution - - - - Independent Agent Runs - Agent A · Agent B · Agent C · different frameworks and goals - - - request an Agent virtual execution environment - - - - AgentVisor - maps shared resources into isolated Agent virtual execution environments - - - - Agent environment A - identity · workspace · authority - Isolated state and effect boundary - - - - Agent environment B - identity · workspace · authority - Isolated state and effect boundary - - - - Agent environment C - identity · workspace · authority - Isolated state and effect boundary - - - - Resource multiplexing - compute · storage · network - Shared pool, bounded consumption - - - - Lifecycle + placement - create · place · suspend · migrate - Stable Run across physical substrates - - - - Continuity + evidence - checkpoint · lineage · outcomes - State and accountability follow the Run - - - - - Identity - human · workload - organization - Policy - intent and constraints - - - - - - Evidence - audit · lineage - attestation - History - durable accountable facts - - - - - capability-aware Attempt placement - - - - Host sandbox - local process boundary - - - Container - shared-kernel isolation - - - MicroVM - dedicated guest kernel - - - Confidential - attested environment - - - Fleet - scheduled remote provider - - - - - - Models · tools · files · data · networks · services · people · physical systems - - Multiple Agents share the infrastructure pool while identity, state, authority, effects, and failures remain isolated. - diff --git a/docs/static/img/diagrams/agentvisor/effect-governance.svg b/docs/static/img/diagrams/agentvisor/effect-governance.svg deleted file mode 100644 index 8d23ad997..000000000 --- a/docs/static/img/diagrams/agentvisor/effect-governance.svg +++ /dev/null @@ -1,108 +0,0 @@ - - The AgentVisor effect governance loop - An Agent action moves through intent, admission, execution, observation, containment, and a decision to promote, reject, or compensate. Evidence closes the loop. Reversible, transactional, compensatable, and irreversible effects require different controls. - - - - - - - - - - - - - - - - - - - The effect governance loop - Autonomy is useful only when consequences have identity, state, policy, and evidence - - - - - - - - - - Intent - Agent proposes - an action - - - Admit - authority + context - - - Execute - allow · deny - or transform - - - Observe - capture actual - outcome - - - Contain - isolate or hold - when possible - - - - - - Resolve the consequence - promote · reject · expire - rollback · compensate · escalate - The decision depends on reversibility - - - - - - Evidence + accountability - intent · decision · mechanism - outcome · lineage · terminal state - Durable facts close the loop - - - - future admission learns from evidence - - Different effects require different controls - - - Reversible - stage · review · promote · discard - - - Transactional - reserve · validate · commit atomically - - - Compensatable - commit + durable counter-action - - - Irreversible - strong admission · minimal authority - - - Observation proves that something happened. It does not, by itself, make the consequence controllable or reversible. - diff --git a/docs/static/img/diagrams/agentvisor/execution-continuum.svg b/docs/static/img/diagrams/agentvisor/execution-continuum.svg deleted file mode 100644 index 7e29c352e..000000000 --- a/docs/static/img/diagrams/agentvisor/execution-continuum.svg +++ /dev/null @@ -1,114 +0,0 @@ - - The AgentVisor execution continuum - A stable Agent Run identity, delegated authority, semantic checkpoint, effect frontier, lineage, and evidence can continue from a personal device through a team environment to a secure fleet, while the physical execution substrate and operational controls change. - - - - - - - - - - - - - - - - One Agent Run across an execution continuum - Semantic portability preserves authority and accountability while infrastructure changes - - - - What remains stable - - Run identity + intent - - Delegated authority - - Semantic checkpoint - - Effect frontier - - Lineage - - Enforcement + outcome evidence - - - - - promote artifacts and checkpoints - schedule with stronger guarantees - - - - Personal device - Low friction · same-owner trust - - Local AgentVisor - Autonomy inside; controlled promotion outside - - Process - sandbox - - Container - local OCI - - MicroVM - local VM - - - - - Team environment - Shared policy · review · artifacts - - Organizational AgentVisor - Identity, budgets, promotion, and common evidence - - Workstation - managed local - - Team pool - shared compute - - - - - Secure fleet - Multi-tenant · attested · reconciled - - Fleet AgentVisor - Fenced ownership, placement, and node evidence - - MicroVM - tenant pool - - Confidential - attested - - Remote - specialized - - - - - Portable artifacts and accountable history - content digests · checkpoints · effect records · evidence bundles · causal lineage - - - - - - Local-to-fleet is semantic continuity first. Live process-memory or VM migration is an optional provider capability, not the category definition. - diff --git a/docs/static/img/diagrams/persisting/libkrun-executor.svg b/docs/static/img/diagrams/persisting/libkrun-executor.svg deleted file mode 100644 index 31d906867..000000000 --- a/docs/static/img/diagrams/persisting/libkrun-executor.svg +++ /dev/null @@ -1,22 +0,0 @@ - - pVisor libkrun execution path - The host Run controller prepares an OCI or explicit root filesystem and a separate workspace, passes a RunnerSpec to the self-executed pVisor runner and libkrun, then mounts the merged root through virtio-fs into a minimal Linux guest that runs the Agent. - - pVisor libkrun execution pathThe VM replaces the executor; pVisor retains Run ownership and finalization - HOST PVISOR - Run controllerRunId · AttemptId · lease - OCI registry/cacheverified image layersor explicit rootfs - Rootfs OverlayFSimmutable lowerper-Run temporary upper - Host workspaceseparate durable mount - VmExecutorRunnerSpec + GuestSpecCPU · RAM · cwd · env - self-exec pVisor runnerLinux confinement or macOS HVF · watchdog · cancellation - - LIBKRUN VMM - libkrunvCPU · RAM · init - virtio-fsguest root + workspace - - MINIMAL LINUX GUEST - embedded initmount · env · exec - Agentguest process tree - exit · cancel · watchdog → Run controller - diff --git a/docs/static/img/diagrams/persisting/pchronicle-product.svg b/docs/static/img/diagrams/persisting/pchronicle-product.svg index 1dfd76521..8f263c3ff 100644 --- a/docs/static/img/diagrams/persisting/pchronicle-product.svg +++ b/docs/static/img/diagrams/persisting/pchronicle-product.svg @@ -38,7 +38,7 @@ Canonical event Sources - Gateway · pVisor lifecycle · native writers + Gateway · event producers · native writers diff --git a/docs/static/img/diagrams/persisting/system-products.svg b/docs/static/img/diagrams/persisting/system-products.svg deleted file mode 100644 index 271928944..000000000 --- a/docs/static/img/diagrams/persisting/system-products.svg +++ /dev/null @@ -1,105 +0,0 @@ - - Persisting workflows and optional integration - pVisor runs one Agent with a reviewable execution boundary, while pChronicle queries trajectory Datasets. Execution and history are independent starting points that can be connected by configured capture. - - - - - - - - - - - - - - - - - - - - - - - - Persisting product paths and optional integration - pPilot plans many Runs; pVisor governs each Attempt; pChronicle stores history - - Inputs - - - Agent command / task - - - - Pinned external Sources - - - - - - - - EXECUTION PATH - pVisor: controlled execution - - - pVisor: one governed Run per Agent - pPilot adds planning, leases, retry, and reconciliation - - - Host · OCI container · libkrun VM - provider boundary and Evidence recorded with the Run - - - - - HISTORY PATH - pChronicle: trajectory Datasets - - - local/S3 files · Storyline Sources · @alias - - - Snapshot · normalize · query · analyze · exchange - - - - - OPTIONAL CAPTURE - Gateway trajectory events - pVisor lifecycle records - recorded execution context - - - configured capture - - - - - Shared principle - keep reusable state durable; keep completed work inspectable - - diff --git a/docs/static/img/diagrams/pvisor/agentvisor-architecture.svg b/docs/static/img/diagrams/pvisor/agentvisor-architecture.svg deleted file mode 100644 index adb179d61..000000000 --- a/docs/static/img/diagrams/pvisor/agentvisor-architecture.svg +++ /dev/null @@ -1,135 +0,0 @@ - - pVisor AgentVisor architecture - An Agent sends one Run contract into pVisor. pVisor governs lifecycle, capabilities, Effects, checkpoints, lineage, Evidence, and runtime drivers. pPilot orchestrates many Runs, while host, container, VM, and future fleet providers execute Attempts. Configured pChronicle capture receives Gateway trajectory events, pVisor lifecycle records, and their event-carried Evidence; the full Run Bundle and its Artifact, lineage, Effect, and broader Evidence inventory remain local. - - - - - - - - - - - - - - - - - - - - - - - pVisor is an AgentVisor - One governed Agent Run across local and clustered execution providers - - - - - - Autonomous Agent - CLI · coding Agent · evaluation worker - - - RunSpec + capability intent - - - - pVisor - AgentVisor control and containment layer - - - - Lifecycle - Run · Attempt · cancel · terminal - Stable identity above process - - - - Capabilities - read · write · network · tools - Evidence per requested dimension - - - - Effects - observe · stage · apply · drop - Control what becomes real - - - - Checkpoint - quiesce · snapshot · fork · lineage - Logical Agent + workspace state - - - - Evidence - Run Bundle · events · metrics - What ran, changed, and enforced - - - - Drivers - AgentCtl · Gateway · Overlay* - One policy context per Attempt - - - - - pPilot - plan · lease · schedule - retry · reconcile - Many Runs - - - - - - pChronicle - Configured event handoff - Dataset · history - Event-carried facts only - - - Gateway trajectory events - pVisor lifecycle records + carried Evidence - - - Attempt placement - - - - Safe host - Linux · macOS - - - Container - Docker · Podman - - - libkrun VM - KVM · HVF - - - Fleet provider - Product gate - - - AgentVisor defines Agent semantics; execution providers define process, container, VM, and node mechanics. - diff --git a/docs/static/img/logos/pvisor-icon.png b/docs/static/img/logos/pvisor-icon.png deleted file mode 100644 index 44e0a74323a6a6722f0e845860735d8f16994ce9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 135697 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�{eF$>btq6rR5*t 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 diff --git a/docs/static/img/logos/pvisor-with-text.png b/docs/static/img/logos/pvisor-with-text.png deleted file mode 100644 index 28b3ae58eb3da926228b9d1df0a9c57abea419d5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 224004 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 z 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 z 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 QD=!2@TSK3mr)J~-KN&IOga7~l diff --git a/docs/zensical.toml b/docs/zensical.toml index 98409ba50..d150a75a5 100644 --- a/docs/zensical.toml +++ b/docs/zensical.toml @@ -17,7 +17,7 @@ nav = [ { "Recording and Analyzing Agent Trajectories" = "en/pchronicle/get-started.md" }, ] }, { "pChronicle" = ["en/pchronicle/index.md", "en/pchronicle/get-started.md", { "Concepts" = ["en/pchronicle/concepts/index.md", "en/pchronicle/concepts/dataset-and-source.md", "en/pchronicle/concepts/facts-and-projections.md"] }, { "Guides" = ["en/pchronicle/guides/index.md", "en/pchronicle/guides/discover-and-query.md", "en/pchronicle/guides/exchange.md", "en/pchronicle/guides/serve.md", "en/pchronicle/guides/serve-gateway.md", "en/pchronicle/guides/ui.md", "en/pchronicle/guides/troubleshooting.md"] }, { "Design" = ["en/pchronicle/design/index.md", "en/pchronicle/design/architecture.md", "en/pchronicle/design/catalog.md", "en/pchronicle/design/trajectory-storage.md", "en/pchronicle/design/storyline-lance.md"] }, { "Reference" = ["en/pchronicle/reference/index.md", "en/pchronicle/reference/cli.md", "en/pchronicle/reference/query-model.md", "en/pchronicle/reference/terminology.md", "en/pchronicle/reference/agenticmd.md", "en/pchronicle/reference/formats/index.md", "en/pchronicle/reference/cases-self.md", "en/pchronicle/reference/cases-platform.md"] }] }, - { "Agent Infra Thinkings" = ["en/why-persisting.md", "en/roadmap.md", "en/guides/using-persisting.md", "en/system-design/index.md", "en/system-design/design-principles.md", "en/system-design/architecture.md", "en/system-design/local-to-fleet.md", "en/system-design/security-evidence.md", "en/project/index.md", "en/project/examples.md", "en/project/engineering.md", "en/project/releasing.md", { "RFCs" = ["en/rfcs/index.md", "en/rfcs/0001-storyline-format.md", "en/rfcs/0002-events-format.md", "en/rfcs/0003-pchronicle-ownership.md", "en/rfcs/0004-actf-format.md", "en/rfcs/0005-pchronicle-revision-lineage.md", "en/rfcs/0006-pchronicle-vortex-backend.md", "en/rfcs/0007-events-contract-pchronicle-sidecar.md", "en/rfcs/0008-atif-format.md", "en/rfcs/0009-openai-messages-format.md", "en/rfcs/0010-agent-corpus-lance-layout.md", "en/rfcs/0012-pchronicle-find-query-syntax.md", "en/rfcs/0013-pchronicle-warehouse-catalog.md", "en/rfcs/0014-compact-jsonl.md", "en/rfcs/0015-chronicle-manifest.md"] }] }, + { "Agent Infra Thinkings" = ["en/why-persisting.md", "en/roadmap.md", "en/guides/using-persisting.md", "en/system-design/index.md", "en/system-design/design-principles.md", "en/system-design/architecture.md", "en/project/index.md", "en/project/examples.md", "en/project/engineering.md", "en/project/releasing.md", { "RFCs" = ["en/rfcs/index.md", "en/rfcs/0001-storyline-format.md", "en/rfcs/0002-events-format.md", "en/rfcs/0003-pchronicle-ownership.md", "en/rfcs/0004-actf-format.md", "en/rfcs/0005-pchronicle-revision-lineage.md", "en/rfcs/0006-pchronicle-vortex-backend.md", "en/rfcs/0007-events-contract-pchronicle-sidecar.md", "en/rfcs/0008-atif-format.md", "en/rfcs/0009-openai-messages-format.md", "en/rfcs/0010-agent-corpus-lance-layout.md", "en/rfcs/0012-pchronicle-find-query-syntax.md", "en/rfcs/0013-pchronicle-warehouse-catalog.md", "en/rfcs/0014-compact-jsonl.md", "en/rfcs/0015-chronicle-manifest.md"] }] }, ] [project.theme]