Skip to content

Commit 72e96a7

Browse files
hyperpolymathclaude
andcommitted
feat(assail): JIT-aware classification for mem::transmute (Rust)
When a Rust file is in a Cranelift JIT context (imports `cranelift_jit` or `cranelift_module`, constructs a `JITModule`, or calls `get_finalized_function`) AND the transmute targets a function-pointer type (`fn(...) -> R` or `unsafe fn(...) -> R`, declared via turbofish or a typed `let` binding), the finding is downgraded from Critical to High and the description carries a Cranelift JIT classification suffix. Motivation: the Cranelift JIT dispatch idiom let f: fn(i64) -> i64 = std::mem::transmute(compiled.fn_ptr); f(args[0]) is the only way to invoke a pointer returned by a code-generation framework in Rust. It isn't arbitrary type-punning — its soundness rests on per-file invariants (signature match, module ownership, thread affinity) that belong in an audit classification register rather than a Critical flag. Reviewers now see High + the JIT signal instead of a blanket Critical that had to be hand-classified every time. Both signals are required: JIT-context alone (without a fn-ptr target) keeps Critical, because a transmute between non-function types in JIT code could still be a real bug. Non-JIT code stays Critical regardless of transmute target. See 007-lang/audits/audit-ffi-unsafe.md §2 for the motivating case. Verified end-to-end: 007's jit_compiler.rs previously surfaced Critical; now surfaces High with the classification suffix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 017fc69 commit 72e96a7

1 file changed

Lines changed: 226 additions & 3 deletions

File tree

src/assail/analyzer.rs

Lines changed: 226 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -906,15 +906,64 @@ impl Analyzer {
906906
});
907907
}
908908

909-
// mem::transmute — type-punning bypasses Rust's type system entirely
909+
// mem::transmute — type-punning bypasses Rust's type system entirely.
910+
//
911+
// JIT-aware classification: when the file is in a Cranelift JIT
912+
// context AND the transmute targets a function pointer type, this
913+
// is the canonical pattern for invoking a JIT-emitted function —
914+
// there is no `unsafe`-free way to call a pointer returned by a
915+
// code-generation framework. In that context the finding is
916+
// downgraded from Critical to High and a classification suffix is
917+
// appended so reviewers know to look at the JIT soundness invariants
918+
// (signature match, lifetime ownership, thread affinity) rather
919+
// than treating the transmute as arbitrary type-punning.
920+
//
921+
// Both signals are required:
922+
// 1. JIT context — file imports cranelift_jit / cranelift_module
923+
// or constructs a JITModule.
924+
// 2. Function-pointer target — the transmute target is `fn(...)`,
925+
// either via turbofish (`transmute::<_, fn(...) -> R>`) or via
926+
// a `let x: fn(...) -> R = transmute(...)` binding.
927+
//
928+
// See 007-lang/audits/audit-ffi-unsafe.md §2 for the motivating
929+
// case (jit_compiler.rs's Cranelift dispatch).
910930
if code_only.contains("transmute(") || code_only.contains("transmute::<") {
931+
let in_jit_context = code_only.contains("cranelift_jit::JITModule")
932+
|| code_only.contains("cranelift_module::Module")
933+
|| code_only.contains("JITModule::new(")
934+
|| (code_only.contains("cranelift_jit")
935+
&& code_only.contains("get_finalized_function"));
936+
937+
let transmute_targets_fn_ptr = code_only.contains("transmute::<_, fn(")
938+
|| code_only.contains("transmute::<_, unsafe fn(")
939+
|| code_only.contains("transmute::<*const u8, fn(")
940+
|| code_only.contains("transmute::<*mut u8, fn(")
941+
|| (code_only.contains(": fn(") && code_only.contains("= std::mem::transmute("))
942+
|| (code_only.contains(": fn(") && code_only.contains("= mem::transmute("))
943+
|| (code_only.contains(": unsafe fn(") && code_only.contains("transmute("));
944+
945+
let (severity, description) = if in_jit_context && transmute_targets_fn_ptr {
946+
(
947+
Severity::High,
948+
format!(
949+
"mem::transmute usage in {} (Cranelift JIT function-pointer dispatch — verify signature match + module ownership invariants per audit-ffi-unsafe.md classification)",
950+
file_path
951+
),
952+
)
953+
} else {
954+
(
955+
Severity::Critical,
956+
format!("mem::transmute usage in {}", file_path),
957+
)
958+
};
959+
911960
weak_points.push(WeakPoint {
912961
file: None,
913962
line: None,
914963
category: WeakPointCategory::UnsafeCode,
915964
location: Some(file_path.to_string()),
916-
severity: Severity::Critical,
917-
description: format!("mem::transmute usage in {}", file_path),
965+
severity,
966+
description,
918967
recommended_attack: vec![AttackAxis::Memory],
919968
suppressed: false,
920969
});
@@ -5334,6 +5383,180 @@ fn few_unwraps() {
53345383
);
53355384
}
53365385

