[HN Gopher] Exploiting null-dereferences in the Linux kernel
___________________________________________________________________
Exploiting null-dereferences in the Linux kernel
Author : kuter
Score : 80 points
Date : 2023-01-19 18:37 UTC (4 hours ago)
(HTM) web link (googleprojectzero.blogspot.com)
(TXT) w3m dump (googleprojectzero.blogspot.com)
| high_byte wrote:
| 8 days to exploit :) pretty neat.
|
| and 2 years on servers? still worth a shot. I bet it can be much
| faster in certain scenarios.
| azakai wrote:
| IIUC steps 5-7 in the exploit cause around 2^32 oopses. I don't
| know much about the Linux kernel - could it perhaps have a limit
| on the number of oopses before it halts the entire system?
|
| The article explains why it is important to not do that in
| general, as an oops allows debugging and recovery etc. But 2^32
| of them seems suspicious.
| roguebantha wrote:
| Yes, there is now an oops limit, specifically because of this
| technique - see the conclusion paragraph.
| azakai wrote:
| Ah, thanks! I should finish reading the entire article before
| commenting, sorry...
| jeffbee wrote:
| The article is about the exploitability of the flaw but really
| the flaw should not exist. Printing /proc/$pid/smaps is not on
| any conceivable performance-critical hot path. It can stand to
| have bounds checks and safety. The call to print out smaps should
| be well-encapsulated in some non-C language.
| [deleted]
| nix0n wrote:
| > Printing /proc/$pid/smaps is not on any conceivable
| performance-critical hot path.
|
| I disagree, for profiling memory usage it's useful to get
| memory map data multiple times per second.
| jeffbee wrote:
| If you think smaps is performance-critical that raises of the
| question of its ridiculous textual format. Clearly, it would
| be vastly more efficient to pass the information as a
| protobuf or whatever. Believe me, as the person who had to
| refactor the smaps-reading library on cost/efficiency grounds
| at Google, this issue it nearer and dearer to me than to
| probably anyone else.
| chc4 wrote:
| Yes, in an ideal world your kernel shouldn't have any bugs. We
| don't live in an ideal world.
|
| Security engineering is the field of _practical_ mitigations -
| given that there are, in fact, null pointer dereferences in the
| kernel, mmap_min_addr and adding count limit to kernel oops
| provides defense in depth to help prevent them from being
| exploitable.
| roguebantha wrote:
| Thankfully this isolated flaw was quite easy to fix. And _yes_
| this code isn 't likely to be on any hot paths, and code can
| always stand to have bounds/sanity checks (and it always
| should). But unfortunately encapsulating all non-hot-paths in
| Linux kernel that might have these sorts of bugs in a memory-
| safe language is at best a very long term goal and at worst a
| pipe-dream. The real goal of the blog post was not to push for
| any sort of rewrite, but rather to note how even the simplest
| and most innocuous of bugs can lead to security-relevant
| primitives. And also to make sure kernel developers and bug
| fixers have strategies like this in mind when they evaluate
| other bugs in the future.
|
| TLDR: However honorable the end-goal is, this blog post is not
| the ammo you need to push for a big rewrite of various
| kernel<->userland interfaces into memory safe languages.
| tedunangst wrote:
| What does your safe language do when it accesses a null object?
| Does it oops?
| deathanatos wrote:
| My safe language doesn't have "null", more or less.
|
| What it has is Option<T>, and I cannot turn that into a T
| without handling the failure case: there is literally no way
| to construct the code otherwise1. One _must_ handle the
| failure path. (That might be way of explicit panic
| /abort/oops, but it's then right there in the code: that
| branch _will_ panic ... and safely.)
|
| 1(this example is using safe Rust. There's unsafe Rust too
| and there I can chase the null pointer all I want with that,
| but the parent's point is that we should be sticking to safe
| interfaces for stuff like this. And I'm using Rust as an
| example, but Option is hardly unique to Rust, heck, Rust
| stole the idea from its predecessors.)
| tedunangst wrote:
| What happens when I call unwrap?
| SpaghettiCthulu wrote:
| You shouldn't be allowed to in the kernel
| monocasa wrote:
| A panic there absolutely could bail out to the read call
| at the root of this kernel stack with an EIO or some
| such.
| deathanatos wrote:
| That's the explicit handling of the None case I mentioned
| in the comment: it causes an explicit, and safe, abort.
| By "explicit", I mean the .unwrap() call will be right
| there, in the method that needs to turn an Option<T> into
| a T, and visible to a code reviewer. In the larger
| context here of kernel code, it should raise the eyebrow
| on the reviewer: "wait, this function _shouldn 't_ abort,
| it needs to handle the edge cases!".
|
| (But for some userland app, aborting might be acceptable.
| The kernel is in a bit of a bind, since an abort -- a
| kernel panic -- means the user loses computer until they
| reboot, and the work along with it.)
|
| Vs. a C pointer ... all uses are more or less equally
| suspect; any given use, you hope the code has done it's
| homework for ensuring they're not NULL, and if they are,
| the consequence is UB. (And in Rust, and in the languages
| Rust steals the idea of Option from, you're only
| using/passing Options where "None"/null/nil is a
| possibility. If it's not, or you've verified or handled
| that at some outer stack frame, then you just pass a
| reference to a T, which is statically guaranteed to point
| to a valid object1.)
|
| 1again, barring buggy code using unsafe Rust, in the
| example of Rust, or calling into C code that fails to
| maintain its invariants, etc.
|
| Take the example in the article, where the code does,
| priv->mm->mmap->vm_start
|
| while trying to generate the output for smaps_rollup.
| That's compilable, but buggy, C, because mmap can be
| null, but we failed to check for it.
|
| Vs., if mmap were an Option<T>, where T is whatever type
| that pointer points to. Let's say our coder attempts to
| write, priv->mm->mmap->vm_start
|
| (In some imaginary language, because C doesn't have
| Option, AFAIK.) The compiler would say, no, you can't
| "->vm_start", because "mmap" _could_ be None (whatever
| you call the "nothing here" value/variant; I'm going to
| call it None, to distinguish it from the null pointer).
|
| In the case of unwrap, the coder could do something like
| (this is psuedo-code)
| (priv->mm->mmap).unwrap().vm_start
|
| It would then be obvious there is an abort there. Their
| reviewer would not be pleased with that, I suspect: we
| don't want kernel panics or oops or aborts while
| generating a file in /proc. And likely our imaginary
| coder would know this too, and when the compiler errored
| the first time, saying, "hey, mmap is an Option", they'd
| raise an eyebrow, say something like, "wait, it is? When
| would mmap be None?" and then proceed to properly handle
| that case. (E.g., by treating it as if it where the empty
| list.)
| tedunangst wrote:
| Ah, I see now, abort is better than oops. Why hasn't
| anyone told the Linux developers they should use abort
| instead of oops?
| loeg wrote:
| These juvenile and facile retorts do not elevate the
| discourse, nor are they a good look. As a long-time
| OpenBSD developer you have a lot of smart things to say
| about technical subjects, including kernels specifically,
| and I wish you would leave the dumb snarky comments
| unwritten.
| Dylan16807 wrote:
| If an abort function still triggers cleanup, then yes it
| is better. C doesn't have such a thing, so your sarcasm
| about 'telling them' is unwarranted.
|
| If an abort function doesn't trigger cleanup, then you
| can block it at compile time to prevent this kind of bug.
| But before you can even think about doing that, you need
| to split pointers into nullable and non-nullable. And the
| kernel devs already know about that idea, and how hard it
| is to implement in C.
|
| Nobody is naively suggesting "hey kernel devs do this
| thing!" as if there isn't decades of momentum behind the
| current codebase. It's just a look at how C is bad at
| this particular kind of bug.
| chc4 wrote:
| The root cause of the bug isn't the kernel dereferencing
| a NULL and causing UB. It's the kernel _doing error
| handling_ and attempting to kill the oopsing task and
| continue. If the semantics of Rust panics also did a
| kernel oops, unwrap() would trigger the exact same bug
| described in the blog post with regards to reference
| count rollover (if Rust in the kernel doesn 't do stack
| unwinding)
| jeffbee wrote:
| Yes, but fundamentally the kernel's inability to handle
| exceptional cases is all in deference to performance.
| Non-performance-critical sections, which in a fair
| analysis would be 99.9% of the kernel at least, should be
| written in a language and style that provides structured
| unwinding, not just jumping to the error case and whoops
| I accidentally jumped beyond all the unlocks and
| reference count decrements and deallocations. That's the
| issue.
| Dylan16807 wrote:
| You wouldn't allow code that aborts without cleanup in
| these areas of the kernel, or in the kernel at all.
|
| You can't make a similar rule against null dereferences,
| because those happen by accident. (Unless you wrap every
| single pointer dereference, which is not happening.)
|
| If you don't allow aborting, then the compiler makes you
| write an error-handling path that returns, and the
| cleanup code will not be skipped.
| deathanatos wrote:
| There are two bugs discussed in the article. One is the
| one this thread started with, which _was_ the kernel
| deref 'ing a NULL.
|
| You're right that this is separate from the handling of
| the oops, which is the main exploitability that TFA is
| getting at, and certainly fixing one deref leading to an
| oops (the proc file chasing NULL) doesn't fix the other
| bug of "any oops can be further exploited".
|
| But the context of this subthread is the implication that
| you must have some null, and some thing must happen when
| it is chased. That assumption is wrong, that's what the
| core of the comment I'm making is getting at: you can't
| follow a null if you don't have the possibility of them
| in the first place. (Or where you must have an Option<T>,
| you can build safe interfaces for handling that fact.)
|
| > _unwrap() would trigger the exact same bug described in
| the blog post with regards to reference count rollover_
|
| If we consider this instead as "an unwrap occurring
| during the oops handling", _maybe_ , but it's not
| guaranteed that that is the case. Other aspects of Rust
| could similarly prevent that bug. I haven't fully grokked
| the latter half of the article, but I didn't think it
| would be necessary for the comment, as, a. the chain was
| about "Printing /proc/$pid/smaps is not on any
| conceivable performance-critical hot path." and b.
| followed by the question about null.
|
| Ref-counting in Rust is often dealt with via RAII, and is
| safe through that, both in that RAII means the refcount
| is managed correctly and without input from the coder,
| but also Rc (and I presume Arc) will abort on overflow. I
| don't _know_ if that would fully translate to kernel
| code, given that we might be taking refs due to the
| actions of userland, and that might be happening near the
| userland /kernel boundary and be reasonably subject to
| unsafe code that could very well fall prey to the same
| problems.
| aseipp wrote:
| The thread panics because the value is None, and it stops
| execution before the attacker can take control of EIP.
|
| Was that supposed to be a hard question?
| avgcorrection wrote:
| It shouldn't be a question that tedunangst should even
| have to ask, given that he has had a chip on his shoulder
| about Rust and its safety guarantees for years and thus
| should have learned _that_ much about it by now.
| btown wrote:
| One of the missing pieces IMO is that your language needs
| the right kind of shorthand to make it easy to say "I'm
| calling things that return Options or Results, I myself
| return an Option or Result, any time I unwrap let's
| pipeline any null values or failures into returning a
| failure immediately."
|
| In Rust this is the ? operator: https://doc.rust-
| lang.org/reference/expressions/operator-exp...
|
| The whole idea is that there should never be a way to
| call unwrap() if you as the caller cannot handle it
| gracefully. And if you do this at every step up until the
| UI layer, which can handle any failure as an error to be
| displayed in the UI, then the job is complete!
| Y_Y wrote:
| The Maybe Monad
| wyldfire wrote:
| In order to have the safe language I believe you would need
| to decompose the code shown here in show_smaps_rollup(). If
| the null deref occurred in the unsafe portion it would likely
| still do an oops. If the null deref occurred in the safe
| portion it would likely exit safely and cause the syscall to
| return some errno that describes a kernel fault.
| monocasa wrote:
| Ideally it has a iterator construct built in so it views an
| empty linked list chain truly as an empty list without
| derefencing the first (null) item preemptively.
| david2ndaccount wrote:
| C could support the concept of nullable vs non-null pointers.
| Clang even already has this as an extension:
| https://clang.llvm.org/docs/AttributeReference.html#nullabil...
|
| There is also an associated nullability sanitizer.
|
| I use this in my own C code all the time and null pointer
| errors vanish if you faithfully annotate every pointer. There's
| also a pragma to make non null pointers the default in a file.
|
| GCC devs would have to be convinced to add this to GCC and then
| nullability annotations would need to be added to the kernel.
| You can then do static analysis/compile error if you do an
| unguarded check of a nullable pointer.
___________________________________________________________________
(page generated 2023-01-19 23:00 UTC)