Live data from Hacker News

A bug that doesn’t exist on x86: Exploiting an ARM-only race condition

github.com

121–130 of 141 posts

Re: A bug that doesn’t exist on x86: Exploiting an ARM-only race condition

#121
post #107
post #40

I spent some time trying to figure out why the lock-free read/write implementation is correct under x86, assuming a multiprocessor environment. My read of the situation was that there's already potential for a double-read / double-write between when the spinlock returns and when the head/tail index is updated. Turns out that I was missing something: there's only one producer thread, and only one consumer thread. If t…

> IMO the use of `new` in modern C++ (as is the case in the writer queue) is often a code smell As a naive practitioner of modern C++, I'd love it if you could elaborate on this.

Whenever you use 'new', you have to decide what is going to 'own' the newly allocated thing. You'll also have to remember to call 'delete' somewhere.

Using unique_ptr/make_unique() or shared_ptr/make_shared() automates lifetime management (obviates the need for a manual 'delete') and makes the ownership policy explicit. They also have appropriately defined copying behavior. For example:

    struct Foo {
        // lots of stuff here ...
    };

    struct A {
        Foo* f = new Foo;
        ~A() { delete f; }
    };
    
    struct B {
        std::unique_ptr f = std::make_unique();
        // no need to define a dtor; the default dtor is fine
    };
For the destructor and the default constructor, compilers will generate basically identical code for both A and B above. If you try to copy a B, the compiler won't let you because unique_ptr isn't copyable. However it won't stop you from copying an A, even though as written (using the default copy ctor) that's almost certainly a mistake and will likely result in a double free in ~A().

Re: A bug that doesn’t exist on x86: Exploiting an ARM-only race condition

#122
post #6

Heh, 10 years ago I gave a presentation about how easy folks used to x86 can trip up when dealing with ARM's weaker memory model. My demonstration then was with a naive implementation of Peterson's algorithm.[1] I have a feeling that we will see a sharp rise of stories like this, now that ARM finds itself in more places which were previously mostly occupied by x86, and all the subtle race conditions that x86's memory…

But Peterson's algorithm requires explicit memory barriers even on x86, it doesn't seem the best example to show the difference.

Here are my slides from back then: https://reinference.net/mp-talk.pdf

You made me wonder, because I definitely remember using Peterson's Algorithm, so I went back to my slides and turns out: I first showed the problem with x86, then indeed added an MFENCE at the right place, and then showed how that was not enough for ARM. So the point back then was to show how weaker memory models can bite you with the example of x86, and then to show how it can still bite you on ARM with its even weaker model (ARMv7 at that time, and C11 atomics aren't mentioned yet either, but their old OS-specific support is).

Re: A bug that doesn’t exist on x86: Exploiting an ARM-only race condition

#123
post #122

Earlier quoted context omitted.

But Peterson's algorithm requires explicit memory barriers even on x86, it doesn't seem the best example to show the difference.

Here are my slides from back then: https://reinference.net/mp-talk.pdf You made me wonder, because I definitely remember using Peterson's Algorithm, so I went back to my slides and turns out: I first showed the problem with x86, then indeed added an MFENCE at the right place, and then showed how that was not enough for ARM. So the point back then was to show how weaker memory models can bite you with the example of x…

Oh, right, yes, ARM additionally needs a release barrier on the unlock path.

Re: A bug that doesn’t exist on x86: Exploiting an ARM-only race condition

#124

Earlier quoted context omitted.

>With "volatile variables" you can use compound assignment operators on the variable. What does that even mean? Nothing. It means exactly the same thing as on a normal variable, and it boggles the mind that people somehow not understand that. Given 'volatile int i', 'i++' means the exact same thing as 'i = i + 1'. Does that also not make any sense to you? If it does, can you explain why you believe they are different…

Yes, it will likely be reverted [1] for bitwise ops, while keeping += and ++ deprecated (to save face someone would say). [1] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p232...

It's true that the relevant committee is now likely to propose this be changed in C++ 23 (or before as an errata)

But the face saving is on the part of embedded C++ proponents who'd rather never fix the C++ language than risk that the mundane C code cheap vendors actually ship can't be counted as "C++ support" because it's no longer valid C++.

