Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
The Binder Linux driver is being rewritten in Rust (kernel.org)
191 points by qsantos on Nov 3, 2023 | hide | past | favorite | 101 comments


For anyone unfamiliar how rust helps, the idea is this: first, you can identify common usage patterns and build safe wrappers around raw system operations.

The wrappers have no additional cost when compiled: A nested structure of Wrapper<OtherWrapper<OverSomePointer>> has a size of the flattened contained data, all the type info will be stripped away.

This part is the hardest, but the reward is that you can then use these wrappers without thinking about lower-level details. Of course, it is possible to do that in other languages, but rust type system can encode more information in types. That means more checks at compile time and less cautionary instructions are needed in the code comments.

I always hear arguments that "Rust still does not prevent X!". That's not the point. Rust type system simply can prevent more if used as intended.


Indeed. What I hate about C is a function that returns an integer, and go figure out the docs about what the value of this integer means. In Rust, an enum with "RequestSuccessul" usually means just that: a successful request. That means I have much less uncertainty about things and spend less time going through docs.


C has enums, too, you know. What do Rust enums do that C enums don't?


It's a reasonable question. Aside from the nice data encapsulation of enums (which make them more like unions), the answer is basically "they're enforced".

Most C libraries and system calls return integers not enums. Often the meaning of those integers is hard coded in a header somewhere. There's no guarantee that the number returned will be one of the values in the header.

When a library does return enums, there's no guarantee the value returned is actually in the enum. It could be anything.

When you pattern match an enum in C, you have no checks to make sure you catch all the cases, or don't drop a break;.

All of the above means that in C, you're relying on the programmer to cross check they haven't made a mistake. Rust does it all for you - if you match against an enum type, you have to handle all the cases. You can't return anything other than the type you said you would. It basically does all the work that the programmer has to do in C. And most importantly, it does it every time you recompile. Even if the first person to call a function in C is meticulous, the members in the enum might change later.


You could have similar warning in C compilers: https://godbolt.org/z/aj6TWsxK3

I agree that Rust has more comprehensive guard railes, but except for pointer ownership and sharing where C is missing tools (although model checking tools can achieve this too), I think you could basically have everything else in C as well by building similar abstractions building on existing compiler warnings and maybe adding a couple of new ones.

So while I like some ideas of Rust, I think in the discussions proponents often compare it to a worst-case C without old programming style and no static analysis applied and comparing to an ideal Rust with no use of unsafe (and also this binder rewrite uses unsafe).


> What do Rust enums do that C enums don't?

In Rust, an enum can also carry data along with the tag, and the type of the data can be different for each value of the tag. A Rust enum is more like a "tagged union" (a struct containing both an enum as the tag and a union for the extra data) in C.


Rust's enums are "sum types" or "tagged unions".

Perhaps the most practical example of this is the Option type, where you can say an Optional value is either `Some(value)` or `Nothing`.

With this, you don't have do deal with null pointers; a `null` pointer would instead be `Nothing`, and a non-null value would be `Some(value)`, and the compiler can enforce you're sticking to this.


I wish "enums" would stay relatively simple numbers or something and "sum types" would remain full sum types, but, here we are. Now whenever I see someone asking for "enums" I have to ask them what they're actually saying, because the majority of the time they mean sum types, but only the majority. It is not even close to always. Sometimes people want more fully featured enumerations, too, e.g., better automatic stringification of a variety of types.


But C does not have algebraic enums. Try doing JSON in C and see how that goes. Every function now returns an int and a void*; go ahead and make sure they are always paired together.


exhaustiveness checking


> I always hear arguments that "Rust still does not prevent X!". That's not the point. Rust type system simply can prevent more if used as intended.

That's just the Nirvana fallacy. Just because it's not perfect shouldn't prevent you from improving stasus quo.

Absurd example: "Seatbelts don't prevent impalements or drowning in car, ergo they are useless.


the seatbelt example isn't a 100% fit as seatbelt-opposers argued that seatbelts actually increase the risk of drowning, so they're not a security improvement but a trade-off. of course, cases of drowning in an automobile where the passengers would have survived without a seatbelt are incredibly rare, while all other accidents where seatbelts help are comparably common.


