My minimal, memory-safe Go rsync steers clear of vulnerabilities

secure1 pts0 comments

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 &ldquo;loqpa&rdquo;)

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...

rsync vulnerabilities security checksum max_digest_len s2length

Related Articles