Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 162 additions & 0 deletions integration-tests/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,166 @@ fn test_fallback_runfiles_dir(config: &TestConfig) -> Result<(), String> {
Ok(())
}

/// Test: Fallback runfiles discovery when the stub is invoked via a RELATIVE
/// argv[0] from a different working directory (the `bazel run` / deployed-binary
/// scenario), with no RUNFILES_DIR / RUNFILES_MANIFEST_FILE set.
///
/// Regression for aspect-build/rules_py#1113: a py_binary launched this way
/// aborted with "execve failed with errno 2". `<argv[0]>.runfiles` discovery
/// must resolve to an absolute location so the resolved program path is valid
/// regardless of the caller's cwd. `test_fallback_runfiles_dir` only covers the
/// absolute-argv[0] case (which already works).
fn test_relative_argv0_discovery(config: &TestConfig) -> Result<(), String> {
println!(" Running test: relative_argv0_discovery");

let test_dir = config.work_dir.join("test_relative_argv0");
let sub_dir = test_dir.join("subdir");
fs::create_dir_all(&sub_dir).map_err(|e| format!("Failed to create test dir: {}", e))?;

// Stub + its .runfiles live in subdir/; we invoke it as "subdir/<stub>"
// from test_dir so argv[0] is relative.
let stub_name = format!("relstub{}", EXE_EXT);
let stub_path = sub_dir.join(&stub_name);
let runfiles_dir = sub_dir.join(format!("{}.runfiles", stub_name));

let binary_dir = runfiles_dir.join(WORKSPACE_NAME).join("bin");
fs::create_dir_all(&binary_dir).map_err(|e| format!("Failed to create binary dir: {}", e))?;

let add_binary = config.test_binaries_dir.join(format!("add-numbers{}", EXE_EXT));
let dest_binary = binary_dir.join(format!("add-numbers{}", EXE_EXT));
fs::copy(&add_binary, &dest_binary).map_err(|e| format!("Failed to copy binary: {}", e))?;

#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&dest_binary)
.map_err(|e| format!("Failed to get permissions: {}", e))?
.permissions();
perms.set_mode(0o755);
fs::set_permissions(&dest_binary, perms)
.map_err(|e| format!("Failed to set permissions: {}", e))?;
}

let add_rlocation = format!("{}/bin/add-numbers{}", WORKSPACE_NAME, EXE_EXT);
finalize_stub(config, &stub_path, &[&add_rlocation, "5", "10"], &[0])?;

// Reproduce `bazel run`'s geometry exactly. bazel run execs the *real* binary
// but (a) passes a RELATIVE argv[0] (e.g. "bazel-bin/hello"), (b) sets cwd
// *inside* the runfiles tree (`<x>.runfiles/_main`), and (c) leaves
// RUNFILES_DIR unset. So `<argv[0]>.runfiles` resolved against cwd points
// somewhere that does not exist, and the launcher must instead resolve
// argv[0] to the real executable (/proc/self/exe, _NSGetExecutablePath).
//
// We exec the real stub path but override argv[0] to a relative string, and
// run from a cwd inside the runfiles where that relative argv[0] does not
// resolve to the stub or its .runfiles.
let inner_cwd = runfiles_dir.join(WORKSPACE_NAME);
let relative_argv0 = PathBuf::from("bin").join(&stub_name);
let mut cmd = Command::new(&stub_path);
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
cmd.arg0(&relative_argv0);
}
cmd.current_dir(&inner_cwd);
cmd.env_remove("RUNFILES_DIR");
cmd.env_remove("RUNFILES_MANIFEST_FILE");

let output = cmd.output().map_err(|e| format!("Failed to run stub: {}", e))?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let exit_code = output.status.code().unwrap_or(-1);

if exit_code != 0 {
return Err(format!(
"Stub failed with exit code {} (relative argv[0], no RUNFILES_DIR): {}",
exit_code, stderr
));
}
if !stdout.contains("SUM:15") {
return Err(format!("Unexpected output: {}. Expected 'SUM:15'", stdout));
}