I have doubts that would even be true. I'll gladly take a struggle to remove the seatbelt over receiving a head trauma immediately prior needing to escape the car and swim to safety.


First, the biggest problem for drowning in a car isn't seatbelts but windows. People will remember to unhook the seatbelts, and by that time it will be too late to open windows. In essence, you're arguing for the removal of windows, not seatbelts.

Second, the nirvana fallacy is comparing actual things to unrealistic, idealized alternatives. You're comparing actual (seat belts) to an idealized alterative (something that prevents all car crashes, no matter the cause).

Third, it's a less generalized example. Stuff seatbelts don't protect and might even lessen chances of survival - rockets, gunfire, rhinoceros, trains, meteors, nuclear blasts, black holes, multiversal collapse.


the argument that seatbelts cause drowning wasn't mine personally but one an original advocate against seatbelts brought. shortly after the guy died in a car accident. i don't even back out of the garage without my seatbelt on.

could have been this one: https://www.snopes.com/fact-check/seat-belt-advocate-killed/


The book unsafe at any speed, is a good background on the resistance regarding security improvements in the car industry.


Something can be more then useless yet still be less than worth it. Not saying rust isn't worth it in this case, just that the extremes are not the only two conditions.


Sure, a Nirvana fallacy depends on the alternative being some idealized, unreachable state. In the Rust case, the alterative is - write better code (or everyone will use static code checkers)!


Trivia: Fuchsia's starnix also has its own implementation of binder [1] and it's written is Rust too (as are most Fuchsia components).

[1]: https://cs.opensource.google/fuchsia/fuchsia/+/main:src/star...


For those, like me, who have never heard of what it is, Binder is an IPC mechanism for Android.


And before that Palm, and before that Be.

https://en.wikipedia.org/wiki/OpenBinder


Also, as mentioned in the patch's message, it's important to highlight that binder is a core component of Android. Its usage is so widespread that, to me, it kinda makes Android architecture very akin to a microkernel-based system.


And IPC stands for interprocess communication

> the mechanisms provided by an operating system for processes to manage shared data

https://en.wikipedia.org/wiki/Inter-process_communication


Here's some more background (from 2006 )

"OpenBinder is the core technology that ex-Be engineers started at Be, Inc. as the “next generation BeOS”, finished implementing at PalmSource "

https://www.osnews.com/story/13674/introduction-to-openbinde...


Really unexpected, that the first kernel feature written in Rust is such a huge driver, I would've expected something much smaller first. The reasoning for the change is well laid out imo. I'm looking forward to this, especially since it looks like performance will be almost on par with the C version which will give Rust a good outlook.


Is it really "huge"? Seems like a pretty well isolated subsystem with a fairly narrow feature set to me.

They say 6,000 lines of code. That's not very much.


It's surprising by importance but not by size, which is actually a sweet spot for evaluating a new systems language - a small dependency that everything uses.


It's just an Android thing which could arguably be entirely in userspace.


That's something that comes up more often, because many feel this shouldn't be in the kernel in the first place. The problem is that there hasn't been a proper userspace alternative with similar performance characteristics.

I think putting this stuff in the kernel is rather silly, but I'd rather have it in the kernel than bring back the rift between Android and Linux.


"similar performance characteristics"? I can exchange data at a quarter of the latency using normal shared memory.


I'm not sure how you'd port binder's security guarantees to simple shared memory. Binder is more than just IPC, it also provides an RPC mechanism as well as some isolation capabilities.


I'm sure it does more things that make it useful, and that it provides a practical programming model for certain platforms, but it's certainly not the lowest latency way to do inter-process communication.


Does the isolation between processes in Android require that it be in the kernel or can some other form of privilege bridge the separation?


Given the microkernel-like approach taken by Project Treble, at least some part of it needs to live on the kernel for fast message interchange.


The only thing that needs to live in the kernel is fast message interchange. And that could also be used as the base mechanism to move all(!) of the drivers out of the kernel.


Since Android 8 that the only in-kernel drivers are what the Android team considers legacy drivers, aka standard Linux kernel drivers pre-Project Treble.

Hence why modern Android drivers are called Binderized drivers.

https://source.android.com/docs/core/architecture/hal


