swactor/crates/data-plane/tests/descriptor_guarantees.rs

613 lines
21 KiB
Rust
Raw Permalink Normal View History

#![cfg(target_os = "linux")]
include!("data_plane_test_support.inc");
use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::Cell;
use proptest::prelude::*;
struct ThreadCountingAllocator;
thread_local! {
static COUNT_ALLOCATIONS: Cell<bool> = const { Cell::new(false) };
static ALLOCATION_COUNT: Cell<usize> = const { Cell::new(0) };
}
#[global_allocator]
static COUNTING_ALLOCATOR: ThreadCountingAllocator = ThreadCountingAllocator;
unsafe impl GlobalAlloc for ThreadCountingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
COUNT_ALLOCATIONS.with(|enabled| {
if enabled.get() {
ALLOCATION_COUNT.with(|count| count.set(count.get() + 1));
}
});
// SAFETY: this allocator delegates the unchanged layout to `System`.
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
// SAFETY: `pointer` came from `System` with this layout.
unsafe { System.dealloc(pointer, layout) };
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
COUNT_ALLOCATIONS.with(|enabled| {
if enabled.get() {
ALLOCATION_COUNT.with(|count| count.set(count.get() + 1));
}
});
// SAFETY: this allocator delegates the unchanged layout to `System`.
unsafe { System.alloc_zeroed(layout) }
}
unsafe fn realloc(&self, pointer: *mut u8, layout: Layout, size: usize) -> *mut u8 {
COUNT_ALLOCATIONS.with(|enabled| {
if enabled.get() {
ALLOCATION_COUNT.with(|count| count.set(count.get() + 1));
}
});
// SAFETY: `pointer` and `layout` came from `System`.
unsafe { System.realloc(pointer, layout, size) }
}
}
fn start_allocation_count() {
ALLOCATION_COUNT.with(|count| count.set(0));
COUNT_ALLOCATIONS.with(|enabled| enabled.set(true));
}
fn finish_allocation_count() -> usize {
COUNT_ALLOCATIONS.with(|enabled| enabled.set(false));
ALLOCATION_COUNT.with(Cell::get)
}
const REQUIRED_MUTATION_TARGETS: &[(&str, &str)] = &[
(
"omit_access_check",
"forbidden_actions_report_stable_errno_and_do_not_mutate_live_state",
),
(
"advance_requested_count",
"generated_blob_sequences_preserve_exact_tagged_prefixes",
),
(
"early_stream_eof",
"raw_stream_descriptor_hides_record_boundaries_and_preserves_eof",
),
(
"release_partial_record",
"partial_record_cursor_keeps_capacity_pinned_until_full_release",
),
(
"duplicate_stream_prefix",
"raw_stream_descriptor_hides_record_boundaries_and_preserves_eof",
),
(
"cross_wire_open_grant",
"concurrent_descriptor_opens_keep_paths_and_payloads_correlated",
),
(
"publish_aborted_blob",
"legal_blob_sequences_cover_boundaries_offsets_mapping_close_and_abort",
),
(
"reclaim_live_mapping",
"raw_blob_mapping_is_bounded_and_can_outlive_descriptor_close",
),
(
"writable_read_only_export",
"test_raw_descriptor_blob_io_mapping_and_errno",
),
(
"retain_cancelled_waiter",
"cancellation_and_transport_faults_reclaim_waiters_and_preserve_unrelated_progress",
),
(
"double_publication",
"raw_blob_descriptor_enforces_offsets_rights_and_terminal_state",
),
(
"authorize_unresolved_alias",
"missing_and_unauthorized_paths_are_rejected",
),
(
"silently_stage_direct_map",
"raw_blob_mapping_is_bounded_and_can_outlive_descriptor_close",
),
(
"touch_outside_region",
"generated_blob_sequences_preserve_exact_tagged_prefixes",
),
(
"leak_failed_open_lease",
"cancelled_write_open_releases_queued_grant",
),
];
#[test]
fn mutation_adequacy_targets_cover_every_required_bad_behavior() {
let mut identifiers: Vec<&str> = REQUIRED_MUTATION_TARGETS
.iter()
.map(|(identifier, _)| *identifier)
.collect();
assert_eq!(identifiers.len(), 15);
identifiers.sort_unstable();
identifiers.dedup();
assert_eq!(identifiers.len(), 15, "mutation identifiers must be unique");
assert!(
REQUIRED_MUTATION_TARGETS
.iter()
.all(|(_, oracle)| !oracle.is_empty())
);
}
#[test]
fn legal_blob_sequences_cover_boundaries_offsets_mapping_close_and_abort() {
let harness = harness(2 << 20);
let data_plane = harness.bootstrap.data_plane.clone();
future::block_on(async {
for (case, length) in [0_usize, 1, 7, 64].into_iter().enumerate() {
let logical = path(&format!("/runs/self/results/legal-{case}"));
let payload: Vec<u8> = (0..length).map(|index| index as u8).collect();
let mut writer = data_plane
.open(&logical, OpenOptions::staged_blob(length as u64))
.await
.expect("legal staged open");
for chunk in payload.chunks(3) {
writer.write_all(chunk).await.expect("legal partial write");
}
writer.close().await.expect("legal publication");
let mut reader = data_plane
.open(&logical, OpenOptions::read_only())
.await
.expect("legal read open");
let mapping = reader
.map(MapRequest {
protection: Protection::Read,
sharing: Sharing::Shared,
target: MapTarget::Host,
offset: 0,
length: length as u64,
})
.expect("legal read mapping");
assert_eq!(mapping.as_ref(), payload);
assert_eq!(mapping.route(), TransferRoute::Direct);
let mut observed = Vec::new();
let mut destination = [0xa5_u8; 11];
loop {
let count = reader
.read(&mut destination[..(case + 1).min(11)])
.await
.expect("legal sequential read");
if count == 0 {
break;
}
observed.extend_from_slice(&destination[..count]);
}
assert_eq!(observed, payload);
assert_eq!(reader.read(&mut destination).await.expect("sticky eof"), 0);
reader.close().await.expect("legal reader close");
}
let aborted = path("/runs/self/results/legal-abort");
let mut writer = data_plane
.open(&aborted, OpenOptions::staged_blob(4))
.await
.expect("abort candidate");
writer.write_all(b"nope").await.expect("staged bytes");
writer.abort().await.expect("explicit abort");
assert_eq!(
data_plane
.open(&aborted, OpenOptions::read_only())
.await
.expect_err("aborted blob is absent")
.errno(),
Errno::Enoent
);
});
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(8))]
#[test]
fn generated_blob_sequences_preserve_exact_tagged_prefixes(
payload in prop::collection::vec(any::<u8>(), 0..96),
write_size in 1_usize..17,
read_size in 1_usize..17,
) {
let harness = harness(2 << 20);
let data_plane = harness.bootstrap.data_plane.clone();
future::block_on(async {
let logical = path("/runs/self/results/property-sequence");
let mut writer = data_plane
.open(&logical, OpenOptions::staged_blob(payload.len() as u64))
.await
.expect("property writer");
for chunk in payload.chunks(write_size) {
writer.write_all(chunk).await.expect("property write");
}
writer.close().await.expect("property publish");
let mut reader = data_plane
.open(&logical, OpenOptions::read_only())
.await
.expect("property reader");
let mut destination = vec![0xa5_u8; read_size + 2];
let mut observed = Vec::new();
loop {
destination.fill(0xa5);
let count = reader
.read(&mut destination[1..=read_size])
.await
.expect("property read");
assert_eq!(destination[0], 0xa5);
assert_eq!(destination[read_size + 1], 0xa5);
if count == 0 {
break;
}
observed.extend_from_slice(&destination[1..1 + count]);
}
// Kills: advance offset by requested rather than completed bytes,
// touch destination outside the returned prefix, duplicate/drop data.
prop_assert_eq!(observed, payload);
Ok(())
})?;
}
}
#[test]
fn forbidden_actions_report_stable_errno_and_do_not_mutate_live_state() {
let harness = harness(2 << 20);
let data_plane = harness.bootstrap.data_plane.clone();
let logical = path("/runs/self/results/negative");
future::block_on(async {
let mut writer = data_plane
.open(&logical, OpenOptions::staged_blob(4))
.await
.expect("negative fixture writer");
writer.write_all(b"safe").await.expect("fixture bytes");
assert_eq!(
writer
.read(&mut [0_u8; 1])
.await
.expect_err("wrong access")
.errno(),
Errno::Ebadf
);
writer.close().await.expect("fixture publication");
let invalid = OpenOptions {
exclusive: true,
..OpenOptions::default()
};
assert_eq!(
data_plane
.open(&logical, invalid)
.await
.expect_err("invalid flags")
.errno(),
Errno::Einval
);
assert_eq!(
data_plane
.open(
&logical,
OpenOptions {
nonblocking: true,
..OpenOptions::default()
},
)
.await
.expect_err("unsupported nonblocking")
.errno(),
Errno::Enotsup
);
assert_eq!(
data_plane
.open(&path("/models/missing-raw"), OpenOptions::read_only())
.await
.expect_err("missing path")
.errno(),
Errno::Enoent
);
assert_eq!(
data_plane
.open(
&path("/models/tiny-linear/weights"),
OpenOptions::staged_blob(1),
)
.await
.expect_err("unauthorized write")
.errno(),
Errno::Eacces
);
assert_eq!(
data_plane
.open(
&logical,
OpenOptions {
exclusive: true,
..OpenOptions::staged_blob(4)
},
)
.await
.expect_err("exclusive existing")
.errno(),
Errno::Eexist
);
let mut reader = data_plane
.open(&logical, OpenOptions::read_only())
.await
.expect("negative fixture reader");
assert_eq!(
reader
.map(MapRequest {
protection: Protection::Read,
sharing: Sharing::Shared,
target: MapTarget::Host,
offset: 3,
length: 2,
})
.expect_err("mapping overrun")
.errno(),
Errno::Einval
);
assert_eq!(
reader
.write(b"x")
.await
.expect_err("read-only write")
.errno(),
Errno::Ebadf
);
let mut bytes = [0_u8; 4];
reader
.read_exact(&mut bytes)
.await
.expect("state unchanged");
assert_eq!(&bytes, b"safe");
reader.close().await.expect("first close");
assert_eq!(
reader.close().await.expect_err("double close").errno(),
Errno::Ebadf
);
// Kills: omitted access checks, mutation after failed bounds checks,
// silent flag downgrade, and revival after close.
});
}
#[test]
fn exclusive_create_is_atomic_and_abort_releases_its_reservation() {
let harness = harness(2 << 20);
let data_plane = harness.bootstrap.data_plane.clone();
let logical = path("/runs/self/results/exclusive-race");
let options = OpenOptions {
exclusive: true,
..OpenOptions::staged_blob(4)
};
future::block_on(async {
let first = data_plane.open(&logical, options.clone());
let second = data_plane.open(&logical, options.clone());
let (first, second) = future::zip(first, second).await;
let (mut winner, loser) = match (first, second) {
(Ok(winner), Err(loser)) | (Err(loser), Ok(winner)) => (winner, loser),
(Ok(_), Ok(_)) => panic!("both exclusive creators succeeded"),
(Err(first), Err(second)) => {
panic!("both exclusive creators failed: {first}; {second}")
}
};
assert_eq!(loser.errno(), Errno::Eexist);
winner.abort().await.expect("abort exclusive winner");
let mut replacement = data_plane
.open(&logical, options)
.await
.expect("reservation released after abort");
replacement
.abort()
.await
.expect("abort replacement reservation");
});
}
#[test]
fn raw_stream_abort_completes_before_peer_observes_broken_pipe() {
let _stream_test = STREAM_TEST_LOCK.lock();
let harness = harness(2 << 20);
let data_plane = harness.bootstrap.data_plane.clone();
let logical = path("/runs/self/results/raw-broken-pipe");
future::block_on(async {
let seed_reader = data_plane.read_stream(&logical);
let seed_writer = data_plane.write_stream(&logical);
let (seed_reader, seed_writer) = future::zip(seed_reader, seed_writer).await;
let mut seed_reader = seed_reader.expect("seed reader");
let mut seed_writer = seed_writer.expect("seed writer");
seed_writer.close().await.expect("seed writer close");
assert_eq!(seed_reader.read().await.expect("seed eof"), None);
let reader = data_plane.open(&logical, OpenOptions::read_only());
let writer = data_plane.open(
&logical,
OpenOptions {
access: AccessMode::WriteOnly,
..OpenOptions::default()
},
);
let (reader, writer) = future::zip(reader, writer).await;
let mut reader = reader.expect("raw reader");
let mut writer = writer.expect("raw writer");
reader.abort().await.expect("reader abort completion");
assert_eq!(
writer
.write(b"late")
.await
.expect_err("write after reader abort")
.errno(),
Errno::Epipe
);
});
}
#[test]
fn concurrent_descriptor_opens_keep_paths_and_payloads_correlated() {
let harness = harness(8 << 20);
let data_plane = harness.bootstrap.data_plane.clone();
std::thread::scope(|scope| {
for index in 0_u8..16 {
let data_plane = data_plane.clone();
scope.spawn(move || {
future::block_on(async move {
let logical = path(&format!("/runs/self/results/concurrent-{index}"));
let payload = [index; 32];
let mut writer = data_plane
.open(&logical, OpenOptions::staged_blob(payload.len() as u64))
.await
.expect("concurrent writer");
writer
.write_all(&payload)
.await
.expect("concurrent payload");
writer.close().await.expect("concurrent publish");
});
});
}
});
std::thread::scope(|scope| {
for index in 0_u8..16 {
let data_plane = data_plane.clone();
scope.spawn(move || {
future::block_on(async move {
let logical = path(&format!("/runs/self/results/concurrent-{index}"));
let mut reader = data_plane
.open(&logical, OpenOptions::read_only())
.await
.expect("concurrent reader");
let mut payload = [0_u8; 32];
reader
.read_exact(&mut payload)
.await
.expect("correlated read");
assert_eq!(payload, [index; 32]);
});
});
}
});
// Kills: accepting a grant for the wrong operation/path or crossing leases.
}
#[test]
fn cancellation_and_transport_faults_reclaim_waiters_and_preserve_unrelated_progress() {
let _stream_test = STREAM_TEST_LOCK.lock();
let harness = harness(2 << 20);
let data_plane = harness.bootstrap.data_plane.clone();
let cancelled_path = path("/runs/self/results/cancelled-open");
future::block_on(async {
let mut cancelled = Box::pin(data_plane.read_stream(&cancelled_path));
assert!(future::poll_once(cancelled.as_mut()).await.is_none());
drop(cancelled);
std::thread::sleep(Duration::from_millis(10));
let reader = data_plane.read_stream(&cancelled_path);
let writer = data_plane.write_stream(&cancelled_path);
let (reader, writer) = future::zip(reader, writer).await;
let mut reader = reader.expect("replacement reader");
let mut writer = writer.expect("replacement writer");
writer.close().await.expect("replacement close");
assert_eq!(reader.read().await.expect("replacement eof"), None);
let mut unrelated = data_plane
.open(&path("/models/second"), OpenOptions::read_only())
.await
.expect("unrelated progress");
let mut bytes = [0_u8; 11];
unrelated
.read_exact(&mut bytes)
.await
.expect("unrelated read");
assert_eq!(&bytes, b"second-blob");
});
let fault_harness = harness_with_transport(2 << 20, Arc::new(RejectingStreamTransport));
let fault_plane = fault_harness.bootstrap.data_plane.clone();
let fault_path = path("/runs/self/results/transport-fault");
future::block_on(async {
let mut reader_open = Box::pin(fault_plane.read_stream(&fault_path));
assert!(future::poll_once(reader_open.as_mut()).await.is_none());
let writer_error = match fault_plane.write_stream(&fault_path).await {
Ok(_) => panic!("faulted writer opened"),
Err(error) => error,
};
let reader_error = match reader_open.await {
Ok(_) => panic!("faulted reader opened"),
Err(error) => error,
};
assert!(matches!(
reader_error,
DataPlaneError::PeerLost | DataPlaneError::StreamFault(_)
));
assert!(matches!(
writer_error,
DataPlaneError::PeerLost | DataPlaneError::StreamFault(_)
));
let mut unrelated = fault_plane
.open(&path("/models/second"), OpenOptions::read_only())
.await
.expect("fault isolation");
let mut bytes = [0_u8; 11];
unrelated
.read_exact(&mut bytes)
.await
.expect("fault-isolated read");
assert_eq!(&bytes, b"second-blob");
});
// Kills: retain a cancelled waiter, complete only one matched endpoint,
// and propagate one descriptor fault into unrelated operations.
}
#[test]
fn steady_state_blob_primitives_allocate_no_heap_memory() {
let harness = harness(2 << 20);
let data_plane = harness.bootstrap.data_plane.clone();
future::block_on(async {
let logical = path("/runs/self/results/allocation-count");
let mut writer = data_plane
.open(&logical, OpenOptions::staged_blob(64))
.await
.expect("allocation writer");
let payload = [7_u8; 64];
start_allocation_count();
let result = writer.write(&payload).await;
let write_allocations = finish_allocation_count();
assert_eq!(result.expect("allocation-count write"), payload.len());
assert_eq!(write_allocations, 0, "blob write primitive allocated");
writer.close().await.expect("allocation publish");
let mut reader = data_plane
.open(&logical, OpenOptions::read_only())
.await
.expect("allocation reader");
let mut destination = [0_u8; 64];
start_allocation_count();
let result = reader.read(&mut destination).await;
let read_allocations = finish_allocation_count();
assert_eq!(result.expect("allocation-count read"), destination.len());
assert_eq!(read_allocations, 0, "blob read primitive allocated");
assert_eq!(destination, payload);
});
}