Skip to content

javasrc2cpg: Java 21-26 lowering, Java framework taggers and semantics - #182

Closed
prabhu wants to merge 2 commits into
mainfrom
java-21-26-support
Closed

prabhu wants to merge 2 commits into
mainfrom
java-21-26-support

Conversation

@prabhu

@prabhu prabhu commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Java support was lagging behind the other frontends in two places: the AstCreator dropped key Java 14-26 constructs into unknown nodes (breaking callgraphs, CFG, dataflow and reachable flows on modern code), and Java had no framework tagging at all - ChennaiTagsPass covered JS/Python/PHP/Ruby/C, and a Java graph needed a hand-written chennai.json to find any source or sink.

Changes

Modern Java lowering (AstCreator, Scope) - the bundled JavaParser already parses all of it; this PR lowers it:

  • Switch expressions (14) to nested <operator>.conditional, giving the JLS 14.11.2 arm-value dataflow through the declared conditional semantic; multi-label arms OR, when guards AND
  • yield as the arm value (plus an <operator>.yield call in stray statement position)
  • instanceof type patterns (16) and record patterns (21): bindings become locals + an assignment from the tested value (JLS 6.3 taint), with the LOCAL delivered through a scope channel that attaches it as a direct child of the enclosing BLOCK - drained per block, so ownership is lexically correct even across lambdas, constructors and nested blocks
  • Method references (8) to METHOD_REF (lambda shape) so router.get("/x").handler(this::h) resolves its handler
  • Local records/classes (16) as full TYPE_DECLs under the METHOD; native methods keep their NATIVE modifier
  • String annotation values additionally lower to a real LITERAL duplicate (the ANNOTATION_LITERAL node supports no TAGGED_BY edges and is invisible to literal traversals), making @GetMapping("/users") routes and @Query("SELECT ...") statements taggable

