diff --git a/crates/forge/tests/e2e_git.rs b/crates/forge/tests/e2e_git.rs index 85d1886..d56f0cb 100644 --- a/crates/forge/tests/e2e_git.rs +++ b/crates/forge/tests/e2e_git.rs @@ -947,6 +947,41 @@ fn a_push_over_the_size_cap_fails_and_changes_nothing() { wait_for("cleanup", || s.app.life.registry.live() == 0); } +/// `--git-memory-limit` is `RLIMIT_DATA`, which only Linux enforces usefully. +#[cfg(target_os = "linux")] +#[test] +fn the_git_memory_limit_stops_a_push_whose_object_needs_more() { + // 128 MiB of zeros: a tiny pack, but git inflates the blob in memory to hash it (it is + // under core.bigFileThreshold), so the receiving git needs well over 64 MiB. + let push_big = |s: &Srv, tag: &str| { + let c = Client::new(tag); + let url = repo(s, "smart"); + let auth = token(&s.admin); + let work = seeded(&c, &url, &auth); // a small push works under the limit + std::fs::write(work.join("zeros.bin"), vec![0u8; 128 << 20]).unwrap(); + c.ok(&work, None, &["add", "."]); + c.ok(&work, None, &["commit", "-q", "-m", "zeros"]); + let (ok, text) = c.git_in(&work, Some(&auth), &["push", "-q", &url, "main"]); + let tip = c.ok(&work, Some(&auth), &["ls-remote", &url, "refs/heads/main"]); + let parent = c.ok(&work, None, &["rev-parse", "HEAD~1"]); + wait_for("cleanup", || s.app.life.registry.live() == 0); + (ok, text, tip.starts_with(parent.trim())) + }; + let limited = Srv::start_transport(scl_forge::cgi::Cfg { + git_memory: 64 << 20, + ..Default::default() + }); + let (ok, text, unchanged) = push_big(&limited, "memlimit"); + assert!(!ok && unchanged, "the limit must stop the push: {text}"); + assert!( + text.contains("Out of memory"), + "stopped by the limit: {text}" + ); + // Control: the same push under the default limit lands. + let (ok, text, unchanged) = push_big(&Srv::start(), "memlimit-control"); + assert!(ok && !unchanged, "{text}"); +} + #[test] fn a_full_disk_reserve_refuses_pushes_with_507_but_not_reads() { let s = Srv::start_transport(scl_forge::cgi::Cfg { diff --git a/docs/THREAT-MODEL.md b/docs/THREAT-MODEL.md index b821600..e8d5448 100644 --- a/docs/THREAT-MODEL.md +++ b/docs/THREAT-MODEL.md @@ -909,7 +909,8 @@ unless said otherwise). The child runs with `RLIMIT_CPU` (900 s), `RLIMIT_FSIZE`, `RLIMIT_CORE=0` and, on Linux, `RLIMIT_DATA` (`--git-memory-limit`, 8 GiB per child, so the host-wide bound is `--max-transports` times that; anonymous memory, which stops a pack declaring a huge compressed object from growing one child without limit; - not enforced on macOS and untested in CI: a cgroup limit on the service is the real bound), a wall-clock limit + not enforced on macOS; on Linux a push whose object needs more fails with git's out-of-memory error and moves nothing, + `the_git_memory_limit_stops_a_push_whose_object_needs_more`, and a cgroup limit on the service is the host-wide bound), a wall-clock limit (`--git-timeout`) and directional stall deadlines (`--git-idle-timeout`: no request bytes while git waits for them, or no room in the response because the client does not read), all of which end the transport (`slots_are_bounded_then_released_when_the_client_disconnects`, `a_stalled_upload_is_cut_off_by_the_idle_deadline`,