I was just mulling this over yesterday with the story about USB tablet driver regression. Running drivers as user processes would essentially give the kernel a stable ABI. But then we'd see a proliferation of closed-source drivers - there wouldn't be motivation to upstream hardware support anymore. Is that a hidden goal behind this fast IPC thingy?


It is a very good candidate I think. Vendor specific, does not affect anything non-Android, Google is on it.


It isn't the first feature, but it may well end up being the first one that makes it into mainline.

https://lpc.events/event/16/contributions/1180/attachments/1...


Even though I have no clue what this binder thing is or does I quite enjoy reading well written docs like this.


I remember working with Binder in the context of Android/AIDL (IPC between apps). Wonder if Dianne Hackborn is still involved, she‘s notably absent in TFA. Her name was synonymous with Binder back then.


Nowadays it also does IPC between kernel and drivers, not only apps.


I am somewhat suprised binder is part of upstream; Are there any other projects aside of Android where binder is used?

Could I theoretically use it in my linux program just like any other IPC mechanism?


> Could I theoretically use it in my linux program just like any other IPC mechanism?

Yes, absolutely.

However, binder is implemented as a kernel module not enabled by default, so it depends on the build time configuration of your system's target kernel (eg., your distro's).

That given, it's important to note that the binder driver/module is just one piece of a greater framework and it's raw IPC features aren't as simple to use as SysV or POSIX's. For example, it requires a userspace process called context (or service) manager. Android has 3 different binder device instances and it builds a big framework on top of them wiring things like an interface definition language (AIDL), a set of libraries and SELinux permissions.


Does Rust prevent all mistakes with locking as the post seems to indicate? It prevents the most common issues of accessing variables without mutexes or something incorrectly by requiring them to be wrapped in RwLock or similar, but you can still get deadlocks can you not?


You can. There are definitely still footguns - some things are just not easy. But there are also fewer footguns, and the overall type system makes modelling problems in ways which are often simpler to reason about more achievable (trivial example - state machines are common, and easy to represent in relatively safe ways with exhaustively checked enums, matching, etc.)

Is rewriting something in Rust a guarantee of no bugs? Nope. But it does likely make it easier for the rewriters to reduce the number of bugs.


Also, since Rust provides and enforces the mechanisms to bypass huge swaths of bugs, like many locking errors, concurrency, exhaustive enum checking, etc., that frees up developers to spend their time debugging or reinforcing other areas where Rust can't make similar guarantees of correctness.

I recall reading somewhere that a lot of the ideas for Rust's safety system came from a Firefox analysis of bugs that had been reported and fixed, where something like 70% of bugs fell into a few broad categories (mostly memory safety, like use-after-free, buffer overruns, off-by-one, etc) which they could solve in a new language that could enforce correctness. The idea of being able to remove 70% of bugs, and to effectively guarantee that any bugs that do occur happen in those remaining 30% of areas, sounds like it could save a lot of developer time.


Rust does not protect against deadlocks, that is correct. It also doesn't fully prevent mistakes related to ref counting. But I don't get the impression that the post is claiming either of those things?


What sorts of errors with ref counting are possible?


One guess: memory leaks from circular references. It’s ref counting, and doesn’t do roots checking.


Yeah, you can definitely do that. Typically rust features are trying to prevent undefined behavior, specifically exploitable undefined behavior. In doing so there is low hanging fruit for increasing the likelihood correctness but I don't feel like "correctness at any cost" is its motto. That is more Ada territory. Deadlocks and memory leaks can sometimes be exploited for denial of service, but they are not undefined behavior.


Well, maybe that's open to the interpretation of "prevents mistakes" in the text I guess :)

I'd argue that "It prevents mistakes with ref counting, locking, bounds checking..." implies "all" mistakes, but hey, maybe not...


Rust-style locks definitely raise the bar, and I wish more languages adopted this - like this https://news.ycombinator.com/item?id=35464152 or https://github.com/dragazo/rustex


Yeah just the mutex style (the mutex being a container) even without borrow checker is already progress because it shows the dependency between the lock and the data in the code.

The main missing piece in rust locks is inter-lock dependencies when they are not nested.


