Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,9 @@ subtle = "2.6"
thiserror = "2.0"
tempfile = "3.14"
zeroize = { version = "1.8", features = ["derive"] }

[target.'cfg(unix)'.dependencies]
rustix = { version = "1.1", features = ["fs"] }

[target.'cfg(unix)'.dev-dependencies]
libc = "0.2"
104 changes: 104 additions & 0 deletions src/bounded.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//! The one way to open a file that untrusted storage controls. Every
//! directory-backend read goes through `open_regular`, so the file-type check
//! happens on the opened handle and size limits apply before any allocation.

use std::fs::File;
use std::io::{self, Read};
use std::path::Path;

pub(crate) enum Opened {
Absent,
/// A regular file, with its length at open time.
Regular(File, u64),
/// A symbolic link, FIFO, socket, device, or directory.
NotRegular,
}

pub(crate) enum Bounded {
Absent,
Within(Vec<u8>),
/// A regular file longer than the limit; its contents were not read.
Oversized,
NotRegular,
}

/// Opens `path` for reading without following a final symbolic link and
/// without blocking on a FIFO, then requires a regular file.
pub(crate) fn open_regular(path: &Path) -> io::Result<Opened> {
let file = match open_no_follow(path)? {
Raw::Absent => return Ok(Opened::Absent),
Raw::Refused => return Ok(Opened::NotRegular),
Raw::Open(file) => file,
};
let metadata = file.metadata()?;
if !metadata.is_file() {
return Ok(Opened::NotRegular);
}
clear_nonblocking(&file)?;
Ok(Opened::Regular(file, metadata.len()))
}

/// Reads a regular file of at most `limit` bytes, rejecting a longer one by
/// its length before reading and again if it grew after opening.
pub(crate) fn read_regular_bounded(path: &Path, limit: u64) -> io::Result<Bounded> {
let (file, length) = match open_regular(path)? {
Opened::Absent => return Ok(Bounded::Absent),
Opened::NotRegular => return Ok(Bounded::NotRegular),
Opened::Regular(file, length) => (file, length),
};
if length > limit {
return Ok(Bounded::Oversized);
}
let mut bytes = Vec::with_capacity(length as usize);
file.take(limit + 1).read_to_end(&mut bytes)?;
if bytes.len() as u64 > limit {
return Ok(Bounded::Oversized);
}
Ok(Bounded::Within(bytes))
}

enum Raw {
Absent,
/// The final component cannot be a regular file.
#[cfg_attr(not(unix), allow(dead_code))]
Refused,
Open(File),
}

#[cfg(unix)]
fn open_no_follow(path: &Path) -> io::Result<Raw> {
use rustix::fs::{Mode, OFlags};
use rustix::io::Errno;

let flags = OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC;
match rustix::fs::open(path, flags, Mode::empty()) {
Ok(fd) => Ok(Raw::Open(File::from(fd))),
Err(Errno::NOENT) => Ok(Raw::Absent),
// A final symlink (ELOOP) or a socket (ENXIO, EOPNOTSUPP).
Err(Errno::LOOP | Errno::NXIO | Errno::OPNOTSUPP) => Ok(Raw::Refused),
Err(error) => Err(error.into()),
}
}

/// Windows has no FIFOs in the filesystem namespace, but this follows
/// symbolic links: a link to a regular file reads as that file.
#[cfg(not(unix))]
fn open_no_follow(path: &Path) -> io::Result<Raw> {
match File::open(path) {
Ok(file) => Ok(Raw::Open(file)),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(Raw::Absent),
Err(error) => Err(error),
}
}

#[cfg(unix)]
fn clear_nonblocking(file: &File) -> io::Result<()> {
let flags = rustix::fs::fcntl_getfl(file)?;
rustix::fs::fcntl_setfl(file, flags - rustix::fs::OFlags::NONBLOCK)?;
Ok(())
}

#[cfg(not(unix))]
fn clear_nonblocking(_file: &File) -> io::Result<()> {
Ok(())
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod bounded;
pub mod continuity;
pub mod crypto;
pub mod error;
Expand Down
26 changes: 19 additions & 7 deletions src/repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use crate::manifest::{
verify_manifest, wrap_predecessor_key,
};
use crate::policy::{DeviceRecord, DeviceRoles, PolicyState};
use crate::storage::{HeadObservation, ObjectKind, Storage, StorageOccupied};
use crate::storage::{HeadObservation, ObjectKind, Storage, StorageOccupied, StoredHead};
use crate::trace;

const MAX_RECOVERY_OUTER_COMMITS: usize = 2048;
Expand Down Expand Up @@ -854,10 +854,14 @@ impl<S: Storage> EncryptedRepository<S> {
&mut |id| self.read_manifest_material(id),
Some(&mut budget),
),
None if observation.head_bytes.is_some() => Err(proven_invalid(anyhow::anyhow!(
"storage HEAD is not a valid manifest object ID"
))),
None => Err(MissingStorageHead.into()),
None => match &observation.head {
Some(head) => {
Err(proven_invalid(head.parse().err().unwrap_or_else(|| {
anyhow::anyhow!("storage HEAD is not a valid head")
})))
}
None => Err(MissingStorageHead.into()),
},
};
match current_chain {
Ok(chain) => {
Expand Down Expand Up @@ -920,7 +924,11 @@ impl<S: Storage> EncryptedRepository<S> {
if report.classification == RecoveryClass::Invalid {
let evidence_id = observation.head_id.clone().unwrap_or_else(|| {
hex::encode(Sha256::digest(
observation.head_bytes.as_deref().unwrap_or_default(),
observation
.head
.as_ref()
.map(StoredHead::evidence)
.unwrap_or_default(),
))
});
record_rejected_head(
Expand Down Expand Up @@ -1360,7 +1368,11 @@ impl<S: Storage> EncryptedRepository<S> {
if self.storage_object_matches(kind, id)? {
return Ok(());
}
if self.storage.restore_historical_object(kind, id)?
// Room for one corrupt maximal copy before the valid one.
let restore_budget = kind.max_bytes().saturating_mul(2);
if self
.storage
.restore_historical_object(kind, id, restore_budget)?
&& self.storage_object_matches(kind, id)?
{
return Ok(());
Expand Down
Loading
Loading