The in-flight budget tests still assumed buffer-only accounting #254
@@ -194,7 +194,7 @@ struct ReassemblyWindow {
|
||||
/// partially-arrived frames of ACTUAL size (≪ max); without this cap, [`HARD_LOSS_WINDOW`]
|
||||
/// max-sized declarations from one header-sized packet each could commit gigabytes — an
|
||||
/// amplification the old sparse per-shard allocation didn't have.
|
||||
const IN_FLIGHT_BUF_FACTOR: usize = 4;
|
||||
pub(super) const IN_FLIGHT_BUF_FACTOR: usize = 4;
|
||||
|
||||
/// Recovery-shard buffer pool ceiling (shard-sized buffers): enough for several max-recovery
|
||||
/// blocks in flight, small enough (~720 KB at a 1408-byte shard) to keep after a loss burst.
|
||||
@@ -208,7 +208,12 @@ const RECOVERY_POOL_MAX: usize = 512;
|
||||
/// can mint thousands of distinct-index blocks while its `FrameBuf::buf` stays pinned near zero —
|
||||
/// they must be metered exactly like the buffer, or the firewall meters only half the allocation
|
||||
/// (security-review 2026-08-15 finding 11).
|
||||
fn block_state_bytes(data_shards: usize, recovery_shards: usize) -> usize {
|
||||
///
|
||||
/// `pub(super)` so the budget tests can locate the refusal boundary from the cost model itself
|
||||
/// rather than from a baked-in frame count — [`BlockState`] gaining a field moves that boundary,
|
||||
/// and a test that hard-codes it answers such a change with an arithmetic puzzle instead of the
|
||||
/// question actually worth asking.
|
||||
pub(super) fn block_state_bytes(data_shards: usize, recovery_shards: usize) -> usize {
|
||||
std::mem::size_of::<BlockState>()
|
||||
+ data_shards // have_data: Vec<bool>
|
||||
+ recovery_shards * std::mem::size_of::<Option<Vec<u8>>>() // recovery slot table
|
||||
|
||||
@@ -520,13 +520,19 @@ fn e2e_unrecoverable_loss_ages_out() {
|
||||
/// gigabytes (the eager whole-frame buffer's amplification defense).
|
||||
#[test]
|
||||
fn in_flight_buffer_budget_bounds_allocation() {
|
||||
let lim = limits(); // max_frame_bytes 4096, shards 16 B, ≤8 data shards × ≤4 blocks
|
||||
// limits(): max_frame_bytes 4096, shards 16 B, ≤8 data shards × ≤4 blocks → budget 16384 B.
|
||||
let lim = limits();
|
||||
let budget = IN_FLIGHT_BUF_FACTOR * lim.max_frame_bytes;
|
||||
// What ONE such frame commits: the largest geometry-consistent buffer (4 blocks × 8 shards
|
||||
// × 16 B = 512 B) plus the state of the single block this first shard opens. Both are sized
|
||||
// from header fields, so the firewall meters both — counting only the buffer is precisely
|
||||
// the hole security-review 2026-08-15 #11 closed, and the boundary moved when it did.
|
||||
let per_frame = 512 + block_state_bytes(8, 0);
|
||||
let fits = budget / per_frame;
|
||||
let mut r = Reassembler::new(lim);
|
||||
let coder = coder_for(FecScheme::Gf8);
|
||||
let stats = StatsCounters::default();
|
||||
// Largest geometry-consistent frame: 4 blocks × 8 shards × 16 B = 512 B per buffer.
|
||||
// Budget = 4 × 4096 = 16384 B → exactly 32 such frames fit; the 33rd must be refused.
|
||||
for i in 0..33u32 {
|
||||
for i in 0..=fits as u32 {
|
||||
let mut h = base_header();
|
||||
h.frame_index = i;
|
||||
h.frame_bytes = 512;
|
||||
@@ -539,6 +545,14 @@ fn in_flight_buffer_budget_bounds_allocation() {
|
||||
1,
|
||||
"the frame past the budget is dropped, everything under it accepted"
|
||||
);
|
||||
// The point of the whole exercise: whatever the geometry, the commitment stays under the
|
||||
// ceiling. Asserted on the live figure, so a release site that forgets half the cost (the
|
||||
// 0.23.0 accounting-drift lesson on `in_flight`) fails here and not in the field.
|
||||
assert!(
|
||||
r.in_flight() <= budget,
|
||||
"in-flight commitment {} must never exceed the {budget} B budget",
|
||||
r.in_flight(),
|
||||
);
|
||||
}
|
||||
|
||||
/// A header whose (data_shards, block_count) disagree with the geometry derived from its own
|
||||
@@ -1519,11 +1533,15 @@ fn streamed_open_commits_its_own_extent_and_stays_bounded() {
|
||||
);
|
||||
|
||||
// A SLICE sentinel whose wire base sits just under the ceiling really does commit a
|
||||
// max-sized frame (base 3968 B + K 8 = 256 shards = 4096 B) — four fit the budget, the
|
||||
// fifth must be refused.
|
||||
let mut r = Reassembler::new(limits());
|
||||
// max-sized frame (base 3968 B + K 8 = 256 shards = 4096 B), plus the state of the block it
|
||||
// opens — so the budget takes fewer of these than the buffer alone would suggest, and the
|
||||
// first one past it must be refused.
|
||||
let lim = limits();
|
||||
let budget = IN_FLIGHT_BUF_FACTOR * lim.max_frame_bytes;
|
||||
let fits = budget / (4096 + block_state_bytes(8, 0));
|
||||
let mut r = Reassembler::new(lim);
|
||||
let stats = StatsCounters::default();
|
||||
for fi in 0..5u32 {
|
||||
for fi in 0..=fits as u32 {
|
||||
let mut h = base_header();
|
||||
h.user_flags = USER_FLAG_SLICE_STREAM;
|
||||
h.block_count = 0;
|
||||
@@ -1537,10 +1555,15 @@ fn streamed_open_commits_its_own_extent_and_stays_bounded() {
|
||||
.unwrap()
|
||||
.is_none());
|
||||
}
|
||||
assert!(
|
||||
r.in_flight() <= budget,
|
||||
"in-flight commitment {} must never exceed the {budget} B budget",
|
||||
r.in_flight(),
|
||||
);
|
||||
assert_eq!(
|
||||
stats.snapshot().packets_dropped,
|
||||
1,
|
||||
"the fifth ceiling-claiming open must be refused by the in-flight budget"
|
||||
"the first ceiling-claiming open past the budget must be refused"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user