Skip to main content

Mach Kernel Abstractions and XNU Internals

· 22 min read
Pranav Ram Joshi
Software Engineer — Systems & Networks

Man is born free, and everywhere he is in chains. - Jean-Jacques Rousseau; The Social Contract

Search for the truth is the noblest occupation of man; its publication is a duty. - Anne Louise Germaine de Staël; De l'Allemagne

Preamble: XNU and the Mach Kernel

An operating system is a set of system software that acts as a bridge between the application software and the underlying hardware. Among the various software packaged with an operating system is the kernel — a program responsible for communicating with the hardware and managing the system in its entirety. The purpose of this post is to explore the XNU kernel source and understand the core abstractions of the Mach kernel that underpin the macOS kernel architecture. We'll walk through Mach's taxonomy — tasks, threads, IPC ports, processor sets — and examine the XNU-specific implementation details, including pointer authentication and key Mach routines like task_info and mach_task_self.

How the API_AVAILABLE Macro Works on macOS: C Preprocessor Deep Dive

· 10 min read
Pranav Ram Joshi
Software Engineer — Systems & Networks

There are various ways one could use C preprocessor macros. Learning how to properly use them--or failing to--can end up producing some bizarre codebases. If you have ever encountered the <os/availability.h> header file on macOS, you'll find some macros that are confusing to interpret.

The API_AVAILABLE Macro Definition

The macro is defined as follows:

Writing Portable C Code: Preprocessor Directives, Data Types, and GNU Autotools

· 45 min read
Pranav Ram Joshi
Software Engineer — Systems & Networks

Preamble

Writing a portable C program turns out to be more of a nightmare than imagined. C is notorious for being platform dependent--also known as portable assembly language, yet most of the software that runs across different systems is written in this language. The secret lies in understanding what varies between platforms--data type widths, byte ordering, memory alignment, available system calls and library functions--and using the right tools to handle those differences at build time. In this post, we'll walk through the C compilation pipeline, explore how preprocessor directives enable conditional compilation, examine data type portability pitfalls like endianness and struct padding, and introduce GNU Autotools as a build system for portable projects. This is the third in a three-part series. The first post covers UNIX terminal devices, and the second explains how the shell executes programs.

Berkeley Packet Filter (BPF): Packet Capture and Filtering in C

· 32 min read
Pranav Ram Joshi
Software Engineer — Systems & Networks

Be conservative in what you do, be liberal in what you accept from others. - RFC 793

Preamble: From Telecommunication to Packet Filtering

Throughout the course of humanity, we have witnessed various forms of communication. We use language to communicate on a daily basis. Writing letters is also a form of communication, albeit a form which has slow transmission and reception time. Then came the era of telecommunication. Through the use of electronic and electrical means, we soon came to the realization that communication can be instantaneous. This, I believe, is one of the greatest achievement for mankind. This really made the world feel like a small village. The Internet is the closest to the anywhere door we know from Doraemon.

How the Shell Executes Programs: Fork, Exec, and Environment Variables

· 12 min read
Pranav Ram Joshi
Software Engineer — Systems & Networks

Preamble

A shell process is a command-line interpreter — the program that reads your input, locates the right program or builtin command, and orchestrates its execution. Whether you use Bourne Shell, Bash, Korn Shell, or another flavor, the underlying mechanics are remarkably similar: the shell forks a child process, the child replaces its image with the target program via exec, and the parent waits for completion. In this post, we'll trace that fork-exec-wait cycle step by step, explore how environment variables are passed to programs, and look at the shell's capabilities as an interpreter — including loops, conditionals, and history expansion. This is the second in a three-part series. The first post covers UNIX terminal devices and line discipline, and the third explores writing portable C code.

How UNIX Terminal Devices Work: TTY, Pseudo-Terminals, and Line Discipline

· 13 min read
Pranav Ram Joshi
Software Engineer — Systems & Networks

Preamble

When you open a terminal, you are greeted with a prompt and the system expects your input. But have you ever wondered what sort of device the terminal really is? A terminal is more than just a window for typing commands — it is a character special device managed by the kernel, with a layered architecture involving device drivers, line discipline modules, and pseudo-terminal pairs. In this post, we'll explore the internals of the UNIX terminal device: how tty and pty devices work, what terminal line discipline does, and how the kernel mediates between the user and the shell process. This is the first in a three-part series. The second post covers how the shell process executes programs, and the third explores writing portable C code.

Use-After-Free in C: Why It Happens, How Static Analyzers Catch It, and What Rust Does Differently

· 11 min read
Pranav Ram Joshi
Software Engineer — Systems & Networks

Introduction: Why Use-After-Free Doesn't Always Crash

Memory is a crucial topic when it comes to building software. Programs running on historical devices referenced physical memory locations directly, a practice superseded by a dedicated hardware component: the Memory Management Unit (MMU). We now work with virtual memory addresses that is managed by MMU for us[0]. For a hands-on exploration of how process memory is laid out on macOS ARM64, see the memory post.

malloc and free: Why Accessing Freed Memory Can Silently Succeed

I recently stumbled across a video regarding the RustTM language. The intent of this post is not to look down on the RustTM language but to observe a behaviour of a trivial C program. A program fragment was shown in the video, similar to the one shown in Listing 1. After building the executable, we can notice that the program exits normally. But before that, we tried to access a region of memory that was "freed". In contrast, RustTM informs this issue during compilation.

Process Memory Layout on macOS ARM64: Address Spaces, Mach-O Segments, and Debugging with LLDB

· 37 min read
Pranav Ram Joshi
Software Engineer — Systems & Networks

Tools for Inspecting Process Memory and Object Files

Before we even step into the world of virtual memory and process address spaces, let's first learn of some tools which might come in handy when dealing with source programs (we'll describe them in lengths later):

  1. Object File Inspection. This can be done through various tools such as otool, objdump, and llvm-objdump.
  2. Debugger. Tools such as gdb and lldb can be helpful when inspecting how the program executes.
  3. Virtual Memory Information. Sometimes, we need to see how a process has arranged its virtual memory space. Tools such as vmmap, leaks, and pmap (also potentially strace) can be used for this purpose.

Other tools can come handy frequently as well. For instance, if we want to list the "files" currently owned by the process, lsof provides useful information.