FFI: global memory-limit tests are process-wide and can race the rest of the --lib suite
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 346
- Forks
- 75
- Avg merge
- 1d 3h
- Merged PRs (30d)
- 15
Description
Summary
bindings/ffi/src/limits.rs::ffi_roundtrips_global_limit sets a process-global memory limit (456 bytes) with no serialization, while the other ~42 tests in the same regorus-ffi lib test binary run in parallel threads. Any concurrent test whose allocation check trips during that window can fail with a spurious LimitError::MemoryLimitExceeded.
This hasn't been observed failing — the window is short — but it's a latent flake, and more importantly it means there is currently no safe way to write a memory-limit test in the FFI crate at all.
Found while adding budget enforcement to the host-await accumulation paths in #672. That PR ships the enforcement without a test specifically because of this gap.
Why it's racy
GLOBAL_MEMORY_LIMIT is a process-wide AtomicU64 (src/utils/limits/memory.rs:9). cargo test --lib runs every unit test in one process across parallel threads, so a limit set by one test applies to all of them.
bindings/ffi/src/limits.rs:259-275:
#[test]
fn ffi_roundtrips_global_limit() {
let limit = 456_u64;
let result = regorus_set_global_memory_limit(limit, true);
// ... limit stays at 456 bytes across these assertions ...
let result = regorus_set_global_memory_limit(0, false);
regorus_result_drop(result);
}
No mutex, no #[ignore]. Concurrent tests that reach check_memory_limit_if_needed() during this window see a 456-byte ceiling. It's survivable today only because the window is ~4 atomic operations with no allocation-heavy work in between, and because the check amortizes (MEMORY_CHECK_STRIDE = 16, MEMORY_CHECK_DELTA_BYTES = 32 KiB, memory.rs:12-15), so most calls don't perform a real comparison.
If the test panics between set and clear, the limit is also never restored — every subsequent test in that process inherits it.
Contrast: the core crate does this correctly
tests/memory_limits.rs has 7 limit tests, all 7 take LimitGuard::lock(), 0 are ignored. It works because:
- It is an integration test, so it gets its own process — nothing else shares the global.
- Every test in the file takes the same mutex, and
Dropresets the limit toNone(tests/memory_limits.rs:18-59).
Why the FFI crate can't copy that pattern
bindings/ffi/Cargo.toml:11:
crate-type = ["cdylib", "staticlib"]
There is no rlib, so Rust integration tests under bindings/ffi/tests/ cannot link against the crate. All FFI tests must live in-crate as unit tests, sharing one process. The isolation that makes the core-crate pattern safe isn't available.
Impact
- Latent flakiness in
regorus-ffi--libruns, most likely to appear under high parallelism or on slower CI runners. - Blocks test coverage. Any FFI test that wants to assert limit-enforcement behaviour has to either accept the race or be
#[ignore]d out of CI. Concretely, #672 addscheck_memory_limit_if_needed()to the host-await accumulation loops and ships it untested for this reason — verified manually instead (1-byte limit →MemoryLimitExceeded, and mutation-checked that a test would catch its removal).
Possible directions
- Subprocess harness — a unit test re-execs the test binary with a marker env var and applies the limit only in the child. Keeps CI coverage, no crate-type change, ~30 lines of scaffolding.
- Add
rlibtocrate-type— enables a properbindings/ffi/tests/memory_limits.rswith the core crate'sLimitGuardpattern. Cleanest, but changes build output for all 9 bindings, so it needs a look at packaging/size implications. - Shared
LimitGuardinside the FFI crate — serializes limit-touching tests against each other, but not against the other ~42 that never take the lock. Partial mitigation only. - Minimum, regardless of the above — make
ffi_roundtrips_global_limitrestore the previous limit via aDropguard so a panic can't leak the 456-byte ceiling into the rest of the run.
Happy to send a PR for whichever direction is preferred.
References
bindings/ffi/src/limits.rs:259-275— unguarded global-limit testbindings/ffi/Cargo.toml:11—crate-typewithoutrlibsrc/utils/limits/memory.rs:9—GLOBAL_MEMORY_LIMITstaticsrc/utils/limits/memory.rs:12-15— amortization constantssrc/utils/limits/memory.rs:168-191—check_memory_limit_if_neededtests/memory_limits.rs:18-59— theLimitGuardpattern that works
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with bindings/ffi/src/limits.rs::ffi_roundtrips_global_limit and compare its process-wide limit handling with tests/memory_limits.rs:18-59. Review bindings/ffi/Cargo.toml and src/utils/limits/memory.rs to understand the test-process constraints. Done means the FFI limit tests are safe under parallel --lib execution, restore limits after failure, and cover the intended enforcement behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- testing
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100