-
Notifications
You must be signed in to change notification settings - Fork 441
Restrict the maximum nesting level in the parser to avoid stack overflows #1030
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
Restrict the maximum nesting level in the parser to avoid stack overflows #1030
Conversation
@swift-ci Please test |
@swift-ci Please test macOS |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is awesome, thank you!
|
||
/// A default maximum nesting level that is used if the client didn't | ||
/// explicitly specify one. | ||
public static let defaultMaximumNestingLevel = 256 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know it's terrible, but should we do some kind of #if DEBUG
check to decide whether to use the lower limit or higher limit?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That’s actually a good idea. The entire check is pretty horrible in itself, so a #if DEBUG
check doesn’t make a huge difference anymore.
Sources/SwiftParser/TopLevel.swift
Outdated
@@ -13,6 +13,27 @@ | |||
@_spi(RawSyntax) import SwiftSyntax | |||
|
|||
extension Parser { | |||
/// Consumes and returns all remaining tokens in the source file. | |||
mutating func consumeRemaingingTokens() -> [RawSyntax] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo Remainging
Sources/SwiftParser/TopLevel.swift
Outdated
|
||
/// If the maximum nesting level has been reached, return the remaining tokens in the source file | ||
/// as unexpected nodes that have the `isMaximumNestingLevelOverflow` bit set. | ||
/// Check this in places that are likley to cause deep recursion and if this returns non-nil, abort parsing. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo likley
|| fileURL.absoluteString.contains("_overflow") | ||
|| fileURL.absoluteString.contains("parser-cutoff.swift") | ||
} | ||
name: "Swift tests", path: testDir, checkDiagnostics: false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is so fantastic, thank you!
d323cbb
to
c3c7599
Compare
@swift-ci Please test |
c3c7599
to
213ddc6
Compare
@swift-ci Please test |
The basic idea is that we start a new nesting level whenever we consume or synthesize an opening bracket (
<
,{
,(
,[
or#if
) and reduce it when we read the matching closing bracket. This should be a pretty simple indicator for the current nesting level that doesn’t require custom logins in productions. IMO the benefit here is that this notion of nesting level is easy to describe and explain.Productions that are known to be able to overflow (I just took the ones that introduce
StructureMarkerRAII
in the C++ parser) can checkremainingTokensIfMaximumNestingLevelReached
and stop parsing if the maximum nesting level has been exceeded.It turns out that a nesting level of 256 (which is the current maximum nesting level in the C++ parser) is totally fine in Release builds of the parser, but debug builds fail with nesting levels > 30-ish.
Since lookahead cannot really bail out to prevent stack overflow, I rewrote
skipSingle
andskipUntil
to work iteratively instead of recursively.