Skip to main content

fxfs_platform_testing/fuchsia/fxblob/
blob.rs

1// Copyright 2023 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 contains the [`FxBlob`] node type used to represent an immutable blob persisted to
6//! disk which can be read back.
7
8use crate::constants::*;
9use crate::fuchsia::directory::FxDirectory;
10use crate::fuchsia::errors::map_to_status;
11use crate::fuchsia::node::{FxNode, OpenedNode};
12use crate::fuchsia::pager::{
13    MarkDirtyRange, PageInRange, PagerBacked, PagerPacketReceiverRegistration, default_page_in,
14};
15use crate::fuchsia::volume::{FxVolume, READ_AHEAD_SIZE};
16use crate::fxblob::atomic_vec::AtomicBitVec;
17use anyhow::{Context, Error, anyhow, bail, ensure};
18use delivery_blob::compression::{CompressionAlgorithm, CompressionInfo};
19use fidl_fuchsia_feedback::{Annotation, Attachment, CrashReport};
20use fidl_fuchsia_mem::Buffer as MemBuffer;
21use fuchsia_component_client::connect_to_protocol;
22use fuchsia_merkle::{Hash, MerkleVerifier, ReadSizedMerkleVerifier};
23use futures::{StreamExt, TryStreamExt, try_join};
24use fxfs::blob_metadata::{BlobFormat, BlobMetadata};
25use fxfs::errors::FxfsError;
26use fxfs::lock_keys;
27use fxfs::log::*;
28use fxfs::object_handle::ObjectHandle;
29use fxfs::object_store::transaction::LockKey;
30use fxfs::object_store::{AttributeId, DataObjectHandle, ObjectDescriptor, StoreObjectHandle};
31use fxfs::round::{round_down, round_up};
32use fxfs_macros::ToWeakNode;
33use mapping::Extent as MappingExtent;
34use std::ops::Range;
35use std::sync::Arc;
36use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
37use storage_device::buffer::{Buffer, BufferFuture, MutableBufferRef};
38use zx::Status;
39
40/// The extent mappings for a blob's data and merkle attributes.
41///
42/// Each `MappingExtent` maps a logical byte range to a physical device offset.
43/// Extents are sorted by logical offset and non-overlapping.
44pub struct BlobExtents {
45    pub data: Vec<MappingExtent>,
46    pub merkle: Vec<MappingExtent>,
47}
48
49// When the top bit of the open count is set, it means the file has been deleted and when the count
50// drops to zero, it will be tombstoned.  Once it has dropped to zero, it cannot be opened again
51// (assertions will fire).
52const PURGED: usize = 1 << (usize::BITS - 1);
53
54/// Represents an immutable blob stored on Fxfs with associated an merkle tree.
55#[derive(ToWeakNode)]
56pub struct FxBlob {
57    handle: StoreObjectHandle<FxVolume>,
58    vmo: zx::Vmo,
59    open_count: AtomicUsize,
60    merkle_root: Hash,
61    merkle_verifier: ReadSizedMerkleVerifier,
62    compression_info: Option<CompressionInfo>,
63    uncompressed_size: u64, // always set.
64    pager_packet_receiver_registration: Arc<PagerPacketReceiverRegistration<Self>>,
65    chunks_supplied: AtomicBitVec,
66}
67
68// Fuchsia can have many open blobs at once. The size of FxBlob is important.
69static_assertions::const_assert!(size_of::<FxBlob>() <= 192);
70
71impl FxBlob {
72    pub async fn new(
73        handle: StoreObjectHandle<FxVolume>,
74        merkle_root: Hash,
75    ) -> Result<Arc<Self>, Error> {
76        let stored_size =
77            handle.store().get_attribute_size(handle.object_id(), AttributeId::DATA).await?;
78        let metadata = BlobMetadata::read_from(&handle).await?;
79        let (uncompressed_size, compression_info) = match &metadata.format {
80            BlobFormat::Uncompressed => (stored_size, None),
81            BlobFormat::ChunkedZstd { uncompressed_size, chunk_size, compressed_offsets } => (
82                *uncompressed_size,
83                Some(CompressionInfo::new(
84                    *chunk_size,
85                    stored_size,
86                    compressed_offsets,
87                    CompressionAlgorithm::Zstd,
88                )?),
89            ),
90            BlobFormat::ChunkedLz4 { uncompressed_size, chunk_size, compressed_offsets } => (
91                *uncompressed_size,
92                Some(CompressionInfo::new(
93                    *chunk_size,
94                    stored_size,
95                    compressed_offsets,
96                    CompressionAlgorithm::Lz4,
97                )?),
98            ),
99        };
100        let merkle_verifier = metadata.into_merkle_verifier(merkle_root)?;
101
102        let min_chunk_size = min_chunk_size(&compression_info);
103        let merkle_verifier =
104            ReadSizedMerkleVerifier::new(merkle_verifier, min_chunk_size as usize)?;
105        let chunks_supplied = AtomicBitVec::new(uncompressed_size.div_ceil(min_chunk_size));
106
107        Ok(Arc::new_cyclic(|weak| {
108            let (vmo, pager_packet_receiver_registration) = handle
109                .owner()
110                .pager()
111                .create_vmo(weak.clone(), uncompressed_size, zx::VmoOptions::empty())
112                .unwrap();
113            set_vmo_name(&vmo, &merkle_root);
114            Self {
115                handle,
116                vmo,
117                open_count: AtomicUsize::new(0),
118                merkle_root,
119                merkle_verifier,
120                compression_info,
121                uncompressed_size,
122                pager_packet_receiver_registration: Arc::new(pager_packet_receiver_registration),
123                chunks_supplied,
124            }
125        }))
126    }
127
128    /// Returns the new blob.
129    pub fn overwrite_me(
130        self: &Arc<Self>,
131        handle: DataObjectHandle<FxVolume>,
132        merkle_verifier: MerkleVerifier,
133        compression_info: Option<CompressionInfo>,
134    ) -> Arc<Self> {
135        let min_chunk_size = min_chunk_size(&compression_info);
136        let merkle_verifier =
137            ReadSizedMerkleVerifier::new(merkle_verifier, min_chunk_size as usize)
138                .expect("The chunk size should have been validated by the delivery blob parser");
139        // The chunk size may have changed between the old blob and the new blob. Preserving the
140        // chunks supplied bits isn't important.
141        let chunks_supplied = AtomicBitVec::new(self.uncompressed_size.div_ceil(min_chunk_size));
142        let vmo = self.vmo.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap();
143
144        let new_blob = Arc::new(Self {
145            handle: handle.into_store_object_handle(),
146            vmo,
147            open_count: AtomicUsize::new(0),
148            merkle_root: self.merkle_root,
149            merkle_verifier,
150            compression_info,
151            uncompressed_size: self.uncompressed_size,
152            pager_packet_receiver_registration: self.pager_packet_receiver_registration.clone(),
153            chunks_supplied,
154        });
155
156        // We have tests that rely on the cache being purged and there are races where the
157        // `FxBlob::drop` isn't called early enough, which can make the test flaky.
158        self.handle.owner().cache().remove(self.as_ref());
159
160        // Lock must be held until the open counts is incremented to prevent concurrent handling of
161        // zero children signals.
162        let receiver_lock =
163            self.pager_packet_receiver_registration.receiver().set_receiver(&new_blob);
164        if receiver_lock.is_strong() {
165            // If there was a strong moved between them, then the counts exchange as well. It is
166            // only important that the increment happen under the lock as it may handle the next
167            // zero children signal, no new requests can now go to the old blob, and because
168            // existing requests hold an open count reference using `try_keep_open` for the duration
169            // of the request, we can immediately decrement the open count of the old blob.
170            new_blob.open_count_add_one();
171            self.clone().open_count_sub_one();
172        }
173        new_blob
174    }
175
176    pub fn root(&self) -> Hash {
177        self.merkle_root
178    }
179
180    /// Returns the extents mapping logical offsets to device offsets for both merkle and data
181    /// attributes. Each `MappingExtent` maps a logical byte range to a physical device offset.
182    /// Extents are sorted by logical offset and non-overlapping.
183    pub async fn get_mapping_extents(&self) -> Result<BlobExtents, Error> {
184        let tree = &self.handle.store().tree();
185        let layer_set = tree.layer_set();
186
187        // Query data extents
188        let mut merger = layer_set.merger();
189        let stream = self.handle.extent_stream(&mut merger, AttributeId::DATA).await?;
190        // TODO(https://fxbug.dev/535489428): Remove use of vec here.
191        let data = stream
192            .map(|res| {
193                let extent = res?;
194                MappingExtent::try_new(extent.logical_range(), Some(extent.device_range().start))
195                    .map_err(|_| anyhow!(FxfsError::Inconsistent))
196            })
197            .try_collect()
198            .await?;
199
200        // Query Merkle extents
201        let mut merger = layer_set.merger();
202        let stream = self.handle.extent_stream(&mut merger, AttributeId::BLOB_METADATA).await?;
203        let merkle = stream
204            .map(|res| {
205                let extent = res?;
206                MappingExtent::try_new(extent.logical_range(), Some(extent.device_range().start))
207                    .map_err(|_| anyhow!(FxfsError::Inconsistent))
208            })
209            .try_collect()
210            .await?;
211
212        Ok(BlobExtents { data, merkle })
213    }
214
215    fn record_page_fault_metric(&self, range: &Range<u64>) {
216        let chunk_size: u64 = min_chunk_size(&self.compression_info);
217
218        let first_chunk = range.start / chunk_size;
219        // The end of the range may not be chunk aligned if it's the last chunk.
220        let last_chunk = range.end.div_ceil(chunk_size);
221
222        let supplied_count = self.chunks_supplied.test_and_set_range(first_chunk, last_chunk);
223
224        if supplied_count > 0 {
225            self.handle
226                .owner()
227                .blob_resupplied_count()
228                .increment(supplied_count, Ordering::Relaxed);
229        }
230    }
231
232    /// Allocates a device buffer for use with hardware/driver reads.
233    fn allocate_buffer(&self, size: u64) -> BufferFuture<'_> {
234        self.handle.store().device().allocate_buffer(size as usize)
235    }
236
237    /// Allocates a trusted buffer that no other processes or drivers can access,
238    /// suitable for zero-copy decompression and Merkle tree verification.
239    fn allocate_trusted_buffer(&self, size: u64) -> BufferFuture<'_> {
240        self.handle.owner().blob_allocator().allocate_buffer(size as usize)
241    }
242
243    async fn read_blocks(&self, offset: u64, buf: MutableBufferRef<'_>) -> Result<(), Error> {
244        let fs = self.handle.store().filesystem();
245        let guard = fs
246            .lock_manager()
247            .read_lock(lock_keys![LockKey::object_attribute(
248                self.handle.store().store_object_id(),
249                self.handle.object_id(),
250                AttributeId::DATA,
251            )])
252            .await;
253        self.handle.read_unchecked(AttributeId::DATA, offset, buf, &guard).await
254    }
255}
256
257impl Drop for FxBlob {
258    fn drop(&mut self) {
259        let volume = self.handle.owner();
260        volume.cache().remove(self);
261    }
262}
263
264impl OpenedNode<FxBlob> {
265    /// Creates a read-only child VMO for this blob backed by the pager. The blob cannot be purged
266    /// until all child VMOs have been destroyed.
267    ///
268    /// *WARNING*: We need to ensure the open count is non-zero before invoking this function, so
269    /// it is only implemented for [`OpenedNode<FxBlob>`]. This prevents the blob from being purged
270    /// before we get a chance to register it with the pager for [`zx::Signals::VMO_ZERO_CHILDREN`].
271    pub fn create_child_vmo(&self) -> Result<zx::Vmo, Status> {
272        let blob = self.0.as_ref();
273        let child_vmo = blob.vmo.create_child(
274            zx::VmoChildOptions::REFERENCE | zx::VmoChildOptions::NO_WRITE,
275            0,
276            0,
277        )?;
278        if blob.handle.owner().pager().watch_for_zero_children(blob).map_err(map_to_status)? {
279            // Take an open count so that we keep this object alive if it is otherwise closed. This
280            // is only valid since we know the current open count is non-zero, otherwise we might
281            // increment the open count after the blob has been purged.
282            blob.open_count_add_one();
283        }
284        Ok(child_vmo)
285    }
286}
287
288impl FxNode for FxBlob {
289    fn object_id(&self) -> u64 {
290        self.handle.object_id()
291    }
292
293    fn parent(&self) -> Option<Arc<FxDirectory>> {
294        unreachable!(); // Add a parent back-reference if needed.
295    }
296
297    fn set_parent(&self, _parent: Arc<FxDirectory>) {
298        // NOP
299    }
300
301    fn open_count_add_one(&self) {
302        let old = self.open_count.fetch_add(1, Ordering::Relaxed);
303        assert!(old != PURGED && old != PURGED - 1);
304    }
305
306    fn open_count_sub_one(self: Arc<Self>) {
307        let old = self.open_count.fetch_sub(1, Ordering::Relaxed);
308        assert!(old & !PURGED > 0);
309        if old == PURGED + 1 {
310            let store = self.handle.store();
311            store
312                .filesystem()
313                .graveyard()
314                .queue_tombstone_object(store.store_object_id(), self.object_id());
315        }
316    }
317
318    fn object_descriptor(&self) -> ObjectDescriptor {
319        ObjectDescriptor::File
320    }
321
322    fn terminate(&self) {
323        self.pager_packet_receiver_registration.stop_watching_for_zero_children();
324    }
325
326    fn mark_to_be_purged(self: Arc<Self>) {
327        let old = self.open_count.fetch_or(PURGED, Ordering::Relaxed);
328        assert!(old & PURGED == 0);
329        if old == 0 {
330            let store = self.handle.store();
331            store
332                .filesystem()
333                .graveyard()
334                .queue_tombstone_object(store.store_object_id(), self.object_id());
335        }
336    }
337}
338
339impl PagerBacked for FxBlob {
340    fn try_keep_open(self: Arc<Self>) -> Result<OpenedNode<Self>, Arc<Self>> {
341        let mut old = self.open_count.load(Ordering::Relaxed);
342        loop {
343            if old & !PURGED == 0 {
344                return Err(self);
345            }
346            assert!(old & !PURGED < PURGED - 1);
347            match self.open_count.compare_exchange_weak(
348                old,
349                old + 1,
350                Ordering::Relaxed,
351                Ordering::Relaxed,
352            ) {
353                Ok(_) => return Ok(OpenedNode(self)),
354                Err(new_value) => old = new_value,
355            }
356        }
357    }
358
359    fn pager(&self) -> &crate::pager::Pager {
360        self.handle.owner().pager()
361    }
362
363    fn pager_packet_receiver_registration(&self) -> &PagerPacketReceiverRegistration<Self> {
364        &self.pager_packet_receiver_registration
365    }
366
367    fn vmo(&self) -> &zx::Vmo {
368        &self.vmo
369    }
370
371    fn page_in(self: Arc<Self>, range: PageInRange<Self>) {
372        let read_ahead_size = if let Some(compression_info) = &self.compression_info {
373            read_ahead_size_for_chunk_size(compression_info.chunk_size(), READ_AHEAD_SIZE)
374        } else {
375            READ_AHEAD_SIZE
376        };
377        // Delegate to the generic page handling code.
378        default_page_in(self, range, read_ahead_size)
379    }
380
381    fn mark_dirty(self: Arc<Self>, _range: MarkDirtyRange<Self>) {
382        unreachable!();
383    }
384
385    fn on_zero_children(self: Arc<Self>) {
386        self.open_count_sub_one();
387    }
388
389    fn byte_size(&self) -> u64 {
390        self.uncompressed_size
391    }
392
393    async fn aligned_read(&self, range: Range<u64>) -> Result<Buffer<'_>, Error> {
394        // The vmo shouldn't have full pages beyond the end of the blob so we shouldn't be getting
395        // page faults for ranges beyond the end of the blob.
396        ensure!(range.start < self.uncompressed_size, FxfsError::InvalidArgs);
397        self.record_page_fault_metric(&range);
398
399        let mut buffer = self.allocate_trusted_buffer(range.end - range.start).await;
400        let unaligned_bytes =
401            (std::cmp::min(range.end, self.uncompressed_size) - range.start) as usize;
402        match &self.compression_info {
403            None => self.read_blocks(range.start, buffer.as_mut()).await?,
404            Some(compression_info) => {
405                let compressed_offsets =
406                    compression_info.compressed_range_for_uncompressed_range(&range)?;
407                let bs = self.handle.block_size();
408                let aligned = round_down(compressed_offsets.start, bs)
409                    ..round_up(compressed_offsets.end, bs).unwrap();
410                let mut compressed_buf = self.allocate_buffer(aligned.end - aligned.start).await;
411
412                let mut decompression_errors = 0;
413                loop {
414                    try_join!(self.read_blocks(aligned.start, compressed_buf.as_mut()), async {
415                        buffer
416                            .allocator()
417                            .buffer_source()
418                            .commit_range(buffer.range())
419                            .map_err(|e| e.into())
420                    })
421                    .with_context(|| {
422                        format!(
423                            "Failed to read compressed range {:?}, len {}",
424                            aligned,
425                            compression_info.compressed_size()
426                        )
427                    })?;
428                    let compressed_buf_range = (compressed_offsets.start - aligned.start) as usize
429                        ..(compressed_offsets.end - aligned.start) as usize;
430
431                    let decompression_result = {
432                        fxfs_trace::duration!("blob-decompress", "len" => unaligned_bytes);
433                        compression_info.decompress(
434                            compressed_buf.as_ptr_slice().subslice(compressed_buf_range),
435                            buffer.subslice_mut(0..unaligned_bytes).try_as_mut_slice().unwrap(),
436                            range.start,
437                        )
438                    };
439                    match decompression_result {
440                        Ok(()) => break,
441                        Err(error) => {
442                            record_decompression_error_crash_report(
443                                &compressed_buf.to_vec(),
444                                &range,
445                                &compressed_offsets,
446                                &self.merkle_root,
447                            )
448                            .await;
449                            decompression_errors += 1;
450                            if decompression_errors == 2 {
451                                bail!(
452                                    anyhow!(FxfsError::IntegrityError)
453                                        .context(format!("Decompression error: {error:?}"))
454                                );
455                            } else {
456                                warn!(error:?; "Decompression error; retrying");
457                            }
458                        }
459                    }
460                } // loop
461                if decompression_errors > 0 {
462                    info!("Read succeeded on second attempt");
463                }
464            }
465        };
466        {
467            // TODO(https://fxbug.dev/42073035): This should be offloaded to the kernel at which
468            // point we can delete this.
469            fxfs_trace::duration!("blob-verify", "len" => unaligned_bytes);
470            self.merkle_verifier.verify(
471                range.start as usize,
472                buffer.subslice(0..unaligned_bytes).try_as_slice().unwrap(),
473            )?;
474        }
475        // Zero the tail.
476        buffer.subslice_mut(unaligned_bytes..buffer.len()).fill(0);
477        Ok(buffer)
478    }
479}
480
481fn set_vmo_name(vmo: &zx::Vmo, merkle_root: &Hash) {
482    let trimmed_merkle = &merkle_root.to_string()[0..BLOB_NAME_HASH_LENGTH];
483    let name = format!("{BLOB_NAME_PREFIX}{trimmed_merkle}");
484    let name = zx::Name::new(&name).unwrap();
485    vmo.set_name(&name).unwrap();
486}
487
488fn min_chunk_size(compression_info: &Option<CompressionInfo>) -> u64 {
489    if let Some(compression_info) = compression_info {
490        read_ahead_size_for_chunk_size(compression_info.chunk_size(), READ_AHEAD_SIZE)
491    } else {
492        READ_AHEAD_SIZE
493    }
494}
495
496fn read_ahead_size_for_chunk_size(chunk_size: u64, suggested_read_ahead_size: u64) -> u64 {
497    if chunk_size >= suggested_read_ahead_size {
498        chunk_size
499    } else {
500        round_down(suggested_read_ahead_size, chunk_size)
501    }
502}
503
504async fn record_decompression_error_crash_report(
505    compressed_buf: &[u8],
506    uncompressed_offsets: &Range<u64>,
507    compressed_offsets: &Range<u64>,
508    merkle_root: &Hash,
509) {
510    static DONE_ONCE: AtomicBool = AtomicBool::new(false);
511    if !DONE_ONCE.swap(true, Ordering::Relaxed) {
512        if let Ok(proxy) = connect_to_protocol::<fidl_fuchsia_feedback::CrashReporterMarker>() {
513            let size = compressed_buf.len() as u64;
514            let vmo = zx::Vmo::create(size).unwrap();
515            vmo.write(compressed_buf, 0).unwrap();
516            if let Err(e) = proxy
517                .file_report(CrashReport {
518                    program_name: Some("fxfs".to_string()),
519                    crash_signature: Some("fuchsia-fxfs-decompression_error".to_string()),
520                    is_fatal: Some(false),
521                    annotations: Some(vec![
522                        Annotation {
523                            key: "fxfs.range".to_string(),
524                            value: format!("{:?}", uncompressed_offsets),
525                        },
526                        Annotation {
527                            key: "fxfs.compressed_offsets".to_string(),
528                            value: format!("{:?}", compressed_offsets),
529                        },
530                        Annotation {
531                            key: "fxfs.merkle_root".to_string(),
532                            value: format!("{}", merkle_root),
533                        },
534                    ]),
535                    attachments: Some(vec![Attachment {
536                        key: "fxfs_compressed_data".to_string(),
537                        value: MemBuffer { vmo, size },
538                    }]),
539                    ..Default::default()
540                })
541                .await
542            {
543                error!(e:?; "Failed to file crash report");
544            } else {
545                warn!("Filed crash report for decompression error");
546            }
547        } else {
548            error!("Failed to connect to crash report service");
549        }
550    }
551}
552
553#[cfg(test)]
554mod tests {
555    use super::*;
556    use crate::fuchsia::fxblob::testing::{BlobFixture, new_blob_fixture};
557    use crate::fuchsia::pager::PageInRange;
558    use crate::fxblob::testing::open_blob_fixture;
559    use assert_matches::assert_matches;
560    use delivery_blob::CompressionMode;
561    use delivery_blob::compression::{ChunkedArchiveOptions, CompressionAlgorithm};
562
563    use fuchsia_async::epoch::Epoch;
564    use fxfs_make_blob_image::FxBlobBuilder;
565    use storage_device::DeviceHolder;
566    use storage_device::fake_device::FakeDevice;
567
568    const BLOCK_SIZE: u64 = fuchsia_merkle::BLOCK_SIZE as u64;
569    const CHUNK_SIZE: usize = 32 * 1024;
570
571    #[fuchsia::test(threads = 10)]
572    async fn test_empty_blob() {
573        let fixture = new_blob_fixture().await;
574
575        let data = vec![];
576        let hash = fixture.write_blob(&data, CompressionMode::Never).await;
577        assert_eq!(fixture.read_blob(hash).await, data);
578
579        fixture.close().await;
580    }
581
582    #[fuchsia::test(threads = 10)]
583    async fn test_large_blob() {
584        let fixture = new_blob_fixture().await;
585
586        let data = vec![3; 3_000_000];
587        let hash = fixture.write_blob(&data, CompressionMode::Never).await;
588
589        assert_eq!(fixture.read_blob(hash).await, data);
590
591        fixture.close().await;
592    }
593
594    #[fuchsia::test(threads = 10)]
595    async fn test_get_multiple_mapping_extents() {
596        let fixture = new_blob_fixture().await;
597
598        {
599            let data = vec![0xff; 3_000_000];
600            let hash = fixture.write_blob(&data, CompressionMode::Never).await;
601            let name = format!("{}", hash);
602
603            let handle = fixture.get_blob_handle(&name).await;
604            let mut transaction =
605                handle.new_transaction().await.expect("failed to create transaction");
606            let mut buf = handle.allocate_buffer(8192).await;
607            buf.as_mut_ptr_slice().fill(0xaa);
608            handle
609                .txn_write(&mut transaction, 0, buf.as_ref())
610                .await
611                .expect("txn_write at offset 0 failed");
612            handle
613                .txn_write(&mut transaction, 16384, buf.as_ref())
614                .await
615                .expect("txn_write at offset 16384 failed");
616            transaction.commit().await.expect("failed to commit transaction");
617
618            let blob = fixture.get_blob(hash).await.expect("getting blob failed");
619            let extents = blob.get_mapping_extents().await.expect("get_mapping_extents failed");
620
621            assert!(!extents.merkle.is_empty(), "Expected at least one merkle extent");
622            assert!(
623                extents.data.len() > 1,
624                "Expected multiple data extents, got {}",
625                extents.data.len()
626            );
627
628            // The overwritten extents should each be exactly 8192 bytes.
629            let overwrite_extent = extents
630                .data
631                .iter()
632                .find(|e| e.logical_range().start == 0)
633                .expect("extent at offset 0");
634            assert_eq!(
635                overwrite_extent.logical_range().end - overwrite_extent.logical_range().start,
636                8192
637            );
638            let overwrite_extent = extents
639                .data
640                .iter()
641                .find(|e| e.logical_range().start == 16384)
642                .expect("extent at offset 16384");
643            assert_eq!(
644                overwrite_extent.logical_range().end - overwrite_extent.logical_range().start,
645                8192
646            );
647        }
648
649        fixture.close().await;
650    }
651
652    #[fuchsia::test(threads = 10)]
653    async fn test_large_compressed_blob() {
654        let fixture = new_blob_fixture().await;
655
656        let data = vec![3; 3_000_000];
657        let hash = fixture.write_blob(&data, CompressionMode::Always).await;
658
659        assert_eq!(fixture.read_blob(hash).await, data);
660
661        fixture.close().await;
662    }
663
664    #[fuchsia::test(threads = 10)]
665    async fn test_non_page_aligned_blob() {
666        let fixture = new_blob_fixture().await;
667
668        let page_size = zx::system_get_page_size() as usize;
669        let data = vec![0xffu8; page_size - 1];
670        let hash = fixture.write_blob(&data, CompressionMode::Never).await;
671        assert_eq!(fixture.read_blob(hash).await, data);
672
673        {
674            let vmo = fixture.get_blob_vmo(hash).await;
675            let mut buf = vec![0x11u8; page_size];
676            vmo.read(&mut buf[..], 0).expect("vmo read failed");
677            assert_eq!(data, buf[..data.len()]);
678            // Ensure the tail is zeroed
679            assert_eq!(buf[data.len()], 0);
680        }
681
682        fixture.close().await;
683    }
684
685    #[fuchsia::test(threads = 10)]
686    async fn test_blob_invalid_contents() {
687        let fixture = new_blob_fixture().await;
688
689        let data = vec![0xffu8; (READ_AHEAD_SIZE + BLOCK_SIZE) as usize];
690        let hash = fixture.write_blob(&data, CompressionMode::Never).await;
691        let name = format!("{}", hash);
692
693        {
694            // Overwrite the second read-ahead window.  The first window should successfully verify.
695            let handle = fixture.get_blob_handle(&name).await;
696            let mut transaction =
697                handle.new_transaction().await.expect("failed to create transaction");
698            let mut buf = handle.allocate_buffer(BLOCK_SIZE as usize).await;
699            buf.fill(0);
700            handle
701                .txn_write(&mut transaction, READ_AHEAD_SIZE, buf.as_ref())
702                .await
703                .expect("txn_write failed");
704            transaction.commit().await.expect("failed to commit transaction");
705        }
706
707        {
708            let blob_vmo = fixture.get_blob_vmo(hash).await;
709            let mut buf = vec![0; BLOCK_SIZE as usize];
710            assert_matches!(blob_vmo.read(&mut buf[..], 0), Ok(_));
711            assert_matches!(
712                blob_vmo.read(&mut buf[..], READ_AHEAD_SIZE),
713                Err(zx::Status::IO_DATA_INTEGRITY)
714            );
715        }
716
717        fixture.close().await;
718    }
719
720    #[fuchsia::test(threads = 10)]
721    async fn test_lz4_blob() {
722        let device = DeviceHolder::new(FakeDevice::new(16384, 512));
723        let blob_data = vec![0xAA; 68 * 1024];
724        let fxblob_builder = FxBlobBuilder::new(device).await.unwrap();
725        let blob = fxblob_builder
726            .generate_blob(blob_data.clone(), Some(CompressionAlgorithm::Lz4))
727            .unwrap();
728        let blob_hash = blob.hash();
729        fxblob_builder.install_blob(&blob).await.unwrap();
730        let device = fxblob_builder.finalize().await.unwrap().0;
731        device.reopen(/*read_only=*/ false);
732        let fixture = open_blob_fixture(device).await;
733
734        assert_eq!(fixture.read_blob(blob_hash).await, blob_data);
735
736        fixture.close().await;
737    }
738
739    #[fuchsia::test(threads = 10)]
740    async fn test_blob_vmos_are_immutable() {
741        let fixture = new_blob_fixture().await;
742
743        let data = vec![0xffu8; 500];
744        let hash = fixture.write_blob(&data, CompressionMode::Never).await;
745        let blob_vmo = fixture.get_blob_vmo(hash).await;
746
747        // The VMO shouldn't be resizable.
748        assert_matches!(blob_vmo.set_size(20), Err(_));
749
750        // The VMO shouldn't be writable.
751        assert_matches!(blob_vmo.write(b"overwrite", 0), Err(_));
752
753        // The VMO's content size shouldn't be modifiable.
754        assert_matches!(blob_vmo.set_stream_size(20), Err(_));
755
756        fixture.close().await;
757    }
758
759    const COMPRESSED_BLOB_CHUNK_SIZE: u64 = 32 * 1024;
760    const MAX_SMALL_OFFSET: u64 = u32::MAX as u64;
761    const ZSTD: CompressionAlgorithm = CompressionAlgorithm::Zstd;
762
763    #[fuchsia::test]
764    fn test_compression_info_offsets_must_start_with_zero() {
765        assert!(CompressionInfo::new(COMPRESSED_BLOB_CHUNK_SIZE, 100, &[], ZSTD).is_err());
766        assert!(CompressionInfo::new(COMPRESSED_BLOB_CHUNK_SIZE, 100, &[1], ZSTD).is_err());
767        assert!(CompressionInfo::new(COMPRESSED_BLOB_CHUNK_SIZE, 100, &[0], ZSTD).is_ok());
768    }
769
770    #[fuchsia::test]
771    fn test_compression_info_offsets_must_be_sorted() {
772        assert!(CompressionInfo::new(COMPRESSED_BLOB_CHUNK_SIZE, 100, &[0, 1, 2], ZSTD).is_ok());
773        assert!(CompressionInfo::new(COMPRESSED_BLOB_CHUNK_SIZE, 100, &[0, 2, 1], ZSTD).is_err());
774        assert!(CompressionInfo::new(COMPRESSED_BLOB_CHUNK_SIZE, 100, &[0, 1, 1], ZSTD).is_err());
775    }
776
777    #[fuchsia::test]
778    fn test_compression_info_compressed_range_for_uncompressed_range() {
779        fn check_compression_ranges(
780            offsets: &[u64],
781            compressed_size: u64,
782            expected_ranges: &[Range<u64>],
783            chunk_size: u64,
784            read_ahead_size: u64,
785        ) {
786            let compression_info =
787                CompressionInfo::new(chunk_size, compressed_size, offsets, ZSTD).unwrap();
788            for (i, expected_range) in expected_ranges.iter().enumerate() {
789                let i = i as u64;
790                let result = compression_info
791                    .compressed_range_for_uncompressed_range(
792                        &(i * read_ahead_size..(i + 1) * read_ahead_size),
793                    )
794                    .unwrap();
795                assert_eq!(&result, expected_range);
796            }
797        }
798        check_compression_ranges(
799            &[0, 10, 20, 30],
800            30,
801            &[0..10, 10..20, 20..30, 30..30],
802            COMPRESSED_BLOB_CHUNK_SIZE,
803            COMPRESSED_BLOB_CHUNK_SIZE,
804        );
805        check_compression_ranges(
806            &[0, 10, 20, 30],
807            30,
808            &[0..20, 20..30],
809            COMPRESSED_BLOB_CHUNK_SIZE,
810            COMPRESSED_BLOB_CHUNK_SIZE * 2,
811        );
812        check_compression_ranges(
813            &[0, 10, 20, 30],
814            30,
815            &[0..30],
816            COMPRESSED_BLOB_CHUNK_SIZE,
817            COMPRESSED_BLOB_CHUNK_SIZE * 4,
818        );
819        check_compression_ranges(
820            &[0, 10, 20, 30, MAX_SMALL_OFFSET + 10],
821            MAX_SMALL_OFFSET + 10,
822            &[0..MAX_SMALL_OFFSET + 10, MAX_SMALL_OFFSET + 10..MAX_SMALL_OFFSET + 10],
823            COMPRESSED_BLOB_CHUNK_SIZE,
824            COMPRESSED_BLOB_CHUNK_SIZE * 4,
825        );
826        check_compression_ranges(
827            &[
828                0,
829                10,
830                20,
831                30,
832                MAX_SMALL_OFFSET + 10,
833                MAX_SMALL_OFFSET + 20,
834                MAX_SMALL_OFFSET + 30,
835                MAX_SMALL_OFFSET + 40,
836                MAX_SMALL_OFFSET + 50,
837            ],
838            MAX_SMALL_OFFSET + 50,
839            &[
840                0..20,
841                20..MAX_SMALL_OFFSET + 10,
842                MAX_SMALL_OFFSET + 10..MAX_SMALL_OFFSET + 30,
843                MAX_SMALL_OFFSET + 30..MAX_SMALL_OFFSET + 50,
844            ],
845            COMPRESSED_BLOB_CHUNK_SIZE,
846            COMPRESSED_BLOB_CHUNK_SIZE * 2,
847        );
848    }
849
850    #[fuchsia::test]
851    fn test_compression_info_compressed_range_for_uncompressed_range_errors() {
852        let compression_info = CompressionInfo::new(
853            COMPRESSED_BLOB_CHUNK_SIZE,
854            MAX_SMALL_OFFSET + 50,
855            &[
856                0,
857                10,
858                20,
859                30,
860                MAX_SMALL_OFFSET + 10,
861                MAX_SMALL_OFFSET + 20,
862                MAX_SMALL_OFFSET + 30,
863                MAX_SMALL_OFFSET + 40,
864                MAX_SMALL_OFFSET + 50,
865            ],
866            ZSTD,
867        )
868        .unwrap();
869
870        // The start of reads must be chunk aligned.
871        assert!(
872            compression_info
873                .compressed_range_for_uncompressed_range(&(1..COMPRESSED_BLOB_CHUNK_SIZE),)
874                .is_err()
875        );
876
877        // Reading entirely past the last offset isn't allowed.
878        assert!(
879            compression_info
880                .compressed_range_for_uncompressed_range(
881                    &(COMPRESSED_BLOB_CHUNK_SIZE * 9..COMPRESSED_BLOB_CHUNK_SIZE * 12),
882                )
883                .is_err()
884        );
885
886        // Reading a different amount than the read-ahead size isn't allowed for middle offsets.
887        assert!(
888            compression_info
889                .compressed_range_for_uncompressed_range(&(0..COMPRESSED_BLOB_CHUNK_SIZE + 1),)
890                .is_err()
891        );
892        assert!(
893            compression_info
894                .compressed_range_for_uncompressed_range(&(0..COMPRESSED_BLOB_CHUNK_SIZE - 1),)
895                .is_err()
896        );
897        assert!(
898            compression_info
899                .compressed_range_for_uncompressed_range(
900                    &(COMPRESSED_BLOB_CHUNK_SIZE..COMPRESSED_BLOB_CHUNK_SIZE * 2 + 1),
901                )
902                .is_err()
903        );
904        assert!(
905            compression_info
906                .compressed_range_for_uncompressed_range(
907                    &(COMPRESSED_BLOB_CHUNK_SIZE..COMPRESSED_BLOB_CHUNK_SIZE * 2 - 1),
908                )
909                .is_err()
910        );
911
912        // Reading less than the read-ahead size for the last offset is allowed.
913        assert!(
914            compression_info
915                .compressed_range_for_uncompressed_range(
916                    &(COMPRESSED_BLOB_CHUNK_SIZE * 8..COMPRESSED_BLOB_CHUNK_SIZE * 8 + 4096),
917                )
918                .is_ok()
919        );
920    }
921
922    #[fuchsia::test]
923    fn test_read_ahead_size_for_chunk_size() {
924        assert_eq!(read_ahead_size_for_chunk_size(32 * 1024, 32 * 1024), 32 * 1024);
925        assert_eq!(read_ahead_size_for_chunk_size(48 * 1024, 32 * 1024), 48 * 1024);
926        assert_eq!(read_ahead_size_for_chunk_size(64 * 1024, 32 * 1024), 64 * 1024);
927
928        assert_eq!(read_ahead_size_for_chunk_size(32 * 1024, 64 * 1024), 64 * 1024);
929        assert_eq!(read_ahead_size_for_chunk_size(48 * 1024, 64 * 1024), 48 * 1024);
930        assert_eq!(read_ahead_size_for_chunk_size(64 * 1024, 64 * 1024), 64 * 1024);
931        assert_eq!(read_ahead_size_for_chunk_size(96 * 1024, 64 * 1024), 96 * 1024);
932
933        assert_eq!(read_ahead_size_for_chunk_size(32 * 1024, 128 * 1024), 128 * 1024);
934        assert_eq!(read_ahead_size_for_chunk_size(48 * 1024, 128 * 1024), 96 * 1024);
935        assert_eq!(read_ahead_size_for_chunk_size(64 * 1024, 128 * 1024), 128 * 1024);
936        assert_eq!(read_ahead_size_for_chunk_size(96 * 1024, 128 * 1024), 96 * 1024);
937    }
938
939    fn build_compression_info(size: usize) -> (CompressionInfo, Vec<u8>, Vec<u8>) {
940        let options =
941            ChunkedArchiveOptions::V3 { compression_algorithm: CompressionAlgorithm::Lz4 };
942        let mut compressor = options.compressor();
943        let mut uncompressed_data = Vec::with_capacity(size);
944        {
945            let mut run_length = 1;
946            let mut run_value: u8 = 0;
947            while uncompressed_data.len() < size {
948                uncompressed_data
949                    .resize(std::cmp::min(uncompressed_data.len() + run_length, size), run_value);
950                run_length = (run_length + 1) % 19 + 1;
951                run_value = (run_value + 1) % 17;
952            }
953        }
954        let mut compressed_offsets = vec![0];
955        let mut compressed_data = vec![];
956        for chunk in uncompressed_data.chunks(CHUNK_SIZE) {
957            let mut compressed_chunk = compressor.compress(chunk, 0).unwrap();
958            compressed_data.append(&mut compressed_chunk);
959            compressed_offsets.push(compressed_data.len() as u64);
960        }
961        compressed_offsets.pop();
962        let compressed_size = compressed_data.len() as u64;
963        (
964            CompressionInfo::new(
965                CHUNK_SIZE as u64,
966                compressed_size,
967                &compressed_offsets,
968                CompressionAlgorithm::Lz4,
969            )
970            .unwrap(),
971            compressed_data,
972            uncompressed_data,
973        )
974    }
975
976    #[fuchsia::test]
977    fn test_compression_info_decompress_single_chunk() {
978        let (compression_info, compressed_data, uncompressed_data) =
979            build_compression_info(CHUNK_SIZE);
980        let mut decompressed_data = vec![0u8; CHUNK_SIZE + 1];
981
982        compression_info
983            .decompress(&compressed_data[..], &mut decompressed_data[0..CHUNK_SIZE], 0)
984            .expect("failed to decompress");
985        assert_eq!(uncompressed_data, decompressed_data[0..CHUNK_SIZE]);
986
987        // Too small of destination buffer.
988        compression_info
989            .decompress(&compressed_data[..], &mut decompressed_data[0..CHUNK_SIZE - 1], 0)
990            .expect_err("decompression should fail");
991
992        // Too large of destination buffer.
993        compression_info
994            .decompress(&compressed_data[..], &mut decompressed_data[0..CHUNK_SIZE - 1], 0)
995            .expect_err("decompression should fail");
996    }
997
998    #[fuchsia::test]
999    fn test_compression_info_decompress_multiple_chunks() {
1000        fn slice_for_chunks<'a>(
1001            compressed_data: &'a [u8],
1002            compression_info: &CompressionInfo,
1003            chunks: Range<u64>,
1004        ) -> &'a [u8] {
1005            let range = compression_info
1006                .compressed_range_for_uncompressed_range(
1007                    &(chunks.start * CHUNK_SIZE as u64..chunks.end * CHUNK_SIZE as u64),
1008                )
1009                .unwrap();
1010            &compressed_data[range.start as usize..range.end as usize]
1011        }
1012
1013        const BLOB_SIZE: usize = CHUNK_SIZE * 4 + 4096;
1014        let (compression_info, compressed_data, uncompressed_data) =
1015            build_compression_info(BLOB_SIZE);
1016        let mut decompressed_data = vec![0u8; BLOB_SIZE];
1017
1018        // Decompress the entire blob.
1019        compression_info
1020            .decompress(&compressed_data[..], &mut decompressed_data[..], 0)
1021            .expect("failed to decompress");
1022        assert_eq!(uncompressed_data, decompressed_data);
1023
1024        // Decompress just the whole chunks.
1025        compression_info
1026            .decompress(
1027                slice_for_chunks(&compressed_data, &compression_info, 0..4),
1028                &mut decompressed_data[0..CHUNK_SIZE * 4],
1029                0,
1030            )
1031            .expect("failed to decompress");
1032        assert_eq!(&uncompressed_data[0..CHUNK_SIZE], &decompressed_data[0..CHUNK_SIZE]);
1033
1034        // Too small of destination buffer for whole chunks.
1035        compression_info
1036            .decompress(
1037                slice_for_chunks(&compressed_data, &compression_info, 0..4),
1038                &mut decompressed_data[0..CHUNK_SIZE * 4 - 1],
1039                0,
1040            )
1041            .expect_err("decompression should fail");
1042
1043        // Too large of destination buffer for whole chunks.
1044        compression_info
1045            .decompress(
1046                slice_for_chunks(&compressed_data, &compression_info, 0..4),
1047                &mut decompressed_data[0..CHUNK_SIZE * 4 + 1],
1048                0,
1049            )
1050            .expect_err("decompression should fail");
1051
1052        // Decompress just the tail.
1053        let partial_chunk = slice_for_chunks(&compressed_data, &compression_info, 4..5);
1054        compression_info
1055            .decompress(partial_chunk, &mut decompressed_data[0..4096], CHUNK_SIZE as u64 * 4)
1056            .expect("failed to decompress");
1057        assert_eq!(&uncompressed_data[CHUNK_SIZE * 4..], &decompressed_data[0..4096]);
1058
1059        // Too small of destination buffer for the tail.
1060        compression_info
1061            .decompress(partial_chunk, &mut decompressed_data[0..4095], CHUNK_SIZE as u64 * 4)
1062            .expect_err("decompression should fail");
1063
1064        // Too large of destination buffer for the tail.
1065        compression_info
1066            .decompress(partial_chunk, &mut decompressed_data[0..4097], CHUNK_SIZE as u64 * 4)
1067            .expect_err("decompression should fail");
1068    }
1069
1070    #[fuchsia::test(threads = 10)]
1071    async fn test_refault_metric() {
1072        let fixture = new_blob_fixture().await;
1073        {
1074            let volume = fixture.volume().volume().clone();
1075            const FILE_SIZE: u64 = READ_AHEAD_SIZE * 4 - 4096;
1076            let data = vec![0xffu8; FILE_SIZE as usize];
1077            let hash = fixture.write_blob(&data, CompressionMode::Never).await;
1078
1079            let blob = fixture.get_opened_blob(hash).await.unwrap();
1080            assert_eq!(blob.chunks_supplied.len(), 4);
1081            // Nothing has been read yet.
1082            assert_eq!(&blob.chunks_supplied.get(), &[false, false, false, false]);
1083
1084            blob.vmo.read_to_vec::<u8>(4096, 4096).unwrap();
1085
1086            assert_eq!(&blob.chunks_supplied.get(), &[true, false, false, false]);
1087
1088            blob.vmo.read_to_vec::<u8>(READ_AHEAD_SIZE * 2 + 4096, READ_AHEAD_SIZE).unwrap();
1089            assert_eq!(&blob.chunks_supplied.get(), &[true, false, true, true]);
1090
1091            // We have loaded pages, but only once each.
1092            assert_eq!(volume.blob_resupplied_count().read(Ordering::SeqCst), 0);
1093
1094            // Re-read some pages.
1095
1096            // We can't evict pages from the VMO to get the kernel to resupply them but we can call
1097            // page_in directly and wait for the counters to change.
1098            blob.clone().page_in(PageInRange::new(
1099                FILE_SIZE - READ_AHEAD_SIZE..FILE_SIZE,
1100                blob.dup(),
1101                Epoch::global().guard(),
1102            ));
1103            Epoch::global().barrier().await;
1104
1105            assert_eq!(volume.blob_resupplied_count().read(Ordering::SeqCst), 2);
1106        }
1107
1108        fixture.close().await;
1109    }
1110}