use std::io::Write;
use std::sync::{Arc, Mutex};
use nixvm::abi::Arch;
use nixvm::fs::MountTable;
use nixvm::kernel::Kernel;
use nixvm::loader::{ProcessSpec, load_static};
use nixvm::vcpu::Backend;
use nixvm::vcpu::GuestMemory;
use nixvm::vcpu::interp::InterpBackend;
use nixvm::vcpu::mem::PAGE_SIZE;
const EHDR_LEN: usize = 64;
const PHDR_LEN: usize = 56;
const EM_AARCH64: u16 = 183;
const BODY_OFF: u64 = (EHDR_LEN + PHDR_LEN) as u64;
const NR_READ: u32 = 63;
const NR_WRITE: u32 = 64;
const NR_PIPE2: u32 = 59;
const NR_CLONE: u32 = 220;
const NR_EXIT: u32 = 93;
const NR_WAIT4: u32 = 260;
const NR_MMAP: u32 = 222;
fn movz(rd: u32, imm16: u32) -> u32 {
0xD280_0000 | (imm16 << 5) | rd
}
fn movk16(rd: u32, imm16: u32) -> u32 {
0xF2A0_0000 | (imm16 << 5) | rd
}
fn svc0() -> u32 {
0xD400_0001
}
fn mov_imm32(rd: u32, val: u32) -> Vec<u32> {
let lo = val & 0xffff;
let hi = (val >> 16) & 0xffff;
let mut words = vec![movz(rd, lo)];
if hi != 0 {
words.push(movk16(rd, hi));
}
words
}
fn mov_addr(rd: u32, val: u32) -> [u32; 2] {
[movz(rd, val & 0xffff), movk16(rd, (val >> 16) & 0xffff)]
}
fn strb(rt: u32, rn: u32, imm12: u32) -> u32 {
0x3900_0000 | (imm12 << 10) | (rn << 5) | rt
}
fn ldrb(rt: u32, rn: u32, imm12: u32) -> u32 {
0x3940_0000 | (imm12 << 10) | (rn << 5) | rt
}
fn ldr_w(rt: u32, rn: u32, imm12: u32) -> u32 {
0xB940_0000 | (imm12 << 10) | (rn << 5) | rt
}
fn mov_reg(rd: u32, rm: u32) -> u32 {
0xAA00_03E0 | (rm << 16) | rd
}
fn cbz(rt: u32, byte_offset: i32) -> u32 {
let imm19 = ((byte_offset / 4) as u32) & 0x7_ffff;
0xB400_0000 | (imm19 << 5) | rt
}
#[derive(Clone)]
struct SharedBuf(Arc<Mutex<Vec<u8>>>);
impl Write for SharedBuf {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
fn build_elf(vaddr: u64, body: &[u8]) -> Vec<u8> {
let mut f = vec![0u8; EHDR_LEN + PHDR_LEN];
f[0..4].copy_from_slice(&[0x7f, b'E', b'L', b'F']);
f[4] = 2; f[5] = 1; f[6] = 1;
f[16..18].copy_from_slice(&2u16.to_le_bytes()); f[18..20].copy_from_slice(&EM_AARCH64.to_le_bytes());
f[20..24].copy_from_slice(&1u32.to_le_bytes());
f[24..32].copy_from_slice(&(vaddr + BODY_OFF).to_le_bytes()); f[32..40].copy_from_slice(&(EHDR_LEN as u64).to_le_bytes()); f[52..54].copy_from_slice(&(EHDR_LEN as u16).to_le_bytes());
f[54..56].copy_from_slice(&(PHDR_LEN as u16).to_le_bytes());
f[56..58].copy_from_slice(&1u16.to_le_bytes());
let total = BODY_OFF + body.len() as u64;
let p = EHDR_LEN;
f[p..p + 4].copy_from_slice(&1u32.to_le_bytes()); f[p + 4..p + 8].copy_from_slice(&7u32.to_le_bytes()); f[p + 16..p + 24].copy_from_slice(&vaddr.to_le_bytes()); f[p + 24..p + 32].copy_from_slice(&vaddr.to_le_bytes());
f[p + 32..p + 40].copy_from_slice(&total.to_le_bytes()); f[p + 40..p + 48].copy_from_slice(&total.to_le_bytes()); f[p + 48..p + 56].copy_from_slice(&PAGE_SIZE.to_le_bytes());
f.extend_from_slice(body);
f
}
fn words_to_bytes(words: &[u32]) -> Vec<u8> {
let mut bytes = Vec::with_capacity(words.len() * 4);
for w in words {
bytes.extend_from_slice(&w.to_le_bytes());
}
bytes
}
fn run_program(vaddr: u64, body: &[u8]) -> (i32, Vec<u8>, Kernel) {
let elf = build_elf(vaddr, body);
let mut mem = GuestMemory::new(vaddr, 256 * PAGE_SIZE);
let spec = ProcessSpec {
argv: vec!["prog".into()],
envp: vec![],
};
let img = load_static(&mut mem, &elf, &spec).unwrap();
let backend = InterpBackend::new(Arch::Aarch64).unwrap();
let vcpu = backend.new_vcpu(img.entry, img.stack_pointer).unwrap();
let captured = Arc::new(Mutex::new(Vec::new()));
let mut kernel = Kernel::new(Arch::Aarch64, MountTable::new());
kernel.set_stdout(Box::new(SharedBuf(captured.clone())));
let exit_code = kernel.run(vcpu, mem).unwrap();
let out = captured.lock().unwrap().clone();
(exit_code, out, kernel)
}
#[test]
fn fork_propagates_child_exit_code() {
const CHILD_EXIT_CODE: u32 = 55;
fn build(wstatus_addr: u32) -> Vec<u32> {
let mut code = Vec::new();
code.extend(mov_imm32(0, 0)); code.extend(mov_imm32(1, 0)); code.extend(mov_imm32(8, NR_CLONE));
code.push(svc0());
let mut parent = Vec::new();
parent.extend(mov_addr(1, wstatus_addr)); parent.extend(mov_imm32(2, 0)); parent.extend(mov_imm32(8, NR_WAIT4));
parent.push(svc0());
parent.push(ldrb(0, 1, 1)); parent.extend(mov_imm32(8, NR_EXIT));
parent.push(svc0());
let mut child = Vec::new();
child.extend(mov_imm32(0, CHILD_EXIT_CODE));
child.extend(mov_imm32(8, NR_EXIT));
child.push(svc0());
let cbz_off_bytes = ((1 + parent.len()) * 4) as i32;
code.push(cbz(0, cbz_off_bytes));
code.extend(parent);
code.extend(child);
code
}
let vaddr = 0x1_0000u64;
let code_words = build(0).len() as u64;
let wstatus_addr = u32::try_from(vaddr + BODY_OFF + code_words * 4).unwrap();
let instrs = build(wstatus_addr);
assert_eq!(
instrs.len() as u64,
code_words,
"two-pass build must be length-stable"
);
let mut body = words_to_bytes(&instrs);
body.extend_from_slice(&[0, 0, 0, 0]);
let (exit_code, out, kernel) = run_program(vaddr, &body);
assert_eq!(
exit_code, CHILD_EXIT_CODE as i32,
"parent's exit code must be the child's, via wait4"
);
assert!(out.is_empty());
assert!(
kernel.unsupported().is_empty(),
"{:?}",
kernel.unsupported()
);
}
#[test]
fn pipe_write_read_roundtrips_to_stdout() {
const MSG: &[u8] = b"hi!\n";
const MSG_LEN: u32 = MSG.len() as u32;
fn build(fds_addr: u32, msg_addr: u32, rbuf_addr: u32) -> Vec<u32> {
let mut code = Vec::new();
code.extend(mov_addr(9, fds_addr));
code.push(mov_reg(0, 9));
code.extend(mov_imm32(1, 0)); code.extend(mov_imm32(8, NR_PIPE2));
code.push(svc0());
code.push(ldr_w(19, 9, 0)); code.push(ldr_w(20, 9, 1));
code.push(mov_reg(0, 20));
code.extend(mov_addr(1, msg_addr));
code.extend(mov_imm32(2, MSG_LEN));
code.extend(mov_imm32(8, NR_WRITE));
code.push(svc0());
code.push(mov_reg(0, 19));
code.extend(mov_addr(1, rbuf_addr));
code.extend(mov_imm32(2, MSG_LEN));
code.extend(mov_imm32(8, NR_READ));
code.push(svc0());
code.extend(mov_imm32(0, 1));
code.extend(mov_addr(1, rbuf_addr));
code.extend(mov_imm32(2, MSG_LEN));
code.extend(mov_imm32(8, NR_WRITE));
code.push(svc0());
code.extend(mov_imm32(0, 0));
code.extend(mov_imm32(8, NR_EXIT));
code.push(svc0());
code
}
let vaddr = 0x1_0000u64;
let code_words = build(0, 0, 0).len() as u64;
let fds_addr = u32::try_from(vaddr + BODY_OFF + code_words * 4).unwrap();
let msg_addr = fds_addr + 8; let rbuf_addr = msg_addr + MSG_LEN;
let instrs = build(fds_addr, msg_addr, rbuf_addr);
assert_eq!(instrs.len() as u64, code_words);
let mut body = words_to_bytes(&instrs);
body.extend_from_slice(&[0u8; 8]); body.extend_from_slice(MSG);
body.extend(std::iter::repeat_n(0u8, MSG_LEN as usize));
let (exit_code, out, kernel) = run_program(vaddr, &body);
assert_eq!(exit_code, 0);
assert_eq!(
&out, MSG,
"bytes written into the pipe should read back unchanged and echo to stdout"
);
assert!(
kernel.unsupported().is_empty(),
"{:?}",
kernel.unsupported()
);
}
#[test]
fn fork_pipe_parent_reads_child_writes_and_exits() {
const MSG: &[u8] = b"ok\n";
const MSG_LEN: u32 = MSG.len() as u32;
fn build(fds_addr: u32, msg_addr: u32, rbuf_addr: u32) -> Vec<u32> {
let mut code = Vec::new();
code.extend(mov_addr(9, fds_addr));
code.push(mov_reg(0, 9));
code.extend(mov_imm32(1, 0));
code.extend(mov_imm32(8, NR_PIPE2));
code.push(svc0());
code.push(ldr_w(19, 9, 0)); code.push(ldr_w(20, 9, 1));
code.extend(mov_imm32(0, 0));
code.extend(mov_imm32(1, 0));
code.extend(mov_imm32(8, NR_CLONE));
code.push(svc0());
let mut parent = Vec::new();
parent.push(mov_reg(21, 0)); parent.push(mov_reg(0, 19)); parent.extend(mov_addr(1, rbuf_addr));
parent.extend(mov_imm32(2, MSG_LEN));
parent.extend(mov_imm32(8, NR_READ));
parent.push(svc0());
parent.extend(mov_imm32(0, 1)); parent.extend(mov_addr(1, rbuf_addr));
parent.extend(mov_imm32(2, MSG_LEN));
parent.extend(mov_imm32(8, NR_WRITE));
parent.push(svc0());
parent.push(mov_reg(0, 21)); parent.extend(mov_imm32(1, 0));
parent.extend(mov_imm32(2, 0));
parent.extend(mov_imm32(8, NR_WAIT4));
parent.push(svc0());
parent.extend(mov_imm32(0, 0));
parent.extend(mov_imm32(8, NR_EXIT));
parent.push(svc0());
let mut child = Vec::new();
child.push(mov_reg(0, 20)); child.extend(mov_addr(1, msg_addr));
child.extend(mov_imm32(2, MSG_LEN));
child.extend(mov_imm32(8, NR_WRITE));
child.push(svc0());
child.extend(mov_imm32(0, 0));
child.extend(mov_imm32(8, NR_EXIT));
child.push(svc0());
let cbz_off_bytes = ((1 + parent.len()) * 4) as i32;
code.push(cbz(0, cbz_off_bytes));
code.extend(parent);
code.extend(child);
code
}
let vaddr = 0x1_0000u64;
let code_words = build(0, 0, 0).len() as u64;
let fds_addr = u32::try_from(vaddr + BODY_OFF + code_words * 4).unwrap();
let msg_addr = fds_addr + 8;
let rbuf_addr = msg_addr + MSG_LEN;
let instrs = build(fds_addr, msg_addr, rbuf_addr);
assert_eq!(instrs.len() as u64, code_words);
let mut body = words_to_bytes(&instrs);
body.extend_from_slice(&[0u8; 8]);
body.extend_from_slice(MSG);
body.extend(std::iter::repeat_n(0u8, MSG_LEN as usize));
let (exit_code, out, kernel) = run_program(vaddr, &body);
assert_eq!(exit_code, 0);
assert_eq!(
&out, MSG,
"the parent should read exactly what the child wrote into the pipe"
);
assert!(
kernel.unsupported().is_empty(),
"{:?}",
kernel.unsupported()
);
}
#[test]
fn mmap_anonymous_store_load_roundtrip() {
const MAP_ANONYMOUS: u32 = 0x20;
const PROT_RW: u32 = 0x3; const VALUE: u32 = 0x2A; let vaddr = 0x1_0000u64;
let mut code = Vec::new();
code.extend(mov_imm32(0, 0)); code.extend(mov_imm32(1, PAGE_SIZE as u32)); code.extend(mov_imm32(2, PROT_RW));
code.extend(mov_imm32(3, MAP_ANONYMOUS));
code.extend(mov_imm32(4, 0)); code.extend(mov_imm32(5, 0)); code.extend(mov_imm32(8, NR_MMAP));
code.push(svc0()); code.push(mov_reg(9, 0)); code.push(movz(2, VALUE));
code.push(strb(2, 9, 0)); code.push(ldrb(1, 9, 0)); code.push(mov_reg(0, 1));
code.extend(mov_imm32(8, NR_EXIT));
code.push(svc0());
let body = words_to_bytes(&code);
let elf = build_elf(vaddr, &body);
let mut mem = GuestMemory::new(vaddr, 256 * PAGE_SIZE);
let spec = ProcessSpec {
argv: vec!["prog".into()],
envp: vec![],
};
let img = load_static(&mut mem, &elf, &spec).unwrap();
let backend = InterpBackend::new(Arch::Aarch64).unwrap();
let vcpu = backend.new_vcpu(img.entry, img.stack_pointer).unwrap();
let captured = Arc::new(Mutex::new(Vec::new()));
let mut kernel = Kernel::new(Arch::Aarch64, MountTable::new());
kernel.set_stdout(Box::new(SharedBuf(captured.clone())));
kernel.set_mmap_area(img.stack_bottom, img.program_break);
let exit_code = kernel.run(vcpu, mem).unwrap();
assert_eq!(
exit_code, VALUE as i32,
"byte stored into the mmap'd page should round-trip through a load"
);
assert!(captured.lock().unwrap().is_empty());
assert!(
kernel.unsupported().is_empty(),
"{:?}",
kernel.unsupported()
);
}