Skip to content

Commit

Permalink
rust: Add a composefs-oci crate
Browse files Browse the repository at this point in the history
The high level goal of this crate is to be an opinionated
generic storage layer using composefs, with direct support
for OCI.  Note not just OCI *containers* but also including
OCI artifacts too.

This crate is intended to be the successor to
the "storage core" of both ostree and containers/storage.

Signed-off-by: Colin Walters <[email protected]>
  • Loading branch information
cgwalters committed May 26, 2024
1 parent c2880ef commit 122ed24
Show file tree
Hide file tree
Showing 13 changed files with 786 additions and 20 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = ["rust/composefs-core", "rust/composefs-sys"]
members = ["rust/composefs-sys", "rust/composefs-core", "rust/composefs-oci"]
resolver = "2"

[profile.dev]
Expand Down
20 changes: 20 additions & 0 deletions libcomposefs/lcfs-writer.c
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,26 @@ static int read_content(int fd, size_t size, uint8_t *buf)
return 0;
}

// Given a file descriptor, enable fsverity.
int lcfs_fd_enable_fsverity(int fd)
{
struct fsverity_enable_arg arg = {};

arg.version = 1;
arg.hash_algorithm = FS_VERITY_HASH_ALG_SHA256;
arg.block_size = 4096;
arg.salt_size = 0;
arg.salt_ptr = 0;
arg.sig_size = 0;
arg.sig_ptr = 0;

if (ioctl(fd, FS_IOC_ENABLE_VERITY, &arg) != 0) {
return -errno;
}
return 0;
}


