Feb 27, 2026·5 min read·6 visits
The `TimeBuf` struct in `gix-date` publicly implemented `std::io::Write`, allowing callers to write raw, invalid UTF-8 bytes into its internal buffer. When `as_str()` was called, it created a `&str` from this corrupted data, triggering Undefined Behavior (UB). Fix: Upgrade to 0.12.0, where the `Write` implementation was removed.
In the world of Rust, the `&str` type is a sacred contract: it promises, under penalty of undefined behavior, that the underlying bytes are valid UTF-8. CVE-2026-0810 is a story of how `gix-date`, a core component of the `gitoxide` project, broke that promise by being too generous with its API surface. By implementing `std::io::Write` for a structure intended to hold a string, the developers inadvertently allowed arbitrary raw bytes—including invalid UTF-8—to be injected into a buffer that was later cast to a string slice. This violation of Rust's safety guarantees turns a simple formatting utility into a potential memory corruption vector.
Rust is famous for its paranoia. It treats memory access like a bomb diffusal operation, forcing you to prove safety before it lets you cut the red wire. One of the most fundamental invariants in the language is the &str type. Unlike C's char* or C++'s std::string, a Rust string slice isn't just a bag of bytes; it is mathematically guaranteed to be valid UTF-8. The compiler optimizes code assuming this is true. Standard library functions rely on it. If you manage to create a &str that points to garbage non-UTF-8 data, you haven't just created a bug—you've entered the realm of Undefined Behavior (UB).
Enter gix-date, a library responsible for parsing and formatting time in gitoxide, the pure-Rust implementation of Git. Time parsing sounds boring, right? It's just numbers and colons. But gix-date needed a buffer to format these dates into strings. They created TimeBuf.
The problem wasn't what TimeBuf did; it was what it allowed others to do. In an effort to be ergonomic (or perhaps just standard-compliant), the developers implemented a trait that essentially unlocked the back door to the memory safe house and left the keys in the ignition.
The vulnerability lies in a classic case of API over-exposure. The TimeBuf struct wraps a byte buffer (Vec<u8> or similar). To make it easy to write data into this buffer, the developers implemented std::io::Write for TimeBuf.
impl std::io::Write for TimeBuf {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.buf.write(buf)
}
// ...
}This looks innocent. Write is a standard trait. However, TimeBuf also had a method called as_str(). This method returns a &str. For that to be safe, the internal buffer must contain valid UTF-8.
By implementing Write publicly, the library effectively said: "Hey, anyone with a mutable reference to this object can write whatever bytes they want." This bypasses any validation logic the library might have had. An attacker (or just a clumsy developer) could use the Write trait to inject 0xFF (an invalid UTF-8 byte) into the buffer. The buffer accepts it happily because Write deals in bytes, not strings. But the moment someone calls as_str(), the code performs a cast that assumes the data is valid. The invariant is broken, and the compiler's assumptions are shattered.
Let's look at the smoking gun. The code below shows the vulnerability in its simplest form. It’s not a complex buffer overflow or a heap groom; it’s a type confusion induced by bad API design.
use gix_date::parse::TimeBuf;
use std::io::Write;
fn main() {
let mut buf = TimeBuf::default();
// 1. The Trojan Horse: Write trait allows arbitrary bytes
// 0xff is NOT valid UTF-8.
buf.write(&[0xff]).unwrap();
// 2. The Trigger: Interpreting garbage as a string
// This likely uses std::str::from_utf8_unchecked internally or essentially equivalent logic
// creating a `&str` that violates the language spec.
let s = buf.as_str();
// 3. The UB: Using the malformed string
println!("This is undefined behavior: {}", s);
}When buf.as_str() is called, it returns a slice pointing to [0xff]. If the program tries to iterate over the characters of this string, search it, or print it, the underlying UTF-8 decoder will likely panic, or worse, read past the bounds if the length calculation was optimized based on UTF-8 assumptions. In Rust, UB means "anything can happen," including remote code execution if the stars align.
You might be thinking, "So it crashes. Big deal." In the context of a library like gitoxide, which is designed to process git repositories (often untrusted ones), stability is paramount. But UB is worse than a crash.
When the compiler sees code that operates on a &str, it often elides bounds checks or makes optimization decisions based on the assumption that the data is valid UTF-8. For example, if you iterate over a &str, the iterator knows it can't land in the middle of a multibyte sequence. If you feed it invalid data, that iterator might skip the null terminator or jump into unmapped memory.
In a security context, if gix-date is used in a server-side component processing git objects, an attacker could craft a payload that triggers this UB. Best case: they DoS your server. Worst case: they exploit the memory corruption to bypass logic checks. While CVSS 7.1 suggests "High" severity, the specific impact depends heavily on how the consuming application handles the resulting malformed string.
The fix provided by the Gitoxide team in commit 76376ef5e97c63e108db0c9fe2eb096f4bfe70f7 is elegantly simple: they removed the feature.
// gix-date/src/parse/mod.rs
// DELETED:
// impl std::io::Write for TimeBuf { ... }
// MODIFIED:
impl Time {
pub fn to_str<'a>(&self, buf: &'a mut TimeBuf) -> &'a str {
buf.clear();
// Instead of writing to 'buf' via the trait, they access the internal buffer directly
// inside the module, where they can guarantee the output is valid ASCII/UTF-8.
self.write_to(&mut buf.buf)
.expect("write to memory of just the right size cannot fail");
buf.as_str()
}
}By removing impl Write for TimeBuf, the developers enforced encapsulation. Now, the only way to get data into TimeBuf is through specific methods provided by the library (like Time::to_str), which are written to ensure they only produce valid text. The internal buffer (buf.buf) is no longer exposed to the chaotic whims of the outside world via a generic trait.
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
gix-date GitoxideLabs | < 0.12.0 | 0.12.0 |
gitoxide GitoxideLabs | < 0.12.0 | 0.12.0 |
| Attribute | Detail |
|---|---|
| CWE | CWE-135 (Incorrect Calculation of Multi-Byte String Length) |
| CVSS v3.1 | 7.1 (High) |
| Vector | AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H |
| EPSS Score | 0.00004 (Low Probability) |
| Attack Vector | Local / Library API Misuse |
| Patch Commit | 76376ef5e97c63e108db0c9fe2eb096f4bfe70f7 |
The application does not correctly calculate the length of a multi-byte string, leading to potential memory corruption or undefined behavior.