[HN Gopher] Simple Linux kernel memory corruption bug can lead t...
___________________________________________________________________
Simple Linux kernel memory corruption bug can lead to complete
system compromise
Author : pabs3
Score : 214 points
Date : 2021-10-20 03:29 UTC (19 hours ago)
(HTM) web link (googleprojectzero.blogspot.com)
(TXT) w3m dump (googleprojectzero.blogspot.com)
| nottorp wrote:
| The title sounds like it's the end of the world. Reading, I see
| it's another local exploit. And long fixed to boot.
|
| Can we stop having tabloid titles for technical matters?
| bruo wrote:
| This text is not a news report, it's a technical one about this
| specific bug. It shows how the attack develops and suggest
| mitigations at the kernel development level.
|
| The bug itself is small and it lead to a whole system
| compromise, and the title is very good to guide us to the point
| they are trying to make... memory corruption is a problem and
| that needs to be addressed at early stages even, even if the
| overhead seems not worth it.
| rndgermandude wrote:
| I agree. The title is quite factual.
|
| It would be nice if the article stated what's affected more
| clearly, and importantly, that patches were rolled out long
| ago for most distros.
| r1ch wrote:
| I think the title is fine, it's showing how even the most
| simple memory safety bugs can be exploited to lead to system
| compromise. Not every submission has to be about something
| happening right now.
| tssva wrote:
| The actual title of the article is "How a simple Linux kernel
| memory corruption bug can lead to complete system compromise".
| Which is a much less tabloid title than the changed title here.
| It also more properly reflects the purpose of the article which
| isn't discussing the specific bug but how such bugs can be
| exploited and more importantly how to prevent such bugs from
| being exploited.
| rndgermandude wrote:
| Nothing is ever really just a local exploit. It's always also
| one half of a remote exploit chain...
| alexfromapex wrote:
| Preventing bugs like this is where Rust would shine in the kernel
| encryptluks2 wrote:
| Seriously, this comment and similar comments is why I'm pretty
| much convinced that Rust will be booted from the Linux kernel
| altogether.
| ylyn wrote:
| If enough people want it, it will happen.
| chc4 wrote:
| Rust would help with bugs like the initial memory unsafety.
| Half the blog post is about resilience even in the face of
| memory unsafety though, especially since the entire point is
| that there only has to be _one_ bug, in any legacy subsystem,
| to exploit the entire kernel. Using Rust doesn 't magically add
| any of those defense-in-depth mitigations and pessimistic
| sanity checks.
| atoav wrote:
| But it might remove another hole in the swiss cheese security
| model, which can be worth it.
| skavi wrote:
| Do we know what the performance impact of adding all these
| checks is?
| habibur wrote:
| Huge. Only checking array bounds on every access degrades
| performance considerably.
|
| But I am waiting for the Rust-OS to complete -- if one is
| under construction now. We can check how that stands when
| released.
| pjmlp wrote:
| I have used C++ since 1993 with bounds checking enabled,
| it hardly has ever mattered.
|
| The very few cases where it actually mattered for project
| delivery acceptance, were proven with a profiler, and
| fixed on those single cases only.
|
| Most of the time it is just cargo cult taken from C into
| other languages.
|
| Multics had a better DoD security profile assessment than
| UNIX, thanks to PL/I being bounds checked by default.
|
| Mac OS used Object Pascal until they decided to switch to
| C++ around 1992, it also hardly impacted their sales,
| using a language with bounds checking.
| fabianhjr wrote:
| AFAIK RedoxOS has been bootable for a while now. (Though,
| a very simple new OS)
|
| https://www.redox-os.org/
| [deleted]
| volta83 wrote:
| > Huge. Only checking array bounds on every access
| degrades performance considerably.
|
| Citation needed, since all evidence points to the
| contrary.
|
| Could you please point us to a Rust application (there
| are hundreds of thousands at this point) that gets
| noticeably faster when disabling bound checks?
|
| In servo, a whole web browser written in Rust, the cost
| of doing this was negligible, to the point that it was
| barely measurable (1-3%, noise levels for such a big
| app).
|
| Same for Firefox which has a substantial amount of Rust.
|
| Go ahead and give Fuchsia a try. You can enable bound
| checks for a substantial part of Android's user space and
| not really notice it.
|
| Same for Redox, or any operating system kernel written in
| Rust.
|
| You have many large applications to choose from, so
| please, just point us to 1 for which this is the case.
|
| ---
|
| Compared with other mitigations already in the kernel,
| that can cost you up to 50% perf, and that people seem to
| be ok with, bound checking all the array accesses seems
| like a no brainer, given that ~70% of CVE are caused by
| memory issues.
|
| When most people think about bound checking all array
| accesses, they think, for some "i can only think inside
| the box" reason, that this happens on hardware, for every
| memory access.
|
| But that is not how Rust works. Rust adds bound checks
| "in Rust", and the Rust compiler and LLVM are really good
| at removing duplicates, hoisting many bound checks out of
| a loop into a single bound check at the beginning of the
| loop, etc.
|
| People also think that this is an all or nothing
| approach, but Rust allows you to manually access without
| bound checks and do the hoisting manually. So if you find
| a function in which this makes a big difference, you can
| just fix the performance issue there manually.
| Measter wrote:
| I did once have a bizarre situation where _removing_ a
| bounds check that always succeeded degraded performance
| by over 30%.
|
| The bounds check wasn't being elided either. I checked
| and it was there in the assembly, so I figured that the
| function is so hot that an unchecked access might help
| things. Apparently not. The only thing I can think of is
| that the reduction in code-size for that function had an
| unintended effect elsewhere, either for the optimizer or
| that it resulted in a hot bit of code crossing a cache
| line?
| rini17 wrote:
| Or the always succeeding check helped CPU's branch
| predictor substantially.
| tialaramex wrote:
| For the optimisation, the compiler will even reason that
| e.g. iterating over the vector necessarily involves
| knowing the size of the vector and stopping before the
| end, so it doesn't need to add the bounds check _at all_
| because that 's redundant. This is easier in Rust because
| the compiler knows nobody else can be mutating this
| vector while you're iterating over it - that's forbidden
| in the language.
|
| So, in general, the idiomatic Rust "twiddle all these
| doodads" compiles to the same machine code as the
| idiomatic C++ for that problem, even though Rust bounds
| checked it and C++ didn't care. Lots of Rust checks are
| like this, they compile away to nothing, so long as what
| you did is necessarily correct. The Option<NonZeroU64>
| stuff a few days ago is another example. Same machine
| code as a C++ long integer using zero as a signal value,
| but with type safety.
| pjmlp wrote:
| Same applies when using C++ with bounds checking enabled,
| but the FUD regarding bounds checking is deep.
| tialaramex wrote:
| What happens about mutation in the C++ case though? Maybe
| I should just try it in Godbolt instead of bothering
| people on HN.
| pjmlp wrote:
| What mutation?
|
| Naturally this only kind of works when everyone on the
| team goes safety first.
|
| Doing some C style coding will just bork it, similarly to
| any unsafe block or FFI call in other better suited
| languages.
|
| But in the subject of making juice with lemons, is way
| better than plain C.
| tialaramex wrote:
| Right, so long as there isn't mutation, we're golden,
| which is why the machine code is the same.
|
| This is, after all, why Godbolt was first invented as I
| suspect you know (Matt Godbolt wondered if C++ iterators
| really _do_ produce the same machine code as a hand-
| rolled C-style for loop, and rather than just trust an
| expert he built the earliest Compiler Explorer to show
| that yes, with real C++ code you get the same machine
| code, any time spent hand-rolling such loops is time
| wasted)
| pjmlp wrote:
| Yeah, but since there are domains where C++ is
| unavoidable, this is the best we can do.
|
| By the way, this should be a nice update about the state
| of affairs on Android (I am yet to watch it).
|
| "Improving Memory Safety in Android 12 Using MTE"
|
| https://devsummit.arm.com/en/sessions/57
| throwaway81523 wrote:
| > Huge. Only checking array bounds on every access
| degrades performance considerably.
|
| I have not found this, at least in application code.
| There is usually at most a few percent different between
| v[i] and v.at(i) (the latter checks bounds) with C++
| std::vector, for example. So I almost always use .at()
| these days, and it does catch bugs.
| gpderetta wrote:
| wouldn't compiling with _GLIBCXX_ASSERTIONS or the
| corresponding for your compiler of choice be a better
| solution? It will also catch quite a few more issues
| (dereferencing null smart pointers, empty optionals,
| etc.), while being still relatively lightweight.
| pjmlp wrote:
| Not the OP, that is my approach, keep on the bounds
| checking settings from VC++ also in release builds.
|
| And before STL was a thing, all the custom types I had
| were bounds checked by default.
| throwaway81523 wrote:
| Thanks, I didn't know (or didn't remember) about this,
| but it's not clear from the docs that it bounds checks
| []. I don't find it difficult to use .at(). I'll try the
| debug mode too, but when I bother writing something in
| C++ it's usually because I actually care about its speed,
| so I don't want the overhead to be too bad.
|
| https://gcc.gnu.org/onlinedocs/libstdc++/manual/using_mac
| ros...
| scns wrote:
| Well, there is the rub. The safe thing to do is more
| verbose, the unsafe way is in muscle memory. Rust did it
| the other way around deliberately. But that is easy if
| you do a new design and don't need to retrofit a safe
| solution. No snark intended.
| pjmlp wrote:
| That is why one should always enable the compiler
| switches that turn operator[]() into at(), and then only
| disable them on case by case scenario, if proven
| worthwhile to do so.
| bbarnett wrote:
| But it is rust, and it fixes every conceivable issue, merely
| by using it, and absolutely must be mentioned in any post
| about anything.
|
| It's almost at the point, where if I see rust on a resume,
| I'll figure the dev has a mental disorder and skip them.
| Cryptonic wrote:
| Well, denialists often think that the normal guys have
| mental disorders. The Rust kernel will come one day. Not
| doing it in the face of todays massive cyber security
| threads is madness. Even Windows and MacOS will be
| rewritten in Rust Ahahahahahahahahhahddahhadad _runs in
| circles_
| kwertyoowiyop wrote:
| After we have rewritten everything in Rust, we will also
| have to delete all code, stored anywhere, that was
| written in C/C++. Storing such code will be considered a
| Thought Crime and punished by exile to the Radioactive
| Wastelands.
| vadfa wrote:
| >It's almost at the point, where if I see rust on a resume,
| I'll figure the dev has a mental disorder and skip them.
|
| I figured that one out when I tried to join the rust
| discussion server on discord and saw the server photo.
| kwertyoowiyop wrote:
| Certain people didn't appreciate your sense of humor! :-)
| andy_threos_io wrote:
| +1000
| scns wrote:
| I have a problem with the classification of traits as
| mental disorders, which may be a huge boon in the right
| situation. A friend of mine could be classified as
| compulsive/obsessive. He works as an airplane mechanic. I
| would rather ride a plane that he worked on, than his
| "normal" colleague, who talked to someone else while
| refilling a tank with pure oxygen, walking away and having
| his arm catch fire from a static discharge. No snark
| intended.
| okamiueru wrote:
| I'm sure if that was the management attitude that would
| have welcomed them, it is a win-win situation.
| bbarnett wrote:
| Employers don't need zealots or evangelists, they need
| realists, and people ready to work.
|
| Not prattle on endlessly about the latest fad.
|
| Rust has some pluses. Yet the evangelism, with the "it
| fixes everything" lunacy circulating, taints rust, makes
| legitimate and sane advocates look like nutjobs.
|
| I don't have the time to sort the wheat from the chaff,
| so when I see a big deal made about rust on a resume,
| odds are it's a zealot, and into the bin with 'em.
| okamiueru wrote:
| You are expressing widely different things though. You
| said you would consider out right rejecting someone and
| question their mental health for having rust on their
| resume.
|
| Having a tool on a resume, or listing experience with
| tools, should certainly not be interpreted as someone
| being a rust zealot, evangelist, "pratting on endlessly",
| and all the rest of the examples you gave. So, maybe you
| initially wrote something different to what you actually
| meant.
| bbarnett wrote:
| The point is, as I said, sorting. I don't have time to
| filter the all the crazies from the sane rusters.
| okamiueru wrote:
| I'm not sure to what extend you are being facetious, so
| I'll take it at face value. Rust doesn't particularly
| attract "crazies", and nor are software developers mostly
| crazies. So, I believe your criteria is exceptionally bad
| at filtering those kinds of developers out. Hiring good
| people is challenging enough as it is without doing the
| job poorly. My advice, unwarranted as it is, is to revise
| your preconceptions.
| Azsy wrote:
| Obviously the top comment is dumb, but please keep on
| doing this.
|
| Nobody is going to be happy if you hire someone who knows
| rust.
|
| > "As a shortcut for filtering out new people and ideas,
| i will filter them on the most annoying person that has
| tried them".
|
| At least your partially honest with how many divergent
| ideas someone working with you is allowed to have.
| throwaway81523 wrote:
| I think that says more about you than it does about the
| rusters.
| lmm wrote:
| Resilience is impossible in C-family languages, given
| undefined behaviour. Any defence-in-depth checks you add can
| only be triggered once you're already in an undefined state,
| so the compiler will helpfully strip them out (this has
| already happened in Linux and is the reason they build with
| -fno-delete-null-pointer-checks, but C compilers have very
| little appetite for broadening that kind of thing).
| IshKebab wrote:
| That seems like a weird way to do it. Wouldn't it be better
| to have compile errors if the compiler figures out that it
| can remove a null check?
| flohofwoe wrote:
| The "inconvenient truth" with Undefined Behaviour is that
| it provides essential wiggle room for the compiler to
| implement some optimizations that wouldn't be possible
| with a stricter specification. Of course the downside is
| that sometimes the compiler goes too far and removes
| critical code (which AFAIK is still a hot discussion
| topic how much compilers should be allowed to exploit UB
| for optimizations).
| pjmlp wrote:
| Because that was the only way for C to catch up with
| stuff that was being done in languages like PL.8.
|
| So compiler optimizers get to take advantage of UB for
| that last mile optimization, which in a language like C
| always expect the developer to be a ISO C expert, and
| when they aren't or are too tired trying to meet project
| deadlines, surprises happen.
|
| "Oh, it was quite a while ago. I kind of stopped when C
| came out. That was a big blow. We were making so much
| good progress on optimizations and transformations. We
| were getting rid of just one nice problem after another.
| When C came out, at one of the SIGPLAN compiler
| conferences, there was a debate between Steve Johnson
| from Bell Labs, who was supporting C, and one of our
| people, Bill Harrison, who was working on a project that
| I had at that time supporting automatic
| optimization...The nubbin of the debate was Steve's
| defense of not having to build optimizers anymore because
| the programmer would take care of it. That it was really
| a programmer's issue.... Seibel: Do you think C is a
| reasonable language if they had restricted its use to
| operating-system kernels? Allen: Oh, yeah. That would
| have been fine. And, in fact, you need to have something
| like that, something where experts can really fine-tune
| without big bottlenecks because those are key problems to
| solve. By 1960, we had a long list of amazing languages:
| Lisp, APL, Fortran, COBOL, Algol 60. These are higher-
| level than C. We have seriously regressed, since C
| developed. C has destroyed our ability to advance the
| state of the art in automatic optimization, automatic
| parallelization, automatic mapping of a high-level
| language to the machine. This is one of the reasons
| compilers are ... basically not taught much anymore in
| the colleges and universities."
|
| -- Fran Allen interview, Excerpted from: Peter Seibel.
| Coders at Work: Reflections on the Craft of Programming
| rcxdude wrote:
| You would have a huge number of false positives, because
| if you're doing a bunch of null checking defensively
| you're going to be doing it redundantly a lot.
| IshKebab wrote:
| Right, but if the compiler has _proved_ that you don 't
| need those null checks then you can simply remove them.
| If you think you do need them then it's a sign you've
| screwed up somewhere and should fix it!
| DSMan195276 wrote:
| The problem is that it's not that simple. A NULL check
| might only become redundant after various rounds of
| inlining and optimizing, and the same NULL check could be
| redundant in one usage and necessary in the other. It's
| also very likely that changes to other parts of the code
| will make a previously 'redundant' NULL check now
| necessary, and obviously you're unlikely to get a warning
| in that direction.
| _flux wrote:
| > Right, but if the compiler has proved that you don't
| need those null checks then you can simply remove them.
|
| This can be the case now, but then later someone adds new
| code that you did need that null check.
|
| > If you think you do need them then it's a sign you've
| screwed up somewhere and should fix it!
|
| Hm, so instead of
|
| > Wouldn't it be better to have compile errors if the
| compiler figures out that it can remove a null check?
|
| you wanted the opposite: warn when a null check is there
| and was actually required :).
|
| Not sure if this is a working solution either. Maybe if
| it was behind a macro, JUST_CHECKING_IF_NULL(x)..
| IshKebab wrote:
| Yeah sounds like the best solution is explicit checks
| that the compiler understands -
| REDUNDANT_NULL_CHECK(...), REQUIRED_NULL_CHECK(...).
|
| I mean obviously the solution is to use a sane language
| but you know...
| vlovich123 wrote:
| Did you read the last part of the article? It explicitly says
| that Rust (or some other kind of languages guarantees) would
| absolutely remove the need for more complex runtime measures.
| Additionally, such checks stop the exploit chain early before
| it's able to pick up steam.
| ncmncm wrote:
| It speculates that Rust would help. That is very far from
| demonstrating that Rust would actually help.
|
| Rust would certainly _not_ help in this instance, because
| nobody is writing pty handling code in Rust. I.e., using
| Rust in place B does not help with bugs in place A. Any
| expectation that Linux would get more secure if some new
| code were Rust is optimistic to the point of fantasy.
|
| The best possible outcome of allowing Rust in kernel code
| is that the kernel would not become _even more_ insecure as
| a result of the added Rust code. That would be good, by
| itself, even if not what we really want. But whether even
| that would be achieved in practice is still to be
| demonstrated.
| smoldesu wrote:
| The best possible outcome of allowing Rust in kernel code
| is being able to guarantee type safety, memory safety and
| perfect concurrency where it's applied. Those 3 pain
| points are where C fails right now, and it's part of the
| reason why scheduling on Linux feels 'worse' than Windows
| or even MacOS at times.
| ncmncm wrote:
| I.e., exactly what I just said, but longer.
| alexgartrell wrote:
| I shared this perspective, but luckily my job is awesome and
| (in a routine 1:1!) Paul told me why it's less straightforward
| than I thought: https://paulmck.livejournal.com/62436.html
|
| my takeaway was essentially that you get sweet perf wins from
| semantics that are hard to replicate with a type system that's
| also making really strong guarantees without making the code
| SUPER gross.
| Koffiepoeder wrote:
| To be honest, the article makes much more nuanced suggestions
| to avoid these kind of bugs. I am not sure Rust would even have
| helped here, since the cause seemed to be a race condition
| because of an invalid lock being used. It might have been
| possible to avoid in Rust with an RwLock, but in this case that
| was also the fix for the original bug (using the correct lock).
| I have only looked into this bug report semi-thoroughly
| however, so I might be mistaken.
| athrowaway3z wrote:
| I was typing a comment about how Rust wouldn't have made a
| difference for the race condition, but after reading through
| it again to be sure i'm now on the side that Rust would have
| errored on the original bug.
| spin_lock_irq(&tty->ctrl_lock);
| put_pid(real_tty->pgrp); real_tty->pgrp =
| get_pid(pgrp); spin_unlock_irq(&tty->ctrl_lock);
|
| rustifying this would be let mut tty_lock =
| tty.ctrl_lock(); put_pid(real_tty); real_tty.pgrp
| = get_pid(pgrp); std::mem::drop(tty_lock);
|
| Which would give an error that you are not allowed to mutate
| real_tty.pgrp.
| nyanpasu64 wrote:
| The problem was that one of two threads locked the wrong lock
| before accessing a shared resource (when two threads read or
| write shared memory, both sides must acquire mutexes or it's
| useless), resulting in a data race.
|
| Rust could prevent this issue by requiring that all non-
| exclusive accesses to the shared data acquire the mutex (and
| if you use Mutex<T> which wraps the data, you'll always
| acquire the right mutex). The & vs. &mut system can model
| exclusive access ("initialization/destruction functions that
| have exclusive access to the entire object and can access
| members without locking"). It doesn't help with RCU vs.
| refcounted references, or "non-RCU members are exclusively
| owned by thread performing teardown" or "RCU callback
| pending, non-RCU members are uninitialized" or "exclusive
| access to RCU-protected members granted to thread performing
| teardown, other members are uninitialized". And Rust is
| _worse_ than C at working with uninitialized memory (Rust not
| using the same type for initialized vs. uninitialized memory
| /references is a feature, but uninitialized memory references
| are too awkward IMO).
| fsflover wrote:
| If you care about such attack vectors, consider security through
| isolation, which can be provided by Qubes OS: https://qubes-
| os.org.
| nyc_pizzadev wrote:
| A handful of C projects I have seen use magic numbers in
| allocated structs to prevent use-after-free and other memory
| bugs[0]. Basically, in this case, when the ref count hits zero
| and the struct is freed, the magic is zeroed and any further
| access will be stopped. The author makes no reference of this, so
| I guess this isn't a widespread safety pattern?
|
| [0] https://github.com/varnishcache/varnish-
| cache/blob/4ae73a5b1...
| geofft wrote:
| This only protects you against _unintentional_ use-after-free.
| If a use-after-free of struct ws is a thing you 're worried
| about an attacker intentionally causing, in order for this to
| be useful, the attacker needs to control one of those four char
| * pointers and point them somewhere useful. Typically they'd do
| that by inducing the program to re-allocating the freed memory
| with a buffer under their control (like input from the network)
| and then filling it in with specific bytes.
|
| If they can do that, they can very easily fill in the magic
| numbers too. It's even easier than pointers because it doesn't
| require inferring information about the running program - the
| magic number is the same across all instances of Varnish and
| right there in the source.
|
| "Heap spray" attacks are a generalization of this where the
| attacker doesn't have precise enough control about what happens
| between the unwanted free and the reuse, but they can allocate
| a very large buffer (e.g., send a lot of data in from the
| network, or open a lot of connections) and put their data in
| that way. This approach would be basically perfect for
| defeating the "magic number" approach.
|
| (The blog post itself has a discussion of a number of more
| advanced variants on the "magic number" approach - see the
| mention of " _tagging pointers with a small number of bits that
| are checked against object metadata on access_ ".)
| nyc_pizzadev wrote:
| > they can very easily fill in the magic numbers too
|
| Right, recreating the magic does side step this defense.
|
| The context for software security these days is defense in
| depth and not something like "total defense" anymore. In this
| case, the use of magics is more of a dev testing mechanism
| than a runtime protect, although it does provide great
| runtime protection. What this means is if you use magics with
| proper testing and load testing, errors should surface before
| you release.
| pmarreck wrote:
| Solutions like this depend on the will, skill and ethics of the
| coder.
|
| Better IMHO to design a language in such a way that dangerous
| errors like this are completely impossible.
|
| (I mean... this is basically why I switched from Ruby to Elixir
| for web dev, eliminating an entire class of bugs... If the
| language itself doesn't provide an error-reduction feature,
| then you are reliant on other developers to "do the right
| thing" and lose any guarantees)
| vlovich123 wrote:
| It's possible some projects do this correctly but I suspect
| most have a false sense of security as the compiler will elide
| all stores that are happening in a struct about to be freed and
| there's no C/C++/LLVM language that's really immune from this
| [1].
|
| Usually a more thorough approach is to turn on malloc
| scribbling, ASAN or valgrind which is something Darwin's
| allocator can be told to do (it'll scribble separate
| uninitialized and freed patterns).
|
| I could see the appeal of there being a magic value though. I
| think that's what memset_s is for so hopefully your favorite
| project is doing that properly.
|
| [1] http://www.daemonology.net/blog/2014-09-04-how-to-zero-a-
| buf...
| nyc_pizzadev wrote:
| > I suspect most have a false sense of security as the
| compiler will elide all stores that are happening in a struct
| about to be freed
|
| The magic field is reset before returning the pointer to the
| allocator, so it's definitely a live write to a valid
| pointer.
| geofft wrote:
| It's a live write to a valid pointer that's never read
| from, so it can't have any influence on the program
| behavior if the rest of the program is valid C. Compilers
| are free to optimize that (and they basically _need_ to for
| performance reasons - unused writes are common in normal
| code).
| turminal wrote:
| It can be turned of though. Compiler optimizations
| performed around memory allocations are weird sometimes.
| geofft wrote:
| Yes, but this is by far the least weird type of
| optimization, and the one you'd least want to turn off.
| It's the sort taught in undergraduate compiler design
| classes.
|
| Here's a piece of code that would go slower if you turned
| it off: for (i = 0; i < 1000; i++) {
| struct foobar *ptr = malloc(sizeof(struct foobar));
| ptr->this = a[i]; ptr->that = b[i];
| if (ptr->this > 0) { do_stuff(ptr);
| } free(ptr); }
|
| You only need to write to ptr->that if the condition
| succeeds. (And frankly you only need to do the allocation
| _at all_ if a[i] > 0.)
|
| Compilers that don't do this get rejected for compilers
| that do.
| nyc_pizzadev wrote:
| The pointer is read by the allocator, which then performs
| its own set of writes and management. I think you guys
| might be confusing stack memory and dynamic memory here.
|
| Interestingly enough, if you were to incorrectly use
| stack memory in this scenario, the magic checks should
| trigger, pointing out improper memory usage.
| DSMan195276 wrote:
| Allocation and free functions from the standard library
| are 'special' though and do actually get their own logic
| in regards to whether a pointer is necessary or not. If
| you do some simple tests you can see that gcc will
| optimize away entire `malloc()` and `free()` calls if it
| can prove the resulting memory or assignments are
| unnecessary (Ex. Store a constant to it, and then read
| the constant). It's willing to do that because the
| behavior of those functions is defined by the standard,
| so it 'knows' that removing those calls has no affect on
| the behavior of the program.
|
| I'm _pretty_ sure you can get similar optimization
| behavior when you mark functions with special attributes,
| I 'm not 100% sure on that point though. So for the Linux
| Kernel I'm not sure that kind of optimization would ever
| be done since obviously it's not using the standard
| defined functions and the compiler might not be given
| enough information to know the functions have the same
| semantics as 'malloc()' and 'free()'.
|
| I personally was able to get the exact behavior described
| here by compiling the below code using `-O2`. The
| volatile write just ensures the first constant write and
| the malloc itself cannot be optimized out (and that does
| happen! Take out the volatile and there are no calls to
| malloc in the result), but gcc is still free to do
| whatever it wants with the other write and for me it's
| completely gone in the resulting program.
| int main() { int *p =
| malloc(sizeof(*p)); *(volatile int *)p = 20;
| printf("p=%d\n", *p); *p = 30; /* this is
| gone from the -O2 compiled code */ free(p);
| return 0; }
|
| The relevant part of the assembly looks like this, I
| don't think I'm missing anything in that the 30
| assignment is completely gone: push
| $0x4 call 460 <malloc@plt> movl
| $0x14,(%eax) # Assignment of 20 mov %eax,%esi
| pop %eax lea -0x1930(%ebx),%eax pop
| %edx pushl (%esi) # push the 20 push
| %eax call 440 <printf@plt> mov
| %esi,(%esp) # load address to pass to free(), no
| assignment between printf() and free() call 450
| <free@plt>
| nyc_pizzadev wrote:
| The point of the magic values are that these values are
| eventually checked. This should break out of these kinds
| of optimizations.
|
| Edit note: allocators are user space libraries (stdlib)
| and not part of the C spec. Use-after-free is extremely
| unsafe, however its completely valid C.
|
| 2nd note: running all programs below give me the expected
| result, writes to pointers are live, regardless if they
| are freed. So please provide more concrete steps to
| reproduce your results.
| DSMan195276 wrote:
| The issue is that value is only checked in the context of
| a use-after-free error though, which the compiler assumes
| never happens, so it doesn't actually matter to the
| optimizer. The code I wrote will optimize the same way
| even if I add a random pointer assignment or read
| elsewhere (any of which could technically trigger a use-
| after-free), because the compiler is allowed to assume
| that because I'm passing that pointer to `free()` I'm
| never going to use it again.
|
| To show this even more, if you add an extra printf to
| print the value of p after the `free()` (so add a literal
| use-after-free to check the 'magic' value) the 30
| assignment is still gone. It prints zero for me because
| free() clears that memory, but the assembly does not
| include the 30 assignment even though I'm clearly reading
| the value it assigned after the `free()` statement, which
| is exactly the behavior you're attempting to catch. If
| `free()` didn't touch the value I'd still see 20.
|
| With a little finessing I got my code to print 20 both
| times (the larger struct gets malloc() to leave the magic
| value alone) even with the 30 assignment still in the
| code: struct foo { char
| bar[35]; int p; }; int
| main() { struct foo *p =
| malloc(sizeof(*p)); *(volatile int
| *)(&p->p) = 20; printf("p=%d\n", p->p);
| p->p = 30; free(p);
| printf("p=%d\n", p->p); /* Prints 20 for me, the 30
| assignment is optimized out completely */
| return 0; }
| DSMan195276 wrote:
| > Edit note: allocators are user space libraries (stdlib)
| and not part of the C spec. Use-after-free is extremely
| unsafe, however its completely valid C.
|
| I'm not sure why you think this, but the C standard
| includes a whole section defining the behavior of the
| standard library functions, including malloc and free.
| And it includes this note as undefined behavior:
|
| > The value of a pointer that refers to space deallocated
| by a call to the free or realloc function is used
| (7.20.3)
|
| So it is undefined behavior to access memory passed to
| free, or IE use-after-free is undefined behavior if
| you're using the standard library.
|
| > 2nd note: running all programs below give me the
| expected result, writes to pointers are live, regardless
| if they are freed. So please provide more concrete steps
| to reproduce your results.
|
| I would check that you're compiling them with `-O2`, and
| also check the assembly output. However as someone
| linked, you can already see from the online compiler
| output that clearly gcc is capable of optimizing the
| assignment out. Here's my second program in the same
| compiler, notice how printf is called twice but the 30
| assignment (which is what is supposed to set your magic
| number) is still completely gone:
| https://godbolt.org/z/9reErcbGK
|
| Edit: Sorry, I previously included the wrong quote from
| the standard (it was about a double-free being UB,
| slightly different), I have the right one now. I got it
| from here if you want to look at it, it's a draft but
| practically the same as the actual one: http://www.open-
| std.org/jtc1/sc22/wg14/www/docs/n1256.pdf
| guerrilla wrote:
| Thanks for going through all this so meticulously. I
| actually didn't know this was possible (although I
| usually disable optimizations, so that might be why.)
| Just to be clear, using volatile on the magic number will
| ensure this is avoided?
| vlovich123 wrote:
| I recommend reading the article I linked. They thought
| they found a solution with volatile and it turns out
| didn't actually work. Malloc/free are 100% special and a
| sufficiently smart compiler can remove this stuff
| (sometimes automatically, sometimes if you turn on LTO).
|
| memset_s is the only function that would be defined to
| work precisely because this was needed for crypto so it
| is solvable but just take it on faith that if it's not
| explicitly called out as forcing the compiler to not do
| dead store elimination, it will happen.
| guerrilla wrote:
| Thanks for the recommendation, I had skipped it. It's
| worse than I thought. Wow, I need to reprogram my brain
| actually. Maybe it's time for me to actually read the
| standard and rethink these things.
| DSMan195276 wrote:
| > Just to be clear, using volatile on the magic number
| will ensure this is avoided?
|
| I think that's your best bet but volatile does bring its
| own set of problems with it (how it works differs from
| compiler to compiler). That said it should probably work
| fine, this is a pretty simple use of volatile. You might
| check the documentation for your particular compiler
| though if you have one in mind, it should tell you what
| it does and if it does anything undesirable (or if
| there's a better way to do this). Also the volatile cast
| like I did may be a better approach than actually making
| the member itself volatile.
|
| FWIW, the whole idea is that whether this happens or not
| has no impact on your program, so in theory you shouldn't
| ever notice this happens. Detecting use-after-free like
| this is not really standards compliant so that's a big
| reason why it's problematic to implement.
|
| Also, if you're not using the standard allcator then most
| of this logic doesn't apply because the compiler won't
| know your special allocator has the semantics of
| 'free()'. There are `malloc` attributes in gcc that might
| trigger similar optimization behavior, but you'd have to
| be using them, and even then I'm not really sure as I
| haven't looked into what all they do.
| vlovich123 wrote:
| Best bet is to use the APIs that standards/compilers
| guarantee things about. The compiler is totally free to
| optimize volatile variables under very specific
| situations. Volatile has a very specific definition, but
| it does not mean "compiler is not allowed to optimize
| this".
|
| Trying to structure code to trick the compiler is a bad
| idea. The compiler authors know the standard better than
| you and eventually the compiler will exploit your
| misunderstanding.
| DSMan195276 wrote:
| I agree, but FWIW I'm not claiming that volatile means
| "the compiler can't optimize this". This program would
| still function anyway even if the volatile is optimized
| out, so this is much more of a hint rather than a "this
| has to happen". A simple volatile store like this is also
| very unlikely to be optimized out, I don't know of any
| compiler that would attempt to do it and frankly it would
| break lots of stuff if it did (just because it can't be
| optimized out doesn't mean it can't cause other problems
| though). But when you get down to it trying to catch
| these use-after-free errors is never going to be
| guaranteed to work since as we've established use-after-
| free already breaks the standard itself. Still, using one
| of the various 'secure zero' APIs if you have one is
| definitely better, though the logic would need to be
| changed slightly.
| reza_n wrote:
| You can use `explicit_bzero()` to bypass DCE (dead code
| elimination). Otherwise, simply initializing your memory
| before using is enough to trigger magic failures when you
| use-after-free. C programs barely function if they do not
| initialize memory. Context, I work on Varnish which the
| OP referenced for this.
| geofft wrote:
| No, it should not, because it is impossible for the check
| to fail in valid C. A use-after-free is invalid C. Type
| confusion is invalid C.
|
| If you have the function void
| free_ws(struct ws *ptr) { ptr->magic =
| 0xdeadc0de; free(ptr); }
|
| there is no possible circumstance, in valid C, where
| ptr->magic could be read and have its value equal to
| 0xdeadc0de. That object is freed immediately after that
| write.
|
| If you do a read in some other function, say
| char read_from_ws(struct ws *ptr) { if
| (ptr->magic != WS_MAGIC) { exit(1);
| } return ptr->s[0]; }
|
| it is impossible for the pointer passed into read_from_ws
| to be a pointer that has passed through free_ws, i.e., it
| is impossible for ptr->magic to have been set to
| 0xdeadc0de by free_ws. Therefore, free_ws doesn't need to
| actually do the write.
|
| You're right that the _correct_ magic check is not dead,
| and cannot be eliminated by the same logic. But the
| effect there is that the magic check always succeeds!
| pjmlp wrote:
| Yes, it is gone. You can play with it on Compiler
| Explorer.
|
| https://godbolt.org/z/Ea3WPcad1
| cogman10 wrote:
| In C and C++, It's unlikely the compiler will optimize
| that because of pointer aliasing. It's basically
| impossible for the compiler to know if a given pointer
| will ever be re-referenced in the future due to pointer
| math or in an extreme case, explicit pointer addresses
| (happens semi frequently in the embedded world).
| Jweb_Guru wrote:
| You can put the code into godbolt right now and see that
| it's optimized out. The C and C++ standards say the
| compiler can make plenty of assumptions about what can
| and can't be aliased, and they generally won't hesitate
| to exploit this for optimization purposes. If they did
| not do this, C++ in particular would be pretty screwed as
| a high performance language (C might still fare okay in
| benchmarks, though not in real programs).
| DSMan195276 wrote:
| If you're passing the pointer to the standard library's
| `free()` then the compiler doesn't actually have to worry
| about pointer aliasing - it's undefined behavior to use a
| pointer after it has been passed to `free()`, so it can
| conclude it never happens in your program.
|
| FWIW most of what you're referring to (reaching a free'd
| pointer via pointer arithmetic, or going straight to its
| address) is actually undefined behavior according to the
| C standard, what you're allowed to do with pointers is
| more limited than that. There are ways of getting around
| some of that (obviously, since it's necessary in embedded
| and other low-level situations), but it requires
| knowledge about your specific compiler and/or potentially
| the use of extensions or similar things not part of the
| standard.
| oneepic wrote:
| In case anyone was confused like me, this appears to be
| describing a past bug from ~10 months ago, not an open one. (the
| blog post links to the bug also)
| https://bugs.chromium.org/p/project-zero/issues/detail?id=21...
| Datagenerator wrote:
| Thank you, security officer on duty goes back to sleep.
| perihelions wrote:
| Did Debian-stable not release the patch until February 2021, or
| did am I confused?
|
| https://lists.debian.org/debian-security-announce/2021/msg00...
|
| https://tracker.debian.org/news/1226431/accepted-linux-41917...
| geofft wrote:
| The commit messages are pretty unclear about whether there's
| any security impact:
|
| https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/lin.
| ..
|
| https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/lin.
| ..
|
| At least the second one says "... leading to use-after-free
| errors." But style in the Linux community is to not mention
| security impact and just to give a dense explanation of the
| bug itself. (Jann Horn, as a person who does care about
| security, tends to be better about this than most kernel
| developers; if the fix were from the average subsystem
| maintainer, I wouldn't expect to even see a mention of "use-
| after-free.")
|
| Also, if you look at the Project Zero bug log
| (https://bugs.chromium.org/p/project-
| zero/issues/detail?id=21...), it's clear that Horn wasn't
| totally sure whether/how this could be exploited, just that
| it seemed funny.
|
| (This should probably lead you to question whether "stable"
| kernels are a meaningful concept and whether the hypothesis
| that stable kernels are patched / otherwise do what they
| claim to do is even falsifiable.)
| tialaramex wrote:
| The kindest way to look at the Linux commit behaviour is
| that almost by definition its bugs are security bugs. The
| bug means the kernel doesn't behave as intended, and you
| are entitled from a security point of view to assume it
| _does_ behave as intended, so, hence bugs are security
| bugs. And thus argubaly having a "security bug" flag is
| redundant.
|
| For example suppose for some crazy reason, on Sundays all
| USB audio devices have stereo flipped by mistake. That's a
| bug. Is it a security bug? At first glance you may think
| "No". It's just a weird logic bug. But the user is entitled
| to reason that if they routed stereo left from their USB
| digital audio feed to the mono "Security Announcements" PA
| in the building, and their "security announcements" code is
| silent on stereo right, that's fine, that will still cause
| the announcements to come from their PA system as desired.
| It's astonishing that on Sunday this Linux bug silences the
| PA and they don't get PA announcements that there's a
| seismic alarm down in gold vaults D, E and F because
| somebody has drilled into them. It was a security bug after
| all.
| shawnz wrote:
| That seems like a stretch to me. If the hypothetical
| issue you are describing were caused by a bug in the
| system's media player software, would you say it's a
| security bug there too, just because it could be used to
| play security announcements? With this way of looking at
| things you are essentially saying that any bug in any
| software that could be called by other software is a
| security bug.
| tialaramex wrote:
| Yes, I would actually be comfortable with the latter
| claim in extremis.
|
| But here we're talking about the Linux kernel and so we
| needn't stretch that far. "The buck stops here" so to
| speak, if you can't trust the OS kernel to do what it
| promised, you're pretty much screwed.
|
| All you have left are broader physical or mathematical
| guarantees e.g. even a Linux kernel bug at Let's Encrypt
| can't leak your TLS server private keys because _they don
| 't have your private keys_ and so mathematically that
| can't happen - or even if there's a really drastic Linux
| kernel zero day this Android phone can't travel faster
| than the speed of light because physics doesn't rely on
| kernel code.
| diegocg wrote:
| It is also a local exploit, not a remote one
| amelius wrote:
| Can it be turned into a remote exploit through a web-browser?
| (Assuming the user knows what they are doing)
| geofft wrote:
| Not without a sandbox escape, at which point you often have
| more juicy targets without even getting to root (e.g., bank
| account credentials). The attack requires the ability to
| open a terminal device and do strange ioctls on it. Web
| browsers don't open terminal devices at all, so you are
| pretty unlikely to induce the browser to reuse parts of its
| code to do it; you'd need the ability to run arbitrary
| code.
| [deleted]
| Steltek wrote:
| At some point in my career, I picked up the notion that there
| an infinite number of local exploits laying around on your
| average Linux box. Any local user could find their way to
| root unless you took extra steps to lock things down. I'm not
| saying that there are still bash one-liners that give you a
| root prompt. Just that the "attack surface" of privileged
| binaries and kernel APIs is so enormous that there must be
| something to leverage. I don't mean to pick on anything
| unfairly but I figured a specially crafted filesystem or FUSE
| command would do the trick quite easily.
|
| Is that still the case or am I just old?
| [deleted]
| kuroguro wrote:
| Not sure about the particular setup you mentioned, but
| given that there's an LPE published every few months, I
| wouldn't be surprised.
|
| https://ssd-disclosure.com/ssd-advisory-overlayfs-pe/
|
| https://blog.qualys.com/vulnerabilities-threat-
| research/2021...
| ncmncm wrote:
| The distinction is moot. A remote exploit that gets you local
| execution combined with a local to root gets you remote to
| root, and game over.
| phendrenad2 wrote:
| Feels like a few years ago, posts about a local privilege
| escalation would be shouted down with "It doesn't matter,
| if someone has access to your machine, it's game over man".
| And remote code execution in a non-privileged context would
| be shouted down with "so what, it can't run as root". Glad
| to see people are finally connecting the dots.
___________________________________________________________________
(page generated 2021-10-20 23:02 UTC)