static void digest_to_path(const uint8_t *csum, char *buf)
{
static const char hexchars[] = "0123456789abcdef";
Expand Down
1 change: 1 addition & 0 deletions libcomposefs/lcfs-writer.h
Original file line number Diff line number Diff line change
Expand Up @@ -157,5 +157,6 @@ LCFS_EXTERN int lcfs_fd_get_fsverity(uint8_t *digest, int fd);

LCFS_EXTERN int lcfs_node_set_from_content(struct lcfs_node_s *node, int dirfd,
const char *fname, int buildflags);
LCFS_EXTERN int lcfs_fd_enable_fsverity(int fd);

#endif
6 changes: 6 additions & 0 deletions rust/composefs-core/src/dumpfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,11 @@ fn unescape_to_osstr(s: &str) -> Result<Cow<OsStr>> {
Ok(r)
}

fn basic_path_validation(p: &Path) -> Result<()> {
anyhow::ensure!(p.is_absolute());
Ok(())
}

/// Unescape a string into a Rust `Path` which is really just an alias for a byte array,
/// although there is an implicit assumption that there are no embedded `NUL` bytes.
fn unescape_to_path(s: &str) -> Result<Cow<Path>> {
Expand Down Expand Up @@ -220,6 +225,7 @@ impl<'p> Entry<'p> {
let mut components = s.split(' ');
let mut next = |name: &str| components.next().ok_or_else(|| anyhow!("Missing {name}"));
let path = unescape_to_path(next("path")?)?;
basic_path_validation(&path)?;
let size = u64::from_str(next("size")?)?;
let modeval = next("mode")?;
let (is_hardlink, mode) = if let Some((_, rest)) = modeval.split_once('@') {
Expand Down
26 changes: 26 additions & 0 deletions rust/composefs-core/src/fsverity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,32 @@ pub fn fsverity_digest_from_fd(fd: BorrowedFd, digest: &mut Digest) -> std::io::
}
}

/// Enable fsverity on the provided fd
#[allow(unsafe_code)]
pub fn fsverity_enable(fd: BorrowedFd) -> std::io::Result<()> {
unsafe { map_result(composefs_sys::lcfs_fd_enable_fsverity(fd.as_raw_fd())) }
}

/// Try to enable fsverity on the provided fd; returns `true`
/// if the fd already had fsverity enabled or it was successfully
/// enabled. Returns `false` if the kernel or filesystem does not support it.
#[allow(unsafe_code)]
pub fn try_fsverity_enable(fd: BorrowedFd) -> std::io::Result<bool> {
match unsafe { map_result(composefs_sys::lcfs_fd_enable_fsverity(fd.as_raw_fd())) } {
Ok(()) => Ok(true),
Err(e) => {
let errno = e.raw_os_error().unwrap_or(libc::ENOSYS);
match errno {
// The file already has fsverity enabled
libc::EEXIST => Ok(true),
// The kernel or filesystem doesn't support it
libc::EOPNOTSUPP | libc::ENOTTY => Ok(false),
_ => Err(e),
}
}
}
}

#[cfg(test)]
mod tests {
use anyhow::Result;
Expand Down
23 changes: 23 additions & 0 deletions rust/composefs-oci/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "composefs-oci"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1.0"
bincode = { version = "1.3.3" }
containers-image-proxy = "0.5.5"
composefs = { path = "../composefs-core" }
cap-std-ext = "4.0"
camino = "1"
clap = { version= "4.2", features = ["derive"] }
fn-error-context = "0.2.0"
rustix = { version = "0.38.34", features = ["fs"] }
libc = "0.2"
serde = "1"
tar = "0.4.38"
tokio = { features = ["io-std", "time", "process", "rt", "net"], version = ">= 1.13.0" }
tokio-util = { features = ["io-util"], version = "0.7" }
tokio-stream = { features = ["sync"], version = "0.1.8" }
hex = "0.4.3"
serde_json = "1.0.117"
44 changes: 44 additions & 0 deletions rust/composefs-oci/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# composefs-oci

The high level goal of this crate is to be an opinionated
generic storage layer using composefs, with direct support
for OCI. Note not just OCI *containers* but also including
OCI artifacts too.

This crate is intended to be the successor to
the "storage core" of both ostree and containers/storage.

## Design

The composefs core just offers the primitive of creating
"superblocks" which can have regular file data point
to underlying "loose" objects stored in an arbitrary place.

cfs-oci (for short) roughly matches the goal of both
ostree and containers/storage in supporting multiple
versioned filesystem trees with associated metadata,
including support for e.g. garbage collection.

## CLI sketch: OCI container images

`cfs-oci --repo=/path/to/repo image list|pull|rm|mount`

## CLI sketch: OCI artifacts

`cfs-oci --repo=/path/to/repo artifact list|pull|rm`

## CLI sketch: Other

### Efficiently clone a repo

`cfs-oci clone /path/to/repo /path/to/clone`
This would use reflinks (if available) or hardlinks if not
for all the loose objects, but allow fully distinct namespacing/ownership
of images.

For example, it would probably make sense to have
bootc and podman use separate physical stores in
`/ostree` and `/var/lib/containers` - but if they're
on the same filesystem, we can efficiently and safely share
backing objects!

153 changes: 153 additions & 0 deletions rust/composefs-oci/src/fileutils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
use std::io;
use std::path::Path;

use anyhow::Result;
use cap_std_ext::{
cap_std::fs::{
DirBuilder, DirBuilderExt as _, OpenOptions, OpenOptionsExt as _, Permissions,
PermissionsExt as _,
},
cap_tempfile::TempFile,
};
use rustix::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd};

/// The default permissions set for directories; we assume
/// nothing else should be accessing this content. If you want
/// that, you can chmod() after, or use ACLs.
pub(crate) fn rwx_perms() -> Permissions {
Permissions::from_mode(0o700)
}
/// The default permissions for regular files. Ditto per above.
pub(crate) fn r_perms() -> Permissions {
Permissions::from_mode(0o400)
}

pub(crate) fn default_dirbuilder() -> DirBuilder {
let mut builder = DirBuilder::new();
builder.mode(rwx_perms().mode());
builder
}

/// For creating a file with the default permissions
pub(crate) fn default_file_create_options() -> OpenOptions {
let mut r = OpenOptions::new();
r.create(true);
r.mode(r_perms().mode());
r
}

/// Given a string, verify it is a single component of a path; it must
/// not contain `/`.
pub(crate) fn validate_single_path_component(s: &str) -> Result<()> {
anyhow::ensure!(!s.contains('/'));
Ok(())
}

pub(crate) fn parent_nonempty(p: &Path) -> Option<&Path> {
p.parent().filter(|v| !v.as_os_str().is_empty())
}

// Just ensures that path is not absolute, so that it can be passed
// to cap-std APIs. This makes no attempt
// to avoid directory escapes like `../` under the assumption
// that will be handled by a higher level function.
pub(crate) fn ensure_relative_path(path: &Path) -> &Path {
path.strip_prefix("/").unwrap_or(path)
}

/// Operates on a generic openat fd
pub(crate) fn ensure_dir(fd: BorrowedFd, p: &Path) -> io::Result<bool> {
use rustix::fs::AtFlags;
let mode = rwx_perms().mode();
match rustix::fs::mkdirat(fd, p, rustix::fs::Mode::from_raw_mode(mode)) {
Ok(()) => Ok(true),
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
let st = rustix::fs::statat(fd, p, AtFlags::SYMLINK_NOFOLLOW)?;
if !(st.st_mode & libc::S_IFDIR > 0) {
// TODO use https://doc.rust-lang.org/std/io/enum.ErrorKind.html#variant.NotADirectory
// once it's stable.
return Err(io::Error::new(io::ErrorKind::Other, "Found non-directory"));
}
Ok(false)
}
// If we got ENOENT, then loop again, but create the parents
Err(e) => Err(e.into()),
}
}

/// The cap-std default does not use RESOLVE_IN_ROOT; this does.
/// Additionally for good measure we use NO_MAGICLINKS and NO_XDEV.
/// We never expect to encounter a mounted /proc in our use cases nor
/// any other mountpoints at all really, but still.
pub(crate) fn openat_rooted(
dirfd: BorrowedFd,
path: impl AsRef<Path>,
) -> rustix::io::Result<OwnedFd> {
use rustix::fs::{OFlags, ResolveFlags};
rustix::fs::openat2(
dirfd,
path.as_ref(),
OFlags::NOFOLLOW | OFlags::CLOEXEC | OFlags::DIRECTORY,
rustix::fs::Mode::empty(),
ResolveFlags::IN_ROOT | ResolveFlags::NO_MAGICLINKS | ResolveFlags::NO_XDEV,
)
}

/// Manual implementation of recursive dir walking using openat2
pub(crate) fn ensure_dir_recursive(fd: BorrowedFd, p: &Path, init: bool) -> io::Result<bool> {
// Optimize the initial case by skipping the recursive calls;
// we just call mkdirat() and no-op if we get EEXIST
if !init {
if let Some(parent) = parent_nonempty(p) {
ensure_dir_recursive(fd, parent, false)?;
}
}
match ensure_dir(fd, p) {
Ok(b) => Ok(b),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => ensure_dir_recursive(fd, p, false),
Err(e) => Err(e),
}
}

/// Given a cap-std tmpfile, reopen its file in read-only mode. This is
/// needed for fsverity support.
pub(crate) fn reopen_tmpfile_ro(tf: &mut TempFile) -> std::io::Result<()> {
let procpath = format!("/proc/self/fd/{}", tf.as_file().as_fd().as_raw_fd());
let tf_ro = cap_std_ext::cap_std::fs::File::open_ambient(
procpath,
cap_std_ext::cap_std::ambient_authority(),
)?;
let tf = tf.as_file_mut();
*tf = tf_ro;
Ok(())
}

// pub(crate) fn normalize_path(path: &Utf8Path) -> Result<Utf8PathBuf> {
// let mut components = path.components().peekable();
// let r = if !matches!(components.peek(), Some(camino::Utf8Component::RootDir)) {
// [camino::Utf8Component::RootDir]
// .into_iter()
// .chain(components)
// .collect()
// } else {
// components.collect()
// };
// Ok(r)
// }

#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_relpath() {
let expected_foobar = "foo/bar";
let cases = [("foo/bar", expected_foobar), ("/foo/bar", expected_foobar)];
for (a, b) in cases {
assert_eq!(ensure_relative_path(Path::new(a)), Path::new(b));
}
let idem = ["./foo/bar", "./foo", "./"];
for case in idem {
assert_eq!(ensure_relative_path(Path::new(case)), Path::new(case));
}
}
}
54 changes: 54 additions & 0 deletions rust/composefs-oci/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use std::ffi::OsString;