println!(" PASS");
Ok(())
}

/// Test: the embedded program is reached through a RELATIVE symlink inside
/// runfiles (the aspect_rules_py venv shape: `<venv>/bin/python` is a relative
/// symlink to the hermetic interpreter), invoked via a relative argv[0] with no
/// RUNFILES_DIR. This mirrors aspect-build/rules_py#1113 more closely than
/// `relative_argv0_discovery`, whose embedded program is a regular file.
fn test_symlinked_program_relative_argv0(config: &TestConfig) -> Result<(), String> {
println!(" Running test: symlinked_program_relative_argv0");

let test_dir = config.work_dir.join("test_symlink_prog");
let sub_dir = test_dir.join("subdir");
fs::create_dir_all(&sub_dir).map_err(|e| format!("Failed to create test dir: {}", e))?;

let stub_name = format!("symstub{}", EXE_EXT);
let stub_path = sub_dir.join(&stub_name);
let runfiles_dir = sub_dir.join(format!("{}.runfiles", stub_name));

// Real binary lives at <runfiles>/<ws>/realbin/add-numbers; the stub's
// embedded program points at <runfiles>/<ws>/bin/python, a RELATIVE symlink
// to it (../realbin/add-numbers) — the venv shape.
let real_dir = runfiles_dir.join(WORKSPACE_NAME).join("realbin");
let venv_bin = runfiles_dir.join(WORKSPACE_NAME).join("bin");
fs::create_dir_all(&real_dir).map_err(|e| format!("mkdir realbin: {}", e))?;
fs::create_dir_all(&venv_bin).map_err(|e| format!("mkdir bin: {}", e))?;

let real_binary = real_dir.join(format!("add-numbers{}", EXE_EXT));
fs::copy(
config.test_binaries_dir.join(format!("add-numbers{}", EXE_EXT)),
&real_binary,
)
.map_err(|e| format!("copy binary: {}", e))?;

let symlink = venv_bin.join(format!("python{}", EXE_EXT));
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&real_binary)
.map_err(|e| format!("perms: {}", e))?
.permissions();
perms.set_mode(0o755);
fs::set_permissions(&real_binary, perms).map_err(|e| format!("set perms: {}", e))?;
// Relative symlink, like a relocatable venv: bin/python -> ../realbin/add-numbers
std::os::unix::fs::symlink(
format!("../realbin/add-numbers{}", EXE_EXT),
&symlink,
)
.map_err(|e| format!("symlink: {}", e))?;
}

let prog_rlocation = format!("{}/bin/python{}", WORKSPACE_NAME, EXE_EXT);
finalize_stub(config, &stub_path, &[&prog_rlocation, "5", "10"], &[0])?;

let relative_argv0 = PathBuf::from("subdir").join(&stub_name);
let mut cmd = Command::new(&relative_argv0);
cmd.current_dir(&test_dir);
cmd.env_remove("RUNFILES_DIR");
cmd.env_remove("RUNFILES_MANIFEST_FILE");

let output = cmd.output().map_err(|e| format!("Failed to run stub: {}", e))?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let exit_code = output.status.code().unwrap_or(-1);

if exit_code != 0 {
return Err(format!(
"Stub failed with exit code {} (symlinked program, relative argv[0], no RUNFILES_DIR): {}",
exit_code, stderr
));
}
if !stdout.contains("SUM:15") {
return Err(format!("Unexpected output: {}. Expected 'SUM:15'", stdout));
}

println!(" PASS");
Ok(())
}

