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;
173
174// The Delivery Queue ring buffer holds `RawDeliveryCommand` structures, which are 32 bytes each.
175// If we establish an 8MB (8,388,608 bytes) VMO with a capacity of 256 pending delivery commands,
176// the struct sizes and capacities stack dynamically bounding to the exact VMO wall, aligning
177// the payload naturally to a 4KB hardware page boundary for zero-copy kernel transfers:
178//
179// Offsets: 0        64            8,256                   12,288                          8,388,608
180// Layout:  | Header |  Cmd Slots  |  Padding to 4KB page  | Payload (Data & Merkle leaves)|
181// Sizes:   |  64 B  |  8,192 B    |       4,032 B         |         8,376,320 B           |
182//
183// Payload slices are variable-sized (`RegisterBlob` leaf hashes, or page-aligned `Data` chunks up
184// to the blob's `read_alignment`). At an 8MB VMO size, the 8,376,320 B payload region can hold an
185// average of ~32KB per command if all 256 command slots are occupied, or e.g. up to 63 full
186// 128KB (`TARGET_READ_AHEAD_SIZE`) data chunks before the payload buffer fills.
187pub const DELIVERY_VMO_SIZE: u64 = 8 * 1024 * 1024;
188pub const PENDING_DELIVERY_COMMANDS_CAPACITY: u32 = 256;
189
190/// A command packet used by the driver to deliver merkle leaves and data chunks for verification.
191#[derive(FromBytes, IntoBytes, KnownLayout, Immutable, Copy, Clone, Debug, PartialEq)]
192#[repr(C)]
193pub struct RawDeliveryCommand {
194    pub opcode: u32,
195    pub read_alignment: u32,
196    pub key: u64,
197    pub target_offset: u64,
198    pub length: u32,
199    pub offset: u32,
200}
201
202#[derive(Copy, Clone, Debug, PartialEq)]
203pub enum DeliveryCommand {
204    /// Informs the verifier that data has been read and decompressed into the delivery queue VMO,
205    /// ready for verification.
206    Data {
207        /// Identifies the blob.
208        key: u64,
209        /// The logical byte offset of this data chunk in the target VMO.
210        target_offset: u64,
211        /// The length of the data chunk in bytes. Must be a multiple of the blob's
212        /// `read_alignment` (or page-aligned for the final chunk of the blob).
213        length: u32,
214        /// The offset in this delivery queue where this data chunk resides.
215        offset: u32,
216    },
217    /// Informs the verifier that the blob's Merkle tree metadata has been transferred to the queue.
218    RegisterBlob {
219        /// Identifies the blob.
220        key: u64,
221        /// The offset in this delivery queue where the Merkle leaves reside.
222        offset: u32,
223        /// The length of the Merkle leaf data in bytes.
224        length: u32,
225        /// The byte alignment (a multiple of the 8 KiB Merkle leaf size) used to configure
226        /// `ReadSizedMerkleVerifier` for this blob.
227        // This is needed per blob because compressed blobs adjust their read-ahead size to a
228        // multiple of their compression chunk size, and the verifier requires incoming
229        // `DeliveryCommand::Data` chunks to align to multiples of this value (except for the final
230        // chunk at EOF).
231        read_alignment: u32,
232    },
233}
234
235impl From<DeliveryCommand> for RawDeliveryCommand {
236    fn from(cmd: DeliveryCommand) -> Self {
237        match cmd {
238            DeliveryCommand::Data { key, target_offset, length, offset } => RawDeliveryCommand {
239                opcode: DELIVERY_DATA_COMMAND,
240                read_alignment: 0,
241                key,
242                target_offset,
243                length,
244                offset,
245            },
246            DeliveryCommand::RegisterBlob { key, offset, length, read_alignment } => {
247                RawDeliveryCommand {
248                    opcode: DELIVERY_REGISTER_BLOB_COMMAND,
249                    read_alignment,
250                    key,
251                    target_offset: 0,
252                    length,
253                    offset,
254                }
255            }
256        }
257    }
258}
259
260impl TryFrom<RawDeliveryCommand> for DeliveryCommand {
261    type Error = Error;
262
263    fn try_from(cmd: RawDeliveryCommand) -> Result<Self, Self::Error> {
264        match cmd.opcode {
265            DELIVERY_DATA_COMMAND => Ok(DeliveryCommand::Data {
266                key: cmd.key,
267                target_offset: cmd.target_offset,
268                length: cmd.length,
269                offset: cmd.offset,
270            }),
271            DELIVERY_REGISTER_BLOB_COMMAND => Ok(DeliveryCommand::RegisterBlob {
272                key: cmd.key,
273                offset: cmd.offset,
274                length: cmd.length,
275                read_alignment: cmd.read_alignment,
276            }),
277            _ => Err(anyhow!("Unknown opcode: {}", cmd.opcode)),
278        }
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    #[test]
287    fn test_mappings_command_round_trip() {
288        let cmd = MappingCommand::Mappings {
289            key: 123,
290            offset: 456,
291            stored_size: 789,
292            device_offset: 1011,
293            metadata_count: 2,
294            extent_count: 3,
295            encrypted: false,
296        };
297        let raw = RawMappingCommand::from(cmd);
298        assert_eq!(raw.opcode(), MAPPINGS_COMMAND);
299        assert!(!raw.is_encrypted());
300        assert_eq!(MappingCommand::try_from(raw).unwrap(), cmd);
301    }
302
303    #[test]
304    fn test_mappings_command_encrypted_round_trip() {
305        let cmd = MappingCommand::Mappings {
306            key: 123,
307            offset: 456,
308            stored_size: 789,
309            device_offset: 1011,
310            metadata_count: 2,
311            extent_count: 3,
312            encrypted: true,
313        };
314        let raw = RawMappingCommand::from(cmd);
315        assert_eq!(raw.opcode(), MAPPINGS_COMMAND);
316        assert!(raw.is_encrypted());
317        assert_eq!(MappingCommand::try_from(raw).unwrap(), cmd);
318    }
319
320    #[test]
321    fn test_close_blob_command_round_trip() {
322        let cmd = MappingCommand::CloseBlob { key: 42 };
323        let raw = RawMappingCommand::from(cmd);
324        assert_eq!(raw.opcode(), CLOSE_BLOB_COMMAND);
325        assert!(!raw.is_encrypted());
326        assert_eq!(MappingCommand::try_from(raw).unwrap(), cmd);
327    }
328
329    #[test]
330    fn test_close_blob_encrypted_flag_rejected() {
331        let raw = RawMappingCommand {
332            opcode: CLOSE_BLOB_COMMAND | MAPPINGS_FLAG_ENCRYPTED,
333            offset: 0,
334            key: 42,
335            stored_size: 0,
336            device_offset: 0,
337            metadata_count: 0,
338            extent_count: 0,
339        };
340        assert!(MappingCommand::try_from(raw).is_err());
341    }
342
343    #[test]
344    fn test_unknown_opcode_rejected() {
345        let raw = RawMappingCommand {
346            opcode: 99,
347            offset: 0,
348            key: 42,
349            stored_size: 0,
350            device_offset: 0,
351            metadata_count: 0,
352            extent_count: 0,
353        };
354        assert!(MappingCommand::try_from(raw).is_err());
355    }
356
357    #[test]
358    fn test_unknown_flags_rejected() {
359        let raw = RawMappingCommand {
360            opcode: MAPPINGS_COMMAND | (1 << 31),
361            offset: 0,
362            key: 42,
363            stored_size: 0,
364            device_offset: 0,
365            metadata_count: 0,
366            extent_count: 0,
367        };
368        assert!(MappingCommand::try_from(raw).is_err());
369    }
370}