← Back to context

Comment by kimixa

2 years ago

> Just look at what Android had to do to implement a secure platform with somewhat of a well defined non-1980's OS API - it basically removed the Unix part of the OS and built a Microkernel-like thing on top of it.

...Huh? Pretty much all Android adds to the kernel is Binder, which is really a performance optimization for it's version of IPC, which today has equivalents that mean it probably wouldn't need it's own version now if it didn't need compatibility.

The security model allows native apps, which have access to all the normal libc, or even direct syscalls. Some hardware access is marshalled through userspace daemons, which may implement security for things like camera/microphone access, but that's been a pretty common thing on other unix-style OSs for ages.

Though the separation of each app into it's own UID might not have been the original intent, but it seems to work implemented with those same unix capabilities.

Yeah, most apps probably don't use POSIX APIs directly, and instead the java Android APIs, but they are very much build on top of the "crufty 1980s OS APIs", rather than bypassing them.

Android blocks most of Linux syscalls with seccomp. They make heavy usage of SELinux.

Binder is not just a simple IPC system. It requires userspace parts which aren't even the parties who are talking through IPC. Like you said it has lots of performance optimizations, and that requires it hooking up to a number of kernel subsystems. At the beginning it was met with a lot of resistance from kernel maintarners when tried to be merged.

Why did Google came up with it? When talking about the 'Android sandbox' people usually talk about how Android gives each app an different uid/gid. But that's just the beginning of the story. It wasn't enough for sandboxing needs since the beginning. Thats why they took the binder idea from BeOS and used on Android.

Since then Google has blocked everything. Native code is needed on any practical system, but today's native code has access mostly to nothing, just the common needs to run (like libc which is used due backward compatibility). The proper way to get something is through Binder (and even that is fine-grained controlled on a per process basis).

Linux and Unixes in general don't implement true capabilities. Android does through Binder, but since Binder lives “inside Linux” everything else has to be blocked to make these capabilities useful.

> Some hardware access is marshalled through userspace daemons

Most of stuff are, at least initially, orchestrated through userspace software - daemons like you said, but mostly HALs (which are also daemons).

