A deep analysis of the twelve rsync security flaws disclosed between January 2025 and May 2026, showing how a minimal Go implementation (gokrazy/rsync) avoids them through memory safety, zero‑initialisation, and the traversal‑resistant os.Root API, and comparing the result with OpenBSD’s openrsync and traditional hardening techniques.
How My Minimal, Memory‑Safe Go rsync Steers Clear of Vulnerabilities
Published 2026‑05‑24 • Tags: golang, rsync
In January 2025 a wave of six security advisories hit the upstream rsync project, exposing heap overflows, info leaks, symlink races and other bugs that could lead to remote code execution or file disclosure. The natural question was whether a re‑implementation written in Go – a language that guarantees bounds checking and zero‑initialisation – would automatically be immune to those classes of flaws. The answer turned out to be “mostly, but not automatically”. In the months that followed we discovered additional, unpublished issues, extending the total to twelve CVEs. This article walks through each vulnerability, explains how Go’s guarantees helped (or did not help), and shows how the minimal design of gokrazy/rsync further reduced the attack surface.

1. The Vulnerabilities Covered
| CVE | Primary Cause | Impact | Upstream Fix | Go mitigation |
|---|---|---|---|---|
| 2024‑12084 | insufficient checksum‑length validation | heap buffer overflow | dynamic sum2 allocation, proper length check | runtime panic on out‑of‑bounds write |
| 2024‑12085 | missing zero‑initialisation of checksum buffer | one‑byte stack leak | initialise sum2 to zero | variables are zero‑initialised by default |
| 2024‑12086 | missing path sanitisation on receiver | arbitrary file leak | verify destination path before opening | os.Root prevents traversal |
| 2024‑12087 | symlink‑based path traversal in incremental recursion | write outside destination | validate merged file list | not applicable – incremental recursion omitted |
| 2024‑12088 | unsafe‑symlink validation bypass | create arbitrary symlinks | stricter unsafe_symlink logic | not applicable – safe‑links flag not implemented |
| 2024‑12747 | TOCTOU race on open() of regular files | leak privileged data | use O_NOFOLLOW on open | os.Root provides per‑component O_NOFOLLOW semantics |
| 2026‑29518 | TOCTOU on parent directory components (daemon mode) | privilege escalation | secure_relative_open() | os.Root API resolves paths safely |
| 2026‑43617 | hostname lookup after chroot | ACL bypass | move DNS lookup earlier | not relevant – only IP‑based ACLs are used |
| 2026‑43618 | integer overflow in compressed‑token decoder | memory disclosure | add overflow checks | panic on overflow (bounds check) |
| 2026‑43619 | TOCTOU on many *at syscalls | privileged file overwrite | use RESOLVE_BENEATH‑style calls | os.Root covers all affected syscalls |
| 2026‑43620 | out‑of‑bounds read in recv_files() | deterministic crash | add parent_ndx guard | panic on out‑of‑bounds access |
| 2026‑45232 | off‑by‑one write in HTTP CONNECT proxy code | stack corruption | validate proxy response length | not applicable – proxy support absent |
The table demonstrates that twelve‑out‑of‑twelve issues are either prevented by Go’s runtime guarantees, mitigated by the os.Root API, or avoided because the vulnerable feature is simply not present in the minimal implementation.
2. How Go’s Guarantees Helped
Bounds Checking Turns Overflows into Panics
When the upstream rsync code wrote past a 16‑byte checksum buffer (CVE‑2024‑12084) the process could corrupt adjacent heap structures and continue execution. In Go the same operation triggers a runtime panic because every slice access is checked against its length. A panic aborts the goroutine, which we later converted into a clean error return, turning a potential remote‑code‑execution vector into a denial‑of‑service that is far easier to contain.
Zero‑Initialisation Eliminates Uninitialised‑Memory Leaks
CVE‑2024‑12085 relied on the fact that the checksum buffer was left partially uninitialised, leaking a byte of stack data. Go zeroes all allocated memory, so even if the length check failed the contents would be deterministic zeros, making the leak moot.
Traversal‑Resistant File APIs Close TOCTOU Gaps
The most subtle class of bugs involved time‑of‑check‑to‑time‑of‑use races on symbolic links and directory components (CVE‑2024‑12747, CVE‑2026‑29518, CVE‑2026‑43619). The newly introduced os.Root API (first in Go 1.24, expanded in 1.25) opens a directory and then performs all subsequent operations relative to the opened file descriptor, using the kernel’s openat2/ O_RESOLVE_BENEATH semantics. This eliminates the window between a path check and the actual system call, because the kernel resolves each component atomically with the required O_NOFOLLOW flag.
3. Minimalism as a Defensive Strategy
The original motivation for gokrazy/rsync was to provide a lightweight, link‑able rsync library for the router‑focused Go appliance platform gokrazy. To keep the binary small and the codebase approachable, the implementation targets protocol version 27 only. Features introduced in later protocol versions – SHA‑256 checksums, incremental recursion, compression, safe‑links handling – are deliberately omitted.
Because many of the CVEs exploit precisely those newer features, the minimal design automatically sidesteps them. For example, the checksum‑length overflow (CVE‑2024‑12084) is still relevant because checksum handling exists, but the lack of incremental recursion means the path‑traversal bug (CVE‑2024‑12087) never appears. This “attack‑surface reduction by omission” is a concrete illustration of the principle complexity begets risk.
4. Comparison with OpenBSD’s openrsync
OpenBSD’s openrsync is a C implementation that emphasizes security through system‑call confinement (unveil(2), pledge(2)). It also avoids many of the same pitfalls:
- It supports only a single checksum algorithm (MD4), so the overflow in CVE‑2024‑12084 cannot be triggered.
- It never implements incremental recursion, thus sidestepping CVE‑2024‑12087 and CVE‑2026‑43620.
- All file operations are performed behind unveil and pledge, providing a strong sandbox.
However, openrsync still suffered from the symlink race discovered in 2026 because it relied solely on O_NOFOLLOW without the per‑component resolution that modern kernels provide. In contrast, gokrazy/rsync uses os.Root, which internally maps to the same kernel facilities, thereby fixing the issue.
5. Defense‑in‑Depth on Linux
Beyond the language‑level guarantees, several Linux mechanisms can be layered on top of gokrazy/rsync:
- Mount and PID namespaces – isolate the rsync process in its own filesystem view; useful for server deployments where root privileges are acceptable.
- systemd hardening – the supplied service file enables DynamicUser, ProtectHome, and ReadOnlyDirectories to limit the process’s capabilities.
- Landlock – an unprivileged LSM that lets a program declare a per‑process access policy (read‑only source, read‑write destination). Implemented in March 2025, it works well with Go because the policy can be built after the program has determined the relevant directories.
- os.Root – the most fine‑grained defense; it replaces traditional path‑based APIs with descriptor‑relative calls, closing the TOCTOU window for every file‑system operation.
When combined, these measures make a compromised rsync daemon extremely difficult to leverage for privilege escalation, even if a future bug were to slip through.
6. The Verdict
Does Go Help?
Yes, but with nuance. Go’s automatic bounds checks and zero‑initialisation neutralise the majority of classic memory‑corruption bugs. The os.Root API provides a clean, idiomatic way to avoid path‑traversal races that historically required careful C‑level handling. Only one of the twelve CVEs (the hostname‑ACL bypass) was a pure logic error that Go could not prevent, and that issue does not affect the default configuration of gokrazy/rsync.
Does Minimal Re‑Implementation Help?
Absolutely. By refusing to implement protocol features that are not needed for the target use‑case, the code avoids the very code paths that contain many of the vulnerabilities. The trade‑off is fewer capabilities, but for router‑backed backup scenarios the reduction in complexity is a net win.
How Does OpenBSD’s openrsync Compare?
OpenBSD’s approach relies on OS‑level sandboxing and a deliberately small feature set, achieving a similar security posture. The main difference is that gokrazy/rsync can be compiled for any Linux target and still benefit from the same per‑operation safety via os.Root, without needing OpenBSD‑specific syscalls.
7. Conclusion
The twelve vulnerabilities examined share two common roots: missing or incorrect validation, and the introduction of new protocol features without revisiting existing checks. Go’s design eliminates the first class by making validation the default (bounds checks, zero‑init). The second class is mitigated by the minimal implementation philosophy and by the os.Root API, which offers a safe‑by‑default way to interact with the filesystem.
For anyone running rsync in production, the practical advice is simple:
- Upgrade upstream rsync to 3.4.3 or newer.
- If you rely on the Go implementation, upgrade to gokrazy/rsync v0.3.3 or later.
- Consider enabling Linux hardening (mount namespaces, Landlock, systemd sandboxing) for daemon deployments.
- Keep the feature set as small as your workflow allows – complexity is the enemy of security.
By embracing memory safety, zero‑initialisation, and a disciplined, minimal design, a Go‑based rsync can indeed steer clear of the majority of the vulnerabilities that have plagued its C counterpart.
Feel free to subscribe to the RSS feed for future deep‑dives into Go security and low‑level networking.

Comments
Please log in or register to join the discussion