-
-
Notifications
You must be signed in to change notification settings - Fork 495
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Visit::alloc
is unsound
#8461
Labels
Comments
overlookmotel
added
C-bug
Category - Bug
A-semantic
Area - Semantic
A-ast
Area - AST
labels
Jan 13, 2025
This was referenced Jan 13, 2025
I think I may have a solution.
/// 'a = AST
/// 'r = References
trait Visit<'a, 'r> {
fn visit_program(&mut self, program: &'r Program<'a>) {
self.enter_node(AstKindRef::Program(program));
// ...
self.leave_node(AstKindRef::Program(program));
}
fn enter_node(&mut self, kind: AstKindRef<'a, 'r>) {}
fn leave_node(&mut self, kind: AstKindRef<'a, 'r>) {}
}
enum AstKindRef<'a, 'r> {
Program(&'r Program<'a>),
// ...
}
type AstKind<'a> = AstKindRef<'a, 'a>; Most visitors remain exactly the same as before, except for an extra impl<'a> Visit<'a, '_> for MyVisitor<'a> { // <-- `'_` is new
fn visit_program(&mut self, program: &Program<'a>) { // <-- `'r` lifetime on `&Program<'a>` omitted
// ...
}
}
struct SemanticBuilder<'a> {
// Actually this is wrapped inside `AstNodes`, but principle is the same
kinds: Vec<AstKind<'a>>,
}
impl<'a> Visit<'a, 'a> for SemanticBuilder<'a> { // <-- `Visit<'a, 'a>`
fn enter_node(&mut self, kind: AstKind<'a>) { // <-- Because 'r = 'a, can use `AstKind<'a>` here
self.kinds.push(kind);
}
}
|
overlookmotel
added a commit
that referenced
this issue
Jan 16, 2025
…f `AstKind`s (#8522) This lint rule keeps a stack tracing the "visitation path" during `Visit`. Only the type of the nodes is used, not their values, so store only `AstType` (1 byte) rather than `AstKind` (16 bytes). This should be more performant, but the main motivation is #8461. This is one of very few places in the codebase which uses `enter_node` and `leave_node`. Not storing `AstKind`s here clears the way to remove the unsound lifetime extension in `Visit::alloc`.
This was referenced Jan 16, 2025
overlookmotel
added a commit
that referenced
this issue
Jan 16, 2025
Use `Visit::visit_*` method instead of `Visit::enter_node` / `leave_node`. A step towards solving #8461, but also may improve performance.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
#8437 surfaced unsoundness in
SemanticBuilder
- a use after free bug.@bluurryy also hit this previously while working on #6668 (discussed on Discord).
The root cause of that problem is the lifetime extension in
Visit::alloc
(which all thewalk_*
methods call to createAstKind
s).oxc/crates/oxc_ast/src/generated/visit.rs
Lines 42 to 47 in ab694b0
The problem
Looking at how the lifetime extension causes #8437:
SemanticBuilder::build
takes a&Program<'a>
(unspecified lifetime on the&
borrow ofProgram
) and returnsSemanticBuilderReturn<'a>
:oxc/crates/oxc_semantic/src/builder.rs
Line 226 in ab694b0
Tracing the effect of this through the different types:
SemanticBuilderReturn<'a>
containsSemantic<'a>
.Semantic<'a>
containsAstNodes<'a>
.AstNodes<'a>
contains aVec
ofAstNode<'a>
.AstNode<'a>
containsAstKind<'a>
.AstKind<'a>
contains&'a
refs to AST nodes (including&'a Program<'a>
).oxc/crates/oxc_semantic/src/builder.rs
Lines 114 to 117 in ab694b0
oxc/crates/oxc_semantic/src/lib.rs
Lines 59 to 67 in ab694b0
oxc/crates/oxc_semantic/src/node.rs
Lines 103 to 111 in ab694b0
oxc/crates/oxc_semantic/src/node.rs
Lines 12 to 15 in ab694b0
oxc/crates/oxc_ast/src/generated/ast_kind.rs
Lines 182 to 348 in ab694b0
So the end result is that
SemanticBuilder::build
borrows theProgram
only for the duration ofbuild
(i.e. the borrow ends whenbuild
returns). But it's returningSemanticBuilderReturn<'a>
which ultimately contains a&'a Program<'a>
. i.e. the&Program
in input params only guarantees theProgram
lives for'very_short_time
but then it returns a reference&'a Program
which it claims will live as long as the allocator.So nothing stops you dropping the
Program
while retaining a reference to it = use after free.Solution for
SemanticBuilder
In the case of
SemanticBuilder
, I think the fix is fairly simple. We can makeSemanticBuilder::build
take a&'a Program<'a>
. This makes the lifetime extension inVisit::alloc
valid, and removes the unsoundness.See #8437 (comment).
Broader solution
The problem is wider than just
SemanticBuilder
, though.Visit::alloc
is, in general, unsound. And that meansVisit::enter_node
andVisit::leave_node
are unsound too.The question is how to fix it in a way that's ergonomic.
The "correct" approach is to add a 2nd lifetime to
AstKind
:But then this probably also requires a 2nd lifetime on
Visit
:or maybe we can avoid that by putting the lifetime on
Visit::enter_node
instead:Either way, this is not at all ideal. In the linter (main user of
AstKind
), the current single-lifetimeAstKind<'a>
is fine, as linter immutably borrows the whole AST for the duration of it's operation, and introducing a 2nd lifetime complicates all that code for no gain.Adding a 2nd lifetime to
Visit
would be really annoying. We useVisit
in a lot of places, and most of them don't useAstKind
orenter_node
/leave_node
so, again, it's a complication for no gain.I'm not entirely sure of solution, but maybe we could:
AstKind
a linter-only thing.Visit::enter_node
+leave_node
. Or make them receive anAstType
instead ofAstKind
, likeVisitMut::enter_node
does -AstType
has no lifetime at all.SemanticBuilder
can createAstKind
s itself, and handle lifetimes correctly internally.In any case,
SemanticBuilder
should not be usingenter_node
/leave_node
because it hurts performance (oxc-project/backlog#72). But that refactor will be a fairly large task.Battle plan
I suggest:
SemanticBuilder::build
take a&'a Program<'a>
.enter_node
from the codebase as much as possible (it's not used much, and most usages outside ofSemanticBuilder
can be removed with simple refactoring).The text was updated successfully, but these errors were encountered: