diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 1d5e4739a..1887ba9be 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2932,6 +2932,15 @@ pub struct RunArgs { /// By default, environment modifications are omitted, but enabled under `--verbose`. #[arg(long, env = EnvVars::UV_SHOW_RESOLUTION, value_parser = clap::builder::BoolishValueParser::new(), hide = true)] pub show_resolution: bool, + + /// Number of times that `uv run` will allow recursive invocations. + /// + /// The current recursion depth is tracked by environment variable. If environment variables are + /// cleared, uv will fail to detect the recursion depth. + /// + /// If uv reaches the maximum recursion depth, it will exit with an error. + #[arg(long, hide = true, env = EnvVars::UV_RUN_MAX_RECURSION_DEPTH)] + pub max_recursion_depth: Option, } #[derive(Args)] diff --git a/crates/uv-static/src/env_vars.rs b/crates/uv-static/src/env_vars.rs index 36085c828..8927509cd 100644 --- a/crates/uv-static/src/env_vars.rs +++ b/crates/uv-static/src/env_vars.rs @@ -623,4 +623,14 @@ impl EnvVars { /// Enables fetching files stored in Git LFS when installing a package from a Git repository. pub const UV_GIT_LFS: &'static str = "UV_GIT_LFS"; + + /// Number of times that `uv run` has been recursively invoked. Used to guard against infinite + /// recursion, e.g., when `uv run`` is used in a script shebang. + #[attr_hidden] + pub const UV_RUN_RECURSION_DEPTH: &'static str = "UV_RUN_RECURSION_DEPTH"; + + /// Number of times that `uv run` will allow recursive invocations, before exiting with an + /// error. + #[attr_hidden] + pub const UV_RUN_MAX_RECURSION_DEPTH: &'static str = "UV_RUN_MAX_RECURSION_DEPTH"; } diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 8ed679e48..3d034fc3e 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::env::VarError; use std::ffi::OsString; use std::fmt::Write; use std::io::Read; @@ -91,7 +92,23 @@ pub(crate) async fn run( env_file: Vec, no_env_file: bool, preview: PreviewMode, + max_recursion_depth: u32, ) -> anyhow::Result { + // Check if max recursion depth was exceeded. This most commonly happens + // for scripts with a shebang line like `#!/usr/bin/env -S uv run`, so try + // to provide guidance for that case. + let recursion_depth = read_recursion_depth_from_environment_variable()?; + if recursion_depth > max_recursion_depth { + bail!( + r" +`uv run` was recursively invoked {recursion_depth} times which exceeds the limit of {max_recursion_depth}. + +hint: If you are running a script with `{}` in the shebang, you may need to include the `{}` flag.", + "uv run".green(), + "--script".green(), + ); + } + // These cases seem quite complex because (in theory) they should change the "current package". // Let's ban them entirely for now. let mut requirements_from_stdin: bool = false; @@ -1034,6 +1051,12 @@ pub(crate) async fn run( )?; process.env(EnvVars::PATH, new_path); + // Increment recursion depth counter. + process.env( + EnvVars::UV_RUN_RECURSION_DEPTH, + (recursion_depth + 1).to_string(), + ); + // Ensure `VIRTUAL_ENV` is set. if interpreter.is_virtualenv() { process.env(EnvVars::VIRTUAL_ENV, interpreter.sys_prefix().as_os_str()); @@ -1464,3 +1487,24 @@ fn is_python_zipapp(target: &Path) -> bool { } false } + +/// Read and parse recursion depth from the environment. +/// +/// Returns Ok(0) if `EnvVars::UV_RUN_RECURSION_DEPTH` is not set. +/// +/// Returns an error if `EnvVars::UV_RUN_RECURSION_DEPTH` is set to a value +/// that cannot ber parsed as an integer. +fn read_recursion_depth_from_environment_variable() -> anyhow::Result { + let envvar = match std::env::var(EnvVars::UV_RUN_RECURSION_DEPTH) { + Ok(val) => val, + Err(VarError::NotPresent) => return Ok(0), + Err(e) => { + return Err(e) + .with_context(|| format!("invalid value for {}", EnvVars::UV_RUN_RECURSION_DEPTH)) + } + }; + + envvar + .parse::() + .with_context(|| format!("invalid value for {}", EnvVars::UV_RUN_RECURSION_DEPTH)) +} diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index b3f735fcd..befe8d812 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1516,6 +1516,7 @@ async fn run_project( args.env_file, args.no_env_file, globals.preview, + args.max_recursion_depth, )) .await } diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 91ba1eba2..f5864066b 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -299,9 +299,16 @@ pub(crate) struct RunSettings { pub(crate) settings: ResolverInstallerSettings, pub(crate) env_file: Vec, pub(crate) no_env_file: bool, + pub(crate) max_recursion_depth: u32, } impl RunSettings { + // Default value for UV_RUN_MAX_RECURSION_DEPTH if unset. This is large + // enough that it's unlikely a user actually needs this recursion depth, + // but short enough that we detect recursion quickly enough to avoid OOMing + // or hanging for a long time. + const DEFAULT_MAX_RECURSION_DEPTH: u32 = 100; + /// Resolve the [`RunSettings`] from the CLI and filesystem configuration. #[allow(clippy::needless_pass_by_value)] pub(crate) fn resolve(args: RunArgs, filesystem: Option) -> Self { @@ -344,6 +351,7 @@ impl RunSettings { show_resolution, env_file, no_env_file, + max_recursion_depth, } = args; let install_mirrors = filesystem @@ -403,6 +411,7 @@ impl RunSettings { env_file, no_env_file, install_mirrors, + max_recursion_depth: max_recursion_depth.unwrap_or(Self::DEFAULT_MAX_RECURSION_DEPTH), } } } diff --git a/crates/uv/tests/it/run.rs b/crates/uv/tests/it/run.rs index f20ec3c9a..05b691556 100644 --- a/crates/uv/tests/it/run.rs +++ b/crates/uv/tests/it/run.rs @@ -4202,3 +4202,41 @@ fn run_without_overlay() -> Result<()> { Ok(()) } + +/// See: +#[cfg(unix)] +#[test] +fn detect_infinite_recursion() -> Result<()> { + use crate::common::get_bin; + use indoc::formatdoc; + use std::os::unix::fs::PermissionsExt; + + let context = TestContext::new("3.12"); + + let test_script = context.temp_dir.child("main"); + test_script.write_str(&formatdoc! { r#" + #!{uv} run + + print("Hello, world!") + "#, uv = get_bin().display()})?; + + fs_err::set_permissions(test_script.path(), PermissionsExt::from_mode(0o0744))?; + + let mut cmd = std::process::Command::new(test_script.as_os_str()); + + // Set the max recursion depth to a lower amount to speed up testing. + cmd.env("UV_RUN_MAX_RECURSION_DEPTH", "5"); + + uv_snapshot!(context.filters(), cmd, @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: `uv run` was recursively invoked 6 times which exceeds the limit of 5. + + hint: If you are running a script with `uv run` in the shebang, you may need to include the `--script` flag. + "###); + + Ok(()) +}