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
44 changes: 32 additions & 12 deletions crates/mirth-lab/src/rustc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -24,6 +23,9 @@ pub struct Finished {
pub exit: Exit,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
/// 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<f64>,
}

impl Finished {
Expand Down Expand Up @@ -66,24 +68,42 @@ pub fn run_command(mut cmd: Command, timeout: Duration) -> std::io::Result<Finis
let _ = err.read_to_end(&mut v);
v
});
let exit = match child.wait_timeout(timeout)? {
Some(status) => 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,
})
}

Expand Down
20 changes: 12 additions & 8 deletions crates/mirth-lab/src/tools/release_diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -47,6 +47,9 @@ static FEATURE: LazyLock<Regex> = 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<String>,
first: String,
Expand Down Expand Up @@ -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::<String>().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(...)]`.
Expand Down Expand Up @@ -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());
Expand Down
18 changes: 16 additions & 2 deletions crates/mirth-lab/src/tools/xlink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Regex> = LazyLock::new(|| Regex::new(r"^(amdgcn|nvptx|bpf|spirv)|avr-none").unwrap());
static UNDEFINED: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"undefined symbol: (\S+)").unwrap());
static ENV_MISSING: LazyLock<Regex> = 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<Regex> = 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)]
Expand Down Expand Up @@ -77,7 +81,9 @@ fn link_flags(spec: &Spec) -> Vec<String> {
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") {
Expand Down Expand Up @@ -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::<String>().chars().rev().collect() };
Res { result: result.into(), flags, undefined, first, tail }
Expand Down
2 changes: 2 additions & 0 deletions docs/checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions docs/hunt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions docs/hunt/vxworks-linker-flavor-ice.md
Original file line number Diff line number Diff line change
@@ -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).
Loading