Monday, August 21, 2023

XNU, a hybrid kernel

XNU was originally based on the Mach microkernel. But nowadays macOS blurs the lines. Though some parts of macOS follow the microkernel spirit, other parts are monolithic. It's more complex than a "pure" microkernel. Perhaps a microkernel has less abstractions. But XNU is a hybrid kernel that nonetheless still employs the priciple of least privilege well - striking a balance between the two realms.

For example, Windows is a hybrid, rather than a monolithic kernel, since various subsystems run in usermode server processes. This is in stark contrast to the Linux world, where essentially everything happens in kernel space.

But in the realm of macOS, we have a hybrid dynamic once more: some parts of the system are loosely coupled and isolated from the kernel, while other parts are tightly coupled with the kernel.

Hybrid Deviation

The spirit of microkernel design is to have as few components as possible tightly integrated within the kernel. Thus, services, drivers, and components are kept isolated from the kernel and instead exist as separate user-level servers, improving system modularity, scalability, and security.

And in stark contrast, components tightly integrated with the kernel, like we find within hybrid or monolithic kernel design, deviate from the traditional microkernel philosophy. And components are instead executed in kernel space, closely tied to the kernel's execution, thus potentially leading to reduced modularity and a higher risk of instability.

For example, in macOS, memory management, scheduling, device drivers, the networking stack, and file system drivers, are all tightly coupled with the kernel. In this context, macOS somewhat drifts from the microkernel spirit

XNU kernel layout

Microkernel Influence

But on the other hand, user-level services like XPC (formerly "mach services") enable the development of user-space servers that run separated from the kernel. Things like font services, printer services, and system daemons like the WindowServer.

Loosely integrated components keep stuff out of the kernel. The aim of microkernel design is to keep services, drivers, and components isolated from the kernel and run them as separate user-level servers. This approach improves system modularity, scalability, and security.

macOS follows the microkernel spirit in its implementation of user space frameworks, interprocess communication mechanisms, in keeping drivers in user space via DriverKit, in providing virtualization mechanisms in user space, and also maintaining kernel extensions (kexts) in user space.

With regard to applications and scripting frameworks like Carbon (now deprecated) and Cocoa — in a very general sense, applications are designed to run in user space. Isolation prevents applications from directly affecting kernel operations, providing increased stability. For example, the Cocoa framework, along with Foundation, AppKit and CoreData, help define objects and interact with macOS, abstracting lower-level interactions with the kernel and hardware, thus reducing the burden on software developers. And Cocoa can also be used with various language bindings, not just Objective-C.

And furthermore, certain file system services like FUSE, as well as cloud storage integrations, are implemented in user space to ensure they don't compromise kernel stability. Features like Keychain Services are also isolated in user space for higher security.

IPC in macOS

Interprocess communication is an aspect in the macOS world that exemplifies the microkernel spirit. For example, throughout the years, macOS has provided an abundance of interprocess infrastructure. Though, in a traditional "pure" microkernel, IPC might be simpler. But that's a triviality. And today it should be noted that a lot of this stuff today simply happens through XPC. And some of these mechanisms are now retired:

A History of IPC infrastructure

Mach ports: a part of Mach microkernel that macOS is built upon. Enables communication between processes by allowing them to send messages and share resources through port rights.

Distributed notifications: allows processes to broadcast and observe notifications across the system. Primarily used for events and notifications that are of interest to multiple processes.

Distributed Objects: An Objective-C framework enabling objects to be used and manipulated across different processes or even on different machines over a network.

AppleEvents and AppleScript: Provides a way to automate tasks with scripting, allowing one application to control another application's functionality through a messaging protocol.

Pasteboard: Used for transferring data between applications. Enables copying and pasting of text, images, etc.

XPC: A modern IPC mechanism that provides a secure and efficient way for processes to communicate with each other. This can be used for various tasks, including interprocess communication and launching helper processes.

Grand Central Dispatch: Manages concurrency and parallelism. Also provides a form of interprocess communication through dispatch queues. Enables processes to asynchronously communicate and exchange data using a queue-based system.

XPC

XPC is a system-level framework in macOS that facilitates interprocess communication between processes. This system enforces strong typing and strict setter/getter methods, ensuring data exchanged between processes adheres to predefined types and structures. It operates on a client-server model, where clients connect to servers bi-directionally for communication. A bit from Apple's documentation about IPC:

XPC provides a lightweight mechanism for basic interprocess communication. It allows you to create lightweight helper tools, called XPC services, that perform work on behalf of your app. The launchd system daemon manages these services, launching them on demand, shutting them down when idle, and restarting them if they crash. Benefits of XPC services include:

So, XPC services are handled by the launchd system daemon - launching them on demand, shutting them down when idle, and restarting them if they crash.

This helps to centralize workloads generated by varying processes and reduce complexity. And it also helps improve privilege isolation.

macOS and XPC utilize a few different strategies for authenticating XPC. For example, Mac Ports in combination with entitlements, code-signing, audit tokens, and access control lists. And sandboxing, too. I won't delve into the details in this post, but will defer to Apple's docs here:

In macOS 14 and later, the operating system uses your app’s code signature to associate it with its sandbox container. If your app tries to access the sandbox container owned by another app, the system asks the person using your app whether to grant access. If the person denies access and your app is already running, then it can’t read or write the files in the other app’s sandbox container. If the person denies access while your app is launching and trying to enter the other app’s sandbox container, your app fails to launch.

The XPC API is built on top of the C programming language, which means it provides C-compatible functions and data structures. But despite being based on C, the API is primarily used within the context of Objective-C.

Messaging-based IPC

The XPC API uses a messaging-based approach for IPC, meaning processes communicate by sending messages to each other. This is quite different from other systems and in some sense is not the traditional function calling observed in other systems. Messages are essentially passed between objects in Objective-C via message handling functions. The convention for a declartion might look something like this:

(void) startProcess:(NSDictionary*)arg0 withReply:(NSString*)arg1;

Kexts

I mentioned earlier that kexts are loaded in userspace. But unlike XPC, this aspect of macOS deviates from microkernel design and is closer to monolithic kernel design. kextload is currently a Kernel Extension Load utility, which macOS uses to load drivers and other kernel extensions on demand. The functionality is reminiscent of modprobe in Linux.

Though, allegedly Apple is trying to phase this behavior out, to keep third-party extensions out of the kernel. From Apple:

Starting with macOS 11, if third-party kernel extensions (kexts) are enabled, they can’t be loaded into the kernel on demand. Instead, they’re merged into an Auxiliary Kernel Collection (AuxKC), which is loaded during the boot process. For a Mac with Apple silicon, the measurement of the AuxKC is signed into the LocalPolicy (for previous hardware, the AuxKC resided on the data volume). Rebuilding the AuxKC requires the user’s approval and restarting of the macOS to load the changes into the kernel, and it requires that the secure boot be configured to Reduced Security.

Altogether, Apple's commitment to recent simplifications of its IPC mechanisms, as well as phasing out on-demand kernel extensions, hints at nudging closer toward the spirit of microkernel design. And overall, such design decisions might bode well with regard to efficiency and security.

No comments:

Post a Comment