Skip to content

Mach traps vs syscalls BSD : deux noyaux dans un seul Mac

XNU mélange un micro-noyau Mach 3 et une personnalité BSD : à quoi sert chaque famille de syscalls et comment elles diffèrent en pratique.

Publié le 5 min de lecture

La chose la plus déroutante à propos de XNU, le noyau de macOS, c'est qu'il s'agit de deux noyaux. En dessous, c'est un descendant de Mach 3 — un micro-noyau conçu à Carnegie Mellon à la fin des années 1980. Par-dessus, Apple a maintenu vivante toute la personnalité BSD : la surface POSIX, le VFS, le scheduler, le modèle de signaux. Le résultat est l'un des noyaux les plus étranges encore en production : un cœur Mach qui se parle à lui-même presque exclusivement par passage de messages, avec un noyau Unix coopérant dans le même espace d'adressage.

Cette hybridation explique pourquoi macOS expose deux familles distinctes de syscalls, acheminées par la même instruction SVC mais vivant dans des tables différentes.

Ce qui vit où

La séparation n'est pas arbitraire. Il existe des règles claires :

DomaineFamillePourquoi
Fichiers, sockets, signauxBSDHéritage direct de 4.4BSD ; POSIX les attend.
Gestion mémoire (mmap)BSDEnrobe les primitives Mach VM sous-jacentes.
Modèle de processus (fork)BSDAPI process POSIX au-dessus des tasks Mach.
Tasks, threads, portsMachObjets Mach de première classe, propriété du micro-noyau.
IPC (tout type, partout)MachLe message Mach est la seule primitive de communication cross-task.
Primitives mémoire virtuelleMachmach_vm_allocate, mach_vm_protect, etc. portent les appels BSD VM.
Synchronisation côté noyauMachsemaphore_*, mk_timer_*, attribution de voucher.

Autrement dit : ce que POSIX veut, BSD le possède. Ce qui touche à la comptabilité du micro-noyau — tasks, threads, ports, memory objects — c'est un Mach trap.

Comment le dispatch diffère

Quand le SVC se déclenche sur arm64, XNU regarde le signe de x16 :

  • Positif → chemin BSD. unix_syscall64() indexe sysent[N], copie les arguments, appelle le handler, positionne le carry dans PSTATE en cas d'échec. Sémantique errno.
  • Négatif → chemin Mach. mach_call_munger64() indexe mach_trap_table[-N], copie les arguments, appelle le handler, retourne un kern_return_t directement. Pas d'errno.

C'est aussi pourquoi tout Mach trap sur ce site a un numéro négatif (mach_msg2_trap est -31, task_for_pid est -45) et tout syscall BSD un numéro positif.

Sur x86_64, l'encodage est différent — (class << 24) | numéro packed dans EAX — mais la séparation conceptuelle est la même.

Mach est la seule IPC

C'est la conséquence la plus importante de la séparation : sur macOS, toute communication inter-processus passe par Mach. Il n'y a pas d'équivalent du sendmsg Linux avec SCM_RIGHTS. Pas de pidfd_send_signal. Si A veut parler à B, A envoie un message Mach à un port que B possède. Point.

Cela a d'énormes conséquences :

  • XPC, le framework IPC moderne de macOS, est construit sur les messages Mach.
  • launchd, le système d'init, est un broker de ports Mach — les services enregistrent des noms, les clients les cherchent, launchd distribue des send rights.
  • L'injection de code passe toujours par Mach : task_for_pid donne un send right sur le port task de la cible, et mach_vm_write / thread_create_running font le reste.
  • Le trafic vers WindowServer (chaque dessin CGContext, chaque livraison d'événement) est un message Mach sous le capot.

Si vous vous êtes déjà demandé pourquoi un outil d'injection de processus sur Linux fait quelques centaines de lignes de ptrace et sur macOS quelques dizaines de lignes de mach_*, c'est pour ça. Mach rend l'opération conceptuellement plus propre — et, quand SIP et AMFI ne s'en mêlent pas, mécaniquement plus simple.

La personnalité BSD

Le côté BSD de XNU existe essentiellement pour garder portable le logiciel POSIX. Quand vous appelez open(), voilà ce qui se passe :

  1. Le handler BSD sys_open s'exécute.
  2. Il résout le chemin via le VFS.
  3. Il alloue une paire fileproc + fileglob dans la table des fd BSD.
  4. En interne, il demande à Mach toute la mémoire nécessaire via kmem_alloc et compagnie.

Idem pour mmap. Le handler BSD est un mince enrobage de mach_vm_map. Si vous regardez le graphe d'appels dans DTrace, vous verrez la plupart des syscalls BSD finir dans Mach VM ou Mach IPC.

Quand utiliser quoi

Pour du code applicatif, vous voulez presque toujours la surface BSD. POSIX est portable, bien documenté, prédictible.

Vous descendez dans Mach quand l'API BSD ne suffit pas :

  • Monter une connexion XPC sans launchd. Vous jonglerez avec des ports Mach.
  • Inspecter la mémoire d'un autre processus. task_for_pid + mach_vm_read.
  • Implémenter une primitive de scheduling très précise. semaphore_wait_trap est plus léger que les sémaphores POSIX.
  • Atteindre les user clients IOKit. iokit_user_client_trap est le dispatch sous-jacent.

Pour les extensions noyau et DriverKit, Mach est la norme — objets IOKit, descripteurs de mémoire, primitives de synchronisation sont tous nativement Mach.

Pour aller plus loin

Articles associés

Ce qui se passe vraiment quand un programme arm64 sur Mac émet une instruction SVC — du stub utilisateur jusqu'au dispatcher BSD, et retour.
Comment task_for_pid fonctionne, pourquoi Apple le verrouille, et ce que son modèle d'entitlements implique pour les outils de sécurité macOS.