RipGrep musl binaries occasionally segfault during very-large searches (github.com)

233 points by throwaway2037 10 hours ago

ndesaulniers 6 hours ago

Heh, from the kernel patch: https://lore.kernel.org/all/CALCETrXbj__SFQMzPZhES5y6-sh4np-...

> I saw a fun bug report in ripgrep and a studious but pretty bad AI-generated analysis

Referring to https://github.com/dfoxfranke/ripgrep-3494-analysis which I indeed thought "that's an awful lot written to have been written by a human."

Looks like that thread is from...today!

gruntled-worker 4 minutes ago

Until ~2000, "cellphone user in public" equated with "smug asshole." We're at that icky rejection stage with AI.

Two years ago, that writeup would have been viewed as a generous gift of time to the community. Now we can't be bothered to read through it because we know where it came from, and that it's just worth $0.06 in tokens. Another reason (I think) is because we know what it portends.

In a few years, digging manually through bugs will probably be a last resort. We'll go from sneering to yawning at the reports, as another AI agent reads them and verifies the fix. Just like we ignore compiler-generated assembly code and people on their cellphones.

vintagedave an hour ago

That was a tough read, but in all the verbiage I do not see it identifying the same code or area as the real genuine human on lore.kernel.

For those who can understand Claude better than me: did it anywhere actually identify the issue?

gpm 35 minutes ago

Assuming Andy (the mailing list post) is right, no it didn't.

I know this because he said so here in this comment section: https://news.ycombinator.com/item?id=49134550

That said the AI was sort of directionality right... it identified roughly the right code.

vaughands 2 hours ago

Thanks for the fun read.

Orphis 8 hours ago

I get why people don't bother replacing the default allocator from musl all the time (it's there, convenient). But in an application whose purpose is to be FAST, I find it weird they haven't bothered replacing it with another more performant one.

mallocng is bad at dealing with contention during multithreading. I've had applications that usually were I/O bound suddenly become "malloc" bound when building with musl in multithreaded scenarios (and only just 8 threads). Switching to mimalloc improved performance by 20x, very close to what glibc offers by default, and just a bit under a glibc + mimalloc configuration.

I get that there's a real issue there and it's interesting (to some) to address it, but it should have never surfaced this way in the first place.

masklinn 8 hours ago

> I get why people don't bother replacing the default allocator from musl all the time (it's there, convenient).

ripgrep actually sets jemalloc as global allocator when built for 64b musl: https://github.com/BurntSushi/ripgrep/blob/435f59fc4b43af3ab...

Orphis 8 hours ago

That's good! But I wonder why it wasn't then enabled for that configuration or why the override wasn't "global" enough and the default allocator was still partly used.

tialaramex 7 hours ago

zeuxcg 7 hours ago

If you look at the sigsegv stack, the allocation comes from opendir which is in musl libc as well. The allocation override mechanism used in Rust doesn’t replace the allocator process-wide; it merely replaces the allocator Rust code talks to.

It does seem like ripgrep should probably avoid using opendir from libc if it allocates using an allocator with a global lock though.

vlovich123 8 hours ago

It’s a kernel bug. While I agree libc allocators suck for no good reason, it seems like this work of equally likely hit other application code including mimalloc and glibc.

Orphis 8 hours ago

Sure, but the problem isn't that the bug exists, is that it has surfaced in a performance application using musl and exerting code paths in a slow allocator.

mallocng should not be used at all.

inigyou 5 hours ago

tripflag 7 hours ago

karel-3d 4 hours ago

It's maybe a kernel bug.

mort96 7 hours ago

The way most programs achieve being fast is by re-using allocations. You don't need a fast allocator if you don't allocate. Nothing of what ripgrep does inherently requires frequent allocations.

entrope 5 hours ago

The ISO C definition of opendir() requires an allocation in practice because it returns a DIR* and it's bad practice for the library to arbitrarily limit how many of those an application has at a time.

Maybe a C library could preallocate several DIRs and only use the heap when those are exhausted, but this ripgrep use case (lots of threads running in parallel on a large tree) would still be likely to trigger that.

