Skip to content
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

Fix unbounded recursion in Parser::parse_expression #1248

Merged
merged 3 commits into from
Jun 22, 2022

Conversation

evanrichter
Copy link
Contributor

fixes a stack overflow with malformed justfiles:

$ just -f <(python -c "print('A:(A' + '('*5000)")
thread 'main' has overflowed its stack
fatal runtime error: stack overflow
Aborted (core dumped)

@casey
Copy link
Owner

casey commented Jun 21, 2022

Nice! A few suggestions:

  • This should have a test that makes sure that the right error is returned when parsing a deep expression. You can create a new file tests/recursion_limit.rs, and put the test in there. Check out other tests that use the Test struct for ideas.
  • I think conceptually, it would be nicer to do this by starting with depth being 0, and increasing it whenever we recurse, and then checking if it's hit the limit instead of checking if it's reached zero.
  • Is it necessary to save and restore the previous depth? I think it should be enough to increment it, recurse, and then decrement it when recurse returns.

@evanrichter evanrichter force-pushed the fix-recursion-bounds branch from 85c3ef8 to ec5b8ce Compare June 21, 2022 16:15
@evanrichter
Copy link
Contributor Author

  • This should have a test that makes sure that the right error is returned when parsing a deep expression. You can create a new file tests/recursion_limit.rs, and put the test in there. Check out other tests that use the Test struct for ideas.
  • ...starting with depth being 0, and increasing it
  • It's ugly, but I got a test now :)
  • Sure, that makes sense!
  • Is it necessary to save and restore the previous depth? I think it should be enough to increment it, recurse, and then decrement it when recurse returns.

I did it that way more as a guard for programming errors. Since inside the body closure, self.depth can be modified (most likely would be a bug), and upon returning I thought I'd rather just restore it to what I know it should be. Now it's a simple inc/exec/dec and I tested some programming errors like adding or subtracting depth where you shouldn't, and the tests quickly blew up. So I think your way is better.

I also made the recursion depth u8::MAX for all compilation configurations. It makes cargo test agree with cargo test --release and I think 255 is plenty recursion even if release mode can technically handle more.

src/compile_error.rs Outdated Show resolved Hide resolved
src/compile_error_kind.rs Outdated Show resolved Hide resolved
src/parser.rs Outdated Show resolved Hide resolved
src/parser.rs Outdated Show resolved Hide resolved
const RECURSION_LIMIT_REACHED: &str = "
error: Recursion depth was exceeded
|
1 | foo: (x ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definitely the best justfile in the test suite.

@casey
Copy link
Owner

casey commented Jun 22, 2022

Added a bunch of comments. It looks like the stack is being overrun on windows.

One issue is that we're actually increasing the size of the stack with this change, I think by two frames per expression. We'll have an extra frame for Parser::recurse, and another frame for the closure. This is probably what's causing the windows stack to overflow.

I think we should switch to just incrementing self.recursion_depth at the beginning of Parser::parse_expression, parsing the expression and saving it to a variable, and then decrementing it before returning. We don't even really need to handle the case in which parsing fails, because in that case parsing will stop. Als

@evanrichter evanrichter force-pushed the fix-recursion-bounds branch from ebe97e1 to 3a30979 Compare June 22, 2022 14:25
@evanrichter
Copy link
Contributor Author

I applied all the rewording suggestions, shortened the greatest test justfile by an order of magnitude, and inlined the recursion check.

Windows test is still failing with stack overflow :(

I'll try reducing the limit from 255 to 128

@evanrichter evanrichter force-pushed the fix-recursion-bounds branch 3 times, most recently from d98d8d1 to 9ed8248 Compare June 22, 2022 15:07
@evanrichter
Copy link
Contributor Author

decreased again to 64 for windows

If this goes on much longer, maybe we should run tests in release mode?

src/parser.rs Outdated
@@ -391,7 +394,16 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {

/// Parse an expression, e.g. `1 + 2`
fn parse_expression(&mut self) -> CompileResult<'src, Expression<'src>> {
if self.accepted_keyword(Keyword::If)? {
if self.depth == 64 {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about we just drop it down for windows:

Suggested change
if self.depth == 64 {
if self.depth == if cfg!(windows) { 64 } else { 256 } {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this change is in the newest push (though 255 not 256)

@casey
Copy link
Owner

casey commented Jun 22, 2022

There are some question marks in the parse_expression function which will bypass decrementing the recursion depth, which is fine, but let's make it consistent, i.e. we don't save a result, just the expression:

    let expression = if self.accepted_keyword(Keyword::If)? {
      self.parse_conditional()?
    } else {
      let value = self.parse_value()?;
      if self.accepted(Plus)? {
        let lhs = Box::new(value);
        let rhs = Box::new(self.parse_expression()?);
        Expression::Concatenation { lhs, rhs }
      } else {
        value
      }
    };

    Ok(expression)

* inline recursion check
* rename recursion depth error
* reword recursion error message
* shorten recursion test
* windows recursion is capped at 64, other platforms at 255
@evanrichter evanrichter force-pushed the fix-recursion-bounds branch from 9ed8248 to ee7cb18 Compare June 22, 2022 21:51
@casey casey merged commit bfceb8f into casey:master Jun 22, 2022
@casey
Copy link
Owner

casey commented Jun 22, 2022

Nice, merged! Thank you very much for finding and fixing this 🙇‍♂️

@casey casey mentioned this pull request Jun 30, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants