Skip to main content

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}