mort96 3 hours ago

lrvick 2 hours ago

We use musl+mimalloc by default for our entire production operating systems: https://stagex.tools

searealist 4 hours ago

There is a real tradeoff:

- The musl allocator is only slow with multi-threading.

- Almost all other allocators have trouble reclaiming memory when using multi-threading. This often results in multiples more RSS than single threaded or musl's allocator.

Agree with you on mimalloc. It can even be configured to be aggressive in memory reclaim at the cost of performance.

dosman33 6 hours ago

Anyone running ripgrep on a an HPC cluster against a large cluster filesystem needs to stop and redesign their workflow. This generates high amounts of small I/O which is the Achilles heel of any large cluster filesystem. You are exporting your workload onto the metadata mechanisms of the filesystem rather keeping it within the higher bandwidth capable memory subsystem on your cluster. It doesn't take but a couple users running these types of jobs simultaneously to bring a high-bandwidth filesystem to its knees. Just stop it already.

btown 6 hours ago

Similarly, I've often wondered if this is the root cause of why GitHub has become fragile of late. All of a sudden you have operations on billions of tiny files that are amplified by AI usage. And object graphs are so fragmented by nature that it's hard to e.g. prefetch a page into memory and ensure that common git operations mainly touch that page's set of objects.

And if https://isolveproblems.substack.com/p/how-microsoft-vaporize... is to be believed even in the slightest, any unoptimized paths in Azure's filesystem abstractions could cause usage spikes to have massive splash radius.

gsu2 5 hours ago

> All of a sudden you have operations on billions of tiny files that are amplified by AI usage. And object graphs are so fragmented by nature that it's hard to e.g. prefetch a page into memory and ensure that common git operations mainly touch that page's set of objects.

