cubelinux: cube(2) walks the store — CUBE_OP_ENUM and CUBE_OP_SPACES
Enumeration was the one operation the interface did not have, and the one a capability interface owes an explanation for: a listing is the opposite of "knowing a coordinate is the authorisation to use it". So the walk is bounded and explicit. A caller names the space, holds a cursor, and gets as many whole records as fit in the buffer it offered, packed `key(24) | value_len(u32) | value` in the store's own order. SPACES walks the distinct spaces that hold a record, one per call; range is that walk with the region as a filter, applied by the caller rather than by a second operation in the kernel. Its own argument block, versioned by its own size: SYSCALL_DEFINE2 peeks `size` and routes — 80 bytes is the coordinate block, 64 is this one. An interface that cannot grow has to be replaced, and this one grows by being given a new block. The end of a walk is the cursor alone. A batch holds as many whole records as fit, so it is FULL only when a record lands on the boundary and most batches come back short; a caller that reads "the buffer was not filled" as "the space is exhausted" truncates its listing to the first batch and cannot tell. ENUM answers a finished walk with no records and the cursor unmoved; SPACES answers it with -ENOENT, because the space after the last one is not there. That rule is in the uapi header because it is a contract detail, not an implementation one. The kernel caches no merged index: each call walks the records in order, asks the log — small, and the only thing that can override one — what the winner is, and skips what the cursor has already covered. O(records) a call, tens of milliseconds for the live store, which is worth more than a cache every write would have to invalidate.
This commit is contained in:
+241
-13
@@ -75,13 +75,44 @@ const SPACE_ID_LEN: usize = 32;
|
||||
const RAW_KEY_LEN: usize = 24;
|
||||
const RECORD_FIXED: usize = SPACE_ID_LEN + RAW_KEY_LEN + 8;
|
||||
|
||||
/// The device to read. A module parameter is the obvious next step; for the boot gate one
|
||||
/// fixed name is honest and has one less way to be wrong.
|
||||
const STORE_DEVICE: &core::ffi::CStr = c_str!("/dev/vda");
|
||||
// The device the store lives on, as the `cube_store=` boot parameter resolved it.
|
||||
//
|
||||
// This was a constant — *"for the boot gate one fixed name is honest and has one less way to
|
||||
// be wrong"* — and that was true while a virtual machine was the only place this driver ran.
|
||||
// It runs on the box now, the box's device is not `/dev/vda`, and a constant cannot be both.
|
||||
// So the box names its device on the kernel command line:
|
||||
//
|
||||
// cube_store=/dev/nvme0n1p2
|
||||
//
|
||||
// The parameter is declared in C (`cube_syscall.c`) because this kernel's Rust can express only
|
||||
// *integer* module parameters — `rust/kernel/module_param.rs` implements `ModuleParam` through
|
||||
// `ParseInt` and nothing else — and a path is not an integer. It is a `__setup` parameter rather
|
||||
// than a module parameter for the naming reason recorded in that file.
|
||||
//
|
||||
// (A `///` here would be an unused doc comment: it would attach to the `extern` block, which
|
||||
// documents nothing.)
|
||||
extern "C" {
|
||||
/// Returns a pointer to a static, NUL-terminated buffer holding the path.
|
||||
fn cubelinux_store_device() -> *const core::ffi::c_char;
|
||||
}
|
||||
|
||||
/// Refuse to pull an unbounded device into memory. The gate's images are tiny; this is the
|
||||
/// guard against a wrong device name turning a read into an allocation storm.
|
||||
const MAX_BYTES: usize = 64 * 1024 * 1024;
|
||||
/// The store's path, NUL-terminated, owned by the C side for the life of the kernel.
|
||||
fn store_device() -> *const u8 {
|
||||
// SAFETY: `cubelinux_store_device` returns a pointer to a static buffer that the module
|
||||
// parameter filled during boot and that nothing writes afterwards, so it stays valid and
|
||||
// NUL-terminated for as long as this runs.
|
||||
unsafe { cubelinux_store_device().cast::<u8>() }
|
||||
}
|
||||
|
||||
/// Refuse to pull an unbounded device into memory: the guard against a wrong device name
|
||||
/// turning a read into an allocation storm. It is sized for a *store device*, not for an image.
|
||||
///
|
||||
/// `Control::layout` gives each of the two slots a quarter of the device, so a device must be at
|
||||
/// least four times the image it holds — and a *sealed* image is larger than the plaintext one,
|
||||
/// because every record gains an envelope. A sealed 18.8 MiB store therefore needs a ~76 MiB
|
||||
/// device, which the original 64 MiB cap refused. 128 MiB leaves room for both to grow while
|
||||
/// still being a bound: pointing this at a whole 119 GiB disk is still refused.
|
||||
const MAX_BYTES: usize = 128 * 1024 * 1024;
|
||||
|
||||
/// The log's own magic, distinct from the image's so a reader that opens the wrong one
|
||||
/// cannot mistake it for the other.
|
||||
@@ -170,9 +201,10 @@ fn fnv1a64(bytes: &[u8], mut h: u64) -> u64 {
|
||||
/// filesystem, say), that is the moment to reach for the block layer directly.
|
||||
fn read_image(image: &mut KVVec<u8>) -> Result<()> {
|
||||
// O_RDONLY is 0 in Linux; filp_open takes the raw flags word.
|
||||
// SAFETY: STORE_DEVICE is a NUL-terminated C string literal, and filp_open either
|
||||
// returns a valid `struct file *` or an error pointer, which is checked below.
|
||||
let file = unsafe { bindings::filp_open(STORE_DEVICE.as_ptr().cast::<u8>(), 0, 0) };
|
||||
// SAFETY: store_device() is a NUL-terminated C string that the module parameter filled at
|
||||
// boot, and filp_open either returns a valid `struct file *` or an error pointer, which is
|
||||
// checked below.
|
||||
let file = unsafe { bindings::filp_open(store_device(), 0, 0) };
|
||||
let file = kernel::error::from_err_ptr(file)?;
|
||||
if file.is_null() {
|
||||
return Err(EINVAL);
|
||||
@@ -1017,9 +1049,9 @@ fn append(
|
||||
return Err(ENOSPC);
|
||||
}
|
||||
|
||||
// SAFETY: STORE_DEVICE is a NUL-terminated literal; filp_open returns a valid file or an
|
||||
// error pointer, which is checked. O_RDWR is 2.
|
||||
let file = unsafe { bindings::filp_open(STORE_DEVICE.as_ptr().cast::<u8>(), 2, 0) };
|
||||
// SAFETY: store_device() is a NUL-terminated C string filled at boot; filp_open returns a
|
||||
// valid file or an error pointer, which is checked. O_RDWR is 2.
|
||||
let file = unsafe { bindings::filp_open(store_device(), 2, 0) };
|
||||
let file = kernel::error::from_err_ptr(file)?;
|
||||
if file.is_null() {
|
||||
return Err(EINVAL);
|
||||
@@ -1134,7 +1166,7 @@ fn checkpoint(ctl: &Control, merged: &mut Merged) -> Result<u64, Error> {
|
||||
}
|
||||
|
||||
// SAFETY: as in `append`.
|
||||
let file = unsafe { bindings::filp_open(STORE_DEVICE.as_ptr().cast::<u8>(), 2, 0) };
|
||||
let file = unsafe { bindings::filp_open(store_device(), 2, 0) };
|
||||
let file = kernel::error::from_err_ptr(file)?;
|
||||
if file.is_null() {
|
||||
return Err(EINVAL);
|
||||
@@ -1295,6 +1327,202 @@ pub unsafe extern "C" fn cubelinux_kernel_get(
|
||||
}
|
||||
}
|
||||
|
||||
/// Pack one record the way the walk's uapi names it: `key(24) | value_len(u32, LE) | value`.
|
||||
///
|
||||
/// The space is not in the frame because the caller named it, and the order is the store's own
|
||||
/// (space, then key), so a listing taken through the kernel and one taken in userspace are
|
||||
/// byte-for-byte comparable — which is how this is tested.
|
||||
fn pack_record(key: &[u8], value: &[u8], out: &mut [u8], at: usize) -> Option<usize> {
|
||||
let need = RAW_KEY_LEN + 4 + value.len();
|
||||
if at + need > out.len() {
|
||||
return None;
|
||||
}
|
||||
out[at..at + RAW_KEY_LEN].copy_from_slice(&key[..RAW_KEY_LEN]);
|
||||
out[at + RAW_KEY_LEN..at + RAW_KEY_LEN + 4]
|
||||
.copy_from_slice(&(value.len() as u32).to_le_bytes());
|
||||
out[at + RAW_KEY_LEN + 4..at + need].copy_from_slice(value);
|
||||
Some(need)
|
||||
}
|
||||
|
||||
/// `CUBE_OP_ENUM`: walk the records of one space into the caller's buffer, `cursor` records in.
|
||||
///
|
||||
/// The cursor is a COUNT OF RECORDS ALREADY RETURNED, not a position in the image. It is opaque to
|
||||
/// the caller (pass back what you were given) and it survives an append, because the contract is
|
||||
/// "the records I had not yet seen" rather than a snapshot. A record can be seen twice if the
|
||||
/// image is rewritten underneath a walk; a caller that needs a snapshot takes one.
|
||||
///
|
||||
/// The walk is O(records) per call, deliberately. The alternative is a merged index held in the
|
||||
/// kernel and invalidated by every write — more to be wrong about than a listing is worth, and the
|
||||
/// live store's 69,636 records make a page of listing tens of milliseconds.
|
||||
///
|
||||
/// `out_len` receives the bytes written, or with -ERANGE the size the first record that did not fit
|
||||
/// would need — exactly as a read reports the size it wants.
|
||||
///
|
||||
/// # Safety
|
||||
/// `space` must point to 32 readable bytes; `buf` must hold `cap` writable bytes; `out_len` and
|
||||
/// `out_cursor` must each point to a writable `u64`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn cubelinux_kernel_enum(
|
||||
space: *const u8,
|
||||
cursor: u64,
|
||||
buf: *mut u8,
|
||||
cap: usize,
|
||||
out_len: *mut u64,
|
||||
out_cursor: *mut u64,
|
||||
) -> i32 {
|
||||
let mut wanted = [0u8; SPACE_ID_LEN];
|
||||
// SAFETY: the caller guarantees 32 readable bytes at `space`.
|
||||
unsafe { core::ptr::copy_nonoverlapping(space, wanted.as_mut_ptr(), SPACE_ID_LEN) };
|
||||
|
||||
let (device, layout) = match device_and_layout() {
|
||||
Ok(pair) => pair,
|
||||
Err(e) => return -(e.to_errno() as i32),
|
||||
};
|
||||
let live = &device[layout.image_off..];
|
||||
let header = match parse_header(live) {
|
||||
Ok(h) => h,
|
||||
Err(what) => {
|
||||
pr_err!("cubelinux: {}\n", what);
|
||||
return -22; // -EINVAL
|
||||
}
|
||||
};
|
||||
let window = log_window(&device, &layout);
|
||||
let end = core::cmp::min(
|
||||
header.image_bytes.unwrap_or(live.len() as u64) as usize,
|
||||
live.len(),
|
||||
);
|
||||
let (mut merged, _applied) = match build_merged(&live[..end], window, &header) {
|
||||
Ok(m) => m,
|
||||
Err(what) => {
|
||||
pr_err!("cubelinux: cannot build the store: {}\n", what);
|
||||
return -22;
|
||||
}
|
||||
};
|
||||
merged.sort_entries();
|
||||
let entries = merged.entries.as_slice();
|
||||
// SAFETY: the shim guarantees `cap` writable bytes at `buf`.
|
||||
let out = unsafe { core::slice::from_raw_parts_mut(buf, cap) };
|
||||
|
||||
let mut written = 0usize;
|
||||
let mut returned: u64 = 0;
|
||||
let mut seen: u64 = 0;
|
||||
let mut first_too_big: usize = 0;
|
||||
|
||||
let mut i = 0usize;
|
||||
while i < entries.len() {
|
||||
let head = &entries[i];
|
||||
// The winner of a coordinate is the LAST of its group: the newest write, or a removal.
|
||||
let mut last = i;
|
||||
while last + 1 < entries.len()
|
||||
&& entries[last + 1].space == head.space
|
||||
&& entries[last + 1].key == head.key
|
||||
{
|
||||
last += 1;
|
||||
}
|
||||
let winner = &entries[last];
|
||||
i = last + 1;
|
||||
if winner.deleted || winner.space != wanted {
|
||||
continue;
|
||||
}
|
||||
seen += 1;
|
||||
if seen <= cursor {
|
||||
continue;
|
||||
}
|
||||
match pack_record(&winner.key, merged.value(winner), out, written) {
|
||||
Some(n) => {
|
||||
written += n;
|
||||
returned += 1;
|
||||
}
|
||||
None => {
|
||||
first_too_big = RAW_KEY_LEN + 4 + winner.value_len as usize;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY: both out-pointers are writable under this function's contract.
|
||||
unsafe {
|
||||
if first_too_big > 0 && written == 0 {
|
||||
// Not one whole record fits. Say how much it needs, the way a read does.
|
||||
*out_len = first_too_big as u64;
|
||||
*out_cursor = cursor;
|
||||
return -34; // -ERANGE
|
||||
}
|
||||
*out_len = written as u64;
|
||||
*out_cursor = cursor + returned;
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
/// `CUBE_OP_SPACES`: the `cursor`-th distinct space that holds a record, or -ENOENT at the end.
|
||||
///
|
||||
/// An index rather than a count of records: a caller that wants a space's records walks it with
|
||||
/// `CUBE_OP_ENUM` once it has learned the name. Entries are already in (space, key) order, so the
|
||||
/// distinct spaces come out sorted.
|
||||
///
|
||||
/// # Safety
|
||||
/// `space_out` must point to 32 writable bytes.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn cubelinux_kernel_spaces(cursor: u64, space_out: *mut u8) -> i32 {
|
||||
let (device, layout) = match device_and_layout() {
|
||||
Ok(pair) => pair,
|
||||
Err(e) => return -(e.to_errno() as i32),
|
||||
};
|
||||
let live = &device[layout.image_off..];
|
||||
let header = match parse_header(live) {
|
||||
Ok(h) => h,
|
||||
Err(what) => {
|
||||
pr_err!("cubelinux: {}\n", what);
|
||||
return -22;
|
||||
}
|
||||
};
|
||||
let window = log_window(&device, &layout);
|
||||
let end = core::cmp::min(
|
||||
header.image_bytes.unwrap_or(live.len() as u64) as usize,
|
||||
live.len(),
|
||||
);
|
||||
let (mut merged, _applied) = match build_merged(&live[..end], window, &header) {
|
||||
Ok(m) => m,
|
||||
Err(what) => {
|
||||
pr_err!("cubelinux: cannot build the store: {}\n", what);
|
||||
return -22;
|
||||
}
|
||||
};
|
||||
merged.sort_entries();
|
||||
let entries = merged.entries.as_slice();
|
||||
|
||||
let mut index: u64 = 0;
|
||||
let mut previous: Option<[u8; SPACE_ID_LEN]> = None;
|
||||
let mut i = 0usize;
|
||||
while i < entries.len() {
|
||||
let head = &entries[i];
|
||||
let mut last = i;
|
||||
while last + 1 < entries.len()
|
||||
&& entries[last + 1].space == head.space
|
||||
&& entries[last + 1].key == head.key
|
||||
{
|
||||
last += 1;
|
||||
}
|
||||
let winner = &entries[last];
|
||||
i = last + 1;
|
||||
if winner.deleted {
|
||||
continue;
|
||||
}
|
||||
if previous != Some(winner.space) {
|
||||
if index == cursor {
|
||||
// SAFETY: the caller guarantees 32 writable bytes at `space_out`.
|
||||
unsafe {
|
||||
core::ptr::copy_nonoverlapping(winner.space.as_ptr(), space_out, SPACE_ID_LEN);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
index += 1;
|
||||
previous = Some(winner.space);
|
||||
}
|
||||
}
|
||||
-2 // -ENOENT: no such space; the walk is finished
|
||||
}
|
||||
|
||||
/// `CUBE_OP_DEL`: remove the record at a coordinate.
|
||||
///
|
||||
/// A removal is a log entry, not an erasure: nothing in an append-only store is rewritten in
|
||||
|
||||
Reference in New Issue
Block a user