From ff2914a105766b1eb593d231d4746b58ac798df0 Mon Sep 17 00:00:00 2001 From: CUBELinux-2 Date: Thu, 13 Aug 2026 17:29:50 -0400 Subject: [PATCH] fix(cubestore): surface raw/un-enveloped records in get_record instead of returning None get_record() previously returned None for any payload lacking a TLV envelope (4-byte header-len | header | body). Records written through the raw path (put_raw / the daemon's 'rawput' verb, used by every OS-layer service) carry no envelope, so callers such as cubefs getattr/read and the 'stat' verb mapped that None to size 0 / empty file: real bytes became SILENTLY INVISIBLE through the filesystem while rawget still returned them. This broke the OS-in-CUBE migration (2026-08-13): every rawput OS record listed as a 0-byte file in /cubefs, which also surface-invalidated the 'the OS boots from cube' resume path. Now a payload that is not a well-formed envelope is surfaced as a raw body under a synthesized header whose size_bytes reports the true length. Enveloped records still decode their real header (no behavior change on the record path). Adds two regression tests: get_record_surfaces_raw_unenveloped_payloads and get_record_still_prefers_the_real_envelope (cargo test -p cubestore: 5/5). --- cubestore/src/lib.rs | 89 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 84 insertions(+), 5 deletions(-) diff --git a/cubestore/src/lib.rs b/cubestore/src/lib.rs index 982099e..da65d94 100644 --- a/cubestore/src/lib.rs +++ b/cubestore/src/lib.rs @@ -387,20 +387,48 @@ impl CubeStore { } /// Fetch and split a record into `(header, body)`. + /// + /// Records written through the record path carry a TLV envelope + /// (`u32 header-len | header | body`). Records written through the *raw* + /// path (`put_raw` / the daemon's `rawput` verb, used by OS-layer services) + /// carry no envelope at all. + /// + /// Historically a non-enveloped payload made this return `None`, which + /// callers such as `cubefs`'s `getattr`/`read` and the daemon's `stat` verb + /// translate into "size 0 / empty file". A record holding real bytes was + /// therefore *silently invisible* through the filesystem while `rawget` + /// happily returned its contents — the OS-in-CUBE migration hit exactly + /// this (2026-08-13): every `rawput` OS record listed as a 0-byte file. + /// + /// Losing data silently is never the right failure mode, so a payload that + /// is not a well-formed envelope is now surfaced as a raw body under a + /// synthesized header. `rawput` data becomes readable through the + /// filesystem, and no caller has to special-case the two write paths. pub fn get_record(&self, label: &Czyx) -> Option<(CubeHeader, Vec)> { let raw = self.backend.get(label)?; + // Fall back to treating the payload as a raw (un-enveloped) body when + // it cannot be parsed as `len | header | body`. + let raw_fallback = |bytes: &[u8]| { + let mut h = CubeHeader::new(); + h.size_bytes = Some(bytes.len() as u64); + Some((h, bytes.to_vec())) + }; if raw.len() < 4 { - return None; + return raw_fallback(&raw); } let mut len = [0u8; 4]; len.copy_from_slice(&raw[..4]); let hlen = u32::from_le_bytes(len) as usize; if raw.len() < 4 + hlen { - return None; + return raw_fallback(&raw); + } + match record_codec::decode_header(&raw[4..4 + hlen]) { + Some(hdr) => { + let body = raw[4 + hlen..].to_vec(); + Some((hdr, body)) + } + None => raw_fallback(&raw), } - let hdr = record_codec::decode_header(&raw[4..4 + hlen])?; - let body = raw[4 + hlen..].to_vec(); - Some((hdr, body)) } /// Raw backend write (non-record payloads, e.g. ACL/xattr/volume buckets). @@ -515,4 +543,55 @@ mod tests { assert_eq!(store.get_raw(&Czyx::new(0, 0, 0, 0)), Some(vec![1])); assert_eq!(store.get_raw(&Czyx::new(0, 0, 0, 1)), Some(vec![2])); } + + /// Regression (2026-08-13, OS-in-CUBE migration): a payload written via the + /// RAW path (`put_raw`, i.e. the daemon's `rawput` verb used by OS-layer + /// services) must still be READABLE through `get_record`, because that is + /// what `cubefs` getattr/read and the `stat` verb go through. Before the + /// fix these records reported size 0 and read back empty — real bytes were + /// silently invisible through the filesystem. + #[test] + fn get_record_surfaces_raw_unenveloped_payloads() { + let mut store = CubeStore::new(HashBackend::new()); + let coord = Czyx::new(200, 70, 1, 1); + let payload = b"CUBELINUX OS PROCESS SNAPSHOT\nprocs: 118\n".to_vec(); + store.put_raw(coord, payload.clone()); + + let (hdr, body) = store + .get_record(&coord) + .expect("raw payload must be visible as a record, not vanish"); + assert_eq!(body, payload, "body must round-trip byte-for-byte"); + assert_eq!( + hdr.size_bytes, + Some(payload.len() as u64), + "synthesized header must report the true size so getattr is correct" + ); + + // A short payload (< 4 bytes, cannot even hold a length prefix) is the + // other edge the old code dropped: counters like "118" land here. + let short = Czyx::new(200, 70, 2, 1); + store.put_raw(short, b"118".to_vec()); + let (h2, b2) = store.get_record(&short).expect("short raw payload visible"); + assert_eq!(b2, b"118"); + assert_eq!(h2.size_bytes, Some(3)); + } + + /// The envelope path must be unaffected by the raw fallback: a properly + /// stored record still decodes its real header (not a synthesized one). + #[test] + fn get_record_still_prefers_the_real_envelope() { + let mut store = CubeStore::new(HashBackend::new()); + let coord = Czyx::new(201, 5, 1, 1); + let mut hdr = CubeHeader::new(); + hdr.title = Some("real-record".to_string()); + store.put_record(coord, &hdr, b"payload"); + + let (got, body) = store.get_record(&coord).expect("enveloped record"); + assert_eq!(body, b"payload"); + assert_eq!( + got.title.as_deref(), + Some("real-record"), + "must decode the true header, not fall back to raw" + ); + } }