mapping/protocol.rs
1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! This module defines the communication protocol over the VMOs shared between the filesystem
6//! (Fxfs), the block driver (`block_server`), and the client/verifier (e.g., `pkg-cache`). The
7//! two VMOs (mapping queue and delivery queue) are provided to
8//! `fuchsia.storage.block/Mapper.OpenSession` to initialize a session.
9//!
10//! Two main channels of communication exist for paging:
11//!
12//! 1. **Mapping Queue (`RawMappingCommand`)**:
13//! Direction: Filesystem (Fxfs) -> Driver (Established via the client during initialization)
14//! Purpose: The filesystem informs the driver of where a file resides on the storage device
15//! (its extents). When the driver receives a page fault from the kernel for a specific `key`,
16//! it consults this mapping to know which blocks to read from disk.
17//!
18//! 2. **Delivery Queue (`RawDeliveryCommand`)**:
19//! Direction: Driver -> Client/Verifier (e.g., `pkg-cache`, which also acts as the Pager)
20//! Purpose: After the driver reads (and decompresses) the requested blocks, it writes the
21//! data into the delivery queue VMO and sends a delivery command. The verifier receives this
22//! command, cryptographically verifies the payload against the Merkle tree, and then supplies
23//! it to the kernel pager (`zx_pager_supply_pages`). The blob's Merkle leaf data is also
24//! transported via the delivery queue when the driver fetches and caches the metadata.
25
26use anyhow::{Error, anyhow};
27use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
28
29pub const MAPPINGS_COMMAND: u32 = 1;
30pub const CLOSE_BLOB_COMMAND: u32 = 2;
31
32/// Flag indicating that the mapping payload contains an encryption key.
33pub const MAPPINGS_FLAG_ENCRYPTED: u32 = 1 << 16;
34
35/// Size of the encryption key in bytes (256 bits).
36pub const ENCRYPTION_KEY_SIZE: usize = 32;
37
38// The `vmo-fifo` divides the VMO into two regions: a fixed-size command slots region, and a
39// dynamically allocated payload region where the actual extents are written.
40//
41// The following is the layout for a 512KB VMO with 256 capacity:
42// [ Headers (64B) | Command Slots: 256 * 40B = 10,240B | .. Padding to 16KB .. | Payload (496KB) ]
43// Note: Each command slot takes 40 bytes for `RawMappingCommand`.
44//
45// 496KB / 8-bytes per extent = 63,488 maximum extents bounded by the payload block.
46pub const MAPPING_VMO_SIZE: u64 = 512 * 1024;
47
48// With a maximum capacity of 256 pending mapping commands, this allows for an average of ~248
49// extents per blob. In the worst case of maximum fragmentation (every 4KB block maps to one
50// extent), 63,488 extents can map up to ~248MB of blob data (or ~496MB if block size is 8KB).
51pub const PENDING_COMMANDS_CAPACITY: u32 = 256;
52
53/// A command packet used to communicate extent mappings.
54#[derive(FromBytes, IntoBytes, KnownLayout, Immutable, Copy, Clone, Debug, PartialEq)]
55#[repr(C)]
56pub struct RawMappingCommand {
57 pub opcode: u32,
58 pub offset: u32,
59 pub key: u64,
60 pub stored_size: u64,
61 pub device_offset: u64,
62 pub metadata_count: u32,
63 pub extent_count: u32,
64}
65
66impl RawMappingCommand {
67 /// Returns the command opcode without flags.
68 pub fn opcode(&self) -> u32 {
69 self.opcode & 0xffff
70 }
71
72 /// Returns whether the command has the encrypted flag set.
73 pub fn is_encrypted(&self) -> bool {
74 (self.opcode & MAPPINGS_FLAG_ENCRYPTED) != 0
75 }
76}
77
78#[derive(Copy, Clone, Debug, PartialEq)]
79pub enum MappingCommand {
80 /// Informs the driver of the extent mappings for a file.
81 /// The VMO payload contains `extent_count` data extent mappings followed by `metadata_count`
82 /// Merkle extent mappings, and optionally a 32-byte encryption key if `encrypted` is true.
83 Mappings {
84 /// Session-unique identifier for the file.
85 key: u64,
86 /// Byte offset within the shared VMO where the extent descriptors begin.
87 offset: u32,
88 /// Total stored size of the file's data (compressed size if compressed, or byte size).
89 stored_size: u64,
90 /// Base physical device byte offset on the underlying storage device.
91 device_offset: u64,
92 /// Number of Merkle tree metadata extent mappings.
93 metadata_count: u32,
94 /// Number of file data extent mappings.
95 extent_count: u32,
96 /// Whether the file is encrypted. If true, the payload contains a 32-byte key following
97 /// the extent descriptors.
98 encrypted: bool,
99 },
100 /// Informs the driver that the file session is closed and mappings can be discarded.
101 CloseBlob {
102 /// Session-unique identifier for the file.
103 key: u64,
104 },
105}
106
107impl From<MappingCommand> for RawMappingCommand {
108 fn from(cmd: MappingCommand) -> Self {
109 match cmd {
110 MappingCommand::Mappings {
111 key,
112 offset,
113 stored_size,
114 device_offset,
115 metadata_count,
116 extent_count,
117 encrypted,
118 } => RawMappingCommand {
119 opcode: MAPPINGS_COMMAND | if encrypted { MAPPINGS_FLAG_ENCRYPTED } else { 0 },
120 offset,
121 key,
122 stored_size,
123 device_offset,
124 metadata_count,
125 extent_count,
126 },
127 MappingCommand::CloseBlob { key } => RawMappingCommand {
128 opcode: CLOSE_BLOB_COMMAND,
129 offset: 0,
130 key,
131 stored_size: 0,
132 device_offset: 0,
133 metadata_count: 0,
134 extent_count: 0,
135 },
136 }
137 }
138}
139
140impl TryFrom<RawMappingCommand> for MappingCommand {
141 type Error = Error;
142
143 fn try_from(cmd: RawMappingCommand) -> Result<Self, Self::Error> {
144 let opcode = cmd.opcode & 0xffff;
145 let encrypted = (cmd.opcode & MAPPINGS_FLAG_ENCRYPTED) != 0;
146 let unknown_flags = cmd.opcode & !(0xffff | MAPPINGS_FLAG_ENCRYPTED);
147 if unknown_flags != 0 {
148 return Err(anyhow!("Unknown flags in opcode: {:#x}", cmd.opcode));
149 }
150 match opcode {
151 MAPPINGS_COMMAND => Ok(MappingCommand::Mappings {
152 key: cmd.key,
153 offset: cmd.offset,
154 stored_size: cmd.stored_size,
155 device_offset: cmd.device_offset,
156 metadata_count: cmd.metadata_count,
157 extent_count: cmd.extent_count,
158 encrypted,
159 }),
160 CLOSE_BLOB_COMMAND => {
161 if encrypted {
162 return Err(anyhow!("Encrypted flag not allowed for CloseBlob"));
163 }
164 Ok(MappingCommand::CloseBlob { key: cmd.key })
165 }
166 _ => Err(anyhow!("Unknown opcode: {}", cmd.opcode)),
167 }
168 }
169}
170
171pub const DELIVERY_DATA_COMMAND: u32 = 1;
172pub const DELIVERY_REGISTER_BLOB_COMMAND: u32 = 2;
173pub const DELIVERY_CHILD_DATA_COMMAND: u32 = 3;
174pub const DELIVERY_CHILD_INLINE_TAIL_COMMAND: u32 = 4;
175
176// The Delivery Queue ring buffer holds `RawDeliveryCommand` structures, which are 64 bytes each
177// (aligned to a 64-byte cache line). If we establish an 8MB (8,388,608 bytes) VMO with a capacity
178// of 256 pending delivery commands, the struct sizes and capacities stack dynamically bounding to
179// the exact VMO wall, aligning the payload naturally to a 4KB hardware page boundary for zero-copy
180// kernel transfers:
181//
182// Offsets: 0 64 16,448 20,480 8,388,608
183// Layout: | Header | Cmd Slots | Padding to 4KB page | Payload (Data & Merkle leaves)|
184// Sizes: | 64 B | 16,384 B | 4,032 B | 8,368,128 B |
185//
186// Payload slices are variable-sized (`RegisterBlob` leaf hashes, or page-aligned `Data` chunks up
187// to the blob's `read_alignment`). At an 8MB VMO size, the 8,368,128 B payload region can hold an
188// average of ~32KB per command if all 256 command slots are occupied, or e.g. up to 63 full
189// 128KB (`TARGET_READ_AHEAD_SIZE`) data chunks before the payload buffer fills.
190pub const DELIVERY_VMO_SIZE: u64 = 8 * 1024 * 1024;
191pub const PENDING_DELIVERY_COMMANDS_CAPACITY: u32 = 256;
192
193/// A command packet used by the driver to deliver merkle leaves and data chunks for verification.
194#[derive(FromBytes, IntoBytes, KnownLayout, Immutable, Copy, Clone, Debug, PartialEq)]
195#[repr(C, align(64))]
196pub struct RawDeliveryCommand {
197 pub opcode: u32,
198 /// For `DELIVERY_REGISTER_BLOB_COMMAND`, the blob's verification alignment in bytes.
199 /// For `DELIVERY_CHILD_DATA_COMMAND`, reused to store `in_chunk_offset`.
200 pub read_alignment: u32,
201 pub key: u64,
202 pub target_offset: u64,
203 pub length: u32,
204 pub offset: u32,
205 /// Used by child delivery commands to specify the target slice in the child VMO.
206 pub child_target_offset: u64,
207 pub child_length: u32,
208 pub padding: [u32; 5],
209}
210
211const _: () = assert!(std::mem::size_of::<RawDeliveryCommand>() == 64);
212
213#[derive(Copy, Clone, Debug, PartialEq)]
214pub enum DeliveryCommand {
215 /// Informs the verifier that data has been read and decompressed into the delivery queue VMO,
216 /// ready for verification.
217 Data {
218 /// Identifies the blob.
219 key: u64,
220 /// The logical byte offset of this data chunk in the target VMO.
221 target_offset: u64,
222 /// The length of the data chunk in bytes. Must be a multiple of the blob's
223 /// `read_alignment` (or page-aligned for the final chunk of the blob).
224 length: u32,
225 /// The offset in this delivery queue where this data chunk resides.
226 offset: u32,
227 },
228 /// Informs the verifier that the blob's Merkle tree metadata has been transferred to the queue.
229 RegisterBlob {
230 /// Identifies the blob.
231 key: u64,
232 /// The offset in this delivery queue where the Merkle leaves reside.
233 offset: u32,
234 /// The length of the Merkle leaf data in bytes.
235 length: u32,
236 /// The byte alignment (a multiple of the 8 KiB Merkle leaf size) used to configure
237 /// `ReadSizedMerkleVerifier` for this blob.
238 // This is needed per blob because compressed blobs adjust their verification alignment to
239 // their compression chunk size, and the verifier requires incoming `DeliveryCommand::Data`
240 // chunks to align to multiples of this value (except for the final chunk at EOF).
241 read_alignment: u32,
242 },
243 /// Informs the verifier that a parent blob chunk has been read and decompressed into the
244 /// delivery queue VMO to service a page fault on a child file mapped over that blob.
245 ///
246 /// When `length > 0`, the verifier first verifies `[offset .. offset + length]` against the
247 /// parent blob's Merkle tree at `parent_offset` and supplies `child_length` bytes from
248 /// `in_chunk_offset` to `child_key` at `child_target_offset`.
249 /// When `length == 0`, the child range is sparse and the verifier supplies `child_length` zero
250 /// bytes to `child_key` at `child_target_offset`.
251 ChildData {
252 /// The `verification_alignment`-aligned byte offset of the chunk in the parent blob.
253 parent_offset: u64,
254 /// The byte offset in the delivery queue VMO where the parent blob chunk resides.
255 offset: u32,
256 /// The byte length of the parent blob chunk in the delivery queue VMO (0 if sparse).
257 length: u32,
258 /// Identifies the child VMO (and its associated parent blob in the verifier).
259 child_key: u64,
260 /// The page-aligned logical byte offset in the child VMO where pages should be supplied.
261 child_target_offset: u64,
262 /// Page-aligned byte offset within the verified parent chunk `[0..length)` where the child
263 /// data starts.
264 in_chunk_offset: u32,
265 /// Page-aligned byte length to supply to the child VMO.
266 child_length: u32,
267 },
268 /// Header-only command informing the verifier to supply a single inline-tail page to a child
269 /// VMO by reading `tail_length` bytes directly from the verified parent VMO at
270 /// `parent_byte_offset`.
271 ChildInlineTail {
272 /// Unaligned byte offset in the parent blob where the inline tail begins.
273 parent_byte_offset: u64,
274 /// Identifies the child VMO (and its associated parent blob in the verifier).
275 child_key: u64,
276 /// Page-aligned logical byte offset of the tail page in the child VMO.
277 child_target_offset: u64,
278 /// Number of valid inline-tail bytes to copy (`0..=4096`).
279 tail_length: u32,
280 },
281}
282
283impl From<DeliveryCommand> for RawDeliveryCommand {
284 fn from(cmd: DeliveryCommand) -> Self {
285 match cmd {
286 DeliveryCommand::Data { key, target_offset, length, offset } => RawDeliveryCommand {
287 opcode: DELIVERY_DATA_COMMAND,
288 read_alignment: 0,
289 key,
290 target_offset,
291 length,
292 offset,
293 child_target_offset: 0,
294 child_length: 0,
295 padding: [0; 5],
296 },
297 DeliveryCommand::RegisterBlob { key, offset, length, read_alignment } => {
298 RawDeliveryCommand {
299 opcode: DELIVERY_REGISTER_BLOB_COMMAND,
300 read_alignment,
301 key,
302 target_offset: 0,
303 length,
304 offset,
305 child_target_offset: 0,
306 child_length: 0,
307 padding: [0; 5],
308 }
309 }
310 DeliveryCommand::ChildData {
311 parent_offset,
312 offset,
313 length,
314 child_key,
315 child_target_offset,
316 in_chunk_offset,
317 child_length,
318 } => RawDeliveryCommand {
319 opcode: DELIVERY_CHILD_DATA_COMMAND,
320 read_alignment: in_chunk_offset,
321 key: child_key,
322 target_offset: parent_offset,
323 length,
324 offset,
325 child_target_offset,
326 child_length,
327 padding: [0; 5],
328 },
329 DeliveryCommand::ChildInlineTail {
330 parent_byte_offset,
331 child_key,
332 child_target_offset,
333 tail_length,
334 } => RawDeliveryCommand {
335 opcode: DELIVERY_CHILD_INLINE_TAIL_COMMAND,
336 read_alignment: 0,
337 key: child_key,
338 target_offset: parent_byte_offset,
339 length: 0,
340 offset: 0,
341 child_target_offset,
342 child_length: tail_length,
343 padding: [0; 5],
344 },
345 }
346 }
347}
348
349impl TryFrom<RawDeliveryCommand> for DeliveryCommand {
350 type Error = Error;
351
352 fn try_from(cmd: RawDeliveryCommand) -> Result<Self, Self::Error> {
353 match cmd.opcode {
354 DELIVERY_DATA_COMMAND => Ok(DeliveryCommand::Data {
355 key: cmd.key,
356 target_offset: cmd.target_offset,
357 length: cmd.length,
358 offset: cmd.offset,
359 }),
360 DELIVERY_REGISTER_BLOB_COMMAND => Ok(DeliveryCommand::RegisterBlob {
361 key: cmd.key,
362 offset: cmd.offset,
363 length: cmd.length,
364 read_alignment: cmd.read_alignment,
365 }),
366 DELIVERY_CHILD_DATA_COMMAND => Ok(DeliveryCommand::ChildData {
367 parent_offset: cmd.target_offset,
368 offset: cmd.offset,
369 length: cmd.length,
370 child_key: cmd.key,
371 child_target_offset: cmd.child_target_offset,
372 in_chunk_offset: cmd.read_alignment,
373 child_length: cmd.child_length,
374 }),
375 DELIVERY_CHILD_INLINE_TAIL_COMMAND => Ok(DeliveryCommand::ChildInlineTail {
376 parent_byte_offset: cmd.target_offset,
377 child_key: cmd.key,
378 child_target_offset: cmd.child_target_offset,
379 tail_length: cmd.child_length,
380 }),
381 _ => Err(anyhow!("Unknown opcode: {}", cmd.opcode)),
382 }
383 }
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389
390 #[test]
391 fn test_mappings_command_round_trip() {
392 let cmd = MappingCommand::Mappings {
393 key: 123,
394 offset: 456,
395 stored_size: 789,
396 device_offset: 1011,
397 metadata_count: 2,
398 extent_count: 3,
399 encrypted: false,
400 };
401 let raw = RawMappingCommand::from(cmd);
402 assert_eq!(raw.opcode(), MAPPINGS_COMMAND);
403 assert!(!raw.is_encrypted());
404 assert_eq!(MappingCommand::try_from(raw).unwrap(), cmd);
405 }
406
407 #[test]
408 fn test_mappings_command_encrypted_round_trip() {
409 let cmd = MappingCommand::Mappings {
410 key: 123,
411 offset: 456,
412 stored_size: 789,
413 device_offset: 1011,
414 metadata_count: 2,
415 extent_count: 3,
416 encrypted: true,
417 };
418 let raw = RawMappingCommand::from(cmd);
419 assert_eq!(raw.opcode(), MAPPINGS_COMMAND);
420 assert!(raw.is_encrypted());
421 assert_eq!(MappingCommand::try_from(raw).unwrap(), cmd);
422 }
423
424 #[test]
425 fn test_close_blob_command_round_trip() {
426 let cmd = MappingCommand::CloseBlob { key: 42 };
427 let raw = RawMappingCommand::from(cmd);
428 assert_eq!(raw.opcode(), CLOSE_BLOB_COMMAND);
429 assert!(!raw.is_encrypted());
430 assert_eq!(MappingCommand::try_from(raw).unwrap(), cmd);
431 }
432
433 #[test]
434 fn test_close_blob_encrypted_flag_rejected() {
435 let raw = RawMappingCommand {
436 opcode: CLOSE_BLOB_COMMAND | MAPPINGS_FLAG_ENCRYPTED,
437 offset: 0,
438 key: 42,
439 stored_size: 0,
440 device_offset: 0,
441 metadata_count: 0,
442 extent_count: 0,
443 };
444 assert!(MappingCommand::try_from(raw).is_err());
445 }
446
447 #[test]
448 fn test_unknown_opcode_rejected() {
449 let raw = RawMappingCommand {
450 opcode: 99,
451 offset: 0,
452 key: 42,
453 stored_size: 0,
454 device_offset: 0,
455 metadata_count: 0,
456 extent_count: 0,
457 };
458 assert!(MappingCommand::try_from(raw).is_err());
459 }
460
461 #[test]
462 fn test_unknown_flags_rejected() {
463 let raw = RawMappingCommand {
464 opcode: MAPPINGS_COMMAND | (1 << 31),
465 offset: 0,
466 key: 42,
467 stored_size: 0,
468 device_offset: 0,
469 metadata_count: 0,
470 extent_count: 0,
471 };
472 assert!(MappingCommand::try_from(raw).is_err());
473 }
474}