Tracing an XML buffer error with a local cyber model
A local 27B model helped trace a buffer error from a compiled binary, without access to application source. The investigation exposed an interaction between two length-handling mistakes.
I used philbert440/Qwen3.8-27B-Uncensored-Cyber, with the compiled ELF as a reference, to trace a buffer error in an XML field handler. Two mistakes in separate C files combined to produce the error.
For this client task, the agent had access only to the ELF binary, local VM commands for reverse engineering, and a QA VM for testing previously unknown vulnerabilities. It had no access to the application's source code.
That investigation happened before Alyria. The next run I recommend will use Alyria to contain the analysis workload. It has not run yet; the architecture below is the plan, not the tooling history of the original finding.
The engineering question is how a length changes meaning between the code that prepares a field and the code that stores it. A caller can validate the input and still pass the wrong quantity. A consumer can check that quantity against a buffer size and still permit an invalid write.
The following example illustrates the interaction without identifying the application or reproducing its source. These C files explain the length contract; they are not source the model saw.
The contract crosses a file boundary
XML parsing does not establish the capacity requirements of every downstream C buffer. An API can return valid, terminated text while application code mishandles its length during a later copy.
For example, libxml2's xmlNodeGetContent returns allocated content that the caller must release. Its xmlStrlen reports a string length, not the destination capacity required to store that string and its terminating zero. These are different quantities. XML character counts are another quantity again: a fixed-size C byte buffer must be checked in bytes, not user-perceived characters.
I'll use a 64-byte destination. The intended interface to consume_field takes a pointer and a payload byte count excluding the terminating zero. A successful call stores those bytes as a C string, so the count must be strictly smaller than the destination capacity.
The adapter gets the payload length right, then changes its meaning when making the call. The consumer trusts that meaning, but its own capacity check is also wrong.
Two files, two mistakes
The first file obtains the XML content, checks that it would fit with a terminator, and calls the consumer. Allocation failure, cleanup, and the ordinary maximum-length check are included so the example does not depend on an unrelated leak or unchecked allocation.
xml_adapter.c:
#include <stddef.h>
#include <libxml/tree.h>
#include <libxml/xmlstring.h>
/* Contract: text_len counts payload bytes, excluding the final zero. */
extern int consume_field(const char *text, size_t text_len);
int import_xml_field(const xmlNode *node)
{
enum { FIELD_CAP = 64 };
xmlChar *value = xmlNodeGetContent(node);
if (value == NULL)
return -1;
int n = xmlStrlen(value);
if (n < 0 || (size_t)n >= FIELD_CAP) {
xmlFree(value);
return -1;
}
/* Mistake 1: passes storage size, not payload length. */
int result = consume_field((const char *)value, (size_t)n + 1);
xmlFree(value);
return result;
}
The second file expects a payload count. record_field represents an ordinary application operation that reads the string synchronously and does not retain the pointer. Its implementation is irrelevant to the copy boundary.
field_consumer.c:
#include <stddef.h>
#include <string.h>
extern int record_field(const char *text);
int consume_field(const char *text, size_t text_len)
{
char field[64];
/* Mistake 2: equality leaves no room for the final zero. */
if (text == NULL || text_len > sizeof field)
return -1;
memcpy(field, text, text_len);
field[text_len] = '\0';
return record_field(field);
}
Neither mistake needs unusual XML syntax. The important relationship is between the decoded content length and the destination capacity, not the appearance of the serialized document.
Let C be that capacity and n the payload byte count. The adapter permits n < C, which is appropriate for a terminated destination. But it supplies n + 1 to an interface defined to receive n.
The consumer accepts text_len <= C. At the largest payload permitted by the adapter, the supplied count therefore equals C. The memcpy fills the destination, including the source's existing terminator. The following assignment then writes a second terminator beyond the destination array. The invalid write is that assignment, not an oversized memcpy in this particular path.
This distinction matters during diagnosis. I want to identify the first operation that violates the destination's bounds, then trace the value controlling it back to the caller. Calling every failure a “copy overflow” can obscure which operation is wrong and what the caller contributed.
Both mistakes are necessary for this adapter path to reach the invalid write. If the adapter passes the actual payload count, its existing limit keeps the consumer's terminator inside the array. If the consumer rejects counts equal to capacity, it rejects the adapter's problematic call. Smaller inputs may appear to work despite the disagreement because the extra terminator still fits. That can hide a broken interface until a boundary case is exercised.
The consumer nevertheless has an independent obligation to defend its destination against any caller. The adapter's validation is not a substitute for that obligation.
Repair the meaning and the bound
I would repair both sides rather than rely on one change to contain the other error. The adapter should pass payload length. The consumer should reserve space for the zero it writes.
Corrected xml_adapter.c function, using the same includes and declaration:
int import_xml_field(const xmlNode *node)
{
enum { FIELD_CAP = 64 };
xmlChar *value = xmlNodeGetContent(node);
if (value == NULL)
return -1;
int n = xmlStrlen(value);
if (n < 0 || (size_t)n >= FIELD_CAP) {
xmlFree(value);
return -1;
}
int result = consume_field((const char *)value, (size_t)n);
xmlFree(value);
return result;
}
Corrected field_consumer.c function, using the same includes and declaration:
int consume_field(const char *text, size_t text_len)
{
char field[64];
if (text == NULL || text_len >= sizeof field)
return -1;
memcpy(field, text, text_len);
field[text_len] = '\0';
return record_field(field);
}
The interface still requires that text reference at least text_len readable bytes. A destination check cannot establish the source object's size. It also assumes that embedded zero bytes are not meaningful field data, consistent with this terminated-string XML path. A general binary-data interface would need a different downstream representation.
In production, put the interface declaration and any shared field limit in an authoritative header rather than duplicating constants. Document whether the limit includes termination and whether rejection or truncation is intended. Silent truncation is not a neutral repair when fields carry identifiers or other meaning.
These checks follow ordinary C library requirements: memcpy copies the requested number of bytes without adding termination, and accessing beyond an array is undefined behavior. SEI CERT's STR31-C guidance specifically addresses reserving room for string data and its terminator.
Confirming the checks in the binary
With source available, identifiers can suggest a contract without proving it. A variable called length might mean payload bytes, allocated bytes, or destination capacity. Without source, even those names may be a decompiler's reconstruction. In either case, the caller and callee must be examined together.
A compiled ELF exposes the operations emitted by the compiler. Depending on optimization and available symbols, disassembly or decompiler output can help establish the argument calculation reaching a call, the actual copy count, the comparison controlling acceptance, and the subsequent terminating store. Those observations connect dataflow without requiring the original source.
For this pattern, the useful questions are concrete. Does the caller add one before passing the length? Does the callee permit equality with capacity? Is the same count used for both the copy and the terminating store? Following those relationships is more informative than finding a call to memcpy and treating its presence as a finding.
Binary analysis still requires interpretation. Optimizers can inline operations or transform checks; decompilers infer types and variable names. I would check the emitted comparison and dataflow rather than treat a decompiler's reconstructed condition as authoritative. The binary must also correspond to the build under investigation. A source fix and an older executable can legitimately show different behavior.
The equality error in the consumer is a straightforward candidate for a local warning. Finding it does not require a model or binary analysis. What a caller-to-consumer trace adds is an explanation of how the XML path actually reaches that boundary: a valid payload length becomes a storage-size count before it crosses the interface.
Whole-program static analysis can reason across translation units and detect these relationships too. AddressSanitizer can detect an exercised out-of-bounds access in an instrumented build. Local linting, cross-file analysis, and runtime instrumentation answer overlapping questions at different levels. The practical goal is to establish the length contract and verify the operations that enforce it, not to argue that one tool category cannot find an off-by-one error.
A validation plan for the example should cover empty content, ordinary content, the largest accepted payload, and rejected lengths. Test the consumer directly and through the adapter, including equality with capacity. Direct tests check the sink's obligations; integration tests check the caller's quantity. Confirm that rejection precedes any write, and check accepted values for accidental truncation as well as memory safety. Source instrumentation is an option when a build is available, not an assumption about a binary-only investigation.
Weight provenance is a separate trust question
A useful analysis does not establish everything I need to know about the model producing it. Abliteration or de-refusal targets refusal-related behavior and can produce behavior different from upstream safety tuning. Fine-tuning updates a model through further training. Merging combines model weights; direct weight editing changes parameters without necessarily training them. These are distinct processes, and each can change behavior. Open weights make an artifact inspectable, but do not prove its training history, security properties, or the intent behind its modifications.
In their November 2, 2026 post, Rikiya Takehi, Jayden Teoh, Alex Zhang, and Omar Khattab describe weight-space programming: directly reading and editing weights without gradient descent. They report experiments with small controlled networks and a targeted improvement to a small language model. The work supports taking direct weight manipulation seriously. It does not establish reliable detection of hidden backdoors, and it does not demonstrate malicious cryptographic-code generation.
That last possibility is a hypothetical supply-chain threat: a modified model could generate subtly compromised cryptographic code. It is not an allegation about Qwen, its maintainers, or any country. A model's upstream reputation or refusal behavior is not an authorization boundary for a derivative, nor is willingness to answer evidence that its output is correct.
The next run: OpenCode, REA, and an isolated workload
I recommend OpenCode with REA's MCP integration, using Ghidra as the analysis backend and the Philbert model served locally. REA documents OpenCode support and native investigation tools for decompilation, assembly, and cross-references. This is the proposed stack, not a claim that REA or Ghidra was used in the original investigation.
The planned flow is:
Read-only ELF -> Statio-isolated analysis workload
OpenCode <-> locally served Philbert model
|
Beacon + Lyra -> restricted local REA -> Ghidra
local stdio MCP; no internet-backed tools
Separate validation stage -> narrow authorized path -> designated QA VM
Statio supplies the execution boundary; Beacon mediates governed tool access; Lyra supplies policy evaluation. Expose only the local binary inspection, disassembly, decompilation, and cross-reference operations needed for this task, not REA's broad catalog. Exclude web search, remote fetch, cloud analysis, browser/device discovery, uploads, and runtime capture. Local stdio MCP needs no internet transport, but that alone does not make its tools offline. Server processes and children must inherit filesystem and outbound-network restrictions. Any permitted shell or script must obey the same boundary. Stage approved tools, dependencies, and model weights before isolation; fetch nothing during analysis.
Here is an agentcontainer.json scaffold using the current schema. The image, provider alias, and restricted MCP registration need deployment bindings. The shared platform gap tracked as ALY517 also remains open, so this scaffold is not launch-ready.
{
"$schema": "https://schemas.alyria.ai/agentcontainer/v1.json",
"name": "xml-buffer-analysis",
"image": "alyria/runner-base:2026.11",
"agent": {
"harness": "opencode",
"model": "local/Qwen3.8-27B-Uncensored-Cyber",
"task": "Inspect the supplied ELF at /inputs/client.elf through REA with Ghidra. Trace XML field lengths, copy bounds, and terminating stores. Write defensive findings to /outputs. Do not execute the target or initiate QA validation in this analysis stage."
},
"isolation": { "runtime": "microvm" },
"egress": { "allow": [] },
"mcp": { "servers": ["rea"] },
"actions": {},
"policies": [
{
"kind": "question",
"id": "read-relevance",
"before": "tool.read_file",
"question": {
"type": "yesno",
"text": "Is this read relevant to the authorized binary analysis?"
},
"mode": "monitor"
}
]
}
The schema provides agent.task for the prompt, but no input mounts, model endpoint configuration, or QA credentials. Bind the ELF read-only and /outputs writable outside this JSON. Configure approved local model and analysis endpoints, pin the prepared image, and resolve the restricted REA registration before admission. Empty egress grants no external connection; local services must not proxy internet access. A server declaration is not a per-tool allowlist, and hiding tools is not enforcement. The companion setup notes separate these deployment bindings from supported JSON fields.
T1 deterministic controls should enforce scope. T2 supports structured history restrictions, but target-specific validation requires correctly bound events and authorization state; a sentence in the prompt is not that binding. Keep the QA target separately authorized and inaccessible from this static-analysis stage. T3 is currently Monitor-only: semantic observations can inform review, not hard prevention. The example read event likewise needs an actual harness event mapping.
Constrain what the harness may read, execute, connect to, and change. Layer supported history restrictions and semantic monitoring over deterministic access controls, and log decisions. Neither the model's refusal behavior nor this architecture guarantees correct code or proves model intent. The next run must establish what was actually enforced.