How My Minimal, Memory‑Safe Go rsync Steers Clear of Vulnerabilities
#Vulnerabilities

How My Minimal, Memory‑Safe Go rsync Steers Clear of Vulnerabilities

Tech Essays Reporter
7 min read

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.

Featured image


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:

  1. Mount and PID namespaces – isolate the rsync process in its own filesystem view; useful for server deployments where root privileges are acceptable.
  2. systemd hardening – the supplied service file enables DynamicUser, ProtectHome, and ReadOnlyDirectories to limit the process’s capabilities.
  3. 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.
  4. 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

Loading comments...