Skip to content

Fix crash when remote config replaces the WAF handle during context creation - #12770

Open
claponcet wants to merge 2 commits into
masterfrom
clara.poncet/appsec-70090-waf-handle-lifetime
Open

claponcet wants to merge 2 commits into
masterfrom
clara.poncet/appsec-70090-waf-handle-lifetime

Conversation

@claponcet

@claponcet claponcet commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

What Does This Do

Stops native context creation from racing with the remote-config thread destroying the previous WAF handle.

  • Every WAF context is now created under the read lock of a per-snapshot StampedLock, and the remote-config thread holds the write lock while it closes the replaced WafHandle. Handle destruction therefore waits for any in-flight ddwaf_context_init on that handle.
  • A request callback that captured the CtxAndAddresses snapshot before a swap re-reads the current one and retries, so a context is never created from a retired handle.
  • Once a request has its WAF context, later WAF/RASP calls fetch it through the new lock-free AppSecRequestContext.getWafContextIfReady instead of locking. The method declines (and the caller falls back to getOrCreateWafContext) while the context is missing, closed, or the RASP/WAF metrics that call needs have not been created yet. Existing contexts own their native ruleset, so they keep evaluating after their handle is replaced.

Motivation

A customer JVM on musl crashed with SIGSEGV (si_code SI_KERNEL, si_addr 0x0) in ddwaf_destroy, called from WAFModule.initOrUpdateWafHandle on the remote-config thread.

The native initWafContext in libddwaf-java reads the handle pointer without taking WafHandle's lock or checking whether it is still online, while WafHandle.close() destroys it. When a request creates its context while remote config replaces the ruleset, it can either:

  • copy the ruleset shared_ptr after ddwaf_destroy released the last reference, so the ruleset is destroyed a second time when that context closes; or
  • dereference a ddwaf::waf that has already been freed.

Both corrupt the heap. musl's allocator aborts on that corruption with hlt, which the kernel reports exactly as the signal in the crash report.

The race reproduces on v1.62.0 (libsqreen 17.3.0) and on current master, on x86_64 musl with JDK 25. An LD_PRELOAD tracer on the libddwaf entry points showed every crash was a context creation overlapping a destroy of the same handle. With this change, 17.35 M traced context creations across 225 K handle replacements showed no such overlap, on both libsqreen 17.3.0 and 17.5.0.

Additional Notes

  • The steady-state path is lock-free. Before this change, every WAF/RASP call went through synchronized (reqCtx) in getOrCreateWafContext. The shared lock is only taken on a request's first WAF call, and on its first RASP call when metrics are enabled.
  • StampedLock instead of ReentrantReadWriteLock avoids per-thread read-hold bookkeeping, and it does not pin virtual-thread carriers.
  • The longer-term fix belongs in libddwaf-java: WafContext(WafHandle) should take the handle's own read lock.
  • WAFModuleSpecification changes are expectation-only: explicit getWafContextIfReady counts, and the context reuse made explicit. The new behaviour is covered by JUnit 5 tests in WAFModuleHandleReloadRaceTest and AppSecRequestContextWafContextRaceTest.

Contributor Checklist

Jira ticket: APPSEC-70090

🤖 Generated with Claude Code

…reation

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@claponcet claponcet added type: bug fix Bug fix comp: asm waf Application Security Management (WAF) tag: ai generated Largely based on code generated by an AI or LLM labels Oct 7, 2026
@datadog-official

This comment has been minimized.

@claponcet
claponcet marked this pull request as ready for review October 8, 2026 16:54
@claponcet
claponcet requested a review from a team as a code owner October 8, 2026 16:54
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-08T16:57:20.164613Z 5cfd892 Draft marked ready
🔒 Security Review ✅ Completed 2026-10-08T16:58:26.489990Z 5cfd892 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@datadog-official datadog-official Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bits Code Review: PASS

More details

Handle retirement waits for context creation, and stale callbacks retry with the current snapshot. The completed static review found that context reuse preserves request-close handling and metrics initialization.

