Skip to main content

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}