feat(boot): gatekeeper gates boot (init→UI→gatekeeper→data); keystore disk + virtio-rng discovery; terminal READ_SECRET; storage SET_KEY; PageCount ≤8192, alloc cap gone; device window 0x01D0_0000; test-qemu types passcode over 2 boots
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,3 @@
|
|||||||
/target
|
/target
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.img
|
*.img
|
||||||
|
|||||||
6
Cargo.lock
generated
6
Cargo.lock
generated
@@ -291,7 +291,13 @@ version = "0.1.0"
|
|||||||
name = "gatekeeper"
|
name = "gatekeeper"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"driver-block-virtio",
|
||||||
|
"driver-rng-virtio",
|
||||||
"keystore",
|
"keystore",
|
||||||
|
"libkos",
|
||||||
|
"port-block-device",
|
||||||
|
"port-entropy",
|
||||||
|
"types",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
12
Makefile
12
Makefile
@@ -55,11 +55,17 @@ build:
|
|||||||
-p init -p echo -p sleep -p snake \
|
-p init -p echo -p sleep -p snake \
|
||||||
-p display-qemu -p input-qemu -p storage-qemu \
|
-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 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
|
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
|
image: build
|
||||||
scripts/make-disk-image.sh
|
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
|
# Boots a test image (normal image + bin/smptest) on $(SMP) harts; init runs
|
||||||
# the SMP self-test and halts. Passes only on "SMPTEST PASS".
|
# 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 \
|
qemu-system-riscv64 -machine virt -smp $(SMP) -nographic -bios default \
|
||||||
-kernel $(KERNEL_BIN) \
|
-kernel $(KERNEL_BIN) \
|
||||||
-drive file=boot.img,if=none,format=raw,id=hd0 \
|
-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
|
run-gui: image
|
||||||
qemu-system-riscv64 -machine virt -smp $(SMP) -bios default \
|
qemu-system-riscv64 -machine virt -smp $(SMP) -bios default \
|
||||||
-kernel $(KERNEL_BIN) \
|
-kernel $(KERNEL_BIN) \
|
||||||
-drive file=boot.img,if=none,format=raw,id=hd0 \
|
-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-gpu-device \
|
||||||
-device virtio-keyboard-device \
|
-device virtio-keyboard-device \
|
||||||
-serial stdio
|
-serial stdio
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ fn run_hello(fs: &mut Storage, died: CapSlot, grant: Option<CapSlot>) -> Result<
|
|||||||
|
|
||||||
/// A file written through storage, which the host then checks with e2fsck.
|
/// A file written through storage, which the host then checks with e2fsck.
|
||||||
fn write_a_file(fs: &mut Storage) -> Result<(), &'static str> {
|
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")?;
|
let h = fs.create(b"selftest.txt").map_err(|_| "cannot create a file")?;
|
||||||
if fs.write(h, NOTE) != Ok(NOTE.len()) {
|
if fs.write(h, NOTE) != Ok(NOTE.len()) {
|
||||||
return Err("short write");
|
return Err("short write");
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ use port_block_device::{BlockDevice, SectorId};
|
|||||||
use types::address::{ByteOffset, PhysicalAddress, VirtualAddress};
|
use types::address::{ByteOffset, PhysicalAddress, VirtualAddress};
|
||||||
use types::bounded::PageCount;
|
use types::bounded::PageCount;
|
||||||
|
|
||||||
const MMIO_VA: VirtualAddress = VirtualAddress::from_raw(0x20000);
|
/// Drivers map their devices in the device window (0x01D0_0000 up, 64 KiB
|
||||||
const VQUEUE_VA: VirtualAddress = VirtualAddress::from_raw(0x21000);
|
/// each), clear of program images and heaps.
|
||||||
const REQ_HDR_VA: VirtualAddress = VirtualAddress::from_raw(0x23000);
|
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;
|
const SECTOR_SIZE: usize = 512;
|
||||||
|
|
||||||
pub struct VirtioBlockDevice {
|
pub struct VirtioBlockDevice {
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ use types::address::{ByteOffset, PhysicalAddress, VirtualAddress};
|
|||||||
use types::bounded::PageCount;
|
use types::bounded::PageCount;
|
||||||
use types::display::{Color, Dimension, ScreenCoord};
|
use types::display::{Color, Dimension, ScreenCoord};
|
||||||
|
|
||||||
const GPU_MMIO_VA: VirtualAddress = VirtualAddress::from_raw(0x20000);
|
/// Drivers map their devices in the device window (0x01D0_0000 up, 64 KiB
|
||||||
const VQUEUE_VA: VirtualAddress = VirtualAddress::from_raw(0x21000);
|
/// each), clear of program images and heaps.
|
||||||
const CMDBUF_VA: VirtualAddress = VirtualAddress::from_raw(0x23000);
|
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_VA: VirtualAddress = VirtualAddress::from_raw(0x100000);
|
||||||
const FB_BPP: usize = 4;
|
const FB_BPP: usize = 4;
|
||||||
/// Pixels converted and stored per run.
|
/// Pixels converted and stored per run.
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ use libkos::Window;
|
|||||||
use port_input_device::{InputDevice, InputEvent, Key};
|
use port_input_device::{InputDevice, InputEvent, Key};
|
||||||
use types::address::{ByteOffset, VirtualAddress};
|
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_RBR: ByteOffset = ByteOffset::new(0);
|
||||||
const UART_IER_OFFSET: ByteOffset = ByteOffset::new(1);
|
const UART_IER_OFFSET: ByteOffset = ByteOffset::new(1);
|
||||||
const UART_LSR_OFFSET: ByteOffset = ByteOffset::new(5);
|
const UART_LSR_OFFSET: ByteOffset = ByteOffset::new(5);
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ use port_input_device::{InputDevice, InputEvent};
|
|||||||
use types::address::{ByteOffset, PhysicalAddress, VirtualAddress};
|
use types::address::{ByteOffset, PhysicalAddress, VirtualAddress};
|
||||||
use types::bounded::PageCount;
|
use types::bounded::PageCount;
|
||||||
|
|
||||||
const INPUT_MMIO_VA: VirtualAddress = VirtualAddress::from_raw(0x25000);
|
/// Drivers map their devices in the device window (0x01D0_0000 up, 64 KiB
|
||||||
const VQUEUE_VA: VirtualAddress = VirtualAddress::from_raw(0x26000);
|
/// each), clear of program images and heaps.
|
||||||
const EVBUF_VA: VirtualAddress = VirtualAddress::from_raw(0x28000);
|
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 QUEUE_SIZE: usize = 32;
|
||||||
const EVENT_SIZE: usize = 8;
|
const EVENT_SIZE: usize = 8;
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ use port_entropy::Entropy;
|
|||||||
use types::address::{ByteOffset, VirtualAddress};
|
use types::address::{ByteOffset, VirtualAddress};
|
||||||
use types::bounded::PageCount;
|
use types::bounded::PageCount;
|
||||||
|
|
||||||
const MMIO_VA: VirtualAddress = VirtualAddress::from_raw(0x2A000);
|
/// Drivers map their devices in the device window (0x01D0_0000 up, 64 KiB
|
||||||
const VQUEUE_VA: VirtualAddress = VirtualAddress::from_raw(0x2B000);
|
/// each), clear of program images and heaps.
|
||||||
const BUFFER_VA: VirtualAddress = VirtualAddress::from_raw(0x2D000);
|
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.
|
/// Asked for at most this much at a time.
|
||||||
const CHUNK: usize = 256;
|
const CHUNK: usize = 256;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
//! Finds the board's devices (`types::bootinfo`): disk, GPU, and keyboard
|
//! Finds the board's devices (`types::bootinfo`): disk, GPU, and keyboard
|
||||||
//! (or the UART when there is none).
|
//! (or the UART when there is none).
|
||||||
|
|
||||||
|
use hw_virtio::block::CONFIG_CAPACITY;
|
||||||
use hw_virtio::mmio::{
|
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::device::{self, Device};
|
||||||
use libkos::syscall;
|
use libkos::syscall;
|
||||||
@@ -19,8 +20,14 @@ pub struct Devices {
|
|||||||
pub disk: Device,
|
pub disk: Device,
|
||||||
pub gpu: Device,
|
pub gpu: Device,
|
||||||
pub input: 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<CapSlot> {
|
fn slot(raw: u64) -> Option<CapSlot> {
|
||||||
if raw == NO_SLOT {
|
if raw == NO_SLOT {
|
||||||
return None;
|
return None;
|
||||||
@@ -28,21 +35,30 @@ fn slot(raw: u64) -> Option<CapSlot> {
|
|||||||
CapSlot::new(u8::try_from(raw).ok()?)
|
CapSlot::new(u8::try_from(raw).ok()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The virtio device id behind `region`, 0 for an empty slot. Read through
|
/// The virtio device id behind `region` (0 for an empty slot) and, for a
|
||||||
/// a copy of the capability, mapped only while looking and then dropped,
|
/// block device, its size in sectors. Read through a copy of the
|
||||||
/// which unmaps it.
|
/// capability, mapped only while looking and then dropped, which unmaps it.
|
||||||
fn virtio_id(region: CapSlot) -> u32 {
|
fn virtio_id(region: CapSlot) -> (u32, u64) {
|
||||||
let perms = PermissionSet::empty().with(Permission::Read).with(Permission::Write);
|
let perms = PermissionSet::empty().with(Permission::Read).with(Permission::Write);
|
||||||
let Ok(copy) = syscall::mint(region, Badge::NONE, perms) else {
|
let Ok(copy) = syscall::mint(region, Badge::NONE, perms) else {
|
||||||
return 0;
|
return (0, 0);
|
||||||
};
|
};
|
||||||
let id = match device::map_mmio(copy, PROBE_VA) {
|
let id = match device::map_mmio(copy, PROBE_VA) {
|
||||||
// The registers stay mapped until the drop below.
|
// The registers stay mapped until the drop below.
|
||||||
Ok(regs) => match regs.read::<u32>(REG_MAGIC) == MAGIC {
|
Ok(regs) => match regs.read::<u32>(REG_MAGIC) == MAGIC {
|
||||||
true => regs.read(REG_DEVICE_ID),
|
true => {
|
||||||
false => 0,
|
let id = regs.read(REG_DEVICE_ID);
|
||||||
|
let size = match id {
|
||||||
|
DEVICE_ID_BLOCK => {
|
||||||
|
regs.read::<u32>(CONFIG_CAPACITY) as u64 | (regs.read::<u32>(CONFIG_CAPACITY + 4) as u64) << 32
|
||||||
|
}
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
(id, size)
|
||||||
|
}
|
||||||
|
false => (0, 0),
|
||||||
},
|
},
|
||||||
Err(_) => 0,
|
Err(_) => (0, 0),
|
||||||
};
|
};
|
||||||
syscall::drop_cap(copy);
|
syscall::drop_cap(copy);
|
||||||
id
|
id
|
||||||
@@ -74,7 +90,13 @@ pub fn console() -> Option<CapSlot> {
|
|||||||
/// Sorts the board's devices out.
|
/// Sorts the board's devices out.
|
||||||
pub fn discover() -> Devices {
|
pub fn discover() -> Devices {
|
||||||
let info = boot_info();
|
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;
|
let mut uart = Device::NONE;
|
||||||
for d in info.devices() {
|
for d in info.devices() {
|
||||||
let Some(region) = slot(d.region_slot) else {
|
let Some(region) = slot(d.region_slot) else {
|
||||||
@@ -84,9 +106,11 @@ pub fn discover() -> Devices {
|
|||||||
let place = match d.kind {
|
let place = match d.kind {
|
||||||
KIND_UART_16550 => &mut uart,
|
KIND_UART_16550 => &mut uart,
|
||||||
KIND_VIRTIO => match virtio_id(region) {
|
KIND_VIRTIO => match virtio_id(region) {
|
||||||
DEVICE_ID_BLOCK => &mut found.disk,
|
(DEVICE_ID_BLOCK, size) if size <= KEYSTORE_MAX_SECTORS => &mut found.keystore,
|
||||||
DEVICE_ID_GPU => &mut found.gpu,
|
(DEVICE_ID_BLOCK, _) => &mut found.disk,
|
||||||
DEVICE_ID_INPUT => &mut found.input,
|
(DEVICE_ID_GPU, _) => &mut found.gpu,
|
||||||
|
(DEVICE_ID_INPUT, _) => &mut found.input,
|
||||||
|
(DEVICE_ID_RNG, _) => &mut found.random,
|
||||||
_ => continue,
|
_ => continue,
|
||||||
},
|
},
|
||||||
_ => continue,
|
_ => continue,
|
||||||
|
|||||||
@@ -110,8 +110,12 @@ impl<'a> Launcher<'a> {
|
|||||||
if let Some(free) = self.served.iter_mut().find(|s| s.is_none()) {
|
if let Some(free) = self.served.iter_mut().find(|s| s.is_none()) {
|
||||||
*free = Some((p.name, ep));
|
*free = Some((p.name, ep));
|
||||||
}
|
}
|
||||||
if let Some(device) = p.device {
|
for device in p.devices {
|
||||||
note = self.give(device, ep);
|
note = self.give(*device, ep);
|
||||||
|
}
|
||||||
|
if p.unlocks {
|
||||||
|
unlock(ep)?;
|
||||||
|
note = ", the owner is in";
|
||||||
}
|
}
|
||||||
if console.is_some() {
|
if console.is_some() {
|
||||||
hand_over_console(ep);
|
hand_over_console(ep);
|
||||||
@@ -139,6 +143,8 @@ impl<'a> Launcher<'a> {
|
|||||||
let (dev, given, none): (BoardDevice, _, _) = match device {
|
let (dev, given, none): (BoardDevice, _, _) = match device {
|
||||||
Device::Gpu => (self.devices.gpu, ", gpu handed over", ", no gpu"),
|
Device::Gpu => (self.devices.gpu, ", gpu handed over", ", no gpu"),
|
||||||
Device::Input => (self.devices.input, ", input device handed over", ", no input device"),
|
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() {
|
match libkos::device::give_device(ep, dev).is_ok() && dev.mmio.is_some() {
|
||||||
true => given,
|
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,
|
/// From here on init prints through the log server, like everyone else,
|
||||||
/// and holds no console of its own.
|
/// and holds no console of its own.
|
||||||
fn hand_over_console(log: CapSlot) {
|
fn hand_over_console(log: CapSlot) {
|
||||||
|
|||||||
@@ -80,6 +80,11 @@ fn launch_all(launcher: &mut launch::Launcher, programs: &[manifest::Program]) {
|
|||||||
libkos::print!("init: loading {}... ", p.name);
|
libkos::print!("init: loading {}... ", p.name);
|
||||||
match launcher.launch(p) {
|
match launcher.launch(p) {
|
||||||
Ok(note) => libkos::println!("ok{}", note),
|
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),
|
Err(why) => libkos::println!("SKIP ({})", why),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ use types::budget::Limits;
|
|||||||
pub enum Device {
|
pub enum Device {
|
||||||
Gpu,
|
Gpu,
|
||||||
Input,
|
Input,
|
||||||
|
Keystore,
|
||||||
|
Random,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A storage grant: a folder (or the whole disk, `b""`) and whether it may
|
/// A storage grant: a folder (or the whole disk, `b""`) and whether it may
|
||||||
@@ -23,7 +25,11 @@ pub struct Program {
|
|||||||
pub limits: Limits,
|
pub limits: Limits,
|
||||||
/// Serves an endpoint, which it finds in slot 0.
|
/// Serves an endpoint, which it finds in slot 0.
|
||||||
pub serves: bool,
|
pub serves: bool,
|
||||||
pub device: Option<Device>,
|
/// 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
|
/// 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.
|
/// program's copy carries its own badge, so a server can tell them apart.
|
||||||
pub uses: &'static [&'static str],
|
pub uses: &'static [&'static str],
|
||||||
@@ -51,7 +57,8 @@ const fn server(name: &'static str, file: &'static [u8], limits: Limits) -> Prog
|
|||||||
file,
|
file,
|
||||||
limits,
|
limits,
|
||||||
serves: true,
|
serves: true,
|
||||||
device: None,
|
devices: &[],
|
||||||
|
unlocks: false,
|
||||||
uses: &[],
|
uses: &[],
|
||||||
storage: None,
|
storage: None,
|
||||||
console: false,
|
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.
|
/// Started on every boot. The log server comes first so the rest can print.
|
||||||
pub const SERVERS: &[Program] = &[
|
pub const SERVERS: &[Program] = &[
|
||||||
Program { console: true, ..server("log", b"bin/log", SMALL) },
|
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.
|
// The input server's only client: it passes input on to the app in focus.
|
||||||
Program {
|
Program {
|
||||||
device: Some(Device::Gpu),
|
devices: &[Device::Gpu],
|
||||||
uses: &["input"],
|
uses: &["input"],
|
||||||
// The framebuffer alone is ~1000 pages at 1280x800.
|
// The framebuffer alone is ~1000 pages at 1280x800.
|
||||||
..server("display", b"bin/display", Limits { pages: 1280, regions: 4, ..SMALL })
|
..server("display", b"bin/display", Limits { pages: 1280, regions: 4, ..SMALL })
|
||||||
},
|
},
|
||||||
// Text on the display, lines from the keyboard (it takes input focus).
|
// Text on the display, lines from the keyboard (it takes input focus).
|
||||||
Program { uses: &["display"], ..server("terminal", b"bin/terminal", SMALL) },
|
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
|
// 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.
|
// files and the owner's decisions, and runs apps through init.
|
||||||
Program {
|
Program {
|
||||||
|
|||||||
@@ -70,7 +70,11 @@ fn out_of_range_arguments_do_not_decode() {
|
|||||||
decode(SYS_REQUEST_PAGES, &[0, 0, 0, 0, 0, 0]).is_none(),
|
decode(SYS_REQUEST_PAGES, &[0, 0, 0, 0, 0, 0]).is_none(),
|
||||||
"zero pages"
|
"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!(
|
assert!(
|
||||||
decode(SYS_MAP_REGION, &[1, 0x30000, 3, 0, 0, 0]).is_none(),
|
decode(SYS_MAP_REGION, &[1, 0x30000, 3, 0, 0, 0]).is_none(),
|
||||||
"bad perms"
|
"bad perms"
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ use core::cell::UnsafeCell;
|
|||||||
use core::ptr;
|
use core::ptr;
|
||||||
|
|
||||||
const PAGE_SIZE: usize = 4096;
|
const PAGE_SIZE: usize = 4096;
|
||||||
const MAX_PAGES: usize = 64;
|
|
||||||
|
|
||||||
struct FreeBlock {
|
struct FreeBlock {
|
||||||
size: usize,
|
size: usize,
|
||||||
@@ -14,10 +13,10 @@ pub struct KosAllocator {
|
|||||||
inner: UnsafeCell<AllocatorInner>,
|
inner: UnsafeCell<AllocatorInner>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How much heap a process gets is its page budget's business (the kernel
|
||||||
|
/// enforces it), not the allocator's.
|
||||||
struct AllocatorInner {
|
struct AllocatorInner {
|
||||||
free_list: *mut FreeBlock,
|
free_list: *mut FreeBlock,
|
||||||
pages: [usize; MAX_PAGES],
|
|
||||||
page_count: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe impl Sync for KosAllocator {}
|
unsafe impl Sync for KosAllocator {}
|
||||||
@@ -25,11 +24,7 @@ unsafe impl Sync for KosAllocator {}
|
|||||||
impl KosAllocator {
|
impl KosAllocator {
|
||||||
pub const fn new() -> Self {
|
pub const fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
inner: UnsafeCell::new(AllocatorInner {
|
inner: UnsafeCell::new(AllocatorInner { free_list: ptr::null_mut() }),
|
||||||
free_list: ptr::null_mut(),
|
|
||||||
pages: [0; MAX_PAGES],
|
|
||||||
page_count: 0,
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,19 +36,12 @@ impl KosAllocator {
|
|||||||
impl AllocatorInner {
|
impl AllocatorInner {
|
||||||
/// `n` pages in one piece, so an allocation larger than a page fits.
|
/// `n` pages in one piece, so an allocation larger than a page fits.
|
||||||
fn request_pages(&mut self, n: usize) -> *mut u8 {
|
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 {
|
let Some(count) = u16::try_from(n).ok().and_then(types::bounded::PageCount::new) else {
|
||||||
return ptr::null_mut();
|
return ptr::null_mut();
|
||||||
};
|
};
|
||||||
let Some(va) = crate::syscall::request_pages(count) else {
|
let Some(va) = crate::syscall::request_pages(count) else {
|
||||||
return ptr::null_mut();
|
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
|
va.raw() as *mut u8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,18 @@ use types::bounded::CapSlot;
|
|||||||
|
|
||||||
/// Reads a typed line into `buf`, newline included if it fits; how much.
|
/// Reads a typed line into `buf`, newline included if it fits; how much.
|
||||||
pub fn read_line(terminal: CapSlot, buf: &mut [u8]) -> usize {
|
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;
|
let mut n = 0;
|
||||||
while n < buf.len() {
|
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;
|
break;
|
||||||
};
|
};
|
||||||
let Some((bytes, len)) = types::log::text_of(words) else {
|
let Some((bytes, len)) = types::log::text_of(words) else {
|
||||||
|
|||||||
@@ -6,3 +6,14 @@ edition = "2024"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
keystore = { workspace = true }
|
keystore = { workspace = true }
|
||||||
zeroize = { 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
|
||||||
|
|||||||
55
crates/servers/gatekeeper/src/devices.rs
Normal file
55
crates/servers/gatekeeper/src/devices.rs
Normal file
@@ -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<D: BlockDevice>(pub D);
|
||||||
|
|
||||||
|
impl<D: BlockDevice> Keystore for Disk<D> {
|
||||||
|
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<E: Entropy>(pub E);
|
||||||
|
|
||||||
|
impl<E: Entropy> keystore::Random for Hardware<E> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -57,6 +57,8 @@ impl Drop for Secret {
|
|||||||
/// The owner, on the screen and keyboard.
|
/// The owner, on the screen and keyboard.
|
||||||
pub trait Prompt {
|
pub trait Prompt {
|
||||||
fn say(&mut self, text: &str);
|
fn say(&mut self, text: &str);
|
||||||
|
/// Blanks the screen.
|
||||||
|
fn clear(&mut self);
|
||||||
/// Shows `question` and reads a passcode without showing it; false if
|
/// Shows `question` and reads a passcode without showing it; false if
|
||||||
/// nothing could be read.
|
/// nothing could be read.
|
||||||
fn secret(&mut self, question: &str, out: &mut Secret) -> bool;
|
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
|
/// A device never set up (or wiped): the owner chooses a passcode, and
|
||||||
/// may choose a duress one. Returns the new master key.
|
/// 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<Key, Failure> {
|
fn setup(ks: &mut impl Keystore, rng: &mut impl Random, prompt: &mut impl Prompt, params: KdfParams, memory: &mut [Block]) -> Result<Key, Failure> {
|
||||||
|
// 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");
|
prompt.say("This device has no passcode yet. Choose one; it protects everything on it.\n");
|
||||||
let mut real = Secret::new();
|
let mut real = Secret::new();
|
||||||
choose(prompt, "New passcode: ", false, &mut real)?;
|
choose(prompt, "New passcode: ", false, &mut real)?;
|
||||||
|
|||||||
@@ -7,8 +7,78 @@
|
|||||||
|
|
||||||
extern crate alloc;
|
extern crate alloc;
|
||||||
|
|
||||||
|
pub mod devices;
|
||||||
pub mod flow;
|
pub mod flow;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "tests/mod.rs"]
|
#[path = "tests/mod.rs"]
|
||||||
mod tests;
|
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()
|
||||||
|
}
|
||||||
|
|||||||
11
crates/servers/gatekeeper/src/main.rs
Normal file
11
crates/servers/gatekeeper/src/main.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#![no_std]
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
#![no_main]
|
||||||
|
|
||||||
|
extern crate libkos;
|
||||||
|
|
||||||
|
libkos::entry!(main);
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
gatekeeper::run();
|
||||||
|
}
|
||||||
@@ -67,18 +67,26 @@ impl Random for Counter {
|
|||||||
/// An owner typing from a script; what was shown is kept.
|
/// An owner typing from a script; what was shown is kept.
|
||||||
struct Owner {
|
struct Owner {
|
||||||
typing: VecDeque<&'static str>,
|
typing: VecDeque<&'static str>,
|
||||||
|
/// On the screen now.
|
||||||
shown: String,
|
shown: String,
|
||||||
|
/// Ever shown, cleared or not.
|
||||||
|
said: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Owner {
|
impl Owner {
|
||||||
fn types(lines: &[&'static str]) -> Self {
|
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 {
|
impl Prompt for Owner {
|
||||||
fn say(&mut self, text: &str) {
|
fn say(&mut self, text: &str) {
|
||||||
self.shown.push_str(text);
|
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 {
|
fn secret(&mut self, question: &str, out: &mut Secret) -> bool {
|
||||||
self.shown.push_str(question);
|
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", "", ""]);
|
guesses.extend(["5678", "5678", "", ""]);
|
||||||
let mut owner = Owner::types(&guesses);
|
let mut owner = Owner::types(&guesses);
|
||||||
let new = run_with(&mut dev, &mut owner).unwrap();
|
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!(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");
|
assert_ne!(new.0, key, "the old master key is gone");
|
||||||
let mut old = Owner::types(&["1234"]);
|
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 mut owner = Owner::types(&["0000", "4321", "4321", "", ""]);
|
||||||
let new = run_with(&mut dev, &mut owner).unwrap();
|
let new = run_with(&mut dev, &mut owner).unwrap();
|
||||||
assert!(!owner.shown.contains("Wrong"), "nothing tells it was a duress passcode");
|
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!(owner.shown.contains("no passcode yet"));
|
||||||
assert_ne!(new.0, key);
|
assert_ne!(new.0, key);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ pub enum StorageRequest {
|
|||||||
Mkdir { path_len: usize },
|
Mkdir { path_len: usize },
|
||||||
/// Carries the endpoint to print to.
|
/// Carries the endpoint to print to.
|
||||||
SetLog,
|
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
|
/// A capability to the path (`path_len` bytes of the buffer), inside
|
||||||
/// the caller's scope.
|
/// the caller's scope.
|
||||||
Grant {
|
Grant {
|
||||||
@@ -74,6 +76,7 @@ impl Encode for StorageRequest {
|
|||||||
Delete { path_len } => [op::DELETE, path_len, 0, 0],
|
Delete { path_len } => [op::DELETE, path_len, 0, 0],
|
||||||
Mkdir { path_len } => [op::MKDIR, path_len, 0, 0],
|
Mkdir { path_len } => [op::MKDIR, path_len, 0, 0],
|
||||||
SetLog => [op::SET_LOG, 0, 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],
|
Grant { path_len, write } => [op::GRANT, path_len, usize::from(write), 0],
|
||||||
Release => [op::RELEASE, 0, 0, 0],
|
Release => [op::RELEASE, 0, 0, 0],
|
||||||
}
|
}
|
||||||
@@ -95,6 +98,7 @@ impl Decode for StorageRequest {
|
|||||||
op::DELETE => Delete { path_len: w[1] },
|
op::DELETE => Delete { path_len: w[1] },
|
||||||
op::MKDIR => Mkdir { path_len: w[1] },
|
op::MKDIR => Mkdir { path_len: w[1] },
|
||||||
op::SET_LOG => SetLog,
|
op::SET_LOG => SetLog,
|
||||||
|
op::SET_KEY => SetKey { half: w[1], words: [w[2], w[3]] },
|
||||||
op::GRANT => Grant {
|
op::GRANT => Grant {
|
||||||
path_len: w[1],
|
path_len: w[1],
|
||||||
write: w[2] != 0,
|
write: w[2] != 0,
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ pub struct StorageServer<F: FileSystem, H: Host> {
|
|||||||
clients: [Option<Client>; MAX_CLIENTS],
|
clients: [Option<Client>; MAX_CLIENTS],
|
||||||
owners: [Option<Owner>; MAX_HANDLES],
|
owners: [Option<Owner>; MAX_HANDLES],
|
||||||
log: Option<CapSlot>,
|
log: Option<CapSlot>,
|
||||||
|
/// 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<F: FileSystem, H: Host> StorageServer<F, H> {
|
impl<F: FileSystem, H: Host> StorageServer<F, H> {
|
||||||
@@ -61,6 +64,7 @@ impl<F: FileSystem, H: Host> StorageServer<F, H> {
|
|||||||
clients: [const { None }; MAX_CLIENTS],
|
clients: [const { None }; MAX_CLIENTS],
|
||||||
owners: [None; MAX_HANDLES],
|
owners: [None; MAX_HANDLES],
|
||||||
log: None,
|
log: None,
|
||||||
|
key: [None; 2],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +87,23 @@ impl<F: FileSystem, H: Host> StorageServer<F, H> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
pub fn filesystem(&self) -> &F {
|
||||||
&self.fs
|
&self.fs
|
||||||
}
|
}
|
||||||
@@ -265,7 +286,7 @@ impl<F: FileSystem, H: Host> StorageServer<F, H> {
|
|||||||
None => R::NotFound,
|
None => R::NotFound,
|
||||||
};
|
};
|
||||||
let response = match request {
|
let response = match request {
|
||||||
Attach | SetLog | Release => R::Error,
|
Attach | SetLog | SetKey { .. } | Release => R::Error,
|
||||||
Grant { path_len, write } => {
|
Grant { path_len, write } => {
|
||||||
let narrower = scope.narrow(&buf[sent(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))) {
|
return match narrower.and_then(|s| grants.add(badge, s, |b| host.mint(b))) {
|
||||||
@@ -368,6 +389,7 @@ impl<F: FileSystem, H: Host> Server for StorageServer<F, H> {
|
|||||||
) -> ([usize; 4], Option<Give>) {
|
) -> ([usize; 4], Option<Give>) {
|
||||||
match StorageRequest::decode(words) {
|
match StorageRequest::decode(words) {
|
||||||
Some(StorageRequest::SetLog) => (self.set_log(badge, cap).encode(), None),
|
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.
|
// A badge storage never issued, or one it has taken back.
|
||||||
Some(request) => match self.grants.scope(badge) {
|
Some(request) => match self.grants.scope(badge) {
|
||||||
None => (StorageResponse::Denied.encode(), None),
|
None => (StorageResponse::Denied.encode(), None),
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ fn every_request_roundtrips() {
|
|||||||
write: false,
|
write: false,
|
||||||
},
|
},
|
||||||
StorageRequest::Release,
|
StorageRequest::Release,
|
||||||
|
StorageRequest::SetKey { half: 1, words: [7, 9] },
|
||||||
] {
|
] {
|
||||||
assert_eq!(StorageRequest::decode(req.encode()), Some(req));
|
assert_eq!(StorageRequest::decode(req.encode()), Some(req));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -472,3 +472,16 @@ fn the_unbadged_holder_cannot_be_released() {
|
|||||||
StorageResponse::Error
|
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());
|
||||||
|
}
|
||||||
|
|||||||
@@ -157,9 +157,9 @@ impl<C: Console> TerminalServer<C> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Edits a line from the keyboard, echoing it, until Enter. Ctrl+C
|
/// Edits a line from the keyboard, echoing it (as `*` if `secret`),
|
||||||
/// drops it and reads as an empty line.
|
/// until Enter. Ctrl+C drops it and reads as an empty line.
|
||||||
fn read_line(&mut self) {
|
fn read_line(&mut self, secret: bool) {
|
||||||
self.console.flush();
|
self.console.flush();
|
||||||
// Flushed now; a flush timer still running is harmless.
|
// Flushed now; a flush timer still running is harmless.
|
||||||
self.flush_pending = false;
|
self.flush_pending = false;
|
||||||
@@ -190,7 +190,7 @@ impl<C: Console> TerminalServer<C> {
|
|||||||
b if self.line_len + 1 < MAX_LINE && (b' '..=b'~').contains(&b) => {
|
b if self.line_len + 1 < MAX_LINE && (b' '..=b'~').contains(&b) => {
|
||||||
self.line[self.line_len] = b;
|
self.line[self.line_len] = b;
|
||||||
self.line_len += 1;
|
self.line_len += 1;
|
||||||
self.grid.put(b);
|
self.grid.put(if secret { b'*' } else { b });
|
||||||
}
|
}
|
||||||
_ => continue,
|
_ => continue,
|
||||||
}
|
}
|
||||||
@@ -202,14 +202,18 @@ impl<C: Console> TerminalServer<C> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The next chunk of the typed line, reading one first if none is left.
|
/// 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 {
|
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 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;
|
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<C: Console> Server for TerminalServer<C> {
|
|||||||
|
|
||||||
fn handle_call(&mut self, _badge: usize, words: [usize; 4], cap: Option<CapSlot>) -> ([usize; 4], Option<libkos::syscall::Give>) {
|
fn handle_call(&mut self, _badge: usize, words: [usize; 4], cap: Option<CapSlot>) -> ([usize; 4], Option<libkos::syscall::Give>) {
|
||||||
let reply = match words[0] {
|
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 => {
|
types::terminal::REDRAW => {
|
||||||
// Taking focus back clears the screen.
|
// Taking focus back clears the screen.
|
||||||
self.console.take_focus();
|
self.console.take_focus();
|
||||||
|
|||||||
@@ -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.
|
// Cleared: the old five cells and the cursor after them are erased.
|
||||||
assert!(t.console().rows_drawn.contains(&(0, 6)), "{:?}", t.console().rows_drawn);
|
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"));
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ use crate::macros::bounded;
|
|||||||
// Kani runs the real kernel with tiny tables; see kernel/src/proofs.
|
// Kani runs the real kernel with tiny tables; see kernel/src/proofs.
|
||||||
bounded!(CapSlot, u8, 0, if cfg!(kani) { 3 } else { 63 });
|
bounded!(CapSlot, u8, 0, if cfg!(kani) { 3 } else { 63 });
|
||||||
bounded!(Priority, u8, 0, 31);
|
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 {
|
impl Priority {
|
||||||
pub const DEFAULT: Self = Self(16);
|
pub const DEFAULT: Self = Self(16);
|
||||||
|
|||||||
13
crates/types/src/gatekeeper.rs
Normal file
13
crates/types/src/gatekeeper.rs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ pub mod bootinfo;
|
|||||||
pub mod bounded;
|
pub mod bounded;
|
||||||
pub mod budget;
|
pub mod budget;
|
||||||
pub mod display;
|
pub mod display;
|
||||||
|
pub mod gatekeeper;
|
||||||
pub mod generation;
|
pub mod generation;
|
||||||
pub mod id;
|
pub mod id;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ pub mod op {
|
|||||||
/// Takes back the grant this is called through, and everything
|
/// Takes back the grant this is called through, and everything
|
||||||
/// granted from it, from every holder.
|
/// granted from it, from every holder.
|
||||||
pub const RELEASE: usize = 12;
|
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).
|
/// Reply tags (word 0).
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
/// the line's last chunk ends with `\n`. Blocks until a line is entered.
|
/// the line's last chunk ends with `\n`. Blocks until a line is entered.
|
||||||
pub const READ: usize = 10;
|
pub const READ: usize = 10;
|
||||||
pub const CLEAR: usize = 11;
|
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
|
/// Repaints the whole screen and takes input focus back, after a program
|
||||||
/// that drew on the screen itself.
|
/// that drew on the screen itself.
|
||||||
pub const REDRAW: usize = 12;
|
pub const REDRAW: usize = 12;
|
||||||
|
|||||||
11
docs/ipc.md
11
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
|
its log endpoint from init later (`SET_LOG`, accepted only from the
|
||||||
unbadged holder of its endpoint).
|
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
|
A program's `uses` endpoints arrive as badged copies with `Execute` and
|
||||||
`Delegate`: the badge (unique per copy) tells the server which client is
|
`Delegate`: the badge (unique per copy) tells the server which client is
|
||||||
calling, and `Delegate` lets the client send capabilities with its calls
|
calling, and `Delegate` lets the client send capabilities with its calls
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ copy permission-manager permission
|
|||||||
copy session-server session
|
copy session-server session
|
||||||
copy terminal-server terminal
|
copy terminal-server terminal
|
||||||
copy shell shell
|
copy shell shell
|
||||||
|
copy gatekeeper gatekeeper
|
||||||
# Apps.
|
# Apps.
|
||||||
app cat
|
app cat
|
||||||
app ls
|
app ls
|
||||||
|
|||||||
@@ -1,20 +1,58 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Boots IMAGE headless and waits for the self-test (init's SMP checks, then
|
# 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
|
# spawntest) to report; the machine halts itself when done. Twice, on one
|
||||||
# "SMPTEST PASS".
|
# 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
|
set -uo pipefail
|
||||||
|
|
||||||
IMAGE="${1:?usage: test-qemu.sh IMAGE}"
|
IMAGE="${1:?usage: test-qemu.sh IMAGE}"
|
||||||
SMP="${SMP:-4}"
|
SMP="${SMP:-4}"
|
||||||
KERNEL="target/riscv64gc-unknown-none-elf/release/adapter-riscv"
|
KERNEL="target/riscv64gc-unknown-none-elf/release/adapter-riscv"
|
||||||
LOG="$(mktemp -t kos-test-qemu.XXXXXX)"
|
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 \
|
# Types `lines` (one per argument) at the gatekeeper once it waits.
|
||||||
-kernel "${KERNEL}" \
|
owner() {
|
||||||
-drive "file=${IMAGE},if=none,format=raw,id=hd0" -device virtio-blk-device,drive=hd0 \
|
for _ in $(seq 300); do grep -aq "gatekeeper: waiting" "${LOG}" 2>/dev/null && break; sleep 0.2; done
|
||||||
-device virtio-gpu-device -device virtio-keyboard-device \
|
for line in "$@"; do
|
||||||
-display none -serial "file:${LOG}" -monitor none
|
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=$?
|
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}"
|
grep -a "smptest:\|spawntest:\|Hello\|SMPTEST" "${LOG}"
|
||||||
# What k-os wrote must be ext4 that the real tools accept.
|
# 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
|
fi
|
||||||
rm -f "${LOG}.fsck"
|
rm -f "${LOG}.fsck"
|
||||||
fi
|
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)"
|
echo "test-qemu: pass (${SMP} harts)"
|
||||||
rm -f "${LOG}"
|
rm -f "${LOG}"
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
Reference in New Issue
Block a user