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.