Skip to content
Merged
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
28 changes: 20 additions & 8 deletions crates/socket-patch-core/src/crawlers/cargo_crawler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,15 +261,10 @@ impl CargoCrawler {
Some((name.to_string(), version.to_string()))
}

/// Get `CARGO_HOME`, defaulting to `$HOME/.cargo` (`None` with no
/// home). An empty value means unset (the env_non_empty convention) —
/// `PathBuf::from("")` would otherwise resolve `registry/src` against
/// the CWD and silently crawl nothing.
/// The cargo home cargo itself would use
/// ([`crate::utils::cargo_home::cargo_home`]).
fn cargo_home() -> Option<PathBuf> {
match std::env::var("CARGO_HOME") {
Ok(v) if !v.trim().is_empty() => Some(PathBuf::from(v)),
_ => crate::utils::fs::home_dir().map(|home| home.join(".cargo")),
}
crate::utils::cargo_home::cargo_home()
}

/// Where cargo takes crates.io packages from for a build in `cwd`: the
Expand Down Expand Up @@ -1114,6 +1109,23 @@ version = "fake"
assert_eq!(packages[0].purl, "pkg:cargo/serde@1.0.200");
}

/// #1135: with `HOME`, `USERPROFILE` and `CARGO_HOME` unset cargo still
/// builds from the passwd home's `~/.cargo`, so the crawler must probe
/// that registry instead of reporting every crate lockfile-only.
#[cfg(unix)]
#[test]
#[serial_test::serial]
fn cargo_home_without_home_env_uses_passwd_home() {
let Some(passwd) = crate::utils::fs::passwd_home() else {
return;
};
let got = {
let _env = crate::utils::cargo_home::NoHomeEnv::set();
CargoCrawler::cargo_home()
};
assert_eq!(got, Some(passwd.join(".cargo")));
}

// ── Project-mode registry scope (#595) ──────────────────────────────

mod lock_scope {
Expand Down
39 changes: 1 addition & 38 deletions crates/socket-patch-core/src/crawlers/gradle_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -522,44 +522,7 @@ impl Env for ProcessEnv {
}
}

/// The account's home directory from the passwd database
/// (`getpwuid_r(getuid())->pw_dir`): what the JVM reports as `user.home`
/// on Linux and macOS, whatever `$HOME` says. `None` on Windows, and when
/// there is no entry or it names no directory.
#[cfg(unix)]
pub fn passwd_home() -> Option<PathBuf> {
use std::ffi::CStr;
use std::os::unix::ffi::OsStrExt;

let uid = unsafe { libc::getuid() };
let mut buf: Vec<libc::c_char> = vec![0; 4096];
// SAFETY: an all-zero `passwd` is a valid out-parameter; it is only
// read when `getpwuid_r` reports a result, and its strings point into
// `buf`, which outlives every read below.
let mut pwd: libc::passwd = unsafe { std::mem::zeroed() };
let mut result: *mut libc::passwd = std::ptr::null_mut();
loop {
// SAFETY: `buf` is writable for `buf.len()` bytes.
let rc =
unsafe { libc::getpwuid_r(uid, &mut pwd, buf.as_mut_ptr(), buf.len(), &mut result) };
if rc == libc::ERANGE && buf.len() < (1 << 20) {
buf.resize(buf.len() * 2, 0);
continue;
}
break;
}
if result.is_null() || pwd.pw_dir.is_null() {
return None;
}
// SAFETY: a non-null `pw_dir` is a NUL-terminated string inside `buf`.
let dir = unsafe { CStr::from_ptr(pwd.pw_dir) }.to_bytes();
(!dir.is_empty()).then(|| PathBuf::from(std::ffi::OsStr::from_bytes(dir)))
}

#[cfg(not(unix))]
pub fn passwd_home() -> Option<PathBuf> {
None
}
pub use crate::utils::fs::passwd_home;

