diff --git a/Makefile b/Makefile index 37ec68cba..de0295a1e 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ NAME = CUBELinux # release. The base version stays in VERSION/PATCHLEVEL/SUBLEVEL above (visible in # `make kernelversion`), while `uname -r` and /lib/modules report the CUBELinux # release, with any -dirty or SCM suffix still appended by setlocalversion. -CUBELINUX_VERSION = CUBELinux.0.2 +CUBELINUX_VERSION = CUBELinux.0.3 # *DOCUMENTATION* # To see a list of typical targets execute "make help" diff --git a/drivers/cube/cubelinux_store.rs b/drivers/cube/cubelinux_store.rs index f7c7d4270..db5480735 100644 --- a/drivers/cube/cubelinux_store.rs +++ b/drivers/cube/cubelinux_store.rs @@ -47,7 +47,7 @@ use kernel::{ bindings, c_str, device::Device, fs::{File, Kiocb}, - iov::IovIterDest, + iov::{IovIterDest, IovIterSource}, miscdevice::{MiscDevice, MiscDeviceOptions, MiscDeviceRegistration}, prelude::*, sync::aref::ARef, @@ -95,6 +95,10 @@ const ENTRY_FIXED: usize = 1 + 4 + 32 + 24 + 4; /// no superblock is needed to find it, and stated by the v2 header's extent. const LOG_ALIGN: usize = 4096; +/// The one mutation this device accepts so far. The coordinate interface's opcodes will be +/// the language's verbs; this is the first of them. +const OP_PUT: u8 = 1; + /// FNV-1a offset basis. const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325; @@ -588,6 +592,198 @@ fn merged_digest(image: &[u8], image_bytes: usize, header: &Header) -> Result
  • [u8; 24] { + let mut k = [0u8; 24]; + let put = |n: usize, v: bool, k: &mut [u8; 24]| { + if !v { + return; + } + let byte = 23 - (n / 8); + k[byte] |= 1 << (n % 8); + }; + for i in 0..64 { + put(3 * i, (x >> i) & 1 != 0, &mut k); + put(3 * i + 1, (y >> i) & 1 != 0, &mut k); + put(3 * i + 2, (z >> i) & 1 != 0, &mut k); + } + k +} + +/// Where the log region starts, given the image's declared extent. +fn log_offset(image_bytes: u64) -> usize { + let n = image_bytes as usize; + (n + LOG_ALIGN - 1) & !(LOG_ALIGN - 1) +} + +/// Length of the log's valid prefix: the header plus every entry that parses and passes its +/// checksum. This is the append position, and the reason a torn tail is overwritten rather +/// than appended to — the same rule the userspace log uses. +fn log_valid_len(log: &[u8]) -> Result { + if log.len() < WAL_HEADER_LEN { + return Err("short-log"); + } + if &log[0..4] != WAL_MAGIC { + return Err("no-log"); + } + if log[4] != WAL_VERSION { + return Err("unsupported-log-version"); + } + let mut off = WAL_HEADER_LEN; + while off + ENTRY_FIXED <= log.len() { + let start = off; + let op = log[off]; + if op != 1 && op != 2 { + break; + } + let mut word = [0u8; 4]; + word.copy_from_slice(&log[off + 1..off + 5]); + let crc = u32::from_le_bytes(word); + word.copy_from_slice(&log[off + 61..off + 65]); + let len = u32::from_le_bytes(word) as usize; + off += ENTRY_FIXED; + if off + len > log.len() { + break; + } + if crc32(&log[start + 5..off + len]) != crc { + break; + } + off += len; + } + Ok(off) +} + +/// A mutation to append: the byte-plane write the store contract calls `put`. +struct Mutation { + space: [u8; 32], + key: [u8; 24], + value: KVVec, +} + +/// Append a mutation to the log and make it durable. +/// +/// The order is the durability contract: the entry is written and flushed to the device +/// *before* the caller is told it happened. A mutation this function has returned `Ok` for +/// survives a power cut; one it has not may or may not, which is exactly the boundary an +/// acknowledgement is supposed to mark. +fn append(image_bytes: u64, m: &Mutation) -> Result<(), Error> { + let log_off = log_offset(image_bytes); + + // 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::(), 2, 0) }; + let file = kernel::error::from_err_ptr(file)?; + if file.is_null() { + return Err(EINVAL); + } + + let mut result = Ok(()); + // The region's header and existing entries, so the append lands after the valid prefix. + let mut head = [0u8; 4096]; + let mut pos: bindings::loff_t = log_off as bindings::loff_t; + // SAFETY: `file` is live; `head` is a 4 KiB buffer we own; `pos` is a valid loff_t. + let n = unsafe { + bindings::kernel_read( + file, + head.as_mut_ptr().cast::(), + head.len(), + &mut pos, + ) + }; + if n < 0 { + result = Err(Error::from_errno(n as i32)); + } + + if result.is_ok() { + // A log region that has never been written is zeros, not a log. Give it a header + // first — the same thing the userspace log does when its file does not exist. + let region = &head[..n as usize]; + let valid = if region.len() < WAL_HEADER_LEN || ®ion[0..4] != WAL_MAGIC { + let mut hdr = [0u8; WAL_HEADER_LEN]; + hdr[0..4].copy_from_slice(&WAL_MAGIC); + hdr[4] = WAL_VERSION; + hdr[5] = 0; // morton + let mut at: bindings::loff_t = log_off as bindings::loff_t; + // SAFETY: `file` is live and writable; `hdr` outlives the call; `at` is valid. + let wrote = unsafe { + bindings::kernel_write( + file, + hdr.as_ptr().cast::(), + hdr.len(), + &mut at, + ) + }; + if wrote < 0 { + result = Err(Error::from_errno(wrote as i32)); + } + WAL_HEADER_LEN + } else { + match log_valid_len(region) { + Ok(v) => v, + Err(what) => { + pr_err!("cubelinux: log region unreadable: {}\n", what); + result = Err(EINVAL); + WAL_HEADER_LEN + } + } + }; + if result.is_ok() { + match Ok::(valid) { + Ok(valid) => { + // Build the entry: op | crc | space | key | len | value. + let total = ENTRY_FIXED + m.value.len(); + let mut entry = KVVec::::with_capacity(total, GFP_KERNEL)?; + entry.extend_from_slice(&[1u8], GFP_KERNEL)?; + entry.extend_from_slice(&[0u8; 4][..], GFP_KERNEL)?; + entry.extend_from_slice(&m.space, GFP_KERNEL)?; + entry.extend_from_slice(&m.key, GFP_KERNEL)?; + entry.extend_from_slice(&(m.value.len() as u32).to_le_bytes(), GFP_KERNEL)?; + entry.extend_from_slice(m.value.as_slice(), GFP_KERNEL)?; + // The checksum covers everything after the crc field. + let crc = crc32(&entry.as_slice()[5..]); + entry.as_mut_slice()[1..5].copy_from_slice(&crc.to_le_bytes()); + + let mut at: bindings::loff_t = (log_off + valid) as bindings::loff_t; + // SAFETY: `file` is live and opened for writing; `entry` is a kernel buffer + // that outlives the call; `at` is a valid loff_t. + let written = unsafe { + bindings::kernel_write( + file, + entry.as_slice().as_ptr().cast::(), + entry.len(), + &mut at, + ) + }; + if written < 0 { + result = Err(Error::from_errno(written as i32)); + } else if written as usize != entry.len() { + result = Err(EIO); + } else { + // Durability before acknowledgement: a short write that was never + // flushed is a write that did not happen. + // SAFETY: `file` is live and writable; datasync is 0 (full sync). + let rc = unsafe { bindings::vfs_fsync(file, 0) }; + if rc < 0 { + result = Err(Error::from_errno(rc)); + } + } + } + Err(what) => { + pr_err!("cubelinux: log region unreadable: {}\n", what); + result = Err(EINVAL); + } + } + } + } + + // SAFETY: `file` came from filp_open and has not been closed. + unsafe { bindings::filp_close(file, core::ptr::null_mut()) }; + result +} + /// The module's registration; holds the misc device for as long as the module lives. #[pin_data] struct CubeStoreModule { @@ -624,6 +820,82 @@ impl MiscDevice for CubeStore { ) } + /// Write one mutation, framed as the argument block the coordinate interface would + /// pass: `op(1) | space(32) | x(8) | y(8) | z(8) | len(4) | value[len]`, little-endian. + /// + /// This is the byte plane — a `put` of bytes at a coordinate, with no header written + /// beside them — which is what the store contract defines and what the differential + /// gate compares against userspace's `cell put`. The header tier sits above this. + fn write_iter(kiocb: Kiocb<'_, Self::Ptr>, iov: &mut IovIterSource<'_>) -> Result { + let mut buf = KVVec::::new(); + let len = iov.copy_from_iter_vec(&mut buf, GFP_KERNEL)?; + let bytes = buf.as_slice(); + if bytes.len() < 1 + 32 + 24 + 4 { + return Err(EINVAL); + } + if bytes[0] != OP_PUT { + return Err(EINVAL); + } + let mut space = [0u8; 32]; + space.copy_from_slice(&bytes[1..33]); + let axis = |at: usize| -> u64 { + let mut w = [0u8; 8]; + w.copy_from_slice(&bytes[at..at + 8]); + u64::from_le_bytes(w) + }; + let (x, y, z) = (axis(33), axis(41), axis(49)); + let mut w = [0u8; 4]; + w.copy_from_slice(&bytes[57..61]); + let value_len = u32::from_le_bytes(w) as usize; + if bytes.len() < 61 + value_len { + return Err(EINVAL); + } + + let mut value = KVVec::::new(); + value.extend_from_slice(&bytes[61..61 + value_len], GFP_KERNEL)?; + let mutation = Mutation { + space, + key: morton_encode(x, y, z), + value, + }; + + // The image's extent says where the log begins. Reading it is not optional: the log + // has no fixed place of its own, on purpose — no superblock to keep in step. + let mut head = KVVec::::new(); + read_image(&mut head)?; + let header = match parse_header(&head) { + Ok(h) => h, + Err(what) => { + pr_err!("cubelinux: cannot append: {}\n", what); + return Err(EINVAL); + } + }; + let image_bytes = match header.image_bytes { + Some(n) => n, + None => { + pr_err!("cubelinux: cannot append to a v1 store (no extent for the log)\n"); + return Err(EINVAL); + } + }; + + let me = kiocb.file(); + match append(image_bytes, &mutation) { + Ok(()) => { + dev_info!( + me.dev, + "cubelinux: appended {} bytes at the log ({} value bytes)\n", + ENTRY_FIXED + value_len, + value_len + ); + Ok(len) + } + Err(e) => { + dev_err!(me.dev, "cubelinux: append failed: {:?}\n", e); + Err(e) + } + } + } + fn read_iter(mut kiocb: Kiocb<'_, Self::Ptr>, iov: &mut IovIterDest<'_>) -> Result { // The work happens here, on demand, rather than at init: by the time anything can // open this device, the block driver that provides the store is certainly up.