It's really cool that google is investing so heavily in rust. However, the blog post is more of a "starting point" than a "lessons learned" (which I hoped for).
I feel like it's a poor starting point, too. embedded-hal or embassy would be a much better starting point. Ultimately if you know enough to know what a memory mapped register is, you can _probably_ become dangerous with Rust really quickly. The alternative is someone using Arduino libraries, who would greatly benefit from a HAL of some kind.
What I found is that the Rust HALs are incredibly thin and understandable, I was digging into MCU datasheets within a month of first using embassy (with no prior embedded experience).
It's not a great title for the post, because it's ambiguous and could go either way, but the post itself doesn't pretend to be anything other than a how-to documentation guide for Android integrators.
In some sense it is bit missleading as well. On firmware level, you typically need to use a lot of unsafe code. Of course, this highlights the potential danger zones very well, if you use it correctly. But there is still chance that human made a mistake that leads into memory bug. It should be addressed, while it greatly reduces the risks.
I'm hacking on a firmware in Rust right now, for the ESP32-C3, and the only unsafe bits are buried deeply in the esp-hal crate for the C FFI to ESP-IDF and low level hardware twiddling. esp-hal exposes safe interfaces to all of that, so I've had to write zero unsafe code.
But someone else just has written that unsafe code. It is there and unsafe is used as intended, to create an abstraction layer and limit the risky area. Note that even if you use safe abstractions, interactions through safe abstractions can leak to issues in unsafe code.
This may come as a shock, but it's actually unsafe all the way down. Turns out everything needs to become machine code eventually, and CPUs can do all kinds of nasty stuff. Doesn't make containing the unsafe codes to a region of the program any less of a good idea.
I was just about to edit my comment, because indeed that is the case, and someone will grab into it. But firmware as a term includes typically writing also that part which parent used ”safely”. In Rust world hal is own term and as abstraction more known term, but that is not the case if you advertise Rust in blog in order to make non-Rust users to make an understanding. If you build your own products, there is no hal, you need to write it. And use of unsafe to limit the dangerous code is indeed excellent practice.
But why leave that part out from the blog post which gets a lot of audience?
If interactions through safe abstractions leak unsafety into your code, that's a bug in the abstractions.
Edit: who voted me down? This is literally the definition of writing a safe abstraction around unsafe code in Rust. If the user of your safe abstraction can trigger a segfault or UB or otherwise do something that is supposed to require unsafe, then your safe abstraction is buggy.
Memory safety and UB sure you could argue for that. But take for example something like FD reuse. In Rust's std library there's abstractions to prevent that and encourage ownership semantics even though there's no segfault or UB that will happen as a result. So clearly "safety" is a spectrum here. And it should be pretty obvious from the existence of unit tests and various property testing frameworks & sanitizers that bugs exist that aren't memory safety / UB related can't be completely prevented through Rust's abstractions.
Safety may be a spectrum, but `unsafe` in Rust is very explicit about what it allows/enables. They never say "if you only write safe Rust you won't encounter logic bugs".
All unsafe does is mean you’re only allowed to call unsafe functions from unsafe blocks or unsafe functions. Idiomatically memory safety is guaranteed if you stick to safe functions that themselves don’t have any issues with memory safety (by being safe or by calling an unsafe function that is correctly implemented).*
For example, retrieving the raw fd from a BorrowFd is unsafe even though there’s no memory corruption issues.
I was just replying to OP that claimed that safety issues in an abstraction are a bug in the abstraction. While that’s a good general rule of thumb, I was highlighting how there are safety issues that are difficult to guarantee within some abstractions.
* and adding on this, technically even using only safe code there’s a few examples how you can create memory unsafety. The canonical example is opening /proc/mem as a file and overwriting memory out from under Rust’s ownership model. This just goes to show how difficult it is to provide a completely safe abstraction even when you limit yourself to memory safety so treating every safety issue a buggy abstraction is too strong. It’s an important opportunity to review but it could just be intractable and you have to establish other conventions to solve the issue
> All unsafe does is mean you’re only allowed to call unsafe functions from unsafe blocks or unsafe functions.
That's the mechanism by which the safety invariant is enforced, not a description of the safety invariant itself; it's the "how", not the "why". The safety invariant itself can be roughly summarized as "memory corruption can't originate in safe code".
I think the Ferrocene people are working on a more formal definition of memory safety in Rust that excludes things like this, since of course no program can defend itself against it.
There are some cases, especially when embedding other runtimes with different invariants, where Rust's safety model isn't quite expressive enough and so people are forced to provide unsound APIs for the sake of practicality. https://docs.rs/pyo3/latest/pyo3/marker/index.html#drawbacks is an example. None of those have been cited here, though, and it's not clear to me that firmware inherently imposes this kind of challenge; as far as I can tell, it's perfectly possible to write sound Rust APIs for firmware.
> This isn't much different than saying that C safe language if you write it perfectly.
The difference is in C, the entire language is unsafe, whereas in Rust only the bits marked `unsafe` are unsafe. Most Rust code does not need to use `unsafe` at all, and by extension most Rust developers don't need to touch `unsafe`. And for those developers who do use `unsafe`, instead of having to prove every single line of the program is safe like you do in C, you only have to prove that the tiny subset of the program contained within `unsafe` is safe.
1. this is nasty
2. this is so much better
3. this is perfect
writing C is (1), most people claim that Rust is (2). (3) is utopia and anyone who claims that is not being honest.
I have seen this argument SO many times. I don't understand why this is so hard to understand? Do C folks use mmap directly instead of malloc? Does "safe" or "sensible" abstraction not exist in C? Do you use void* everywhere because "safe" abstraction like struct S* simply doesnt exist and if they do exist then "well, it could be wrong anyway so why not just give up?"
The problem with manual memory management is that it makes safe and easy abstraction a lot harder. Every non-trivial function has a set of rules on who owns what that only exist in comments (or worse, in someone's head). Using the function wrong won't cause a compile error, but random unsoundness (that is sometimes data/dataflow dependent so pretty much impossible to sanitize for).
There is Rust code I saw that was cluttered with "unsafe block" to an extend that I am a bit skeptical there was meaningful memory safety left.. There is C code which is nicely structured where I have more confidence in. I am also skeptical about the overall complexity of Rust and I think cargo is "nasty" because of supply chain risk, proliferation of dependencies. So I do not believe for a second the gap between C and Rust so big Rust proponents want us to believe.
> There is Rust code I saw that was cluttered with "unsafe block" to an extend that I am a bit skeptical there was meaningful memory safety left.. There is C code which is nicely structured where I have more confidence in.
This is anecdotal but I have seen FAR more nasty C code than I have Rust. I can probably count using all my fingers and toes the number of times I have seen and have to vet unsafe code.
I spent a week vetting a WebSocket implementation in C to not have buffer overflows, memory leak, use-after-free, overflow, etc etc before I even start vetting the logic. They also have their own bespoke async library so I have to make sense of that first too.
I spent a day looking at WebSocket implementation in Rust exactly because I don't have to worry about stuff like is this void* reliable and following the call stack to make sure it makes sense.
> I am also skeptical about the overall complexity of Rust and I think cargo is "nasty" because of supply chain risk, proliferation of dependencies
I am curious how C solves this problem, if you need a btree, a JSON parser, a tree-sitter, a string with SSO optimization; where do you get this? Write your own? Vendor some packages? Rely on the package manager?
They all have the same issue of there's simply too many code to vet.
> They all have the same issue of there's simply too many code to vet.
The problem is not the amount of code to vet, it's the amount of people who own it, and thus the amount of people I need to trust.
In C, I use a "standard library" replacement like GLib or APR, and if they don't have what I need, then I implement it myself. Thus, the number of owners is just 2.
Is my own code less trustworthy? For a start, of course. But once I fix it, once a tool becomes stable, it stays fixed, in the face of all future dependency updates.
I don't see why you can't do this with Rust? If you want to use your own internet checksum implementation that doesn't draw in "random dependencies" for simd or whatever else. You are free to implement, verify and optimize your own.
It's not possible to do with crates.io because of the culture it has.
When I pull in GLib, APR, libuv, etc as a dependency, that is literally the only dependency. They depend on glibc, maybe libpthread/libm, and nothing else.
I'm being a bit unfair to tokio, since they seem to actually be trying really hard to keep their dependencies down.
But I'll stick to my belief, as that seems more unusual than usual to me, for crates.io libraries in general. C has a culture of minimalism. Rust has a culture of easy of use. The latter is a nightmare for trust.
Cargo being part of the language has lowered the barrier for code re-use. This is a good thing. And the reason Rust libraries seem to be more granular and numerous than C or C++ libraries which seem to conglomerate into monstrosities like Boost. Cargo allows for pinning dependencies, vendoring dependencies, and even the dependencies of dependencies.
Supply chain attacks are real threats however, which plague any language with or without a package manager (see the XZ backdoor for a great example of a supply chain attack in C). Rust projects may have many more dependencies than C projects, but that's precisely because each is smaller and easier to verify, because Cargo makes integrating many libraries easier than the process has been historically for C/C++.
If I pin dependencies, I am susceptible to not getting security updates.
> because each is smaller and easier to verify
If I do not pin dependencies, then I am susceptible to crates.io shenanigans, where the artifacts uploaded to crates.io are not necessarily the code that was verified on github.
One of the 100 owners of my mini-dependencies will bump up their minor version by 0.0.1, push a rootkit/backdoor to crates.io (which was never on github), wait for a few users to retrieve that, then push another 0.0.1 update which removes it. Nobody will notice anything.
I can't believe I'm defending Boost here (I hate it) but at least it has some basic gatekeeping; one of their maintainers will have to approve that change, one of Debian's maintainers will have to pull that change, and I know the artifact I get was built on Debian's servers from the source code that is now set in stone forever, visible to everyone.
I don't get that with crates.io. Especially when each of those 100 mini-dependency owners can directly push code straight to my PC.
> If I pin dependencies, I am susceptible to not getting security updates.
True in every language.
> If I do not pin dependencies, then I am susceptible to crates.io shenanigans
I do think it would be nice to have a chain of trust associated with crates.io. Nothing precludes doing that, as far as I know. There's probably already a cargo plugin for it.
> One of the 100 owners of my mini-dependencies will bump up their minor version by 0.0.1, push a rootkit/backdoor to crates.io
This is a situation Cargo.lock can prevent.
Thankfully crates.io is much easier to audit than millions of lines of decentralized [c/q/]make files, bash/zsh/csh scripts, etc.
> There is Rust code I saw that was cluttered with "unsafe block" to an extend that I am a bit skeptical there was meaningful memory safety left..
There certainly is. But is it more than 1% of the Rust code out there?
And let's assume we're talking about a project where it is more than 1%. Let's say there's only 50% safe code (I personally have never seen this, not even in embedded). Is that still not a strict improvement over C?
> I am also skeptical about the overall complexity of Rust
When you're building a small project, using something like C++ or Rust will indeed feel like that.
But when you're building a large project, then using a simple language doesn't make things simpler... it simply moves the complexity elsewhere, and more specifically, into your code.
In that case, consolidating the complexity into a single place (the language) makes it so that this complexity is reusable, you learn it once and you understand all places that use it, even in other projects.
The time it takes to learn, yes, it'll be huge. Afterwards, programming in Rust becomes significantly easier than in C.
> I think cargo is "nasty" because of supply chain risk, proliferation of dependencies.
This one I actually really agree with, and am very sad about it.
ESP-IDF is a pretty high-level framework on top of FreeRTOS. Both do the vast majority of the heavy lifting. Once you're working on bare metal without these fancy frameworks, things get real hairy real fast.
I strongly encourage you to poke around in the lower levels of the IDF. It's equal parts fascinating and horrifying.
Espressif's code is... interesting, to say the least.
> Espressif's code is... interesting, to say the least.
Of that, I have no doubt. I'm anxiously awaiting esp-openmac's completion, so that I can use Wifi without the ESP-IDF as a dependency. It's the only reason I pull that crate in, because I'm aware of the FreeRTOS dependency.
In my experience, one never really has to work at the bare metal layer in Rust, unless you are designing new chips yourself. All the existing microcontrollers I would think of using already have well worn hals. Truly impressive what the Rust community has accomplished.
Yes, on hal level there will be a bunch of unsafe. On firmware application level it's not often I find the need to reach for unsafe though. It would typically be if the hal is lacking in some sense.
I was a bit taken aback when I realized that Embassy's low-level PAC crates expose all of the devices raw registers through "safe" interfaces though. I'm pretty sure that letting you configure DMA engines arbitrarily at any time is not in the spirit of safe Rust. AFAIK their high-level HAL crates do aim to be properly safe but their PAC approach makes me nervous.
Usually the HAL is between the main firmware and the PAC, so whether the PAC methods are marked as unsafe could almost be considered an implementation detail.
But yes, there has been a lot of discussion around how to handle DMA peripherals - the embedded_dma crate offers some abstractions that I've found handy.
This isn’t relevant to either thing you’ve said. The post is about taking the firmware code that’s on various peripherals (eg the microcontroller the IMU is connected to which Android then talks to or the WiFi/BT microcontroller that talks to the actual PHY radio).
Rust in Linux is about the Linux kernel having more rust code but Linux is not firmware in the traditional sense (unless you’re talking about ucLinux which no one in my experience uses) and this would have nothing to do with porting Android to another kernel which I can’t see happening considering how much custom kernel code there is (unless you are talking about fuschia but fuschia is not Android even though it implements an Android compat layer).
Once 100% of the firmware is migrated to Rust. What do you think will be this Google teams’ next steps?
Very likely if Firmware is successful they would use the current positive talking points from this blog post and lessons learned and keep trying to secure Android.
My guess would be the kernel and/or JVM are on the next step list.
> My guess would be the kernel and/or JVM are on the next step list
I wouldn't bother integrating Rust into the JVM because there's limited security value. There may be ergonomic advantages in terms of project management but my hunch is that those are limited for something mature like Dalvik. If they rewrote it to use Futamura projections like Truffle, then Rust would be much more interesting as it would reduce the possibility of JIT bugs AND basically make it impossible to do a sandbox escape (most sandbox escapes target the JIT and thus other memory safety issues are rare & unlikely). Finally Dalvik security is unlikely to matter all that much because they rely on process isolation as the security model (Chrome does too but sometimes it loads multiple V8 isolates in a single process so V8 escapes are more valuable).
> My guess would be the kernel
There's already a Rust in Linux project & if you missed the news there's a lot of political in-fighting on the LKML. It's unlikely that Google has much sway here because even though they employ maintainers like Ted Tso, I suspect their influence on this matter is limited given how staunchly opposed he is & I doubt they'd be firing engineers like Ted over this. The Android team is going to have limited additional influence beyond what's already going on, but the Linux kernel itself is under no threat of being rewritten in Rust any time soon & Android isn't migrating to another kernel any time soon.
The goal of rust for linux isn't to wholesale translate linux into rust, but simply to be able to write pieces of linux (largely new ones) in rust. I think it's very unlikely anyone (including google) will take on a wholesale translation anytime soon. That said
> It's unlikely that Google has much sway here
Google has helped fund the rust for linux project pretty much from the start [1], they're one of three organizations mentioned on the homepage due to their sponorship [2]. They're actively involved in it, and have already ported their android "binder" driver into it with the intent to ship it in android. This strikes me as a very weird take.
Google is a contributor but you’re going to have a bad time if you think that Google is some monolithic entity. While a part of Google is funding it, other Googlers may be maintainers that are opposed to helping the project (eg look at the article about the rust for Linux maintainer stepping down due to burnout from politics and the comments made my Ted Tso a Google employee against the R4L project).
But yes, R4L is a thing that’s still going on but it’s unrelated to rust in firmware unless you bucket everything Rust together. R4L is adapting Rust and establishing conventions and build systems relevant only to the Linux kernel and that’s not necessarily a whole lot in common with firmware projects which are typically managed and maintained very differently with very different build systems.
One of the really nice parts of the Rust linux project is that the new Rust code is getting a some of the memory management patterns in the C code actually written down rather than just being in the heads of the programmers.
Can you please explain what this means ? Isn't it now just in Rust code instead of C code ? Or is there some design documentation being written around this ?
Rust bindings for it are being written so you can call the C code from safe rust. The C code still exists and only the api is duplicated into rust.
The convenient side effect of that is that you need to know what constraints you have to enforce to make that memory safe, and generally memory safe here is going to translate to knowing how to properly call the api and enforcing that.
Very little C code is actually being rewritten in rust. None of the core C code that is really at issue here (but, for example, android's binder driver has been rewritten).
Kind of, but Rust in Linux is already an effort that's way ahead of what this blog is talking about and has adapted itself to things specific to the Linux kernel whereas firmware environments are typically even more barebones.
Honestly this is way closer to a conventional Rust environment than the kernel gives you. In the kennel external crates are pretty much verboten, large chunks of alloc are verboten, no panicking is allowed, etc.
Firmware is not new territory for rust at all. Oxide compiter has been making their firmware in rust for a few years, they even have an embedded OS (called hubris).
This effort may ne new for Android, but its not a "break into a new area" type thing.
And in fact I've been giving more or less annual "State of Embedded Software on Rust" talks since 2017, and every time it has been running as slides on the Nintendo 64 written in Rust.
Embedded software in Rust is pretty much as mature as C and C++ at this point. The vendor won't give you examples in Rust, but honestly their code generally barely works anyway and you end up rewriting it. Might as well rewrite for your use case in Rust.
With all due respect to the Oxide folks who are doing fantastic work and blazing the trail on rust development, the scale of Android devices is inherently much larger (potentially billions of devices will end up running this code)
The most painful thing about this is strings. To convert to/from cstrings, you have to use the stdlib FFI function, which will allocate memory. This was a nice surprise in the form of a bunch of undefined GNU ABI references when linking to C. I was writing bare metal firmware. I hadn't imported libgcc or even libc yet. Having to convert every string and use unsafe blocks everywhere is a pain. And last time I tried it wasn't even possible to interface with a varargs function.
Im not a big Zig fan but it looks so much better from a FFI perspective. I haven't tried it but it looks like everything just works.
But you would have to manually pass cstrings into it? Is there a way to make a wrapper to pass rust strings into it?
Now that I think about it that may have been my issue.
Correct, Rust and C strings are different, so you have to do some sort of conversion between the two, if you have a String and want to pass a *const c_char.
For going from C to Rust, I think you'd just use CString::to_str instead of CString::to_string. I'm pretty sure this just calls std::transmute behind the scenes and doesn't cause an allocation. (In the same way that you generally don't clone vanilla strings and instead pass references around.) iirc, it's still tricky to go the other way. I think if I was working with super C-FFI heavy code, I'd just end up using CString as my default for that code. Custom allocators would also help there, since you could just create the strings on the stack or whatever, but I don't remember if the string version of those is even finished in nightly yet.
You'd use CStr::from_ptr if you've received a C string (which does not allocate, that's correct, though not via transmute, just a cast), but since Rust strings aren't null terminated, you'll need to allocate some memory to add the null byte on the end. Which yeah, then I'd want to try to use CString as much as possible, in that case.
(If you wrote to an array on the stack, you could turn it into a Cstr too, instead of heap allocating, but then you'd have a max string size, which may or may not be something you'd want.)
alloc::string::String (a.k.a. std::string::String) doesn't support custom allocators yet, even on nightly, but that doesn't mean you can't use them; it just means you have to use a third-party crate, like https://docs.rs/string or https://docs.rs/smol_str.
Impressive that they managed to make a plain blog post break. What kind of insane Javascript must be running, that a vertical scroll view with text and pictures has been overcomplicated to the point of breakage
Last time I looked up a function in the Android API reference, it involved a 10MB of file transfer (mostly crude custom fonts, IIRC), and the browser gave up trying to resolve the anchor url meant to jump me to the function itself, so I had to Ctrl-f or manually scroll to actually get to my information.
In recent years, FAANG web teams are way more invested in automating things than engineering them to be efficient, durable, or gracefully adaptive products.
I also got a blank page on Firefox so I opened the inspector and found manmade horrors sadly within my comprehension: a bunch of elements that initially load as hidden then get unhidden later by JS (which never runs) and the body of the blog post is nothing but a script tag. What the fuck, Google.
Uh... wow. https://bugzilla.mozilla.org/show_bug.cgi?id=1845775#c14
"Long story short: the watchdog thread interrupted us four times, so we gave up and threw an error. This also explains why Chrome is significantly faster here: when interrupted, we stop the regexp, handle the interrupt, then restart regexp execution from the beginning, whereas V8 does scary code crimes to keep the regexp running despite the possibility of triggering a GC with unrooted values on the stack. (The words "manually relocate unhandlified references" are involved.)"
If you use the uBlock Origin "disable Javascript for this site," it loads totally fine (all of the content is hidden behind either <noscript> tags or some broken javascript).
Works fine for me with no JavaScript, but if I open it in a private browsing window without LibreJS I get what you describe. Funny, usually it's the opposite!
Try refreshing a few times. It seems that they've managed to partially break the page on firefox. I'm currently also on x64/firefox/linux and it works roughly half the time for me.
It doesn't seem to display properly on all browsers - I got that on Linux Firefox at work earlier, but Android Firefox seems to manage to display it OK.
Not always. I just tried it in FF on my Android phone. No luck. Assuming I'm not missing anything, if they can't be bothered to make a simple blog page loadable.
I gave up on Firefox about a month ago. The stupid not being able to copy bug was enough for me. Have had plenty of webpage issues that Chrome and Edge seem to handle just fine. I don't know what's changed lately, but seems as if it's gotten quite buggy.
I gave up on having feet about a month ago. The stupid I hit my toe on the corner of the wall bug was enough for me. Have had plenty of times now where I've ignored that walls might have corners and I haven't banged my toes anymore. I don't know what's changed lately, but seems as if having feet have gotten quite buggy.
Personally when I bang my toes against a corner, I don't think about removing my feet just to prevent toe banging.
---------------
Reductio ad absurdum aside, if I'm experiencing an issue in a browser, my first thought is to diagnose and fix the issue or find an expectable workaround rather than change to a browser with obvious privacy issues.
Currently, you'll see words, phrases, and names like "Firefox sends", "telemetry", "Google", "Microsoft", "share this with our partners", "share that data with its partners", "sends us data", "share aggregated data", "our third-party ad platform Kevel", "AdMarketplace (a third-party referral platform)", "Cloudflare", "Comcast", "Adjust", "Google advertising ID", "our partner Fastly", and so forth.
In my opinion, I wouldn't expect to see any of those in the privacy policy of any software product that truly respects my privacy.
The browser, for me, is a tool to view web pages. If it does not do that for some web page, I'm going to switch to another browser to view the web page. Lots of browsers to try to view that web page I want to see!
Lol privacy issues. Best not use the Internet at all my dude. There is no fix for the copy bug. There is no fix for not loading many webpages properly. Firefox can't do it's primary job well enough for me. The other browsers can.
Ad blockers get really weird with Google blog pages, especially if you use multiple or very strict ones. There are plenty of rule lists where most Google domains are just blacklisted by default in case something tries to load javascript from them, and those will break the blog posts.
Page works for me, but the CSS is screwed up by adblock. It is what it is.
Comes up essentially blank for me too on Firefox desktop on ubuntu 24.04. Still comes up blank with the only adblocker I use (ublock) disabled as well.
Same for me. It was going on for quite some time, but guess it has started with full force now. Google will now steadily turn Firefox useless by having everything break on a non-Chromium browser.