/// Test: Fallback runfiles_manifest file discovery
fn test_fallback_runfiles_manifest(config: &TestConfig) -> Result<(), String> {
println!(" Running test: fallback_runfiles_manifest");
Expand Down Expand Up @@ -977,6 +1137,8 @@ fn main() -> ExitCode {
("orchestrator_env_propagation", test_orchestrator_env_propagation),
("mixed_arguments", test_mixed_arguments),
("fallback_runfiles_dir", test_fallback_runfiles_dir),
("relative_argv0_discovery", test_relative_argv0_discovery),
("symlinked_program_relative_argv0", test_symlinked_program_relative_argv0),
("fallback_runfiles_manifest", test_fallback_runfiles_manifest),
("print_env", test_print_env),
("large_manifest", test_large_manifest),
Expand Down
40 changes: 39 additions & 1 deletion runfiles-stub/src/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,10 @@ mod syscall_numbers {
pub const SYS_LSEEK: usize = 8;
pub const SYS_MMAP: usize = 9;
pub const SYS_ACCESS: usize = 21;
pub const SYS_READLINKAT: usize = 267;
pub const SYS_EXECVE: usize = 59;
pub const SYS_EXIT: usize = 60;
pub const AT_FDCWD: i32 = -100;
}

#[cfg(target_arch = "aarch64")]
Expand All @@ -77,6 +79,7 @@ mod syscall_numbers {
pub const SYS_LSEEK: usize = 62;
pub const SYS_MMAP: usize = 222;
pub const SYS_FACCESSAT: usize = 48; // faccessat is used on aarch64
pub const SYS_READLINKAT: usize = 78;
pub const SYS_EXECVE: usize = 221;
pub const SYS_EXIT: usize = 93;
pub const AT_FDCWD: i32 = -100; // Special fd for openat/faccessat to work like open/access
Expand Down Expand Up @@ -155,6 +158,15 @@ mod sc {
ret
}};
}
macro_rules! syscall4 {
($ty:ty; $nr:expr, $a1:expr, $a2:expr, $a3:expr, $a4:expr) => {{
let ret: $ty;
core::arch::asm!("syscall", in("rax") $nr, in("rdi") $a1, in("rsi") $a2, in("rdx") $a3,
in("r10") $a4,
lateout("rax") ret, lateout("rcx") _, lateout("r11") _);
ret
}};
}
macro_rules! syscall6 {
($ty:ty; $nr:expr, $a1:expr, $a2:expr, $a3:expr, $a4:expr, $a5:expr, $a6:expr) => {{
let ret: $ty;
Expand All @@ -164,7 +176,7 @@ mod sc {
ret
}};
}
pub(super) use {syscall2, syscall3, syscall6, syscall_noreturn, syscall_void};
pub(super) use {syscall2, syscall3, syscall4, syscall6, syscall_noreturn, syscall_void};
}

#[cfg(target_arch = "aarch64")]
Expand Down Expand Up @@ -328,6 +340,32 @@ pub fn path_exists(path: &[u8]) -> bool {
}
}

/// Absolute path of the running executable, via `readlinkat(/proc/self/exe)`.
///
/// Used for runfiles discovery when `argv[0]` is relative (as under `bazel run`):
/// `<argv[0]>.runfiles` can't be located against the cwd, so resolve the real
/// executable path instead. Returns None on failure (callers fall back to
/// argv[0]). Not wired for s390x (no readlinkat number defined here yet).
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
pub fn executable_path() -> Option<alloc::vec::Vec<u8>> {
let mut buf = alloc::vec![0u8; 4096];
// readlinkat(AT_FDCWD, "/proc/self/exe", buf, buf.len()); readlink does NOT
// NUL-terminate, so the return value is the byte length.
let path = b"/proc/self/exe\0";
let n: i64 =
unsafe { syscall4!(i64; SYS_READLINKAT, AT_FDCWD, path.as_ptr(), buf.as_mut_ptr(), buf.len()) };
if n <= 0 || (n as usize) >= buf.len() {
return None;
}
buf.truncate(n as usize);
Some(buf)
}

#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
pub fn executable_path() -> Option<alloc::vec::Vec<u8>> {
None
}

