Files
wrenn/envd-rs/src/http/snapshot.rs
pptx704 e34c9e1abe Harden envd concurrency and fix CoW loop device leaks
envd (0.6.0):
- Run blocking syscalls (sync/drop_caches, directory walks and
  removals, stdin/pty writes) on the blocking pool so health checks
  and exec streams don't stall during snapshot quiesce or large-tree
  operations
- Stop writing sandbox metadata into envd's own environment at init;
  it was undefined behavior with the async runtime running
- Retry a memory preload that previously failed or was cancelled
  instead of blocking every future pause until the next init
- Match RAM ranges against kernel core segments on page boundaries so
  preload no longer rejects kernels with unaligned low-memory ranges
- Tolerate a concurrent request creating a directory first

host agent:
- Detach the copy-on-write loop device when snapshot removal fails so
  deleting its backing file can no longer orphan it permanently
- Clean the sandbox directory on pause rollback, and skip paused
  restore for directories still owned by a live VM that failed to
  re-attach
2026-07-07 20:48:39 +06:00

68 lines
2.8 KiB
Rust

use std::sync::Arc;
use axum::extract::State;
use axum::http::{StatusCode, header};
use axum::response::IntoResponse;
use nix::unistd::sync;
use crate::state::AppState;
/// POST /snapshot/prepare — called by the host agent immediately before it
/// invokes vm.pause + vm.snapshot. The handler quiesces guest state so the
/// resulting snapshot is clean: outstanding writes are flushed to disk, the
/// VFS page cache is dropped (so the dm-snapshot CoW is the source of truth),
/// and the port forwarder is stopped to prevent socat children from being
/// frozen mid-handshake.
pub async fn post_snapshot_prepare(State(state): State<Arc<AppState>>) -> impl IntoResponse {
// Stop port forwarder + scanner so no socat process is captured in the
// snapshot with a half-open TCP connection. /init on resume restarts it.
if let Some(ref port_sub) = state.port_subsystem {
port_sub.stop();
}
// Flush in-memory FS state, then drop the VFS page cache +
// dentries/inodes. sync first so the pages we drop are clean; dropping
// reduces snapshot size by ensuring CH only persists memory pages the
// guest actually needs.
flush_and_drop_caches("first pass").await;
// Best-effort fstrim on the rootfs so unused blocks are returned to the
// dm-snapshot, keeping CoW size minimal.
let _ = tokio::process::Command::new("fstrim")
.arg("/")
.output()
.await;
// Second pass after fstrim: fstrim re-reads superblock / group descriptor
// pages that we just evicted, putting them back in the page cache. This
// drops those and any other late readers (e.g. sync flushers).
flush_and_drop_caches("second pass").await;
// No balloon settle window here: free-page reporting drains asynchronously,
// so any pages not yet hole-punched by the host at snapshot time are written
// verbatim — but with init_on_free=1 the guest zeroes them on free, and the
// host-side background zero-page punch reclaims them off the pause critical
// path. Trading a fixed ~1s of pause latency for a slightly larger artifact
// that the async punch later shrinks anyway.
tracing::info!("snapshot/prepare: quiesced");
(
StatusCode::NO_CONTENT,
[(header::CACHE_CONTROL, "no-store")],
)
}
// sync(2) can block for seconds flushing dirty pages, and the drop_caches
// write blocks while the kernel evicts — both run on the blocking pool or
// they starve every async task sharing the worker thread (health probes,
// exec streams) for the whole quiesce window.
async fn flush_and_drop_caches(pass: &'static str) {
let _ = tokio::task::spawn_blocking(move || {
sync();
if let Err(e) = std::fs::write("/proc/sys/vm/drop_caches", "3") {
tracing::warn!(error = %e, pass, "drop_caches failed (continuing)");
}
})
.await;
}