use super::*;
#[cfg(target_arch = "aarch64")]
#[test]
fn aarch64_initrd_stays_below_pvtime_carve() {
use crate::vmm::{aarch64::fdt::pvtime_base, kvm::DRAM_START};
for &(mem, cpus) in &[(512u32, 2u32), (512, 8), (2048, 256), (4096, 512)] {
let pvt = pvtime_base(mem, cpus);
let max = pvt - DRAM_START - (1 << 20);
let load = aarch64_initrd_addr(mem, cpus, max).expect("near-max initrd must fit");
assert!(
load >= DRAM_START,
"initrd underflows DRAM_START (mem={mem} cpus={cpus} load={load:#x})"
);
assert!(
load + max <= pvt,
"initrd top {:#x} entered the PVTIME carve at {pvt:#x} (mem={mem} cpus={cpus})",
load + max
);
}
}
#[cfg(target_arch = "aarch64")]
#[test]
fn aarch64_initrd_oversized_returns_err_not_panic_or_wrap() {
use crate::vmm::{aarch64::fdt::pvtime_base, kvm::DRAM_START};
for &(mem, cpus) in &[(512u32, 2u32), (512, 8), (2048, 256), (4096, 512)] {
let pvt = pvtime_base(mem, cpus);
let oversized = (pvt - DRAM_START) + (1 << 20);
let result = aarch64_initrd_addr(mem, cpus, oversized);
assert!(
result.is_err(),
"oversized initrd must Err (mem={mem} cpus={cpus} \
oversized={oversized:#x} pvt={pvt:#x}), got {result:?}"
);
}
let (mem, cpus) = (512u32, 2u32);
let pvt = pvtime_base(mem, cpus);
let huge = pvt + (1 << 20);
assert!(
aarch64_initrd_addr(mem, cpus, huge).is_err(),
"initrd larger than pvtime_base must Err (huge={huge:#x} pvt={pvt:#x})"
);
}
#[test]
fn disk_auto_mount_cmdline_tokens_raw_emits_nothing() {
let disk = disk_config::DiskConfig::default();
assert_eq!(disk.filesystem, disk_config::Filesystem::Raw);
assert_eq!(disk_auto_mount_cmdline_tokens(&disk), "");
}
#[test]
fn disk_auto_mount_cmdline_tokens_btrfs_default() {
let disk = disk_config::DiskConfig::default().filesystem(disk_config::Filesystem::Btrfs);
assert_eq!(
disk_auto_mount_cmdline_tokens(&disk),
" KTSTR_DISK0_FS=btrfs KTSTR_DISK0_MOUNT=/mnt/disk0",
);
}
#[test]
fn disk_auto_mount_cmdline_tokens_btrfs_named() {
let disk = disk_config::DiskConfig::default()
.filesystem(disk_config::Filesystem::Btrfs)
.with_name("data");
assert_eq!(
disk_auto_mount_cmdline_tokens(&disk),
" KTSTR_DISK0_FS=btrfs KTSTR_DISK0_MOUNT=/mnt/data",
);
}
#[test]
fn disk_auto_mount_cmdline_tokens_btrfs_read_only() {
let disk = disk_config::DiskConfig::default()
.filesystem(disk_config::Filesystem::Btrfs)
.read_only();
assert_eq!(
disk_auto_mount_cmdline_tokens(&disk),
" KTSTR_DISK0_FS=btrfs KTSTR_DISK0_MOUNT=/mnt/disk0 KTSTR_DISK0_RO=1",
);
}
#[test]
fn disk_auto_mount_cmdline_tokens_no_auto_mount_suppresses() {
let disk = disk_config::DiskConfig::default()
.filesystem(disk_config::Filesystem::Btrfs)
.no_auto_mount();
assert_eq!(disk_auto_mount_cmdline_tokens(&disk), "");
let disk = disk_config::DiskConfig::default()
.filesystem(disk_config::Filesystem::Btrfs)
.with_name("data")
.read_only()
.no_auto_mount();
assert_eq!(disk_auto_mount_cmdline_tokens(&disk), "");
}
#[test]
fn disk_auto_mount_cmdline_tokens_raw_with_no_auto_mount() {
let disk = disk_config::DiskConfig::default().no_auto_mount();
assert_eq!(disk.filesystem, disk_config::Filesystem::Raw);
assert_eq!(disk_auto_mount_cmdline_tokens(&disk), "");
}
#[test]
fn disk_auto_mount_cmdline_tokens_starts_with_space() {
let disk = disk_config::DiskConfig::default().filesystem(disk_config::Filesystem::Btrfs);
let s = disk_auto_mount_cmdline_tokens(&disk);
assert!(
s.starts_with(' '),
"non-empty tokens must start with a space for safe \
cmdline concatenation; got {s:?}",
);
}
fn build_synthetic_staged_set(
names: &[&str],
) -> (
tempfile::TempDir,
PathBuf,
Vec<crate::vmm::builder::StagedScheduler>,
) {
let dir = tempfile::Builder::new()
.prefix("ktstr-assemble-test-")
.tempdir()
.unwrap();
let payload = dir.path().join("payload");
std::fs::write(&payload, b"payload-content").unwrap();
let staged: Vec<crate::vmm::builder::StagedScheduler> = names
.iter()
.map(|name| {
let bin = dir.path().join(format!("staged_bin_{name}"));
std::fs::write(&bin, format!("staged-content-{name}").as_bytes()).unwrap();
crate::vmm::builder::StagedScheduler {
name: (*name).to_string(),
binary: bin,
sched_args: vec![format!("--variant={name}")],
}
})
.collect();
(dir, payload, staged)
}
fn staged_extras_names_for(staged: &[crate::vmm::builder::StagedScheduler]) -> Vec<String> {
staged
.iter()
.map(|s| {
format!(
"{}/scheduler",
crate::test_support::staged::staged_scheduler_archive_dir(&s.name),
)
})
.collect()
}
#[test]
fn assemble_extras_and_key_emits_staged_binary_under_correct_archive_path() {
let (_tmp, payload, staged) = build_synthetic_staged_set(&["scx_foo", "scx_bar"]);
let names = staged_extras_names_for(&staged);
let (extras, _key) = assemble_extras_and_key(
payload.as_path(),
None,
None,
None,
&staged,
&names,
&[],
None,
false,
)
.unwrap();
let extras_names: Vec<&str> = extras.iter().map(|(n, _)| *n).collect();
assert!(
extras_names.contains(&"staging/schedulers/scx_foo/scheduler"),
"missing scx_foo at canonical archive path; got {extras_names:?}",
);
assert!(
extras_names.contains(&"staging/schedulers/scx_bar/scheduler"),
"missing scx_bar at canonical archive path; got {extras_names:?}",
);
}
#[test]
fn assemble_extras_and_key_preserves_staged_iteration_order_in_extras() {
let (_tmp, payload, staged) = build_synthetic_staged_set(&["alpha", "beta", "gamma"]);
let names = staged_extras_names_for(&staged);
let (extras, _key) = assemble_extras_and_key(
payload.as_path(),
None,
None,
None,
&staged,
&names,
&[],
None,
false,
)
.unwrap();
for (i, name) in ["alpha", "beta", "gamma"].iter().enumerate() {
let (entry_name, entry_path) = extras[i];
let expected_name = format!("staging/schedulers/{name}/scheduler");
assert_eq!(
entry_name, expected_name,
"extras[{i}] expected name '{expected_name}', got '{entry_name}'",
);
assert!(
entry_path
.to_string_lossy()
.ends_with(&format!("staged_bin_{name}")),
"extras[{i}] binary path '{}' does not match expected staged_bin_{name}",
entry_path.display(),
);
}
}
#[test]
fn assemble_extras_and_key_threads_staged_into_basekey_in_both_modes() {
let (_tmp, payload, staged) = build_synthetic_staged_set(&["mitosis_a"]);
let names = staged_extras_names_for(&staged);
let empty: Vec<crate::vmm::builder::StagedScheduler> = vec![];
let empty_names: Vec<String> = vec![];
let (_, key_with_staged_nonshell) = assemble_extras_and_key(
payload.as_path(),
None,
None,
None,
&staged,
&names,
&[],
None,
false,
)
.unwrap();
let (_, key_empty_nonshell) = assemble_extras_and_key(
payload.as_path(),
None,
None,
None,
&empty,
&empty_names,
&[],
None,
false,
)
.unwrap();
assert_ne!(
key_with_staged_nonshell, key_empty_nonshell,
"non-shell-mode BaseKey must reflect staged contribution",
);
let stub_busybox: &[u8] = b"#!/bin/sh\n";
let (_, key_with_staged_shell) = assemble_extras_and_key(
payload.as_path(),
None,
None,
None,
&staged,
&names,
&[],
Some(stub_busybox),
false,
)
.unwrap();
let (_, key_empty_shell) = assemble_extras_and_key(
payload.as_path(),
None,
None,
None,
&empty,
&empty_names,
&[],
Some(stub_busybox),
false,
)
.unwrap();
assert_ne!(
key_with_staged_shell, key_empty_shell,
"shell-mode BaseKey must reflect staged contribution",
);
assert_ne!(
key_with_staged_nonshell, key_with_staged_shell,
"shell-mode and non-shell-mode keys for same staged set \
must differ — confirms each arm calls its respective \
BaseKey constructor",
);
}
#[test]
fn try_cow_overlay_maps_segment_and_preserves_adjacent_region() {
use vm_memory::{Bytes, GuestAddress};
let page = host_page_size() as usize;
let region_a_size = page * 4;
let region_b_size = page * 4;
let region_a_start: u64 = 0;
let region_b_start: u64 = (region_a_size as u64) + (1 << 20); let mem = GuestMemoryMmap::<()>::from_ranges(&[
(GuestAddress(region_a_start), region_a_size),
(GuestAddress(region_b_start), region_b_size),
])
.unwrap();
let marker: Vec<u8> = (0..region_b_size).map(|i| (i & 0xff) as u8).collect();
mem.write_slice(&marker, GuestAddress(region_b_start))
.unwrap();
let hash = 0xC0FF_EE00_DEAD_F00Du64;
let _ = rustix::shm::unlink(initramfs::shm_lz4_segment_name(hash).as_str());
let mut segment = initramfs::LZ4_LEGACY_MAGIC.to_vec();
segment.extend((segment.len()..page).map(|i| (i & 0xff) as u8));
assert_eq!(segment.len(), page, "segment sized to one host page");
initramfs::shm_store_lz4(hash, &segment).unwrap();
let key = BaseKey(hash);
let guard = KtstrVm::try_cow_overlay(&mem, &key, segment.len(), region_a_start);
assert!(
guard.is_some(),
"overlay of a valid, in-bounds, page-aligned segment must succeed",
);
let mut a_readback = vec![0u8; segment.len()];
mem.read_slice(&mut a_readback, GuestAddress(region_a_start))
.unwrap();
assert_eq!(
a_readback, segment,
"region A must reflect the COW-mapped segment bytes",
);
let mut b_readback = vec![0u8; region_b_size];
mem.read_slice(&mut b_readback, GuestAddress(region_b_start))
.unwrap();
assert_eq!(
b_readback, marker,
"adjacent region B must be untouched by the overlay",
);
drop(mem);
drop(guard);
let _ = rustix::shm::unlink(initramfs::shm_lz4_segment_name(hash).as_str());
}
#[test]
fn try_cow_overlay_rejects_oversized_request_and_preserves_region() {
use vm_memory::{Bytes, GuestAddress};
let page = host_page_size() as usize;
let region_a_size = page * 2;
let region_b_size = page * 2;
let region_a_start: u64 = 0;
let region_b_start: u64 = (region_a_size as u64) + (1 << 20); let mem = GuestMemoryMmap::<()>::from_ranges(&[
(GuestAddress(region_a_start), region_a_size),
(GuestAddress(region_b_start), region_b_size),
])
.unwrap();
let marker: Vec<u8> = (0..region_b_size).map(|i| (i & 0xff) as u8).collect();
mem.write_slice(&marker, GuestAddress(region_b_start))
.unwrap();
let hash = 0xBADC_0DE0_0BAD_F00Du64;
let _ = rustix::shm::unlink(initramfs::shm_lz4_segment_name(hash).as_str());
let oversized_len = region_a_size + page;
let mut segment = initramfs::LZ4_LEGACY_MAGIC.to_vec();
segment.extend((segment.len()..oversized_len).map(|i| (i & 0xff) as u8));
assert_eq!(segment.len(), oversized_len);
initramfs::shm_store_lz4(hash, &segment).unwrap();
let key = BaseKey(hash);
let guard = KtstrVm::try_cow_overlay(&mem, &key, segment.len(), region_a_start);
assert!(
guard.is_none(),
"an overlay whose rounded length overruns region A must be rejected",
);
let mut b_readback = vec![0u8; region_b_size];
mem.read_slice(&mut b_readback, GuestAddress(region_b_start))
.unwrap();
assert_eq!(
b_readback, marker,
"region B must survive a rejected overlay",
);
drop(mem);
let _ = rustix::shm::unlink(initramfs::shm_lz4_segment_name(hash).as_str());
}
#[test]
fn try_cow_overlay_rejects_stale_non_lz4_magic_segment() {
use vm_memory::{Bytes, GuestAddress};
let page = host_page_size() as usize;
let region_a_size = page * 4;
let region_b_size = page * 4;
let region_a_start: u64 = 0;
let region_b_start: u64 = (region_a_size as u64) + (1 << 20); let mem = GuestMemoryMmap::<()>::from_ranges(&[
(GuestAddress(region_a_start), region_a_size),
(GuestAddress(region_b_start), region_b_size),
])
.unwrap();
let marker: Vec<u8> = (0..region_b_size).map(|i| (i & 0xff) as u8).collect();
mem.write_slice(&marker, GuestAddress(region_b_start))
.unwrap();
let hash = 0x5741_4C45_F00D_BEEFu64;
let _ = rustix::shm::unlink(initramfs::shm_lz4_segment_name(hash).as_str());
let segment: Vec<u8> = vec![0xABu8; page];
assert_ne!(
segment[..4],
initramfs::LZ4_LEGACY_MAGIC,
"fixture header must NOT be the LZ4 legacy magic",
);
initramfs::shm_store_lz4(hash, &segment).unwrap();
let key = BaseKey(hash);
let guard = KtstrVm::try_cow_overlay(&mem, &key, segment.len(), region_a_start);
assert!(
guard.is_none(),
"a segment without the LZ4 legacy magic must be rejected",
);
let mut b_readback = vec![0u8; region_b_size];
mem.read_slice(&mut b_readback, GuestAddress(region_b_start))
.unwrap();
assert_eq!(
b_readback, marker,
"region B must survive a magic-rejected overlay",
);
drop(mem);
let _ = rustix::shm::unlink(initramfs::shm_lz4_segment_name(hash).as_str());
}
#[test]
fn try_cow_overlay_rejects_unaligned_load_addr() {
use vm_memory::{Bytes, GuestAddress};
let page = host_page_size() as usize;
let region_a_size = page * 4;
let region_b_size = page * 4;
let region_a_start: u64 = 0;
let region_b_start: u64 = (region_a_size as u64) + (1 << 20); let mem = GuestMemoryMmap::<()>::from_ranges(&[
(GuestAddress(region_a_start), region_a_size),
(GuestAddress(region_b_start), region_b_size),
])
.unwrap();
let marker: Vec<u8> = (0..region_b_size).map(|i| (i & 0xff) as u8).collect();
mem.write_slice(&marker, GuestAddress(region_b_start))
.unwrap();
let hash = 0x0FF5_E700_A11A_BEEFu64;
let _ = rustix::shm::unlink(initramfs::shm_lz4_segment_name(hash).as_str());
let mut segment = initramfs::LZ4_LEGACY_MAGIC.to_vec();
segment.extend((segment.len()..page).map(|i| (i & 0xff) as u8));
assert_eq!(segment.len(), page, "segment sized to one host page");
initramfs::shm_store_lz4(hash, &segment).unwrap();
let unaligned_addr: u64 = 1;
let key = BaseKey(hash);
let guard = KtstrVm::try_cow_overlay(&mem, &key, segment.len(), unaligned_addr);
assert!(
guard.is_none(),
"a non-host-page-aligned load_addr must be rejected",
);
let mut b_readback = vec![0u8; region_b_size];
mem.read_slice(&mut b_readback, GuestAddress(region_b_start))
.unwrap();
assert_eq!(
b_readback, marker,
"region B must survive an alignment-rejected overlay",
);
drop(mem);
let _ = rustix::shm::unlink(initramfs::shm_lz4_segment_name(hash).as_str());
}
#[cfg(target_arch = "aarch64")]
#[test]
fn aarch64_initrd_addr_returns_host_page_aligned_address() {
use crate::vmm::{aarch64::fdt::pvtime_base, kvm::DRAM_START};
let page = host_page_size();
for &(mem, cpus) in &[(512u32, 2u32), (512, 8), (2048, 256), (4096, 512)] {
let pvt = pvtime_base(mem, cpus);
let size = pvt - DRAM_START - 12345;
let load = aarch64_initrd_addr(mem, cpus, size)
.expect("non-oversized initrd must produce a load address");
assert_eq!(
load & (page - 1),
0,
"initrd load addr {load:#x} not host-page-aligned \
(mem={mem} cpus={cpus} size={size:#x} page={page:#x})",
);
let pre_mask_top = pvt - size;
assert_ne!(
pre_mask_top & (page - 1),
0,
"fixture must present a non-page-aligned pre-mask top so the \
mask is exercised (mem={mem} cpus={cpus})",
);
assert!(
load < pre_mask_top,
"masked load {load:#x} must round DOWN from unaligned top \
{pre_mask_top:#x} (mem={mem} cpus={cpus})",
);
}
}
#[cfg(target_arch = "aarch64")]
#[test]
fn aarch64_initrd_addr_exact_value_for_aligned_fit() {
use crate::vmm::aarch64::fdt::pvtime_base;
let page = host_page_size();
for &(mem, cpus, size) in &[(512u32, 2u32, 1u64 << 20), (2048, 256, 7_000_000)] {
let expected = (pvtime_base(mem, cpus) - size) & !(page - 1);
assert_eq!(
aarch64_initrd_addr(mem, cpus, size).unwrap(),
expected,
"exact load addr drift (mem={mem} cpus={cpus} size={size:#x})",
);
}
}
#[test]
fn base_guest_cmdline_splices_arch_tail_and_pins_common_flags() {
let s = base_guest_cmdline("KFENCE_TAIL_MARKER");
assert!(
s.contains("sysctl.vm.overcommit_memory=1"),
"missing overcommit_memory=1 (the OOM-prevention flag); got {s:?}",
);
assert!(
s.contains("sysctl.kernel.sched_schedstats=1"),
"missing sched_schedstats=1; got {s:?}",
);
assert!(s.contains("delayacct"), "missing delayacct; got {s:?}");
assert!(
s.contains("KFENCE_TAIL_MARKER"),
"arch_extra tail not spliced in; got {s:?}",
);
assert!(
s.starts_with("console=ttyS0"),
"cmdline must open with console=ttyS0; got {s:?}",
);
assert!(
s.ends_with("KTSTR_GUEST=1"),
"cmdline must end with the KTSTR_GUEST=1 trailer; got {s:?}",
);
}
#[test]
fn numa_balancing_token_uses_kernel_accepted_spellings() {
use crate::vmm::topology::{NumaNode, Topology};
let uniform = Topology::new(1, 1, 2, 1);
assert!(!uniform.has_memory_only_nodes());
assert_eq!(
numa_balancing_cmdline_token(&uniform),
" numa_balancing=disable",
"disable token must be the kernel-accepted 'disable' string, not '0'",
);
static NODES: [NumaNode; 3] = [
NumaNode::new(2, 512),
NumaNode::new(2, 512),
NumaNode::new(0, 1024),
];
let cxl = Topology::with_nodes(4, 1, &NODES);
assert!(cxl.has_memory_only_nodes());
assert_eq!(numa_balancing_cmdline_token(&cxl), " numa_balancing=enable");
}