diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5899c0ec77400..3b15097ce40c6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -385,7 +385,21 @@ jobs: curl --proto '=https' --tlsv1.2 -LsSf \ "https://github.com/astral-sh/hawk/releases/download/${HAWK_VERSION}/cargo-hawk-installer.sh" | sh - name: "Hawk" - run: cargo +1.99.0 hawk check --target-dir target/hawk --color=always -D warnings + # The fork SPO/transcode libraries are an external API: OGAR and the + # transcode consumers call them from outside this workspace, which a + # closed-world analysis cannot see. Hawk compiles them but does not + # report their public declarations. + run: >- + cargo +1.99.0 hawk check --target-dir target/hawk --color=always -D warnings + --exclude-crate ruff_cpp_codegen + --exclude-crate ruff_cpp_spo + --exclude-crate ruff_csharp_spo + --exclude-crate ruff_python_dto_check + --exclude-crate ruff_python_spo + --exclude-crate ruff_ruby_spo + --exclude-crate ruff_spo_address + --exclude-crate ruff_spo_triplet + --exclude-crate ruff_sqlalchemy_spo cargo-publish-dry-run: name: "cargo publish dry-run" @@ -656,6 +670,54 @@ jobs: MSRV: ${{ steps.msrv.outputs.value }} run: cargo "+${MSRV}" test --no-run --all-features + cargo-r2il: + # `ruff_r2il` (fork-only) is excluded from the workspace because it path-depends + # on the `r2sleigh` and `a2ui-rs` sibling checkouts, so the workspace-wide fmt, + # clippy and test jobs never reach it. This job checks out both siblings next to + # this repository, matching the `../../../` paths in its Cargo.toml. + name: "cargo (ruff_r2il)" + runs-on: ubuntu-latest + needs: determine_changes + if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} + timeout-minutes: 30 + env: + CARGO_PROFILE_DEV_DEBUG: line-tables-only + R2IL_MANIFEST: crates/ruff_r2il/Cargo.toml + defaults: + run: + working-directory: ruff + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: ruff + persist-credentials: false + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: AdaWorldAPI/r2sleigh + path: r2sleigh + persist-credentials: false + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: AdaWorldAPI/a2ui-rs + path: a2ui-rs + persist-credentials: false + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + workspaces: ruff/crates/ruff_r2il + save-if: ${{ github.ref == 'refs/heads/main' }} + - name: "Install Rust toolchain" + run: rustup component add rustfmt clippy + - name: "Install mold" + uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 + - name: "cargo fmt" + run: cargo fmt --manifest-path "${R2IL_MANIFEST}" --check + - name: "cargo clippy" + run: cargo clippy --manifest-path "${R2IL_MANIFEST}" --all-targets --locked -- -D warnings + - name: "cargo clippy (lift)" + run: cargo clippy --manifest-path "${R2IL_MANIFEST}" --features lift --all-targets --locked -- -D warnings + - name: "cargo test" + run: cargo test --manifest-path "${R2IL_MANIFEST}" --locked + cargo-fuzz-build: name: "cargo fuzz build" runs-on: ${{ github.repository == 'astral-sh/ruff' && 'github-ubuntu-24.04-x86_64-4' || 'ubuntu-latest' }} @@ -1537,6 +1599,7 @@ jobs: - cargo-test-linux - cargo-test-wasm - cargo-build-msrv + - cargo-r2il - shellcheck - scripts - prek diff --git a/crates/ruff_r2il/Cargo.lock b/crates/ruff_r2il/Cargo.lock index e8d146d16819a..ff3d0525d499d 100644 --- a/crates/ruff_r2il/Cargo.lock +++ b/crates/ruff_r2il/Cargo.lock @@ -375,6 +375,12 @@ dependencies = [ "rawpointer", ] +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + [[package]] name = "ndarray" version = "0.17.2" @@ -508,10 +514,19 @@ version = "0.1.0" dependencies = [ "libsla", "r2il", - "sleigh-config", + "r2source", + "roxmltree", "thiserror", ] +[[package]] +name = "r2source" +version = "0.1.0" +dependencies = [ + "r2il", + "serde", +] + [[package]] name = "r2ssa" version = "0.1.0" @@ -519,8 +534,8 @@ dependencies = [ "petgraph", "r2il", "r2sleigh-lift", + "r2source", "serde", - "thiserror", ] [[package]] @@ -529,6 +544,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" +[[package]] +name = "roxmltree" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb" +dependencies = [ + "memchr", +] + [[package]] name = "ruff_r2il" version = "0.1.0" diff --git a/crates/ruff_r2il/examples/r2il_corpus_profile.rs b/crates/ruff_r2il/examples/r2il_corpus_profile.rs index 8e184185aae0c..9ca6ce4dba34d 100644 --- a/crates/ruff_r2il/examples/r2il_corpus_profile.rs +++ b/crates/ruff_r2il/examples/r2il_corpus_profile.rs @@ -661,6 +661,7 @@ struct Pass2Stats { vocab_interned_id_bytes: Vec, facet_ok: usize, facet_unknown_custom_space: usize, + facet_unresolved_space: usize, facet_ordinal_exhausted: usize, smelt_harvested: usize, smelt_classified: usize, @@ -723,12 +724,15 @@ fn print_pass2(stats: &Pass2Stats) { print_distribution("vocab.ssa_name_bytes", &stats.vocab_ssa_name_bytes); print_distribution("vocab.interned_id_bytes", &stats.vocab_interned_id_bytes); - let facet_total = - stats.facet_ok + stats.facet_unknown_custom_space + stats.facet_ordinal_exhausted; + let facet_total = stats.facet_ok + + stats.facet_unknown_custom_space + + stats.facet_unresolved_space + + stats.facet_ordinal_exhausted; println!( - " facet::project sweep: ok={} FacetOverflow(UnknownCustomSpace)={} FacetOverflow(CustomOrdinalExhausted)={} total={}", + " facet::project sweep: ok={} FacetOverflow(UnknownCustomSpace)={} FacetOverflow(UnresolvedSpace)={} FacetOverflow(CustomOrdinalExhausted)={} total={}", stats.facet_ok, stats.facet_unknown_custom_space, + stats.facet_unresolved_space, stats.facet_ordinal_exhausted, facet_total ); @@ -932,6 +936,9 @@ fn run_pass2_over( Err(facet::FacetOverflow::UnknownCustomSpace { .. }) => { stats.facet_unknown_custom_space += 1; } + Err(facet::FacetOverflow::UnresolvedSpace) => { + stats.facet_unresolved_space += 1; + } Err(facet::FacetOverflow::CustomOrdinalExhausted { .. }) => { stats.facet_ordinal_exhausted += 1; } diff --git a/crates/ruff_r2il/src/facet.rs b/crates/ruff_r2il/src/facet.rs index 20c9144afb9a9..b021c639283a0 100644 --- a/crates/ruff_r2il/src/facet.rs +++ b/crates/ruff_r2il/src/facet.rs @@ -125,6 +125,11 @@ pub enum FacetOverflow { /// back to `raw as u16` (`65541` and `5` both truncate to `5`, silently colliding two /// distinct architecture-defined spaces). UnknownCustomSpace { raw: u32 }, + /// The varnode's space is `SpaceId::Unresolved`: the lifter could not name it, so it has + /// no stable discriminant. Projection REFUSES rather than assign one, because every + /// unresolved space compares equal and a shared discriminant would merge spaces that may + /// be distinct. + UnresolvedSpace, /// The set of distinct custom-space raw ids exceeds [`MAX_CUSTOM_ORDINAL`]. Overflow is /// EVIDENCE a route needs factoring, never silent truncation (the `mint_factored` /// principle). @@ -137,6 +142,12 @@ impl core::fmt::Display for FacetOverflow { FacetOverflow::UnknownCustomSpace { raw } => { write!(f, "custom space {raw} is not in the interned table") } + FacetOverflow::UnresolvedSpace => { + write!( + f, + "the varnode's address space was not resolved by the lifter" + ) + } FacetOverflow::CustomOrdinalExhausted { count } => { write!( f, @@ -228,6 +239,7 @@ pub fn project(vn: &Varnode, spaces: &CustomSpaceTable) -> Result spaces .ordinal_of(raw) .ok_or(FacetOverflow::UnknownCustomSpace { raw })?, + SpaceId::Unresolved => return Err(FacetOverflow::UnresolvedSpace), }; let classid = ((PROVISIONAL_R2IL_VARNODE as u32) << 16) | discriminant as u32; let offset_lo = (vn.offset & 0xFFFF_FFFF) as u32; @@ -305,6 +317,21 @@ mod tests { assert_eq!(back, vn); } + #[test] + fn unresolved_space_is_refused_rather_than_given_a_discriminant() { + // Every unresolved space compares equal, so no table, however populated, can + // give one a discriminant without risking a merge of two different spaces. + let table = CustomSpaceTable::from_ids([0, 5]).expect("2 ids is within budget"); + let vn = Varnode::new(SpaceId::Unresolved, 0x10, 4); + assert_eq!(project(&vn, &table), Err(FacetOverflow::UnresolvedSpace)); + assert_eq!( + project(&vn, &CustomSpaceTable::default()), + Err(FacetOverflow::UnresolvedSpace) + ); + // The same offset and size in a named space still projects. + assert!(project(&Varnode::new(SpaceId::Ram, 0x10, 4), &table).is_ok()); + } + #[test] fn custom_space_outside_the_table_errors_and_never_truncates() { let empty = CustomSpaceTable::default(); diff --git a/crates/ruff_r2il/src/furnace.rs b/crates/ruff_r2il/src/furnace.rs index dc8273a457eee..8e8cbe73080ed 100644 --- a/crates/ruff_r2il/src/furnace.rs +++ b/crates/ruff_r2il/src/furnace.rs @@ -391,6 +391,18 @@ pub fn smelt( prov, ); } + Err(FacetOverflow::UnresolvedSpace) => { + // Same fallback as an unknown custom space: the residual is anchored + // at the enclosing op's block so it still carries an address. + let at = any_anchor_facet(behavior, prov, conv); + push_named_residual( + &mut ledger, + ResidualReason::UnresolvedSpace, + at, + conv, + prov, + ); + } Err(FacetOverflow::CustomOrdinalExhausted { count }) => { // Structurally unreachable for a validly-constructed `CustomSpaceTable` // (the budget is enforced at `from_ids`/`from_arch` time) — handled diff --git a/crates/ruff_r2il/src/oracle.rs b/crates/ruff_r2il/src/oracle.rs index 1320f9ac81021..858f966820631 100644 --- a/crates/ruff_r2il/src/oracle.rs +++ b/crates/ruff_r2il/src/oracle.rs @@ -479,6 +479,7 @@ pub fn permissive_convention(blocks: &[R2ILBlock]) -> Result (String, String) { (expected.as_str().to_string(), found.as_str().to_string()) } ResidualReason::NoConventionRowAtAddress + | ResidualReason::UnresolvedSpace | ResidualReason::MemoryObjectEscaped | ResidualReason::IndirectTarget | ResidualReason::NoFacetCoordinate => (String::new(), String::new()), @@ -526,6 +527,7 @@ fn parse_reason(name: &str, p1: &str, p2: &str) -> io::Result { "custom_space_not_in_convention" => ResidualReason::CustomSpaceNotInConvention { raw: parse_u32(p1)?, }, + "unresolved_space" => ResidualReason::UnresolvedSpace, "facet_overflow_at_key" => ResidualReason::FacetOverflowAtKey { raw: parse_u32(p1)?, }, diff --git a/crates/ruff_r2il/src/slag.rs b/crates/ruff_r2il/src/slag.rs index a4481376c0edf..dd3f578799bee 100644 --- a/crates/ruff_r2il/src/slag.rs +++ b/crates/ruff_r2il/src/slag.rs @@ -90,6 +90,10 @@ pub enum ResidualReason { /// The varnode's `SpaceId::Custom(raw)` is not in the convention's /// [`crate::facet::CustomSpaceTable`]. CustomSpaceNotInConvention { raw: u32 }, + /// The varnode's space is `SpaceId::Unresolved`: the lifter could not name it, + /// so no stable facet coordinate exists for it. Every such varnode compares + /// equal, so there is no payload to distinguish them. + UnresolvedSpace, /// The `Custom(raw)` space exists in the arch data but exceeds the /// interned-ordinal budget at CONFIG-KEY construction time (§4's /// promoted falsifier) — a typed overflow, never a silent truncation. @@ -126,6 +130,7 @@ impl ResidualReason { "no_convention_row_at_address", "userop_not_in_convention", "custom_space_not_in_convention", + "unresolved_space", "facet_overflow_at_key", "variadic_arity", "phi_fan_in_exceeds_predecessors", @@ -143,6 +148,7 @@ impl ResidualReason { ResidualReason::NoConventionRowAtAddress => "no_convention_row_at_address", ResidualReason::UserOpNotInConvention { .. } => "userop_not_in_convention", ResidualReason::CustomSpaceNotInConvention { .. } => "custom_space_not_in_convention", + ResidualReason::UnresolvedSpace => "unresolved_space", ResidualReason::FacetOverflowAtKey { .. } => "facet_overflow_at_key", ResidualReason::VariadicArity { .. } => "variadic_arity", ResidualReason::PhiFanInExceedsPredecessors { .. } => "phi_fan_in_exceeds_predecessors", @@ -172,6 +178,7 @@ impl ResidualReason { ResidualReason::CustomSpaceNotInConvention { raw } => { bytes.extend_from_slice(&raw.to_le_bytes()); } + ResidualReason::UnresolvedSpace => {} ResidualReason::FacetOverflowAtKey { raw } => { bytes.extend_from_slice(&raw.to_le_bytes()); } @@ -463,13 +470,14 @@ mod tests { // `ResidualReason`, this fails to COMPILE until this test (and // `ResidualReason::ALL`) are updated to match. That non-compiling // failure mode is the falsifier for "no catch-all reason". - let sample: [ResidualReason; 11] = [ + let sample: [ResidualReason; 12] = [ ResidualReason::OpcodeNotInConvention { opcode: OpTag::Copy, }, ResidualReason::NoConventionRowAtAddress, ResidualReason::UserOpNotInConvention { userop: 0 }, ResidualReason::CustomSpaceNotInConvention { raw: 0 }, + ResidualReason::UnresolvedSpace, ResidualReason::FacetOverflowAtKey { raw: 0 }, ResidualReason::VariadicArity { arity: 0 }, ResidualReason::PhiFanInExceedsPredecessors { @@ -492,6 +500,7 @@ mod tests { ResidualReason::NoConventionRowAtAddress => 1, ResidualReason::UserOpNotInConvention { .. } => 1, ResidualReason::CustomSpaceNotInConvention { .. } => 1, + ResidualReason::UnresolvedSpace => 1, ResidualReason::FacetOverflowAtKey { .. } => 1, ResidualReason::VariadicArity { .. } => 1, ResidualReason::PhiFanInExceedsPredecessors { .. } => 1, @@ -502,7 +511,7 @@ mod tests { // Deliberately NO `_ =>` arm. }; } - assert_eq!(variant_count, 11); + assert_eq!(variant_count, 12); assert_eq!(ResidualReason::ALL.len(), variant_count); let mut sorted = ResidualReason::ALL.to_vec();