/// The Gradle user home this process's Gradle would use
/// ([`GradleHome::resolve`] over the process environment and, on Unix,
Expand Down
2 changes: 1 addition & 1 deletion crates/socket-patch-core/src/update/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ impl ChannelEnv {
.map(PathBuf::from)
}
ChannelEnv {
cargo_home: path_var("CARGO_HOME"),
cargo_home: crate::utils::cargo_home::cargo_home(),
xdg_cache_home: path_var("XDG_CACHE_HOME"),
home: crate::utils::fs::home_dir(),
local_app_data: path_var("LOCALAPPDATA"),
Expand Down
13 changes: 5 additions & 8 deletions crates/socket-patch-core/src/utils/cargo_build_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,11 @@ pub struct Invalidation {

/// The project's build directories, reading the process environment.
pub fn build_dirs(cwd: &Path) -> BuildDirs {
build_dirs_with(cwd, &|k| std::env::var(k).ok(), cargo_home().as_deref())
}

fn cargo_home() -> Option<PathBuf> {
match std::env::var("CARGO_HOME") {
Ok(v) if !v.trim().is_empty() => Some(PathBuf::from(v)),
_ => crate::utils::fs::home_dir().map(|h| h.join(".cargo")),
}
build_dirs_with(
cwd,
&|k| std::env::var(k).ok(),
super::cargo_home::cargo_home().as_deref(),
)
}

/// [`build_dirs`] with an injected environment and `CARGO_HOME`.
Expand Down
191 changes: 191 additions & 0 deletions crates/socket-patch-core/src/utils/cargo_home.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
//! The cargo home cargo itself would use: `$CARGO_HOME`, else
//! `<home>/.cargo` with `<home>` resolved the way cargo's `home` crate
//! resolves it.
//!
//! [`super::fs::home_dir`] reads only `HOME` / `USERPROFILE`, but cargo
//! falls back to the account's passwd entry on Unix when `HOME` is unset
//! (`env -i`, systemd units, stripped CI and container entrypoints). A
//! crawler that stopped at `home_dir` probed no registry cache there and
//! reported the crate lockfile-only while cargo built the unpatched copy
//! from the passwd home's `~/.cargo` (#1135). The cargo-home readers (the
//! crawler, the `[patch]` config chain, the build-cache cleaner and the
//! self-update channel check) go through [`cargo_home`] so they agree with
//! cargo and with each other.

use std::ffi::OsString;
use std::path::PathBuf;

/// The variable cargo's `home` crate reads the user home from: `HOME` on
/// Unix, `USERPROFILE` on Windows (cargo ignores the other one).
const HOME_VAR: &str = if cfg!(windows) { "USERPROFILE" } else { "HOME" };

/// The process's cargo home: `$CARGO_HOME` when set and non-blank, else
/// `<home>/.cargo`, where `<home>` is `HOME` (`USERPROFILE` on Windows) or,
/// with that variable unset or empty on Unix, the passwd entry. `None` when
/// none of them names an absolute directory.
pub fn cargo_home() -> Option<PathBuf> {
resolve(
std::env::var_os("CARGO_HOME"),
std::env::var_os(HOME_VAR),
super::fs::passwd_home,
)
}

/// [`cargo_home`] over an injected `CARGO_HOME`, home variable and passwd
/// home (consulted only when the home variable is unset or empty).
fn resolve(
cargo_home_var: Option<OsString>,
home_var: Option<OsString>,
passwd_home: impl FnOnce() -> Option<PathBuf>,
) -> Option<PathBuf> {
// An empty value means unset (the env_non_empty convention):
// `PathBuf::from("")` would resolve `registry/src` against the CWD.
if let Some(v) = cargo_home_var.filter(|v| !v.to_string_lossy().trim().is_empty()) {
return Some(PathBuf::from(v));
}
// std's `home_dir` (which cargo's `home` crate calls on Unix) skips an
// empty `HOME` for the passwd entry. A relative one it uses as is, so
// that stays no home here (see `fs::home_dir`): probing it would read
// directories inside the project, and the passwd home is not where
// cargo looks then either.
let home = match home_var.filter(|v| !v.is_empty()) {
Some(v) => Some(PathBuf::from(v)),
Comment thread
mikolalysenko marked this conversation as resolved.
Resolved
None => passwd_home(),
};
home.filter(|h| super::fs::is_usable_home(h))
.map(|home| home.join(".cargo"))
}

/// Removes `HOME`, `USERPROFILE` and `CARGO_HOME` from the process
/// environment until dropped, restoring them even if the test panics.
/// Users are `#[serial_test::serial]`.
#[cfg(test)]
pub(crate) struct NoHomeEnv(Vec<(&'static str, Option<OsString>)>);

#[cfg(test)]
impl NoHomeEnv {
const KEYS: [&'static str; 3] = ["HOME", "USERPROFILE", "CARGO_HOME"];

pub(crate) fn set() -> Self {
let saved = Self::KEYS
.into_iter()
.map(|k| (k, std::env::var_os(k)))
.collect();
for k in Self::KEYS {
std::env::remove_var(k);
}
NoHomeEnv(saved)
}
}

#[cfg(test)]
impl Drop for NoHomeEnv {
fn drop(&mut self) {
for (k, v) in self.0.drain(..) {
match v {
Some(v) => std::env::set_var(k, v),
None => std::env::remove_var(k),
}
}
}
}

#[cfg(test)]
mod tests {
use super::*;

fn no_passwd() -> Option<PathBuf> {
None
}

fn passwd_unused() -> Option<PathBuf> {
panic!("passwd home consulted with the home variable set")
}

#[test]
fn cargo_home_var_wins() {
let got = resolve(
Some("/opt/cargo".into()),
Some("/home/u".into()),
passwd_unused,
);
assert_eq!(got, Some(PathBuf::from("/opt/cargo")));
}

#[test]
fn blank_cargo_home_var_is_unset() {
let got = resolve(Some(" ".into()), Some("/home/u".into()), passwd_unused);
assert_eq!(got, Some(PathBuf::from("/home/u/.cargo")));
}

#[test]
fn home_var_wins_over_passwd() {
let got = resolve(None, Some("/home/u".into()), passwd_unused);
assert_eq!(got, Some(PathBuf::from("/home/u/.cargo")));
}

/// #1135: with `HOME` and `CARGO_HOME` unset cargo builds from the
/// passwd home's `~/.cargo`, so the crawler must probe it too.
#[test]
fn unset_home_var_falls_back_to_passwd_home() {
let got = resolve(None, None, || Some("/root".into()));
assert_eq!(got, Some(PathBuf::from("/root/.cargo")));
}

/// std's `home_dir` treats `HOME=` like an unset `HOME`, so cargo
/// builds from the passwd home then too.
#[test]
fn empty_home_var_falls_back_to_passwd_home() {
let got = resolve(None, Some("".into()), || Some("/root".into()));
assert_eq!(got, Some(PathBuf::from("/root/.cargo")));
}

/// A relative home is no home, not a cue to patch the shared
/// passwd-home cache cargo isn't using.
#[test]
fn relative_home_var_is_no_home() {
assert_eq!(resolve(None, Some("rel/home".into()), passwd_unused), None);
}

#[test]
fn relative_or_missing_passwd_home_is_no_home() {
assert_eq!(resolve(None, None, || Some("rel".into())), None);
assert_eq!(resolve(None, None, || Some(PathBuf::new())), None);
assert_eq!(resolve(None, None, no_passwd), None);
}

/// The process wrapper reads `HOME` only on Unix: a `USERPROFILE`
/// left over from a Windows-forwarding shell is not cargo's home.
#[cfg(unix)]
#[test]
#[serial_test::serial]
fn process_cargo_home_ignores_userprofile_on_unix() {
let want = super::super::fs::passwd_home().map(|h| h.join(".cargo"));
let got = {
let _env = NoHomeEnv::set();
std::env::set_var("USERPROFILE", "/mnt/c/Users/x");
cargo_home()
};
assert_eq!(got, want);
}

/// #1135 through the process environment: `HOME`, `USERPROFILE` and
/// `CARGO_HOME` all unset, or `HOME` empty, still finds the passwd
/// home's `.cargo`.
#[cfg(unix)]
#[test]
#[serial_test::serial]
fn process_cargo_home_without_home_uses_passwd_entry() {
let Some(passwd) = super::super::fs::passwd_home() else {
return; // no passwd entry for this uid: nothing cargo could use either
};
let (unset, empty) = {
let _env = NoHomeEnv::set();
let unset = cargo_home();
std::env::set_var("HOME", "");
(unset, cargo_home())
};
assert_eq!(unset, Some(passwd.join(".cargo")));
assert_eq!(empty, Some(passwd.join(".cargo")));
}
}
47 changes: 47 additions & 0 deletions crates/socket-patch-core/src/utils/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,53 @@ pub(crate) fn home_from_env(var: impl Fn(&str) -> Option<std::ffi::OsString>) ->
.find(|home| is_usable_home(home))
}

/// The account's home directory from the passwd database
/// (`getpwuid_r(getuid())->pw_dir`): what the JVM reports as `user.home`
/// on Linux and macOS, and the home cargo falls back to with `HOME` unset,
/// whatever `$HOME` says. `None` on Windows, and when there is no entry or
/// it names no directory. Looked up once per process (an NSS lookup may
/// reach a directory server).
#[cfg(unix)]
pub fn passwd_home() -> Option<PathBuf> {
static HOME: std::sync::OnceLock<Option<PathBuf>> = std::sync::OnceLock::new();
HOME.get_or_init(lookup_passwd_home).clone()
}

#[cfg(unix)]
fn lookup_passwd_home() -> Option<PathBuf> {
use std::ffi::CStr;
use std::os::unix::ffi::OsStrExt;

let uid = unsafe { libc::getuid() };
let mut buf: Vec<libc::c_char> = vec![0; 4096];
// SAFETY: an all-zero `passwd` is a valid out-parameter; it is only
// read when `getpwuid_r` reports a result, and its strings point into
// `buf`, which outlives every read below.
let mut pwd: libc::passwd = unsafe { std::mem::zeroed() };
let mut result: *mut libc::passwd = std::ptr::null_mut();
loop {
// SAFETY: `buf` is writable for `buf.len()` bytes.
let rc =
unsafe { libc::getpwuid_r(uid, &mut pwd, buf.as_mut_ptr(), buf.len(), &mut result) };
if rc == libc::ERANGE && buf.len() < (1 << 20) {
buf.resize(buf.len() * 2, 0);
continue;
}
break;
}
if result.is_null() || pwd.pw_dir.is_null() {
return None;
}
// SAFETY: a non-null `pw_dir` is a NUL-terminated string inside `buf`.
let dir = unsafe { CStr::from_ptr(pwd.pw_dir) }.to_bytes();
(!dir.is_empty()).then(|| PathBuf::from(std::ffi::OsStr::from_bytes(dir)))
}

#[cfg(not(unix))]
pub fn passwd_home() -> Option<PathBuf> {
None
}

/// Whether `home` may anchor per-user probes: not empty and not
/// CWD-relative.
pub(crate) fn is_usable_home(home: &Path) -> bool {
Expand Down
1 change: 1 addition & 0 deletions crates/socket-patch-core/src/utils/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod cargo_build_cache;
pub mod cargo_home;
pub mod cargo_workspace;
pub mod composer_version;
pub mod concurrent;
Expand Down
9 changes: 3 additions & 6 deletions crates/socket-patch-core/src/vendor/cargo_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,13 +150,10 @@ pub struct ChainConfig {
/// every ancestor directory's, and `$CARGO_HOME`'s (default `~/.cargo`), in
/// that order (cargo lets a config item replace the manifest item with the
/// same key, whatever its version). Read-only and fail-soft: missing /
/// unreadable / malformed files contribute nothing, and with no
/// `$CARGO_HOME` and no home directory there is no home config.
/// unreadable / malformed files contribute nothing, and with no cargo home
/// ([`crate::utils::cargo_home::cargo_home`]) there is no home config.
pub async fn read_config_chain(project_root: &Path) -> Vec<ChainConfig> {
let cargo_home = match std::env::var("CARGO_HOME") {
Ok(v) if !v.trim().is_empty() => Some(PathBuf::from(v)),
_ => crate::utils::fs::home_dir().map(|home| home.join(".cargo")),
};
let cargo_home = crate::utils::cargo_home::cargo_home();
read_config_chain_with(project_root, cargo_home.as_deref()).await
}

Expand Down
5 changes: 4 additions & 1 deletion docs/ecosystems.md
Original file line number Diff line number Diff line change
Expand Up @@ -1247,7 +1247,10 @@ the repository list, so it is left as a
Agent mode patches the crate in place wherever the crawler finds it. For a non-vendored
crate that means the **shared** `$CARGO_HOME/registry` cache: the patch affects every
project on the machine, and is silently reset by `cargo clean` or a cache prune. Use
`--mode vendored` for a project-local, committable patch.
`--mode vendored` for a project-local, committable patch. With `CARGO_HOME` unset the
cache is `<home>/.cargo/registry`, with `<home>` found the way cargo finds it: `HOME`
(`USERPROFILE` on Windows), then, on Unix, the account's passwd entry, so a run with
`HOME` unset or empty still patches the copy cargo builds.

The cache holds one extracted copy of a crate per registry index directory under
`registry/src` (cargo 1.70–1.84 and cargo 1.85+ use different ones), so two toolchains
Expand Down
Loading