diff --git a/crates/mirth-lab/src/rustc.rs b/crates/mirth-lab/src/rustc.rs index 8952f3c..b215564 100644 --- a/crates/mirth-lab/src/rustc.rs +++ b/crates/mirth-lab/src/rustc.rs @@ -7,7 +7,6 @@ use std::process::{Command, Stdio}; use std::time::Duration; use serde::{Deserialize, Serialize}; -use wait_timeout::ChildExt; /// How a command ended. #[derive(Clone, Debug, PartialEq, Eq, Serialize)] @@ -24,6 +23,9 @@ pub struct Finished { pub exit: Exit, pub stdout: Vec, pub stderr: Vec, + /// User plus system CPU seconds of the command and every descendant it waited for (none on + /// a timeout). Unlike wall time, it does not grow with the machine's load. + pub cpu: Option, } impl Finished { @@ -66,24 +68,42 @@ pub fn run_command(mut cmd: Command, timeout: Duration) -> std::io::Result match status.code() { - Some(code) => Exit::Code(code), - None => { - use std::os::unix::process::ExitStatusExt; - Exit::Signal(status.signal().unwrap_or(0)) - } - }, - None => { + // Reap with wait4 rather than waitpid, for the child's resource usage. + let pid = child.id() as libc::pid_t; + let deadline = std::time::Instant::now() + timeout; + let mut status: libc::c_int = 0; + // SAFETY: rusage is plain data. + let mut usage: libc::rusage = unsafe { std::mem::zeroed() }; + let mut nap = Duration::from_millis(1); + let (exit, cpu) = loop { + // SAFETY: waiting on our own child; status and usage outlive the call. + let r = unsafe { libc::wait4(pid, &mut status, libc::WNOHANG, &mut usage) }; + if r == pid { + let secs = |t: libc::timeval| t.tv_sec as f64 + t.tv_usec as f64 / 1e6; + let cpu = secs(usage.ru_utime) + secs(usage.ru_stime); + break if libc::WIFEXITED(status) { + (Exit::Code(libc::WEXITSTATUS(status)), Some(cpu)) + } else { + (Exit::Signal(libc::WTERMSIG(status)), Some(cpu)) + }; + } + if r < 0 { + return Err(std::io::Error::last_os_error()); + } + if std::time::Instant::now() > deadline { let _ = child.kill(); - let _ = child.wait(); - Exit::Timeout + // SAFETY: reap the killed child. + unsafe { libc::wait4(pid, &mut status, 0, &mut usage) }; + break (Exit::Timeout, None); } + std::thread::sleep(nap); + nap = (nap * 2).min(Duration::from_millis(50)); }; Ok(Finished { exit, stdout: out_thread.join().unwrap_or_default(), stderr: err_thread.join().unwrap_or_default(), + cpu, }) } diff --git a/crates/mirth-lab/src/tools/release_diff.rs b/crates/mirth-lab/src/tools/release_diff.rs index b56ae9e..44981e5 100644 --- a/crates/mirth-lab/src/tools/release_diff.rs +++ b/crates/mirth-lab/src/tools/release_diff.rs @@ -4,8 +4,8 @@ //! `cargo check --locked --offline --workspace` of each repository in a corpus of real crates //! with an older and a newer toolchain (dependencies fetched first), each in its own target //! directory, removed afterwards. Findings: a regression (old passes, new fails), an ICE, or the -//! new toolchain taking more than --slower times as long. A regression whose failing crate -//! enables unstable features (`#![feature]`, often only when it detects a nightly) is noted, not +//! new toolchain taking more than --slower times as much CPU time (wall time follows the load). +//! A regression whose failing crate enables unstable features (`#![feature]`, often only when it detects a nightly) is noted, not //! reported. use std::path::{Path, PathBuf}; @@ -47,6 +47,9 @@ static FEATURE: LazyLock = LazyLock::new(|| Regex::new(r"#!\[(cfg_attr\([ struct Check { exit: Exit, seconds: f64, + /// CPU seconds of cargo and the compilers it ran; what "slower" compares, since wall time + /// grows with the machine's load. + cpu: f64, ice: bool, codes: Vec, first: String, @@ -83,13 +86,14 @@ fn check(args: &Args, repo: &Path, toolchain: &str) -> Check { let done = run_command(cmd, Duration::from_secs(args.timeout)); let seconds = (start.elapsed().as_secs_f64() * 10.0).round() / 10.0; let _ = std::fs::remove_dir_all(&target); - let (exit, err) = match done { - Ok(d) => (d.exit.clone(), d.stderr_text()), - Err(e) => (Exit::Code(-1), e.to_string()), + let (exit, err, cpu) = match done { + Ok(d) => (d.exit.clone(), d.stderr_text(), d.cpu.unwrap_or(0.0)), + Err(e) => (Exit::Code(-1), e.to_string(), 0.0), }; + let cpu = (cpu * 10.0).round() / 10.0; let first = err.lines().find(|l| ERROR_LINE.is_match(l)).unwrap_or("").chars().take(300).collect(); let tail = if exit == Exit::Code(0) { String::new() } else { err.chars().rev().take(3000).collect::().chars().rev().collect() }; - Check { exit, seconds, ice: is_ice(&err), codes: error_codes(&err), first, tail } + Check { exit, seconds, cpu, ice: is_ice(&err), codes: error_codes(&err), first, tail } } /// The crate a first error points into, if its source enables `#![feature(...)]`. @@ -123,8 +127,8 @@ fn one(args: &Args, repo: &Path) -> Rec { } } else if !ok(&old) && ok(&new) { notes.push("fixed".into()); - } else if ok(&old) && ok(&new) && old.seconds > 5.0 && new.seconds > args.slower * old.seconds { - found.push(format!("slower: {}s -> {}s", old.seconds, new.seconds)); + } else if ok(&old) && ok(&new) && old.cpu > 5.0 && new.cpu > args.slower * old.cpu { + found.push(format!("slower: {}s -> {}s of CPU", old.cpu, new.cpu)); } if new.ice && !found.iter().any(|f| f == "ice") { found.push("ice".into()); diff --git a/crates/mirth-lab/src/tools/xlink.rs b/crates/mirth-lab/src/tools/xlink.rs index 79e4529..861601f 100644 --- a/crates/mirth-lab/src/tools/xlink.rs +++ b/crates/mirth-lab/src/tools/xlink.rs @@ -38,7 +38,11 @@ pub struct Args { /// Targets that need more than a target name (a CPU, a linker that is not lld). static SKIP: LazyLock = LazyLock::new(|| Regex::new(r"^(amdgcn|nvptx|bpf|spirv)|avr-none").unwrap()); static UNDEFINED: LazyLock = LazyLock::new(|| Regex::new(r"undefined symbol: (\S+)").unwrap()); -static ENV_MISSING: LazyLock = LazyLock::new(|| Regex::new(r"unable to find library|cannot open crt|cannot open .*\.o\b|No such file").unwrap()); +// Missing system files, or an architecture or ABI lld does not support (csky, m68k, sparc, +// xtensa, ppc64 ELFv1, aarch64 ILP32, sparc64 relocations): this host cannot link those. +static ENV_MISSING: LazyLock = LazyLock::new(|| { + Regex::new(r"unable to find library|cannot open crt|cannot open .*\.o\b|No such file|unsupported e_machine|ABI version 1 is not supported|unknown relocation \(\d+\)|R_SPARC_64 cannot be used").unwrap() +}); /// The parts of a target spec the link options depend on. #[derive(Deserialize, Default)] @@ -77,7 +81,9 @@ fn link_flags(spec: &Spec) -> Vec { flags.extend(["-Clink-arg=--no-entry", "-Clink-arg=--export=probe_entry"]); } else if spec.is_like_msvc || f.starts_with("msvc") { if !own_lld { - flags.extend(["-Clinker=rust-lld", "-Clinker-flavor=lld-link"]); + // A custom entry point leaves lld-link unable to infer the subsystem (UEFI targets + // bring their own linker and subsystem). + flags.extend(["-Clinker=rust-lld", "-Clinker-flavor=lld-link", "-Clink-arg=/SUBSYSTEM:CONSOLE"]); } flags.extend(["-Clink-arg=/ENTRY:probe_entry", "-Clink-arg=/NODEFAULTLIB"]); } else if spec.is_like_darwin || f.starts_with("darwin") { @@ -145,6 +151,14 @@ fn one(args: &Args, probe: &Path, target: &str) -> Res { undefined.sort(); undefined.dedup(); undefined.truncate(20); + // Symbols the platform's C runtime provides, which the probe links without: the MSVC CRT + // (`/NODEFAULTLIB`: stack probes, float markers, 64-bit division, libm), and the `__atomic_*` + // builtins NuttX's libc supplies for cores without atomic instructions (a documented choice in + // those target specs). + let from_runtime = |u: &String| { + (spec.is_like_msvc && !target.ends_with("-uefi")) || u.starts_with("__atomic_") + }; + let result = if result == "link-undefined" && !undefined.is_empty() && undefined.iter().all(from_runtime) { "env" } else { result }; let first = err.lines().find(|l| l.contains("error[") || l.contains("error:")).unwrap_or("").chars().take(300).collect(); let tail = if result == "ok" { String::new() } else { err.chars().rev().take(2500).collect::().chars().rev().collect() }; Res { result: result.into(), flags, undefined, first, tail } diff --git a/docs/checks.md b/docs/checks.md index e324728..4e1d2c8 100644 --- a/docs/checks.md +++ b/docs/checks.md @@ -424,6 +424,8 @@ Ten new findings (19–28) in [`hunt.md`](hunt.md), none from the checks mirth h | diagnostic invariants (13) | `mirth-lab diag-check` | 18,374 tests | finding 30: debug output in two diagnostics; spans all in bounds | | determinism (15) | `mirth-lab repro-diff` | 6,886 tests × repeat, other directory with `--remap-path-prefix`, `-Zthreads=8`, decoy `-L` library | nothing new: only `-Zthreads` differences, all in the known async fn (#162202) and RPIT (#163878) families | | feature gates (17) | `mirth-lab gate-check` | 143 unstable attributes × 14 positions; 156 unstable library items with resolvable paths × use, renamed use, glob, impl, value, type | every library spelling gated; finding 31 (an ICE after the gate error for `#[rustc_main]` on non-functions); `#[feature]` outside the crate root only warns (intended) | +| cross-target link (5) | `mirth-lab xlink` | every target rustc knows (334): `-Zbuild-std=core,alloc` and a link of a `no_std` probe with the target's linker, or `rust-lld` when it is missing | 222 link; 9 VxWorks targets ICE with `-Clinker-flavor=ld.lld` (finding 59); 4 need OS-provided `__atomic_*` builtins (NuttX, risc0: expected); the rest are this host's environment (missing C toolchains and runtimes, the MSVC CRT under `/NODEFAULTLIB`, architectures lld cannot link: csky, m68k, sparc, xtensa, ppc64 ELFv1, aarch64 ILP32) or timeouts waiting on Cargo's package lock under load | +| scaling and budgets (10) | `mirth-lab scale-check` | 8 shapes × opt 0/2 × N up to 800: compile CPU time and memory (wait4 rusage), stack frames, future sizes | linear or better except `iter-chain`, whose over-limit rejection is slow and noisy under the new solver (finding 32, corrected: not a regression on valid code) | ### Third batch (2026-10-10) diff --git a/docs/hunt.md b/docs/hunt.md index 6599e27..f9b0906 100644 --- a/docs/hunt.md +++ b/docs/hunt.md @@ -79,6 +79,7 @@ with `-Zthreads=8`. `rustc/check.sh wide` runs the ordinary checks. | 53 | "AliasConst::type_of got InherentSelf - args should always be InherentImpl at this point" (`const_kind.rs:85`, `check_well_formed`) for `fn to_bytes() -> [u8; gca!(Self::SIZE)]` in an inherent impl, when `generic_const_exprs` is also enabled; without it the program is accepted as in #162147's regression test (`gca/wf-inherentimpl.rs`) | **looks new** (a route around closed #162147, fixed 2026-09-03), low; nightly-2026-10-06; found by `gate-mutate` (a splice whose `--cfg full` turns on `generic_const_exprs`); [repro](hunt/tests/gate-mutate/gce-inherent-self.rs) | | 54 | `rustc --test` panics "expected statement" (`rustc_expand/src/base.rs:172`) after E0736 for a `#[test] #[unsafe(naked)] extern "C" fn` nested inside another function's body; a `#[test]` inner fn without `naked` only warns "cannot test inner items" | **looks new** (closed issues with the message: #112360, #109816 (both `--test`), #83469, #149980; none open, none with `naked`), low (error recovery), **stable**: 1.82.0 through 1.88.0, 1.90.0, 1.98.0 and nightly-2026-10-06 (each tested) (1.81.0 rejects with E0658/E0787; before 1.88 the panic comes before the gate error); found by `gate-mutate` (an item moved into a generic fn); [repro](hunt/tests/gate-mutate/naked-test-inner-fn.rs) | | 55 | a hang under the new trait solver: a closure with a `for<'a, 'b>` binder returning a TAIT with two lifetimes, passed where a `for<'a> AsyncFn<&'a mut C, …>` bound (a trait with an `FnMut` supertrait and an associated future) is required, does not finish compiling (still running after 200 s); with `-Znext-solver=coherence` (the old solver) it reports E0046/E0308/E0277 in 0.05 s | **looks new** (no issue found), medium: the new solver is nightly's default, so the plain `rustc` hangs on nightly-2026-10-06; also hangs on nightly-2026-07-18 with `-Znext-solver=globally` (not a recent regression); found by `gate-mutate` (a module splice of two tests); [repro](hunt/tests/gate-mutate/next-solver-hang.rs) | +| 59 | `*-wrs-vxworks` targets (9): linking with `-Clinker-flavor=ld.lld` panics, "assertion failed: l.is_cc()" (`rustc_codegen_ssa/src/back/linker.rs:225`), where the target's linker setup expects a C-compiler flavor; without the flag, the expected "linker not found" | **looks new**, low (ICE instead of an error; tier 3, `-Zbuild-std`); found by the cross-target link check; [facts](hunt/vxworks-linker-flavor-ice.md) | Findings 1 and 2 are single-threaded: an ordinary `cargo build`, an edit, another `cargo build`, and the metadata differs from a clean build of the edited source. Both come diff --git a/docs/hunt/vxworks-linker-flavor-ice.md b/docs/hunt/vxworks-linker-flavor-ice.md new file mode 100644 index 0000000..0d17955 --- /dev/null +++ b/docs/hunt/vxworks-linker-flavor-ice.md @@ -0,0 +1,36 @@ +# VxWorks targets: `-Clinker-flavor=ld.lld` panics in rustc's linker setup + +Facts for finding 59. Found by the cross-target link check (`mirth-lab xlink`, +[`checks.md`](../checks.md) check 5), which links a small `no_std` probe for every target with +`rust-lld` when the target's default linker is not installed. + +## What happens + +For every `*-wrs-vxworks` target (aarch64, armv7 eabihf, i686, powerpc, powerpc-spe, powerpc64, +riscv32, riscv64, x86_64), linking with `-Clinker=rust-lld -Clinker-flavor=ld.lld` panics: + +``` +thread 'rustc' panicked at compiler/rustc_codegen_ssa/src/back/linker.rs:225:5: +assertion failed: l.is_cc() +``` + +Without `-Clinker-flavor`, the same build reports the expected "linker `wr-c++` not found". The +VxWorks linker setup assumes a C-compiler-style linker flavor; a stable command-line option +reaches the assertion instead of an error. + +## Reproduction + +With the probe crate (`rustc/xlink-probe`) or any `no_std` binary: + +```sh +RUSTFLAGS="-Clinker=rust-lld -Clinker-flavor=ld.lld -Clink-arg=--entry=probe_entry" \ + cargo +nightly-2026-10-06 build --release -Zbuild-std=core,alloc \ + -Zbuild-std-features=compiler-builtins-mem --target x86_64-wrs-vxworks +``` + +## Scope + +Tier 3 targets, reached only with `-Zbuild-std` (no prebuilt std) and an explicit +`-Clinker-flavor` the target does not expect. Low severity: an ICE where an error belongs. No +rust-lang/rust issue found (search for `is_cc` and vxworks, 2026-10-10). Same result in the +earlier Python xlink sweep (2026-10-09).