In theory, that's what git's packfiles are for: loose objects get packed together into a file, and if the file is well-arranged then objects that tend to get read together will also end up getting paged-in together (see also: https://www.kernel.org/pub/software/scm/git/docs/technical/p...).

GitHub in particular has spent a lot of time and effort on packing things well (e.g. https://github.blog/open-source/git/scaling-monorepo-mainten...), so I doubt that the "billions of tiny files" problem is at the root of their recent instability.

hyperpape 10 hours ago

The analysis of the kernel bug may be a better thing to link to: https://github.com/dfoxfranke/ripgrep-3494-analysis.

smukherjee19 8 hours ago

I tried reading and gave up at the "Headline"...

Quoting from the bug analysis:

>Headline. The crash is real and reproducible. With musl instrumentation we pin the in-process mechanism precisely: a thread's own store to a freshly- faulted anonymous page becomes invisible to that same thread's reload ~10 instructions later, because the page's backing is replaced mid-function. A pagemap read at the instant of the fault shows the backing is the kernel's zero page. A captured core dump confirms the crash site with matched virtual addresses. The mechanism localizes to the interaction between the per-VMA-lock anonymous-fault fast path and a concurrent munmap's TLB shootdown. A source-level review of Linux 7.0.12 identifies a specific race in that interaction, and a git comparison across v6.19/v7.0/v7.1/mainline identifies the v7.0-introduced change on the munmap-teardown side that widens it.

"a thread's own store to a freshly- faulted anonymous page becomes invisible to that same thread's reload ~10 instructions later, because the page's backing is replaced mid-function." -> ??? What's a "backing" of a page? Freshly-faulted? Fresh fries? "~10 instructions later"?

"A pagemap read at the instant of the fault shows the backing is the kernel's zero page." -> Backing?

"The mechanism localizes to the interaction between the per-VMA-lock anonymous-fault fast path and a concurrent munmap's TLB shootdown." -> How can a mechanism "localize"?

And more.

This is... words strung together. Nothing more. I wonder how people read and make sense of this.

In case people have forgotten what real technical writing looks like, here's a sample (I am not the author): https://yifan.lu/2019/01/11/the-first-f00d-exploit/

cerved 8 hours ago

I'm becoming increasingly allergic to the performative writing of LLMs.

The worst part is how compulsive LLMs are at writing like this. Like the system prompt instructs it to "reason" and like a college Sophomore it pontificates and quotes Nietzsche to fein intelligence and orginal thought.

fasterik 8 hours ago

csense 6 hours ago

This is technical writing for kernel developers who already know how memory management works. If you want to understand it better, you should study up on the x86 MMU and the kernel memory management subsystem. I'm not a kernel expert but I found it pretty understandable, so I'll try to explain it.

MMU: The memory management unit, part of the CPU designed to allow an OS kernel to flexibly control how memory addresses used by a program map to physical RAM.

Page: A 4k region of memory addresses.

Anonymous: The page is just regular old memory used by a single process for general purposes, it's not part of e.g. a memory-mapped file.

Backing: The memory corresponding to a page. The kernel can direct the MMU to map any page to any part of RAM, or delete the mapping entirely.

Zero page: The kernel keeps a single 4 KB page filled with zeros (the "zero page").

Page fault: An error that occurs when you try to write a page that's not present, or write to a read-only page. Some page faults are normal and expected by the kernel, they support features like swapping, memory-mapped files, etc. Such "expected" page faults are invisible to your program. (On the other hand, an unexpected page fault occurs when your program just straight-up accesses an address it's not supposed to, which will end your program with a SIGSEGV signal -- an all-too-familiar experience for a C programmer.)

When you allocate say 4 MB of memory, the kernel simply sets it to 1k read-only copies of the 4 KB zero page. Then when your program tries to write to a page, it faults. The kernel handles this expected fault by assigning that page an individualized memory region. If a program allocates a lot of memory, it's only assigned memory for what it actually uses, when it first uses it. This is an optimization: Every program that allocates more memory than it needs is wasteful, but the kernel can recover that waste by not backing the allocation with memory -- the program has to prove each page is needed by writing to it.

Store to a freshly-faulted anonymous page: The program stored data to its own memory, which caused an expected page fault.

> becomes invisible to that same thread's reload ~10 instructions later

The thread fairly quickly tried to read the data from the same place in memory it had just written to. It doesn't get the same data back. This is a major problem that causes the program to malfunction and crash.

> A pagemap read at the instant of the fault shows the backing is the kernel's zero page

The expected fault occurred because the program tried to write to an address that was mapped to the zero page, probably because it's the first write to freshly allocated memory. (This rules out other reasons it might be faulted, e.g. the page had been swapped to disk.)

> concurrent munmap's TLB shootdown

The page table is big and slow so the CPU caches parts of it in an area called the TLB (translation lookaside buffer). munmap is a kernel function that removes the backing for an address region. Since munmap changes the mapping, it has to inform the MMU the relevant part of the TLB is no longer valid.

> per-VMA-lock anonymous-fault fast path

This is what the kernel does when the program first writes to a zero page. Saying it's the "fast path" implies there's some (slow) special case handling in the kernel code, but the bug doesn't trigger any of the special cases, we're in the most common case, which the kernel code tries to handle as quickly as possible because it has measurable impact on performance. I would guess per-VMA-lock has to do with how the kernel synchronizes this code (a "lock" is a basic synchronization primitive, you should be familiar with it if you do any kind of multithreaded programming).

> The mechanism localizes to the interaction between the per-VMA-lock anonymous-fault fast path and a concurrent munmap's TLB shootdown. A source-level review of Linux 7.0.12 identifies a specific race in that interaction

The mechanism: What's actually happening to cause the program not to be able to load data it just stored in memory.

Localizes: The cause of a problem like this is a needle in a haystack -- it could be caused by the program, the kernel or the hardware. The analysis has narrowed down the haystack to a specific part of the kernel code.

Race in that interaction: Two parts of the kernel code, (1) The kernel code for munmap and (2) the kernel code to handle a program's first write to the read-only zero page for a fresh allocation. These two parts work fine individually but the problem occurs when they both try to change the page tables / TLB at exactly the same time. This is quite a small, specific haystack compared to "somewhere in the program, kernel or hardware" we started with.

kelnos 4 hours ago

MBCook 5 hours ago

freedomben 4 hours ago

newsoftheday 8 hours ago

Agreed, the article you linked is a great example of clear, understandable, detailed technical writing.

Retr0id 8 hours ago

fwiw "backing" is standard jargon in this context, and "freshly faulted" is phrasing you'll see in Linux source comments. The writeup as a whole is indeed slop, though.

cyberax 5 hours ago

This is a pretty clear analysis if you are a kernel developer.

> a thread's own store to a freshly- faulted anonymous page becomes invisible to that same thread's reload ~10 instructions later, because the page's backing is replaced mid-function

A result of a machine instruction to store something in RAM got erased, when the same thread attempted to load it ~10 machine instructions later. The reason is that the physical RAM page backing that particular virutal page got replaced while the thread was running.

The fault happens in the fast path for anonymous (i.e. not backed by files on disk) pages, in the granular per-VMA (virtual memory area) locks.

> "A pagemap read at the instant of the fault shows the backing is the kernel's zero page." -> Backing?

Yes, it's typical kernel terminology. "A backing page" or a "backing file".

> This is... words strung together. Nothing more. I wonder how people read and make sense of this.

There's nothing wrong with this description. It's just written for kernel developers. It needs to be expanded and explained for people who are not.

dark-star 8 hours ago

"backing" is a Virtual-Memory related term. With virtual memory, you can have memory areas (called "pages") that are not really there but only present in the metadata (the "book-keeping", so to say). The first time, someone actually tries to access this page, that access is interrupted ("faulted") and the kernel gets a say in what should be done to that piece of memory (i.e. load it from disk somewhere, reserve actual physical memory and fill it with zeroes, or crash the process).

The "backing" refers to the physical memory that might or might not be present for every "virtual" piece of memory that your program has allocated.

"Freshly faulted" means that a page of (virtual) memory has just received a "backing" by the process above and is, thus, very fresh in physical memory (even though the virtual memory might have been allocated much earlier)

"~10 instructions later" refers to the assembly- (machine-) code, which, contrary to a high-level language like C, usually has long(ish) sequences of rather simple "instructions". 10 instructions is a rather short interval in assembly code.

As for the "localize", the term used is actually "localizes to" which I read as "turns out to be located in" (probably just a bad English translation by the original author)

While this whole summary reads a bit weird, I don't think it is necessarily the result of an LLM, it's probably just that someone who is rather inexperienced at writing up technical summaries did it.

yuliyp 7 hours ago

entrope 4 hours ago

smukherjee19 7 hours ago

samatman 7 hours ago

I don't care if the cat is black or white, so long as it catches mice.

Translation: if this nails-on-chalkboard LLM spew identifies a kernel bug, which is then patched, its aesthetic qualities do not interest me in the slightest.

smukherjee19 6 hours ago

fwlr 9 hours ago

    The overflow would still overflow; the use-after-free would still use after free; a musl mask race would still race. 
Hilarious. Apart from the computer poetry, the conclusion seems to be “it’s something in Linux 7.0 + musl 1.2.5”, although the only reproduction is still on the same physical Threadripper CPU and only sometimes when heavily exercised, so it hasn’t really ruled out a hardware issue.

internetter 8 hours ago

The computer seems to have identified source code to blame though? Unless of course it hallucinated which there’s always a non zero chance of

MBCook 5 hours ago

fwlr 6 hours ago

p0nce 9 hours ago

Reading an AI-generated bug report is awful.

oxidant 8 hours ago

Stopped after

> Headline. The crash is real and reproducible.

BetterThanSober 8 hours ago

dguest 7 hours ago

Maybe it's not going to get any better. LLMs were trained to do two things:

- Mimic human language (and logic, since that's part of what we express with language). This includes code written by humans.

- Write code to solve problems. The figure of merit here is solving the problem, not reproducing anything human.

I'm not an expert on LLMs, but it's not obvious that the human reasoning they are fitting in the first case is going to be anything like the problem solving required in the second case. It was trained to get itself out of a problem, not to do it in a way that a human would relate to.

And we're already running out of human data to train on, while synthetic data has no limit. Bug reports in the future might amount to "fix this because then I'll get a cookie".

PunchyHamster 5 hours ago

amluto 9 hours ago

Nice sleuthing, nonsense explanation. An extra TLB flush is never an error. (The CPU is free to flush whenever it feels like doing so.) The error seems to be that somehow a zero-page PTE was present when it shouldn’t have been.

This sounds to me like either (a) a complex race involving a CPU migration at an awkward time or (b) a bug in the zap path transiently exposing wrong PTEs.

Also, I don’t think the zero page has pfn zero.

If I had to throw a dart, I would guess that direct page table zapping is allowing a CPU to read through a higher-level-paging-structure cached entry to a table that has been freed and reused. Yuck. I’ve debugged one of these before.

Retr0id 9 hours ago

The part that doesn't make sense to me is that trying to read through a zeroed PTE should be an immediate segfault, not something that reads back zeroes (regardless of whether the zero page is pfn 0 (it isn't)).

