Skip to content

[P2] Do not report an atomic store that was never built from its read - #1193

Merged
PIsberg merged 5 commits into
mainfrom
fix/atomic-get-set-data-flow
Nov 5, 2026
Merged

PIsberg merged 5 commits into
mainfrom
fix/atomic-get-set-data-flow

Conversation

@PIsberg

@PIsberg PIsberg commented Nov 5, 2026 •

Copy link
Copy Markdown
Owner

Why

AtomicNonAtomicUpdate reported a get followed by a plain store to the same atomic even when the
stored value came from unrelated code. gauge.get() for a log line, then gauge.set(queue.size())
cannot lose an update, but the weaver sent every store to the hook that reports one, so correct code
was flagged (#1187).

What

  • The agent reads each woven class's bytes from its loader and runs a per-method taint analysis
    (AtomicStoreTaintAnalysis). For each atomic store it records whether the stored value was derived
    from an atomic read in the same method.
  • A store that was not derived is woven through a twin hook named <hook>Unrelated. The twin reports
    the store as an update with no read before it. Twins exist for set, lazySet, setPlain, setOpaque and
    setRelease on AtomicInteger, and for set, lazySet and setRelease on AtomicLong. Each twin keeps the
    original's point and spin-lock release.
  • Stores are matched to verdicts by ordinal within their method, using one predicate shared by the
    analysis and the weaver. A store the analysis cannot reach gets the conservative verdict, so later
    stores keep their ordinals.
  • A class that cannot be read, or cannot be analysed, keeps every store on the old hook.

Verification

  • AtomicUnrelatedStoreWeavingEndToEndTest (CI=true) was red before this change with
    Expected no finding from detector 'AtomicNonAtomicUpdate', but got 1 (75 get+set sequences) and is
    green now. It now includes a lazySet case.
  • AtomicDerivedStoreWeavingEndToEndTest, the positive control, is still green.
  • AtomicStoreTaintAnalysisTest (7 tests, one built with ASM for a dead store after return),
    AtomicStoreTwinHooksTest, SubstitutionHooksArePointsTest and ControlledScheduleHookPointsTest
    pass.
  • Fast tier: 3905 library tests and 106 agent tests, zero failures. ArchitectureTest passes.

Not verified: the lazySet end-to-end case has not been shown red without its twin, and the cost of
re-reading the class bytes has not been measured.

Docs

  • docs/CHANGELOG.md: Unreleased, Fixed entry with the known limit.
  • Javadoc on the changed code (AtomicStoreTaintAnalysis, FieldAccessWeaver.visitor, the weaver
    branch, the twin hooks, AsyncTestAgent.storeVerdicts).
  • Not updated: the @AICore note on AsyncTestAgent still lists the store forms as they were. It is
    generated from the annotation, so it is left for the next vibetags regeneration.

Follow-ups

Fixes #1187

🤖 Generated with Claude Code

CI fixes on this branch

  • f702273c4 gives the Gradle agent build the ASM artifacts Maven already declares and relocates them in its shadow jar. Without it, :async-test-agent:compileJava failed in the Gradle Test Suite job. Verified locally: compileJava passes, and the shadow jar has 98 shaded ASM entries and none under org/objectweb.
  • a685f3999 removes a PMD UnusedNullCheckInEquals violation in AtomicStoreTaintAnalysis.isAtomicStore, which failed the Maven verify gate in Corpus Eval.
  • Corpus Eval failed once on Java 26 with agent_jctoolsHandOff_offererWritesAfterTheOffer silent, the known flake in [P3] Flaky: corpus agent-pairs row agent_jctoolsHandOff_offererWritesAfterTheOffer silent once #1133. Its re-run passed that gate. Other red jobs in this PR were GitHub infrastructure: a checkout that could not reach github.com, a Maven Central plugin that could not be fetched, and a runner that lost communication. Each was re-run and passed.

PIsberg and others added 5 commits November 4, 2026 22:49
…1187)

