## Summary

A materially distinct alternate trigger for CVE-2026-84645 was confirmed on vulnerable Jenkins 2.579, but it is **not a fix bypass**. Instead of the parent reproduction's `SCMTrigger.BuildAction#run` carrier and `/pollingLog/...` route, the alternate uses `Fingerprinter.FingerprintAction#build` and `/fingerprints/run/project/parent/scriptText`. A low-privilege authenticated user submitted a forged `FreeStyleBuild -> FreeStyleProject -> Hudson` graph through job `config.xml`; Jenkins 2.579 executed Groovy, created a controller-local marker, and ran `id`. On the exact same payload, Jenkins 2.580 removed the nested build, returned HTTP 404 for the forged route, and created no marker. A newly routable `ParametersAction#run` candidate and reference/replacer exception-confusion candidates also failed closed on 2.580.

## Fix Coverage / Assumptions

The primary fix is commit [`0d731367e08656f8cd1e8275f0e820f97af07fc6`](https://github.com/jenkinsci/jenkins/commit/0d731367e08656f8cd1e8275f0e820f97af07fc6), included in Jenkins 2.580 commit `497de4961ad80d97e26bfdeb0d2e40442a84ecb0`. It establishes this invariant in `hudson.util.RobustReflectionConverter#doUnmarshal`: a concrete type implementing `hudson.model.PersistenceRoot` must not be materialized as an ordinary nested field value.

The fix covers normal Jenkins reflective XStream conversion globally rather than matching one endpoint or gadget. It throws `CriticalXStreamException` before `unmarshalField` and preserves narrow compatibility cases:

1. `reference=` resolves an already-deserialized graph object.
2. A non-root `resolves-to=` placeholder without a `class=` override performs a registry lookup in `readResolve`.
3. A concrete class with a registered `SingleValueConverter` consumes a scalar identifier and returns an existing object.

`XStream2#setupConverters` supplies the actual converter lookup. `Jenkins#readResolve` rejects a second singleton; `Jenkins#writeReplace` emits a safe replacer when nested. Queue load handles a critical violation by starting empty. Related 2.580 changes annotate transient back-reference fields such as `SCMTrigger.BuildAction#run`, `Fingerprinter.FingerprintAction#build`, `ParametersAction#run`, `Run#project`, and `Node#parent` with `@XStreamNotDeserializable`.

The fix assumes custom converters, replacement classes, and scalar converters preserve the same identity-only semantics. A plugin parser or wholly custom converter that manually constructs a root is not directly inspected by this reflective field check. No such same-boundary bypass was found here. Full details are in `bundle/vuln_variant/patch_analysis.md`.

## Variant / Alternate Trigger

### Confirmed alternate path: FingerprintAction

- **Configuration entry point:** authenticated `POST /job/carrier/config.xml`.
- **Different carrier:** `hudson.tasks.Fingerprinter$FingerprintAction`.
- **Different field:** transient `build`, whose concrete value is a nested `hudson.model.FreeStyleBuild`.
- **Object graph:** `FingerprintAction#build -> FreeStyleBuild#project -> FreeStyleProject#parent -> hudson.model.Hudson`.
- **Different Stapler route:** authenticated `POST /job/carrier/fingerprints/run/project/parent/scriptText`.
- **Sink:** forged `Hudson#doScriptText`; `AuthorizationStrategy$Unsecured` on the forged root makes its local ACL check pass.

This is materially distinct from the parent `SCMTrigger.BuildAction#run` and URL name `pollingLog`. The two paths converge only after their separate Run-bearing actions/getters reach `Run#getProject`. The equivalence record is `bundle/vuln_variant/root_cause_equivalence.json`.

### Candidate B: ParametersAction

`ParametersAction#run` was tested through `/job/carrier/parameters/run/project/parent/scriptText`. Jenkins 2.579 contains the transient field but no public `getRun`, so the route is not traversable. Jenkins 2.580 adds a getter but simultaneously marks the field non-deserializable, and the global root policy removes the forged value. Both targets returned HTTP 404 with no marker. This candidate probes a genuinely new fixed-version routing surface but does not produce a bypass.

### Candidate C: fix exception confusion

A `reference=` node combined with an incompatible `class=` and a nonexistent `Run$Replacer` identifier tested the two broad compatibility assumptions. Neither case created a root graph on either target; both candidate action routes returned HTTP 404 and all markers remained absent. Only three candidates are reported because the source scan found three materially different classes: an alternate core Run-bearing action, a newly routable fixed-version action, and a direct exception-assumption test. Additional method names over the same graph would not constitute new variants.

## Impact

- **Package/component:** Jenkins core XStream persistence (`XStream2` / `RobustReflectionConverter`) and Stapler routing over model objects.
- **Vulnerable target tested:** Jenkins 2.579, source commit `9095ea3a5c5e7dcd392695a5dd880af1c9910ddf`, official image digest `sha256:a7342867ea33efaacf825229d50b7fc77c144ecada9719ab4e32419f5d7412be`.
- **Fixed target tested:** Jenkins 2.580, source commit `497de4961ad80d97e26bfdeb0d2e40442a84ecb0`, official image digest `sha256:0e50a5b11ac14f3b84e529d725ed3a1c4b17ba16188dfa8d9a0189428b0839b1`.
- **Risk on affected releases:** High; an authenticated user with `Item/Configure`, not `Overall/Administer`, can execute arbitrary controller Groovy/commands.
- **Fixed-target consequence:** None reproduced. The tested alternate route is non-routable and has no process effect on 2.580.

The trust boundary matches Jenkins' security policy. Administrators already having Script Console access are excluded as a vulnerability case, so the script provisions an attacker with only Overall/Read, Item/Read, and Item/Configure and requires direct `/scriptText` to return HTTP 403. The XML and Groovy arrive over authenticated HTTP. This is not a local administrator loading their own file.

## Impact Parity

- **Disclosed/claimed maximum impact:** Authenticated remote code execution on the Jenkins controller.
- **Reproduced impact from this alternate trigger:** Full controller command execution on Jenkins 2.579. Groovy created a unique controller-local marker, and `id` returned `uid=1000(jenkins) gid=1000(jenkins) groups=1000(jenkins)`.
- **Parity:** `full` for the confirmed alternate trigger on the affected release; `none` on the fixed variant target.
- **Not demonstrated:** No fixed-version command execution, persistence, credential extraction, lateral movement, or destructive action. The proof stops at a temporary marker and `id`.

## Root Cause

JEP-200 validates which classes XStream may deserialize, but Jenkins 2.579 does not validate whether an allowed `PersistenceRoot` appears in a safe graph position. `FingerprintAction#build` can therefore contain a newly constructed `FreeStyleBuild`; that build can contain a forged project; and that project can contain a second forged `Hudson` with attacker-selected authorization state. `Actionable#getDynamic` locates the action by URL name, and Stapler follows `FingerprintAction#getRun`, `Run#getProject`, and `AbstractItem#getParent` to the forged script endpoint.

The parent and alternate triggers have the same attacker-controlled source, structural defect, `Run -> Job -> Hudson` graph, Stapler sink, permission-boundary crossing, and RCE impact. The material difference is the core carrier/field and its URL name. Jenkins 2.580 closes both the generic root-position defect and this specific transient field, so the alternate does not bypass the fix.

## Reproduction Steps

1. Run `bundle/vuln_variant/reproduction_steps.sh` from any directory. Set `PRUVA_ROOT` only if the bundle root cannot be inferred.
2. The script obtains pinned Matrix Authorization Strategy 3.3 and its pinned dependencies from the prepared cache or official update URLs and validates SHA-256 values.
3. It pulls immutable Jenkins 2.579 and 2.580 image digests and records image/source identity in `bundle/logs/vuln_variant/fixed_version.txt`.
4. It starts the vulnerable controller first and the fixed controller second. Each receives a fresh security realm, a low-privilege attacker, and a `carrier` job.
5. It requires direct Script Console access to return HTTP 403, then exercises the FingerprintAction alternate graph, ParametersAction path, and exception-confusion payloads.
6. It captures response headers/bodies, resulting job XML, controller-local marker presence, controller logs, and structured observations under `bundle/logs/vuln_variant/proof/`.
7. Expected output is:

   ```text
   NO FIX BYPASS: the distinct FingerprintAction carrier executes on vulnerable Jenkins 2.579 but is blocked on Jenkins 2.580; the newly routable ParametersAction and reference/replacer exception-confusion candidates also fail closed.
   ```

8. The script exits 1 by design because the validated alternate trigger works only on the vulnerable release. Exit 0 is reserved for a fixed-version bypass; exit 2 means the matrix was inconclusive.

## Evidence

- `bundle/logs/vuln_variant/proof/vulnerable.fingerprint.route.response.body`:

  ```text
  Result: {marker=CVE_2026_84645_VARIANT_vulnerable_fingerprint_28611_d5120ae57236, id=uid=1000(jenkins) gid=1000(jenkins) groups=1000(jenkins)}
  ```

- `vulnerable.fingerprint.observation.json`: route HTTP 200 and `marker_present: true`.
- `vulnerable.fingerprint.marker.txt`: the same unique marker read from inside the controller container.
- `fixed.fingerprint.observation.json`: route HTTP 404 and `marker_present: false`.
- `fixed.fingerprint.config.after.xml`: preserves only `<hudson.tasks.Fingerprinter_-FingerprintAction/>`; no nested build/project/Hudson remains.
- `fixed.fingerprint.marker_absent.txt`: controller marker did not exist.
- `vulnerable.parameters.observation.json` and `fixed.parameters.observation.json`: both route HTTP 404 and no marker.
- `fixed.exceptions.config.after.xml`: reference/replacer candidate leaves only empty actions and no root graph.
- `vulnerable.exceptions.*.marker_absent.txt` and `fixed.exceptions.*.marker_absent.txt`: all exception-candidate markers absent.
- `vulnerable.direct_console.headers` and `fixed.direct_console.headers`: direct root Script Console returned HTTP 403.
- `vulnerable.service.log` and `fixed.service.log`: real controller startup as versions 2.579 and 2.580.
- `bundle/logs/vuln_variant/fixed_version.txt`: exact commits, image digests, image IDs, architecture, OS, and fix commit.
- `bundle/vuln_variant/runtime_manifest.json`: SHA-256 binding for proof files.
- `bundle/logs/vuln_variant/reproduction_first.log` and `reproduction_second.log`: two consecutive complete executions, each ending with expected exit code 1.

## Recommendations / Next Steps

- Upgrade weekly Jenkins to 2.580 or later, or LTS to 2.568.3 or later.
- Preserve the global nested-`PersistenceRoot` check and propagation of `CriticalXStreamException`; do not rely only on the targeted `@XStreamNotDeserializable` annotations.
- Retain the Jenkins singleton `readResolve` guard and replacement behavior as defense in depth.
- Audit plugin-defined XStream converters, `SingleValueConverter` implementations, and replacement classes. Any nested `PersistenceRoot` result must come from an identity/registry lookup and must not accept child state.
- Require plugin-defined independent persistence roots to implement `PersistenceRoot` so the global policy can recognize them.
- Add regression tests for plugin actions exposing routable root getters and for partial graph state after a critical conversion failure.
- Keep tests for `reference=`, `resolves-to=`, and scalar Item/Run queue references so compatibility changes cannot weaken the invariant unnoticed.

## Additional Notes

- **Classification:** Confirmed alternate vulnerable-version trigger; no Jenkins 2.580 bypass.
- **Idempotency:** Confirmed. The final script was executed twice and both runs completed normally with expected exit code 1.
- **Safety:** Every controller is ephemeral. The only command effects are a unique file under `/tmp` and `id`; containers are removed on exit/interruption.
- **Endpoint nuance:** The fixed job configuration update may return HTTP 200 while safely omitting the forbidden nested fields. Security is established by graph removal, route HTTP 404, and marker absence rather than requiring the config POST itself to fail.
- **Scope limitation:** This run tested Jenkins core carriers and fix exceptions. It did not install arbitrary third-party plugins to search for separate plugin-specific custom converters; such a bug would need independent same-root-cause and same-trust-boundary validation.