I think the broad strokes are that, due to bad locking in the kernel, mmap() returns a VA that a concurrent munmap() is still in the middle of unmapping. The specifics beyond that seem murky/speculative/inconsistent.

Alternatively, it could be a hardware bug, since afaict it's only been repro'd on one hardware config. (I know there are a bunch of ARM cores with erratas around TLB invalidation)

amluto 9 hours ago

gpm 8 hours ago

inigyou 9 hours ago

The explanation is obviously written by an AI agent.

kelnos 4 hours ago

Not really. It's typical rambling LLM slop-analysis. It may be a correct analysis (I couldn't stomach reading it in detail), but it's a pain in the ass to read. A human doing the same analysis would have written something 1/5th the length.

yawndex 10 hours ago

soulless unreadable AI slop

hoppp 10 hours ago

I can read it but not sure if worth spending energy on it.

It doesn't make sense for the reader to spend more energy than the writer spent on creating it.

agumonkey 10 hours ago

serf 9 hours ago

wild_pointer 10 hours ago

rfgplk 10 hours ago

Why is it unreadable? I actually find LLM bug reports/breakdowns to be far more detailed and concise that classical human written ones. If you read the linked repo it clearly goes it depth where the bug was found, how to reproduce it (and in depth). Most disclosures that are human written don't do this at all, they barely even tell you _how_ to reproduce the bug. Just look at the "3.3 The self-store tear", the LLM clearly describes exactly what went wrong, so you can verify it by hand.