The main takeaway is that binder is _the core_ of Android userspace. It's tightly integrated with SELinux and everything passes through it at some point. Permissions are enforced through it. In the end, the whole “binder subsystem” is architectured like a Microkernel system.

  • I'd argue that permissions aren't really enforced by binder, just binder is used to talk to the endpoints that enforce permissions. Binder has no interaction with SELinux outside the policy requirements to allow apps to open it's device nodes - there's no interaction in the kernel code at all [0]. It's just an opaque data blob IPC mechanism, not tagging payloads with capabilities or similar in-kernel.

    At the point that binder was created, there wasn't a good kernel-level IPC system integrated into linux, true, but I'm not sure I agree with the idea that it's in any way tied into the capabilities/permissions system outside from being a communication channel to the userspace daemons things that do.

    And native code does have access to every service on the device - just as any app. It's just more painful than the Java layer, and some things aren't guaranteed to be stable over API versions. Less useful for apps, but still not part of the security model. At an extreme level, the java code is run in the same memory address space as any native code, there's no protection from that. Hell, some apps even used to patch dalvik/ART live. It was insane and fragile, but entirely possible.

    And the "disallowed" syscalls are those not exposed by libc though some more are restricted for user apps (mostly around UID management, which is the big "major" deviation from standard unix, and system stuff like loading kernel modules or rebooting...) [1] So maybe it doesn't allow some linux extensions, or could remove the "old" versions of replaced syscalls (when arguments were found not to be large enough, flags arguments added etc.). But there's not really anything missing from there as used in 99% of unix userspace programs anyway.

    So I maintain it's still a system build on top of a unix kernel and libc with a few additions on the side, and all that is still accessible to apps. Perhaps you think I'm including all the stuff people associate with modern "Desktop Linux"? Like X11 and/or wayland? Or the entire FHS? That's the main thing Android has completely divorced from, but that also isn't "Unix", or even really from the 80s even if it has some ancestors there.

    [0] https://android.googlesource.com/kernel/common/+/refs/heads/...

    [1] See SECCOMP*.txt and SYSCALLS.txt from https://android.googlesource.com/platform/bionic/+/refs/head...

    • > I'd argue that permissions aren't really enforced by binder, just binder is used to talk to the endpoints that enforce permissions. Binder has no interaction with SELinux outside the policy requirements to allow apps to open it's device nodes - there's no interaction in the kernel code at all [0]. It's just an opaque data blob IPC mechanism, not tagging payloads with capabilities or similar in-kernel.

      I'm going to describe some flows so you can have a perspective on what's going on. I'm describing them as I remember when I worked with this stuff.

      First, you have to know about SEAndroid which is a thing built on top of SELinux. It doesn't add anything new from a kernel perspective but there's a whole 'firmware image build-time framework' to _legislate_/declare permissions about all sensitive stuff on the system. Like, you want to add a new daemon to the system? Maybe this daemon will serve some AIDL (such daemon is called a service)? Oh, then you should declare its SELinux stuff, like "it's a daemon", "it should have access to sockets/whatever" and "it will serve such and such AIDL interfaces" and so on. You should state the things it should have access to. The important part is that the SEAndroid stuff has 'Binder services' as a first class citizen, much like a fs path, socket or anything else SELinux is capable of legislating about (even though the kernel and kernel's binder knows nothing about binder services from an SELinux perspective). A good part of these policies don't talk about vanilla Linux stuff (like processes, files, sockets, etc.) but are about _services_ from "binder realm" - the kernel doesn't know about and ignores them. During the system image build process these policies are "compiled" and put somewhere on a filesystem.

      When Android boots, after all the device-mapper mess (which, as most things, is a subject on its own) eventually the "base" filesystems are mounted (this mess comes in part from Project Treble). At this point there's no SELinux stuff loaded (which means, IIRC the SELinux status from the kernel is "off"/insecure/whatever). Then comes the time when the SEAndroid/SELinux policies are to be loaded. A system process looks up and loads the SELinux blobs generated during the system's image build process, and then enables SELinux.

      Ok, now a flow from runtime:

      You have process A, which in this example will be on the client role, and process B which will be the server. Process A comes from a program A which is a plain ELF binary on the filesystem. Same for process B. The SEAndroid policies loaded during boot define the SELinux contexts associated with process A and B. Program's B context includes the list of AIDL interfaces/services it implements (or serves). And program's A context includes an statement that it should have access to such and such AIDL services (it states nothing about program B or others who implement the AIDL services).

      Every binder driver device is exposed to userspace through the /dev fs and they each need to have an associated _service manager_ process. When I worked with these there were 3 binder devices, and so 3 associated service managers. Originially there was just 1 binder. So process A needs to have access to, say /dev/binder, and this is enforced with vanilla Linux acccess controls. Same for process B on this part. Process A tries to establish communication with whatever process is implementing the AIDL service it's trying to talk to (process B) (services have a string name, it's a binder thing). This attempt actually ends up on the binder's service manager, which asks the kernel's SELinux policy 'Can this process A here access that AIDL interface?'. The kernel doesn't know about AIDL interfaces, but the SELinux policy blobs downloaded into it earlier encode this info in such a way that such questions can be asked and answered. If so, it awakes/starts process B and communication goes on.

      As an addendum, binder supports transporting file descriptors and that's what I meant by true capabilities. That has lots of implications and caveats given it's going on a Linux-based system but that's another conversation...

      > And native code does have access to every service on the device

      No, it dependes on which process, or more specifically, which SELinux context the process running that code has been attributed to.

      > At an extreme level, the java code is run in the same memory address space as any native code, there's no protection from that

      Yes, on the level this conversation is concerned about, it doesn't matter what you program is doing, if it's a Java thing, python, web browser, if it's a code dynamically linked into your process from a shared library object - what matters most is that it ends up calling syscalls and that each process has an identity from the SELinux perspective inside the kernel (so the SELinux subsystem inside the kernel knows which process is calling a given syscall).

      > Perhaps you think I'm including all the stuff people associate with modern "Desktop Linux"? Like X11 and/or wayland? Or the entire FHS?

      My comment about this part would be that libc is... not related at all. It's importance comes from 'Oh, this is C/C++ code so it doesn't actually issue syscalls, it does all that though a libc because, well, this is C and Unix'. On this abstraction level I would put it as just an implementation detail as apps being programmed in Java.