@@ -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