{"slug": "memory-safety-bugs-in-rust-standard-library", "title": "Memory Safety Bugs in Rust Standard Library", "summary": "Microsoft's FORGE Lab reported several memory safety bugs in Rust's standard library (std and core), including cases where code written entirely in safe Rust can trigger undefined behavior, with findings produced using Hyenas, FORGE's frontier research version of its AI-powered vulnerability-scanning system codename MDASH. The researchers identified three recurring patterns of misplaced trust in unsafe code: trusting code not to panic, trusting user-provided implementations to be correct, and trusting the underlying system, and reported the bugs to the Rust issue tracker for fixes. The findings matter because Microsoft made Rust a Tier-1 internal language alongside C++, C#, and TypeScript, and GitHub rewrote the Copilot agent runtime in Rust, while Bun, Android, the Linux kernel, and Cloudflare are also moving code to the language.", "body_md": "[← All writing](https://forge-blog.github.io/posts/)\n\n# Memory Safety Bugs in Rust Standard Library\n\nMicrosoft’s FORGE Lab found several memory safety issues in Rust’s standard library (`std` and `core`), including cases where code written entirely in safe Rust can trigger undefined behavior. The research used Hyenas, FORGE’s frontier research version of codename MDASH, an AI-powered vulnerability-scanning system. The findings reveal three recurring patterns of misplaced trust in unsafe code: trusting code not to panic, trusting user-provided implementations to be correct, and trusting the underlying system.\n\nRust’s safety guarantees are precisely what make these findings interesting. Its rich type system and ownership model rule out whole classes of memory-safety issues, but much of the standard library relies on unsafe code underneath its safe APIs. When that code breaks its safety contract, even safe Rust can encounter behavior the language is designed to prevent.\n\nRust’s memory-safety guarantees are increasingly important as the language\nbecomes part of software that billions of people rely on every day. Billions of\npeople are estimated to interact with Rust-based software or operating systems\ndaily, often without knowing it.\nMicrosoft made Rust a\n[Tier-1 internal language](https://rustfoundation.org/media/guest-post-rust-is-tier-1-language-at-microsoft/)\nalongside C++, C#, and TypeScript, GitHub\n[rewrote the Copilot agent runtime in Rust](https://github.blog/ai-and-ml/generative-ai/migrating-the-github-copilot-runtime-to-rust-using-copilot/),\nand [Bun](https://bun.com/blog/bun-in-rust),\n[Android](https://blog.google/security/rust-in-android-move-fast-fix-things/),\nthe [Linux kernel](https://lwn.net/Articles/1050174/), and\n[Cloudflare](https://blog.cloudflare.com/20-percent-internet-upgrade/) are\nmoving code to it as well. Much of this software relies on Rust’s foundational\nlibraries, `core` and `std`, and the safety guarantees of the APIs they provide.\nBugs in those libraries can have consequences well beyond the code where they\noriginate.\n\nUnsafe Rust has been a long-running interest for us. Our team includes the\npeople behind [Rudra](https://dl.acm.org/doi/10.1145/3477132.3483570), a static\nanalyzer that looked for memory-safety bugs across the Rust ecosystem in 2021.\nWe also write many of our own tools in Rust, so its safety is more than a\nresearch topic for us. This year, we took a closer look at the standard library\nto see what kinds of mistakes still show up in its unsafe code today.\n\nIn this post, we share some of the bugs we reported to the Rust issue tracker. We use them to discuss what Rust does and doesn’t guarantee, why writing correct unsafe Rust is so difficult, and why Rust’s safety model still matters even though its memory safety isn’t perfect.\n\n## How we found and validated the bugs\n\nAt [Microsoft FORGE Lab](https://www.microsoft.com/en-us/security/blog/2026/10/07/3-lessons-from-frontier-ai-vulnerability-research/), we study vulnerabilities in complex software, including the Rust standard library. Our team combines years of experience in vulnerability research with AI-assisted tools that help us investigate code and find bugs that are difficult to uncover through traditional analysis alone.\n\nSome of the bugs in this post were found with help from Hyenas, our research version of [codename MDASH](https://www.microsoft.com/en-us/security/blog/2026/05/12/defense-at-ai-speed-microsofts-new-multi-model-agentic-security-system-tops-leading-industry-benchmark/). Our researchers investigated the underlying code, reproduced the bugs, assessed their potential security impact, and worked with Rust maintainers to report and fix them.\n\nThis post focuses on soundness bugs in the standard library’s unsafe code. Bugs in the compiler itself, such as miscompilations and holes in the type system, are a different topic that we don’t cover here.\n\n## Patterns of Rust’s Safety Bugs\n\nAt the lowest level, Rust uses\n[`unsafe` blocks](https://doc.rust-lang.org/book/ch20-01-unsafe-rust.html) to\nwork with raw pointers and interact directly with the operating system.\nMuch of the standard library, including `Vec` and `String`,\nis built from small unsafe pieces wrapped in safe APIs.\nInside an `unsafe` block, the programmer takes responsibility for the guarantees\nthe compiler can’t check, and that responsibility extends to the safe API built\naround it. That API must be\n[sound](https://doc.rust-lang.org/nomicon/safe-unsafe-meaning.html): no matter\nhow safe code uses it, it must be impossible to trigger undefined behavior.\n\nUpholding that promise is harder than it looks. Consider a function that returns\nthe byte at `idx` from any type that can be viewed as a byte slice, skipping the\nredundant bounds check after doing its own:\n\n``` php\npub fn get<S: AsRef<[u8]>>(s: &S, idx: usize) -> Option<u8> {\n    if idx < s.as_ref().len() {\n        // SAFETY: we just checked that `idx` is in bounds.\n        Some(unsafe { *s.as_ref().get_unchecked(idx) })\n    } else {\n        None\n    }\n}\n```\n\nThe hard part of unsafe Rust is knowing what you can trust. The function above\nlooks correct, but it assumes that two calls to `as_ref` return the same slice.\nBecause `S` is chosen by the caller, a safe `AsRef`\nimplementation can return a long slice the first time and an empty one the\nsecond, and `get` reads out of bounds. Unsafe code must be careful with every\ncall it makes, and the bugs we found fall into three patterns of misplaced\ntrust: trusting code not to panic, trusting user-provided implementations to be\ncorrect, and trusting the underlying system.\n\n### Pattern 1: Trusting code not to panic\n\nUnsafe code often breaks an invariant for a moment and restores it afterward; for example, it may allocate an uninitialized buffer and fill it directly, avoiding redundant writes to zero the memory first. That’s fine as long as the function runs to the end. But many operations in Rust can panic, from out-of-bounds indexing to calls into user-provided code, and by default a panic unwinds: it leaves the function early while still running destructors. Unwinding inside that window becomes an invisible exit that exposes the broken invariant to code that trusts it. Bugs of this kind are called panic safety bugs.\n\n#### `MapWindows::clone` (#156501)\n\nWe identified\n[a panic safety issue in `MapWindows::clone`](https://github.com/rust-lang/rust/issues/156501).\n[`Iterator::map_windows`](https://doc.rust-lang.org/nightly/std/iter/trait.Iterator.html#method.map_windows),\na nightly API, yields overlapping windows of `N` items. It keeps the current\nwindow in a `Buffer` whose destructor drops the `N` items starting at `start`.\nIn Rust 1.97.0,\n[`library/core/src/iter/adapters/map_windows.rs`](https://github.com/rust-lang/rust/blob/1.97.0/library/core/src/iter/adapters/map_windows.rs#L37-L43)\ndefines it as:\n\n``` js\n37struct Buffer<T, const N: usize> {\n38    // Invariant: `self.buffer[self.start..self.start + N]` is initialized,\n39    // with all other elements being uninitialized. This also\n40    // implies that `self.start <= N`.\n41    buffer: [[MaybeUninit<T>; N]; 2],\n42    start: usize,\n43}\n```\n\n[`Buffer::clone`](https://github.com/rust-lang/rust/blob/1.97.0/library/core/src/iter/adapters/map_windows.rs#L195-L203),\nin the same file, built the new buffer first and filled it afterward:\n\n``` js\n195impl<T: Clone, const N: usize> Clone for Buffer<T, N> {\n196    fn clone(&self) -> Self {\n197        let mut buffer = Buffer {\n198            buffer: [[const { MaybeUninit::uninit() }; N], [const { MaybeUninit::uninit() }; N]],\n199            start: self.start,\n200        };\n201        buffer.as_uninit_array_mut().write(self.as_array_ref().clone());\n202        buffer\n203    }\n204}\n```\n\nAfter line 197, `buffer` breaks its own invariant: its slots are uninitialized,\nbut its destructor is already armed. The `clone()` call on line 201 runs the\nuser’s `T::clone` before the slots are written. If that call panics, unwinding\ndrops `buffer` and runs `drop_in_place` on `N` uninitialized values. Our proof of\nconcept uses only safe code: an element type whose `clone` always panics, which causes\na crash when the destructor accesses uninitialized memory. The\n[fix](https://github.com/rust-lang/rust/commit/4a647a5084fa9100cb506185fa9ee9ed454374ab)\nholds the new buffer in `ManuallyDrop` until it is fully written, so unwinding\nleaks the half-built buffer instead of dropping it.\n\nThe general lesson is to treat every call to code you don’t control, such as\n`clone`, `drop`, comparisons, closures, and iterator methods, as a possible early\nexit. Either finish the work that may panic before breaking an invariant, or\nmake the broken state safe to unwind through: hold it in `ManuallyDrop`, or use\na guard whose destructor restores the invariant, like `Vec`’s internal\n`SetLenOnDrop`. The Rustonomicon covers this under\n[exception safety](https://doc.rust-lang.org/nomicon/exception-safety.html).\n\n#### Allocators must not unwind\n\nThe recently stabilized `Allocator` API shows how hard panic safety is to get\nright. With `std`, an allocation failure\n[aborts the process](https://doc.rust-lang.org/std/alloc/fn.handle_alloc_error.html)\nby default instead of unwinding, so much of the standard library’s unsafe code\nassumes allocation never unwinds. Custom allocators can break that assumption.\nWhile the API was being stabilized, we reported\n[one place in `BTreeMap::insert`](https://github.com/rust-lang/rust/issues/156490)\nthat becomes unsound when an allocator unwinds, and others reported more\n([#159334](https://github.com/rust-lang/rust/issues/159334),\n[#159368](https://github.com/rust-lang/rust/issues/159368),\n[#159370](https://github.com/rust-lang/rust/issues/159370)). Fixing them one by\none would mean auditing everything that allocates, so the\n[stabilization](https://github.com/rust-lang/rust/pull/156882), shipping in\nRust 1.100 on November 12, instead requires implementors never to unwind from\nthe trait’s methods or from `drop`. The proposal notes that “unwinding out of\nmany allocation-related methods was found to be a pervasive source of\nunsoundness.”\n\n### Pattern 2: Trusting user-provided implementations to be correct\n\nUnsafe code often calls into code its caller provides: a comparator, a trait\nimplementation, a closure. That code is safe, but safe doesn’t mean correct, and\nit can return inconsistent or plainly wrong answers. The question is who is\nresponsible when it does. In C++, it’s the caller. `std::sort` requires its\ncomparator to be a strict weak ordering, and breaking that rule is undefined\nbehavior. With libstdc++, sorting equal elements with `<=` reads past the end of\nthe vector:\n\n```\nstd::vector<int> v(100, 42);\n// Bug: `<=` is not a strict weak ordering, since cmp(a, a) is true.\nstd::sort(v.begin(), v.end(), [](int a, int b) { return a <= b; });\n// AddressSanitizer: heap-buffer-overflow in std::__introsort_loop\n```\n\nThe same comparator passed to Rust’s\n[`sort_by`](https://doc.rust-lang.org/std/primitive.slice.html#method.sort_by)\nis only a logic error. The sort may panic or leave the slice in an unspecified\norder, but it never causes undefined behavior. The responsibility sits with the\nunsafe code. The comparator is safe code supplied by the caller, so the unsafe\ncode inside the sort must stay sound no matter what it returns. The same goes for every safe\ntrait a user can implement: comparisons can be inconsistent, an\n`ExactSizeIterator` can report a length that doesn’t match the items it yields,\nand `Borrow` can return a different value on each call.\n\n#### `str::replace` with a custom `Pattern` (#156491)\n\nAnother of our bug reports shows how easy this trust is to miss:\n[`str::replace` with a custom `Pattern` can create a non-UTF-8 `String`](https://github.com/rust-lang/rust/issues/156491).\nString search APIs accept any type that implements the nightly `Pattern` trait,\nand a pattern can describe itself through `as_utf8_pattern`. `str::replace` uses\nthat description to take a fast path when replacing one ASCII byte with another.\nIn Rust 1.96.0, the fast path in\n[`library/alloc/src/str.rs`](https://github.com/rust-lang/rust/blob/1.96.0/library/alloc/src/str.rs#L268-L278)\nlooked like this:\n\n``` php\n268pub fn replace<P: Pattern>(&self, from: P, to: &str) -> String {\n269    // Fast path for replacing a single ASCII character with another.\n270    if let Some(from_byte) = match from.as_utf8_pattern() {\n271        Some(Utf8Pattern::StringPattern([from_byte])) => Some(*from_byte),\n272        Some(Utf8Pattern::CharPattern(c)) => c.as_ascii().map(|ascii_char| ascii_char.to_u8()),\n273        _ => None,\n274    } {\n275        if let [to_byte] = to.as_bytes() {\n276            return unsafe { replace_ascii(self.as_bytes(), from_byte, *to_byte) };\n277        }\n278    }\n```\n\n[`replace_ascii`](https://github.com/rust-lang/rust/blob/1.96.0/library/alloc/src/str.rs#L708-L712),\nin the same file, then rewrites the bytes:\n\n``` php\n708unsafe fn replace_ascii(utf8_bytes: &[u8], from: u8, to: u8) -> String {\n709    let result: Vec<u8> = utf8_bytes.iter().map(|b| if *b == from { to } else { *b }).collect();\n710    // SAFETY: We replaced ascii with ascii on valid utf8 strings.\n711    unsafe { String::from_utf8_unchecked(result) }\n712}\n```\n\nThe `CharPattern` branch on line 272 checks that the character is ASCII, but the\n`StringPattern` branch on line 271 assumes a one-byte string pattern must be\nASCII. That holds for `&str` patterns, but `StringPattern` stored `&[u8]`, and\n`as_utf8_pattern` is a safe method any `Pattern` can implement. In our proof of\nconcept, a custom pattern reports the single byte `0x80`, which only appears\ninside multibyte UTF-8 sequences:\n\n```\nimpl Pattern for NonAsciiBytePattern {\n    // ...\n    fn as_utf8_pattern(&self) -> Option<Utf8Pattern<'_>> {\n        static NEEDLE: [u8; 1] = [0x80];\n        Some(Utf8Pattern::StringPattern(&NEEDLE))\n    }\n}\n\n// \"\\u{d000}\" is [ed, 80, 80]; the result is [ed, 3f, 3f], which is not UTF-8.\nlet corrupted = \"\\u{d000}\".replace(NonAsciiBytePattern, \"?\");\n```\n\n`str::replace` rewrites the continuation bytes and returns a `String` that isn’t\nvalid UTF-8, and later string operations that rely on UTF-8 reach undefined\nbehavior. The\n[fix](https://github.com/rust-lang/rust/pull/156556) changed `StringPattern` to\nhold `&str` instead of `&[u8]`, so the type itself now guarantees UTF-8.\n\nWhen unsafe code depends on a property of a user-provided implementation, don’t\ntake it on faith. Call the implementation once and keep the result, validate\nwhat it returns, or encode the property in a type that safe code can’t forge, as\nthis fix did. If none of those work, make the trait `unsafe` so implementors\nexplicitly promise to uphold it.\n\n#### More examples\n\n`join` shows how easy it is to fix this kind of bug only partway. In 2020, we\nreported that it trusted `Borrow::borrow` to return the same string on each call\n([CVE-2020-36323](https://rustsec.org/advisories/CVE-2020-36323.html)), and the\nfix handled strings that shrink between calls. In April 2026, the same function\n[needed another fix](https://github.com/rust-lang/rust/pull/155708): `join`\nborrows the first element twice, and if it grows on the second call, the\nremaining-length calculation underflows into a heap overflow. The earlier fix\nhad never covered that path.\n\nOthers reported similar problems in the `Pattern` API this year. A safe\n`Pattern` can hand back a valid searcher built for a different string, and\nconsumers such as `strip_prefix_of` apply its ranges to the original string\nunchecked, reading out of bounds\n([#158265](https://github.com/rust-lang/rust/issues/158265)). The same problem\nturned up in `split_once`\n([#165076](https://github.com/rust-lang/rust/issues/165076)) after a report that\nthe `Searcher` contract doesn’t spell out what `str::split` relies on\n([#164904](https://github.com/rust-lang/rust/issues/164904)). Both bugs are\nstill open.\n\n### Pattern 3: Trusting the underlying system\n\nThe first two patterns stay inside Rust code. The third sits at the boundary with the operating system, where unsafe code trusts process-wide state such as signal stacks, memory mappings, and the environment, which other threads, C libraries, or the kernel can touch at any time. These bugs have been hard to find automatically, because a static analyzer would need to model all of that surrounding behavior. AI agents change the picture: they understand enough about the environment the code runs in to make this area practical to search. We started exploring it and found some interesting bugs.\n\n#### `process::exit` and the main thread’s signal stack (#161018)\n\nWhen a thread overflows its stack, Rust prints a clear message and aborts instead of crashing with a bare segmentation fault:\n\n```\nthread 'main' (3307186) has overflowed its stack\nfatal runtime error: stack overflow, aborting\n```\n\nThis relies on a few pieces of process-wide state. Each thread’s stack ends in a\nguard page, an inaccessible region just past the stack’s limit: `pthread`\nreserves one for every spawned thread, and on Linux the kernel’s own stack guard\nplays that role for the main thread. Touching it raises `SIGSEGV`, and Rust\nregisters a handler for that signal. The handler can’t run on the stack that just\noverflowed, so Rust also gives each thread an alternate signal stack, a small\nseparate mapping with a guard page of its own, which the kernel switches to when\nit delivers the signal. The handler checks that the faulting address is inside\nthe guard page, prints the message, and aborts.\n\n[We found](https://github.com/rust-lang/rust/issues/161018) that calling\n`process::exit` from a child thread tears down the main thread’s alternate stack\nwhile the main thread is still running. `process::exit` runs the runtime’s\ncleanup before calling libc’s `exit`, and in Rust 1.98.0 that cleanup ends in\n[`library/std/src/sys/pal/unix/stack_overflow.rs`](https://github.com/rust-lang/rust/blob/1.98.0/library/std/src/sys/pal/unix/stack_overflow.rs#L196-L203):\n\n```\n196pub unsafe fn cleanup() {\n197    if cfg!(panic = \"immediate-abort\") {\n198        return;\n199    }\n200    // FIXME: I probably cause more bugs than I'm worth!\n201    // see https://github.com/rust-lang/rust/issues/111272\n202    unsafe { drop_handler(MAIN_ALTSTACK.load(Ordering::Relaxed)) };\n203}\n```\n\nLine 202 passes `MAIN_ALTSTACK`, the main thread’s alternate stack, to\n[`drop_handler`](https://github.com/rust-lang/rust/blob/1.98.0/library/std/src/sys/pal/unix/stack_overflow.rs#L297-L301):\n\n```\n297// SAFETY: we warned the caller this disables the alternate signal stack!\n298unsafe { sigaltstack(&disabling_stack, ptr::null_mut()) };\n299// SAFETY: We know from `get_stackp` that the alternate stack we installed is part of\n300// a mapping that started one page earlier, so walk back a page and unmap from there.\n301unsafe { munmap(data.sub(page_size), sigstack_size + page_size) };\n```\n\nLine 298 disables the alternate stack of the calling thread, but line 301 unmaps\nthe main thread’s. The code assumes it runs on the main thread, which isn’t true\nwhen a child thread calls `process::exit`. The main thread keeps running until\nlibc’s `exit` finishes, with its signal stack registration still pointing at\nunmapped memory. If it overflows its stack in that window, the kernel can’t\ndeliver the signal, and our proof of concept dies with a plain segmentation\nfault instead of Rust’s message. The bug dates back to\n[2015](https://github.com/rust-lang/rust/pull/28069), and the\n[fix](https://github.com/rust-lang/rust/pull/161037) stops cleaning up the main\nthread’s alternate stack altogether.\n\nWe also checked whether this could become a security issue. In theory, it could\nlead to code execution: if a writable mapping landed at the old alternate\nstack’s address, the kernel would deliver the main thread’s next signal onto it,\nand another thread could overwrite the handler’s frame, such as its return\naddress, while it runs. In practice, the conditions are very tight. Everything\nhas to happen in the brief window before libc’s `exit` finishes, the new mapping\nmust have exactly the right size to reuse that address, and overwriting the\nframe takes a second race inside a short handler. For issue #161018, our testing\nfound no reliable exploitation path under the conditions examined, and we\nassessed practical exploitation as unlikely. This assessment does not\nautomatically apply to the other issues discussed.\nThe [issue](https://github.com/rust-lang/rust/issues/161018) has more details in\nits security impact analysis.\n\n#### `CommandExt::exec` and the global environment (#156951)\n\n[Our other report](https://github.com/rust-lang/rust/issues/156951) involves the\nprocess environment. On Unix, `CommandExt::exec` replaces the current process\nwith a new program. When the command has its own environment variables, `exec`\npoints the process-global `environ` at a new array, calls `execvp`, and restores\n`environ` if `execvp` fails. In Rust 1.99.0, `Command::exec` takes the\nenvironment lock in\n[`library/std/src/sys/process/unix/unix.rs`](https://github.com/rust-lang/rust/blob/1.99.0/library/std/src/sys/process/unix/unix.rs#L242-L244):\n\n```\n242let _lock = sys::env::env_read_lock();\n243\n244let Err(e) = self.do_exec(theirs, envp.as_ref());\n```\n\n[`Command::do_exec`](https://github.com/rust-lang/rust/blob/1.99.0/library/std/src/sys/process/unix/unix.rs#L409-L414),\nin the same file, then swaps the environment:\n\n```\n409    _reset = Some(Reset(*sys::env::environ()));\n410    *sys::env::environ() = envp.as_ptr();\n411}\n412\n413libc::execvp(self.get_program_cstr().as_ptr(), self.get_argv().as_ptr());\n414Err(io::Error::last_os_error())\n```\n\nLine 242 takes only the read side of the environment lock, yet line 410 writes\nthe process-global `environ`; line 409 saves the old value so it can be restored\nif `execvp` on line 413 fails. Writing `environ` under a read lock lets other\nthreads in. A thread calling `env::var` takes the same read lock and can see\nvariables meant only for the new program, and two failed `exec` calls racing\neach other can leave `environ` pointing at a freed array that a later\n`env::vars_os()` reads. Both proofs of concept use only safe Rust. A\n[2018 fix](https://github.com/rust-lang/rust/pull/55939) held an exclusive lock\nhere, but when the environment lock became a\n[read-write lock](https://github.com/rust-lang/rust/pull/81850) in 2021, `exec`\nkept the read side. A fix that avoids touching `environ` by calling `execve`\ndirectly is [in review](https://github.com/rust-lang/rust/pull/157144).\n\nThe environment has caused trouble before. After a\n[discussion that began in 2015](https://github.com/rust-lang/rust/issues/27970),\nthe 2024 edition\n[marked `env::set_var` as `unsafe`](https://doc.rust-lang.org/edition-guide/rust-2024/newly-unsafe-functions.html),\nbecause C functions on most Unix platforms read the environment without going\nthrough Rust’s lock.\n\n#### `process::exit` and thread-local destructors (#164243)\n\nGetting this boundary right is hard, and more problems are still open. While\nexploring this area, we independently ran into a double free in safe code that\n`process::exit` can cause through thread-local destructors. It was reported by\nsomeone else ([#164243](https://github.com/rust-lang/rust/issues/164243)), but\nit’s worth mentioning here. libc’s `exit` first runs the calling thread’s\nthread-local destructors, as C++ requires, and Rust registers its destructors\nthe same way. If a destructor calls `process::exit` while a thread local is\nbeing replaced, libc drops that thread local a second time. A\n[proposed fix](https://github.com/rust-lang/rust/pull/165125) switches\n`process::exit` to POSIX `_exit`, which ends the process without running these\nhandlers.\n\n## Conclusion\n\nEvery unsafe block rests on assumptions about the code around it. The bugs in this post came from three kinds of misplaced trust:\n\n- that code won’t panic at the wrong moment,\n- that a caller-provided implementation behaves correctly, and\n- that the process-wide state Rust shares with the operating system and the C library stays the way the code expects.\n\n### Familiar patterns are still difficult (Patterns 1 and 2)\n\nThe first two patterns are well studied. [Rudra](https://dl.acm.org/doi/10.1145/3477132.3483570) looked for both in 2021, and the standard library was already fixing panic safety bugs in 2015 ([CVE-2015-20001](https://rustsec.org/advisories/CVE-2015-20001.html)). They remain challenging to avoid because any call inside unsafe code might unwind or run code the caller supplied.\n\nWhat we saw this year was mixed. On the positive side, the nightly process is\nworking. Many reports were caught while the affected APIs were still unstable,\nand several were fixed by changing the design before stabilization.\n`StringPattern` now [holds `&str` instead of `&\\[u8\\]`](https://github.com/rust-lang/rust/pull/156556),\nand `Allocator` was\n[stabilized](https://github.com/rust-lang/rust/pull/156882) with a rule that\nimplementors must not unwind. At the same time, bugs in these patterns still\nreach stable releases, and their fixes aren’t always complete. `join` was fixed\nin 2020 and\n[needed another fix](https://github.com/rust-lang/rust/pull/155708) in 2026 for\na path the first fix had missed.\n\n### System boundary bugs are now within reach (Pattern 3)\n\nThe third pattern is different. Its bugs live where Rust meets the layers below\nit: the kernel, through system calls such as `sigaltstack` and `munmap`, and the\nC library, through process-wide state such as `environ` and the cleanup that\nlibc’s `exit` runs. Getting them right takes knowledge that lives outside\nRust’s type system, which has made these bugs difficult to find with\ntraditional static analysis. That may also be why they last so long. The\n`process::exit` signal stack bug was in the standard library for nearly 11\nyears, and `exec` has written `environ` under a read lock since 2021. Unlike our\nreports in the first two patterns, both affect stable Rust.\n\nLanguage models already know a great deal about how these interfaces behave, so agentic scanners can check unsafe code against the system it runs on, not just the language. We think this boundary is one of the most promising places for agentic security research, in Rust and beyond.\n\n### Rust’s high bar is hard to meet but worth keeping\n\nRust holds its safe APIs to a stricter standard than C and C++. Safe code must never be able to cause undefined behavior, no matter how it uses an API. In C++, a comparator that breaks `std::sort`’s ordering requirement causes undefined behavior, while Rust’s `sort` must stay memory-safe with any `Ord` implementation. POSIX doesn’t guarantee that `setenv` is safe to call while another thread reads the environment. That makes memory safety bugs in Rust hard to compare one-to-one with bugs in other languages.\n\nFor the same reason, a soundness bug in the standard library isn’t automatically\na vulnerability in the programs that use it. Each bug in this post needs an\nunusual setup to trigger, such as a panic at exactly the wrong moment, a\ndeliberately misbehaving implementation, or calling `process::exit` from a child\nthread. It becomes a serious vulnerability only if a real program reaches the\nAPI in that way.\n\nRust’s safety standard is strict and difficult to meet, but it’s also useful. It gives an unambiguous answer to what counts as a soundness bug, so problems can be fixed and prevented once in the library, instead of in every program that uses it. Rust’s safety model strikes the right balance between control, safety, and performance for systems programming, which is why so many projects choose it.\n\n## Beyond the lab\n\nAt Microsoft FORGE Lab, we’re exploring how technology such as codename MDASH can help researchers find bugs like the ones in this post at greater scale. Rust’s standard library is one of several foundational open source projects where we’re applying this approach.\n\n**This research also extends beyond the lab.** It helps inform the development of codename MDASH, which brings agentic vulnerability discovery into Microsoft Defender. These findings can also serve as inputs to Microsoft Perception, our new multi-agent security system, where specialized agents work together across broader security workflows. For example, the Web App Pen Test Agent can use codename MDASH findings to help prioritize testing, validate exploitability against live applications, and produce actionable vulnerabilities that downstream agents can use to prioritize and support remediation. This connects AI-powered code analysis with real-world security validation and response, helping security teams move from identifying potential vulnerabilities to understanding and addressing actual risk.\n\nOur goal isn’t to simply find more bugs, it’s to identify meaningful issues, turn those findings into actionable research, and improve the safety of foundational software that developers and organizations depend on.", "url": "https://wpnews.pro/news/memory-safety-bugs-in-rust-standard-library", "canonical_source": "https://forge-blog.github.io/posts/rust-std-memory-safety/", "published_at": "2026-11-09 22:56:15+00:00", "updated_at": "2026-11-09 23:23:23.858059+00:00", "lang": "en", "topics": ["ai-safety", "ai-research", "artificial-intelligence"], "entities": ["Microsoft", "Microsoft FORGE Lab", "Rust", "Hyenas", "MDASH", "GitHub", "Bun", "Cloudflare"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/memory-safety-bugs-in-rust-standard-library", "markdown": "https://wpnews.pro/news/memory-safety-bugs-in-rust-standard-library.md", "text": "https://wpnews.pro/news/memory-safety-bugs-in-rust-standard-library.txt", "jsonld": "https://wpnews.pro/news/memory-safety-bugs-in-rust-standard-library.jsonld"}}