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
8 changes: 7 additions & 1 deletion crates/socket-patch-cli/src/commands/apply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2699,7 +2699,13 @@ async fn apply_maven_base(m: &MavenBase<'_>) -> MavenApplied {
let m2_copies: Vec<String> = copies
.consumed
.iter()
.filter(|c| c.starts_with(&m.scope.env.m2_repo))
.filter(|c| {
m.scope
.env
.m2_repo
.as_ref()
.is_some_and(|m2| c.starts_with(m2))
})
.map(|p| p.display().to_string())
.collect();
if matches!(
Expand Down
87 changes: 84 additions & 3 deletions crates/socket-patch-cli/src/commands/get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1517,10 +1517,16 @@ async fn filter_to_installed_purls(
// mark the installed version — but the pnpm-lock.yaml the hosted
// rewriter will edit is right there. Read its raw text once and gate the
// keep-branch below on version membership, so a large advisory fan-out
// doesn't request grants for every version ever patched (raw
// `read_to_string` matches the hosted flow's own candidate-file reads).
// doesn't request grants for every version ever patched. Read FIFO-safe,
// like the hosted flow's own candidate-file reads: a FIFO planted at
// `pnpm-lock.yaml` must not wedge `get` in open(2).
let pnpm_pnp_lock_text: Option<String> = (pnp_pnpm && mode == super::scan::ScanMode::Hosted)
.then(|| std::fs::read_to_string(common.cwd.join("pnpm-lock.yaml")).ok())
.then(|| {
socket_patch_core::utils::fs::read_regular_to_string_sync(
&common.cwd.join("pnpm-lock.yaml"),
)
.ok()
})
.flatten();
let pnpm_pnp_lock = pnpm_pnp_lock_text.as_deref().map(PnpmLock::parse);

Expand Down Expand Up @@ -6393,6 +6399,81 @@ mod tests {
assert_eq!(out.skip_records[0]["errorCode"], "package_not_installed");
}

/// B74: a FIFO planted at `pnpm-lock.yaml` of a pnpm-PnP project must
/// not wedge hosted `get` in open(2). A FIFO present from the start is
/// already refused by the lock inventory (so this guards the whole
/// hosted filter path, and passes on the old code too); the raw read
/// behind the pnpm-PnP keep-gate is now FIFO-safe as well, which closes
/// the window where a FIFO is swapped in after the inventory read. A
/// watchdog thread
/// opens the FIFO's write end (non-blocking) after a grace period, which
/// releases a reader stuck in open(2): the test then FAILS instead of
/// hanging the suite.
#[cfg(unix)]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn filter_to_installed_purls_pnpm_pnp_hosted_fifo_lock_does_not_wedge() {
use std::os::unix::fs::OpenOptionsExt;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;

let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join(".pnp.cjs"), b"// pnp loader\n").unwrap();
let lock = tmp.path().join("pnpm-lock.yaml");
let c = std::ffi::CString::new(lock.to_str().unwrap()).unwrap();
assert_eq!(unsafe { libc::mkfifo(c.as_ptr(), 0o600) }, 0);
std::fs::create_dir_all(tmp.path().join("node_modules")).unwrap();
std::fs::write(tmp.path().join("node_modules/.modules.yaml"), b"").unwrap();

let done = Arc::new(AtomicBool::new(false));
let rescued = Arc::new(AtomicBool::new(false));
let watchdog = {
let (done, rescued, lock) = (done.clone(), rescued.clone(), lock.clone());
std::thread::spawn(move || {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while !done.load(Ordering::SeqCst) && std::time::Instant::now() < deadline {
std::thread::sleep(std::time::Duration::from_millis(50));
}
// Keep releasing until the body returns: each open lets one
// blocked reader through to EOF.
while !done.load(Ordering::SeqCst) {
if std::fs::OpenOptions::new()
.write(true)
.custom_flags(libc::O_NONBLOCK)
.open(&lock)
.is_ok()
{
rescued.store(true, Ordering::SeqCst);
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
})
};

let common = crate::args::GlobalArgs {
cwd: tmp.path().to_path_buf(),
..Default::default()
};
let accessible = vec![mk_patch(
"88888888-8888-4888-8888-888888888888",
"pkg:npm/covgap-judged@1.0.0",
"free",
"2024-01-01",
)];
let out = filter_to_installed_purls(
&accessible,
&common,
crate::commands::scan::ScanMode::Hosted,
)
.await;
done.store(true, Ordering::SeqCst);
watchdog.join().unwrap();
assert!(
!rescued.load(Ordering::SeqCst),
"a FIFO pnpm-lock.yaml wedged get in open(2)"
);
assert!(out.kept.is_empty(), "{:?}", out.kept);
}

/// pnpm-PnP + hosted: a purl the lock probe CANNOT judge (no `@version`
/// coordinate to look for) must keep the layout-refusal code — the same
/// no-judgment fallback as an unreadable lock — never a false
Expand Down
13 changes: 7 additions & 6 deletions crates/socket-patch-cli/src/commands/scan/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1267,19 +1267,20 @@ async fn gradle_scan(
.collect();
candidates.sort();
candidates.dedup();
let only_m2: Vec<String> = if candidates.is_empty() {
Vec::new()
} else {
let m2_repo = env.m2_repo.as_ref().filter(|_| !candidates.is_empty());
let only_m2: Vec<String> = if let Some(m2_repo) = m2_repo {
let mut found: Vec<String> = socket_patch_core::crawlers::MavenCrawler
.find_by_purls(&env.m2_repo, &candidates)
.find_by_purls(m2_repo, &candidates)
.await
.unwrap_or_default()
.into_keys()
.collect();
found.sort();
found
} else {
Vec::new()
};
if !only_m2.is_empty() {
if let Some(m2_repo) = m2_repo.filter(|_| !only_m2.is_empty()) {
const SHOWN: usize = 5;
let mut list = only_m2[..only_m2.len().min(SHOWN)].join(", ");
if only_m2.len() > SHOWN {
Expand All @@ -1291,7 +1292,7 @@ async fn gradle_scan(
"this Gradle build declares no mavenLocal(), so it does not resolve \
from the Maven local repository ({}); {} found only there {} not \
scanned: {list}",
env.m2_repo.display(),
m2_repo.display(),
plural(only_m2.len(), "module", "modules"),
if only_m2.len() == 1 { "is" } else { "are" },
),
Expand Down
4 changes: 3 additions & 1 deletion crates/socket-patch-cli/src/commands/vex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ pub struct VexArgs {
///
/// Auto-detection tries, in order:
/// 1. the git `origin` remote: pkg:github/<owner>/<repo> for github.com
/// (likewise gitlab.com and bitbucket.org), the raw URL otherwise
/// (likewise gitlab.com and bitbucket.org), the raw URL otherwise.
/// The nearest checkout counts (a submodule or worktree names
/// itself); a repository at the home directory only when run there
/// 2. package.json: pkg:npm/<name>@<version>
/// 3. pyproject.toml: pkg:pypi/<name>@<version>
/// 4. Cargo.toml: pkg:cargo/<name>@<version>
Expand Down
8 changes: 7 additions & 1 deletion crates/socket-patch-cli/src/ecosystem_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -747,7 +747,13 @@ impl JvmScope {
for path in paths {
if self.is_read_only(path) {
out.read_only.push(path.clone());
} else if path.starts_with(&self.env.m2_repo) && !self.m2_consumed() {
} else if self
.env
.m2_repo
.as_ref()
.is_some_and(|m2| path.starts_with(m2))
&& !self.m2_consumed()
{
out.m2_ignored.push(path.clone());
} else {
out.consumed.push(path.clone());
Expand Down
18 changes: 10 additions & 8 deletions crates/socket-patch-core/src/crawlers/cargo_crawler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,9 @@ impl CargoCrawler {
/// Each subdirectory corresponds to a registry index
/// (e.g. `index.crates.io-6f17d22bba15001f/`).
async fn get_registry_src_paths() -> Vec<PathBuf> {
let cargo_home = Self::cargo_home();
let Some(cargo_home) = Self::cargo_home() else {
return Vec::new();
};
let registry_src = cargo_home.join("registry").join("src");

let mut paths = Vec::new();
Expand Down Expand Up @@ -346,14 +348,14 @@ impl CargoCrawler {
Some((name.to_string(), version.to_string()))
}

/// Get `CARGO_HOME`, defaulting to `$HOME/.cargo`. An empty value means
/// unset (the env_non_empty convention) — `PathBuf::from("")` would
/// otherwise resolve `registry/src` against the CWD and silently crawl
/// nothing.
fn cargo_home() -> PathBuf {
/// 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.
fn cargo_home() -> Option<PathBuf> {
match std::env::var("CARGO_HOME") {
Ok(v) if !v.trim().is_empty() => PathBuf::from(v),
_ => crate::utils::fs::home_dir().join(".cargo"),
Ok(v) if !v.trim().is_empty() => Some(PathBuf::from(v)),
_ => crate::utils::fs::home_dir().map(|home| home.join(".cargo")),
}
}
}
Expand Down
38 changes: 4 additions & 34 deletions crates/socket-patch-core/src/crawlers/composer_crawler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ use std::path::{Path, PathBuf};
use super::types::{CrawledPackage, CrawlerOptions};
use crate::patch::path_safety;
use crate::utils::composer_version::composer_versions_equivalent;
use crate::utils::fs::{is_dir, is_dir_sync, is_file, normalize_lexically, run_blocking};
use crate::utils::fs::{is_dir, is_dir_sync, is_file, run_blocking};
use crate::utils::process::{CommandRunner, GlobalProbeRunner};
use crate::utils::relpath::normalize_lexically;

#[cfg(test)]
mod oracle;
Expand Down Expand Up @@ -530,17 +531,7 @@ fn normalize_config_vendor_dir(raw: &str) -> Option<String> {
if raw.starts_with(['/', '\\']) {
return None;
}
let mut segments: Vec<&str> = Vec::new();
for segment in raw.split(['/', '\\']) {
match segment {
"" | "." => {}
".." => {
segments.pop()?;
}
other => segments.push(other),
}
}
(!segments.is_empty()).then(|| segments.join("/"))
crate::utils::relpath::resolve_rel("", raw, 0).filter(|segments| !segments.is_empty())
}

/// Read `config.vendor-dir` from a composer.json on disk. Read with
Expand Down Expand Up @@ -589,8 +580,7 @@ async fn resolve_project_root(vendor_path: &Path) -> PathBuf {
}

// `normalize_lexically` (resolve `.`/`..` without touching the filesystem)
// lives in `crate::utils::fs` — shared with the ruby crawler's
// config-sourced `BUNDLE_PATH` containment guard.
// lives in `crate::utils::relpath` with every other lexical normalizer.

/// Resolve an installed.json `install-path` against the vendor tree.
///
Expand Down Expand Up @@ -1597,26 +1587,6 @@ mod tests {
));
}

#[test]
fn test_normalize_lexically() {
let n = |p: &str| normalize_lexically(Path::new(p));
// `.` drops out, `..` pops the previous segment.
assert_eq!(
n("/a/b/composer/../monolog/monolog").unwrap(),
PathBuf::from("/a/b/monolog/monolog")
);
assert_eq!(
n("/a/b/composer/./installers").unwrap(),
PathBuf::from("/a/b/composer/installers")
);
assert_eq!(n("/a/b/c/../../../web/x").unwrap(), PathBuf::from("/web/x"));
// Popping above the path's own root fails closed.
assert_eq!(n("/a/../.."), None);
assert_eq!(n("../x"), None);
// Relative paths stay relative.
assert_eq!(n("a/b/../c").unwrap(), PathBuf::from("a/c"));
}

#[tokio::test]
async fn test_resolve_install_path_rejects_absolute_escape_from_relative_root() {
// The CLI defaults to `--cwd .`, so local discovery hands the
Expand Down
9 changes: 1 addition & 8 deletions crates/socket-patch-core/src/crawlers/coursier_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ pub fn is_coursier_cache_dir(p: &Path) -> bool {
/// `.sbtopts`) is named in the `SOCKET_DEBUG` log.
pub fn process_cache_dirs(cwd: &Path) -> Vec<PathBuf> {
let env = |name: &str| std::env::var(name).ok().filter(|v| !v.is_empty());
let home = process_home();
let home = crate::utils::fs::home_dir();
coursier_cache_dirs(TargetOs::host(), &env, home.as_deref(), cwd)
.into_iter()
.map(|(dir, source)| {
Expand All @@ -237,13 +237,6 @@ pub fn process_cache_dirs(cwd: &Path) -> Vec<PathBuf> {
.collect()
}

/// This process's home directory, only when absolute: the shared
/// `home_dir()` fallback (`~`) would resolve every default location
/// against the process's working directory.
pub(crate) fn process_home() -> Option<PathBuf> {
Some(crate::utils::fs::home_dir()).filter(|h| h.is_absolute())
}

/// Debug-log a cache location and the source that named it; a location a
/// repository file chose is called out as such.
pub(crate) fn log_source(what: &str, dir: &Path, source: &str) {
Expand Down
Loading
Loading