That's the core of the argument in P2327. Not that this isn't a bad idea (it's obviously a bad idea) or that we shouldn't do better (alternatives to C++ already do better) but that this is the status quo and C++ can't expect to improve upon that. A sad state of affairs.

P2327's dismissal of the problem caused by UART1−>UCSR0B |= (1<<UCSZ01 ); comes down to the usual Real Programmer stance, surely every single embedded programmer will arrange "by construction" that the problem can't happen. No actual examples were examined to see if embedded developers reliably do that, which seems unlikely - we're just invited to imagine that they're all inerrant.

Re: A bug that doesn’t exist on x86: Exploiting an ARM-only race condition

#125
post #112

Earlier quoted context omitted.

Sounds like a compiler bug to me. Can you file a bug to clang with a reduced standalone test (or I can do it for you if you share the standalone test).

Here is a complete simplified Kahan summation test and indeed it works with -O3 but fails with -Ofast. There must have been something else going on in my real program at -O3. However the original point that 'volatile' can be a workaround for some optimization problems is still valid (you may want the rest of your program to benefit from -Ofast without breaking certain parts). Changing the three kahan_* variables to v…

I hope this isn't the actual "real" code, because you've got undefined behavior before you even have to worry about the associativity optimizations. There's an uninitialized read of 'kahan_c' on the first loop iteration.

Re: A bug that doesn’t exist on x86: Exploiting an ARM-only race condition

#126

Earlier quoted context omitted.

Yes P1152 was taken for C++ 20. The purpose of abolishing volatile isn't so much to reinforce that it's not intended for this sort of threading nonsense (indeed on Windows the MSVC guarantees mean it almost is intended for this sort of nonsense) but to make it explicit that "volatile variables" were never really a thing anyway by abolishing the volatile qualifier on variables. The thing your hardware can actually do…

>With "volatile variables" you can use compound assignment operators on the variable. What does that even mean? Nothing. It means exactly the same thing as on a normal variable, and it boggles the mind that people somehow not understand that. Given 'volatile int i', 'i++' means the exact same thing as 'i = i + 1'. Does that also not make any sense to you? If it does, can you explain why you believe they are different…

> It means exactly the same thing as on a normal variable, and it boggles the mind that people somehow not understand that.

If you just want normal variables, write a normal variable, J F. Bastien's change doesn't have any consequences for your normal variables.

> Given 'volatile int i', 'i++' means the exact same thing as 'i = i + 1'

No. That's the post-increment operator so i++ has the same value as i did before the increment, whereas i = i + 1 has the value of i after the increment.

And you might say "Oh, but I never use those values". Good. J.F. Bastien's change forbids that for volatiles too, we're on the same page.

What you apparently want is to do two volatile operations, one read and one write, sandwiching an arbitrary integer operation. You can do that explicitly of course, Bastien's change doesn't alter that - but when you write it out, doubt appears on the horizon. If these are two operations, can't I race with somebody else such that they modify this thing before I write my modified version back and then I overwrite their changes?

And that doubt should be there. But what we know from asking people is that the compound operators falsely reassure them. That's why they were deprecated and why it's sad that there's enthusiasm to bring them back.

Re: A bug that doesn’t exist on x86: Exploiting an ARM-only race condition

#127

Earlier quoted context omitted.

There are almost certainly more multi-core ARM chips than x86 chips around, too, tho.

That's a good question. I guess we would need to compare all the arm multi-core in smartphones & some laptops, vs all the intel laptops/desktops/servers. Hmmm... tough one.

Virtually all modern (last five years, for the low end) smartphones have multicore chips. It's estimated that there are around _7 billion_ smartphones worldwide, with another 1.5bn sold every year. So I don't think it's a very tough one.

Re: A bug that doesn’t exist on x86: Exploiting an ARM-only race condition

#128
post #107
post #40

I spent some time trying to figure out why the lock-free read/write implementation is correct under x86, assuming a multiprocessor environment. My read of the situation was that there's already potential for a double-read / double-write between when the spinlock returns and when the head/tail index is updated. Turns out that I was missing something: there's only one producer thread, and only one consumer thread. If t…

> IMO the use of `new` in modern C++ (as is the case in the writer queue) is often a code smell As a naive practitioner of modern C++, I'd love it if you could elaborate on this.

Exactly as usefulcat points out: with modern C++, the bulk of object lifetimes should be handled with unique_ptr and/or directly on the stack, so your destructors are automatically called for you (reducing the risk of double frees or memory leaks).

unique_ptr forces you to think about your dependencies and when objects can / should be cleaned up.

Re: A bug that doesn’t exist on x86: Exploiting an ARM-only race condition

#129
post #21

Like quantum physics, memory ordering is deeply unintuitive (on platforms like ARM). Unlike quantum physics, which is an unfortunate immutable fact of the universe, we got ourselves into this mess and we have no one to blame but ourselves for it. I'm only somewhat joking. People need to understand these memory models if they intend on writing atomic operations in their software, even if they aren't currently targetin…

One piece of friction that hurts here is that the C++/Rust and ARM memory models aren't the same, and the consequences of this are unintuitive - compilers and CPUs can both screw with execution ordering. People who write in C++ should technically _only_ be concerned with the C++ memory model, but x86 has let them be very lax and undisciplined with std::memory_order_relaxed. ARM has some alluring constructs that don't…

ARMv8 basically exactly mirrors the C++ memory model without any explicit memory orderings (the default on atomics being sequentially consistent).

Re: A bug that doesn’t exist on x86: Exploiting an ARM-only race condition

#130
post #67
post #57

Earlier quoted context omitted.

Unless you're the maintainer of the parking_lot crate, you're not "inventing your own". And since parking_lot is AFAIK the second most popular implementation of mutexes and RW locks in Rust (the most popular one being obviously the one in the Rust standard library, which wraps the OS-provided lock implementations), you can assume it's well tested.

Everything about which people tell you to “not invent your own” must be invented by someone .

Don't invent anything unless your job is to invent it. Of course you might not know what your job is until you are in the middle of it.
Post reply on HN