folly::Synchronized provides a similar pattern in C++. I guess it's not widely used outside of Facebook, but it is commonly used inside Facebook FWIW.


>> Rust-style locks definitely raise the bar, and I wish more languages adopted this

This locking pattern is quite old and frequently available in safe languages.

Ada calls it "protected objects" and has had it since Ada 95:

https://learn.adacore.com/courses/intro-to-ada/chapters/task...

Java calls it "synchronized" and has had it since Java 5 or 6:

https://docs.oracle.com/javase/tutorial/essential/concurrenc...


If I'm not misunderstanding it, those always associate exactly one lock to a exactly one object, which is not what Rust locking is.

The Rust (also C++) mechanism is that when you lock a mutex you get a lock guard object and when the scope ends the guard is dropped and therefore the mutex is released; you don't need to put the code accessing the locked data inside a particular locking object.

For example, how would one implement https://doc.rust-lang.org/std/sync/struct.RwLock.html with that?


I realize that that is actually factually incorrect: you do associate the contents to the locks in Rust, e.g. RwLock<Contents>, though the structure is a bit different as an object doesn't inherently to have a lock.

However, you do get to choose what kind of locking scheme you use and, crucially, it is impossible to access said data without holding a lock. Indeed it is not possible to have concurrent access on data without having some locking implemented and used.


A difference being that you're safe even without a Mutex at zero cost.


Fairly ambiguous. “Prevents some mistakes” would be more clear, but I also don’t read “prevents” as “eliminates the possibility of”.


All mistakes is such an unachievable high bar, it’s almost a strawman argument. You can always imagine a programmer terrible enough that they will find every possible failure case.

However, Rust can prevent quite a lot of common mistakes. Getting rid of UAF, data races, and having deterministic destruction that unlocks locks is already a major quality improvement.

Rust can’t prevent deadlocks caused by wrong architecture, but of all concurrency issues deadlocks are the easiest to diagnose.


> You can always imagine a programmer terrible enough that they will find every possible failure case.

Uhh, no. That is an amazing programmer! Why? Because truly terrible programmers imagine a subset of every possible failure case and simply refuse to acknowledge other failure cases, especially the ones that are particularly common with particularly severe consequences.


Rust can prevent races. It cannot guarantee no Deadlocks by itself.

However, there are schedulers that do guarantee deadlock free operation written in Rust, see RTIC for example.


I didn’t know Binder is upstream. Is it really?


Yes, since quite some time.


Looking at the c code and comparing it with standard c++ (raii, smart pointers).. why not use c++? Sure rust has additional safety features that alone is worth it but they were mentioning ref counting,locking , bounds checking,error handling .. most of that is doable with pedestrian c++


First and foremost, algebraic data types, specifically, proper sum types, called "enums" in rust. Think safe C unions or sane C++ variant. Everything is built on them, they help to encode various states and ensure they aren't misused.

Second, moves by default. They make building wrappers that depend on creation and destruction of a value much easier. They can track various things: memory usage, threads, temporary pointers, or whatever else. Unlike unique_ptr, they are on stack and part of the type system.


Linus Torvalds famously does not like and will not allow the use of C++ in the Linux kernel.

Also, Rust explicitly puts safety first, and C++ cannot give the same guarantees for normal code. The uses of `unsafe` in Rust should be relatively uncommon and deserve extra scrutiny.


It’s actually sort of interesting why Linus has basically closed the door on C++ ever getting into the kernel, while Rust seems to have gotten the green light.

If you look at Linus’s initial reasoning [0] (which was from a long time ago, nearly 20 years now, way before C++11), it was because the abstractions it offered/encouraged could make it hard to reason about what’s really happening, which may be bad for kernel code, and that allowing C++ would open the floodgates for C++ programmers to contribute, and Linus famously hated C++ programmers.

Rust-in-the-kernel came up many years later, and IMO if Linus was really honest with himself he’d either (1) not allow rust for essentially the same reasons he disallowed C++, or (2) admit his views have evolved, and say that C++ should be allowed on the same tentative basis that Rust is.

I get that rust offers safety advantages that C++ doesn’t, but I don’t think that’s the complete picture… modern C++ offers so many of the same advantages (although obviously not all) that if we’re really being honest with ourselves, it would seem to be unfair that Rust is let in while C++ is not. Both of them are enormous steps up in safety over C.

