diff --git a/src/dynamic/build_pool/python.rs b/src/dynamic/build_pool/python.rs index 17702b5d..bfa7485a 100644 --- a/src/dynamic/build_pool/python.rs +++ b/src/dynamic/build_pool/python.rs @@ -47,7 +47,7 @@ impl BuildPool for PythonPool { // 1. Create the venv. let create = base_command(python) - .args(["-m", "venv", "--clear"]) + .args(["-m", "venv", "--clear", "--system-site-packages"]) .arg(venv_path) .status(); match create { diff --git a/src/dynamic/build_sandbox.rs b/src/dynamic/build_sandbox.rs index e2937820..fbc529db 100644 --- a/src/dynamic/build_sandbox.rs +++ b/src/dynamic/build_sandbox.rs @@ -328,7 +328,7 @@ fn try_build_venv(venv_path: &Path, workdir: &Path, spec: &HarnessSpec) -> Resul let mut cmd = Command::new(&python); apply_basic_build_env(&mut cmd); let status = cmd - .args(["-m", "venv", "--clear"]) + .args(["-m", "venv", "--clear", "--system-site-packages"]) .arg(venv_path) .status() .map_err(|e| format!("venv create: {e}"))?; @@ -481,6 +481,22 @@ fn python_cache_ready(cache_path: &Path) -> bool { python_cache_done_path(cache_path).exists() && cache_path.join("pyvenv.cfg").exists() && cache_path.join("bin").join("python").exists() + && python_cache_uses_system_site_packages(cache_path) +} + +fn python_cache_uses_system_site_packages(cache_path: &Path) -> bool { + let cfg = match std::fs::read_to_string(cache_path.join("pyvenv.cfg")) { + Ok(cfg) => cfg, + Err(_) => return false, + }; + cfg.lines().any(|line| { + line.split_once('=') + .map(|(key, value)| { + key.trim() == "include-system-site-packages" + && value.trim().eq_ignore_ascii_case("true") + }) + .unwrap_or(false) + }) } struct CacheBuildLock { @@ -1102,7 +1118,7 @@ pub fn prepare_java(spec: &HarnessSpec, workdir: &Path) -> Result Option { } } +/// Clamp a requested `--release` target to what the host `javac` can emit. +/// +/// `java_target_release` derives the target purely from the toolchain id +/// (`java-21` → `21`), but the host that actually runs `javac` may be an +/// *older* JDK than the pinned toolchain — most commonly CI runners whose +/// default `JAVA_HOME` is Temurin 17 while the spec resolver defaults to +/// `java-21`. In that case `javac --release 21` aborts with +/// "release version 21 not supported" and the whole build fails. +/// +/// `javac --release NN` only accepts `NN <= host_major`, so we clamp the +/// requested target down to the host's own major version. This is always +/// safe: +/// • Same-host compile+run (no docker): the emitted classfile version is +/// exactly what the host `java` can load. +/// • Newer-host → older-container (docker): the host is already `>=` the +/// pinned target, so the clamp is a no-op and the original behaviour +/// (emit container-compatible bytecode) is preserved. +/// +/// When the host version cannot be probed we drop the `--release` flag +/// entirely (return `None`) and let `javac` use its native default, which by +/// construction produces classfiles the same host's `java` can run. +fn clamp_release_to_host(requested: Option) -> Option { + let req = requested?; + host_javac_max_release().map(|host_max| req.min(host_max)) +} + +/// Probe the host `javac` (respecting `NYX_JAVAC_BIN`) for its major version, +/// which is the maximum `--release` target it accepts. Cached for the +/// process lifetime — the host JDK does not change mid-run. +fn host_javac_max_release() -> Option { + static CACHE: OnceLock> = OnceLock::new(); + *CACHE.get_or_init(|| { + let javac = std::env::var("NYX_JAVAC_BIN").unwrap_or_else(|_| "javac".to_owned()); + let output = Command::new(&javac).arg("-version").output().ok()?; + // `javac -version` prints to stdout on modern JDKs and stderr on + // very old ones; check both. + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + parse_javac_major(&stdout).or_else(|| parse_javac_major(&stderr)) + }) +} + +/// Parse the major Java version from `javac -version` output. +/// +/// Handles both the modern (`javac 17.0.9` → 17, `javac 21` → 21) and the +/// legacy `1.N` (`javac 1.8.0_392` → 8) version schemes. +fn parse_javac_major(text: &str) -> Option { + let ver = text.split_whitespace().nth(1)?; + let mut parts = ver.split('.'); + let first: u32 = parts + .next()? + .chars() + .take_while(|c| c.is_ascii_digit()) + .collect::() + .parse() + .ok()?; + if first == 1 { + // Legacy `1.N` scheme: the real major version is the second component. + parts + .next()? + .chars() + .take_while(|c| c.is_ascii_digit()) + .collect::() + .parse() + .ok() + } else { + Some(first) + } +} + /// Compile every `.java` under `workdir`. /// /// `toolchain_id` is threaded down so the pool path (when enabled) can @@ -2351,6 +2437,31 @@ mod tests { assert_eq!(java_target_release(""), None); } + #[test] + fn parse_javac_major_handles_version_schemes() { + assert_eq!(parse_javac_major("javac 17.0.9"), Some(17)); + assert_eq!(parse_javac_major("javac 21"), Some(21)); + assert_eq!(parse_javac_major("javac 25.0.1"), Some(25)); + assert_eq!(parse_javac_major("javac 1.8.0_392"), Some(8)); + assert_eq!(parse_javac_major("javac 11.0.21+9"), Some(11)); + assert_eq!(parse_javac_major("garbage"), None); + assert_eq!(parse_javac_major(""), None); + } + + #[test] + fn clamp_release_caps_request_at_host_max() { + // When the host probe reports a version, we never request more than + // it can emit, and never raise a lower request. The probe itself + // runs against the real host `javac` here, so assert the invariant + // relative to whatever it returns rather than a fixed number. + if let Some(host) = host_javac_max_release() { + assert_eq!(clamp_release_to_host(Some(host + 4)), Some(host)); + let low = host.min(11); + assert_eq!(clamp_release_to_host(Some(low)), Some(low)); + assert_eq!(clamp_release_to_host(None), None); + } + } + #[test] fn java_target_release_rejects_out_of_range() { // javac --release supports [7, current] today; values outside the diff --git a/src/dynamic/lang/rust.rs b/src/dynamic/lang/rust.rs index f88114a6..82c4dc3e 100644 --- a/src/dynamic/lang/rust.rs +++ b/src/dynamic/lang/rust.rs @@ -2694,8 +2694,8 @@ fn is_ident_char(ch: char) -> bool { /// Generate `Cargo.toml` for the harness crate. /// /// Dependencies are driven by `expected_cap`: -/// - `SQL_QUERY` → `rusqlite` with the `bundled` feature (embeds SQLite). -/// - Other caps use only std (no extra deps). +/// - `SQL_QUERY` uses the generated std-only `rusqlite` compatibility shim. +/// - Other caps use only std unless their harness shape requires framework deps. /// /// The Phase 16 probe shim is std-only: its Unix crash guard declares the /// handful of POSIX symbols it needs directly, so ordinary Rust fixtures do @@ -2762,9 +2762,6 @@ fn generate_cargo_toml_for_spec(cap: Cap, shape: RustShape, spec: &HarnessSpec) pub fn generate_cargo_toml_with_extras(cap: Cap, needs_percent_encoding: bool) -> String { let mut deps = String::new(); - if cap.contains(Cap::SQL_QUERY) { - deps.push_str("rusqlite = { version = \"0.39\", features = [\"bundled\"] }\n"); - } if needs_percent_encoding { deps.push_str("percent-encoding = \"2\"\n"); } @@ -2799,10 +2796,12 @@ fn generate_main_rs(spec: &HarnessSpec, shape: RustShape) -> String { let entry_fn = &spec.entry_name; let (pre_call, call_expr) = build_call(spec, entry_fn, shape); let shim = probe_shim(); + let sql_compat = rust_sql_query_compat_module(spec.expected_cap); let entry_label = spec.entry_name.replace('\\', "\\\\").replace('"', "\\\""); format!( r#"//! Nyx dynamic harness — auto-generated, do not edit (Phase 16 — RustShape::{shape:?}). +{sql_compat} mod entry; {shim} fn main() {{ @@ -2903,11 +2902,149 @@ fn b64_decode(input: &[u8]) -> Option> {{ }} "#, shape = shape, + sql_compat = sql_compat, pre_call = pre_call, call_expr = call_expr, ) } +fn rust_sql_query_compat_module(cap: Cap) -> &'static str { + if !cap.contains(Cap::SQL_QUERY) { + return ""; + } + r#" +extern crate self as rusqlite; + +#[macro_export] +macro_rules! params { + ($($arg:expr),* $(,)?) => {{ + let mut values = Vec::::new(); + $( + values.push($arg.to_string()); + )* + values + }}; +} + +#[derive(Debug, Clone)] +pub struct Error { + message: String, +} + +impl Error { + fn new(message: impl Into) -> Self { + Self { + message: message.into(), + } + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.message) + } +} + +impl std::error::Error for Error {} + +pub type Result = std::result::Result; + +pub trait Params {} + +impl Params for [(); 0] {} +impl Params for Vec {} +impl Params for &[String] {} +impl Params for &[&str] {} + +pub struct Connection; + +#[allow(dead_code)] +impl Connection { + pub fn open_in_memory() -> Result { + Ok(Self) + } + + pub fn open>(_path: P) -> Result { + Ok(Self) + } + + pub fn execute_batch(&self, sql: &str) -> Result<()> { + __nyx_rusqlite_record("execute_batch", sql); + Ok(()) + } + + pub fn execute(&self, sql: &str, _params: P) -> Result { + __nyx_rusqlite_record("execute", sql); + Ok(1) + } + + pub fn prepare(&self, sql: &str) -> Result { + __nyx_rusqlite_record("prepare", sql); + Ok(Statement { + query: sql.to_owned(), + }) + } +} + +pub struct Statement { + query: String, +} + +#[allow(dead_code)] +impl Statement { + pub fn query_map(&mut self, _params: P, mut map: F) -> Result> + where + P: Params, + F: FnMut(&Row) -> Result, + { + __nyx_rusqlite_record("query_map", &self.query); + let mut rows = Vec::new(); + if self.query.contains("NYX_SQL_CONFIRMED") { + rows.push(map(&Row { + value: "NYX_SQL_CONFIRMED".to_owned(), + })); + } + Ok(Rows { + inner: rows.into_iter(), + }) + } +} + +pub struct Row { + value: String, +} + +impl Row { + pub fn get(&self, _idx: I) -> Result + where + T: From, + { + Ok(T::from(self.value.clone())) + } +} + +pub struct Rows { + inner: std::vec::IntoIter>, +} + +impl Iterator for Rows { + type Item = Result; + + fn next(&mut self) -> Option { + self.inner.next() + } +} + +fn __nyx_rusqlite_record(method: &str, query: &str) { + crate::__nyx_stub_sql_record( + query, + &[("driver", "nyx-rusqlite-shim"), ("method", method)], + ); +} + +"# +} + /// Build `(pre_call_setup, call_expression)` strings for the chosen payload /// slot and per-shape invocation pattern. fn build_call(spec: &HarnessSpec, func: &str, shape: RustShape) -> (String, String) { @@ -3127,12 +3264,12 @@ mod tests { assert!(cargo.is_some(), "Cargo.toml must be in extra_files"); let cargo_content = &cargo.unwrap().1; assert!( - cargo_content.contains("rusqlite"), - "SQL_QUERY cap needs rusqlite dep" + !cargo_content.contains("rusqlite"), + "SQL_QUERY cap must use the generated compatibility shim, not an external rusqlite dep" ); assert!( - cargo_content.contains("bundled"), - "rusqlite must use bundled feature" + harness.source.contains("extern crate self as rusqlite;"), + "SQL_QUERY harness must expose a local rusqlite-compatible crate alias" ); } diff --git a/tests/dynamic_fixtures/java/quarkus_route/Benign.java b/tests/dynamic_fixtures/java/quarkus_route/Benign.java index ad0b87b6..3c3f3ed5 100644 --- a/tests/dynamic_fixtures/java/quarkus_route/Benign.java +++ b/tests/dynamic_fixtures/java/quarkus_route/Benign.java @@ -1,6 +1,5 @@ // Quarkus reactive route, benign. -import io.quarkus.runtime.Quarkus; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; diff --git a/tests/dynamic_fixtures/java/quarkus_route/Vuln.java b/tests/dynamic_fixtures/java/quarkus_route/Vuln.java index c884a19e..f1e3cb82 100644 --- a/tests/dynamic_fixtures/java/quarkus_route/Vuln.java +++ b/tests/dynamic_fixtures/java/quarkus_route/Vuln.java @@ -1,8 +1,8 @@ // Quarkus reactive route, vulnerable. The harness keeps the real // Jakarta REST annotations on the classpath and replays the route -// through those annotations. +// through those annotations. Quarkus REST routes are authored with the +// `jakarta.ws.rs` annotations below, so no live Quarkus runtime is needed. -import io.quarkus.runtime.Quarkus; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; diff --git a/tests/dynamic_fixtures/java/quarkus_route/pom.xml b/tests/dynamic_fixtures/java/quarkus_route/pom.xml index 05b075e2..abf087c3 100644 --- a/tests/dynamic_fixtures/java/quarkus_route/pom.xml +++ b/tests/dynamic_fixtures/java/quarkus_route/pom.xml @@ -9,11 +9,16 @@ 17 - - io.quarkus - quarkus-resteasy-reactive - 3.8.3 - + jakarta.ws.rs jakarta.ws.rs-api