From 75f01d443cdbabe17b3ee8409f55ba58285fb8fa Mon Sep 17 00:00:00 2001 From: Jakub Sztandera Date: Thu, 30 Jul 2026 15:29:40 +0200 Subject: [PATCH] fix(docker): prevent root-owned bind mount directories Pre-create and validate bind mount sources before invoking Docker so Docker cannot silently create missing host directories as root. Replace legacy -v bind syntax with explicit --mount specifications, remove unnecessary runtime Cargo mounts, and support writable and read-only bind mounts through a shared helper. This prevents builder cache ownership failures that block Lotus builds and leave required binaries such as lotus-seed unavailable. Signed-off-by: Jakub Sztandera --- src/commands/build/docker.rs | 22 ++-- src/commands/start/curio/daemon.rs | 60 ++++----- src/commands/start/curio/db_setup.rs | 17 +-- src/commands/start/curio/verification.rs | 11 +- src/commands/start/foc_deployer/mod.rs | 21 +-- .../start/genesis/construction/creation.rs | 17 +-- .../start/genesis/construction/miner.rs | 26 ++-- .../start/genesis/construction/signers.rs | 22 +--- .../start/genesis/proof_parameters.rs | 61 ++++----- src/commands/start/genesis/sectors.rs | 27 ++-- src/commands/start/lotus/setup.rs | 38 +++--- .../start/lotus_miner/docker_command.rs | 48 +++---- src/commands/start/mod.rs | 4 +- .../start/multicall3_deploy/deployment.rs | 11 +- src/commands/start/usdfc_deploy/deployment.rs | 6 +- .../start/usdfc_deploy/foundry_setup.rs | 14 +- .../start/usdfc_deploy/verification.rs | 6 +- .../start/usdfc_funding/funding_operations.rs | 18 +-- src/docker/mod.rs | 2 + src/docker/mounts.rs | 123 ++++++++++++++++++ src/docker/portainer.rs | 10 +- src/docker/shell.rs | 14 +- 22 files changed, 338 insertions(+), 240 deletions(-) create mode 100644 src/docker/mounts.rs diff --git a/src/commands/build/docker.rs b/src/commands/build/docker.rs index 70e656d1..f9962872 100644 --- a/src/commands/build/docker.rs +++ b/src/commands/build/docker.rs @@ -2,6 +2,7 @@ //! //! This module handles Docker image building and container execution for project builds. +use crate::docker::push_bind_mount; use crate::docker::{ build::build_docker_image, core::{get_current_gid, get_current_uid, image_exists}, @@ -9,7 +10,7 @@ use crate::docker::{ use crate::embedded_assets; use crate::paths::foc_devnet_docker_volumes_cache; use std::collections::HashMap; -use std::fs; +use std::path::Path; use tracing::info; use super::Project; @@ -89,11 +90,17 @@ pub fn setup_docker_run_args( container_name, "-e".to_string(), "HOME=/home/foc-user".to_string(), - "-v".to_string(), - format!("{}:{}", source_dir, container_source_dir), - "-v".to_string(), - format!("{}:{}", output_dir, container_output_dir), ]; + push_bind_mount( + &mut docker_run_args, + Path::new(source_dir), + container_source_dir, + )?; + push_bind_mount( + &mut docker_run_args, + Path::new(output_dir), + container_output_dir, + )?; // Load and apply volume mappings for this image let volume_map = load_volume_map("builder")?; @@ -103,10 +110,7 @@ pub fn setup_docker_run_args( for (host_subdir, container_path) in volume_map { let host_path = image_volumes_dir.join(&host_subdir); - // Ensure the directory exists - fs::create_dir_all(&host_path)?; - docker_run_args.push("-v".to_string()); - docker_run_args.push(format!("{}:{}", host_path.display(), container_path)); + push_bind_mount(&mut docker_run_args, &host_path, &container_path)?; } } diff --git a/src/commands/start/curio/daemon.rs b/src/commands/start/curio/daemon.rs index 7a7d80bf..28198db0 100644 --- a/src/commands/start/curio/daemon.rs +++ b/src/commands/start/curio/daemon.rs @@ -9,7 +9,9 @@ use crate::commands::start::curio::constants::CURIO_LAYERS; use crate::docker::command_logger::run_and_log_command_strings; use crate::docker::init::set_volume_ownership; use crate::docker::network::{lotus_network_name, pdp_miner_network_name}; -use crate::docker::{container_exists, stop_and_remove_container}; +use crate::docker::{ + container_exists, push_bind_mount, push_read_only_bind_mount, stop_and_remove_container, +}; use crate::paths::{ foc_devnet_bin, foc_devnet_curio_sp_volume, foc_devnet_genesis_sectors_pdp_sp, foc_devnet_proof_parameters, CONTAINER_FILECOIN_PROOF_PARAMS_PATH, @@ -201,35 +203,33 @@ fn build_docker_create_args( format!("{}:4702", pdp_port), ]); - // Volume mounts - let volume_mounts = vec![ - format!( - "{}:/home/foc-user/.curio", - curio_sp_dir.join(".curio").display() - ), - format!( - "{}:/home/foc-user/curio/fast-storage", - curio_sp_dir.join("fast-storage").display() - ), - format!( - "{}:/home/foc-user/curio/long-term-storage", - curio_sp_dir.join("long-term-storage").display() - ), - format!("{}:/usr/local/bin/lotus-bins", bin_dir.display()), - format!( - "{}:/home/foc-user/genesis-sectors:ro", - genesis_sectors_dir.display() - ), - format!( - "{}:{}", - proof_params_dir.display(), - CONTAINER_FILECOIN_PROOF_PARAMS_PATH - ), - ]; - - for mount in volume_mounts { - docker_args.extend_from_slice(&["-v".to_string(), mount]); - } + // Validated bind mounts + push_bind_mount( + &mut docker_args, + &curio_sp_dir.join(".curio"), + "/home/foc-user/.curio", + )?; + push_bind_mount( + &mut docker_args, + &curio_sp_dir.join("fast-storage"), + "/home/foc-user/curio/fast-storage", + )?; + push_bind_mount( + &mut docker_args, + &curio_sp_dir.join("long-term-storage"), + "/home/foc-user/curio/long-term-storage", + )?; + push_bind_mount(&mut docker_args, &bin_dir, "/usr/local/bin/lotus-bins")?; + push_read_only_bind_mount( + &mut docker_args, + &genesis_sectors_dir, + "/home/foc-user/genesis-sectors", + )?; + push_bind_mount( + &mut docker_args, + &proof_params_dir, + CONTAINER_FILECOIN_PROOF_PARAMS_PATH, + )?; // Add environment variables using shared builders let foc_env = build_foc_contract_env_vars(context)?; diff --git a/src/commands/start/curio/db_setup.rs b/src/commands/start/curio/db_setup.rs index 19c562c9..3c423fed 100644 --- a/src/commands/start/curio/db_setup.rs +++ b/src/commands/start/curio/db_setup.rs @@ -12,6 +12,7 @@ use crate::commands::start::lotus_utils::{build_fullnode_api_info, read_lotus_to use crate::constants::{ DB_NAME, DB_PASSWORD, DB_USER, POSTGRES_CONTAINER_PORT, SCYLLA_CQL_CONTAINER_PORT, }; +use crate::docker::bind_mount; use crate::docker::command_logger::run_and_log_command; use crate::docker::containers::{ lotus_container_name, postgres_container_name, scylla_container_name, @@ -172,11 +173,11 @@ fn create_base_cluster( // Get binary directory for volume mount let bin_dir = foc_devnet_bin(); - let bin_mount = format!("{}:/usr/local/bin/lotus-bins", bin_dir.display()); + let bin_mount = bind_mount(&bin_dir, "/usr/local/bin/lotus-bins")?; // Get lotus-data directory for volume mount (needed for token and LOTUS_PATH) let lotus_data_dir = foc_devnet_docker_volumes().join("lotus-data"); - let lotus_data_mount = format!("{}:/lotus-data", lotus_data_dir.display()); + let lotus_data_mount = bind_mount(&lotus_data_dir, "/lotus-data")?; // Create a unique container name for this operation let container_name = format!("foc-{}-curio-db-setup-{}", run_id, sp_index); @@ -189,9 +190,9 @@ fn create_base_cluster( &container_name, "--network", &pdp_network, - "-v", + "--mount", &bin_mount, - "-v", + "--mount", &lotus_data_mount, ]; @@ -282,11 +283,11 @@ fn create_pdp_layer(context: &SetupContext, sp_index: usize) -> Result<(), Box Result<(), Box&1 | tee /tmp/foc-dep docker_args.push(format!("{}={}", key, value)); } - // Add volumes - docker_args.push("-v".to_string()); - docker_args.push(format!("{}:/opt/bin", bin_dir.display())); - docker_args.push("-v".to_string()); - docker_args.push(format!( - "{}:/home/foc-user/.cargo", - builder_volumes_dir.join("cargo").display() - )); - docker_args.push("-v".to_string()); - docker_args.push(format!("{}:/service_contracts", contracts_dir.display())); + // Add validated bind mounts + push_bind_mount(&mut docker_args, &bin_dir, "/opt/bin")?; + push_bind_mount(&mut docker_args, &contracts_dir, "/service_contracts")?; if let Some(pdp_repo_path) = params.pdp_repo_path { let pdp_repo = pdp_repo_path .canonicalize() .unwrap_or_else(|_| pdp_repo_path.to_path_buf()); - docker_args.push("-v".to_string()); - docker_args.push(format!("{}:/service_contracts/lib/pdp", pdp_repo.display())); + push_bind_mount(&mut docker_args, &pdp_repo, "/service_contracts/lib/pdp")?; } // Add image and command diff --git a/src/commands/start/genesis/construction/creation.rs b/src/commands/start/genesis/construction/creation.rs index b7aacc0d..92d8ad24 100644 --- a/src/commands/start/genesis/construction/creation.rs +++ b/src/commands/start/genesis/construction/creation.rs @@ -3,7 +3,8 @@ //! This module handles creating the initial genesis file using lotus-seed. use crate::commands::start::genesis::constants; -use crate::paths::{foc_devnet_bin, foc_devnet_docker_volumes_cache, foc_devnet_genesis}; +use crate::docker::push_bind_mount; +use crate::paths::{foc_devnet_bin, foc_devnet_genesis}; use std::fs; use std::process::Command; use tracing::info; @@ -31,8 +32,6 @@ pub fn create_genesis_file(run_id: &str) -> Result<(), Box Result<(), Box Result<(), Box Result<(), Box Result<(), Box> { "run", "-u", "root", - "-v", - &format!("{}:/work", parent.display()), + "--mount", + &format!("type=bind,source={},target=/work", parent.display()), crate::constants::BUILDER_DOCKER_IMAGE, "rm", "-rf", diff --git a/src/commands/start/multicall3_deploy/deployment.rs b/src/commands/start/multicall3_deploy/deployment.rs index 835fb561..14e92652 100644 --- a/src/commands/start/multicall3_deploy/deployment.rs +++ b/src/commands/start/multicall3_deploy/deployment.rs @@ -6,6 +6,7 @@ use super::key_management; use super::prerequisites::check_required_addresses; use crate::commands::start::lotus_utils::get_lotus_rpc_url; use crate::docker::command_logger::run_and_log_command_strings; +use crate::docker::push_bind_mount; use crate::paths::foc_devnet_multicall3_repo; use std::error::Error; use tracing::{error, info}; @@ -52,8 +53,7 @@ pub fn deploy_multicall3( ); let container_name = format!("foc-{}-multicall3-deploy", run_id); - let volume_mount = format!("{}:/workspace", multicall3_repo.display()); - let args: Vec = vec![ + let mut args = vec![ "run".to_string(), "-u".to_string(), "foc-user".to_string(), @@ -61,13 +61,14 @@ pub fn deploy_multicall3( container_name, "--network".to_string(), "host".to_string(), // Use host network to access Lotus RPC on dynamic port - "-v".to_string(), - volume_mount, + ]; + push_bind_mount(&mut args, &multicall3_repo, "/workspace")?; + args.extend([ crate::constants::BUILDER_DOCKER_IMAGE.to_string(), "bash".to_string(), "-c".to_string(), deploy_cmd, - ]; + ]); let key = format!("multicall3_deploy_{}", run_id); let output = run_and_log_command_strings("docker", &args, context, &key)?; diff --git a/src/commands/start/usdfc_deploy/deployment.rs b/src/commands/start/usdfc_deploy/deployment.rs index a8e54e2c..fe1d9ef1 100644 --- a/src/commands/start/usdfc_deploy/deployment.rs +++ b/src/commands/start/usdfc_deploy/deployment.rs @@ -7,6 +7,7 @@ use super::key_management::get_deployer_private_key; use super::prerequisites::check_required_addresses; use crate::commands::start::lotus_utils::get_lotus_rpc_url; use crate::commands::start::step::SetupContext; +use crate::docker::bind_mount; use crate::docker::command_logger::run_and_log_command; use std::error::Error; use std::path::PathBuf; @@ -43,6 +44,7 @@ pub fn deploy_mock_usdfc_foundry( ); let key = format!("usdfc_deploy_{}", run_id); + let contract_mount = bind_mount(&contract_dir, "/workspace")?; let output = run_and_log_command( "docker", &[ @@ -53,8 +55,8 @@ pub fn deploy_mock_usdfc_foundry( &format!("foc-{}-usdfc-deploy", run_id), "--network", "host", // Use host network to access Lotus RPC on dynamic port - "-v", - &format!("{}:/workspace", contract_dir.display()), + "--mount", + &contract_mount, crate::constants::BUILDER_DOCKER_IMAGE, "bash", "-c", diff --git a/src/commands/start/usdfc_deploy/foundry_setup.rs b/src/commands/start/usdfc_deploy/foundry_setup.rs index d0fb823c..3e2b953b 100644 --- a/src/commands/start/usdfc_deploy/foundry_setup.rs +++ b/src/commands/start/usdfc_deploy/foundry_setup.rs @@ -4,6 +4,7 @@ //! for deploying the MockUSDFC contract. use crate::commands::start::step::SetupContext; +use crate::docker::bind_mount; use crate::docker::command_logger::run_and_log_command; use crate::embedded_assets; use crate::paths::foc_devnet_run_dir; @@ -48,6 +49,7 @@ pub fn setup_foundry_project( contract_dir: &Path, run_id: &str, ) -> Result<(), Box> { + let contract_mount = bind_mount(contract_dir, "/workspace")?; let openzeppelin_path = contract_dir.join("lib/openzeppelin-contracts"); if !openzeppelin_path.exists() { @@ -67,8 +69,8 @@ pub fn setup_foundry_project( &container_name, "-u", "foc-user", - "-v", - &format!("{}:/workspace", contract_dir.display()), + "--mount", + &contract_mount, crate::constants::BUILDER_DOCKER_IMAGE, "bash", "-c", @@ -98,8 +100,8 @@ pub fn setup_foundry_project( &container_name, "-u", "foc-user", - "-v", - &format!("{}:/workspace", contract_dir.display()), + "--mount", + &contract_mount, crate::constants::BUILDER_DOCKER_IMAGE, "bash", "-c", @@ -134,8 +136,8 @@ pub fn setup_foundry_project( &container_name, "-u", "foc-user", - "-v", - &format!("{}:/workspace", contract_dir.display()), + "--mount", + &contract_mount, crate::constants::BUILDER_DOCKER_IMAGE, "bash", "-c", diff --git a/src/commands/start/usdfc_deploy/verification.rs b/src/commands/start/usdfc_deploy/verification.rs index b2c6c576..0466a0da 100644 --- a/src/commands/start/usdfc_deploy/verification.rs +++ b/src/commands/start/usdfc_deploy/verification.rs @@ -3,6 +3,7 @@ //! This module handles the verification of deployed MockUSDFC contracts. use crate::commands::start::step::SetupContext; +use crate::docker::bind_mount; use crate::docker::command_logger::run_and_log_command; use crate::utils::retry::{retry_with_fixed_delay, DEFAULT_MAX_RETRIES, DEFAULT_RETRY_DELAY_SECS}; use std::error::Error; @@ -28,6 +29,7 @@ pub fn verify_mock_usdfc( std::thread::sleep(std::time::Duration::from_secs( TRANSACTION_CONFIRMATION_WAIT_SECS, )); + let contract_mount = bind_mount(contract_dir, "/workspace")?; // Retry verification with fixed delay let verification_result = retry_with_fixed_delay( @@ -54,8 +56,8 @@ pub fn verify_mock_usdfc( "foc-user", "--network", "host", - "-v", - &format!("{}:/workspace", contract_dir.display()), + "--mount", + &contract_mount, crate::constants::BUILDER_DOCKER_IMAGE, "bash", "-c", diff --git a/src/commands/start/usdfc_funding/funding_operations.rs b/src/commands/start/usdfc_funding/funding_operations.rs index 0786c9d0..eaa06235 100644 --- a/src/commands/start/usdfc_funding/funding_operations.rs +++ b/src/commands/start/usdfc_funding/funding_operations.rs @@ -3,11 +3,13 @@ //! This module provides utilities for transferring MockUSDFC tokens between addresses. use crate::commands::start::step::SetupContext; +use crate::docker::bind_mount; use crate::docker::command_logger::run_and_log_command; use crate::utils::retry::{retry_with_fixed_delay, DEFAULT_MAX_RETRIES, DEFAULT_RETRY_DELAY_SECS}; use ethers_core::types::U256; use hex; use std::error::Error; +use std::path::Path; use tracing::info; /// Parameters for MockUSDFC transfer operations @@ -55,6 +57,7 @@ pub fn transfer_mock_usdfc( context.run_id(), params.description.replace(" ", "-").replace("→", "to") ); + let workspace_mount = bind_mount(Path::new("/tmp"), "/workspace")?; let output = run_and_log_command( "docker", &[ @@ -63,8 +66,8 @@ pub fn transfer_mock_usdfc( &container_name, "--network", "host", // Use host network to access localhost:1234 - "-v", - "/tmp:/workspace", + "--mount", + &workspace_mount, crate::constants::BUILDER_DOCKER_IMAGE, "bash", "-c", @@ -99,6 +102,8 @@ pub fn check_mock_usdfc_balance( context.run_id(), ð_address[..8] ); + let contract_dir = crate::paths::project_root()?.join("contracts/MockUSDFC"); + let workspace_mount = bind_mount(&contract_dir, "/workspace")?; let output = run_and_log_command( "docker", &[ @@ -108,13 +113,8 @@ pub fn check_mock_usdfc_balance( &container_name, "--network", "host", - "-v", - &format!( - "{}:/workspace", - crate::paths::project_root()? - .join("contracts/MockUSDFC") - .display() - ), + "--mount", + &workspace_mount, crate::constants::BUILDER_DOCKER_IMAGE, "bash", "-c", diff --git a/src/docker/mod.rs b/src/docker/mod.rs index 162c3f4d..91506991 100644 --- a/src/docker/mod.rs +++ b/src/docker/mod.rs @@ -10,6 +10,7 @@ pub mod containers; pub mod core; pub mod init; pub mod logs; +pub mod mounts; pub mod network; pub mod portainer; pub mod shell; @@ -38,6 +39,7 @@ pub use logs::{ list_foc_devnet_containers, persist_foc_container_logs, remove_dead_foc_containers, write_post_start_status_log, }; +pub use mounts::{bind_mount, prepare_bind_source, push_bind_mount, push_read_only_bind_mount}; pub use network::{ connect_container_to_network, create_all_networks, delete_all_networks, is_foc_devnet_network, lotus_miner_network_name, lotus_network_name, pdp_miner_network_name, diff --git a/src/docker/mounts.rs b/src/docker/mounts.rs new file mode 100644 index 00000000..8c8f19c3 --- /dev/null +++ b/src/docker/mounts.rs @@ -0,0 +1,123 @@ +//! Docker bind mount preparation. +//! +//! This module ensures bind source directories exist and are writable before +//! they are passed to Docker. + +use std::error::Error; +use std::fs; +use std::path::Path; + +/// Create a bind source directory and verify the current user can write to it. +pub fn prepare_bind_source(source: &Path) -> Result<(), Box> { + fs::create_dir_all(source).map_err(|error| { + format!( + "Failed to create Docker bind source {}: {}", + source.display(), + error + ) + })?; + verify_bind_source_writable(source) +} + +/// Verify the current user can create files in a bind source directory. +fn verify_bind_source_writable(source: &Path) -> Result<(), Box> { + tempfile::tempfile_in(source) + .map_err(|error| { + format!( + "Docker bind source is not writable by the current user: {}: {}", + source.display(), + error + ) + .into() + }) + .map(|_| ()) +} + +/// Build a validated directory bind mount value for Docker `--mount`. +pub fn bind_mount(source: &Path, target: &str) -> Result> { + bind_mount_with_mode(source, target, false) +} + +/// Append a validated directory bind mount to Docker command arguments. +pub fn push_bind_mount( + args: &mut Vec, + source: &Path, + target: &str, +) -> Result<(), Box> { + push_bind_mount_with_mode(args, source, target, false) +} + +/// Append a validated read-only directory bind mount to Docker arguments. +pub fn push_read_only_bind_mount( + args: &mut Vec, + source: &Path, + target: &str, +) -> Result<(), Box> { + push_bind_mount_with_mode(args, source, target, true) +} + +/// Append a validated directory bind mount with the requested access mode. +fn push_bind_mount_with_mode( + args: &mut Vec, + source: &Path, + target: &str, + read_only: bool, +) -> Result<(), Box> { + args.push("--mount".to_string()); + args.push(bind_mount_with_mode(source, target, read_only)?); + Ok(()) +} + +/// Build a validated directory bind mount with the requested access mode. +fn bind_mount_with_mode( + source: &Path, + target: &str, + read_only: bool, +) -> Result> { + prepare_bind_source(source)?; + let read_only_option = if read_only { ",readonly" } else { "" }; + Ok(format!( + "type=bind,source={},target={}{}", + source.display(), + target, + read_only_option + )) +} + +#[cfg(test)] +mod tests { + use super::{bind_mount, push_read_only_bind_mount}; + + /// Missing bind sources are created before the mount is returned. + #[test] + fn bind_mount_creates_source_directory() { + let root = tempfile::tempdir().unwrap(); + let source = root.path().join("nested/source"); + + let mount = bind_mount(&source, "/workspace").unwrap(); + + assert!(source.is_dir()); + assert_eq!( + mount, + format!("type=bind,source={},target=/workspace", source.display()) + ); + } + + /// Read-only mounts use Docker's `readonly` mount option. + #[test] + fn read_only_mount_appends_mount_arguments() { + let root = tempfile::tempdir().unwrap(); + let mut args = vec!["run".to_string()]; + + push_read_only_bind_mount(&mut args, root.path(), "/data").unwrap(); + + assert_eq!(args[1], "--mount"); + assert_eq!( + args[2], + format!( + "type=bind,source={},target=/data,readonly", + root.path().display() + ) + ); + } +} diff --git a/src/docker/portainer.rs b/src/docker/portainer.rs index cd02ce92..6e9a9136 100644 --- a/src/docker/portainer.rs +++ b/src/docker/portainer.rs @@ -86,7 +86,7 @@ pub fn start_portainer(run_id: &str, port: u16) -> Result<(), Box> { // Start Portainer container info!("Starting container..."); let port_mapping = format!("{}:9000", port); - let volume_mapping = format!("{}:/data", PORTAINER_DATA_VOLUME); + let volume_mount = format!("type=volume,source={},target=/data", PORTAINER_DATA_VOLUME); docker_command(&[ "run", "-d", @@ -94,10 +94,10 @@ pub fn start_portainer(run_id: &str, port: u16) -> Result<(), Box> { &container_name, "-p", &port_mapping, - "-v", - "/var/run/docker.sock:/var/run/docker.sock", - "-v", - &volume_mapping, + "--mount", + "type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock", + "--mount", + &volume_mount, "--restart", "unless-stopped", PORTAINER_IMAGE, diff --git a/src/docker/shell.rs b/src/docker/shell.rs index cb4e24cc..ed288892 100644 --- a/src/docker/shell.rs +++ b/src/docker/shell.rs @@ -101,16 +101,18 @@ pub fn docker_run_host_network(image: &str, args: &[&str]) -> Result Result> { let mut full_args = vec!["run"]; - for volume in volumes { - full_args.push("-v"); - full_args.push(volume); + for mount in mounts { + full_args.push("--mount"); + full_args.push(mount); } full_args.extend_from_slice(args); full_args.push(image);