> 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.