Skip to content

Commit 1ad0078

Browse files
committed
Disallow hard link rendering to external directories
It's not clear that supporting this was ever intended, but it was possible to run spfs-render and ask it to hard link into an external directory. The various other internal calls had `RenderType::Copy` hard coded. Using hard links into an external directory exposes the spfs repository to unintentional corruption since it allows those files to be edited and consequently modify payloads that are meant to be immutable. The change was prompted by wanting to refactor the code to work towards having different trait bounds for the functions that need a repository that supports renders (e.g., rendering into a repository) versus functions that render into a plain directory. Signed-off-by: J Robert Ray <[email protected]>
1 parent 086c6b9 commit 1ad0078

File tree

7 files changed

+424
-368
lines changed

7 files changed

+424
-368
lines changed

crates/spfs-cli/cmd-render/src/cmd_render.rs

+10-9
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,14 @@ pub struct CmdRender {
2929

3030
/// The strategy to use when rendering. Defaults to `Copy` when
3131
/// using a local directory and `HardLink` for the repository.
32+
///
33+
/// Only `Copy` is supported when rendering to a directory.
3234
#[clap(
3335
long,
34-
value_parser = clap::builder::PossibleValuesParser::new(spfs::storage::fs::RenderType::VARIANTS)
35-
.map(|s| s.parse::<spfs::storage::fs::RenderType>().unwrap())
36+
value_parser = clap::builder::PossibleValuesParser::new(spfs::storage::fs::CliRenderType::VARIANTS)
37+
.map(|s| s.parse::<spfs::storage::fs::CliRenderType>().unwrap())
3638
)]
37-
strategy: Option<spfs::storage::fs::RenderType>,
39+
strategy: Option<spfs::storage::fs::CliRenderType>,
3840