fn execve(filename: *const u8, argv: *const *const u8, envp: *const *const u8) -> i32 {
#[cfg(not(target_arch = "s390x"))]
{
Expand Down
37 changes: 37 additions & 0 deletions runfiles-stub/src/macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ mod sys {
pub fn close(fd: i32) -> i32;
pub fn access(path: *const u8, mode: i32) -> i32;
pub fn execve(path: *const u8, argv: *const *const u8, envp: *const *const u8) -> i32;
// Fills buf with the absolute path of the running executable. On the
// first call `*size` is the buffer capacity; if too small it's set to
// the required size and -1 is returned. Returns 0 on success.
pub fn _NSGetExecutablePath(buf: *mut u8, size: *mut u32) -> i32;
pub fn mmap(
addr: *mut core::ffi::c_void,
len: usize,
Expand Down Expand Up @@ -66,6 +70,39 @@ pub fn path_exists(path: &[u8]) -> bool {
unsafe { sys::access(path.as_ptr(), 0) == 0 } // F_OK = 0
}

/// Absolute path of the running executable, via `_NSGetExecutablePath`.
///
/// Used for runfiles discovery: a relative `argv[0]` (as passed by `bazel run`)
/// can't be joined against the cwd to locate `<exe>.runfiles`, so we resolve the
/// real executable path instead. The returned path is NOT canonicalized (it may
/// contain `.`/`..`/symlinks), which is fine — appending `.runfiles` and probing
/// still resolves correctly.
pub fn executable_path() -> Option<Vec<u8>> {
// Probe the required size, then fetch.
let mut size: u32 = 0;
unsafe {
// First call with size 0 sets `size` to the required capacity and
// returns -1.
sys::_NSGetExecutablePath(core::ptr::null_mut(), &mut size);
}
if size == 0 {
return None;
}
let mut buf: Vec<u8> = alloc::vec![0u8; size as usize];
let rc = unsafe { sys::_NSGetExecutablePath(buf.as_mut_ptr(), &mut size) };
if rc != 0 {
return None;
}
// Trim at the NUL terminator _NSGetExecutablePath wrote.
let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
buf.truncate(len);
if buf.is_empty() {
None
} else {
Some(buf)
}
}

// Environment variable lookup via the libc `environ` pointer.
pub fn get_env_var(name: &[u8]) -> Option<String> {
unsafe {
Expand Down
15 changes: 13 additions & 2 deletions runfiles-stub/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,19 @@ pub fn main(rt: platform::RuntimeArgs) -> ! {
let needs_transform = (transform_flags & argc_mask) != 0;
let needs_runfiles = needs_transform || export_runfiles_env;

// argv[0] (the stub's own path) is the fallback for runfiles discovery.
let executable_path = rt.program_path();
// Resolve the path used to locate `<exe>.runfiles`. argv[0] is the usual
// source, but `bazel run` (and any caller that execs us with a relative
// argv[0] from an unrelated cwd) makes a relative argv[0] useless for
// discovery — `<argv[0]>.runfiles` joined to cwd points nowhere. In that
// case fall back to the real executable path (/proc/self/exe,
// _NSGetExecutablePath). RUNFILES_DIR / RUNFILES_MANIFEST_FILE, when set,
// still take precedence inside Runfiles::create.
let argv0 = rt.program_path();
let resolved_exe: Option<Vec<u8>> = match argv0 {
Some(p) if !p.is_empty() && p[0] == b'/' => None, // absolute argv[0] is fine
_ => platform::executable_path(),
};
let executable_path = resolved_exe.as_deref().or(argv0);

let runfiles = if needs_runfiles {
match Runfiles::create(executable_path) {
Expand Down
9 changes: 9 additions & 0 deletions runfiles-stub/src/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,15 @@ extern "system" {
pub const SEP: char = '\\';
pub const NEWLINE: &[u8] = b"\r\n";

/// Real executable path for runfiles discovery. Not implemented on Windows:
/// the Windows backend launches via CreateProcessW and derives argv[0] from the
/// command line rather than a relative path, so the relative-argv[0] discovery
/// gap addressed on Unix doesn't arise here. Returns None to keep argv[0]-based
/// discovery (GetModuleFileNameW could be wired up later if needed).
pub fn executable_path() -> Option<alloc::vec::Vec<u8>> {
None
}

pub fn is_absolute(path: &str) -> bool {
let b = path.as_bytes();
b.len() >= 2
Expand Down
Loading