5386+
// ---------------------------------------------------------------
5387+
// mem::transmute: JIT-aware classification
5388+
// (Cranelift function-pointer dispatch downgrades Critical → High)
5389+
// ---------------------------------------------------------------
5390+
5391+
#[test]
5392+
fn analyze_rust_transmute_in_jit_context_downgrades_to_high() {
5393+
let analyzer = Analyzer {
5394+
target: std::path::PathBuf::from("jit_compiler.rs"),
5395+
language: crate::types::Language::Rust,
5396+
verbose: false,
5397+
browser_extension: false,
5398+
};
5399+
// Canonical Cranelift JIT dispatch pattern: a function pointer
5400+
// returned by JITModule::get_finalized_function() is transmuted
5401+
// to a typed `fn(...) -> R` so it can be invoked from Rust.
5402+
let content = r#"
5403+
use cranelift_jit::JITModule;
5404+
use cranelift_module::Module;
5405+
5406+
pub struct CompiledFunction {
5407+
fn_ptr: *const u8,
5408+
param_count: usize,
5409+
_module: JITModule,
5410+
}
5411+
5412+
pub fn call(compiled: &CompiledFunction, args: &[i64]) -> Option<i64> {
5413+
unsafe {
5414+
match compiled.param_count {
5415+
0 => {
5416+
let f: fn() -> i64 = std::mem::transmute(compiled.fn_ptr);
5417+
Some(f())
5418+
}
5419+
1 => {
5420+
let f: fn(i64) -> i64 = std::mem::transmute(compiled.fn_ptr);
5421+
Some(f(args[0]))
5422+
}
5423+
_ => None,
5424+
}
5425+
}
5426+
}
5427+
"#;
5428+
let mut stats = ProgramStatistics {
5429+
total_lines: 0,
5430+
unsafe_blocks: 0,
5431+
panic_sites: 0,
5432+
unwrap_calls: 0,
5433+
allocation_sites: 0,
5434+
io_operations: 0,
5435+
threading_constructs: 0,
5436+
};
5437+
let mut weak_points = Vec::new();
5438+
analyzer
5439+
.analyze_rust(content, &mut stats, &mut weak_points, "jit_compiler.rs")
5440+
.unwrap();
5441+
5442+
let transmute_findings: Vec<_> = weak_points
5443+
.iter()
5444+
.filter(|wp| wp.description.contains("mem::transmute"))
5445+
.collect();
5446+
5447+
assert_eq!(
5448+
transmute_findings.len(),
5449+
1,
5450+
"Exactly one mem::transmute finding expected"
5451+
);
5452+
assert_eq!(
5453+
transmute_findings[0].severity,
5454+
Severity::High,
5455+
"JIT-context transmute targeting fn(...) -> R should downgrade Critical → High"
5456+
);
5457+
assert!(
5458+
transmute_findings[0]
5459+
.description
5460+
.contains("Cranelift JIT function-pointer dispatch"),
5461+
"Downgraded finding should carry the JIT classification suffix; got: {}",
5462+
transmute_findings[0].description
5463+
);
5464+
}
5465+
5466+
#[test]
5467+
fn analyze_rust_transmute_outside_jit_context_stays_critical() {
5468+
let analyzer = Analyzer {
5469+
target: std::path::PathBuf::from("util.rs"),
5470+
language: crate::types::Language::Rust,
5471+
verbose: false,
5472+
browser_extension: false,
5473+
};
5474+
// Arbitrary type-punning transmute, NO Cranelift markers anywhere.
5475+
// Must remain Critical — the JIT-aware downgrade should never
5476+
// suppress real type-punning bugs.
5477+
let content = r#"
5478+
fn pun_u64_to_two_u32s(x: u64) -> (u32, u32) {
5479+
unsafe { std::mem::transmute(x) }
5480+
}
5481+
"#;
5482+
let mut stats = ProgramStatistics {
5483+
total_lines: 0,
5484+
unsafe_blocks: 0,
5485+
panic_sites: 0,
5486+
unwrap_calls: 0,
5487+
allocation_sites: 0,
5488+
io_operations: 0,
5489+
threading_constructs: 0,
5490+
};
5491+
let mut weak_points = Vec::new();
5492+
analyzer
5493+
.analyze_rust(content, &mut stats, &mut weak_points, "util.rs")
5494+
.unwrap();
5495+
5496+
let transmute_findings: Vec<_> = weak_points
5497+
.iter()
5498+
.filter(|wp| wp.description.contains("mem::transmute"))
5499+
.collect();
5500+
5501+
assert_eq!(transmute_findings.len(), 1);
5502+
assert_eq!(
5503+
transmute_findings[0].severity,
5504+
Severity::Critical,
5505+
"Non-JIT transmute must stay Critical — JIT-aware downgrade must not over-suppress"
5506+
);
5507+
assert!(
5508+
!transmute_findings[0]
5509+
.description
5510+
.contains("Cranelift JIT"),
5511+
"Non-JIT finding must not carry the JIT classification suffix"
5512+
);
5513+
}
5514+
5515+
#[test]
5516+
fn analyze_rust_jit_marker_without_fn_ptr_target_stays_critical() {
5517+
// Edge case: file IS a JIT file (cranelift_jit imported) but the
5518+
// transmute target is NOT a function pointer — could be a real bug
5519+
// in JIT code. Stay Critical.
5520+
let analyzer = Analyzer {
5521+
target: std::path::PathBuf::from("jit_misc.rs"),
5522+
language: crate::types::Language::Rust,
5523+
verbose: false,
5524+
browser_extension: false,
5525+
};
5526+
let content = r#"
5527+
use cranelift_jit::JITModule;
5528+
5529+
fn pun_in_jit_file(x: u64) -> [u8; 8] {
5530+
unsafe { std::mem::transmute(x) }
5531+
}
5532+
"#;
5533+
let mut stats = ProgramStatistics {
5534+
total_lines: 0,
5535+
unsafe_blocks: 0,
5536+
panic_sites: 0,
5537+
unwrap_calls: 0,
5538+
allocation_sites: 0,
5539+
io_operations: 0,
5540+
threading_constructs: 0,
5541+
};
5542+
let mut weak_points = Vec::new();
5543+
analyzer
5544+
.analyze_rust(content, &mut stats, &mut weak_points, "jit_misc.rs")
5545+
.unwrap();
5546+
5547+
let transmute_findings: Vec<_> = weak_points
5548+
.iter()
5549+
.filter(|wp| wp.description.contains("mem::transmute"))
5550+
.collect();
5551+
5552+
assert_eq!(transmute_findings.len(), 1);
5553+
assert_eq!(
5554+
transmute_findings[0].severity,
5555+
Severity::Critical,
5556+
"JIT-context but no fn-ptr target — must stay Critical (could be real bug)"
5557+
);
5558+
}
5559+
53375560
// ---------------------------------------------------------------
53385561
// CryptoMisuse: missing signature verification (new patterns)
53395562
// ---------------------------------------------------------------

0 commit comments

Comments
 (0)