# Variant RCA Report — CVE-2026-49844

## Summary

This variant analysis searched for a **bypass** of the CVE-2026-49844 fix (PR #4163, `formatDouble`/`formatFloat` gating in `MapMessageJsonFormatter`) and for **alternate triggers** that reach the same sink from different data-path entry points. The original reproduction (`bundle/repro/NonFiniteJsonTest`) only exercised the *direct* scalar `Double`/`Float` and primitive `double[]`/`float[]` paths. This variant harness exercises every *alternate* dispatcher branch in `MapMessageJsonFormatter.format()` that can carry a non-finite floating-point value — nested `java.util.Map`, `List`, `Object[]`, non-`List` `Collection` (`Set`/`ArrayDeque`), a custom `Number` subclass (the `formatNumber` `else`-branch), a `BigInteger` whose `doubleValue()` overflows to `Infinity`, and a `StringBuilderFormattable`. **Result: no bypass was found.** The alternate data paths reproduce the bug on the **vulnerable** versions (2.25.4 and 2.26.0 both emit 17 bare `NaN`/`Infinity`/`-Infinity` tokens with 17 strict-JSON parse failures), but the **fixed** versions (2.25.5 and 2.26.1) quote **all 18** cases (0 bare tokens, 0 parse failures). The fix is complete: every code path that can emit a non-finite `double`/`float` value routes through `formatDouble`/`formatFloat`, which gate non-finite values into `formatString` (quoting them as JSON strings). The only path that does *not* route through `formatDouble`/`formatFloat` — `StringBuilderFormattable` via `formatFormattable` — was always safe because it wraps the formattable's output in quotes and JSON-escapes it (confirmed quoted in both vulnerable and fixed versions).

## Fix Coverage / Assumptions

- **Invariant the fix relies on:** every `Number`-bearing value that reaches `MapMessageJsonFormatter` ultimately flows through one of: `formatNumber` (for scalar `Number`), `formatDoubleArray` (for `double[]`), or `formatFloatArray` (for `float[]`). The fix gates the three raw `sb.append(<double>/<float>)` sites — and the `formatNumber` `else`-branch's `sb.append(doubleValue)` for non-`Double`/`Float`/`BigDecimal`/integer `Number` subclasses — through the new `formatDouble`/`formatFloat` helpers, which check `Double.isFinite`/`Float.isFinite` and quote non-finite values via `formatString`.
- **Code paths the fix explicitly covers:**
  - `formatNumber`: `Double` → `formatDouble`; `Float` → `formatFloat`; the `else` branch (custom `Number`, `BigInteger`, etc.) → `formatDouble(sb, doubleValue)`.
  - `formatDoubleArray`: each `double` element → `formatDouble`.
  - `formatFloatArray`: each `float` element → `formatFloat`.
  - All container branches (`formatMap`, `formatList`, `formatCollection`, `formatObjectArray`, `formatIndexedStringMap`) recurse into `format()` → `formatNumber`, so they inherit the fix transitively.
- **What the fix does NOT cover (tested and ruled out):** none of the alternate data paths bypass the fix. The `StringBuilderFormattable` branch (`formatFormattable`) is intentionally outside the `formatDouble`/`formatFloat` gating, but it is safe-by-construction because it wraps output in quotes + `escapeJson`. This was verified empirically (control case G: quoted in both vulnerable and fixed).
- **Sibling component (out of scope, ruled out):** `JsonWriter` in `log4j-layout-template-json` (the CVE-2026-34481 surface) independently implements the same `isFinite` gating in its `writeNumber(double)`/`writeNumber(float)` and its `else`-branch. When `JsonTemplateLayout`'s `MessageResolver` encounters a `MapMessage`, it calls `MapMessage.getFormattedMessage(["JSON"])` and writes the result via `writeRawString` — so it inherits the `MapMessageJsonFormatter` fix. No separate unfixed sink was found.

## Variant / Alternate Trigger

Although **no bypass** of the fixed code was found, the following **alternate triggers** reproduce the *same* root-cause bug (bare non-finite JSON tokens) on the **vulnerable** versions. These are materially different dispatcher branches reaching the same sink, and were *not* covered by the original repro's 13 cases:

| # | Data path | Dispatcher branch reached | Entry point |
|---|-----------|---------------------------|-------------|
| A | nested `java.util.Map` (e.g. `HashMap`/`LinkedHashMap` with `Double.NaN`) | `formatMap` → `format` → `formatNumber` | `MapMessage.with("data", map).getFormattedMessage(["JSON"])` |
| B | `java.util.List` (e.g. `ArrayList` of `Double`) | `formatList` → `format` → `formatNumber` | `MapMessage.with("items", list).getFormattedMessage(["JSON"])` |
| C | `Object[]` of boxed `Double` | `formatObjectArray` → `format` → `formatNumber` | `MapMessage.with("arr", objArray).getFormattedMessage(["JSON"])` |
| D | non-`List` `Collection` (`LinkedHashSet`, `ArrayDeque`) | `formatCollection` → `format` → `formatNumber` | `MapMessage.with("set", set).getFormattedMessage(["JSON"])` |
| E | custom `Number` subclass with `doubleValue()` = NaN/±Infinity | `formatNumber` **else-branch** → `sb.append(doubleValue)` | `MapMessage.with("custom", new CustomNumber(NaN)).getFormattedMessage(["JSON"])` |
| F | `BigInteger` so large that `doubleValue()` overflows to `Infinity` | `formatNumber` **else-branch** (BigInteger is *not* special-cased in `MapMessageJsonFormatter`, unlike `JsonWriter`) | `MapMessage.with("huge", BigInteger.TEN.pow(400)).getFormattedMessage(["JSON"])` |
| G | `StringBuilderFormattable` emitting bare `NaN` | `formatFormattable` (quotes + escapes) — **control, always safe** | `MapMessage.with("formattable", new NaNEmittingFormattable()).getFormattedMessage(["JSON"])` |

- **Exact entry points:** all reachable via the public API `MapMessage.getFormattedMessage(new String[]{"JSON"})` (and equivalently `MapMessage.asJson(StringBuilder)` via a subclass, or `MapMessage.asString("JSON")`). These are the same public entry points as the parent CVE; the difference is the *value type* placed in the map, which selects a different internal dispatcher branch.
- **Specific code paths (file/function):** `log4j-api/src/main/java/org/apache/logging/log4j/message/MapMessageJsonFormatter.java` — `format(sb, object, depth)` dispatcher → `formatMap` / `formatList` / `formatCollection` / `formatObjectArray` / `formatNumber` (and its `else` branch) / `formatFormattable`.

## Impact

- **Package/component affected:** `org.apache.logging.log4j:log4j-api` — `org.apache.logging.log4j.message.MapMessageJsonFormatter`, reached via `MapMessage.asJson(StringBuilder)` and `MapMessage.getFormattedMessage(String[])` with the `"JSON"` format. Same component as the parent CVE.
- **Affected versions (as tested):**
  - **Vulnerable:** `log4j-api` 2.25.4 (rel/2.25.4, commit `0628e53b`) and 2.26.0 (rel/2.26.0, commit `c1ad2a66`). Both emit 17 bare non-finite tokens / 17 strict-JSON parse failures across the alternate paths.
  - **Fixed:** `log4j-api` 2.25.5 (rel/2.25.5, commit `2e1d9c62`) and 2.26.1 (rel/2.26.1, commit `dd0f9d25`). Both quote all 18 cases (0 bare, 0 parse failures). The fix is also present on `2.x` HEAD (`b6ba7d0a`).
- **Risk level / consequences:** Medium (CWE-116). Same as parent: malformed JSON in log output; strict RFC 8259 parsers / log-ingestion pipelines reject affected records. Not an RCE. The alternate paths widen the set of attacker-controllable inputs that can trigger the bug on unpatched versions (any nested container or custom `Number`, not just scalar/`double[]`), but the patched versions are fully covered.

## Impact Parity

- **Disclosed/claimed maximum impact (parent):** improper encoding of non-finite floating-point values during `MapMessage` JSON serialization producing output that is not valid JSON (serialization/encoding flaw); explicitly not an RCE.
- **Reproduced impact from this variant run:** on vulnerable versions, the alternate data paths produce the identical invalid-JSON symptom — bare `NaN`/`Infinity`/`-Infinity` tokens rejected by a strict (RFC 8259) Gson `JsonReader` (`lenient=false`). On fixed versions, all alternate paths produce valid quoted-string JSON.
- **Parity:** `full` — the alternate triggers exhibit byte-for-byte the same invalid-JSON behavior as the parent on vulnerable versions, and the fix neutralizes them identically.
- **Not demonstrated:** N/A — no code execution was claimed or expected.

## Root Cause

The root cause is unchanged from the parent CVE: `MapMessageJsonFormatter` appends primitive `double`/`float` values directly to the `StringBuilder` via `sb.append(<double>)`, and `StringBuilder.append(double)` delegates to `Double.toString(double)`, which returns the bare literals `"NaN"`, `"Infinity"`, `"-Infinity"` for non-finite values. These are not valid JSON number tokens (RFC 8259 §6 permits only finite numbers). The alternate data paths (A–F) all funnel into the same `formatNumber`/`formatDoubleArray`/`formatFloatArray` sinks, so on unpatched versions any container or custom `Number` carrying a non-finite value produces the same bare tokens.

The fix (commit `19edb23e` on `2.x`; `feadf8eb` on `2.25.x`; `1352b987` on `2.26.x` — PR #4163) introduces `formatDouble`/`formatFloat` helpers that gate non-finite values through `formatString` (quoting them). Critically, the fix updates **all three** raw-append sites, including the `formatNumber` `else`-branch (`formatDouble(sb, doubleValue)`), which is why the custom-`Number` (E) and `BigInteger`-overflow (F) alternate triggers — which reach that `else`-branch rather than the `Double`/`Float` `instanceof` branches — are also covered.

Because the fix touches the *sink* (`formatNumber`/`formatDoubleArray`/`formatFloatArray`) rather than any single *entry path*, and because every `Number`-bearing dispatcher branch ultimately recurses to that sink, the fix is complete: there is no remaining code path that appends a raw non-finite `double`/`float` to the JSON output.

## Reproduction Steps

1. **Script:** `bundle/vuln_variant/reproduction_steps.sh` (self-contained, idempotent).
2. **What the script does:**
   - Installs OpenJDK 17 if needed.
   - Downloads the official Apache `log4j-api` jars from Maven Central for **four** versions: 2.25.4 (vulnerable), 2.25.5 (fixed), 2.26.0 (vulnerable), 2.26.1 (fixed), plus `gson-2.11.0` for strict JSON validation.
   - Verifies fix presence per jar via `javap` (`formatDouble(java.lang.StringBuilder, double)` present iff fixed).
   - Compiles `bundle/vuln_variant/VariantTest.java` — a harness that builds a concrete `MapMessage` subclass and places each alternate value type (nested `Map`, `List`, `Object[]`, `Set`/`ArrayDeque`, custom `Number`, `BigInteger` overflow, `StringBuilderFormattable`) into the map, then serializes via `getFormattedMessage(["JSON"])`. Each output is scanned for bare non-finite tokens and validated with a strict RFC 8259 parser (Gson `JsonReader`, `lenient=false`, `skipValue()`).
   - Runs the harness against all four jars, emits a `VERDICT|...` line per version, and writes `bundle/vuln_variant/runtime_manifest.json` and `bundle/logs/vuln_variant/latest_version.txt`.
3. **Expected evidence:**
   - Vulnerable 2.25.4 / 2.26.0: `bare=17 quoted=1 parseFail=17` (the 1 quoted is the `StringBuilderFormattable` control case G).
   - Fixed 2.25.5 / 2.26.1: `bare=0 quoted=18 parseFail=0`.
   - Exit code 1 = no bypass (fix covers all alternate paths); exit 0 = bypass. This run exits 1.

## Evidence

- **Log files:**
  - `bundle/logs/vuln_variant/variant_2.25.4.log` — vulnerable 2.25.x line
  - `bundle/logs/vuln_variant/variant_2.25.5.log` — fixed 2.25.x line
  - `bundle/logs/vuln_variant/variant_2.26.0.log` — vulnerable 2.26.x line
  - `bundle/logs/vuln_variant/variant_2.26.1.log` — fixed 2.26.x line
  - `bundle/logs/vuln_variant/compile.log` — javac output
  - `bundle/logs/vuln_variant/latest_version.txt` — tested fixed refs
- **Key excerpts (vulnerable 2.25.4, alternate paths):**
  ```
  --- A. nested java.util.Map value (formatMap -> formatNumber) ---
    [nested-map] value=NaN
      JSON output: {"data":{"v":NaN}}
      >> BARE non-finite token detected (invalid JSON per RFC 8259)
      >> strict JSON parse FAILED (invalid per RFC 8259)
  --- E. custom Number subclass (formatNumber else-branch) ---
    [custom-number] value=Infinity
      JSON output: {"custom":Infinity}
      >> BARE non-finite token detected (invalid JSON per RFC 8259)
  --- F. BigInteger overflow -> Infinity (formatNumber else-branch) ---
    (sanity) huge.doubleValue() = Infinity  (expect Infinity; isFinite=false)
      JSON output: {"huge":Infinity}
      >> BARE non-finite token detected (invalid JSON per RFC 8259)
  --- G. StringBuilderFormattable emitting bare NaN (control: always quoted) ---
      JSON output: {"formattable":"NaN"}
      >> non-finite value is QUOTED (valid JSON string)
  SUMMARY: bare=17, quoted=1, parseFail=17
  VERDICT|2.25.4-vulnerable-2.25.x|VULNERABLE|bare=17|quoted=1|parseFail=17
  ```
- **Key excerpts (fixed 2.25.5, same alternate paths):**
  ```
  --- A. nested java.util.Map value (formatMap -> formatNumber) ---
    [nested-map] value=NaN
      JSON output: {"data":{"v":"NaN"}}
      >> non-finite value is QUOTED (valid JSON string)
      >> strict JSON parse OK (valid RFC 8259 JSON)
  --- E. custom Number subclass (formatNumber else-branch) ---
    [custom-number] value=Infinity
      JSON output: {"custom":"Infinity"}
      >> non-finite value is QUOTED (valid JSON string)
  --- F. BigInteger overflow -> Infinity (formatNumber else-branch) ---
      JSON output: {"huge":"Infinity"}
      >> non-finite value is QUOTED (valid JSON string)
  SUMMARY: bare=0, quoted=18, parseFail=0
  VERDICT|2.25.5-fixed-2.25.x|FIXED|bare=0|quoted=18|parseFail=0
  ```
- **javap fix-presence verification:**
  - 2.25.4 / 2.26.0: no `formatDouble`/`formatFloat` (vulnerable).
  - 2.25.5 / 2.26.1: `private static void formatDouble(java.lang.StringBuilder, double)` and `formatFloat(...)` present (fixed).
- **Environment:** OpenJDK 17.0.19, Gson 2.11.0 (strict validator), official Apache `log4j-api` jars from Maven Central.
- **Runtime manifest:** `bundle/vuln_variant/runtime_manifest.json`

## Recommendations / Next Steps

- **No fix gap identified.** The PR #4163 fix is complete: it gates the sink rather than individual entry paths, so every `Number`-bearing dispatcher branch is covered. No code change is required to close a bypass.
- **Defense-in-depth suggestions (optional):**
  1. Add a regression test that exercises the *container* and *custom-`Number`* data paths (nested `Map`, `List`, `Object[]`, non-`List` `Collection`, a custom `Number` whose `doubleValue()` is non-finite, and a `BigInteger` whose `doubleValue()` overflows to `Infinity`) — the existing `MapMessageTest` regression tests only cover scalar `double`/`float`. This variant harness (`VariantTest.java`) can serve as the test skeleton.
  2. Add a `BigInteger` `instanceof` branch to `MapMessageJsonFormatter.formatNumber` mirroring `JsonWriter`'s handling, so `BigInteger` is serialized via `BigDecimal`-style logic rather than falling through to the `else`-branch's `longValue()`/`doubleValue()` comparison (currently safe only because the `else`-branch routes through `formatDouble`, but a `BigInteger`→`Infinity` value is semantically better represented as a quoted string or a `BigDecimal`-rounded finite value).
  3. Consider adding strict-JSON-parser validation to CI for *all* `MapMessage` JSON output shapes (not just scalar values).
- **Upgrade guidance (unchanged):** upgrade `log4j-api` to ≥ 2.25.5 (2.25.x line) or ≥ 2.26.1 (2.26.x line).

## Additional Notes

- **Idempotency confirmation:** the script was run twice consecutively; both runs produced identical results (exit code 1; `bare=17` for vulnerable, `bare=0` for fixed across both release lines). The script skips re-downloading jars that already exist.
- **Bounded search justification:** the search space for variants within the CVE's sink (`MapMessageJsonFormatter`) is the set of dispatcher branches in `format()` that can carry a non-finite floating-point value. There are exactly six such branches plus the `StringBuilderFormattable` control (A–G above). Every one was tested. No additional materially distinct entry/data path exists within this sink: the remaining branches (`null`, `Boolean`, `char[]`, `boolean[]`, `byte[]`, `short[]`, `int[]`, `long[]`, and the `else` `String` branch) cannot carry non-finite floating-point values by type. Searching further within this sink would repeat the same root cause/surface.
- **Out-of-scope sibling sinks:** `JsonWriter` (CVE-2026-34481 surface) and `JsonLayout` (log4j-core, Jackson-backed) are separate components with their own non-finite handling. A defect in those would be a separate bug, not a bypass of the `MapMessageJsonFormatter` fix, and is therefore out of scope for this variant analysis per the rule that a bug in component A is not a bypass of a fix in component B. `JsonWriter` was inspected and confirmed to already implement `isFinite` gating.
- **Edge cases covered:** nested `Map` of `Double`, `List` of `Double`, `Object[]` of boxed `Double`, non-`List` `Collection` (`Set`/`ArrayDeque`) of `Double`, custom `Number` subclass (else-branch), `BigInteger` overflow-to-`Infinity` (else-branch), and `StringBuilderFormattable` (control). The `BigDecimal` path is unaffected (`BigDecimal` is always finite).
