CVE-2026-70456:rsync: Off-by-one out-of-bounds heap write
A historical CVE, retraced through the questions Lachesis asks of vulnerable code.
A malicious rsync sender crafts a post-dot argument line whose glob expansion drives the argument count exactly onto the allocation boundary.
That is the shape of the failure. The rest of this case file follows the evidence behind it.
What was known.
What Lachesis established.
Known before Lachesis ran
The CVE and vulnerable release were selected from public history. We are not claiming novel discovery.
- CVE
- CVE-2026-70456
- Vulnerable target
- v3.4.1 (rsync 3.0.1 through 3.4.x are affected) · vulnerable
- Prior knowledge
- CVE-2026-70456 · v3.4.1 (rsync 3.0.1 through 3.4.x are affected) is vulnerable
Independent re-detection
The experiment graph was built around the vulnerable read_args() implementation (release v3.4.1, io.c:1292-1346), rather than starting from arbitrary rsync source. Lachesis identified the indexed store and reconstructed the argument-count/capacity issue leading to the trailing argv[argc] = NULL write. We did not point the tool at the CVE: enumeration ran over the whole candidate registry (every family), and memory.index surfaced on its own alongside memory.copy and lifecycle.acquire. Every field under sast_output is the enumerator's own emission; everything under adjudication is my reading of the source and fix and is labelled as such.
- Seeded inputs
- None
- Run timestamp
- 2026-08-24T12:06:09Z
- Evidence artifact
- ~/.lachesis/graphs/rsync_read_args.kuzu
The commands and outputs below come from this recorded Lachesis run.
What Lachesis reconstructed.
One historical repository. Four captured queries. A complete source-to-sink argument.
Lachesis follows the same evidence path through unfamiliar repositories.
Here is how the path becomes visible.
These are the recorded questions Lachesis asked of the historical vulnerable code, followed by the raw result and source location each query returned.
Captured runThis block reflects the real blind drive of the graph we built for rsync. We loaded the whole graph, listed every bug family with nothing chosen ahead of time, and the array index store sink came out of that. The handles, capacity inference and index reading below are the facts captured on that drive on that drive. No sender input was seeded; the finding was rediscovered from graph structure, so the mode is independent-redetection.
Load the graph and list every bug family
First we load the graph and ask the tool to list every bug family, with nothing picked ahead of time. It reports all 8 domains and 31 sink constructors and says the census is complete for what the graph can observe. The memory index family is one row among all of them.
{"move": "load_graph", "graph": "/Users/riyandhiman/.lachesis/graphs/rsync_read_args.kuzu", "profile": "all"}
CANDIDATE_CENSUS
move: candidate_census
taxonomy (8):
domain=lifecycle title=Resource lifecycle enumerable=True
domain=memory title=Memory safety enumerable=True
domain=injection title=Injection enumerable=True
domain=navigation title=Request forgery & redirection enumerable=True
domain=object-integrity title=Object integrity enumerable=True
domain=filesystem title=Filesystem enumerable=True
domain=crypto-config title=Cryptography & transport config enumerable=True
domain=resource title=Resource exhaustion enumerable=True
constructors (31): complete_for_observable_graph=True
applied: TrueA small line with a large consequence.
The fix adds a capacity check immediately before the trailing null write. If argument parsing has filled the array, rsync grows it before storing argv[argc], so the terminator always lands inside the allocation.
argv[argc] = NULL; /* no preceding capacity check */
// 3.5.0 inserts `if (argc >= maxargs) argv = realloc_array(argv, char*, maxargs = argc + 1);` immediately before the store.
The original record.
Read the historical advisory and vulnerability record behind this reconstruction.