[0] https://harmful.cat-v.org/software/c++/linus


> Both of them are enormous steps up in safety over C.

I would argue one (Rust) is, but not C++. Rust is more "batteries included" and doesn't require specific compiler flags or complicated tooling, it just works and gives you guarantees out of the box. C++ seems to allow escape hatches by default while Rust requires you to call that out via `unsafe`.


C++ makes things very hard to reason about still compared to Rust even. C++ programmers are bike shed machines in my experience, always chasing their own ideal of pedantic C++ which does not exist in practice.

How would C++ features like RTTI, exceptions, and the std template library be used? What about the insane ideas of classes and inheritance which induce mind numbing impossible to comprehend code?

Rust is actually pretty readable and reviewable. C++ tends to be mind numbingly hard to review.


> not allow rust for essentially the same reasons he disallowed C++,

Unless what he disliked was the inheritance/OOP stuff in C++, which isn't an issue in Rust.


Rust has its own flavour of OOP, only data inheritance isn't part of the picture.

Interfaces/traits, dynamic dispatch, static dispatch, encapsulation, polymorphism, traits inheritance, is all there.

Additionally macro system as powerful as Common Lisp, which allows to do stuff that would make Linus blowup on the spot.


> Rust has its own flavour of OOP, only data inheritance isn't part of the picture.

I think it's fair to say that Rust is a lot less 'OOP' than C++. There is no concept of 'protected', traits are separate from structs, there's no data inheritance, there's no 'isinstance', dynamic dispatch is explicitly behind 'dyn' keyword, etc.

> Additionally macro system as powerful as Common Lisp,

It's not like the Linux kernel doesn't make use of C's macros.


There is no "less" or "more" OOP as per CS definition.

Plain textual text replacement isn't the same as a proper macro system.


Abusing inheritance of virtual methods instead of interfaces (which makes classes closed to extension, and the possibility of overriding non-zero methods is nothing short of weird), and implicit "this" parameter (which makes stuff needlessly hard to read and refactor), are two of the biggest annoyances on my list -- because they are so basic.


Well, you can enjoy explicit this parameter in C++23, if that is your thing.

As for the complaints, they are kind of doable in Rust as well.

Use traits with function pointers, empty types, and some macros.


> C++ cannot give the same guarantees for normal code

Can you give me example of a normal C++ code where compiler and/or language will not guarantee safety? Let's put aside UBs, because they are everything but normal code.


Undefined behavior is absolutely easy to trigger in normal C++ code, and being able to prevent it is a massive safety benefit. Ignoring UB while talking about safety is like ignoring car crashes when judging whether seat belts give any benefit.

An easy way to trigger undefined behavior in C++ is by using a pointer after the thing it's pointing to has been freed. Doing this in a process that takes any user input can easily create a vulnerability that allows an attacker to inject code into the process. There are many ways to accidentally do this while thinking you're safe. A pointer to an item inside a data structure like a vector or hash table is dangerous if you use it after some items may have been inserted into the data structure, because inserts can cause the data structure to reallocate and expand its backing memory. This mistake can easily go unnoticed for a long time because only a small fraction of inserts will cause that. This mistake isn't possible to make in safe Rust.


Please define “normal”. Normal C++ for me runs the gamut of looking a lot like C to be a template metaprogramming monstrosity.

Almost all C++ code interacts with bare pointers at some point. And that’s just one of the places where you get into issues with safety.

You can also run into problems with iterators being invalidated: your iterator into a std::vector is no longer valid after a call to push_back().

Anything that stores a reference and can exist after the lifetime of the object, really. Lambdas are a problem here. But so are instances with reference members.

C++ is littered with these things.


> Almost all C++ code interacts with bare pointers at some point.

This is a device driver. If it has to talk to memory-mapped hardware, that's a raw pointer, inherently, inevitably.

Other things are language issues, and can be solved by changing languages, but raw pointers are inherent in the task.


> This is a device driver. If it has to talk to memory-mapped hardware

This is an IPC "device driver". It does not talk to any hardware, it's used to pass messages between processes.


> This is a device driver.

