Pangram verdict · v3.3
We believe that this text is a mix of AI and human-written content.
AI likelihood · overall
MixedArticle text · 1,439 words · 6 segments analyzed
Last week we announced Echo's partnership with NanoClaw, designed to extend the vision and security of the open source project. In this post, we want to pull back the curtain and show you exactly how Echo's agentic hardening process works.How do we detect CVEs?Before we can fix anything, we need a complete, trustworthy picture of what's actually in the image. We scan and analyze the upstream NanoClaw container using several independent vulnerability scanners, including Trivy, Grype, and Wiz. Here's the raw result of scanning the open source NanoClaw image with Grype, sorted by severity:NanoClaw's default image scan resultsAnd here's how that stacks up against comparable agent runtimes (Hermes and OpenClaw) across both Grype and Trivy (we also added the NanoClaw Echo image to this comparison - which we will dive into soon):Now, let’s move to the fixing and CVE reduction.Step 1: Start with what we can safely bumpEvery library in the image is its own problem to solve.
So the first thing we do is separate the findings into "safe to bump" and "needs real work." The easy wins are the libraries we know we can upgrade without breaking NanoClaw.
Chromium is a great example. It's well known for backward compatibility, so we can trust their updates and bump with confidence. What we are left with are the CVEs that can’t be fixed, and the ones with major jumps. Once we strip out the Chromium-related CVEs, we're still left with roughly 600 vulnerabilities that need fixing. So what happens next?Step 2: The bumps that require real researchSome upgrades require a major version jump, which will likely not work out of the box. In these cases, we have to patch it ourselves and verify the patch actually holds without breaking the app. See below, a concrete example: Hono's node-serverScan results - showing @Hono/node-server - flagged with a major-version jump to a fixed releaseOn paper, this looked like a major jump. But when we dug into the source code, we found the fix was available within a much closer version to what was installed, version 1.19.14 (even if the scanners’ vulnerability databases weren't aware of yet). As part of our contribution to the open source community, we added it to their advisory (it’s a process we do on a daily basis in Echo). We move on to the next step - the “won’t fix” ones. Step 3: Patching and BackportingThen you hit the wall: the rest of the findings that are marked as won't fix or that the distro maintainers only fix in new majors, which as mentioned above are likely to break your app. For these, the fix strategy of choice is to backport, which means taking a patch from a newer version of the package and applying it to the older version that the app requires. In our case, that means we find the fix in the latest upstream version, and start working on NanoClaw’s source code directly. There are three main challenges we need to balance between:Finding the right fix - understanding where the bug is, tracing the fixing commit, and confirming the fix is genuinely safe and complete - not all fix sources are safe to use, so further research is needed.Applying it without breaking anything - the patch has to be compatible with the existing app cleanly.Validating - compatibility, functionality and that the CVE was truly resolved.On top of the application dependencies, there's the operating system underneath everything. NanoClaw's Dockerfile builds on Debian 12NanoClaw's upstream Dockerfile - built on node:22-slim, i.e. Debian 12Debian 12 base images bring a long tail of OS-level vulnerabilities with them. This is where Echo OS comes into play. It's the Linux distro that Echo maintains, and it's compatible with the common upstream distros - Ubuntu, Debian, RHEL, Amazon Linux, and more. Every part of it is built from source so it can be continuously patched by our AI patching agents. It offers thousands of patched OS packages and has eliminated more than 1.1 million CVEs across them. How Echo conducts backportingLet’s grab one of the latest backports we did as part of the NanoClaw project. We’ll focus on CVE-2025-59375 in expat. This process is conducted by Echo’s proprietary backporter agent.Why did we pick CVE-2025-59375?It's the hardest kind of backport there is - a security fix that isn't a bounds check but a new subsystem threaded through the middle of the library, carried from a newer upstream release down into the older version we ship, and one where the upstream maintainer explicitly warned distributors not to attempt a partial cherry-pick.How can CVE-2025-59375 be exploited?An attacker sends a small, entirely well-formed XML document and the parser allocates a wildly disproportionate amount of heap. Upstream's own figure: a ~250 KiB document caused roughly 800 MiB of allocation - an amplification factor of about 3,300. The result is memory exhaustion and process death, or an OOM kill that takes neighbours with it.Why is it hard to do a backport fix?expat already had protection against amplification attacks - the billion-laughs defence, which enforces a 100x limit once output passes 8 MiB.
But that accounting measures parsed output bytes, direct plus entity expansion. It never looked at the actual heap. So you can stay comfortably inside the entity budget while the parser's internal structures - hash tables, string pools, DTD scaffolding - balloon.
he fix had to introduce real allocation accounting, and that's invasive:• Every internal allocation now goes through `expat_malloc` / `expat_free` / `expat_realloc`, which prepend a `size_t` header to every block so that free and realloc know how much to subtract.
That changes the pointer handed back to callers. Mix a raw allocation with a tracked free anywhere and you corrupt the heap.• Structures that used to carry the memory-function table now carry the parser handle instead, because the counter lives on the root parser. That cascades into signature changes across `dtdCreate`, `dtdReset`, `dtdDestroy`, `dtdCopy`, `hashTableInit`, `poolInit`, `copyString`.• `parserCreate` gains a parent-parser argument so external-entity child parsers charge the root parser's budget, with strict initialisation ordering - some fields must be set before any tracked allocation can happen.• Three categories of allocation must deliberately bypass tracking: the app-facing `XML_MemMalloc`/`XML_MemRealloc`, the main input buffer from `XML_GetBuffer`, and the content model handed to the element-declaration handler (applications free that one with plain `free()`). Get any of them wrong and you either corrupt memory or start rejecting legitimate documents.What did we do and what eventually worked?The upstream fix was written against a newer expat than the one we ship, so this is a backport from a higher version down to a lower one, and it is large in every direction. Upstream's change spans 17 files; ours ends up touching 9, across the parser core, the public headers, the CLI tool, the documentation and the test suite. The reference patch does not apply to our tree, and landing only the parts that happen to fit produces exactly the state upstream warns about - some allocations tracked and some not, which is worse than not patching at all. What worked was treating it as a rebase rather than an apply. Our agent runs the whole thing end to end: apply, build, run the full upstream test suite, diagnose each failure into a specific class, hand it to the fixer built for that class, and repeat until the build and the entire test suite pass. The classes are things like a hunk whose line numbers no longer match, surrounding context that has changed, a file that has moved or been split, code that no longer compiles against this version, and tests that fail after a clean compile - each with its own dedicated fixer.Three things had to be solved for this patch specifically:1. A renamed build guard. The fix wraps the new tracker in `#ifdef XML_DTD`; in our version that macro was renamed to `XML_GE` back in 2.6.0. Keep the old name and the preprocessor silently drops the whole fix - green build, passing tests, scanner sees a patched version, vulnerability still present. This is the most dangerous failure mode in the whole job, because nothing tells you.2. A re-organized test suite. Upstream had split its monolithic `runtests.c` into per-area files, so the patch's test changes had to be redistributed into `alloc_tests.c` and `nsalloc_tests.c`. That's why our patch touches 9 files where the reference touches 8.3. Tests that break because the fix is correct. This is what drove us to add a dedicated test-fixing capability. Two real examples: an existing allocation test asserted that parsing survives a certain number of failing reallocations - no longer true once realloc routes through the tracked allocator, so the assertion needed adapting rather than deleting.