miniextendr_lint/rules/vec_into_sexp.rs
1//! `into_sexp()` inside a `vec!`/array literal — use-after-free idiom.
2//!
3//! - MXL302: Warns on `into_sexp()` / `into_sexp_unchecked()` calls that appear as
4//! elements *inside* a `vec!` or `&[...]` literal.
5//!
6//! Each `into_sexp` allocates a fresh SEXP. When several occur in one literal
7//! (`vec![(k, a.into_sexp()), (k, b.into_sexp())]`), nothing roots the earlier
8//! elements until the whole `Vec` reaches `List::from_raw_pairs`, so building a
9//! later element can trigger a GC that collects an earlier, still-unprotected one
10//! — a use-after-free. This recurred enough (#307, the 2026-05-07 gctorture audit)
11//! that the `IntoList` / `DataFrameRow` derives now wrap every element in
12//! `__scope.protect_raw(...)`; this lint stops new hand-written sites from
13//! reintroducing the raw form silently.
14
15use crate::crate_index::CrateIndex;
16use crate::diagnostic::Diagnostic;
17use crate::lint_code::LintCode;
18
19pub fn check(index: &CrateIndex, diagnostics: &mut Vec<Diagnostic>) {
20 for (path, data) in &index.file_data {
21 for (call_name, line) in &data.vec_into_sexp_calls {
22 diagnostics.push(
23 Diagnostic::new(
24 LintCode::MXL302,
25 path,
26 *line,
27 format!(
28 "`{}()` is called inside a `vec!`/array literal. Each `into_sexp` \
29 allocates; an earlier element built this way is left unprotected \
30 across the next element's allocation — a use-after-free under GC.",
31 call_name
32 ),
33 )
34 .with_help(
35 "Protect each element as it is built: open a `ProtectScope` and wrap each \
36 call, e.g. `__scope.protect_raw(x.into_sexp())`, then hand the `Vec` to \
37 `List::from_raw_pairs` / `from_raw_values`. The `IntoList` and \
38 `DataFrameRow` derives already do this — prefer them over a hand-rolled \
39 literal. Suppress an intentional, provably-safe site with \
40 `// mxl::allow(MXL302)`.",
41 ),
42 );
43 }
44 }
45}