Was this helpful? React 👍 or 👎

Open Bits AI session

🤖 Bits Code Review · Commit 5cfd892 · @DataDog review to ask questions

final Collection<Address<?>> addressesOfInterest;
final WafHandle ctx;

/** Read-held while creating a context from {@link #ctx}; write-held while closing it. */

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really nice investigation, thanks for the detailed write-up! The description mentions that the long-term fix belongs in libddwaf-java (WafContext(WafHandle) taking the handle's read lock). From a quick look at libddwaf-java master it seems that initWafContext still reads nativeHandle without the handle lock, so I think this workaround could be with us for a while. What do you think about opening a ticket/issue for it and referencing it from the handleLock Javadoc (something like "Workaround for libddwaf-java#NNN, can be removed once WafContext takes the handle read lock")? That way it might be easier to remember to clean this up later. Would that make sense to you?

final WafHandle ctx;

/** Read-held while creating a context from {@link #ctx}; write-held while closing it. */
final StampedLock handleLock = new StampedLock();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small thought: since StampedLock is not reentrant, it seems that if something inside the read-locked section ever re-entered doRunWaf on the same thread while the RC thread is waiting for the write lock, it could deadlock. As far as I can see this can't happen today, because the section only builds a WafContext under synchronized(reqCtx). Do you think it would be worth adding a short note to the Javadoc saying this section should stay callback-free? Totally optional, just in case it helps future readers.

WafContext wafContext = reqCtx.getWafContextIfReady(wafMetricsEnabled, gwCtx.isRasp);
if (wafContext == null) {
for (; ; ) {
long stamp = ctxAndAddr.handleLock.readLock();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be missing something here, but it seems that the write lock is only taken after the CAS has already retired the snapshot. If so, a failed tryReadLock() would already tell us that ctxAndAddr is stale. Would it make sense to use tryReadLock() and re-read ctxAndAddresses when it returns 0? I think that could keep request threads from parking while the RC thread runs ddwaf_destroy. The window looks quite small though, so happy to leave it as is if you think it's not worth it. What do you think?

Comment on lines 325 to 328
// WAF context closed concurrently between the isWafContextClosed() check and context
// creation; skip
// (APPSEC-69085). raspRuleEval() was already counted above, so don't also count
// raspRuleSkipped() here - that counter is reserved for calls that never attempted eval.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tiny nit: it looks like this comment change might not be related to the fix, and the reflow seems to have left creation; skip on its own line before // (APPSEC-69085). Would you mind either reverting it or re-wrapping it as a single paragraph? Feel free to ignore if I'm misreading it.

@dd-octo-sts

dd-octo-sts Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

🟡 Java Benchmark SLOs — Performance SLO warning (near threshold)

Suite Status
Startup 🟡 warning

SLO thresholds are defined here based on automatically generated metrics. A warning is raised when results are within 5% of the threshold.

PR vs. master results
Scenario Candidate master Δ (95% CI of mean)
startup:insecure-bank:iast:Agent 14.03 s 14.03 s [-0.6%; +0.6%] (no difference)
startup:insecure-bank:tracing:Agent 12.98 s 13.08 s [-1.4%; -0.2%] (maybe better)
startup:petclinic:appsec:Agent 17.88 s 17.81 s [-0.5%; +1.3%] (no difference)
startup:petclinic:iast:Agent 17.52 s 17.67 s [-1.6%; -0.1%] (maybe better)
startup:petclinic:profiling:Agent 17.70 s 17.29 s [+1.1%; +3.7%] (significantly worse)
startup:petclinic:sca:Agent 17.90 s 17.73 s [+0.0%; +1.9%] (maybe worse)
startup:petclinic:tracing:Agent 16.66 s 16.89 s [-2.4%; -0.3%] (maybe better)

Commit: eef22e30 · CI Pipeline · Benchmarking Platform UI


Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp: asm waf Application Security Management (WAF) tag: ai generated Largely based on code generated by an AI or LLM type: bug fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants