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}