Skip to main content

erofs/
mapper.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
5use crate::{ErofsError, ErofsFilesystem, FileNode, InodeDataLayout};
6use fidl_fuchsia_storage_block as fblock;
7use fuchsia_sync::Mutex;
8use mapping::{
9    DeviceExtent, Extent, Extents, MAPPING_VMO_SIZE, MappingCommand, PENDING_COMMANDS_CAPACITY,
10    RawMappingCommand,
11};
12use std::collections::HashMap;
13use vmo_fifo::SyncSender;
14
15/// Manages a driver-backed `MapperSession` for an EROFS volume.
16pub struct VolumeMapper {
17    session: fblock::MapperSessionSynchronousProxy,
18    sender: Mutex<SyncSender<RawMappingCommand>>,
19    vmos_by_nid: Mutex<HashMap<u64, zx::Vmo>>,
20    file_offset: u64,
21}
22
23impl VolumeMapper {
24    /// Creates a new `VolumeMapper` over an established `MapperSession` and its `mapping_vmo`.
25    pub fn new(
26        session: fblock::MapperSessionSynchronousProxy,
27        mapping_vmo: zx::Vmo,
28        file_offset: u64,
29    ) -> Result<Self, zx::Status> {
30        let sender =
31            SyncSender::<RawMappingCommand>::new(mapping_vmo, 8, PENDING_COMMANDS_CAPACITY)?;
32        Ok(Self {
33            session,
34            sender: Mutex::new(sender),
35            vmos_by_nid: Mutex::new(HashMap::new()),
36            file_offset,
37        })
38    }
39
40    /// Allocates a `mapping_vmo`, invokes `open_fn` to bind the server end of a `MapperSession`,
41    /// and returns the initialized `VolumeMapper`.
42    pub fn open_with_session(
43        file_offset: u64,
44        open_fn: impl FnOnce(
45            zx::Vmo,
46            fidl::endpoints::ServerEnd<fblock::MapperSessionMarker>,
47        ) -> Result<(), zx::Status>,
48    ) -> Result<Self, zx::Status> {
49        let mapping_vmo = zx::Vmo::create(MAPPING_VMO_SIZE)?;
50        let mapping_vmo_dup = mapping_vmo.duplicate_handle(zx::Rights::SAME_RIGHTS)?;
51        let (session_client, session_server) =
52            fidl::endpoints::create_endpoints::<fblock::MapperSessionMarker>();
53        open_fn(mapping_vmo_dup, session_server)?;
54        Self::new(session_client.into_sync_proxy(), mapping_vmo, file_offset)
55    }
56
57    /// Returns a read-only VMO handle for `node`.
58    ///
59    /// Driver-backed pager VMOs (`FlatPlain` and `FlatInline`) are cached in `vmos_by_nid` for the
60    /// lifetime of the `VolumeMapper` so that their driver mappings stay registered even if a VFS
61    /// node is evicted while a process still has the VMO mapped, and so subsequent opens reuse the
62    /// existing VMO. Anonymous VMOs created for empty or compressed files (`key == None`) are
63    /// returned directly so their memory can be reclaimed once the caller drops the VMO.
64    pub fn get_or_create_vmo(
65        &self,
66        fs: &ErofsFilesystem,
67        node: &FileNode,
68    ) -> Result<zx::Vmo, zx::Status> {
69        let nid = node.nid();
70        if let Some(vmo) = self.vmos_by_nid.lock().get(&nid) {
71            return vmo.duplicate_handle(zx::Rights::SAME_RIGHTS);
72        }
73
74        let (vmo, key) = self.create_vmo_internal(fs, node)?;
75        let Some(key) = key else {
76            return Ok(vmo);
77        };
78        let mut vmos = self.vmos_by_nid.lock();
79        if let Some(existing) = vmos.get(&nid) {
80            self.close_file(key);
81            return existing.duplicate_handle(zx::Rights::SAME_RIGHTS);
82        }
83        let dup = vmo.duplicate_handle(zx::Rights::SAME_RIGHTS)?;
84        vmos.insert(nid, vmo);
85        Ok(dup)
86    }
87
88    fn create_vmo_internal(
89        &self,
90        fs: &ErofsFilesystem,
91        node: &FileNode,
92    ) -> Result<(zx::Vmo, Option<u64>), zx::Status> {
93        let size = node.size();
94        let block_size = fs.block_size();
95        let read_only_rights = zx::Rights::VMO_DEFAULT - zx::Rights::WRITE;
96
97        if size == 0 {
98            let vmo = zx::Vmo::create(0)?.replace_handle(read_only_rights)?;
99            return Ok((vmo, None));
100        }
101
102        let extents = match node.data_layout() {
103            InodeDataLayout::FlatPlain => {
104                let offset = node
105                    .blkaddr_offset(block_size, 0)
106                    .map_err(|e| ErofsError::from(e).to_status())?;
107                let aligned_size = size.next_multiple_of(block_size);
108                vec![Extent::new(0..aligned_size, DeviceExtent::Block(self.file_offset + offset))]
109            }
110            InodeDataLayout::FlatInline => {
111                let full_blocks_len = (size / block_size) * block_size;
112                let tail_len = size - full_blocks_len;
113                let mut extents = Vec::with_capacity(2);
114                if full_blocks_len > 0 {
115                    let offset = node
116                        .blkaddr_offset(block_size, 0)
117                        .map_err(|e| ErofsError::from(e).to_status())?;
118                    extents.push(Extent::new(
119                        0..full_blocks_len,
120                        DeviceExtent::Block(self.file_offset + offset),
121                    ));
122                }
123                if tail_len > 0 {
124                    let inline_offset =
125                        node.metadata_end_offset().map_err(|e| ErofsError::from(e).to_status())?;
126                    extents.push(Extent::new(
127                        full_blocks_len..(full_blocks_len + block_size),
128                        DeviceExtent::InlineTail(self.file_offset + inline_offset),
129                    ));
130                }
131                extents
132            }
133            InodeDataLayout::CompressedFull | InodeDataLayout::CompressedCompact => {
134                let vmo = zx::Vmo::create(size)?;
135                let mut buf = vec![0u8; size as usize];
136                fs.read_file_range(node, 0, &mut buf).map_err(ErofsError::to_status)?;
137                vmo.write(&buf, 0)?;
138                let vmo = vmo.replace_handle(read_only_rights)?;
139                return Ok((vmo, None));
140            }
141        };
142
143        let encoded_extents = Extents::try_new(&extents, self.file_offset).map_err(|error| {
144            log::error!(error:?; "Failed to build Extents for EROFS nid {}", node.nid());
145            zx::Status::IO_DATA_INTEGRITY
146        })?;
147        let extent_count = encoded_extents.len() as u32;
148        let allocation_size = extent_count as usize * 8;
149
150        let (key, vmo) = self
151            .session
152            .create_vmo(size, fblock::CreateVmoOptions::empty(), zx::MonotonicInstant::INFINITE)
153            .map_err(|_| zx::Status::PEER_CLOSED)
154            .and_then(|r| r.map_err(zx::Status::err_from_raw))?;
155
156        let commit_res = (|| -> Result<zx::Vmo, zx::Status> {
157            let mut sender = self.sender.lock();
158            let mut payload = sender.reserve_payload(allocation_size)?;
159            for (mut chunk, val) in
160                payload.data().chunks_mut(8).zip(Extents::encode_extents(&encoded_extents))
161            {
162                chunk.copy_from_slice(&val.to_le_bytes());
163            }
164            let cmd = MappingCommand::Mappings {
165                key,
166                offset: payload.offset(),
167                stored_size: size,
168                device_offset: self.file_offset,
169                metadata_count: 0,
170                extent_count,
171                encrypted: false,
172            };
173            payload.commit(cmd.into())?;
174            drop(sender);
175            vmo.replace_handle(read_only_rights)
176        })();
177
178        match commit_res {
179            Ok(vmo) => Ok((vmo, Some(key))),
180            Err(status) => {
181                self.close_file(key);
182                Err(status)
183            }
184        }
185    }
186
187    /// Closes a previously mapped file key in both the block driver and `BlobPagerAndVerifier`.
188    pub fn close_file(&self, key: u64) {
189        let _ = self.sender.lock().push(MappingCommand::CloseBlob { key }.into());
190        let _ = self.session.close_vmo(key);
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197    use crate::readers::VecReader;
198    use crate::{Node, format};
199    use futures::TryStreamExt as _;
200    use std::sync::Arc;
201    use zerocopy::IntoBytes as _;
202    use zerocopy::byteorder::little_endian::{U16 as LEU16, U32 as LEU32, U64 as LEU64};
203
204    fn make_test_fs() -> ErofsFilesystem {
205        let block_size = 4096usize;
206        let num_blocks = 8;
207        let mut image = vec![0u8; num_blocks * block_size];
208
209        let sb = format::SuperBlock {
210            magic: LEU32::new(format::EROFS_MAGIC),
211            checksum: LEU32::new(0),
212            feature_compat: LEU32::new(0),
213            block_size_bits: 12,
214            sb_ext_slots: 0,
215            root_nid: LEU16::new(0),
216            inode_count: LEU64::new(5),
217            epoch: LEU64::new(0),
218            fixed_nsec: LEU32::new(0),
219            blocks: LEU32::new(num_blocks as u32),
220            meta_block_addr: LEU32::new(1),
221            xattr_block_addr: LEU32::new(0),
222            uuid: [0; 16],
223            volume_name: [0; 16],
224            feature_incompat: LEU32::new(0),
225            available_compr_algs: LEU16::new(0),
226            extra_devices: LEU32::new(0),
227            dirblkbits: 0,
228            reserved: [0; 37],
229        };
230        image[1024..1024 + 128].copy_from_slice(sb.as_bytes());
231
232        // Root dir at nid 0 (offset 4096)
233        let root_inode = format::InodeCompact {
234            format: LEU16::new(0),
235            xattr_icount: LEU16::new(0),
236            mode: LEU16::new(0o040755),
237            link_count: LEU16::new(2),
238            size: LEU32::new(0),
239            reserved_1: [0; 4],
240            i_u: [0; 4],
241            ino: LEU32::new(1),
242            uid: LEU16::new(0),
243            gid: LEU16::new(0),
244            reserved_2: [0; 4],
245        };
246        image[4096..4096 + 32].copy_from_slice(root_inode.as_bytes());
247
248        // nid 1 (offset 4128): FlatPlain file, 5000 bytes, starting at block 2 (offset 8192)
249        let plain_inode = format::InodeCompact {
250            format: LEU16::new((InodeDataLayout::FlatPlain as u16) << 1),
251            xattr_icount: LEU16::new(0),
252            mode: LEU16::new(0o100644),
253            link_count: LEU16::new(1),
254            size: LEU32::new(5000),
255            reserved_1: [0; 4],
256            i_u: 2u32.to_le_bytes(),
257            ino: LEU32::new(2),
258            uid: LEU16::new(0),
259            gid: LEU16::new(0),
260            reserved_2: [0; 4],
261        };
262        image[4128..4128 + 32].copy_from_slice(plain_inode.as_bytes());
263
264        // nid 2 (offset 4160): FlatInline tail-only file, 500 bytes (inline data at 4192..4692)
265        let inline_tail_only = format::InodeCompact {
266            format: LEU16::new((InodeDataLayout::FlatInline as u16) << 1),
267            xattr_icount: LEU16::new(0),
268            mode: LEU16::new(0o100644),
269            link_count: LEU16::new(1),
270            size: LEU32::new(500),
271            reserved_1: [0; 4],
272            i_u: 0u32.to_le_bytes(),
273            ino: LEU32::new(3),
274            uid: LEU16::new(0),
275            gid: LEU16::new(0),
276            reserved_2: [0; 4],
277        };
278        image[4160..4160 + 32].copy_from_slice(inline_tail_only.as_bytes());
279
280        // nid 20 (offset 4096 + 20 * 32 = 4736): FlatInline multi-extent file, 4096 + 300 bytes,
281        // full block at block 4 (offset 16384), inline tail at 4768..5068
282        let inline_multi = format::InodeCompact {
283            format: LEU16::new((InodeDataLayout::FlatInline as u16) << 1),
284            xattr_icount: LEU16::new(0),
285            mode: LEU16::new(0o100644),
286            link_count: LEU16::new(1),
287            size: LEU32::new(4096 + 300),
288            reserved_1: [0; 4],
289            i_u: 4u32.to_le_bytes(),
290            ino: LEU32::new(4),
291            uid: LEU16::new(0),
292            gid: LEU16::new(0),
293            reserved_2: [0; 4],
294        };
295        image[4736..4736 + 32].copy_from_slice(inline_multi.as_bytes());
296
297        // nid 40 (offset 4096 + 40 * 32 = 5376): Empty file (0 bytes)
298        let empty_inode = format::InodeCompact {
299            format: LEU16::new((InodeDataLayout::FlatPlain as u16) << 1),
300            xattr_icount: LEU16::new(0),
301            mode: LEU16::new(0o100644),
302            link_count: LEU16::new(1),
303            size: LEU32::new(0),
304            reserved_1: [0; 4],
305            i_u: 0u32.to_le_bytes(),
306            ino: LEU32::new(5),
307            uid: LEU16::new(0),
308            gid: LEU16::new(0),
309            reserved_2: [0; 4],
310        };
311        image[5376..5376 + 32].copy_from_slice(empty_inode.as_bytes());
312
313        ErofsFilesystem::new(Arc::new(VecReader::new(image))).expect("parse test fs")
314    }
315
316    #[fuchsia::test]
317    fn test_volume_mapper_extents_and_caching() {
318        let fs = make_test_fs();
319        let file_offset = 4096u64;
320        let created_vmos = Arc::new(Mutex::new(Vec::<(u64, u64)>::new()));
321        let created_vmos_clone = created_vmos.clone();
322
323        let (receiver_tx, receiver_rx) = std::sync::mpsc::channel();
324        let mapper =
325            VolumeMapper::open_with_session(file_offset, move |mapping_vmo, server_end| {
326                let receiver = vmo_fifo::Receiver::<RawMappingCommand>::new(
327                    mapping_vmo,
328                    PENDING_COMMANDS_CAPACITY,
329                )
330                .expect("create Receiver");
331                receiver_tx.send(receiver).unwrap();
332
333                std::thread::spawn(move || {
334                    let mut executor = fuchsia_async::LocalExecutor::default();
335                    executor.run_singlethreaded(async move {
336                        let mut stream = server_end.into_stream();
337                        let mut next_key = 1u64;
338                        while let Ok(Some(req)) = stream.try_next().await {
339                            match req {
340                                fblock::MapperSessionRequest::CreateVmo {
341                                    size, responder, ..
342                                } => {
343                                    let key = next_key;
344                                    next_key += 1;
345                                    created_vmos_clone.lock().push((key, size));
346                                    let vmo = zx::Vmo::create(size).unwrap();
347                                    let _ = responder.send(Ok((key, vmo)));
348                                }
349                                fblock::MapperSessionRequest::CloseVmo { .. } => {}
350                                fblock::MapperSessionRequest::Close { responder } => {
351                                    let _ = responder.send(Ok(()));
352                                    break;
353                                }
354                                _ => unreachable!(),
355                            }
356                        }
357                    });
358                });
359                Ok(())
360            })
361            .expect("open_with_session");
362
363        let receiver = receiver_rx.recv().unwrap();
364
365        // 1. Empty file (nid 40): creates 0-byte read-only VMO without any driver RPC, and does
366        // not retain the anonymous VMO in `vmos_by_nid`.
367        let Node::File(empty_node) = fs.node(40).unwrap() else { panic!() };
368        let empty_vmo = mapper.get_or_create_vmo(&fs, &empty_node).unwrap();
369        assert_eq!(empty_vmo.get_size().unwrap(), 0);
370        assert!(!empty_vmo.basic_info().unwrap().rights.contains(zx::Rights::WRITE));
371        assert!(created_vmos.lock().is_empty());
372        assert!(!mapper.vmos_by_nid.lock().contains_key(&40));
373
374        // 2. FlatPlain file (nid 1, 5000 bytes at block 2 = 8192):
375        let Node::File(plain_node) = fs.node(1).unwrap() else { panic!() };
376        let plain_vmo1 = mapper.get_or_create_vmo(&fs, &plain_node).unwrap();
377        let plain_vmo2 = mapper.get_or_create_vmo(&fs, &plain_node).unwrap();
378        assert_eq!(plain_vmo1.koid().unwrap(), plain_vmo2.koid().unwrap());
379        assert!(mapper.vmos_by_nid.lock().contains_key(&1));
380        assert!(!plain_vmo1.basic_info().unwrap().rights.contains(zx::Rights::WRITE));
381        assert_eq!(*created_vmos.lock(), vec![(1, 5000)]);
382
383        let msg = receiver.claim().unwrap();
384        let MappingCommand::Mappings {
385            key, offset, stored_size, device_offset, extent_count, ..
386        } = MappingCommand::try_from(*msg).unwrap()
387        else {
388            panic!("expected Mappings command");
389        };
390        assert_eq!(key, 1);
391        assert_eq!(stored_size, 5000);
392        assert_eq!(device_offset, file_offset);
393        let slice = msg.payload_slice(offset, extent_count * 8);
394        let mut words = vec![0u64; extent_count as usize];
395        for (i, word) in words.iter_mut().enumerate() {
396            let mut bytes = [0u8; 8];
397            slice.subslice(i * 8..(i + 1) * 8).copy_to_slice(&mut bytes);
398            *word = u64::from_le_bytes(bytes);
399        }
400        let decoded = Extents::from_encoded(words, device_offset).unwrap();
401        assert_eq!(
402            decoded,
403            Extents::try_new(
404                &[Extent::new(0..8192, DeviceExtent::Block(file_offset + 2 * 4096))],
405                device_offset
406            )
407            .unwrap()
408        );
409        msg.pop().unwrap();
410
411        // 3. FlatInline tail-only file (nid 2, 500 bytes at inline offset 4160 + 32 = 4192):
412        let Node::File(inline_node) = fs.node(2).unwrap() else { panic!() };
413        let _inline_vmo = mapper.get_or_create_vmo(&fs, &inline_node).unwrap();
414        assert_eq!(*created_vmos.lock(), vec![(1, 5000), (2, 500)]);
415
416        let msg = receiver.claim().unwrap();
417        let MappingCommand::Mappings {
418            key, offset, stored_size, device_offset, extent_count, ..
419        } = MappingCommand::try_from(*msg).unwrap()
420        else {
421            panic!("expected Mappings command");
422        };
423        assert_eq!(key, 2);
424        assert_eq!(stored_size, 500);
425        let slice = msg.payload_slice(offset, extent_count * 8);
426        let mut words = vec![0u64; extent_count as usize];
427        for (i, word) in words.iter_mut().enumerate() {
428            let mut bytes = [0u8; 8];
429            slice.subslice(i * 8..(i + 1) * 8).copy_to_slice(&mut bytes);
430            *word = u64::from_le_bytes(bytes);
431        }
432        let decoded = Extents::from_encoded(words, device_offset).unwrap();
433        assert_eq!(
434            decoded,
435            Extents::try_new(
436                &[Extent::new(0..4096, DeviceExtent::InlineTail(file_offset + 4192))],
437                device_offset
438            )
439            .unwrap()
440        );
441        msg.pop().unwrap();
442
443        // 4. FlatInline multi-extent file (nid 20, 4396 bytes: block 4 + inline offset 4768):
444        let Node::File(multi_node) = fs.node(20).unwrap() else { panic!() };
445        let _multi_vmo = mapper.get_or_create_vmo(&fs, &multi_node).unwrap();
446        assert_eq!(*created_vmos.lock(), vec![(1, 5000), (2, 500), (3, 4396)]);
447
448        let msg = receiver.claim().unwrap();
449        let MappingCommand::Mappings {
450            key, offset, stored_size, device_offset, extent_count, ..
451        } = MappingCommand::try_from(*msg).unwrap()
452        else {
453            panic!("expected Mappings command");
454        };
455        assert_eq!(key, 3);
456        assert_eq!(stored_size, 4396);
457        let slice = msg.payload_slice(offset, extent_count * 8);
458        let mut words = vec![0u64; extent_count as usize];
459        for (i, word) in words.iter_mut().enumerate() {
460            let mut bytes = [0u8; 8];
461            slice.subslice(i * 8..(i + 1) * 8).copy_to_slice(&mut bytes);
462            *word = u64::from_le_bytes(bytes);
463        }
464        let decoded = Extents::from_encoded(words, device_offset).unwrap();
465        assert_eq!(
466            decoded,
467            Extents::try_new(
468                &[
469                    Extent::new(0..4096, DeviceExtent::Block(file_offset + 4 * 4096)),
470                    Extent::new(4096..8192, DeviceExtent::InlineTail(file_offset + 4768)),
471                ],
472                device_offset
473            )
474            .unwrap()
475        );
476        msg.pop().unwrap();
477    }
478}