Skip to content

Commit 3a9e12a

Browse files
committed
Unified: Enforce JSON depth guard in SwiftSyntaxFFI
Adds a depth parameter to the serialisation and throws an error when the depth exceeds 2048. This replaces the previous scan that took place on the Rust side. For files that hit the limit, extraction will fail (obivously), and a message is written to stderr explaining the failure. In due course, we should probably consider adding a way to report that error back to the Rust side (currently it just behaves as if the parser failed for some reason), but for now I think the present solution is sufficient.
1 parent bd10083 commit 3a9e12a

3 files changed

Lines changed: 34 additions & 73 deletions

File tree

‎unified/extractor/src/languages/swift/adapter.rs‎

Lines changed: 0 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -58,53 +58,6 @@ const VARYING_TOKEN_KINDS: &[&str] = &[
5858
"unknown",
5959
];
6060

61-
/// Maximum structural nesting accepted in the serialized JSON tree.
62-
///
63-
/// This exceeds the deepest tree in the Swift corpus while bounding the
64-
/// recursive serde deserialization and AST construction that follow.
65-
const MAX_JSON_DEPTH: usize = 2048;
66-
67-
/// Check JSON structural depth without recursively parsing it.
68-
///
69-
/// Brackets and braces inside strings are ignored. Full JSON validation is
70-
/// still performed by serde_json afterward.
71-
fn check_json_depth(json: &str) -> Result<(), String> {
72-
let mut depth = 0;
73-
let mut in_string = false;
74-
let mut escaped = false;
75-
76-
for byte in json.bytes() {
77-
if in_string {
78-
if escaped {
79-
escaped = false;
80-
} else {
81-
match byte {
82-
b'\\' => escaped = true,
83-
b'"' => in_string = false,
84-
_ => {}
85-
}
86-
}
87-
} else {
88-
match byte {
89-
b'"' => in_string = true,
90-
b'{' | b'[' => {
91-
depth += 1;
92-
if depth > MAX_JSON_DEPTH {
93-
return Err(format!(
94-
"invalid JSON: nesting depth exceeds supported maximum \
95-
({MAX_JSON_DEPTH})"
96-
));
97-
}
98-
}
99-
b'}' | b']' => depth = depth.saturating_sub(1),
100-
_ => {}
101-
}
102-
}
103-
}
104-
105-
Ok(())
106-
}
107-
10861
/// Keys of a node object that carry metadata rather than a structural child.
10962
fn is_metadata_key(key: &str) -> bool {
11063
matches!(
@@ -373,7 +326,6 @@ const SWIFT_NODE_TYPES: &str = include_str!("../../../swift_node_types.yml");
373326
/// authoritative swift-syntax schema ([`SWIFT_NODE_TYPES`]); the adapter only
374327
/// ever consumes swift-syntax input, so the schema is not a parameter.
375328
pub fn json_to_ast(json: &str) -> Result<AdaptedTree, String> {
376-
check_json_depth(json)?;
377329
let mut deserializer = serde_json::Deserializer::from_str(json);
378330
deserializer.disable_recursion_limit();
379331
let root = Value::deserialize(&mut deserializer).map_err(|e| format!("invalid JSON: {e}"))?;
@@ -582,25 +534,6 @@ mod tests {
582534
json_to_ast(&json).expect("adapter should accept JSON nested beyond serde_json's default");
583535
}
584536

585-
#[test]
586-
fn rejects_json_beyond_supported_depth() {
587-
let json = deeply_nested_json(MAX_JSON_DEPTH);
588-
let error = match json_to_ast(&json) {
589-
Ok(_) => panic!("adapter should reject excessively nested JSON"),
590-
Err(error) => error,
591-
};
592-
assert!(
593-
error.contains("nesting depth exceeds supported maximum"),
594-
"{error}"
595-
);
596-
}
597-
598-
#[test]
599-
fn ignores_brackets_inside_json_strings_when_checking_depth() {
600-
check_json_depth(r#"{"text":"[[[{{{\\\""}"#)
601-
.expect("string contents should not contribute to JSON depth");
602-
}
603-
604537
#[test]
605538
fn collects_extras_into_side_channel() {
606539
// A token carrying a trailing line comment in its trivia.

‎unified/swift-syntax-rs/src/lib.rs‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,15 @@ mod tests {
136136
);
137137
}
138138

139+
#[test]
140+
fn rejects_excessively_deep_syntax_tree() {
141+
let source = format!("let x = a{}\n", ".a".repeat(2100));
142+
assert!(
143+
matches!(parse_to_json(&source), Err(ParseError::SwiftFailure)),
144+
"serialization should reject syntax trees beyond the supported depth"
145+
);
146+
}
147+
139148
#[test]
140149
fn serializes_json_strings_and_keys_deterministically() {
141150
let source = "/* quote \" slash / backslash \\ tab \t newline\n emoji 😀 combining e\u{301} control \u{1} */\nlet x = 1";

‎unified/swift-syntax-rs/swift/Sources/SwiftSyntaxFFI/SwiftSyntaxFFI.swift‎

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,22 @@ private func serializeTrivia(
8585
/// therefore simply a JSON array. This drops the collection node's own
8686
/// `kind`/location, which are unnamed and largely recoverable from the
8787
/// elements.
88-
private func serialize(_ node: Syntax) -> Any {
88+
private let maximumSerializationDepth = 2048
89+
90+
private struct SerializationDepthError: Error, CustomStringConvertible {
91+
var description: String {
92+
"syntax tree exceeds maximum serialization depth (\(maximumSerializationDepth))"
93+
}
94+
}
95+
96+
private func serialize(_ node: Syntax, depth: Int = 1) throws -> Any {
97+
if depth > maximumSerializationDepth {
98+
throw SerializationDepthError()
99+
}
100+
89101
if node.kind.isSyntaxCollection {
90-
return node.children(viewMode: .sourceAccurate).map {
91-
serialize($0)
102+
return try node.children(viewMode: .sourceAccurate).map {
103+
try serialize($0, depth: depth + 1)
92104
}
93105
}
94106

@@ -133,10 +145,10 @@ private func serialize(_ node: Syntax) -> Any {
133145
// parent (the same mechanism SwiftSyntax uses for its debug dump). A
134146
// child that is a collection serializes to an array (see above).
135147
if let keyPath = child.keyPathInParent, let name = childName(keyPath) {
136-
result[name] = serialize(child)
148+
result[name] = try serialize(child, depth: depth + 1)
137149
} else {
138150
// Defensive fallback for any unnamed layout child.
139-
result["child\(unnamed)"] = serialize(child)
151+
result["child\(unnamed)"] = try serialize(child, depth: depth + 1)
140152
unnamed += 1
141153
}
142154
}
@@ -311,7 +323,14 @@ public func ssr_parse_json(
311323
// converter built from the original tree maps the folded tree correctly.
312324
let folded = foldOperators(in: tree)
313325
let converter = SourceLocationConverter(fileName: "<input>", tree: tree)
314-
guard var json = serialize(folded) as? [String: Any] else {
326+
let serialized: Any
327+
do {
328+
serialized = try serialize(folded)
329+
} catch {
330+
writeToStandardError("SwiftSyntaxFFI: JSON serialization failed: \(error)\n")
331+
return nil
332+
}
333+
guard var json = serialized as? [String: Any] else {
315334
return nil
316335
}
317336
json["$lineStarts"] = lineStarts(converter)

0 commit comments

Comments
 (0)