1use std::{
8 collections::HashSet,
9 ffi::CStr,
10 fs::{canonicalize, File},
11 io::{Read, Write},
12 os::fd::{AsFd, OwnedFd},
13 path::{Path, PathBuf},
14 sync::Arc,
15};
16
17use anyhow::{bail, ensure, Context, Result};
18use once_cell::sync::OnceCell;
19use rustix::{
20 fs::{
21 fdatasync, flock, linkat, mkdirat, open, openat, readlinkat, AtFlags, Dir, FileType,
22 FlockOperation, Mode, OFlags, CWD,
23 },
24 io::{Errno, Result as ErrnoResult},
25};
26use sha2::{Digest, Sha256};
27
28use crate::{
29 fsverity::{
30 compute_verity, enable_verity_maybe_copy, ensure_verity_equal, measure_verity,
31 CompareVerityError, EnableVerityError, FsVerityHashValue, MeasureVerityError,
32 },
33 mount::{composefs_fsmount, mount_at},
34 splitstream::{DigestMap, SplitStreamReader, SplitStreamWriter},
35 util::{proc_self_fd, replace_symlinkat, ErrnoFilter, Sha256Digest},
36};
37
38fn ensure_dir_and_openat(dirfd: impl AsFd, filename: &str, flags: OFlags) -> ErrnoResult<OwnedFd> {
43 match openat(
44 &dirfd,
45 filename,
46 flags | OFlags::CLOEXEC | OFlags::DIRECTORY,
47 0o666.into(),
48 ) {
49 Ok(file) => Ok(file),
50 Err(Errno::NOENT) => match mkdirat(&dirfd, filename, 0o777.into()) {
51 Ok(()) | Err(Errno::EXIST) => openat(
52 dirfd,
53 filename,
54 flags | OFlags::CLOEXEC | OFlags::DIRECTORY,
55 0o666.into(),
56 ),
57 Err(other) => Err(other),
58 },
59 Err(other) => Err(other),
60 }
61}
62
63#[derive(Debug)]
70pub struct Repository<ObjectID: FsVerityHashValue> {
71 repository: OwnedFd,
72 objects: OnceCell<OwnedFd>,
73 insecure: bool,
74 _data: std::marker::PhantomData<ObjectID>,
75}
76
77impl<ObjectID: FsVerityHashValue> Drop for Repository<ObjectID> {
78 fn drop(&mut self) {
79 flock(&self.repository, FlockOperation::Unlock).expect("repository unlock failed");
80 }
81}
82
83impl<ObjectID: FsVerityHashValue> Repository<ObjectID> {
84 pub fn objects_dir(&self) -> ErrnoResult<&OwnedFd> {
86 self.objects
87 .get_or_try_init(|| ensure_dir_and_openat(&self.repository, "objects", OFlags::PATH))
88 }
89
90 pub fn open_path(dirfd: impl AsFd, path: impl AsRef<Path>) -> Result<Self> {
92 let path = path.as_ref();
93
94 let repository = openat(dirfd, path, OFlags::RDONLY | OFlags::CLOEXEC, Mode::empty())
96 .with_context(|| format!("Cannot open composefs repository at {}", path.display()))?;
97
98 flock(&repository, FlockOperation::LockShared)
99 .context("Cannot lock composefs repository")?;
100
101 Ok(Self {
102 repository,
103 objects: OnceCell::new(),
104 insecure: false,
105 _data: std::marker::PhantomData,
106 })
107 }
108
109 pub fn open_user() -> Result<Self> {
111 let home = std::env::var("HOME").with_context(|| "$HOME must be set when in user mode")?;
112
113 Self::open_path(CWD, PathBuf::from(home).join(".var/lib/composefs"))
114 }
115
116 pub fn open_system() -> Result<Self> {
118 Self::open_path(CWD, PathBuf::from("/sysroot/composefs".to_string()))
119 }
120
121 fn ensure_dir(&self, dir: impl AsRef<Path>) -> ErrnoResult<()> {
122 mkdirat(&self.repository, dir.as_ref(), 0o755.into()).or_else(|e| match e {
123 Errno::EXIST => Ok(()),
124 _ => Err(e),
125 })
126 }
127
128 pub async fn ensure_object_async(self: &Arc<Self>, data: Vec<u8>) -> Result<ObjectID> {
133 let self_ = Arc::clone(self);
134 tokio::task::spawn_blocking(move || self_.ensure_object(&data)).await?
135 }
136
137 pub fn ensure_object(&self, data: &[u8]) -> Result<ObjectID> {
139 let dirfd = self.objects_dir()?;
140 let id: ObjectID = compute_verity(data);
141
142 let path = id.to_object_pathname();
143
144 match openat(
146 dirfd,
147 &path,
148 OFlags::RDONLY | OFlags::CLOEXEC,
149 Mode::empty(),
150 ) {
151 Ok(fd) => {
152 match ensure_verity_equal(&fd, &id) {
155 Ok(()) => {}
156 Err(CompareVerityError::Measure(MeasureVerityError::VerityMissing))
157 if self.insecure =>
158 {
159 match enable_verity_maybe_copy::<ObjectID>(dirfd, fd.as_fd()) {
160 Ok(Some(fd)) => ensure_verity_equal(&fd, &id)?,
161 Ok(None) => ensure_verity_equal(&fd, &id)?,
162 Err(other) => Err(other)?,
163 }
164 }
165 Err(CompareVerityError::Measure(
166 MeasureVerityError::FilesystemNotSupported,
167 )) if self.insecure => {}
168 Err(other) => Err(other)?,
169 }
170 return Ok(id);
171 }
172 Err(Errno::NOENT) => {
173 }
175 Err(other) => {
176 return Err(other).context("Checking for existing object in repository")?;
177 }
178 }
179
180 let fd = ensure_dir_and_openat(dirfd, &id.to_object_dir(), OFlags::RDWR | OFlags::TMPFILE)?;
181 let mut file = File::from(fd);
182 file.write_all(data)?;
183 fdatasync(&file)?;
184
185 let ro_fd = open(
187 proc_self_fd(&file),
188 OFlags::RDONLY | OFlags::CLOEXEC,
189 Mode::empty(),
190 )?;
191 drop(file);
192
193 let ro_fd = match enable_verity_maybe_copy::<ObjectID>(dirfd, ro_fd.as_fd()) {
194 Ok(maybe_fd) => {
195 let ro_fd = maybe_fd.unwrap_or(ro_fd);
196 match ensure_verity_equal(&ro_fd, &id) {
197 Ok(()) => ro_fd,
198 Err(CompareVerityError::Measure(
199 MeasureVerityError::VerityMissing
200 | MeasureVerityError::FilesystemNotSupported,
201 )) if self.insecure => ro_fd,
202 Err(other) => Err(other).context("Double-checking verity digest")?,
203 }
204 }
205 Err(EnableVerityError::FilesystemNotSupported) if self.insecure => ro_fd,
206 Err(other) => Err(other).context("Enabling verity digest")?,
207 };
208
209 match linkat(
210 CWD,
211 proc_self_fd(&ro_fd),
212 dirfd,
213 path,
214 AtFlags::SYMLINK_FOLLOW,
215 ) {
216 Ok(()) => {}
217 Err(Errno::EXIST) => {
218 }
220 Err(other) => {
221 return Err(other).context("Linking created object file");
222 }
223 }
224
225 Ok(id)
226 }
227
228 fn open_with_verity(&self, filename: &str, expected_verity: &ObjectID) -> Result<OwnedFd> {
229 let fd = self.openat(filename, OFlags::RDONLY)?;
230 match ensure_verity_equal(&fd, expected_verity) {
231 Ok(()) => {}
232 Err(CompareVerityError::Measure(
233 MeasureVerityError::VerityMissing | MeasureVerityError::FilesystemNotSupported,
234 )) if self.insecure => {}
235 Err(other) => Err(other)?,
236 }
237 Ok(fd)
238 }
239
240 pub fn set_insecure(&mut self, insecure: bool) -> &mut Self {
245 self.insecure = insecure;
246 self
247 }
248
249 pub fn create_stream(
253 self: &Arc<Self>,
254 sha256: Option<Sha256Digest>,
255 maps: Option<DigestMap<ObjectID>>,
256 ) -> SplitStreamWriter<ObjectID> {
257 SplitStreamWriter::new(self, maps, sha256)
258 }
259
260 fn format_object_path(id: &ObjectID) -> String {
261 format!("objects/{}", id.to_object_pathname())
262 }
263
264 pub fn has_stream(&self, sha256: &Sha256Digest) -> Result<Option<ObjectID>> {
267 let stream_path = format!("streams/{}", hex::encode(sha256));
268
269 match readlinkat(&self.repository, &stream_path, []) {
270 Ok(target) => {
271 let bytes = target.as_bytes();
279 ensure!(
280 bytes.starts_with(b"../"),
281 "stream symlink has incorrect prefix"
282 );
283 Ok(Some(ObjectID::from_object_pathname(bytes)?))
284 }
285 Err(Errno::NOENT) => Ok(None),
286 Err(err) => Err(err)?,
287 }
288 }
289
290 pub fn check_stream(&self, sha256: &Sha256Digest) -> Result<Option<ObjectID>> {
292 let stream_path = format!("streams/{}", hex::encode(sha256));
293 match self.openat(&stream_path, OFlags::RDONLY) {
294 Ok(stream) => {
295 let path = readlinkat(&self.repository, stream_path, [])?;
296 let measured_verity = match measure_verity(&stream) {
297 Ok(found) => found,
298 Err(
299 MeasureVerityError::VerityMissing
300 | MeasureVerityError::FilesystemNotSupported,
301 ) if self.insecure => FsVerityHashValue::from_object_pathname(path.to_bytes())?,
302 Err(other) => Err(other)?,
303 };
304 let mut context = Sha256::new();
305 let mut split_stream = SplitStreamReader::new(File::from(stream))?;
306
307 for entry in &split_stream.refs.map {
309 if self.check_stream(&entry.body)?.as_ref() != Some(&entry.verity) {
310 bail!("reference mismatch");
311 }
312 }
313
314 split_stream.cat(&mut context, |id| -> Result<Vec<u8>> {
316 let mut data = vec![];
317 File::from(self.open_object(id)?).read_to_end(&mut data)?;
318 Ok(data)
319 })?;
320 if *sha256 != Into::<[u8; 32]>::into(context.finalize()) {
321 bail!("Content didn't match!");
322 }
323
324 Ok(Some(measured_verity))
325 }
326 Err(Errno::NOENT) => Ok(None),
327 Err(err) => Err(err)?,
328 }
329 }
330
331 pub fn write_stream(
334 &self,
335 writer: SplitStreamWriter<ObjectID>,
336 reference: Option<&str>,
337 ) -> Result<ObjectID> {
338 let Some((.., ref sha256)) = writer.sha256 else {
339 bail!("Writer doesn't have sha256 enabled");
340 };
341 let stream_path = format!("streams/{}", hex::encode(sha256));
342 let object_id = writer.done()?;
343 let object_path = Self::format_object_path(&object_id);
344 self.symlink(&stream_path, &object_path)?;
345
346 if let Some(name) = reference {
347 let reference_path = format!("streams/refs/{name}");
348 self.symlink(&reference_path, &stream_path)?;
349 }
350
351 Ok(object_id)
352 }
353
354 pub fn name_stream(&self, sha256: Sha256Digest, name: &str) -> Result<()> {
357 let stream_path = format!("streams/{}", hex::encode(sha256));
358 let reference_path = format!("streams/refs/{name}");
359 self.symlink(&reference_path, &stream_path)?;
360 Ok(())
361 }
362
363 pub fn ensure_stream(
378 self: &Arc<Self>,
379 sha256: &Sha256Digest,
380 callback: impl FnOnce(&mut SplitStreamWriter<ObjectID>) -> Result<()>,
381 reference: Option<&str>,
382 ) -> Result<ObjectID> {
383 let stream_path = format!("streams/{}", hex::encode(sha256));
384
385 let object_id = match self.has_stream(sha256)? {
386 Some(id) => id,
387 None => {
388 let mut writer = self.create_stream(Some(*sha256), None);
389 callback(&mut writer)?;
390 let object_id = writer.done()?;
391
392 let object_path = Self::format_object_path(&object_id);
393 self.symlink(&stream_path, &object_path)?;
394 object_id
395 }
396 };
397
398 if let Some(name) = reference {
399 let reference_path = format!("streams/refs/{name}");
400 self.symlink(&reference_path, &stream_path)?;
401 }
402
403 Ok(object_id)
404 }
405
406 pub fn open_stream(
408 &self,
409 name: &str,
410 verity: Option<&ObjectID>,
411 ) -> Result<SplitStreamReader<File, ObjectID>> {
412 let filename = format!("streams/{name}");
413
414 let file = File::from(if let Some(verity_hash) = verity {
415 self.open_with_verity(&filename, verity_hash)
416 .with_context(|| format!("Opening ref 'streams/{name}'"))?
417 } else {
418 self.openat(&filename, OFlags::RDONLY)
419 .with_context(|| format!("Opening ref 'streams/{name}'"))?
420 });
421
422 SplitStreamReader::new(file)
423 }
424
425 pub fn open_object(&self, id: &ObjectID) -> Result<OwnedFd> {
428 self.open_with_verity(&Self::format_object_path(id), id)
429 }
430
431 pub fn merge_splitstream(
437 &self,
438 name: &str,
439 verity: Option<&ObjectID>,
440 stream: &mut impl Write,
441 ) -> Result<()> {
442 let mut split_stream = self.open_stream(name, verity)?;
443 split_stream.cat(stream, |id| -> Result<Vec<u8>> {
444 let mut data = vec![];
445 File::from(self.open_object(id)?).read_to_end(&mut data)?;
446 Ok(data)
447 })?;
448
449 Ok(())
450 }
451
452 pub fn write_image(&self, name: Option<&str>, data: &[u8]) -> Result<ObjectID> {
460 let object_id = self.ensure_object(data)?;
461
462 let object_path = Self::format_object_path(&object_id);
463 let image_path = format!("images/{}", object_id.to_hex());
464
465 self.symlink(&image_path, &object_path)?;
466
467 if let Some(reference) = name {
468 let ref_path = format!("images/refs/{reference}");
469 self.symlink(&ref_path, &image_path)?;
470 }
471
472 Ok(object_id)
473 }
474
475 pub fn import_image<R: Read>(&self, name: &str, image: &mut R) -> Result<ObjectID> {
483 let mut data = vec![];
484 image.read_to_end(&mut data)?;
485 self.write_image(Some(name), &data)
486 }
487
488 fn open_image(&self, name: &str) -> Result<(OwnedFd, bool)> {
491 let image = self
492 .openat(&format!("images/{name}"), OFlags::RDONLY)
493 .with_context(|| format!("Opening ref 'images/{name}'"))?;
494
495 if name.contains("/") {
496 return Ok((image, true));
497 }
498
499 match measure_verity::<ObjectID>(&image) {
501 Ok(found) if found == FsVerityHashValue::from_hex(name)? => Ok((image, true)),
502 Ok(_) => bail!("fs-verity content mismatch"),
503 Err(MeasureVerityError::VerityMissing | MeasureVerityError::FilesystemNotSupported)
504 if self.insecure =>
505 {
506 Ok((image, false))
507 }
508 Err(other) => Err(other)?,
509 }
510 }
511
512 pub fn mount(&self, name: &str) -> Result<OwnedFd> {
515 let (image, enable_verity) = self.open_image(name)?;
516 Ok(composefs_fsmount(
517 image,
518 name,
519 self.objects_dir()?,
520 enable_verity,
521 )?)
522 }
523
524 pub fn mount_at(&self, name: &str, mountpoint: impl AsRef<Path>) -> Result<()> {
526 Ok(mount_at(
527 self.mount(name)?,
528 CWD,
529 &canonicalize(mountpoint)?,
530 )?)
531 }
532
533 pub fn symlink(&self, name: impl AsRef<Path>, target: impl AsRef<Path>) -> ErrnoResult<()> {
539 let name = name.as_ref();
540
541 let mut symlink_components = name.parent().unwrap().components().peekable();
542 let mut target_components = target.as_ref().components().peekable();
543
544 let mut symlink_ancestor = PathBuf::new();
545
546 while symlink_components.peek() == target_components.peek() {
548 symlink_ancestor.push(symlink_components.next().unwrap());
549 target_components.next().unwrap();
550 }
551
552 let mut relative = PathBuf::new();
553 for symlink_component in symlink_components {
556 symlink_ancestor.push(symlink_component);
557 self.ensure_dir(&symlink_ancestor)?;
558 relative.push("..");
559 }
560
561 for target_component in target_components {
563 relative.push(target_component);
564 }
565
566 replace_symlinkat(&relative, &self.repository, name)
568 }
569
570 fn read_symlink_hashvalue(dirfd: &OwnedFd, name: &CStr) -> Result<ObjectID> {
571 let link_content = readlinkat(dirfd, name, [])?;
572 Ok(ObjectID::from_object_pathname(link_content.to_bytes())?)
573 }
574
575 fn walk_symlinkdir(fd: OwnedFd, objects: &mut HashSet<ObjectID>) -> Result<()> {
576 for item in Dir::read_from(&fd)? {
577 let entry = item?;
578 match entry.file_type() {
581 FileType::Directory => {
582 let filename = entry.file_name();
583 if filename != c"." && filename != c".." {
584 let dirfd = openat(&fd, filename, OFlags::RDONLY, Mode::empty())?;
585 Self::walk_symlinkdir(dirfd, objects)?;
586 }
587 }
588 FileType::Symlink => {
589 objects.insert(Self::read_symlink_hashvalue(&fd, entry.file_name())?);
590 }
591 _ => {
592 bail!("Unexpected file type encountered");
593 }
594 }
595 }
596
597 Ok(())
598 }
599
600 fn openat(&self, name: &str, flags: OFlags) -> ErrnoResult<OwnedFd> {
602 openat(
604 &self.repository,
605 name,
606 flags | OFlags::CLOEXEC,
607 Mode::empty(),
608 )
609 }
610
611 fn gc_category(&self, category: &str) -> Result<HashSet<ObjectID>> {
612 let mut objects = HashSet::new();
613
614 let Some(category_fd) = self
615 .openat(category, OFlags::RDONLY | OFlags::DIRECTORY)
616 .filter_errno(Errno::NOENT)
617 .context("Opening {category} dir in repository")?
618 else {
619 return Ok(objects);
620 };
621
622 if let Some(refs) = openat(
623 &category_fd,
624 "refs",
625 OFlags::RDONLY | OFlags::DIRECTORY,
626 Mode::empty(),
627 )
628 .filter_errno(Errno::NOENT)
629 .context("Opening {category}/refs dir in repository")?
630 {
631 Self::walk_symlinkdir(refs, &mut objects)?;
632 }
633
634 for item in Dir::read_from(&category_fd)? {
635 let entry = item?;
636 let filename = entry.file_name();
637 if filename != c"refs" && filename != c"." && filename != c".." {
638 if entry.file_type() != FileType::Symlink {
639 bail!("category directory contains non-symlink");
640 }
641
642 continue;
645
646 }
655 }
656
657 Ok(objects)
658 }
659
660 pub fn objects_for_image(&self, name: &str) -> Result<HashSet<ObjectID>> {
662 let (image, _) = self.open_image(name)?;
663 let mut data = vec![];
664 std::fs::File::from(image).read_to_end(&mut data)?;
665 Ok(crate::erofs::reader::collect_objects(&data)?)
666 }
667
668 pub fn gc(&self) -> Result<()> {
674 flock(&self.repository, FlockOperation::LockExclusive)?;
675
676 let mut objects = HashSet::new();
677
678 for ref object in self.gc_category("images")? {
679 println!("{object:?} lives as an image");
680 objects.insert(object.clone());
681 objects.extend(self.objects_for_image(&object.to_hex())?);
682 }
683
684 for object in self.gc_category("streams")? {
685 println!("{object:?} lives as a stream");
686 objects.insert(object.clone());
687
688 let mut split_stream = self.open_stream(&object.to_hex(), None)?;
689 split_stream.get_object_refs(|id| {
690 println!(" with {id:?}");
691 objects.insert(id.clone());
692 })?;
693 }
694
695 for first_byte in 0x0..=0xff {
696 let dirfd = match self.openat(
697 &format!("objects/{first_byte:02x}"),
698 OFlags::RDONLY | OFlags::DIRECTORY,
699 ) {
700 Ok(fd) => fd,
701 Err(Errno::NOENT) => continue,
702 Err(e) => Err(e)?,
703 };
704 for item in Dir::new(dirfd)? {
705 let entry = item?;
706 let filename = entry.file_name();
707 if filename != c"." && filename != c".." {
708 let id =
709 ObjectID::from_object_dir_and_basename(first_byte, filename.to_bytes())?;
710 if !objects.contains(&id) {
711 println!("rm objects/{first_byte:02x}/{filename:?}");
712 } else {
713 println!("# objects/{first_byte:02x}/{filename:?} lives");
714 }
715 }
716 }
717 }
718
719 Ok(flock(&self.repository, FlockOperation::LockShared)?) }
721
722 }