use anyhow::Result;
use camino::Utf8PathBuf;
use clap::Parser;
use pull::cli_pull;

mod fileutils;
pub mod pull;
pub mod repo;

/// Options for specifying the repository
#[derive(Debug, Parser)]
pub(crate) struct RepoOpts {
/// Path to the repository
#[clap(long, value_parser)]
repo: Utf8PathBuf,
}

/// Options for importing a tar archive.
#[derive(Debug, Parser)]
pub(crate) struct PullOpts {
#[clap(flatten)]
repo_opts: RepoOpts,

/// Image reference
image: String,
}

/// Toplevel options for extended ostree functionality.
#[derive(Debug, Parser)]
#[clap(name = "ostree-ext")]
#[clap(rename_all = "kebab-case")]
#[allow(clippy::large_enum_variant)]
pub(crate) enum Opt {
/// Pull an image
Pull(PullOpts),
}

/// Parse the provided arguments and execute.
/// Calls [`clap::Error::exit`] on failure, printing the error message and aborting the program.
pub async fn run_from_iter<I>(args: I) -> Result<()>
where
I: IntoIterator,
I::Item: Into<OsString> + Clone,
{
run_from_opt(Opt::parse_from(args)).await
}

async fn run_from_opt(opt: Opt) -> Result<()> {
match opt {
Opt::Pull(opts) => cli_pull(opts).await,
}
}
17 changes: 17 additions & 0 deletions rust/composefs-oci/src/pull.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use anyhow::Result;

use crate::PullOpts;

pub async fn pull(
proxy: &containers_image_proxy::ImageProxy,
img: &containers_image_proxy::OpenedImage,
) -> Result<()> {
todo!()
}

pub(crate) async fn cli_pull(opts: PullOpts) -> Result<()> {
let proxy = containers_image_proxy::ImageProxy::new().await?;
let img = proxy.open_image(&opts.image).await?;

todo!()
}
Loading

0 comments on commit 122ed24

Please sign in to comment.