A get whose value is only logged, then a set from queue.size(), is correct, but the
woven pair reports it as a PROMPT on AtomicNonAtomicUpdate. The end-to-end test fails
on that finding (red, verified with CI=true mvn -pl async-test-agent -am test). The
positive control, a set built from the read (counter.set(v + 1)), passes and must stay
green.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
… from a read or not (#1187)

The woven get-then-set rule reports a store from an unrelated size as a lost update,
because the hook sees calls and not values. This adds the proof of concept for telling
them apart from the class's own bytes: a taint pass over ASM's analysis of each method,
where a bit marks what came from an atomic read. Verified by AtomicStoreTaintAnalysisTest:
the unrelated store is not derived, a read-built store is derived, a two-word (long)
value keeps its taint through the add, a constant store after a dropped read is not
derived, a read reaching a store on one branch is derived, and a read of one atomic
stored into another is derived by the conservative rule. Not yet wired into the weaver,
so the end-to-end false positive is still red.

Adds org.ow2.asm:asm-tree and asm-analysis at the root's asm.version, shaded into the
agent jar with org.objectweb.asm relocated, since Byte Buddy's shaded ASM has no tree
or analysis package. Verified: the 1.14.1 agent jar holds 98 relocated ASM entries and
no unrelocated org/objectweb entries; the fast tier passes (lib 3905, agent 103).

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
…ts read (#1187)

AtomicNonAtomicUpdate reported a get followed by a plain store to the same
atomic even when the stored value came from unrelated code. gauge.get() for a
log line, then gauge.set(queue.size()), cannot lose an update, but the weaver
routed every store to the hook that reports one, so correct code was flagged.

The agent now reads each class's bytes from its loader, runs a taint analysis
per method (AtomicStoreTaintAnalysis), and says for each atomic store whether
the stored value was derived from an atomic read in that method. A store that
was not derived is woven through a twin hook (<hook>Unrelated) that reports it
as an update with no read before it. Covers set, lazySet, setPlain, setOpaque
and setRelease on AtomicInteger, and set, lazySet and setRelease on AtomicLong.
Stores are matched to verdicts by ordinal within the method, using one shared
predicate, and a store the analysis cannot reach keeps its old hook.

Verified: AtomicUnrelatedStoreWeavingEndToEndTest was red before this change
(75 get+set sequences reported) and is green with CI=true, now including a
lazySet case; AtomicDerivedStoreWeavingEndToEndTest, the positive control, is
still green. AtomicStoreTaintAnalysisTest (7, including a dead-store ordinal
case built with ASM), AtomicStoreTwinHooksTest, SubstitutionHooksArePointsTest
and ControlledScheduleHookPointsTest pass. Fast tier: 3905 library tests and
106 agent tests, zero failures.

Not done: the lazySet end-to-end case has not been shown red without its twin.
Re-reading the class costs a second parse of every woven class when fields=true;
not measured.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
…eady has (#1187)

AtomicStoreTaintAnalysis reads each class's atomic stores with unshaded ASM,
which the Maven POM declares (asm, asm-tree, asm-analysis) and relocates into
the agent jar. The Gradle build declared only Byte Buddy, so
:async-test-agent:compileJava failed with "package org.objectweb.asm does not
exist" in the Gradle Test Suite job.

The Gradle build now declares the same three artifacts at the version the root
build already reads from pom.xml (asm.version), and its shadowJar includes them
and relocates org.objectweb.asm to se.deversity.asynctest.agent.shaded.asm, as
the Maven shade does.

Verified locally: ./gradlew :async-test-agent:compileJava succeeds, and
:async-test-agent:shadowJar produces async-test-agent-1.14.1.jar with 98 shaded
ASM entries, no org/objectweb entries, and Byte Buddy still relocated.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
The Maven verify gate (maven-pmd-plugin check) failed the Corpus Eval job with
one violation: UnusedNullCheckInEquals at AtomicStoreTaintAnalysis:119. The
null check on the expected descriptor came before descriptor.equals(expected),
which PMD reads as a redundant null check.

The expected descriptor is now chosen from the owner after the owner is known
to be an AtomicInteger or AtomicLong, so it is never null and the check goes
away. The predicate's result is unchanged.

Verified locally: mvn -pl async-test-agent -am verify -DskipTests
-Djacoco.skip=true passes; before this change the same command reported the one
PMD violation above.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Nov 5, 2026

Copy link
Copy Markdown

@PIsberg
PIsberg merged commit 7f04997 into main Nov 5, 2026
65 of 71 checks passed
@PIsberg
PIsberg deleted the fix/atomic-get-set-data-flow branch November 5, 2026 17:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[P2] ATOMIC_NON_ATOMIC_UPDATE reports a get then set whose stored value does not depend on the read

1 participant