You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
So the end result is that SemanticBuilder::build borrows the Program only for the duration of build (i.e. the borrow ends when build returns). But it's returning SemanticBuilderReturn<'a> which ultimately contains a &'a Program<'a>. i.e. the &Program in input params only guarantees the Program 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 make SemanticBuilder::build take a &'a Program<'a>. This makes the lifetime extension in Visit::alloc valid, and removes the unsoundness.
The problem is wider than just SemanticBuilder, though. Visit::alloc is, in general, unsound. And that means Visit::enter_node and Visit::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:
/// `'a` is lifetime of the AST nodes./// `'r` is lifetime of the *references* to the AST nodes.enumAstKind<'a,'r>{BooleanLiteral(&'rBooleanLiteral),NullLiteral(&'rNullLiteral),NumericLiteral(&'rNumericLiteral<'a>),// ...}
But then this probably also requires a 2nd lifetime on Visit:
Either way, this is not at all ideal. In the linter (main user of AstKind), the current single-lifetime AstKind<'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 use Visit in a lot of places, and most of them don't use AstKind or enter_node / leave_node so, again, it's a complication for no gain.
I'm not entirely sure of solution, but maybe we could:
Make AstKind a linter-only thing.
Remove Visit::enter_node + leave_node. Or make them receive an AstType instead of AstKind, like VisitMut::enter_node does - AstType has no lifetime at all.
SemanticBuilder can create AstKinds itself, and handle lifetimes correctly internally.
In any case, SemanticBuilder should not be using enter_node / leave_node because it hurts performance (oxc-project/oxc#18098). But that refactor will be a fairly large task.
In meantime, try to remove use of enter_node from the codebase as much as possible (it's not used much, and most usages outside of SemanticBuilder can be removed with simple refactoring).
oxc-project/oxc#8437 surfaced unsoundness in
SemanticBuilder- a use after free bug.@bluurryy also hit this previously while working on oxc-project/oxc#6668 (discussed on Discord).
The root cause of that problem is the lifetime extension in
Visit::alloc(which all thewalk_*methods call to createAstKinds).https://github.com/oxc-project/oxc/blob/ab694b064a6e7ff334c9e7e1a12245df5265a68f/crates/oxc_ast/src/generated/visit.rs#L42-L47
The problem
Looking at how the lifetime extension causes oxc-project/oxc#8437:
SemanticBuilder::buildtakes a&Program<'a>(unspecified lifetime on the&borrow ofProgram) and returnsSemanticBuilderReturn<'a>:https://github.com/oxc-project/oxc/blob/ab694b064a6e7ff334c9e7e1a12245df5265a68f/crates/oxc_semantic/src/builder.rs#L226
Tracing the effect of this through the different types:
SemanticBuilderReturn<'a>containsSemantic<'a>.Semantic<'a>containsAstNodes<'a>.AstNodes<'a>contains aVecofAstNode<'a>.AstNode<'a>containsAstKind<'a>.AstKind<'a>contains&'arefs to AST nodes (including&'a Program<'a>).https://github.com/oxc-project/oxc/blob/ab694b064a6e7ff334c9e7e1a12245df5265a68f/crates/oxc_semantic/src/builder.rs#L114-L117
https://github.com/oxc-project/oxc/blob/ab694b064a6e7ff334c9e7e1a12245df5265a68f/crates/oxc_semantic/src/lib.rs#L59-L67
https://github.com/oxc-project/oxc/blob/ab694b064a6e7ff334c9e7e1a12245df5265a68f/crates/oxc_semantic/src/node.rs#L103-L111
https://github.com/oxc-project/oxc/blob/ab694b064a6e7ff334c9e7e1a12245df5265a68f/crates/oxc_semantic/src/node.rs#L12-L15
https://github.com/oxc-project/oxc/blob/ab694b064a6e7ff334c9e7e1a12245df5265a68f/crates/oxc_ast/src/generated/ast_kind.rs#L182-L348
So the end result is that
SemanticBuilder::buildborrows theProgramonly for the duration ofbuild(i.e. the borrow ends whenbuildreturns). But it's returningSemanticBuilderReturn<'a>which ultimately contains a&'a Program<'a>. i.e. the&Programin input params only guarantees theProgramlives for'very_short_timebut then it returns a reference&'a Programwhich it claims will live as long as the allocator.So nothing stops you dropping the
Programwhile retaining a reference to it = use after free.Solution for
SemanticBuilderIn the case of
SemanticBuilder, I think the fix is fairly simple. We can makeSemanticBuilder::buildtake a&'a Program<'a>. This makes the lifetime extension inVisit::allocvalid, and removes the unsoundness.See oxc-project/oxc#8437 (comment).
Broader solution
The problem is wider than just
SemanticBuilder, though.Visit::allocis, in general, unsound. And that meansVisit::enter_nodeandVisit::leave_nodeare 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_nodeinstead: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
Visitwould be really annoying. We useVisitin a lot of places, and most of them don't useAstKindorenter_node/leave_nodeso, again, it's a complication for no gain.I'm not entirely sure of solution, but maybe we could:
AstKinda linter-only thing.Visit::enter_node+leave_node. Or make them receive anAstTypeinstead ofAstKind, likeVisitMut::enter_nodedoes -AstTypehas no lifetime at all.SemanticBuildercan createAstKinds itself, and handle lifetimes correctly internally.In any case,
SemanticBuildershould not be usingenter_node/leave_nodebecause it hurts performance (oxc-project/oxc#18098). But that refactor will be a fairly large task.Battle plan
I suggest:
SemanticBuilder::buildtake a&'a Program<'a>.enter_nodefrom the codebase as much as possible (it's not used much, and most usages outside ofSemanticBuildercan be removed with simple refactoring).