Feb 27, 2026·5 min read·24 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.
CVE-2026-115906 is a cryptographic authentication bypass vulnerability in the Rust zip library (zip-rs/zip2) affecting versions from 0.6.0 prior to 9.0.0. In the WinZip AE-x AES decryption reader implementation within src/aes.rs, the HMAC-SHA1-80 authentication tag is only validated when the internal byte counter (data_remaining) decrements to exactly zero. Because this counter is initialized from the unauthenticated compressed_size header field, an attacker can craft a ZIP archive where the entry stream hits premature physical end-of-file (EOF) before data_remaining reaches zero, causing the reader to return Ok(0) and skip HMAC verification entirely.
CVE-2026-115908 (GHSA-hjqx-j4rw-g7xv) is a high-severity path traversal and link-following vulnerability in the Rust zip library (zip-rs/zip2) affecting versions >= 7.2.0 and < 9.0.0. Because ZipArchive::extract and ZipArchive::extract_unwrapped_root_dir validate symbolic link targets solely via lexical prefix comparisons rather than resolving intermediate filesystem links, an attacker who knows or can guess the absolute extraction path can construct multi-hop symlink chains that escape the destination directory, enabling arbitrary file creation and overwrite with the privileges of the extracting process.
CVE-2026-115907 is an uncontrolled resource consumption vulnerability in the Rust zip crate (versions 5.1.0 through 8.6.0, and 9.0.0 pre-releases) when compiled with the optional legacy-zip feature flag. The vulnerability resides in the legacy decompression decoders—Shrink, Reduce, and Implode—which directly pass an archive-declared 64-bit uncompressed size from a ZIP64 extended information extra field to Vec::reserve() before validating the compressed data stream. By supplying a malformed ZIP archive with a declared uncompressed size exceeding isize::MAX (such as u64::MAX), an unauthenticated attacker can trigger an unhandled capacity overflow panic in the Rust standard library allocator, terminating the calling thread or aborting the entire process.
Twisted versions up to and including 26.4.0 are vulnerable to an unauthenticated remote denial-of-service attack via the HTTP/2 Rapid Reset technique (a variant of CVE-2023-44487). The HTTP/2 connection handler in twisted.web processes pipelined HEADERS and RST_STREAM frames sequentially within the same TCP segment. Because the request dispatch and synchronous rendering occur upon receiving the StreamEnded event before the subsequent StreamReset event is evaluated, an attacker can force the server to expend CPU cycles rendering responses while immediately evicting stream state to bypass concurrent stream limits.
An inefficient algorithmic complexity vulnerability exists in the npm package decode-uri-component prior to version 0.5.1. When processing malformed URI strings containing invalid percent-escape sequences, the library falls back to a custom decoder that collects distinct percent-encoded runs into a dictionary and executes a full-string regular expression replacement for each entry. Supplying a payload with thousands of distinct percent-encoded sequences alongside a malformed escape triggers quadratic O(K * L) execution time, starving the single-threaded Node.js event loop and resulting in an unauthenticated Denial of Service.
Open Neural Network Exchange (ONNX) versions 1.4.0 prior to 1.22.0 contain a null pointer dereference (CWE-476) and a signed-to-unsigned integer conversion error (CWE-195) in the shape inference functions for the Scan operator. When processing an untrusted ONNX model, the functions ScanInferenceFunction, ScanInferenceFunction_opset8, and ScanInferenceFunction_opset9 fail to validate the presence and value of the required num_scan_inputs attribute. Omitting the attribute results in a direct null pointer dereference that crashes the host process via SIGSEGV, while supplying a negative value triggers an integer wrap-around during type narrowing that leads to an unhandled allocation failure. Notably, models containing a negative num_scan_inputs value bypass standard validation via onnx.checker.check_model().