composefs/fsverity/
mod.rs

1//! Linux fs-verity support for integrity verification.
2//!
3//! This module provides complete fs-verity functionality including userspace
4//! digest computation, kernel ioctl interfaces for enabling and measuring
5//! verity, and hash value types for SHA-256 and SHA-512.
6
7mod digest;
8mod hashvalue;
9mod ioctl;
10
11use std::{
12    fs::File,
13    io::{Error, Seek},
14    os::{
15        fd::{AsFd, BorrowedFd, OwnedFd},
16        unix::fs::PermissionsExt,
17    },
18};
19
20use rustix::fs::{open, openat, Mode, OFlags};
21use thiserror::Error;
22
23pub use hashvalue::{FsVerityHashValue, Sha256HashValue, Sha512HashValue};
24
25use crate::util::proc_self_fd;
26
27/// Measuring fsverity failed.
28#[derive(Error, Debug)] // can't derive PartialEq because of std::io::Error
29pub enum MeasureVerityError {
30    /// I/O operation failed.
31    #[error("{0}")]
32    Io(#[from] Error),
33    /// fs-verity is not enabled on the file.
34    #[error("fs-verity is not enabled on file")]
35    VerityMissing,
36    /// The filesystem does not support fs-verity.
37    #[error("fs-verity is not supported by filesystem")]
38    FilesystemNotSupported,
39    /// The hash algorithm does not match the expected algorithm.
40    #[error("Expected algorithm {expected}, found {found}")]
41    InvalidDigestAlgorithm {
42        /// The expected algorithm identifier.
43        expected: u16,
44        /// The actual algorithm identifier found.
45        found: u16,
46    },
47    /// The digest size does not match the expected size.
48    #[error("Expected digest size {expected}")]
49    InvalidDigestSize {
50        /// The expected digest size in bytes.
51        expected: u16,
52    },
53}
54
55/// Enabling fsverity failed.
56#[derive(Error, Debug)]
57pub enum EnableVerityError {
58    /// I/O operation failed.
59    #[error("{0}")]
60    Io(#[from] Error),
61    /// The filesystem does not support fs-verity.
62    #[error("Filesystem does not support fs-verity")]
63    FilesystemNotSupported,
64    /// fs-verity is already enabled on the file.
65    #[error("fs-verity is already enabled on file")]
66    AlreadyEnabled,
67    /// The file has an open writable file descriptor.
68    #[error("File is opened for writing")]
69    FileOpenedForWrite,
70}
71
72/// A verity comparison failed.
73#[derive(Error, Debug)]
74pub enum CompareVerityError {
75    /// Failed to measure the fs-verity digest.
76    #[error("failed to read verity")]
77    Measure(#[from] MeasureVerityError),
78    /// The measured digest does not match the expected digest.
79    #[error("Expected digest {expected} but found {found}")]
80    DigestMismatch {
81        /// The expected digest as a hex string.
82        expected: String,
83        /// The actual digest found as a hex string.
84        found: String,
85    },
86}
87
88/// Compute the fs-verity digest for a given block of data, in userspace.
89///
90/// The fs-verity digest is a cryptographic hash over the fs-verity descriptor, which itself
91/// contains the root hash of a Merkle tree with an arity determined by the chosen block size and
92/// the output size of the chosen hash algorithm.
93///
94/// It's possible to choose the hash algorithm (via the generic parameter) but the blocksize is
95/// currently hardcoded to 4096.  Salt is not supported.
96///
97/// See <https://www.kernel.org/doc/html/latest/filesystems/fsverity.html#file-digest-computation>
98///
99/// # Arguments:
100///
101///  * `data`: the data to hash
102pub fn compute_verity<H: FsVerityHashValue>(data: &[u8]) -> H {
103    digest::FsVerityHasher::<H, 12>::hash(data)
104}
105
106/// Enable fs-verity on the given file.
107///
108/// This essentially boils down to the FS_IOC_ENABLE_VERITY ioctl.
109///
110/// The file must be stored on a filesystem which supports fs-verity.  The file descriptor must be
111/// opened O_RDONLY and there must be no other writable file descriptors or mappings for the file.
112///
113/// It's possible to choose the hash algorithm (via the generic parameter) but the blocksize is
114/// currently hardcoded to 4096.  Salt is not supported.
115pub fn enable_verity_raw<H: FsVerityHashValue>(fd: impl AsFd) -> Result<(), EnableVerityError> {
116    ioctl::fs_ioc_enable_verity::<H>(fd)
117}
118
119/// Enable fs-verity on the given file, retrying if file is opened for writing.
120///
121/// This uses `enable_verity_raw()` and is subject to the same restrictions and features.
122///
123/// A common pattern with fsverity files is:
124///
125/// * Open a read-write file descriptor
126/// * Write data to the read-write file descriptor
127/// * Re-open the file descriptor as a new read-only descriptor
128/// * Close the read-write file descriptor
129/// * Enable fsverity on the read-only file descriptor
130///
131/// However, in a multi-threaded program, it is possible that another
132/// thread calls `fork()` while the read-write descriptor is valid,
133/// thus making a copy of the read-write descriptor.  If the forked
134/// process does not close the file descriptor either explicitly or by
135/// calling `exec()` via O_CLOEXEC, then attempting to enable fsverity
136/// on the read-only file descriptor will fail with ETXTBSY.  It is
137/// generally assumed that the file descriptor will be closed rather
138/// quickly under these circumstances, so this function will try to
139/// enable verity three times, pausing for one millisecond between
140/// attempts.
141pub fn enable_verity_with_retry<H: FsVerityHashValue>(
142    fd: impl AsFd,
143) -> Result<(), EnableVerityError> {
144    let mut attempt = 1;
145    loop {
146        match enable_verity_raw::<H>(&fd) {
147            Err(EnableVerityError::FileOpenedForWrite) if attempt < 3 => {
148                std::thread::sleep(std::time::Duration::from_millis(1));
149                attempt += 1;
150            }
151            other => return other,
152        }
153    }
154}
155
156/// Enable fs-verity on the given file.  If the given file cannot be
157/// enabled because it is opened as writable, then a new copy of the
158/// file will be returned instead.  No attempt is made to sync the
159/// copied file contents to disk, it is up to the caller to do so if
160/// desired.
161///
162/// Take special note that in the case where a copied file descriptor
163/// is returned, the returned file is created as a tempfile and is
164/// unlinked.  Presumably the caller should take care to make this
165/// file permanent, using a combination of `linkat` and `renameat` to
166/// replace the original file.
167///
168/// This uses `enable_verity_raw()` and `enable_verity_with_retry()`
169/// and is subject to the same restrictions.
170///
171/// # Arguments:
172/// * `dirfd`: A directory file descriptor, used to determine the placement (via O_TMPFILE) of the new file (if necessary).
173/// * `fd`: The file decriptor to enable verity on
174/// # Return Value:
175/// * `Ok(None)` is returned if verity was enabled on the original file
176/// * `Ok(Some(OwnedFd))` is returned if a copy was made
177pub fn enable_verity_maybe_copy<H: FsVerityHashValue>(
178    dirfd: impl AsFd,
179    fd: BorrowedFd,
180) -> Result<Option<OwnedFd>, EnableVerityError> {
181    match enable_verity_with_retry::<H>(&fd) {
182        Ok(()) => Ok(None),
183        Err(EnableVerityError::FileOpenedForWrite) => {
184            let fd = enable_verity_on_copy::<H>(dirfd, fd)?;
185            Ok(Some(fd))
186        }
187        Err(other) => Err(other),
188    }
189}
190
191/// Enable fs-verity on a new copy of `fd`, consuming `fd` and
192/// returning the new copy.  The copy is created via O_TMPFILE
193/// relative to `dirfd`.
194fn enable_verity_on_copy<H: FsVerityHashValue>(
195    dirfd: impl AsFd,
196    fd: BorrowedFd,
197) -> Result<OwnedFd, EnableVerityError> {
198    let fd = fd.try_clone_to_owned().map_err(EnableVerityError::Io)?;
199    let mut fd = File::from(fd);
200    let mode = fd.metadata()?.permissions().mode();
201
202    loop {
203        fd.rewind().map_err(EnableVerityError::Io)?;
204
205        let mut new_rw_fd = File::from(
206            openat(
207                &dirfd,
208                ".",
209                OFlags::CLOEXEC | OFlags::RDWR | OFlags::TMPFILE,
210                mode.into(),
211            )
212            .map_err(|e| EnableVerityError::Io(e.into()))?,
213        );
214
215        std::io::copy(&mut fd, &mut new_rw_fd)?;
216        let new_ro_fd = open(
217            proc_self_fd(&new_rw_fd),
218            OFlags::RDONLY | OFlags::CLOEXEC,
219            Mode::empty(),
220        )
221        .map_err(|e| EnableVerityError::Io(e.into()))?;
222        drop(new_rw_fd);
223        if enable_verity_with_retry::<H>(&new_ro_fd).is_ok() {
224            return Ok(new_ro_fd);
225        }
226    }
227}
228
229/// Measures fs-verity on the given file.
230///
231/// This essentially boils down to the FS_IOC_MEASURE_VERITY ioctl.
232///
233/// If the file has fs-verity enabled then the hash of the fs-verity descriptor is reported as the
234/// successful return value.  In this case, the kernel guarantees that the file content cannot
235/// possibly change for as long as the file descriptor exists.
236///
237/// If the file doesn't have fs-verity enabled then an error will be returned.
238///
239/// This function is generic over the hash algorithm, which means that you need to choose the
240/// expected hash algorithm in advance.  If the file has fs-verity enabled, but with a different
241/// hash algorithm, then this is also considered an error.
242///
243/// For a version of this function which returns an Option<> depending on if fs-verity is enabled
244/// or not, see `measure_verity_opt()`.
245///
246/// Simply measuring the fs-verity value of a file is not a common operation: you usually want to
247/// compare it to a value that you already know.  In that case, it's better to use the
248/// `compare_verity()` function in this module.
249pub fn measure_verity<H: FsVerityHashValue>(fd: impl AsFd) -> Result<H, MeasureVerityError> {
250    ioctl::fs_ioc_measure_verity(fd)
251}
252
253/// Measures fs-verity on the given file.
254///
255/// This essentially boils down to the FS_IOC_MEASURE_VERITY ioctl.
256///
257/// This is the `_opt()` variant of `measure_verity()`.  If the file doesn't have fs-verity
258/// enabled, or resides on a filesystem where fs-verity is unsupported, this function returns None.
259/// Other errors are still passed through.
260pub fn measure_verity_opt<H: FsVerityHashValue>(
261    fd: impl AsFd,
262) -> Result<Option<H>, MeasureVerityError> {
263    match ioctl::fs_ioc_measure_verity(fd) {
264        Ok(result) => Ok(Some(result)),
265        Err(MeasureVerityError::VerityMissing | MeasureVerityError::FilesystemNotSupported) => {
266            Ok(None)
267        }
268        Err(other) => Err(other),
269    }
270}
271
272/// Compare the fs-verity digest of the file versus the expected digest.
273///
274/// This calls `measure_verity()` and verifies that the result is equal to the expected value.
275///
276/// If this function returns successfully then the values match.  In this case, the kernel
277/// guarantees that the file content cannot possibly change for as long as the file descriptor
278/// exists.
279///
280/// If the file doesn't have fs-verity enabled, the hash value doesn't match, or if a different
281/// hash algorithm is in use, the comparison will fail.
282pub fn ensure_verity_equal(
283    fd: impl AsFd,
284    expected: &impl FsVerityHashValue,
285) -> Result<(), CompareVerityError> {
286    let found = measure_verity(fd)?;
287    if expected == &found {
288        Ok(())
289    } else {
290        Err(CompareVerityError::DigestMismatch {
291            expected: expected.to_hex(),
292            found: found.to_hex(),
293        })
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use std::{collections::BTreeSet, io::Write, os::unix::process::CommandExt, time::Duration};
300
301    use once_cell::sync::Lazy;
302    use rand::Rng;
303    use rustix::{
304        fd::OwnedFd,
305        fs::{open, Mode, OFlags},
306    };
307    use tempfile::{tempfile_in, TempDir};
308    use tokio::{task::JoinSet, time::Instant};
309
310    use crate::{test::tempdir, util::proc_self_fd};
311
312    use super::*;
313
314    static TEMPDIR: Lazy<TempDir> = Lazy::new(|| tempdir());
315    static TD_FD: Lazy<File> = Lazy::new(|| File::open(TEMPDIR.path()).unwrap());
316
317    fn tempfile() -> File {
318        tempfile_in(TEMPDIR.path()).unwrap()
319    }
320
321    fn rdonly_file_with(data: &[u8]) -> OwnedFd {
322        let mut file = tempfile();
323        file.write_all(data).unwrap();
324        file.sync_data().unwrap();
325        let fd = open(
326            proc_self_fd(&file),
327            OFlags::RDONLY | OFlags::CLOEXEC,
328            Mode::empty(),
329        )
330        .unwrap();
331        drop(file); // can't enable verity with outstanding writable fds
332        fd
333    }
334
335    fn empty_file_in_tmpdir(flags: OFlags, mode: Mode) -> (tempfile::TempDir, OwnedFd) {
336        let tmpdir = tempdir();
337        let path = tmpdir.path().join("empty");
338        let fd = open(path, OFlags::CLOEXEC | OFlags::CREATE | flags, mode).unwrap();
339        (tmpdir, fd)
340    }
341
342    #[test]
343    fn test_verity_missing() {
344        let tf = rdonly_file_with(b"");
345
346        assert!(matches!(
347            measure_verity::<Sha256HashValue>(&tf).unwrap_err(),
348            MeasureVerityError::VerityMissing
349        ));
350
351        assert!(measure_verity_opt::<Sha256HashValue>(&tf)
352            .unwrap()
353            .is_none());
354
355        assert!(matches!(
356            ensure_verity_equal(&tf, &Sha256HashValue::EMPTY).unwrap_err(),
357            CompareVerityError::Measure(MeasureVerityError::VerityMissing)
358        ));
359    }
360
361    #[test]
362    fn test_verity_simple() {
363        let tf = rdonly_file_with(b"hello world");
364
365        // first time: success
366        let tf = enable_verity_maybe_copy::<Sha256HashValue>(&*TD_FD, tf.as_fd())
367            .unwrap()
368            .unwrap_or(tf);
369
370        // second time: fail with "already enabled"
371        assert!(matches!(
372            enable_verity_maybe_copy::<Sha256HashValue>(&*TD_FD, tf.as_fd()).unwrap_err(),
373            EnableVerityError::AlreadyEnabled
374        ));
375
376        assert_eq!(
377            measure_verity::<Sha256HashValue>(&tf).unwrap().to_hex(),
378            "1e2eaa4202d750a41174ee454970b92c1bc2f925b1e35076d8c7d5f56362ba64"
379        );
380
381        assert_eq!(
382            measure_verity_opt::<Sha256HashValue>(&tf)
383                .unwrap()
384                .unwrap()
385                .to_hex(),
386            "1e2eaa4202d750a41174ee454970b92c1bc2f925b1e35076d8c7d5f56362ba64"
387        );
388
389        ensure_verity_equal(
390            &tf,
391            &Sha256HashValue::from_hex(
392                "1e2eaa4202d750a41174ee454970b92c1bc2f925b1e35076d8c7d5f56362ba64",
393            )
394            .unwrap(),
395        )
396        .unwrap();
397
398        let Err(CompareVerityError::DigestMismatch { expected, found }) = ensure_verity_equal(
399            &tf,
400            &Sha256HashValue::from_hex(
401                "1e2eaa4202d750a41174ee454970b92c1bc2f925b1e35076d8c7000000000000",
402            )
403            .unwrap(),
404        ) else {
405            panic!("Didn't fail with expected error");
406        };
407        assert_eq!(
408            expected,
409            "1e2eaa4202d750a41174ee454970b92c1bc2f925b1e35076d8c7000000000000"
410        );
411        assert_eq!(
412            found,
413            "1e2eaa4202d750a41174ee454970b92c1bc2f925b1e35076d8c7d5f56362ba64"
414        );
415    }
416
417    #[allow(unsafe_code)]
418    #[tokio::test]
419    async fn test_verity_forking() {
420        const DELAY_MIN: u64 = 0;
421        const DELAY_MAX: u64 = 10;
422        // Break if we get 100 successes
423        const SUCCESS_LIMIT: u32 = 100;
424        // Don't run more than 10s by default
425        const TIMEOUT: Duration = Duration::from_secs(10);
426        let start = Instant::now();
427
428        let cpus = std::thread::available_parallelism().unwrap();
429        // use half of capacity for forking
430        let threads = cpus.get() >> 1;
431        assert!(threads >= 1);
432        eprintln!("using {threads} threads");
433        let mut txs = vec![];
434        let mut jhs = vec![];
435
436        for _ in 0..threads {
437            let (tx, rx) = std::sync::mpsc::channel();
438            let jh = std::thread::spawn(move || {
439                let mut rng = rand::rng();
440
441                loop {
442                    if rx.try_recv().is_ok() {
443                        break;
444                    }
445
446                    let delay = rng.random_range(DELAY_MIN..=DELAY_MAX);
447                    let delay = Duration::from_millis(delay);
448                    unsafe {
449                        std::process::Command::new("true")
450                            .pre_exec(move || {
451                                std::thread::sleep(delay);
452                                Ok(())
453                            })
454                            .status()
455                            .unwrap();
456                    }
457                }
458            });
459
460            txs.push(tx);
461            jhs.push(jh);
462        }
463
464        let raw_verity_enabler = async move {
465            let mut successes = 0;
466            let mut failures = 0;
467
468            loop {
469                if tokio::time::Instant::now().duration_since(start) > TIMEOUT {
470                    break;
471                }
472                if successes == SUCCESS_LIMIT {
473                    break;
474                }
475
476                let r = tokio::task::spawn_blocking(move || {
477                    let ro_fd = rdonly_file_with(b"hello world");
478                    enable_verity_raw::<Sha256HashValue>(&ro_fd)
479                })
480                .await
481                .unwrap();
482                if r.is_ok() {
483                    successes += 1;
484                } else {
485                    failures += 1;
486                }
487            }
488
489            (successes, failures)
490        };
491
492        let retry_verity_enabler = async move {
493            let mut successes = 0;
494            let mut failures = 0;
495
496            loop {
497                if tokio::time::Instant::now().duration_since(start) > TIMEOUT {
498                    break;
499                }
500                if successes == SUCCESS_LIMIT {
501                    break;
502                }
503
504                let r = tokio::task::spawn_blocking(move || {
505                    let ro_fd = rdonly_file_with(b"hello world");
506                    enable_verity_with_retry::<Sha256HashValue>(&ro_fd)
507                })
508                .await
509                .unwrap();
510                if r.is_ok() {
511                    successes += 1;
512                } else {
513                    failures += 1;
514                }
515            }
516
517            (successes, failures)
518        };
519
520        let copy_verity_enabler = async move {
521            let mut orig = 0;
522            let mut copy = 0;
523
524            loop {
525                if tokio::time::Instant::now().duration_since(start) > TIMEOUT {
526                    break;
527                }
528                if orig + copy == SUCCESS_LIMIT {
529                    break;
530                }
531
532                let is_copy = tokio::task::spawn_blocking(|| {
533                    let ro_fd = rdonly_file_with(b"Hello world");
534                    enable_verity_maybe_copy::<Sha256HashValue>(&*TD_FD, ro_fd.as_fd())
535                        .unwrap()
536                        .is_some()
537                })
538                .await
539                .unwrap();
540
541                if is_copy {
542                    copy += 1;
543                } else {
544                    orig += 1;
545                }
546            }
547
548            (orig, copy)
549        };
550
551        let ts = tokio::time::Instant::now();
552
553        let mut set = JoinSet::new();
554        set.spawn(async move {
555            let (successes, failures) = raw_verity_enabler.await;
556            let elapsed = ts.elapsed().as_millis();
557            eprintln!("raw verity enabled ({successes} attempts succeeded, {failures} attempts failed) in {elapsed}ms");
558        });
559        set.spawn(async move {
560            let (successes, failures) = retry_verity_enabler.await;
561            let elapsed = ts.elapsed().as_millis();
562            eprintln!("retry verity enabled ({successes} attempts succeeded, {failures} attempts failed) in {elapsed}ms");
563        });
564        set.spawn(async move {
565            let (orig, copy) = copy_verity_enabler.await;
566            assert!(orig > 0 || copy > 0);
567            let elapsed = ts.elapsed().as_millis();
568            eprintln!("copy verity enabled ({orig} original, {copy} copies) in {elapsed}ms");
569        });
570
571        while let Some(res) = set.join_next().await {
572            res.unwrap();
573        }
574
575        txs.into_iter().for_each(|tx| tx.send(()).unwrap());
576        jhs.into_iter().for_each(|jh| jh.join().unwrap());
577    }
578
579    #[test_with::path(/dev/shm)]
580    #[test]
581    fn test_verity_error_noverity() {
582        let tf = tempfile_in("/dev/shm").unwrap();
583
584        assert!(matches!(
585            enable_verity_with_retry::<Sha256HashValue>(&tf).unwrap_err(),
586            EnableVerityError::FilesystemNotSupported
587        ));
588
589        assert!(matches!(
590            measure_verity::<Sha256HashValue>(&tf).unwrap_err(),
591            MeasureVerityError::FilesystemNotSupported
592        ));
593
594        assert!(measure_verity_opt::<Sha256HashValue>(&tf)
595            .unwrap()
596            .is_none());
597
598        assert!(matches!(
599            ensure_verity_equal(&tf, &Sha256HashValue::EMPTY).unwrap_err(),
600            CompareVerityError::Measure(MeasureVerityError::FilesystemNotSupported)
601        ));
602    }
603
604    #[test]
605    fn test_verity_wrongdigest_sha512_sha256() {
606        let tf = rdonly_file_with(b"hello world");
607
608        // Enable with SHA-512 but then try to read with SHA-256
609        let tf = enable_verity_maybe_copy::<Sha512HashValue>(&*TD_FD, tf.as_fd())
610            .unwrap()
611            .unwrap_or(tf);
612
613        assert!(matches!(
614            measure_verity::<Sha256HashValue>(&tf).unwrap_err(),
615            MeasureVerityError::InvalidDigestSize { .. }
616        ));
617
618        assert!(matches!(
619            measure_verity_opt::<Sha256HashValue>(&tf).unwrap_err(),
620            MeasureVerityError::InvalidDigestSize { .. }
621        ));
622
623        assert!(matches!(
624            ensure_verity_equal(&tf, &Sha256HashValue::EMPTY).unwrap_err(),
625            CompareVerityError::Measure(MeasureVerityError::InvalidDigestSize { .. })
626        ));
627    }
628
629    #[test]
630    fn test_verity_wrongdigest_sha256_sha512() {
631        let tf = rdonly_file_with(b"hello world");
632
633        // Enable with SHA-256 but then try to read with SHA-512
634        let tf = enable_verity_maybe_copy::<Sha256HashValue>(&*TD_FD, tf.as_fd())
635            .unwrap()
636            .unwrap_or(tf);
637
638        assert!(matches!(
639            measure_verity::<Sha512HashValue>(&tf).unwrap_err(),
640            MeasureVerityError::InvalidDigestAlgorithm { .. }
641        ));
642
643        assert!(matches!(
644            measure_verity_opt::<Sha512HashValue>(&tf).unwrap_err(),
645            MeasureVerityError::InvalidDigestAlgorithm { .. }
646        ));
647
648        assert!(matches!(
649            ensure_verity_equal(&tf, &Sha512HashValue::EMPTY).unwrap_err(),
650            CompareVerityError::Measure(MeasureVerityError::InvalidDigestAlgorithm { .. })
651        ));
652    }
653
654    #[test]
655    fn crosscheck_interesting_cases() {
656        // Test the kernel against our userspace calculations.
657        //
658        // We try to pick some "interesting" sizes to test the edge cases.  The arity of a
659        // SHA-256/4096 Merkle tree is 128 = 4096 / 32.  With SHA-512 it's 64 = 4096 / 64.
660        // So we try to chose values around the page size times powers of 32 and 64.
661        let mut cases = BTreeSet::new();
662        for arity in [32, 64] {
663            for layer4 in [/* -1, */ 0 /*, 1 */] {
664                /* otherwise it's too slow */
665                for layer3 in [-1, 0, 1] {
666                    for layer2 in [-1, 0, 1] {
667                        for layer1 in [-1, 0, 1] {
668                            for layer0 in [-1, 0, 1] {
669                                let candidate = layer4 * (arity * arity * arity * arity)
670                                    + layer3 * (arity * arity * arity)
671                                    + layer2 * (arity * arity)
672                                    + layer1 * arity
673                                    + layer0;
674                                if let Ok(size) = usize::try_from(candidate) {
675                                    cases.insert(size);
676                                }
677                            }
678                        }
679                    }
680                }
681            }
682        }
683
684        fn assert_kernel_equal<H: FsVerityHashValue>(data: &[u8], expected: H) {
685            let fd = rdonly_file_with(data);
686            let fd = enable_verity_maybe_copy::<H>(&*TD_FD, fd.as_fd())
687                .unwrap()
688                .unwrap_or(fd);
689            ensure_verity_equal(&fd, &expected).unwrap();
690        }
691
692        for size in cases {
693            // the actual data is uninteresting
694            let data = vec![0x5a; size];
695            assert_kernel_equal(&data, compute_verity::<Sha256HashValue>(&data));
696            assert_kernel_equal(&data, compute_verity::<Sha512HashValue>(&data));
697        }
698    }
699
700    #[test]
701    fn test_enable_verity_maybe_copy_without_copy() {
702        // Enabling verity on an empty file created without a
703        // read-write file descriptor ever existing should always
704        // succeed and hand us back the original file descriptor.
705        let (tempdir, fd) = empty_file_in_tmpdir(OFlags::RDONLY, 0o644.into());
706        let tempdir_fd = File::open(tempdir.path()).unwrap();
707        let fd = enable_verity_maybe_copy::<Sha256HashValue>(&tempdir_fd, fd.as_fd()).unwrap();
708        assert!(fd.is_none());
709    }
710
711    #[test]
712    fn test_enable_verity_maybe_copy_with_copy() {
713        // Here we intentionally try to enable verity on a read-write
714        // file descriptor, which will never work directly, so we
715        // expect to always get back a new copy of the requested file.
716        let (tempdir, fd) = empty_file_in_tmpdir(OFlags::RDWR, 0o644.into());
717        let tempdir_fd = File::open(tempdir.path()).unwrap();
718        let mut fd = File::from(fd);
719        let _ = fd.write(b"hello world").unwrap();
720        let fd = enable_verity_maybe_copy::<Sha256HashValue>(&tempdir_fd, fd.as_fd())
721            .unwrap()
722            .unwrap();
723
724        // The new fd has the correct data
725        assert!(ensure_verity_equal(
726            fd,
727            &Sha256HashValue::from_hex(
728                "1e2eaa4202d750a41174ee454970b92c1bc2f925b1e35076d8c7d5f56362ba64",
729            )
730            .unwrap(),
731        )
732        .is_ok());
733    }
734}