jerf 8 hours ago

skydhash 9 hours ago

anorwell 10 hours ago

I had the opposite reaction. Clear, detailed, well-organized. Pretty close to the ideal writeup.

Retr0id 9 hours ago

munch117 8 hours ago

orphea 8 hours ago

IshKebab 9 hours ago

porridgeraisin 9 hours ago

sligor 10 hours ago

So, why the bug triggers only with muslc and not other libc ?

inigyou 4 hours ago

Coincidence. It also only triggers on one machine.

cyberax 4 hours ago

Probably because musl's allocator exposes single newly faulted pages directly to the app. Other allocators tend to pre-allocate multiple pages at a time, so that the race window is narrower.

walk12111 10 hours ago

I would normally suspect musl's thread stack size. Is the kernel bug confirmed?

wild_pointer 10 hours ago

wow, everything is broken lol

villgax 9 hours ago

No wonder search in codex is so a$$

Ar-Curunir 3 hours ago

What does one have to do with the other?

ramon156 8 hours ago

Codex uses musl?

derdi 7 hours ago

Codex bundles a ripgrep binary that is linked against musl. This is noted in the bug report: "I originally encountered this bug in the rg bundled with OpenAI Codex. That binary is byte-for-byte identical with the one in https://github.com/BurntSushi/ripgrep/releases/download/15.2... [...]"