Framework taggers (ChennaiTagsPass.tagJavaRoutes, vocabulary in JavaFrameworks.scala): Spring MVC/WebFlux, JAX-RS/Jakarta, Micronaut, servlets (recognised by servlet parameter types), Vert.x/Javalin/Spark router DSLs, gRPC StreamObserver services, JDBC/JPA/MyBatis/Mongo/Redis, AI SDKs (LangChain4j, Spring AI, OpenAI, Gemini, Bedrock, Azure - ai-invoke/ai-prompt on the Python tagger's vocabulary), MCP @Tool methods, AWS Lambda + cloud SDKs, JNI/FFM natives, Kafka/JMS/RabbitMQ listeners, http-client SDKs. EasyTagsPass gains the Python arm's sink families (code-execution, ssrf, file-io, unsafe-deserialization, reflection). Bare-name collisions are gated (imports, receiver types, annotation names) - see the review pass notes in the second commit.

Flow semantics (JavaFrameworkSemantics): fully-qualified sanitizers (OWASP Encoder, Spring HtmlUtils/UriUtils, commons-text, URLEncoder) and Jackson/Gson deserialisation carriers in the language-neutral javaFlows; bare-name request readers (getParameter, getHeader, ... receiver->return) language-gated through flowsForLanguage, mirroring the PHP sanitizer gating.

Version: 3.3.0 -> 3.4.0 (minor). Docs: README, lessons 2 (modern Java lowering table + framework tagging/semantics), 11, 14.

Testing

  • New fixtures measured the gap first: 34 of 39 tests failed at baseline; all pass now - ModernJavaSyntaxTests, ModernJavaDataflowTests (incl. parameter sources through pattern switches and return/assignment position), JavaFrameworkTagsTest, PatternLocalOwnershipTests
  • AnnotationTests updated for the dual annotation-value shape
  • sbt clean stage test createDistribution publishLocal passes; full suite 3272 tests / 0 failures across all modules
  • End-to-end (frontend, taggers, dataflow engine, reachable slicer, no chennai.json): verified from atom's companion PR (Switch to the new @cdxgen/cdxgen package namespace atom#261)

Team AppThreat added 2 commits September 15, 2026 19:53
Modern Java source lowering (javasrc2cpg AstCreator). The bundled
JavaParser already parses every construct through Java 26 syntax; the
AstCreator dropped the ones below into unknown nodes, which broke
callgraphs, CFG, dataflow and reachable flows on any post-8 codebase
that uses them:

- Switch expressions (14) lower to nested <operator>.conditional calls,
  so the declared conditional semantic gives the construct its JLS
  14.11.2 dataflow: taint in any arm value flows to the expression's
  value. Multi-label arms OR their tests; a `when` guard ANDs in.
- yield (14) contributes the arm value; a yield reached in plain
  statement position lowers to an <operator>.yield call.
- instanceof type patterns (16) and record patterns (21) bind their
  variables as locals plus an assignment from the tested/matched value,
  carrying the JLS 6.3 definite-assignment taint. The LOCAL is
  registered on a new scope channel (Scope.registerPatternLocalAst) and
  attached as a direct child of the body BLOCK, where method.local and
  the dataflow engine expect a method's locals; the binding assignments
  return as leading ASTs which astsForVariableDecl/astForReturnNode
  hoist into statement position (under the conditional they were
  invisible to reaching definitions, and ARGUMENT edges into LOCAL
  violate the schema).
- Method references lower to METHOD_REF nodes (the lambda shape), so a
  router registration like router.get("/x").handler(this::h) exposes
  its handler for call-graph and route resolution.
- Local records/classes (16) become full TYPE_DECLs under the METHOD.
- `native` methods keep their NATIVE modifier.
- String-valued annotation members additionally lower to a real LITERAL
  duplicate under the parameter assignment: the ANNOTATION_LITERAL value
  node supports no TAGGED_BY edges and is invisible to literal
  traversals, so routes (@GetMapping("/users")) and statements
  (@query("SELECT ...")) that existed only there were untaggable.
  AnnotationTests pins both shapes.

New fixtures ModernJavaSyntaxTests / ModernJavaDataflowTests /
JavaFrameworkTagsTest measure and pin all of the above; before this
change 34 of their 39 tests fail, after it all pass.

Java framework boundaries (ChennaiTagsPass.tagJavaRoutes, EasyTagsPass):

- HTTP: Spring mapping annotations and request-data parameters,
  JAX-RS/Jakarta (import-gated), Micronaut, servlet overrides
  (recognised by servlet parameter TYPES so a user service() stays
  untagged), and router DSLs (Vert.x/Javalin/Spark - route literal
  plus handler resolution through METHOD_REF).
- gRPC: StreamObserver service methods are grpc-service entrypoints;
  the observer parameter and onNext/onError/onCompleted calls are
  framework-output.
- Databases: JDBC/JPA/JdbcTemplate calls and @Query/@Select/...
  annotation values tagged sql; document stores import-gated.
- AI/LLM: LangChain4j, Spring AI, OpenAI, Gemini, Bedrock, Azure -
  invocations ai-llm+ai-invoke, prompt builders ai-llm+ai-prompt (the
  Python tagger's vocabulary).
- MCP: @tool methods are mcp-tool entrypoints with client-facing
  parameters; exchange parameters are framework-input.
- Cloud: AWS/GCP/Azure calls cloud; RequestHandler.handleRequest
  implementations are event-facing entrypoints.
- Native: System.loadLibrary, NATIVE methods and the java.lang.foreign
  (FFM) machinery tagged native.
- Messaging/SDK: @KafkaListener/@JmsListener/@RabbitListener methods
  are queue-driven inputs; OkHttp/Retrofit/Feign/java.net.http calls
  are http-client.
- EasyTagsPass.tagJavaPatterns gains the Python arm's sink families
  (code-execution for Runtime.exec/ProcessBuilder.start, ssrf, file-io,
  unsafe-deserialization, reflection) so a Java graph has tagged sinks
  without a hand-written chennai.json.

The shapes and collision gates live in
x2cpg/passes/taggers/java/JavaFrameworks.scala.

Java framework flow semantics (dataflowengineoss):

- New JavaFrameworkSemantics documents the vocabulary: fully-qualified
  sanitizers (OWASP Encoder, Spring HtmlUtils/UriUtils, commons-text
  escapers, URLEncoder) clear taint in the language-neutral javaFlows;
  Jackson/Gson deserialisation carriers pass input taint into the
  returned object; String.format/concat declare explicit index maps.
- The bare-name request readers (getParameter, getHeader, ... - the
  returned value IS request data, receiver 0 to return -1) are gated on
  JVM languages through DefaultSemantics.flowsForLanguage, mirroring
  the PHP sanitizer gating; LanguageScopedSemanticsTests pins the split.

Version 3.3.0 -> 3.4.0 (minor: new language coverage and taggers).

Docs: README language/framework summary, lesson 2 (modern Java lowering
table and the two invariants that were bugs first, framework tagging
and semantics), lesson 11 (ChennaiTagsPass family list), lesson 14
(language-scoped and framework semantics).

Signed-off-by: ZCode <zcode@appthreat.com>
Signed-off-by: Team AppThreat <cloud@appthreat.com>
… and taggers

Build: the new taggers.java sub-package made every plain `import java.io.*`
/ `java.util.*` in the parent package resolve `java` against the sibling
package (Scala 3 relative package resolution), breaking CdxPass and
TrackersTagsPass on a clean build - incremental compilation had masked it.
JavaFrameworks now lives directly in the taggers package and the `_root_`
prefixes in ChennaiTagsPass are reverted to plain imports.

Review fixes, each with a regression test:

- Pattern-local ownership: the scope channel was a flat buffer drained by
  method/lambda body builders, so a lambda built after a pattern binding
  stole the enclosing method's local, and constructor bodies and lambda
  BLOCK bodies never drained at all. The drain now happens per BLOCK in
  astForBlockStatement (innermost first), which is the lexically correct
  granularity for every case. PatternLocalOwnershipTests pins all three
  shapes; note javasrc2cpg models a lambda as capturing a copy of every
  in-scope local, so a lambda listing a binding is the closure model, not
  the channel - ownership is asserted on the enclosing method.

- Condition edges: astForIf took the FIRST condition AST, which for a
  switch-expression condition (`if (switch (x) {...})`) is a hoisted
  binding, not the value. The CONDITION edge now binds the VALUE (the
  last AST); same for while/do via conditionAstsFor.

- Plain assignments (`value = switch (...)`) now hoist the switch's
  leading statements like declarations and returns already did, and the
  assignment's type comes from the VALUE instead of the first leading
  binding. New ModernJavaDataflowTests case pins the flow.

- Tagger precision (bare-name noise):
  - `load` dropped from the JNI library loaders (every loader method
    matched); System.load is matched through java.lang.System.load* on
    the resolved methodFullName instead.
  - `allocate` dropped from the FFM names (every buffer builder
    matched) and the remaining bare FFM names are import-gated; resolved
    calls were already covered by the java.lang.foreign.* prefix.
  - JPA split: createQuery/createNativeQuery/createStoredProcedureQuery
    stay ungated (distinctive); persist/merge/remove are everyday
    collection/cache names and are now import-gated on
    jakarta/javax.persistence, org.hibernate, spring-orm.
  - The JdbcTemplate block additionally requires "Template" in the
    resolved name, matching the document-store arm's receiver gate.
  - AI prompt builders trimmed to distinctive names (prompt,
    systemMessage, userMessage, aiMessage, messages, template) - bare
    system/user/parameters fabricated prompt sinks on unrelated helpers.
  - software.amazon.awssdk dropped from the ssrf patterns; AWS calls are
    tagged cloud, and their URLs are not user-buildable.

Cleanup: unused `given DiffGraphBuilder` lines and the unused
`asExpressionValue` parameter removed; stale comments corrected; lesson
doc paths updated for the moved file.

Verified: sbt clean stage test createDistribution publishLocal passes;
the full suite is 3272 tests / 0 failures across all modules; atom's Java
reachables fixtures pass against the republished 3.4.0.

Signed-off-by: ZCode <zcode@appthreat.com>
Signed-off-by: Team AppThreat <cloud@appthreat.com>
@prabhu

prabhu commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Closing - reviewing via the pushed branch directly instead.

@prabhu prabhu closed this Sep 15, 2026
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.

1 participant