Skip to content

Commit

Permalink
Detect infinite recursion in uv run.
Browse files Browse the repository at this point in the history
Handle potential infinite recursion if `uv run` recursively invokes `uv
run`. This can happen if the shebang line of a script includes `uv run`.

Handled by adding a new environment variable `UV_RUN_RECURSION_DEPTH`, which
contains a counter of the number of times that uv run has been transitively
invoked. If unset, it defaults to zero, and each time uv run starts a
subprocess it increments the counter.

Closes astral-sh#11220.
  • Loading branch information
ssanderson committed Feb 10, 2025
1 parent cf366a5 commit 3fa9ccb
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 0 deletions.
10 changes: 10 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2932,6 +2932,16 @@ 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 this process has been recursively invoked by uv
/// run. Used to detect and error on potential infinite recursion.
#[arg(long, hide = true, env = EnvVars::UV_RUN_RECURSION_DEPTH)]
pub recursion_depth: Option<u32>,

/// Number of times that uv run will recursively invoke itself before
/// giving up.
#[arg(long, hide = true, env = EnvVars::UV_RUN_MAX_RECURSION_DEPTH)]
pub max_recursion_depth: Option<u32>,
}

#[derive(Args)]
Expand Down
11 changes: 11 additions & 0 deletions crates/uv-static/src/env_vars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -614,4 +614,15 @@ 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 recursively invoked itself. Used to
/// guard against infinite recursion when uv run is in a script shebang
/// line.
#[attr_hidden]
pub const UV_RUN_RECURSION_DEPTH: &'static str = "UV_RUN_RECURSION_DEPTH";

/// Maximum number of times that uv run will invoke itself before giving
/// up.
#[attr_hidden]
pub const UV_RUN_MAX_RECURSION_DEPTH: &'static str = "UV_RUN_MAX_RECURSION_DEPTH";
}
17 changes: 17 additions & 0 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,18 @@ pub(crate) async fn run(
env_file: Vec<PathBuf>,
no_env_file: bool,
preview: PreviewMode,
recursion_depth: u32,
max_recursion_depth: u32,
) -> anyhow::Result<ExitStatus> {
// 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.
if recursion_depth > max_recursion_depth {
bail!(
"Exiting because `uv run` invoked itself recursively {max_recursion_depth} times. If you are running a script with `uv run` as the shebang line, you may need to include --script in the arguments to uv.",
);
}

// 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;
Expand Down Expand Up @@ -1080,6 +1091,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());
Expand Down
2 changes: 2 additions & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1512,6 +1512,8 @@ async fn run_project(
args.env_file,
args.no_env_file,
globals.preview,
args.recursion_depth,
args.max_recursion_depth,
))
.await
}
Expand Down
12 changes: 12 additions & 0 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,9 +299,17 @@ pub(crate) struct RunSettings {
pub(crate) settings: ResolverInstallerSettings,
pub(crate) env_file: Vec<PathBuf>,
pub(crate) no_env_file: bool,
pub(crate) recursion_depth: u32,
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<FilesystemOptions>) -> Self {
Expand Down Expand Up @@ -344,6 +352,8 @@ impl RunSettings {
show_resolution,
env_file,
no_env_file,
recursion_depth,
max_recursion_depth,
} = args;

let install_mirrors = filesystem
Expand Down Expand Up @@ -403,6 +413,8 @@ impl RunSettings {
env_file,
no_env_file,
install_mirrors,
recursion_depth: recursion_depth.unwrap_or(0),
max_recursion_depth: max_recursion_depth.unwrap_or(Self::DEFAULT_MAX_RECURSION_DEPTH),
}
}
}
Expand Down
36 changes: 36 additions & 0 deletions crates/uv/tests/it/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4120,3 +4120,39 @@ fn run_without_overlay() -> Result<()> {

Ok(())
}

/// See: <https://github.com/astral-sh/uv/issues/11220>
#[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()})?;

std::fs::set_permissions(test_script.path(), PermissionsExt::from_mode(0744))?;

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: Exiting because `uv run` invoked itself recursively 5 times. If you are running a script with `uv run` as the shebang line, you may need to include --script in the arguments to uv.
"###);

Ok(())
}

0 comments on commit 3fa9ccb

Please sign in to comment.