Skip to content

Commit 3b2df40

Browse files
authored
fix: prevent CPU runaway in HttpNodesStatsAction JSON parsing (#18)
* fix: prevent CPU runaway in HttpNodesStatsAction JSON parsing (#18) Three parse methods introduced in #14 were missing `parser.nextToken()` before calling `consumeObject()` or sub-parse methods when the current token was START_OBJECT. This caused `consumeObject()` to consume tokens beyond its scope (including sibling fields), corrupting the parser state and eventually leading to an infinite loop at EOF — spinning all eshttp threads at 100% CPU each. Fixes: - Add `parser.nextToken()` in parseSearchBackpressureStats, parseTaskCancellationStats, and parseSearchPipelineStats - Add null-token (EOF) guards in fromXContent, parseNodes, parseNodeStats, and consumeObject to throw IOException instead of spinning - Add START_ARRAY handling in consumeObject - Remove orphan `new ArrayList<>()` in parseNodeStats Tests: add 61 unit tests covering token boundary verification, field ordering, multiple nodes, partial sub-fields, deeply nested structures, truncated JSON, and concurrent parsing. * fix: handle initial parser state and reduce test log output - Fix fromXContent to call nextToken() when parser is uninitialized (currentToken is null before first read), preventing false EOF detection that broke integration tests - Change test log level from ALL to INFO in 5 integration test classes to reduce output from ~30k lines to ~1k lines for GitHub Actions
1 parent 005ad1c commit 3b2df40

7 files changed

Lines changed: 1064 additions & 11 deletions

File tree

src/main/java/org/codelibs/fesen/client/action/HttpNodesStatsAction.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,14 @@ protected NodesStatsResponse fromXContent(final XContentParser parser) throws IO
157157
List<NodeStats> nodes = Collections.emptyList();
158158
String fieldName = null;
159159
ClusterName clusterName = ClusterName.DEFAULT;
160+
if (parser.currentToken() == null) {
161+
parser.nextToken();
162+
}
160163
XContentParser.Token token;
161164
while ((token = parser.currentToken()) != XContentParser.Token.END_OBJECT) {
165+
if (token == null) {
166+
throw new IOException("Unexpected end of JSON stream while parsing NodesStatsResponse");
167+
}
162168
if (token == XContentParser.Token.FIELD_NAME) {
163169
fieldName = parser.currentName();
164170
} else if (token == XContentParser.Token.START_OBJECT) {
@@ -181,6 +187,9 @@ protected List<NodeStats> parseNodes(final XContentParser parser) throws IOExcep
181187
String fieldName = null;
182188
XContentParser.Token token;
183189
while ((token = parser.currentToken()) != XContentParser.Token.END_OBJECT) {
190+
if (token == null) {
191+
throw new IOException("Unexpected end of JSON stream while parsing nodes");
192+
}
184193
if (token == XContentParser.Token.FIELD_NAME) {
185194
fieldName = parser.currentName();
186195
} else if (token == XContentParser.Token.START_OBJECT) {
@@ -193,7 +202,6 @@ protected List<NodeStats> parseNodes(final XContentParser parser) throws IOExcep
193202
}
194203

195204
protected NodeStats parseNodeStats(final XContentParser parser, final String nodeId) throws IOException {
196-
new ArrayList<>();
197205
String fieldName = null;
198206
String nodeName = "";
199207
long timestamp = 0;
@@ -230,6 +238,9 @@ protected NodeStats parseNodeStats(final XContentParser parser, final String nod
230238
XContentParser.Token token;
231239
TransportAddress transportAddress = new TransportAddress(TransportAddress.META_ADDRESS, 0);
232240
while ((token = parser.currentToken()) != XContentParser.Token.END_OBJECT) {
241+
if (token == null) {
242+
throw new IOException("Unexpected end of JSON stream while parsing node stats for " + nodeId);
243+
}
233244
if (token == XContentParser.Token.FIELD_NAME) {
234245
fieldName = parser.currentName();
235246
} else if (token == XContentParser.Token.START_OBJECT) {
@@ -399,6 +410,7 @@ protected SearchBackpressureStats parseSearchBackpressureStats(final XContentPar
399410
if (token == XContentParser.Token.FIELD_NAME) {
400411
fieldName = parser.currentName();
401412
} else if (token == XContentParser.Token.START_OBJECT) {
413+
parser.nextToken();
402414
if ("search_task".equals(fieldName) || "search_shard_task".equals(fieldName)) {
403415
consumeObject(parser);
404416
} else {
@@ -544,6 +556,7 @@ protected TaskCancellationStats parseTaskCancellationStats(final XContentParser
544556
if (token == XContentParser.Token.FIELD_NAME) {
545557
fieldName = parser.currentName();
546558
} else if (token == XContentParser.Token.START_OBJECT) {
559+
parser.nextToken();
547560
if ("search_task".equals(fieldName)) {
548561
searchTaskCancellationStats = parseSearchTaskCancellationStats(parser);
549562
} else if ("search_shard_task".equals(fieldName)) {
@@ -606,6 +619,7 @@ protected SearchPipelineStats parseSearchPipelineStats(final XContentParser pars
606619
if (token == XContentParser.Token.FIELD_NAME) {
607620
fieldName = parser.currentName();
608621
} else if (token == XContentParser.Token.START_OBJECT) {
622+
parser.nextToken();
609623
if ("total_request".equals(fieldName)) {
610624
totalRequestStats = parseOperationStats(parser);
611625
} else if ("total_response".equals(fieldName)) {
@@ -2322,9 +2336,14 @@ protected int[] parseNodeResults(final XContentParser parser) throws IOException
23222336
protected void consumeObject(final XContentParser parser) throws IOException {
23232337
XContentParser.Token token;
23242338
while ((token = parser.currentToken()) != XContentParser.Token.END_OBJECT) {
2339+
if (token == null) {
2340+
throw new IOException("Unexpected end of JSON stream while consuming object");
2341+
}
23252342
if (token == XContentParser.Token.START_OBJECT) {
23262343
parser.nextToken();
23272344
consumeObject(parser);
2345+
} else if (token == XContentParser.Token.START_ARRAY) {
2346+
parser.skipChildren();
23282347
}
23292348
parser.nextToken();
23302349
}

src/test/java/org/codelibs/fesen/client/Elasticsearch7ClientTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,10 @@ static void setUpAll() {
131131
static void setupLogger() {
132132
System.setProperty("java.util.logging.SimpleFormatter.format", "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS %4$s %2$s %5$s%6$s%n");
133133
final Logger rootLogger = Logger.getLogger("");
134-
rootLogger.setLevel(Level.ALL);
134+
rootLogger.setLevel(Level.INFO);
135135
final ConsoleHandler handler = new ConsoleHandler();
136136
handler.setFormatter(new SimpleFormatter());
137-
handler.setLevel(Level.ALL);
137+
handler.setLevel(Level.INFO);
138138
rootLogger.addHandler(handler);
139139
}
140140

src/test/java/org/codelibs/fesen/client/Elasticsearch8ClientTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,10 @@ static void setUpAll() {
130130
static void setupLogger() {
131131
System.setProperty("java.util.logging.SimpleFormatter.format", "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS %4$s %2$s %5$s%6$s%n");
132132
final Logger rootLogger = Logger.getLogger("");
133-
rootLogger.setLevel(Level.ALL);
133+
rootLogger.setLevel(Level.INFO);
134134
final ConsoleHandler handler = new ConsoleHandler();
135135
handler.setFormatter(new SimpleFormatter());
136-
handler.setLevel(Level.ALL);
136+
handler.setLevel(Level.INFO);
137137
rootLogger.addHandler(handler);
138138
}
139139

src/test/java/org/codelibs/fesen/client/OpenSearch1ClientTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,10 @@ static void setUpAll() {
131131
static void setupLogger() {
132132
System.setProperty("java.util.logging.SimpleFormatter.format", "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS %4$s %2$s %5$s%6$s%n");
133133
final Logger rootLogger = Logger.getLogger("");
134-
rootLogger.setLevel(Level.ALL);
134+
rootLogger.setLevel(Level.INFO);
135135
final ConsoleHandler handler = new ConsoleHandler();
136136
handler.setFormatter(new SimpleFormatter());
137-
handler.setLevel(Level.ALL);
137+
handler.setLevel(Level.INFO);
138138
rootLogger.addHandler(handler);
139139
}
140140

src/test/java/org/codelibs/fesen/client/OpenSearch2ClientTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,10 @@ static void setUpAll() {
131131
static void setupLogger() {
132132
System.setProperty("java.util.logging.SimpleFormatter.format", "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS %4$s %2$s %5$s%6$s%n");
133133
final Logger rootLogger = Logger.getLogger("");
134-
rootLogger.setLevel(Level.ALL);
134+
rootLogger.setLevel(Level.INFO);
135135
final ConsoleHandler handler = new ConsoleHandler();
136136
handler.setFormatter(new SimpleFormatter());
137-
handler.setLevel(Level.ALL);
137+
handler.setLevel(Level.INFO);
138138
rootLogger.addHandler(handler);
139139
}
140140

src/test/java/org/codelibs/fesen/client/OpenSearch3ClientTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,10 @@ static void setUpAll() {
131131
static void setupLogger() {
132132
System.setProperty("java.util.logging.SimpleFormatter.format", "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS %4$s %2$s %5$s%6$s%n");
133133
final Logger rootLogger = Logger.getLogger("");
134-
rootLogger.setLevel(Level.ALL);
134+
rootLogger.setLevel(Level.INFO);
135135
final ConsoleHandler handler = new ConsoleHandler();
136136
handler.setFormatter(new SimpleFormatter());
137-
handler.setLevel(Level.ALL);
137+
handler.setLevel(Level.INFO);
138138
rootLogger.addHandler(handler);
139139
}
140140

0 commit comments

Comments
 (0)