How my minimal, memory-safe Go rsync steers clear of vulnerabilities - Michael Stapelberg
How my minimal, memory-safe Go rsync steers clear of vulnerabilities
published 2026-05-24
in tags
golang
rsync
Table of contents
Back in January 2025, multiple different security researchers published a total<br>of 6 security vulnerabilities in<br>rsync, some of which<br>allow arbitrary code execution and file leaks, so naturally I was wondering<br>whether/how my gokrazy/rsync implementation<br>was affected. Did implementing my own (compatible, but minimal) rsync in Go, a<br>modern and memory-safe programming language, really rule out entire classes of<br>security vulnerabilities?
This deep dive article was in the making since January 2025, but was delayed<br>because we uncovered more unpublished vulnerabilities in the process! The<br>“Security Vulnerabilities” section now covers all 12 vulnerabilities from the<br>January 2025 batch and the May 2026 batch.
If you are running (upstream, samba)<br>rsync in production, upgrade to version<br>3.4.3 or newer.
If you are running gokrazy/rsync in<br>production, upgrade to version v0.3.3 or newer.
Feel free to skip over the nitty-gritty security issue details and jump directly to:
The verdict on whether using Go has helped.
The verdict on whether a minimal re-implementation like gokrazy/rsync helps.
My comparison with OpenBSD’s openrsync (written in C).
Defense in depth mechanisms one can use on Linux.
The conclusion.
Context: My own rsync
For context, I blogged about rsync, how I use it, and how it<br>works back in June 2022. See also all posts<br>tagged “rsync”.
The original motivation for writing my own rsync (back then only a server, today<br>all directions are supported) was to provide the software packages of distri,<br>my Linux distribution research project for fast package<br>management, which I wanted to host on<br>router7, my small home Linux+Go internet router, which<br>in turn is built on gokrazy, my Go appliance platform.
I am still running multiple gokrazy/rsync servers for this original purpose, and<br>also many others! Having rsync available as a primitive (that you can link into<br>your Go programs!) is really nice.
Security Vulnerabilities
This article covers the following security vulnerabilities:
CVE-2024-12084 to 12088 (original report)
CVE-2024-12747 (discovered separately by Aleksei Gorban “loqpa”)
CVE-2026-29518 (discovered by Damien Neil and myself! and independently by Nullx3D)
CVE-2026-43617 to 43620
CVE-2026-45232
The first batch of the vulnerabilities above was announced on the oss-security<br>mailing list, but<br>note that the original report has more detail compared to the oss-security<br>summaries!
The later vulnerabilities were announced via GitHub Security Advisories on the<br>rsync project.
January 2025 batch
CVE-2024-12084: Heap Buffer Overflow (9.8)
Summary:
rsync performed insufficient validation: It read the (attacker-controlled)<br>checksum length from the network and compared the length against<br>MAX_DIGEST_LEN.
However, rsync’s data structures always declared a 16 byte buffer: char sum2[SUM_LENGTH]
SUM_LENGTH is always 16 (bytes), which is sufficient to hold an<br>MD4 or<br>MD5 checksum.
MAX_DIGEST_LEN used to be 16 (bytes), but can be larger when rsync is<br>compiled with SHA256 or SHA512 checksum support.
Hence, the bounds check was ineffective! An attacker could write out of bounds.
This issue was introduced with commit ae16850 in September<br>2022,<br>which added SHA256/SHA512 checksum support.
Click to expand the full description of the improper checksum length validation (quoting the Google Security<br>report)
When the checksums are read by the daemon, two different checksums are read:
A 32-bit Adler-CRC32 Checksum
A digest of the file chunk. The digest algorithm is determined at the beginning of the protocol negotiation.<br>The corresponding code can be seen below:<br>sender.c:
s->sums = new_array(struct sum_buf, s->count);
for (i = 0; i s->count; i++) {<br>s->sums[i].sum1 = read_int(f);<br>read_buf(f, s->sums[i].sum2, s->s2length);
Most importantly, note that sum2 field is filled with s->s2length bytes. sum2 always has a size of 16:<br>rsync.h
#define SUM_LENGTH 16<br>// …<br>struct sum_buf {<br>OFF_T offset; /**<br>int32 len; /**<br>uint32 sum1; /**<br>int32 chain; /**<br>short flags; /**<br>char sum2[SUM_LENGTH]; /**<br>};
s2length is an attacker-controlled value and can have a value up to MAX_DIGEST_LEN bytes, as the next snipper shows:
io.c
sum->s2length = protocol_version 27 ? csum_length : (int)read_int(f);<br>if (sum->s2length 0 || sum->s2length > MAX_DIGEST_LEN) {<br>rprintf(FERROR, "Invalid checksum length %d [%s]\n",<br>sum->s2length, who_am_i());<br>exit_cleanup(RERR_PROTOCOL);
The problem here is that MAX_DIGEST_LEN can be larger than 16 bytes, depending on the digest support the binary was compiled with:
md-defines.h
#define MD4_DIGEST_LEN 16<br>#define MD5_DIGEST_LEN 16<br>#if defined SHA512_DIGEST_LENGTH<br>#define MAX_DIGEST_LEN SHA512_DIGEST_LENGTH<br>#elif defined SHA256_DIGEST_LENGTH<br>#define MAX_DIGEST_LEN...