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// At an 8MB VMO size, the payload space allows for an average of ~32KB per chunk/leaf block
184// in-flight for 256 outstanding commands.
185pub const DELIVERY_VMO_SIZE: u64 = 8 * 1024 * 1024;
186pub const PENDING_DELIVERY_COMMANDS_CAPACITY: u32 = 256;
187
188/// The chunk size requirement for delivering payloads from the block driver across the delivery
189/// queue. The driver must supply [`DeliveryCommand::Data`] chunks where the `target_offset` and
190/// `length` align to `DELIVERY_DATA_SIZE` boundaries (unless representing the final chunk of the
191/// blob).
192///
193/// This chunk size is set as 128 KiB size, matching Fxfs's target read-ahead size. This is used to
194/// set the read size of `fuchsia_merkle::ReadSizedMerkleVerifier`, which optimizes memory usage
195/// when verifying reads. See `fuchsia_merkle::ReadSizedMerkleVerifier` for more information.
196pub const DELIVERY_DATA_SIZE: usize = 128 * 1024;
197
198/// A command packet used by the driver to deliver merkle leaves and data chunks for verification.
199#[derive(FromBytes, IntoBytes, KnownLayout, Immutable, Copy, Clone, Debug, PartialEq)]
200#[repr(C)]
201pub struct RawDeliveryCommand {
202    pub opcode: u32,
203    pub _padding: u32,
204    pub key: u64,
205    pub target_offset: u64,
206    pub length: u32,
207    pub offset: u32,
208}
209
210#[derive(Copy, Clone, Debug, PartialEq)]
211pub enum DeliveryCommand {
212    /// Informs the verifier that data has been read and decompressed into the delivery queue VMO,
213    /// ready for verification.
214    Data {
215        /// Identifies the blob.
216        key: u64,
217        /// The logical byte offset of this data chunk in the target VMO.
218        target_offset: u64,
219        /// The length of the data chunk.
220        length: u32,
221        /// The offset in this delivery queue where this data chunk resides.
222        offset: u32,
223    },
224    /// Informs the verifier that the blob's Merkle tree metadata has been transferred to the queue.
225    RegisterBlob {
226        /// Identifies the blob.
227        key: u64,
228        /// The offset in this delivery queue where the Merkle leaves reside.
229        offset: u32,
230        /// The length of the Merkle leaf data in bytes.
231        length: 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                _padding: 0,
241                key,
242                target_offset,
243                length,
244                offset,
245            },
246            DeliveryCommand::RegisterBlob { key, offset, length } => RawDeliveryCommand {
247                opcode: DELIVERY_REGISTER_BLOB_COMMAND,
248                _padding: 0,
249                key,
250                target_offset: 0,
251                length,
252                offset,
253            },
254        }
255    }
256}
257
258impl TryFrom<RawDeliveryCommand> for DeliveryCommand {
259    type Error = Error;
260
261    fn try_from(cmd: RawDeliveryCommand) -> Result<Self, Self::Error> {
262        match cmd.opcode {
263            DELIVERY_DATA_COMMAND => Ok(DeliveryCommand::Data {
264                key: cmd.key,
265                target_offset: cmd.target_offset,
266                length: cmd.length,
267                offset: cmd.offset,
268            }),
269            DELIVERY_REGISTER_BLOB_COMMAND => Ok(DeliveryCommand::RegisterBlob {
270                key: cmd.key,
271                offset: cmd.offset,
272                length: cmd.length,
273            }),
274            _ => Err(anyhow!("Unknown opcode: {}", cmd.opcode)),
275        }
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    #[test]
284    fn test_mappings_command_round_trip() {
285        let cmd = MappingCommand::Mappings {
286            key: 123,
287            offset: 456,
288            stored_size: 789,
289            device_offset: 1011,
290            metadata_count: 2,
291            extent_count: 3,
292            encrypted: false,
293        };
294        let raw = RawMappingCommand::from(cmd);
295        assert_eq!(raw.opcode(), MAPPINGS_COMMAND);
296        assert!(!raw.is_encrypted());
297        assert_eq!(MappingCommand::try_from(raw).unwrap(), cmd);
298    }
299
300    #[test]
301    fn test_mappings_command_encrypted_round_trip() {
302        let cmd = MappingCommand::Mappings {
303            key: 123,
304            offset: 456,
305            stored_size: 789,
306            device_offset: 1011,
307            metadata_count: 2,
308            extent_count: 3,
309            encrypted: true,
310        };
311        let raw = RawMappingCommand::from(cmd);
312        assert_eq!(raw.opcode(), MAPPINGS_COMMAND);
313        assert!(raw.is_encrypted());
314        assert_eq!(MappingCommand::try_from(raw).unwrap(), cmd);
315    }
316
317    #[test]
318    fn test_close_blob_command_round_trip() {
319        let cmd = MappingCommand::CloseBlob { key: 42 };
320        let raw = RawMappingCommand::from(cmd);
321        assert_eq!(raw.opcode(), CLOSE_BLOB_COMMAND);
322        assert!(!raw.is_encrypted());
323        assert_eq!(MappingCommand::try_from(raw).unwrap(), cmd);
324    }
325
326    #[test]
327    fn test_close_blob_encrypted_flag_rejected() {
328        let raw = RawMappingCommand {
329            opcode: CLOSE_BLOB_COMMAND | MAPPINGS_FLAG_ENCRYPTED,
330            offset: 0,
331            key: 42,
332            stored_size: 0,
333            device_offset: 0,
334            metadata_count: 0,
335            extent_count: 0,
336        };
337        assert!(MappingCommand::try_from(raw).is_err());
338    }
339
340    #[test]
341    fn test_unknown_opcode_rejected() {
342        let raw = RawMappingCommand {
343            opcode: 99,
344            offset: 0,
345            key: 42,
346            stored_size: 0,
347            device_offset: 0,
348            metadata_count: 0,
349            extent_count: 0,
350        };
351        assert!(MappingCommand::try_from(raw).is_err());
352    }
353
354    #[test]
355    fn test_unknown_flags_rejected() {
356        let raw = RawMappingCommand {
357            opcode: MAPPINGS_COMMAND | (1 << 31),
358            offset: 0,
359            key: 42,
360            stored_size: 0,
361            device_offset: 0,
362            metadata_count: 0,
363            extent_count: 0,
364        };
365        assert!(MappingCommand::try_from(raw).is_err());
366    }
367}