System calls, signals, and exceptions
For the full common signal path and ARM64 user-frame return protocol, continue with Signal posting, handler entry, and sigreturn.
The syscall table is generated
bsd/kern/syscalls.master is the authoritative input for BSD syscall numbers, argument types, handler names, audit events, and generation flags. Its header identifies the products of makesyscalls.sh: dispatch table, syscall-name table, number/prototype headers, and audit mappings. The file also makes configuration-dependent omissions explicit; a reserved slot can call nosys() instead of a feature handler.
struct sysent holds sy_call, return type, argument count, argument-byte count, and, where configured, a 32-bit argument munger. Handlers have the broad form handler(proc, argument_block, return_values). Their C return is usually an errno-style status; the syscall's successful value is placed in the separate return storage. Confusing those two returns makes syscall handlers difficult to follow.
The userspace side is partly in libsyscall. create-syscalls.pl reads the master file plus platform mappings/custom implementations and emits stubs. custom/SYS.h supplies architecture-specific trap/error macros; specialized wrappers live alongside it and under wrappers/. NO_SYSCALL_STUB marks calls whose normal public entry is not supplied by the generic stub treatment. This tree contains a syscall interface layer, not all of libc, libpthread, dyld, or libdispatch.
A concrete ARM64 call path
Mermaid source
sequenceDiagram
participant U as User wrapper
participant M as ARM64 exception entry
participant D as unix_syscall
participant H as BSD handler
participant A as Return AST handling
U->>M: SVC with call number and arguments
M->>D: BSD syscall branch
D->>D: Select sysent#59; collect arguments
D->>H: sy_call(proc, uu_arg, uu_rval)
H-->>D: error or special return status
D->>D: Prepare saved registers and carry flag
D->>A: Return path
A-->>U: Resume user execution or signal trampoline
The SVC handling in osfmk/arm64/sleh.c distinguishes Mach traps from Unix syscalls. unix_syscall() obtains the current uthread, selects a bounded table entry, fills uu_arg, initializes uu_rval, records the syscall number, and applies configured filtering and audit hooks around sy_call. Arguments are copied from saved registers or the relevant user stack representation, with ABI-specific conversion where needed. These checks are part of the entry boundary; subsystem handlers must still validate the content of pointers and arguments they receive.
arm_prepare_u64_syscall_return() implements three cases that recur throughout BSD:
| Handler result | Machine return action | Meaning for the caller |
|---|---|---|
| Ordinary success/error | Pack return values, or place errno in x0 and set carry |
The wrapper returns the value or takes its cerror path |
ERESTART |
Move the saved PC back by the SVC instruction width | Re-enter the syscall after returning from the kernel |
EJUSTRETURN |
Leave the specially prepared saved state alone | A handler has already arranged how execution resumes |
bsd/dev/i386/systemcalls.c provides the analogous x86 paths, including unix_syscall64(). Register details belong in the platform chapter, while sysent and subsystem handlers remain the shared logic.
Cancellation and interruption are also separate. For example, read() calls __pthread_testcancel() before read_nocancel(). A blocking operation can return EINTR or ERESTART; the I/O helper may still report successfully transferred bytes if progress already occurred. An interrupted syscall is not automatically a canceled pthread.
Where signal state lives
Signal disposition is process-wide: p_sigacts.ps_sigact[] stores handlers, ps_trampact[] stores trampolines, and the associated masks encode reset, alternate-stack, interruption, and masking behavior. p_sigignore and p_sigcatch summarize disposition. Pending and blocked signals are chiefly per-thread: uu_siglist, uu_sigmask, uu_sigwait, and uu_oldmask. p_siglist can hold signals captured back from threads. See the declarations in proc_internal.h and user.h.
psignal_internal() supports process-directed and thread-directed variants. It rejects inappropriate targets, handles ignored/exiting processes, coordinates signal serialization, selects an appropriate thread, updates pending state, and requests attention through an AST or thread interruption. The process lock protects the targeted thread/uthread during parts of posting; the source explicitly warns that their existence is no longer guaranteed just because a raw pointer remains after that protection is dropped.
Posting does not execute a user handler on the posting CPU's call stack. bsd_ast(), reached through asynchronous system-trap handling on the target's return path, checks timers and pending signals. Under process locking it loops through issignal_locked() and postsig_locked(). Selection deals with blocked, ignored, stop/continue, and traced-process cases; delivery consumes a pending signal and chooses termination or a user catcher.
postsig_locked() routes fatal default actions through the exit machinery and, where configured, core-dump handling. For a catcher it prepares masks and calls the machine-dependent sendsig(). bsd/dev/arm/unix_signal.c and the x86 counterpart build user contexts/signal frames and alter saved thread state to enter a trampoline. sigreturn() validates and restores state. The architecture files were surveyed here; their complete frame-validation rules require a dedicated security/ABI review.
Mach exceptions become signals through a real IPC boundary
exception_triage_thread() attempts thread, task, then host exception handlers. A debugger or other registered handler can therefore handle an exception before BSD signal conversion. ux_handler_setup() registers the Unix exception handler at the host level for its selected exception mask. Its catch_mach_exception_raise() converts the supplied thread port into a referenced thread and calls handle_ux_exception().
The BSD bridge first asks machine_exception() for an architecture-specific translation, then applies generic mappings: invalid-address EXC_BAD_ACCESS becomes SIGSEGV, other bad-access cases generally become SIGBUS, bad instructions become SIGILL, and breakpoints become SIGTRAP. There is additional stack-overflow handling to select SIGSEGV and account for alternate-stack readiness. The bridge stores exception metadata in the uthread and calls threadsignal(), which sets pending state and requests a BSD AST.
This path connects hardware faults, Mach exception IPC, debugger interception, and POSIX signals. It does not mean every Mach exception is synonymous with a signal, or that a successful exception handler must terminate the process.