It's not, it's an IPC subsystem. It doesn't talk to any hardware.


I would avoid saying that it's "Rust" that "gives guarantees". It paints Rust as this magical thing that will solve anything. My preferred explanation is that Rust provides better tools to build wrappers that can't be misused. The idea is to solve hard problem once, and reap the benefits many times. But it all depends on wrapper author. In that regard, it is perfectly possible to write horrible Rust code.


> I would avoid saying that it's "Rust" that "gives guarantees".

Why would you avoid saying that? Rust does give guarantees: about memory safety and concurrency primarily, but also regarding the lack of undefined behavior.

> It paints Rust as this magical thing that will solve anything.

It does not, the above are not magical they are just challenging problems (although at some point they may have been deemed impossible problems and hence magical, I don't know)

> My preferred explanation is that Rust provides better tools to build wrappers that can't be misused.

"wrappers that can't be misused" sounds a LOT like it "gives guarantees".


The biggest wrapper that gives guarantees is the standard library, and usually, when people say that Rust does not do something, they have standard library in mind. For example, standard library made the choice to hide panics in out-of-memory situations. That does not mean you can't write your own version of relevant structures to gain different guarantees. I like to highlight the actual strengths of Rust (as a tool) instead of particular implementation details, especially when we are talking about situation (kernel) where Rust is used without its standard library.


Rust the language gives these guarantees without the use of the unsafe keyword:

* you will not be able to compile undefined behavior

* you will not be able to create a data race

* a shared reference is read-only

* you cannot write past the end of an array

There are others, but those are pretty big ones that both guarantees and part of the language itself, independent of stdlib.


Allows you to modify containers while iterating.


Defining "normal" code as "not having UB" is quite disingenuous though, isn't it? Iterating over a vector while adding elements for example looks normal, but isn't generally safe, unless you know to pre-allocate enough memory.


To flip the question on its head, why bother with C++ when you can use Rust?


30+ years of experience and time to ripen?


exactly the reasons to use rust, actually


> time to ripen

You mean time to rot?


C++ with raii was an option 25 years ago. Today rust is the hot new in thing, but we waited a long time. There are rough edges in C too.


C++ with RAII doesn't make the language safe, it makes it less unsafe. Instead, we have 25 years of incremental language additions without meaningfully deprecating things. The officially sanctioned way to write memory-safe code in C++ is a non-existent set of compiler warnings that error when you use 80% of the language (profiles). To quote Stroustrup and P2410 [0]:

    "Experience shows that this [memory safety] cannot be done without static analysis and run-time support. Furthermore, for fundamental reasons this cannot done even with such support if arbitrary legal language constructs are accepted while conventional good performance must be maintained."
[0] https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p24...


We are comparing to C here, which is clearly even more unsafe!


If it's just about number of years then Rust 1.0 was in May 2015, so more than 8 years. Does a "thing" need to be around for a decade+ to no longer be "hot" or "new"?


Google is making the smart bet that there will be juniors who will learn Rust and can contribute to Binder 10 years from now, while their experienced C++ devs will only shrink and need to be reserved for more critical projects.


> Additionally, we've been able to use the more expressive type system to encode the ownership semantics of the various structs and pointers, which takes the complexity of managing object lifetimes out of the hands of the programmer, reducing the risk of use-after-frees and similar problems.

I find statements like this humorous. Who is "the programmer" in the above sentence, is it the "it" in "it is raining"?

It doesn't take it out of the hands of the programmer, it separates/delegates it to the programmer creating the types vs the programmer creating the implementation. They might be the same programmer and that programmer might be very happy they could separately encode such checks, but it doesn't take it out of anyone's hands.


It takes the burden of remembering the interactions between lifetimes across the entire system out of the programmer's hands. Sure, they still have to write the code that denotes the lifetimes, and they have to make sure the code continues to compile. But they don't have to remember that an object passed into some function call might be free'd inside a conditional 3 function calls deep


I don't believe the source was talking about lifetimes but rather creating clever types regarding ownership. Some "programmer" must still construct said types.

I would agree that lifetimes are taken out of "the programmer's" hands in Rust for all "programmers" who are not working on the Rust lifetime compiler :D




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: