Skip to content

Commit b0ed0b1

Browse files
committed
test(appsec): fix APPSEC-68682 regression test correctness
- Use serverResponseBodyRuleset() (match_regex on server.response.body) instead of ARACHNI_ATOM_V2_1: the new ruleset makes check_new_rule_targets() return true so eval_rules() traverses the nested PWArgsSegments in the ephemeral arena, reproducing the actual APPSEC-68682 crash pattern - Remove 6 unnecessary (Map<String,Object>) casts on Groovy map literals - Add assertThat(result.result, is(Waf.Result.OK)) per iteration - Change @SuppressWarnings to single-string form matching sibling tests - Use 3 GC threads to maximise ZGC concurrent cycle frequency - Add arenas.clear() in finally to avoid stale bytes across tests
1 parent e0f0ec3 commit b0ed0b1

1 file changed

Lines changed: 59 additions & 16 deletions

File tree

src/test/groovy/com/datadog/ddwaf/ReachabilityFenceTest.groovy

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -246,49 +246,62 @@ class ReachabilityFenceTest implements WafTrait {
246246
* raw pointers into them.
247247
*
248248
* Distinct from APPSEC-62784 (StringsSegment freed via run()) but same root cause and fix.
249+
*
250+
* Requires testGCRace task: relies on -XX:-TieredCompilation -XX:CompileThreshold=1 so
251+
* WafContext.run() reaches C2 immediately without a warmup phase.
249252
*/
250253
@Test
251-
@SuppressWarnings(['ExplicitGarbageCollection', 'BusyWait'])
254+
@SuppressWarnings('ExplicitGarbageCollection')
252255
void 'ephemeral arena with nested map survives concurrent GC (APPSEC-68682)'() {
253-
wafDiagnostics = builder.addOrUpdateConfig('test', ARACHNI_ATOM_V2_1)
256+
wafDiagnostics = builder.addOrUpdateConfig('test', serverResponseBodyRuleset())
257+
assert wafDiagnostics.numConfigOK == 1, "Rule must load: ${wafDiagnostics.allErrors}"
254258
handle = builder.buildWafHandleInstance()
255259
context = new WafContext(handle)
256260
runBudget = 60_000_000L
257261
258262
// Simulate ObjectIntrospection.convert() output for a streaming SSE response POJO.
259263
// Nested structure forces multiple PWArgsSegment allocations in the ephemeral arena.
260-
Map<String, Object> ephemeralData = (Map<String, Object>) [
261-
'server.response.body': (Map<String, Object>) [
262-
field1: (Map<String, Object>) [
264+
// match_regex on server.response.body triggers eval_rules() which traverses all nested
265+
// MAP entries, reading the PWArgsSegments that are freed without the leaseFenceSink fix.
266+
def ephemeralData = [
267+
'server.response.body': [
268+
field1: [
263269
subA: 'value1',
264270
subB: null,
265-
subC: (Map<String, Object>) [deepA: 'deep1', deepB: null, deepC: 'deep3'],
271+
subC: [deepA: 'deep1', deepB: null, deepC: 'deep3'],
266272
],
267273
field2: null,
268-
field3: (Map<String, Object>) [
274+
field3: [
269275
subD: 'value2',
270-
subE: (Map<String, Object>) [x: '1', y: '2', z: '3'],
276+
subE: [x: '1', y: '2', z: '3'],
271277
],
272278
field4: 'scalar',
273-
field5: (Map<String, Object>) [nestedA: 'a', nestedB: 'b'],
279+
field5: [nestedA: 'a', nestedB: 'b'],
274280
],
275281
]
276282
277283
def keepRunningGc = new AtomicBoolean(true)
278-
def gcThread = Thread.startDaemon('gc-pressure-ephemeral') {
279-
while (keepRunningGc.get()) {
280-
System.gc()
281-
Thread.sleep(1)
284+
def gcThreads4 = (0..<3).collect { int n ->
285+
Thread.startDaemon("gc-pressure-ephemeral-${n}") {
286+
while (keepRunningGc.get()) {
287+
System.gc()
288+
Thread.sleep(1)
289+
}
282290
}
283291
}
284292
285293
try {
286-
500.times {
287-
context.runEphemeral(ephemeralData, limits, metrics)
294+
500.times { int i ->
295+
def result = context.runEphemeral(ephemeralData, limits, metrics)
296+
assertThat(
297+
"Iteration ${i}: expected DDWAF_OK (regex never matches response body)",
298+
result.result,
299+
is(Waf.Result.OK))
288300
}
289301
} finally {
290302
keepRunningGc.set(false)
291-
gcThread.join(1000)
303+
gcThreads4.each { it.join(1000) }
304+
ByteBufferSerializer.ArenaPool.INSTANCE.arenas.clear()
292305
}
293306
}
294307
@@ -354,6 +367,36 @@ class ReachabilityFenceTest implements WafTrait {
354367
]
355368
}
356369
370+
/**
371+
* Minimal ruleset targeting server.response.body with a match_regex that never matches.
372+
* The rule evaluator traverses all nested MAP entries at server.response.body, reading
373+
* the ephemeral PWArgsSegments that are freed without the leaseFenceSink fix (APPSEC-68682).
374+
*/
375+
static Map serverResponseBodyRuleset() {
376+
[
377+
version : '2.1',
378+
metadata: [rules_version: '1.0.0'],
379+
rules : [
380+
[
381+
id : 'test-appsec-68682-body-inspection',
382+
name : 'Response body inspection (APPSEC-68682 regression)',
383+
tags : [type: 'test', category: 'test', module: 'waf'],
384+
conditions: [
385+
[
386+
operator : 'match_regex',
387+
parameters: [
388+
inputs : [[address: 'server.response.body']],
389+
regex : 'IMPOSSIBLE_APPSEC68682_MARKER',
390+
options: [case_sensitive: true, min_length: 30],
391+
],
392+
]
393+
],
394+
transformers: [],
395+
],
396+
],
397+
]
398+
}
399+
357400
/**
358401
* The minimal input bundle that triggers the crash: the 9 addresses published by
359402
* GatewayBridge.maybePublishRequestData() on every HTTP request.

0 commit comments

Comments
 (0)