diff --git a/.gitignore b/.gitignore index 6035f8f..6d5e14b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /target .DS_Store -*.img \ No newline at end of file +*.img diff --git a/Cargo.lock b/Cargo.lock index 9ca30b3..b39c030 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -291,7 +291,13 @@ version = "0.1.0" name = "gatekeeper" version = "0.1.0" dependencies = [ + "driver-block-virtio", + "driver-rng-virtio", "keystore", + "libkos", + "port-block-device", + "port-entropy", + "types", "zeroize", ] diff --git a/Makefile b/Makefile index 47f1b9e..1104e09 100644 --- a/Makefile +++ b/Makefile @@ -55,11 +55,17 @@ build: -p init -p echo -p sleep -p snake \ -p display-qemu -p input-qemu -p storage-qemu \ -p log-server -p permission-manager -p session-server -p smptest -p spawntest -p hello \ - -p terminal-server -p shell -p cat -p ls -p counter + -p terminal-server -p shell -p cat -p ls -p counter -p gatekeeper cargo build --target $(RISCV_TARGET) --release -p adapter-riscv +# The keystore holds the owner's sealed keys: made once, never rebuilt. +KEYS_IMG ?= keys.img +QEMU_KEYS = -drive file=$(KEYS_IMG),if=none,format=raw,id=ks -device virtio-blk-device,drive=ks \ + -device virtio-rng-device + image: build scripts/make-disk-image.sh + [ -f $(KEYS_IMG) ] || truncate -s 64K $(KEYS_IMG) # Boots a test image (normal image + bin/smptest) on $(SMP) harts; init runs # the SMP self-test and halts. Passes only on "SMPTEST PASS". @@ -75,13 +81,13 @@ run-disk: image qemu-system-riscv64 -machine virt -smp $(SMP) -nographic -bios default \ -kernel $(KERNEL_BIN) \ -drive file=boot.img,if=none,format=raw,id=hd0 \ - -device virtio-blk-device,drive=hd0 + -device virtio-blk-device,drive=hd0 $(QEMU_KEYS) run-gui: image qemu-system-riscv64 -machine virt -smp $(SMP) -bios default \ -kernel $(KERNEL_BIN) \ -drive file=boot.img,if=none,format=raw,id=hd0 \ - -device virtio-blk-device,drive=hd0 \ + -device virtio-blk-device,drive=hd0 $(QEMU_KEYS) \ -device virtio-gpu-device \ -device virtio-keyboard-device \ -serial stdio diff --git a/crates/apps/spawntest/src/main.rs b/crates/apps/spawntest/src/main.rs index 7eed431..8cb4476 100644 --- a/crates/apps/spawntest/src/main.rs +++ b/crates/apps/spawntest/src/main.rs @@ -57,6 +57,8 @@ fn run_hello(fs: &mut Storage, died: CapSlot, grant: Option) -> Result< /// A file written through storage, which the host then checks with e2fsck. fn write_a_file(fs: &mut Storage) -> Result<(), &'static str> { + // Left by an earlier boot of the same disk. + let _ = fs.delete(b"selftest.txt"); let h = fs.create(b"selftest.txt").map_err(|_| "cannot create a file")?; if fs.write(h, NOTE) != Ok(NOTE.len()) { return Err("short write"); diff --git a/crates/drivers/block/virtio/src/lib.rs b/crates/drivers/block/virtio/src/lib.rs index a7578fb..f6eda75 100644 --- a/crates/drivers/block/virtio/src/lib.rs +++ b/crates/drivers/block/virtio/src/lib.rs @@ -8,9 +8,11 @@ use port_block_device::{BlockDevice, SectorId}; use types::address::{ByteOffset, PhysicalAddress, VirtualAddress}; use types::bounded::PageCount; -const MMIO_VA: VirtualAddress = VirtualAddress::from_raw(0x20000); -const VQUEUE_VA: VirtualAddress = VirtualAddress::from_raw(0x21000); -const REQ_HDR_VA: VirtualAddress = VirtualAddress::from_raw(0x23000); +/// Drivers map their devices in the device window (0x01D0_0000 up, 64 KiB +/// each), clear of program images and heaps. +const MMIO_VA: VirtualAddress = VirtualAddress::from_raw(0x01D0_0000); +const VQUEUE_VA: VirtualAddress = VirtualAddress::from_raw(0x01D0_1000); +const REQ_HDR_VA: VirtualAddress = VirtualAddress::from_raw(0x01D0_3000); const SECTOR_SIZE: usize = 512; pub struct VirtioBlockDevice { diff --git a/crates/drivers/framebuffer/virtio/src/lib.rs b/crates/drivers/framebuffer/virtio/src/lib.rs index 4bd50ae..9113a8e 100644 --- a/crates/drivers/framebuffer/virtio/src/lib.rs +++ b/crates/drivers/framebuffer/virtio/src/lib.rs @@ -10,9 +10,11 @@ use types::address::{ByteOffset, PhysicalAddress, VirtualAddress}; use types::bounded::PageCount; use types::display::{Color, Dimension, ScreenCoord}; -const GPU_MMIO_VA: VirtualAddress = VirtualAddress::from_raw(0x20000); -const VQUEUE_VA: VirtualAddress = VirtualAddress::from_raw(0x21000); -const CMDBUF_VA: VirtualAddress = VirtualAddress::from_raw(0x23000); +/// Drivers map their devices in the device window (0x01D0_0000 up, 64 KiB +/// each), clear of program images and heaps. +const GPU_MMIO_VA: VirtualAddress = VirtualAddress::from_raw(0x01D2_0000); +const VQUEUE_VA: VirtualAddress = VirtualAddress::from_raw(0x01D2_1000); +const CMDBUF_VA: VirtualAddress = VirtualAddress::from_raw(0x01D2_3000); const FB_VA: VirtualAddress = VirtualAddress::from_raw(0x100000); const FB_BPP: usize = 4; /// Pixels converted and stored per run. diff --git a/crates/drivers/input/uart/src/lib.rs b/crates/drivers/input/uart/src/lib.rs index a2081c8..3c57e62 100644 --- a/crates/drivers/input/uart/src/lib.rs +++ b/crates/drivers/input/uart/src/lib.rs @@ -5,7 +5,9 @@ use libkos::Window; use port_input_device::{InputDevice, InputEvent, Key}; use types::address::{ByteOffset, VirtualAddress}; -const UART_VA: VirtualAddress = VirtualAddress::from_raw(0x20000); +/// Drivers map their devices in the device window (0x01D0_0000 up, 64 KiB +/// each), clear of program images and heaps. +const UART_VA: VirtualAddress = VirtualAddress::from_raw(0x01D4_0000); const UART_RBR: ByteOffset = ByteOffset::new(0); const UART_IER_OFFSET: ByteOffset = ByteOffset::new(1); const UART_LSR_OFFSET: ByteOffset = ByteOffset::new(5); diff --git a/crates/drivers/input/virtio/src/lib.rs b/crates/drivers/input/virtio/src/lib.rs index 89844c8..758d454 100644 --- a/crates/drivers/input/virtio/src/lib.rs +++ b/crates/drivers/input/virtio/src/lib.rs @@ -10,9 +10,11 @@ use port_input_device::{InputDevice, InputEvent}; use types::address::{ByteOffset, PhysicalAddress, VirtualAddress}; use types::bounded::PageCount; -const INPUT_MMIO_VA: VirtualAddress = VirtualAddress::from_raw(0x25000); -const VQUEUE_VA: VirtualAddress = VirtualAddress::from_raw(0x26000); -const EVBUF_VA: VirtualAddress = VirtualAddress::from_raw(0x28000); +/// Drivers map their devices in the device window (0x01D0_0000 up, 64 KiB +/// each), clear of program images and heaps. +const INPUT_MMIO_VA: VirtualAddress = VirtualAddress::from_raw(0x01D1_0000); +const VQUEUE_VA: VirtualAddress = VirtualAddress::from_raw(0x01D1_1000); +const EVBUF_VA: VirtualAddress = VirtualAddress::from_raw(0x01D1_3000); const QUEUE_SIZE: usize = 32; const EVENT_SIZE: usize = 8; diff --git a/crates/drivers/rng/virtio/src/lib.rs b/crates/drivers/rng/virtio/src/lib.rs index 791da2f..d740c5a 100644 --- a/crates/drivers/rng/virtio/src/lib.rs +++ b/crates/drivers/rng/virtio/src/lib.rs @@ -10,9 +10,11 @@ use port_entropy::Entropy; use types::address::{ByteOffset, VirtualAddress}; use types::bounded::PageCount; -const MMIO_VA: VirtualAddress = VirtualAddress::from_raw(0x2A000); -const VQUEUE_VA: VirtualAddress = VirtualAddress::from_raw(0x2B000); -const BUFFER_VA: VirtualAddress = VirtualAddress::from_raw(0x2D000); +/// Drivers map their devices in the device window (0x01D0_0000 up, 64 KiB +/// each), clear of program images and heaps. +const MMIO_VA: VirtualAddress = VirtualAddress::from_raw(0x01D3_0000); +const VQUEUE_VA: VirtualAddress = VirtualAddress::from_raw(0x01D3_1000); +const BUFFER_VA: VirtualAddress = VirtualAddress::from_raw(0x01D3_3000); /// Asked for at most this much at a time. const CHUNK: usize = 256; diff --git a/crates/init/src/devices.rs b/crates/init/src/devices.rs index fdedd28..39d4fa5 100644 --- a/crates/init/src/devices.rs +++ b/crates/init/src/devices.rs @@ -1,8 +1,9 @@ //! Finds the board's devices (`types::bootinfo`): disk, GPU, and keyboard //! (or the UART when there is none). +use hw_virtio::block::CONFIG_CAPACITY; use hw_virtio::mmio::{ - DEVICE_ID_BLOCK, DEVICE_ID_GPU, DEVICE_ID_INPUT, MAGIC, REG_DEVICE_ID, REG_MAGIC, + DEVICE_ID_BLOCK, DEVICE_ID_GPU, DEVICE_ID_INPUT, DEVICE_ID_RNG, MAGIC, REG_DEVICE_ID, REG_MAGIC, }; use libkos::device::{self, Device}; use libkos::syscall; @@ -19,8 +20,14 @@ pub struct Devices { pub disk: Device, pub gpu: Device, pub input: Device, + /// The gatekeeper's: a small disk of its own for the key slots. + pub keystore: Device, + pub random: Device, } +/// A block device this small (1 MiB) is the keystore, not the disk. +const KEYSTORE_MAX_SECTORS: u64 = 2048; + fn slot(raw: u64) -> Option { if raw == NO_SLOT { return None; @@ -28,21 +35,30 @@ fn slot(raw: u64) -> Option { CapSlot::new(u8::try_from(raw).ok()?) } -/// The virtio device id behind `region`, 0 for an empty slot. Read through -/// a copy of the capability, mapped only while looking and then dropped, -/// which unmaps it. -fn virtio_id(region: CapSlot) -> u32 { +/// The virtio device id behind `region` (0 for an empty slot) and, for a +/// block device, its size in sectors. Read through a copy of the +/// capability, mapped only while looking and then dropped, which unmaps it. +fn virtio_id(region: CapSlot) -> (u32, u64) { let perms = PermissionSet::empty().with(Permission::Read).with(Permission::Write); let Ok(copy) = syscall::mint(region, Badge::NONE, perms) else { - return 0; + return (0, 0); }; let id = match device::map_mmio(copy, PROBE_VA) { // The registers stay mapped until the drop below. Ok(regs) => match regs.read::(REG_MAGIC) == MAGIC { - true => regs.read(REG_DEVICE_ID), - false => 0, + true => { + let id = regs.read(REG_DEVICE_ID); + let size = match id { + DEVICE_ID_BLOCK => { + regs.read::(CONFIG_CAPACITY) as u64 | (regs.read::(CONFIG_CAPACITY + 4) as u64) << 32 + } + _ => 0, + }; + (id, size) + } + false => (0, 0), }, - Err(_) => 0, + Err(_) => (0, 0), }; syscall::drop_cap(copy); id @@ -74,7 +90,13 @@ pub fn console() -> Option { /// Sorts the board's devices out. pub fn discover() -> Devices { let info = boot_info(); - let mut found = Devices { disk: Device::NONE, gpu: Device::NONE, input: Device::NONE }; + let mut found = Devices { + disk: Device::NONE, + gpu: Device::NONE, + input: Device::NONE, + keystore: Device::NONE, + random: Device::NONE, + }; let mut uart = Device::NONE; for d in info.devices() { let Some(region) = slot(d.region_slot) else { @@ -84,9 +106,11 @@ pub fn discover() -> Devices { let place = match d.kind { KIND_UART_16550 => &mut uart, KIND_VIRTIO => match virtio_id(region) { - DEVICE_ID_BLOCK => &mut found.disk, - DEVICE_ID_GPU => &mut found.gpu, - DEVICE_ID_INPUT => &mut found.input, + (DEVICE_ID_BLOCK, size) if size <= KEYSTORE_MAX_SECTORS => &mut found.keystore, + (DEVICE_ID_BLOCK, _) => &mut found.disk, + (DEVICE_ID_GPU, _) => &mut found.gpu, + (DEVICE_ID_INPUT, _) => &mut found.input, + (DEVICE_ID_RNG, _) => &mut found.random, _ => continue, }, _ => continue, diff --git a/crates/init/src/launch.rs b/crates/init/src/launch.rs index 60b7bfe..823a0ae 100644 --- a/crates/init/src/launch.rs +++ b/crates/init/src/launch.rs @@ -110,8 +110,12 @@ impl<'a> Launcher<'a> { if let Some(free) = self.served.iter_mut().find(|s| s.is_none()) { *free = Some((p.name, ep)); } - if let Some(device) = p.device { - note = self.give(device, ep); + for device in p.devices { + note = self.give(*device, ep); + } + if p.unlocks { + unlock(ep)?; + note = ", the owner is in"; } if console.is_some() { hand_over_console(ep); @@ -139,6 +143,8 @@ impl<'a> Launcher<'a> { let (dev, given, none): (BoardDevice, _, _) = match device { Device::Gpu => (self.devices.gpu, ", gpu handed over", ", no gpu"), Device::Input => (self.devices.input, ", input device handed over", ", no input device"), + Device::Keystore => (self.devices.keystore, ", keystore handed over", ", no keystore"), + Device::Random => (self.devices.random, ", randomness handed over", ", no randomness"), }; match libkos::device::give_device(ep, dev).is_ok() && dev.mmio.is_some() { true => given, @@ -147,6 +153,23 @@ impl<'a> Launcher<'a> { } } +/// Waits for the gatekeeper behind `ep` to authenticate the owner, then +/// passes the master key on to storage, which alone keeps it. +fn unlock(ep: CapSlot) -> Result<(), &'static str> { + use types::gatekeeper::{KEY, UNLOCK, reply}; + match syscall::request(ep, [UNLOCK, 0, 0, 0]) { + Ok([reply::OK, ..]) => {} + _ => return Err("the gatekeeper could not unlock"), + } + let mut key = syscall::request(ep, [KEY, 0, 0, 0]).map_err(|_| "the gatekeeper gave no key")?; + let given = [0, 1].iter().all(|&half| { + let words = [types::storage::op::SET_KEY, half, key[half * 2], key[half * 2 + 1]]; + syscall::request(crate::STORAGE_EP, words).is_ok_and(|r| r[0] == types::storage::reply::OK) + }); + key.fill(0); + if given { Ok(()) } else { Err("storage would not take the key") } +} + /// From here on init prints through the log server, like everyone else, /// and holds no console of its own. fn hand_over_console(log: CapSlot) { diff --git a/crates/init/src/lib.rs b/crates/init/src/lib.rs index 4fbf816..2fd98bc 100644 --- a/crates/init/src/lib.rs +++ b/crates/init/src/lib.rs @@ -80,6 +80,11 @@ fn launch_all(launcher: &mut launch::Launcher, programs: &[manifest::Program]) { libkos::print!("init: loading {}... ", p.name); match launcher.launch(p) { Ok(note) => libkos::println!("ok{}", note), + // Without the owner, nothing that touches user data may start. + Err(why) if p.unlocks => { + libkos::println!("STOP ({})", why); + idle(); + } Err(why) => libkos::println!("SKIP ({})", why), } } diff --git a/crates/init/src/manifest.rs b/crates/init/src/manifest.rs index 687776e..32751b9 100644 --- a/crates/init/src/manifest.rs +++ b/crates/init/src/manifest.rs @@ -7,6 +7,8 @@ use types::budget::Limits; pub enum Device { Gpu, Input, + Keystore, + Random, } /// A storage grant: a folder (or the whole disk, `b""`) and whether it may @@ -23,7 +25,11 @@ pub struct Program { pub limits: Limits, /// Serves an endpoint, which it finds in slot 0. pub serves: bool, - pub device: Option, + /// Handed over in this order, after it starts. + pub devices: &'static [Device], + /// Authenticates the owner: init waits for the master key before + /// starting anything that touches user data. + pub unlocks: bool, /// Earlier programs' endpoints it may call, in its next slots; each /// program's copy carries its own badge, so a server can tell them apart. pub uses: &'static [&'static str], @@ -51,7 +57,8 @@ const fn server(name: &'static str, file: &'static [u8], limits: Limits) -> Prog file, limits, serves: true, - device: None, + devices: &[], + unlocks: false, uses: &[], storage: None, console: false, @@ -63,16 +70,26 @@ const fn server(name: &'static str, file: &'static [u8], limits: Limits) -> Prog /// Started on every boot. The log server comes first so the rest can print. pub const SERVERS: &[Program] = &[ Program { console: true, ..server("log", b"bin/log", SMALL) }, - Program { device: Some(Device::Input), ..server("input", b"bin/input", Limits { regions: 4, ..SMALL }) }, + Program { devices: &[Device::Input], ..server("input", b"bin/input", Limits { regions: 4, ..SMALL }) }, // The input server's only client: it passes input on to the app in focus. Program { - device: Some(Device::Gpu), + devices: &[Device::Gpu], uses: &["input"], // The framebuffer alone is ~1000 pages at 1280x800. ..server("display", b"bin/display", Limits { pages: 1280, regions: 4, ..SMALL }) }, // Text on the display, lines from the keyboard (it takes input focus). Program { uses: &["display"], ..server("terminal", b"bin/terminal", SMALL) }, + // Nothing that touches user data starts before the owner is in. It + // holds the screen (through the terminal), its keystore and randomness, + // and exits once it has handed over the key. + Program { + uses: &["terminal"], + devices: &[Device::Keystore, Device::Random], + unlocks: true, + // argon2's 16 MiB, its image and a little more. + ..server("gatekeeper", b"bin/gatekeeper", Limits { pages: 4096 + 160, regions: 6, ..SMALL }) + }, // The only one that equips apps (docs/apps.md): it reaches every app's // files and the owner's decisions, and runs apps through init. Program { diff --git a/crates/kernel/src/tests/abi.rs b/crates/kernel/src/tests/abi.rs index 2561ce4..b1fee40 100644 --- a/crates/kernel/src/tests/abi.rs +++ b/crates/kernel/src/tests/abi.rs @@ -70,7 +70,11 @@ fn out_of_range_arguments_do_not_decode() { decode(SYS_REQUEST_PAGES, &[0, 0, 0, 0, 0, 0]).is_none(), "zero pages" ); - assert!(decode(SYS_REQUEST_PAGES, &[1025, 0, 0, 0, 0, 0]).is_none()); + let too_many = types::bounded::PageCount::MAX as usize + 1; + assert!( + decode(SYS_REQUEST_PAGES, &[too_many, 0, 0, 0, 0, 0]).is_none(), + "more pages than one request may ask for" + ); assert!( decode(SYS_MAP_REGION, &[1, 0x30000, 3, 0, 0, 0]).is_none(), "bad perms" diff --git a/crates/libkos/src/alloc_impl.rs b/crates/libkos/src/alloc_impl.rs index 1e43a5b..ca5c16e 100644 --- a/crates/libkos/src/alloc_impl.rs +++ b/crates/libkos/src/alloc_impl.rs @@ -3,7 +3,6 @@ use core::cell::UnsafeCell; use core::ptr; const PAGE_SIZE: usize = 4096; -const MAX_PAGES: usize = 64; struct FreeBlock { size: usize, @@ -14,10 +13,10 @@ pub struct KosAllocator { inner: UnsafeCell, } +/// How much heap a process gets is its page budget's business (the kernel +/// enforces it), not the allocator's. struct AllocatorInner { free_list: *mut FreeBlock, - pages: [usize; MAX_PAGES], - page_count: usize, } unsafe impl Sync for KosAllocator {} @@ -25,11 +24,7 @@ unsafe impl Sync for KosAllocator {} impl KosAllocator { pub const fn new() -> Self { Self { - inner: UnsafeCell::new(AllocatorInner { - free_list: ptr::null_mut(), - pages: [0; MAX_PAGES], - page_count: 0, - }), + inner: UnsafeCell::new(AllocatorInner { free_list: ptr::null_mut() }), } } @@ -41,19 +36,12 @@ impl KosAllocator { impl AllocatorInner { /// `n` pages in one piece, so an allocation larger than a page fits. fn request_pages(&mut self, n: usize) -> *mut u8 { - if self.page_count + n > MAX_PAGES { - return ptr::null_mut(); - } let Some(count) = u16::try_from(n).ok().and_then(types::bounded::PageCount::new) else { return ptr::null_mut(); }; let Some(va) = crate::syscall::request_pages(count) else { return ptr::null_mut(); }; - if let Some(slot) = self.pages.get_mut(self.page_count) { - *slot = va.raw(); - } - self.page_count += n; va.raw() as *mut u8 } diff --git a/crates/libkos/src/terminal.rs b/crates/libkos/src/terminal.rs index ff4cf62..36f4932 100644 --- a/crates/libkos/src/terminal.rs +++ b/crates/libkos/src/terminal.rs @@ -6,9 +6,18 @@ use types::bounded::CapSlot; /// Reads a typed line into `buf`, newline included if it fits; how much. pub fn read_line(terminal: CapSlot, buf: &mut [u8]) -> usize { + read(terminal, types::terminal::READ, buf) +} + +/// Reads a typed line without showing it (`*` for each character). +pub fn read_secret(terminal: CapSlot, buf: &mut [u8]) -> usize { + read(terminal, types::terminal::READ_SECRET, buf) +} + +fn read(terminal: CapSlot, op: usize, buf: &mut [u8]) -> usize { let mut n = 0; while n < buf.len() { - let Ok(words) = syscall::request(terminal, [types::terminal::READ, 0, 0, 0]) else { + let Ok(words) = syscall::request(terminal, [op, 0, 0, 0]) else { break; }; let Some((bytes, len)) = types::log::text_of(words) else { diff --git a/crates/servers/gatekeeper/Cargo.toml b/crates/servers/gatekeeper/Cargo.toml index 96e5c09..48c3242 100644 --- a/crates/servers/gatekeeper/Cargo.toml +++ b/crates/servers/gatekeeper/Cargo.toml @@ -6,3 +6,14 @@ edition = "2024" [dependencies] keystore = { workspace = true } zeroize = { workspace = true } +types = { workspace = true } +libkos = { workspace = true } +port-block-device = { workspace = true } +port-entropy = { workspace = true } +driver-block-virtio = { workspace = true } +driver-rng-virtio = { workspace = true } + +[[bin]] +name = "gatekeeper" +test = false +bench = false diff --git a/crates/servers/gatekeeper/src/devices.rs b/crates/servers/gatekeeper/src/devices.rs new file mode 100644 index 0000000..89aeb1a --- /dev/null +++ b/crates/servers/gatekeeper/src/devices.rs @@ -0,0 +1,55 @@ +//! The flow's ports on real devices: the keystore disk, virtio-rng, and +//! the terminal. + +use libkos::io::print_to; +use port_block_device::{BlockDevice, SectorId}; +use port_entropy::Entropy; +use types::bounded::CapSlot; + +use crate::flow::{Keystore, MAX_SECRET, Prompt, Secret}; +use keystore::RECORD_SIZE; + +/// The record's two copies are its first two sectors. +pub struct Disk(pub D); + +impl Keystore for Disk { + fn read(&mut self, copy: usize, out: &mut [u8; RECORD_SIZE]) -> bool { + self.0.sector_size() == RECORD_SIZE && self.0.read_sector(SectorId::new(copy as u32), out) + } + + fn write(&mut self, copy: usize, data: &[u8; RECORD_SIZE]) -> bool { + self.0.sector_size() == RECORD_SIZE && self.0.write_sector(SectorId::new(copy as u32), data) + } +} + +pub struct Hardware(pub E); + +impl keystore::Random for Hardware { + fn fill(&mut self, buf: &mut [u8]) -> bool { + self.0.fill(buf) + } +} + +pub struct Terminal(pub CapSlot); + +impl Prompt for Terminal { + fn say(&mut self, text: &str) { + print_to(self.0, format_args!("{}", text)); + } + + fn clear(&mut self) { + libkos::terminal::clear(self.0); + } + + fn secret(&mut self, question: &str, out: &mut Secret) -> bool { + print_to(self.0, format_args!("{}", question)); + let mut line = Secret::new(); + let mut buf = [0u8; MAX_SECRET + 1]; + let n = libkos::terminal::read_secret(self.0, &mut buf); + let typed = buf[..n].strip_suffix(b"\n").unwrap_or(&buf[..n]); + line.set(typed); + buf.fill(0); + out.set(line.as_bytes()); + n > 0 + } +} diff --git a/crates/servers/gatekeeper/src/flow.rs b/crates/servers/gatekeeper/src/flow.rs index 9a64454..87655ae 100644 --- a/crates/servers/gatekeeper/src/flow.rs +++ b/crates/servers/gatekeeper/src/flow.rs @@ -57,6 +57,8 @@ impl Drop for Secret { /// The owner, on the screen and keyboard. pub trait Prompt { fn say(&mut self, text: &str); + /// Blanks the screen. + fn clear(&mut self); /// Shows `question` and reads a passcode without showing it; false if /// nothing could be read. fn secret(&mut self, question: &str, out: &mut Secret) -> bool; @@ -128,6 +130,8 @@ fn choose(prompt: &mut impl Prompt, question: &str, optional: bool, out: &mut Se /// A device never set up (or wiped): the owner chooses a passcode, and /// may choose a duress one. Returns the new master key. fn setup(ks: &mut impl Keystore, rng: &mut impl Random, prompt: &mut impl Prompt, params: KdfParams, memory: &mut [Block]) -> Result { + // After a wipe nothing earlier may show: it must look like a new device. + prompt.clear(); prompt.say("This device has no passcode yet. Choose one; it protects everything on it.\n"); let mut real = Secret::new(); choose(prompt, "New passcode: ", false, &mut real)?; diff --git a/crates/servers/gatekeeper/src/lib.rs b/crates/servers/gatekeeper/src/lib.rs index c3074a5..a1bc2c9 100644 --- a/crates/servers/gatekeeper/src/lib.rs +++ b/crates/servers/gatekeeper/src/lib.rs @@ -7,8 +7,78 @@ extern crate alloc; +pub mod devices; pub mod flow; #[cfg(test)] #[path = "tests/mod.rs"] mod tests; + +use alloc::vec; +use keystore::{Block, KdfParams}; +use libkos::syscall::{self, Received}; +use types::bounded::CapSlot; +use types::gatekeeper::{KEY, UNLOCK, reply}; + +/// Where init's manifest put them: its own endpoint (devices arrive on +/// it, then init's calls), then the terminal. +const EP: CapSlot = CapSlot::const_new(0); +const TERMINAL: CapSlot = CapSlot::const_new(1); + +/// Answers init's next call, if it is `op`, with `words`. +fn answer(op: usize, words: [usize; 4]) -> bool { + loop { + match syscall::receive_msg(EP) { + Ok(Received::Call { badge: 0, words: [w, ..], .. }) if w == op => { + return syscall::reply(words, None).is_ok(); + } + // Anyone else, or anything else: refused. + Ok(Received::Call { .. }) => { + let _ = syscall::reply([reply::FAILED, 0, 0, 0], None); + } + Ok(_) => {} + Err(_) => return false, + } + } +} + +pub fn run() -> ! { + // Init hands over the keystore disk, then the randomness device. + let keystore = libkos::device::receive_device(EP).mmio.and_then(driver_block_virtio::VirtioBlockDevice::new); + let random = libkos::device::receive_device(EP).mmio.and_then(driver_rng_virtio::VirtioRng::new); + let (Some(keystore), Some(random)) = (keystore, random) else { + libkos::println!("gatekeeper: no keystore or no randomness, nothing to unlock with"); + let _ = answer(UNLOCK, [reply::FAILED, 0, 0, 0]); + syscall::exit(); + }; + let mut memory = vec![Block::default(); KdfParams::DEFAULT.memory_kib as usize]; + libkos::println!("gatekeeper: waiting for the owner"); + let key = flow::run( + &mut devices::Disk(keystore), + &mut devices::Hardware(random), + &mut devices::Terminal(TERMINAL), + KdfParams::DEFAULT, + &mut memory, + ); + // The guesses' working memory goes before anything else happens. + for b in memory.iter_mut() { + *b = Block::default(); + } + drop(memory); + match key { + Ok(key) => { + libkos::println!("gatekeeper: unlocked"); + libkos::terminal::clear(TERMINAL); + let word = |i: usize| usize::from_le_bytes(key.0[i * 8..i * 8 + 8].try_into().unwrap_or([0; 8])); + if answer(UNLOCK, [reply::OK, 0, 0, 0]) { + let _ = answer(KEY, [word(0), word(1), word(2), word(3)]); + } + } + Err(why) => { + libkos::println!("gatekeeper: cannot go on: {:?}", why); + let _ = answer(UNLOCK, [reply::FAILED, 0, 0, 0]); + } + } + // Its capabilities go with it. + syscall::exit() +} diff --git a/crates/servers/gatekeeper/src/main.rs b/crates/servers/gatekeeper/src/main.rs new file mode 100644 index 0000000..cfc000d --- /dev/null +++ b/crates/servers/gatekeeper/src/main.rs @@ -0,0 +1,11 @@ +#![no_std] +#![forbid(unsafe_code)] +#![no_main] + +extern crate libkos; + +libkos::entry!(main); + +fn main() { + gatekeeper::run(); +} diff --git a/crates/servers/gatekeeper/src/tests/mod.rs b/crates/servers/gatekeeper/src/tests/mod.rs index 91579aa..2032dfe 100644 --- a/crates/servers/gatekeeper/src/tests/mod.rs +++ b/crates/servers/gatekeeper/src/tests/mod.rs @@ -67,18 +67,26 @@ impl Random for Counter { /// An owner typing from a script; what was shown is kept. struct Owner { typing: VecDeque<&'static str>, + /// On the screen now. shown: String, + /// Ever shown, cleared or not. + said: String, } impl Owner { fn types(lines: &[&'static str]) -> Self { - Self { typing: lines.iter().copied().collect(), shown: String::new() } + Self { typing: lines.iter().copied().collect(), shown: String::new(), said: String::new() } } } impl Prompt for Owner { fn say(&mut self, text: &str) { self.shown.push_str(text); + self.said.push_str(text); + } + /// What a fresh screen shows: nothing that came before. + fn clear(&mut self) { + self.shown.clear(); } fn secret(&mut self, question: &str, out: &mut Secret) -> bool { self.shown.push_str(question); @@ -158,7 +166,7 @@ fn too_many_wrong_passcodes_wipe_the_keys_and_the_device_starts_fresh() { guesses.extend(["5678", "5678", "", ""]); let mut owner = Owner::types(&guesses); let new = run_with(&mut dev, &mut owner).unwrap(); - assert!(owner.shown.contains("One try left")); + assert!(owner.said.contains("One try left")); assert!(owner.shown.ends_with("This device has no passcode yet. Choose one; it protects everything on it.\nNew passcode: Again: A duress passcode wipes the device when it is entered instead. Leave it empty for none.\nDuress passcode: Again: ")); assert_ne!(new.0, key, "the old master key is gone"); let mut old = Owner::types(&["1234"]); @@ -173,6 +181,7 @@ fn the_duress_passcode_wipes_silently_and_the_device_looks_fresh() { let mut owner = Owner::types(&["0000", "4321", "4321", "", ""]); let new = run_with(&mut dev, &mut owner).unwrap(); assert!(!owner.shown.contains("Wrong"), "nothing tells it was a duress passcode"); + assert!(owner.shown.starts_with("This device has no passcode yet"), "nor is the passcode prompt left showing"); assert!(owner.shown.contains("no passcode yet")); assert_ne!(new.0, key); } diff --git a/crates/servers/storage-server/src/protocol.rs b/crates/servers/storage-server/src/protocol.rs index 10039b6..c0f790d 100644 --- a/crates/servers/storage-server/src/protocol.rs +++ b/crates/servers/storage-server/src/protocol.rs @@ -25,6 +25,8 @@ pub enum StorageRequest { Mkdir { path_len: usize }, /// Carries the endpoint to print to. SetLog, + /// Half (`half`: 0 or 1) of the master key. + SetKey { half: usize, words: [usize; 2] }, /// A capability to the path (`path_len` bytes of the buffer), inside /// the caller's scope. Grant { @@ -74,6 +76,7 @@ impl Encode for StorageRequest { Delete { path_len } => [op::DELETE, path_len, 0, 0], Mkdir { path_len } => [op::MKDIR, path_len, 0, 0], SetLog => [op::SET_LOG, 0, 0, 0], + SetKey { half, words } => [op::SET_KEY, half, words[0], words[1]], Grant { path_len, write } => [op::GRANT, path_len, usize::from(write), 0], Release => [op::RELEASE, 0, 0, 0], } @@ -95,6 +98,7 @@ impl Decode for StorageRequest { op::DELETE => Delete { path_len: w[1] }, op::MKDIR => Mkdir { path_len: w[1] }, op::SET_LOG => SetLog, + op::SET_KEY => SetKey { half: w[1], words: [w[2], w[3]] }, op::GRANT => Grant { path_len: w[1], write: w[2] != 0, diff --git a/crates/servers/storage-server/src/server.rs b/crates/servers/storage-server/src/server.rs index ca0ff20..d081533 100644 --- a/crates/servers/storage-server/src/server.rs +++ b/crates/servers/storage-server/src/server.rs @@ -50,6 +50,9 @@ pub struct StorageServer { clients: [Option; MAX_CLIENTS], owners: [Option; MAX_HANDLES], log: Option, + /// The master key's halves, from the gatekeeper through init: what the + /// disk's encryption will be keyed with. + key: [Option<[usize; 2]>; 2], } impl StorageServer { @@ -61,6 +64,7 @@ impl StorageServer { clients: [const { None }; MAX_CLIENTS], owners: [None; MAX_HANDLES], log: None, + key: [None; 2], } } @@ -83,6 +87,23 @@ impl StorageServer { } } + /// Only the unbadged holder (init), each half only once: nobody can + /// replace the key later. + fn set_key(&mut self, badge: usize, half: usize, words: [usize; 2]) -> StorageResponse { + match (badge, self.key.get_mut(half)) { + (0, Some(slot @ None)) => { + *slot = Some(words); + StorageResponse::Ok + } + _ => StorageResponse::Error, + } + } + + /// Whether the whole master key has arrived. + pub fn unlocked(&self) -> bool { + self.key.iter().all(Option::is_some) + } + pub fn filesystem(&self) -> &F { &self.fs } @@ -265,7 +286,7 @@ impl StorageServer { None => R::NotFound, }; let response = match request { - Attach | SetLog | Release => R::Error, + Attach | SetLog | SetKey { .. } | Release => R::Error, Grant { path_len, write } => { let narrower = scope.narrow(&buf[sent(path_len)], write); return match narrower.and_then(|s| grants.add(badge, s, |b| host.mint(b))) { @@ -368,6 +389,7 @@ impl Server for StorageServer { ) -> ([usize; 4], Option) { match StorageRequest::decode(words) { Some(StorageRequest::SetLog) => (self.set_log(badge, cap).encode(), None), + Some(StorageRequest::SetKey { half, words }) => (self.set_key(badge, half, words).encode(), None), // A badge storage never issued, or one it has taken back. Some(request) => match self.grants.scope(badge) { None => (StorageResponse::Denied.encode(), None), diff --git a/crates/servers/storage-server/src/tests/protocol.rs b/crates/servers/storage-server/src/tests/protocol.rs index 209db2c..3c08618 100644 --- a/crates/servers/storage-server/src/tests/protocol.rs +++ b/crates/servers/storage-server/src/tests/protocol.rs @@ -25,6 +25,7 @@ fn every_request_roundtrips() { write: false, }, StorageRequest::Release, + StorageRequest::SetKey { half: 1, words: [7, 9] }, ] { assert_eq!(StorageRequest::decode(req.encode()), Some(req)); } diff --git a/crates/servers/storage-server/src/tests/server.rs b/crates/servers/storage-server/src/tests/server.rs index 3c1cc7d..ea436c5 100644 --- a/crates/servers/storage-server/src/tests/server.rs +++ b/crates/servers/storage-server/src/tests/server.rs @@ -472,3 +472,16 @@ fn the_unbadged_holder_cannot_be_released() { StorageResponse::Error ); } + +#[test] +fn only_init_sets_the_key_and_only_once() { + let mut s = server(); + let set = |s: &mut Srv, badge, half| call(s, badge, StorageRequest::SetKey { half, words: [1, 2] }).0; + assert_eq!(set(&mut s, 5, 0), StorageResponse::Error, "a client cannot"); + assert_eq!(set(&mut s, 0, 0), StorageResponse::Ok); + assert!(!s.unlocked()); + assert_eq!(set(&mut s, 0, 0), StorageResponse::Error, "nor can it be replaced"); + assert_eq!(set(&mut s, 0, 2), StorageResponse::Error, "two halves only"); + assert_eq!(set(&mut s, 0, 1), StorageResponse::Ok); + assert!(s.unlocked()); +} diff --git a/crates/servers/terminal-server/src/server.rs b/crates/servers/terminal-server/src/server.rs index 3f778af..cbca61b 100644 --- a/crates/servers/terminal-server/src/server.rs +++ b/crates/servers/terminal-server/src/server.rs @@ -157,9 +157,9 @@ impl TerminalServer { } } - /// Edits a line from the keyboard, echoing it, until Enter. Ctrl+C - /// drops it and reads as an empty line. - fn read_line(&mut self) { + /// Edits a line from the keyboard, echoing it (as `*` if `secret`), + /// until Enter. Ctrl+C drops it and reads as an empty line. + fn read_line(&mut self, secret: bool) { self.console.flush(); // Flushed now; a flush timer still running is harmless. self.flush_pending = false; @@ -190,7 +190,7 @@ impl TerminalServer { b if self.line_len + 1 < MAX_LINE && (b' '..=b'~').contains(&b) => { self.line[self.line_len] = b; self.line_len += 1; - self.grid.put(b); + self.grid.put(if secret { b'*' } else { b }); } _ => continue, } @@ -202,14 +202,18 @@ impl TerminalServer { } /// The next chunk of the typed line, reading one first if none is left. - fn read(&mut self) -> [usize; 4] { + /// A secret line never reaches the screen, and is forgotten once read. + fn read(&mut self, secret: bool) -> [usize; 4] { if self.line_read == self.line_len { - self.read_line(); + self.read_line(secret); } let end = (self.line_read + TEXT_BYTES).min(self.line_len); - let chunk = &self.line[self.line_read..end]; + let words = text_words(&self.line[self.line_read..end]); self.line_read = end; - text_words(chunk) + if secret && self.line_read == self.line_len { + self.line.fill(0); + } + words } } @@ -230,7 +234,8 @@ impl Server for TerminalServer { fn handle_call(&mut self, _badge: usize, words: [usize; 4], cap: Option) -> ([usize; 4], Option) { let reply = match words[0] { - types::terminal::READ => self.read(), + types::terminal::READ => self.read(false), + types::terminal::READ_SECRET => self.read(true), types::terminal::REDRAW => { // Taking focus back clears the screen. self.console.take_focus(); diff --git a/crates/servers/terminal-server/src/tests/server.rs b/crates/servers/terminal-server/src/tests/server.rs index b371048..78c3456 100644 --- a/crates/servers/terminal-server/src/tests/server.rs +++ b/crates/servers/terminal-server/src/tests/server.rs @@ -176,3 +176,13 @@ fn rows_are_drawn_only_as_wide_as_their_text_or_what_it_replaces() { // Cleared: the old five cells and the cursor after them are erased. assert!(t.console().rows_drawn.contains(&(0, 6)), "{:?}", t.console().rows_drawn); } + +#[test] +fn a_secret_line_shows_stars_and_is_forgotten() { + let mut t = term(b"hunter2\n"); + let (words, _) = t.handle_call(1, [types::terminal::READ_SECRET, 0, 0, 0], None); + let (bytes, len) = text_of(words).unwrap(); + assert_eq!(&bytes[..len], b"hunter2\n"); + assert_eq!(&t.grid().row(0)[..8], b"*******\x20"); + assert!(!t.grid().row(0).windows(7).any(|w| w == b"hunter2")); +} diff --git a/crates/types/src/bounded.rs b/crates/types/src/bounded.rs index 2213391..dfde17a 100644 --- a/crates/types/src/bounded.rs +++ b/crates/types/src/bounded.rs @@ -3,7 +3,9 @@ use crate::macros::bounded; // Kani runs the real kernel with tiny tables; see kernel/src/proofs. bounded!(CapSlot, u8, 0, if cfg!(kani) { 3 } else { 63 }); bounded!(Priority, u8, 0, 31); -bounded!(PageCount, u16, 1, 1024); +// Up to a process's whole address window (32 MiB): one request may be big (a +// KDF's memory); budgets bound how much anyone gets. +bounded!(PageCount, u16, 1, 8192); impl Priority { pub const DEFAULT: Self = Self(16); diff --git a/crates/types/src/gatekeeper.rs b/crates/types/src/gatekeeper.rs new file mode 100644 index 0000000..7430140 --- /dev/null +++ b/crates/types/src/gatekeeper.rs @@ -0,0 +1,13 @@ +//! Init and the gatekeeper: after handing it the keystore and randomness +//! (device setup calls), init calls `UNLOCK`, which returns once the owner +//! is authenticated (`OK`) or the gatekeeper cannot go on (`FAILED`); then +//! `KEY`, whose reply is the 32-byte master key in its four words. The +//! gatekeeper then exits. Only the unbadged holder (init) is answered. + +pub const UNLOCK: usize = 1; +pub const KEY: usize = 2; + +pub mod reply { + pub const OK: usize = 0; + pub const FAILED: usize = 1; +} diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index 5ee6b8e..6859c39 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -8,6 +8,7 @@ pub mod bootinfo; pub mod bounded; pub mod budget; pub mod display; +pub mod gatekeeper; pub mod generation; pub mod id; pub mod input; diff --git a/crates/types/src/storage.rs b/crates/types/src/storage.rs index e2b0a0b..6875d15 100644 --- a/crates/types/src/storage.rs +++ b/crates/types/src/storage.rs @@ -33,6 +33,9 @@ pub mod op { /// Takes back the grant this is called through, and everything /// granted from it, from every holder. pub const RELEASE: usize = 12; + /// Half the master key (word 1: which half, words 2..4: it), from the + /// unbadged holder (init) only, each half once. + pub const SET_KEY: usize = 13; } /// Reply tags (word 0). diff --git a/crates/types/src/terminal.rs b/crates/types/src/terminal.rs index 31c5faa..aa10ad7 100644 --- a/crates/types/src/terminal.rs +++ b/crates/types/src/terminal.rs @@ -5,6 +5,9 @@ /// the line's last chunk ends with `\n`. Blocks until a line is entered. pub const READ: usize = 10; pub const CLEAR: usize = 11; +/// As `READ`, but what is typed shows as `*` and is forgotten once read: +/// for passcodes. +pub const READ_SECRET: usize = 14; /// Repaints the whole screen and takes input focus back, after a program /// that drew on the screen itself. pub const REDRAW: usize = 12; diff --git a/docs/ipc.md b/docs/ipc.md index 1603641..84da8d3 100644 --- a/docs/ipc.md +++ b/docs/ipc.md @@ -91,6 +91,17 @@ and budget listed there. Storage, started before the log server, receives its log endpoint from init later (`SET_LOG`, accepted only from the unbadged holder of its endpoint). +Nothing that touches user data starts before the owner is authenticated. +Init starts the log, input, display and terminal servers, then the +gatekeeper (`types::gatekeeper`), handing it only the terminal, the keystore +(a small disk of its own, told from the data disk by its size) and the +virtio-rng device. Init calls `UNLOCK`, which returns once the owner is in, +then `KEY`, whose reply is the master key; init passes it to storage +(`SET_KEY`, from the unbadged holder only, each half once) and only then +starts the permission manager and the desktop. If the gatekeeper cannot +unlock, init stops there. The gatekeeper exits, and its capabilities go +with it. + A program's `uses` endpoints arrive as badged copies with `Execute` and `Delegate`: the badge (unique per copy) tells the server which client is calling, and `Delegate` lets the client send capabilities with its calls diff --git a/scripts/make-disk-image.sh b/scripts/make-disk-image.sh index bda5975..fcc9c59 100755 --- a/scripts/make-disk-image.sh +++ b/scripts/make-disk-image.sh @@ -46,6 +46,7 @@ copy permission-manager permission copy session-server session copy terminal-server terminal copy shell shell +copy gatekeeper gatekeeper # Apps. app cat app ls diff --git a/scripts/test-qemu.sh b/scripts/test-qemu.sh index bb20b06..9312868 100755 --- a/scripts/test-qemu.sh +++ b/scripts/test-qemu.sh @@ -1,20 +1,58 @@ #!/bin/bash # Boots IMAGE headless and waits for the self-test (init's SMP checks, then -# spawntest) to report; the machine halts itself when done. Exit 0 only on -# "SMPTEST PASS". +# spawntest) to report; the machine halts itself when done. Twice, on one +# fresh keystore: the first boot sets a passcode up at the gatekeeper, the +# second gets it wrong once and then right. Exit 0 only if both unlock and +# both say "SMPTEST PASS". set -uo pipefail IMAGE="${1:?usage: test-qemu.sh IMAGE}" SMP="${SMP:-4}" KERNEL="target/riscv64gc-unknown-none-elf/release/adapter-riscv" LOG="$(mktemp -t kos-test-qemu.XXXXXX)" +KEYS="${LOG}.keys" +MON="${LOG}.mon" +truncate -s 64K "${KEYS}" -timeout ${QT:-120} qemu-system-riscv64 -machine virt -smp "${SMP}" -bios default \ - -kernel "${KERNEL}" \ - -drive "file=${IMAGE},if=none,format=raw,id=hd0" -device virtio-blk-device,drive=hd0 \ - -device virtio-gpu-device -device virtio-keyboard-device \ - -display none -serial "file:${LOG}" -monitor none +# Types `lines` (one per argument) at the gatekeeper once it waits. +owner() { + for _ in $(seq 300); do grep -aq "gatekeeper: waiting" "${LOG}" 2>/dev/null && break; sleep 0.2; done + for line in "$@"; do + for ((i = 0; i < ${#line}; i++)); do + echo "sendkey ${line:i:1}" | socat - "unix-connect:${MON}" >/dev/null 2>&1; sleep 0.05 + done + echo "sendkey ret" | socat - "unix-connect:${MON}" >/dev/null 2>&1; sleep 0.3 + done +} + +# One boot; `owner` types the arguments. Its serial output lands in LOG. +boot() { + : > "${LOG}"; rm -f "${MON}" + owner "$@" & + timeout ${QT:-120} qemu-system-riscv64 -machine virt -smp "${SMP}" -bios default \ + -kernel "${KERNEL}" \ + -drive "file=${IMAGE},if=none,format=raw,id=hd0" -device virtio-blk-device,drive=hd0 \ + -drive "file=${KEYS},if=none,format=raw,id=ks" -device virtio-blk-device,drive=ks \ + -device virtio-rng-device -device virtio-gpu-device -device virtio-keyboard-device \ + -display none -serial "file:${LOG}" -monitor "unix:${MON},server,nowait" + local s=$? + wait + return $s +} + +# First boot: a new passcode (twice), no duress passcode (twice empty). +boot 1234 1234 "" "" status=$? +first_ok=0 +grep -aq "the owner is in" "${LOG}" && grep -aq "SMPTEST PASS" "${LOG}" && first_ok=1 +echo "test-qemu: first boot: passcode set up: $([ ${first_ok} -eq 1 ] && echo ok || echo FAIL)" +# Second boot: the keystore remembers; a wrong guess, then the passcode. +boot 9999 1234 +[ ${status} -eq 0 ] && status=$? +second_ok=0 +grep -aq "the owner is in" "${LOG}" && second_ok=1 +echo "test-qemu: second boot: wrong passcode refused, right one unlocks: $([ ${second_ok} -eq 1 ] && echo ok || echo FAIL)" +rm -f "${KEYS}" "${MON}" grep -a "smptest:\|spawntest:\|Hello\|SMPTEST" "${LOG}" # What k-os wrote must be ext4 that the real tools accept. @@ -29,7 +67,7 @@ if command -v e2fsck >/dev/null && command -v debugfs >/dev/null; then fi rm -f "${LOG}.fsck" fi -if [ "${disk_ok}" -eq 1 ] && grep -aq "SMPTEST PASS" "${LOG}"; then +if [ "${disk_ok}" -eq 1 ] && [ ${first_ok} -eq 1 ] && [ ${second_ok} -eq 1 ] && grep -aq "SMPTEST PASS" "${LOG}"; then echo "test-qemu: pass (${SMP} harts)" rm -f "${LOG}" exit 0