3941
/// The tag or digest of what to render, use a '+' to join multiple layers
4042
reference: String,
@@ -73,6 +75,9 @@ impl CmdRender {
7375
let fallback = FallbackProxy::new(repo, remotes);
7476

7577
let rendered = match &self.target {
78+
Some(_) if !matches!(self.strategy, Some(spfs::storage::fs::CliRenderType::Copy)) => {
79+
miette::bail!("Only 'Copy' strategy is supported when rendering to a directory")
80+
}
7681
Some(target) => self.render_to_dir(fallback, env_spec, target).await?,
7782
None => self.render_to_repo(fallback, env_spec).await?,
7883
};
@@ -118,11 +123,7 @@ impl CmdRender {
118123
]),
119124
);
120125
renderer
121-
.render_into_directory(
122-
env_spec,
123-
&target_dir,
124-
self.strategy.unwrap_or(spfs::storage::fs::RenderType::Copy),
125-
)
126+
.render_into_directory(env_spec, &target_dir)
126127
.await?;
127128
Ok(RenderResult {
128129
paths_rendered: vec![target_dir],
@@ -165,7 +166,7 @@ impl CmdRender {
165166
.collect();
166167
tracing::trace!("stack: {:?}", stack);
167168
renderer
168-
.render(&stack, self.strategy)
169+
.render(&stack, self.strategy.map(Into::into))
169170
.await
170171
.map(|paths_rendered| RenderResult {
171172
paths_rendered,

crates/spfs/src/resolve.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize};
1313
use super::config::get_config;
1414
use crate::prelude::*;
1515
use crate::storage::fallback::FallbackProxy;
16-
use crate::storage::fs::{ManifestRenderPath, RenderSummary};
16+
use crate::storage::fs::{CliRenderType, ManifestRenderPath, RenderSummary};
1717
use crate::{graph, runtime, storage, tracking, Error, Result};
1818

1919
#[cfg(test)]
@@ -53,7 +53,7 @@ async fn render_via_subcommand(
5353
// of overlayfs. To avoid any issues editing files and
5454
// hardlinks the rendering for them switches to Copy.
5555
cmd.arg("--strategy");
56-
cmd.arg::<&str>(crate::storage::fs::RenderType::Copy.into());
56+
cmd.arg::<&str>(CliRenderType::Copy.into());
5757
}
5858
cmd.arg(spec.to_string());
5959
tracing::debug!("{:?}", cmd);

crates/spfs/src/storage/fs/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ pub use render_reporter::{
2626
};
2727
pub use render_summary::{RenderSummary, RenderSummaryReporter};
2828
pub use renderer::{
29+
CliRenderType,
30+
HardLinkRenderType,
2931
RenderType,
3032
Renderer,
3133
DEFAULT_MAX_CONCURRENT_BLOBS,

crates/spfs/src/storage/fs/renderer.rs

+40-6
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,39 @@ pub const DEFAULT_MAX_CONCURRENT_BLOBS: usize = 100;
4040
/// See: [`Renderer::with_max_concurrent_branches`]
4141
pub const DEFAULT_MAX_CONCURRENT_BRANCHES: usize = 5;
4242

43+
/// Render type options available to command line commands.
4344
#[derive(Debug, Copy, Clone, strum::EnumString, strum::VariantNames, strum::IntoStaticStr)]
44-
pub enum RenderType {
45+
pub enum CliRenderType {
4546
HardLink,
4647
HardLinkNoProxy,
4748
Copy,
4849
}
4950

51+
#[derive(Debug, Default, Copy, Clone)]
52+
pub enum HardLinkRenderType {
53+
#[default]
54+
WithProxy,
55+
WithoutProxy,
56+
}
57+
58+
#[derive(Debug, Copy, Clone)]
59+
pub enum RenderType {
60+
HardLink(HardLinkRenderType),
61+
Copy,
62+
}
63+
64+
impl From<CliRenderType> for RenderType {
65+
fn from(cli_render_type: CliRenderType) -> Self {
66+
match cli_render_type {
67+
CliRenderType::HardLink => RenderType::HardLink(HardLinkRenderType::WithProxy),
68+
CliRenderType::HardLinkNoProxy => {
69+
RenderType::HardLink(HardLinkRenderType::WithoutProxy)
70+
}
71+
CliRenderType::Copy => RenderType::Copy,
72+
}
73+
}
74+
}
75+
5076
impl OpenFsRepository {
5177
fn get_render_storage(&self) -> Result<&crate::storage::fs::FsHashStore> {
5278
match &self.fs_impl.renders {
@@ -280,12 +306,13 @@ where
280306
futures.try_collect().await
281307
}
282308

283-
/// Recreate the full structure of a stored environment on disk
309+
/// Recreate the full structure of a stored environment on disk.
310+
///
311+
/// The `RenderType::Copy` strategy will be used.
284312
pub async fn render_into_directory<E: Into<tracking::EnvSpec>, P: AsRef<Path>>(
285313
&self,
286314
env_spec: E,
287315
target_dir: P,
288-
render_type: RenderType,
289316
) -> Result<()> {
290317
let env_spec = env_spec.into();
291318
let mut stack = graph::Stack::default();
@@ -306,8 +333,15 @@ where
306333
manifest.update(&next.to_tracking_manifest());
307334
}
308335
let manifest = manifest.to_graph_manifest();
309-
self.render_manifest_into_dir(&manifest, target_dir, render_type)
310-
.await
336+
self.render_manifest_into_dir(
337+
&manifest,
338+
target_dir,
339+
// Creating hard links outside of the spfs repo makes it possible
340+
// for users to modify payloads which are meant to be immutable,
341+
// therefore using the Copy strategy is enforced here.
342+
RenderType::Copy,
343+
)
344+
.await
311345
}
312346

313347
/// Render a manifest into the renders area of the underlying repository,
@@ -338,7 +372,7 @@ where
338372
self.render_manifest_into_dir(
339373
manifest,
340374
&working_dir,
341-
render_type.unwrap_or(RenderType::HardLink),
375+
render_type.unwrap_or(RenderType::HardLink(HardLinkRenderType::default())),
342376
)
343377
.await
344378
.map_err(|err| {

0 commit comments

Comments
 (0)