Merge pull request #246 from hexagonal-sun/virtio-rng

virtio rng
This commit is contained in:
Matthew Leach
2026-03-07 07:37:08 +00:00
committed by GitHub
12 changed files with 440 additions and 40 deletions

69
Cargo.lock generated
View File

@@ -58,6 +58,24 @@ version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "blake2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
dependencies = [
"digest",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "byteorder"
version = "1.5.0"
@@ -126,6 +144,16 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "defmt"
version = "0.3.100"
@@ -176,6 +204,17 @@ dependencies = [
"powerfmt",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
name = "embedded-io"
version = "0.6.1"
@@ -305,6 +344,16 @@ dependencies = [
"slab",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getargs"
version = "0.5.0"
@@ -492,6 +541,8 @@ dependencies = [
"arm-pl011-uart",
"async-trait",
"bitflags 2.11.0",
"blake2",
"chacha20",
"fdt-parser",
"futures",
"getargs",
@@ -815,6 +866,12 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.117"
@@ -911,6 +968,12 @@ dependencies = [
"syn",
]
[[package]]
name = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "unicode-ident"
version = "1.0.24"
@@ -933,6 +996,12 @@ dependencies = [
"parking_lot",
]
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "virtio-drivers"
version = "0.12.0"

View File

@@ -42,6 +42,8 @@ rustc-hash = { version = "2.1", default-features = false }
smoltcp = { version = "0.12.0", default-features = false, features = ["alloc", "medium-ethernet", "medium-ip", "proto-ipv4", "proto-ipv6", "socket-tcp", "socket-udp"] }
tock-registers = "0.10.1"
virtio-drivers = "0.12.0"
blake2 = { version = "0.10.6", default-features = false }
chacha20 = { version = "0.10.0", default-features = false, features = ["rng"] }
[build-dependencies]
time = { version = "0.3.47", features = ["formatting", "macros"] } # For build timestamping via build.rs

View File

@@ -23,6 +23,10 @@ pub enum ProbeError {
// Driver probing should be tried again after other probes have succeeded.
#[error("Driver probing deferred for other dependencies")]
Deferred,
// Device inspected but not a match for this driver; skip silently.
#[error("Device not matched by driver")]
NoMatch,
}
#[derive(Debug, Error, PartialEq, Eq, Clone)]

View File

@@ -41,9 +41,12 @@ default_args = {
"-nographic": None,
"-s": None,
"-kernel": bin_executable_location,
"-append": f"{append_args} --rootfs=ext4fs --automount=/dev,devfs --automount=/tmp,tmpfs --automount=/proc,procfs --automount=/sys,sysfs"
"-append": f"{append_args} --rootfs=ext4fs --automount=/dev,devfs --automount=/tmp,tmpfs --automount=/proc,procfs --automount=/sys,sysfs",
}
# Arguments that can appear multiple times (e.g. -device)
extra_args = ["-device", "virtio-rng-device"]
if args.debug:
default_args["-S"] = None
@@ -53,7 +56,7 @@ if args.display:
default_args["-nic"] = "none"
# Add uart
default_args["-serial"] = "stdio"
default_args["-device"] = "virtio-gpu-device"
extra_args += ["-device", "virtio-gpu-device"]
qemu_command = ["qemu-system-aarch64"]
@@ -62,4 +65,6 @@ for key, value in default_args.items():
if value is not None:
qemu_command.append(value)
qemu_command += extra_args
subprocess.run(qemu_command, check=True)

View File

@@ -3,6 +3,7 @@ use super::meta::VSUSP;
use super::{TtyInputHandler, meta::*};
use crate::console::Console;
use crate::kernel::kpipe::KPipe;
use crate::kernel::rand::entropy_pool;
use crate::process::thread_group::Pgid;
use crate::process::thread_group::signal::SigId;
use crate::process::thread_group::signal::kill::send_signal_to_pg;
@@ -52,6 +53,11 @@ impl TtyInputHandler for SpinLock<TtyInputCooker> {
..
} = &mut *this;
// Seed the entropy pool.
//
// SAFETY: A console interrupt isn't periodic.
entropy_pool().add_temporal_entropy();
// Handle signal-generating control characters
if termios.c_lflag.contains(TermiosLocalFlags::ISIG) {
let intr_char = termios.c_cc[VINTR];

View File

@@ -7,10 +7,11 @@ use crate::{
fops::FileOps,
open_file::{FileCtx, OpenFile},
},
kernel::rand::sys_getrandom,
kernel::rand::fill_random_bytes,
kernel_driver,
memory::uaccess::copy_to_user_slice,
};
use alloc::{boxed::Box, string::ToString, sync::Arc};
use alloc::{boxed::Box, string::ToString, sync::Arc, vec};
use async_trait::async_trait;
use core::{future::Future, pin::Pin};
use libkernel::{
@@ -34,7 +35,12 @@ impl FileOps for RandomFileOps {
}
async fn readat(&mut self, buf: UA, count: usize, _offset: u64) -> Result<usize> {
sys_getrandom(buf.cast(), count as _, 0).await
// TODO: Add an implementation of `/dev/urandom` which doesn't block if
// the entropy pool hasn't yet been seeded.
let mut kbuf = vec![0u8; count];
fill_random_bytes(&mut kbuf).await;
copy_to_user_slice(&kbuf, buf).await?;
Ok(count)
}
fn poll_read_ready(&self) -> Pin<Box<dyn Future<Output = Result<()>> + Send>> {

View File

@@ -127,15 +127,15 @@ fn virtio_gpu_probe(_dm: &mut DriverManager, d: DeviceDescriptor) -> Result<Arc<
// Construct the transport first; it can report its device type.
let transport = unsafe {
MmioTransport::new(header, size).map_err(|e| {
log::error!("Failed to initialize virtio-mmio transport: {e}");
KernelError::Other("virtio-mmio transport init failed")
})?
match MmioTransport::new(header, size) {
Ok(t) => t,
Err(_) => return Err(KernelError::Probe(ProbeError::NoMatch)),
}
};
// Only bind to GPU here; other virtio-mmio devices should be handled by their own drivers.
if !matches!(transport.device_type(), DeviceType::GPU) {
return Err(KernelError::Probe(ProbeError::Deferred));
return Err(KernelError::Probe(ProbeError::NoMatch));
}
if mapped != VA::from_value(0xffffd00000f92e00) {

View File

@@ -73,6 +73,9 @@ pub fn probe_for_fdt_devices() {
Err(KernelError::Probe(ProbeError::Deferred)) => {
deferred_list.push(desc);
}
Err(KernelError::Probe(ProbeError::NoMatch)) => {
// Driver inspected the device but it's not a match; skip silently.
}
Ok(None) => {
// No driver found for this compatible string. Not an error, just ignore.
}

View File

@@ -27,6 +27,7 @@ pub mod fs;
pub mod init;
pub mod interrupts;
pub mod probe;
pub mod rng;
pub mod timer;
pub mod uart;
mod virtio_hal;

1
src/drivers/rng/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod virtio;

115
src/drivers/rng/virtio.rs Normal file
View File

@@ -0,0 +1,115 @@
use crate::drivers::virtio_hal::VirtioHal;
use crate::sync::SpinLock;
use crate::{
arch::ArchImpl,
drivers::{
Driver, DriverManager,
init::PlatformBus,
probe::{DeviceDescriptor, DeviceMatchType},
},
kernel::rand::{EntropySource, register_entropy_source},
kernel_driver,
};
use alloc::{boxed::Box, sync::Arc};
use core::ptr::NonNull;
use libkernel::{
KernAddressSpace, VirtualMemory,
error::{KernelError, ProbeError, Result},
memory::{
address::{PA, VA},
region::PhysMemoryRegion,
},
};
use log::info;
use virtio_drivers::{
device::rng::VirtIORng,
transport::{
DeviceType, Transport,
mmio::{MmioTransport, VirtIOHeader},
},
};
pub struct VirtioRngDriver {
rng: SpinLock<VirtIORng<VirtioHal, MmioTransport<'static>>>,
}
impl Driver for VirtioRngDriver {
fn name(&self) -> &'static str {
"virtio-rng"
}
}
impl EntropySource for VirtioRngDriver {
fn get_entropy(&self, buf: &mut [u8]) -> (usize, usize) {
let mut rng = self.rng.lock_save_irq();
match rng.request_entropy(buf) {
Ok(n) => (n, n * 8),
Err(_) => (0, 0),
}
}
}
fn virtio_rng_probe(_dm: &mut DriverManager, d: DeviceDescriptor) -> Result<Arc<dyn Driver>> {
match d {
DeviceDescriptor::Fdt(fdt_node, _flags) => {
let region = fdt_node
.reg()
.ok_or(ProbeError::NoReg)?
.next()
.ok_or(ProbeError::NoReg)?;
let size = region.size.ok_or(ProbeError::NoRegSize)?;
let mapped: VA =
ArchImpl::kern_address_space()
.lock_save_irq()
.map_mmio(PhysMemoryRegion::new(
PA::from_value(region.address as usize),
size,
))?;
let header = NonNull::new(mapped.value() as *mut VirtIOHeader)
.ok_or(KernelError::InvalidValue)?;
let transport = unsafe {
match MmioTransport::new(header, size) {
Ok(t) => t,
Err(_) => return Err(KernelError::Probe(ProbeError::NoMatch)),
}
};
if !matches!(transport.device_type(), DeviceType::EntropySource) {
return Err(KernelError::Probe(ProbeError::NoMatch));
}
info!("virtio-rng found (node {})", fdt_node.name);
let rng = VirtIORng::<VirtioHal, _>::new(transport)
.map_err(|_| KernelError::Other("virtio-rng init failed"))?;
let driver = Arc::new(VirtioRngDriver {
rng: SpinLock::new(rng),
});
register_entropy_source(driver.clone());
Ok(driver)
}
}
}
fn virtio_rng_init(bus: &mut PlatformBus, _dm: &mut DriverManager) -> Result<()> {
bus.register_platform_driver(
DeviceMatchType::FdtCompatible("virtio,mmio"),
Box::new(virtio_rng_probe),
);
bus.register_platform_driver(
DeviceMatchType::FdtCompatible("virtio-mmio"),
Box::new(virtio_rng_probe),
);
Ok(())
}
kernel_driver!(virtio_rng_init);

View File

@@ -1,39 +1,227 @@
// TODO: generate a pool of entropy.
use core::sync::atomic::{AtomicUsize, Ordering};
use alloc::{sync::Arc, vec::Vec};
use crate::{
drivers::timer::uptime,
memory::uaccess::copy_to_user_slice,
sync::{OnceLock, SpinLock},
per_cpu_private,
sync::{CondVar, OnceLock, SpinLock},
};
use alloc::vec::Vec;
use libkernel::error::Result;
use blake2::{Blake2s256, Digest};
use chacha20::ChaCha20Rng;
use libkernel::memory::address::TUA;
use rand::{Rng, SeedableRng, rngs::SmallRng};
use libkernel::{error::Result, sync::condvar::WakeupType};
use rand::{Rng, SeedableRng};
pub async fn sys_getrandom(ubuf: TUA<u8>, size: isize, _flags: u32) -> Result<usize> {
let buf = {
let mut rng = ENTROPY_POOL
.get_or_init(|| {
let now = uptime();
SpinLock::new(SmallRng::seed_from_u64(
(now.as_micros() & 0xffffffff_ffffffff) as u64,
))
})
.lock_save_irq();
let mut buf = Vec::with_capacity(size as usize);
for _ in 0..size {
buf.push((rng.next_u32() & 0xff) as u8);
}
buf
};
copy_to_user_slice(&buf, ubuf.to_untyped()).await?;
Ok(size as _)
/// A hardware or software source of entropy that the pool can query.
pub trait EntropySource: Send + Sync {
/// Pull up to `buf.len()` bytes of entropy from this source.
/// Returns `(bytes_written, approx_entropy_bits)`.
fn get_entropy(&self, buf: &mut [u8]) -> (usize, usize);
}
static ENTROPY_POOL: OnceLock<SpinLock<SmallRng>> = OnceLock::new();
/// Number of bytes generated before a per-CPU RNG reseeds from the entropy pool.
const RESEED_BYTES: usize = 1024 * 1024;
pub struct EntropyPool {
state: SpinLock<Blake2s256>,
pool_waiters: CondVar<bool>,
pool_bits: AtomicUsize,
sources: SpinLock<Vec<Arc<dyn EntropySource>>>,
}
impl EntropyPool {
fn new() -> Self {
Self {
state: SpinLock::new(Blake2s256::default()),
pool_waiters: CondVar::new(false),
pool_bits: AtomicUsize::new(0),
sources: SpinLock::new(Vec::new()),
}
}
pub fn add_entropy(&self, entropy: &[u8], approx_bits: usize) {
self.state.lock_save_irq().update(entropy);
let old_val = self.pool_bits.fetch_add(approx_bits, Ordering::Relaxed);
if old_val + approx_bits >= 256 && old_val < 256 {
// Pool was initialised. Wake up any waiters.
self.pool_waiters.update(|s| {
*s = true;
WakeupType::All
});
}
}
/// Add entropy into this pool based upon the uptime of the system.
///
/// WARNING: This method should *not* be called for predictable or periodic
/// events.
pub fn add_temporal_entropy(&self) {
let uptime = uptime();
self.add_entropy(
&uptime.subsec_micros().to_le_bytes(),
1_000_000_usize.ilog2() as usize,
);
}
/// Poll all registered entropy sources and feed their output into the pool.
fn poll_sources(&self) {
let sources = self.sources.lock_save_irq();
let mut buf = [0u8; 64];
for source in sources.iter() {
let (written, bits) = source.get_entropy(&mut buf);
if written > 0 && bits > 0 {
self.add_entropy(&buf[..written], bits);
}
}
}
/// Block until the pool has accumulated at least 256 bits of entropy, then
/// return a 32-byte seed derived from the pool state.
pub async fn extract_seed(&self) -> [u8; 32] {
self.poll_sources();
self.pool_waiters
.wait_until(|s| if *s { Some(()) } else { None })
.await;
self.poll_sources();
self.extract_seed_inner()
}
/// Non-blocking seed extraction. Returns `None` if the pool has not yet
/// accumulated 256 bits of entropy.
pub fn try_extract_seed(&self) -> Option<[u8; 32]> {
self.poll_sources();
if self.pool_bits.load(Ordering::Relaxed) < 256 {
return None;
}
Some(self.extract_seed_inner())
}
fn extract_seed_inner(&self) -> [u8; 32] {
let mut state = self.state.lock_save_irq();
let hash = (*state).clone().finalize();
let mut seed = [0u8; 32];
seed.copy_from_slice(&hash);
// Feed the extracted hash back so the pool state diverges from the
// output (forward secrecy).
state.update(seed);
seed
}
}
pub fn entropy_pool() -> &'static EntropyPool {
ENTROPY_POOL.get_or_init(EntropyPool::new)
}
/// Register a new entropy source that the pool can pull from.
pub fn register_entropy_source(source: Arc<dyn EntropySource>) {
entropy_pool().sources.lock_save_irq().push(source);
}
static ENTROPY_POOL: OnceLock<EntropyPool> = OnceLock::new();
struct CpuRng {
rng: ChaCha20Rng,
seeded: bool,
bytes_since_reseed: usize,
}
unsafe impl Send for CpuRng {}
impl CpuRng {
fn new() -> Self {
Self {
rng: ChaCha20Rng::from_seed([0u8; 32]),
seeded: false,
bytes_since_reseed: 0,
}
}
fn apply_seed(&mut self, seed: [u8; 32]) {
self.rng = ChaCha20Rng::from_seed(seed);
self.seeded = true;
self.bytes_since_reseed = 0;
}
/// Reseed by XOR-ing a fresh BLAKE2 seed with 32 bytes of our own output,
/// then constructing a new ChaCha20 instance from the combined material.
fn reseed_with_blake(&mut self, blake_seed: [u8; 32]) {
let mut self_bytes = [0u8; 32];
self.rng.fill_bytes(&mut self_bytes);
let mut new_seed = [0u8; 32];
for i in 0..32 {
new_seed[i] = blake_seed[i] ^ self_bytes[i];
}
self.rng = ChaCha20Rng::from_seed(new_seed);
self.bytes_since_reseed = 0;
}
fn fill(&mut self, buf: &mut [u8]) {
self.rng.fill_bytes(buf);
self.bytes_since_reseed += buf.len();
}
}
per_cpu_private! {
static CPU_RNG: CpuRng = CpuRng::new;
}
/// Fill `buf` with cryptographically-strong random bytes.
///
/// On the first invocation per CPU the call blocks until the global entropy
/// pool has been seeded (>= 256 bits). Subsequent calls are non-blocking;
/// periodic reseeding from the pool happens inline.
pub async fn fill_random_bytes(buf: &mut [u8]) {
// Ensure the per-CPU RNG has been seeded at least once.
let seeded = CPU_RNG.borrow().seeded;
if !seeded {
let seed = entropy_pool().extract_seed().await;
CPU_RNG.borrow_mut().apply_seed(seed);
}
// Reseed from the entropy pool if we have generated enough bytes.
let needs_reseed = CPU_RNG.borrow().bytes_since_reseed >= RESEED_BYTES;
if needs_reseed && let Some(blake_seed) = entropy_pool().try_extract_seed() {
CPU_RNG.borrow_mut().reseed_with_blake(blake_seed);
}
CPU_RNG.borrow_mut().fill(buf);
}
const GETRANDOM_CHUNK: usize = 256;
pub async fn sys_getrandom(ubuf: TUA<u8>, size: isize, _flags: u32) -> Result<usize> {
let total = size as usize;
let mut buf = [0u8; GETRANDOM_CHUNK];
let mut offset = 0;
while offset < total {
let n = (total - offset).min(GETRANDOM_CHUNK);
let chunk = &mut buf[..n];
fill_random_bytes(chunk).await;
copy_to_user_slice(chunk, ubuf.to_untyped().add_bytes(offset)).await?;
offset += n;
}
Ok(total)
}