Skip to content

Commit 34f8395

Browse files
authored
Merge pull request #502 from imageworks/overlayfs-xattrs
Overlayfs basic supported features check and default inode index
2 parents b47cf8c + e963da8 commit 34f8395

File tree

9 files changed

+263
-12
lines changed

9 files changed

+263
-12
lines changed

Cargo.lock

Lines changed: 95 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ version = "0.36.0"
2828
[workspace.dependencies]
2929
anyhow = "1.0"
3030
async-trait = "0.1"
31+
cached = "0.42.0"
3132
chrono = { version = "0.4.19", features = ["serde"] }
3233
clap = { version = "4.3", features = ["derive", "env"] }
3334
colored = "2.0.0"

crates/spfs-cli/cmd-enter/src/cmd_enter.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,11 @@ impl CmdEnter {
127127
&mut self,
128128
config: &spfs::Config,
129129
) -> Result<Option<spfs::runtime::OwnedRuntime>> {
130+
// this function will eventually be required to discover the overlayfs
131+
// attributes. It can take many milliseconds to run so we prime the cache as
132+
// soon as possible in a separate thread
133+
std::thread::spawn(spfs::runtime::overlayfs::overlayfs_available_options_prime_cache);
134+
130135
let mut runtime = self.load_runtime(config).await?;
131136

132137
if self.exit.enabled {

crates/spfs/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ async-compression = { version = "0.3.15", features = ["tokio", "bzip2"] }
2020
async-trait = "0.1.52"
2121
async-recursion = "1.0"
2222
async-stream = "0.3"
23+
cached = { workspace = true }
2324
caps = "0.5.3"
2425
chrono = { workspace = true }
2526
close-err = "1.0"

crates/spfs/src/env.rs

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -803,28 +803,58 @@ fn have_required_join_capabilities() -> Result<bool> {
803803
&& effective.contains(&caps::Capability::CAP_SYS_CHROOT))
804804
}
805805

806+
const OVERLAY_ARGS_RO_PREFIX: &str = "ro";
807+
const OVERLAY_ARGS_INDEX: &str = "index";
808+
const OVERLAY_ARGS_INDEX_ON: &str = "index=on";
809+
const OVERLAY_ARGS_METACOPY: &str = "metacopy";
810+
const OVERLAY_ARGS_METACOPY_ON: &str = "metacopy=on";
811+
806812
/// A struct for holding the options that will be included
807813
/// in the overlayfs mount command when mounting an environment.
808814
#[derive(Default)]
809815
pub(crate) struct OverlayMountOptions {
810-
pub(crate) read_only: bool,
816+
/// Specifies that the overlay file system is mounted as read-only
817+
pub read_only: bool,
818+
/// When true, inodes are indexed in the mount so that
819+
/// files which share the same inode (hardlinks) are broken
820+
/// in the final mount and changes to one file don't affect
821+
/// the other.
822+
///
823+
/// This is the desired default behavior for
824+
/// spfs, since we rely on hardlinks for deduplication but
825+
/// expect that file to be able to appear in mutliple places
826+
/// as separate files that just so happen to share the same content.
827+
break_hardlinks: bool,
828+
/// When true, overlayfs will use extended file attributes to avoid
829+
/// copying file data when only the metadata of a file has changed.
830+
/// https://www.kernel.org/doc/html/latest/filesystems/overlayfs.html#metadata-only-copy-up
831+
metadata_copy_up: bool,
811832
}
812833

813834
impl OverlayMountOptions {
814835
/// Create the mount options for a runtime state
815836
fn new(rt: &runtime::Runtime) -> Self {
816837
Self {
817838
read_only: !rt.status.editable,
839+
break_hardlinks: true,
840+
metadata_copy_up: true,
818841
}
819842
}
820843

821844
/// Return the options that should be included in the mount request.
822-
pub(crate) fn options(&self) -> Vec<&str> {
845+
pub fn to_options(&self) -> Vec<&'static str> {
846+
let params = runtime::overlayfs::overlayfs_available_options();
847+
let mut opts = Vec::new();
823848
if self.read_only {
824-
vec![OVERLAY_ARGS_RO_PREFIX]
825-
} else {
826-
Vec::default()
849+
opts.push(OVERLAY_ARGS_RO_PREFIX);
827850
}
851+
if self.break_hardlinks && params.contains(OVERLAY_ARGS_INDEX) {
852+
opts.push(OVERLAY_ARGS_INDEX_ON);
853+
}
854+
if self.metadata_copy_up && params.contains(OVERLAY_ARGS_METACOPY) {
855+
opts.push(OVERLAY_ARGS_METACOPY_ON);
856+
}
857+
opts
828858
}
829859
}
830860

@@ -842,7 +872,7 @@ pub(crate) fn get_overlay_args<P: AsRef<Path>>(
842872
let mut args = String::with_capacity(4096);
843873

844874
let mount_options = OverlayMountOptions::new(rt);
845-
for option in mount_options.options() {
875+
for option in mount_options.to_options() {
846876
args.push_str(option);
847877
args.push(',');
848878
}
@@ -875,8 +905,6 @@ pub(crate) fn get_overlay_args<P: AsRef<Path>>(
875905
Ok(args)
876906
}
877907

878-
pub(crate) const OVERLAY_ARGS_RO_PREFIX: &str = "ro";
879-
880908
#[cfg(feature = "fuse-backend")]
881909
fn get_fuse_args(config: &runtime::Config, owner: &IsRootUser, read_only: bool) -> String {
882910
use fuser::MountOption::*;

crates/spfs/src/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ pub enum Error {
102102
#[error("Command, arguments or environment contained a nul byte, this is not supported")]
103103
CommandHasNul(#[source] std::ffi::NulError),
104104

105+
#[cfg(target_os = "linux")]
106+
#[error("OverlayFS kernel module does not appear to be installed")]
107+
OverlayFSNotInstalled,
108+
105109
#[error("{}, and {} more errors during clean", errors.get(0).unwrap(), errors.len() - 1)]
106110
IncompleteClean { errors: Vec<Self> },
107111
}

crates/spfs/src/runtime/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
//! Handles the setup and initialization of runtime environments
66
7-
mod overlayfs;
7+
pub mod overlayfs;
88
mod startup_csh;
99
mod startup_sh;
1010
mod storage;

crates/spfs/src/runtime/overlayfs.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,16 @@
22
// SPDX-License-Identifier: Apache-2.0
33
// https://github.com/imageworks/spk
44

5+
use std::collections::HashSet;
6+
use std::io::{BufRead, BufReader};
57
use std::os::unix::fs::MetadataExt;
68

9+
use crate::{Error, Result};
10+
11+
#[cfg(test)]
12+
#[path = "./overlayfs_test.rs"]
13+
mod overlayfs_test;
14+
715
pub fn is_removed_entry(meta: &std::fs::Metadata) -> bool {
816
// overlayfs uses character device files to denote
917
// a file that was removed, using this special file
@@ -14,3 +22,50 @@ pub fn is_removed_entry(meta: &std::fs::Metadata) -> bool {
1422
// - the device is always 0/0 for a whiteout file
1523
meta.rdev() == 0
1624
}
25+
26+
/// Get the set of supported overlayfs arguments on this machine
27+
#[cfg(target_os = "linux")]
28+
#[cached::proc_macro::once(sync_writes = true)]
29+
pub fn overlayfs_available_options() -> HashSet<String> {
30+
query_overlayfs_available_options().unwrap_or_else(|err| {
31+
tracing::warn!("Failed to detect supported overlayfs params: {err}");
32+
tracing::warn!(" > Falling back to the most conservative set, which is undesirable");
33+
Default::default()
34+
})
35+
}
36+
37+
/// Read available overlayfs settings from the kernel
38+
fn query_overlayfs_available_options() -> Result<HashSet<String>> {
39+
let output = std::process::Command::new("/sbin/modinfo")
40+
.arg("overlay")
41+
.output()
42+
.map_err(|err| Error::process_spawn_error("/sbin/modinfo".into(), err, None))?;
43+
44+
if output.status.code().unwrap_or(1) != 0 {
45+
return Err(Error::OverlayFSNotInstalled);
46+
}
47+
48+
parse_modinfo_params(&mut BufReader::new(output.stdout.as_slice()))
49+
}
50+
51+
/// Parses the available parameters from the output of `modinfo` for a kernel module
52+
#[cfg(target_os = "linux")]
53+
fn parse_modinfo_params<R: BufRead>(reader: &mut R) -> Result<HashSet<String>> {
54+
let mut params = HashSet::new();
55+
for line in reader.lines() {
56+
let line = line.map_err(|err| {
57+
Error::String(format!("Failed to read kernel module information: {err}"))
58+
})?;
59+
let param = match line.strip_prefix("parm:") {
60+
Some(remainder) => remainder.trim(),
61+
None => continue,
62+
};
63+
let name = match param.split_once(':') {
64+
Some((name, _remainder)) => name,
65+
None => param,
66+
};
67+
params.insert(name.to_owned());
68+
}
69+
70+
Ok(params)
71+
}

0 commit comments

Comments
 (0)