| # Exception Handling | |
| ## Introduction | |
| When a thread encounters a fault condition, for example a segfault, execution | |
| is paused and the thread enters exception handling. Handlers which have | |
| registered to receive these exceptions are notified and given a chance to | |
| inspect or correct the condition. | |
| This functionality is commonly used by debuggers or crash loggers, which want | |
| to have a chance to interact with threads before they would otherwise crash. | |
| For applications that just want to track task lifecycles without needing to | |
| intercept crashes, [signals](signals.md) may be a better choice. | |
| ## The Basics | |
| Exceptions are handled from userspace by creating an exception channel on a | |
| task (thread, process, or job) with the [`zx_task_create_exception_channel()`] | |
| system call. The created handle is a standard Zircon | |
| [channel](/docs/reference/kernel_objects/channel.md), but is created read-only so can only be used | |
| for receiving exception messages. | |
| When an exception occurs, the thread is paused and a message containing a | |
| `zx_exception_info_t` and an exception handle is sent to the channel. The | |
| lifetime of the exception is bound to the lifetime of this exception handle, so | |
| when the receiver is done processing, closing this exception handle will resume | |
| the exception. This exception handle is non-copyable, meaning that at any given | |
| time, there is only one handler for this exception. | |
| By default, closing an exception handle will keep the thread paused and send | |
| the exception to the next handler. If the receiver has corrected the exception | |
| and wants the thread to resume execution instead, it can change the exception | |
| state to `ZX_EXCEPTION_STATE_HANDLED` via [`zx_object_set_property()`] before | |
| closing. | |
| ## Exception Handles | |
| Exception handles behave similarly to suspend tokens by keeping the thread | |
| paused until they are closed. Additionally, exception handles have functions | |
| to help receivers process the exception: | |
| * [`zx_object_set_property()`] with `ZX_PROP_EXCEPTION_STATE` to set behavior | |
| on handle close | |
| * [`zx_exception_get_thread()`] to get a handle to the exception thread | |
| * [`zx_exception_get_process()`] to get a handle to the exception process | |
| (process or job exception channels only) | |
| Task handles retrieved from exceptions will have the same rights as the task | |
| originally passed into [`zx_task_create_exception_channel()`]. | |
| ### Example | |
| This simple example creates an exception channel and loops reading exceptions | |
| until the task closes. | |
| ```cpp | |
| void ExceptionHandlerLoop(zx_handle_t task) { | |
| // Create the exception channel. | |
| uint32_t options = 0; | |
| zx_handle_t channel; | |
| zx_status_t status = zx_task_create_exception_channel(task, options, | |
| &channel); | |
| // ... check status ... | |
| while (true) { | |
| // Wait until we get ZX_CHANNEL_READABLE (exception) or | |
| // ZX_CHANNEL_PEER_CLOSED (task terminated). | |
| zx_signals_t signals = 0; | |
| status = zx_object_wait_one(channel, | |
| ZX_CHANNEL_READABLE | ZX_CHANNEL_PEER_CLOSED, | |
| ZX_TIME_INFINITE, &signals); | |
| // ... check status ... | |
| if (signals & ZX_CHANNEL_READABLE) { | |
| // Read the exception info and handle from the channel. | |
| zx_exception_info_t info; | |
| zx_handle_t exception; | |
| status = zx_channel_read(channel, 0, &info, &exception, sizeof(info), 1, | |
| nullptr, nullptr); | |
| // ... check status ... | |
| // Send the exception out to some other function for processing, which | |
| // returns true if the exception has been handled and we can resume the | |
| // thread, or false to pass the exception to the next handler. | |
| bool handled = process_exception(info, exception); | |
| if (handled) { | |
| uint32_t state = ZX_EXCEPTION_STATE_HANDLED; | |
| status = zx_object_set_property(exception, ZX_PROP_EXCEPTION_STATE, | |
| &state, sizeof(state)); | |
| // ... check status ... | |
| } | |
| // Close the exception to finish handling. | |
| zx_handle_close(exception); | |
| } else { | |
| // We got ZX_CHANNEL_PEER_CLOSED, the task has terminated. | |
| zx_handle_close(channel); | |
| return; | |
| } | |
| } | |
| } | |
| ``` | |
| ## Exception Types | |
| At a high level there are two types of exceptions: architectural and synthetic. | |
| Architectural exceptions are things like a segfault (e.g., dereferencing the | |
| NULL pointer) or executing an undefined instruction. Synthetic exceptions are | |
| things like thread start/stop notifications or | |
| [policy violations](/docs/reference/syscalls/job_set_policy.md). | |
| Architectural and policy exceptions are considered fatal, and will cause the | |
| process to be killed if they are unhandled. Debugger-only exceptions - thread | |
| start/stop and process start - are informational and will continue execution | |
| normally even if the thread isn't explicitly resumed. These exceptions are | |
| meant to give a debugger a chance to react to these lifetime events correctly, | |
| as the corresponding thread will be paused until the exception is resumed. | |
| Exception types are defined in | |
| [`<zircon/syscalls/exception.h>`](/zircon/system/public/zircon/syscalls/exception.h). | |
| ## Exception Channel Types | |
| Exception channels have different characteristics depending on the task type and | |
| whether the `ZX_EXCEPTION_CHANNEL_DEBUGGER` flag is passed to | |
| [`zx_task_create_exception_channel()`]. The table below summarizes the | |
| differences between the various channel types: | |
| Channel Type | `get_thread` | `get_process` | Architectural & Policy Exceptions | Thread Start/Stop Exceptions | Process Start Exception | |
| ------------- | :----------: | :-----------: | :-------------------------------: | :--------------------------: | :---------------------: | |
| Thread | X | | X | | | |
| Process | X | X | X | | | |
| Process Debug | X | X | X | X | | |
| Job | X | X | X | | | |
| Job Debug | X | X | | | X | |
| The channel type also determines the order in which exception channels will be | |
| given the chance to handle an exception: | |
| 1. process debug | |
| 2. thread | |
| 3. process | |
| 4. process debug (optionally, if the exception is [`'second-chance'`](#process-debugger-first-and-possibly-again-later)) | |
| 5. job (parent job -> grandparent job -> etc) | |
| If there are no remaining exception channels to try, the kernel terminates the | |
| process as if [`zx_task_kill()`] was called. The return code of a process | |
| terminated by an exception is `ZX_TASK_RETCODE_EXCEPTION_KILL`, and can be | |
| obtained with [`zx_object_get_info()`] using `ZX_INFO_PROCESS`. | |
| Each task only supports a single exception channel per type, so for example | |
| given a process with a debug exception channel attached, trying to create | |
| a second debug exception channel will fail, but creating a non-debug channel | |
| will succeed. | |
| ### `ZX_EXCP_PROCESS_STARTING` and Job Debugger Channels | |
| The `ZX_EXCP_PROCESS_STARTING` behaves differently than other exceptions. | |
| It is only sent to job debugger exception channels, and is only sent to the | |
| first found handler, essentially assuming `ZX_EXCEPTION_STATE_HANDLED` | |
| regardless of actual handler behavior. This is also the only exception that | |
| job debugger channels receive, making them a special-case handler for just | |
| detecting new processes. | |
| ### Process Debugger First... and Possibly Again Later | |
| In Zircon the process debugger exception channel is tried first. This is useful | |
| for at least a few reasons: | |
| - Allows "fix and continue" debugging, e.g. if a thread gets a segfault, | |
| the debugger user can fix the segfault and resume the thread without any | |
| non-debugger channels seeing the exception. | |
| - Ensures debugger breakpoints get sent directly to the debugger without | |
| other handlers having to explicitly pass them along. | |
| If an exception has ZX_EXCEPTION_STRATEGY_SECOND_CHANCE set and remains | |
| unhandled after the process exception channel is tried, the process debugger | |
| exception channel will be given a second chance. The utility of this lies in | |
| the case in which a process listens to its own exceptions and uses that | |
| information for correct functioning; in this case, it serves to have debugger | |
| inspect in the event of a failed correction. | |
| ## Interaction with Task Suspension | |
| Exceptions and thread suspensions are treated separately. | |
| In other words, a thread can be both in an exception and be suspended. | |
| This can happen if the thread is suspended while waiting for a response | |
| from an exception handler. The thread stays paused until it is resumed | |
| for both the exception and the suspension: | |
| ```cpp | |
| zx_handle_close(exception); | |
| zx_handle_close(suspend_token); | |
| ``` | |
| The order does not matter. | |
| ## Interaction with Task Kill | |
| [`zx_task_kill()`] stops any exception handling on the task. If it is called on | |
| a thread (or its parent process/jobs) while the thread is in an exception: | |
| - the thread will stop waiting for the current exception handler | |
| - no further exception handlers will receive the exception | |
| - [`zx_exception_get_thread()`] and [`zx_exception_get_process()`] on the | |
| outstanding exception handle will continue to provide valid task handles | |
| - [`zx_object_set_property()`] to set the exception's state will still return | |
| `ZX_OK`, though the state won't have any effect since the thread is no longer | |
| blocking on the handler | |
| Additionally, a killed thread will still send a `ZX_EXCP_THREAD_EXITING` | |
| exception (if a process debug handler is registered), but as above will not | |
| wait for a response from the handler. | |
| Although [`zx_task_kill()`] is generally asynchronous, meaning the thread may | |
| not finish terminating by the time the syscall returns, it does synchronously | |
| stop exception handling such that once it returns, closing an exception handle | |
| will not resume the thread or pass the exception to another handler. | |
| ## Signals | |
| [Signals](signals.md) are the core Zircon mechanism for observing state changes | |
| on kernel objects (a channel becoming readable, a process terminating, an event | |
| becoming signaled, etc). | |
| Unlike exceptions, signals do not require a response from an exception handler. | |
| On the other hand signals are sent to whomever is waiting on the thread's | |
| handle, instead of being sent to the exception channel that could be bound to | |
| the thread's process. | |
| A common pattern in Zircon is to have a message loop that waits for signals on | |
| one or more objects and handles them as they come in. To incorporate exception | |
| handling into this pattern, use [`zx_object_wait_async()`] to wait for | |
| `ZX_CHANNEL_READABLE` (and optionally `ZX_CHANNEL_PEER_CLOSED`) on the | |
| exception channel: | |
| ```cpp | |
| zx_handle_t port; | |
| zx_status_t status = zx_port_create(0, &port); | |
| // ... check status ... | |
| // Start waiting on relevant signals on the exception channel. | |
| status = zx_object_wait_async(exception_channel, port, kMyExceptionKey, | |
| ZX_CHANNEL_READABLE | ZX_CHANNEL_PEER_CLOSED, 0); | |
| // ... check status ... | |
| // ... add other objects to |port| with wait_async() ... | |
| while (1) { | |
| zx_port_packet_t packet; | |
| status = zx_port_wait(port, ZX_TIME_INFINITE, &packet); | |
| // ... check status ... | |
| if (packet.key == kMyExceptionKey) { | |
| if (packet.signal.observed & ZX_CHANNEL_READABLE) { | |
| // ... extract exception from |exception_channel| and process it ... | |
| // wait_async() is one-shot so we need to reload it to continue | |
| // receiving signals. | |
| status = zx_object_wait_async( | |
| exception_channel, port, kMyExceptionKey, | |
| ZX_CHANNEL_READABLE | ZX_CHANNEL_PEER_CLOSED, 0); | |
| // ... check status ... | |
| } else { | |
| // Got ZX_CHANNEL_PEER_CLOSED, task has terminated. | |
| zx_handle_close(exception_channel); | |
| } | |
| } else { | |
| // ... handle other objects added to |port| ... | |
| } | |
| } | |
| ``` | |
| Note: There is both an exception and a signal for thread termination. The | |
| `ZX_EXCP_THREAD_EXITING` exception is sent first. When the thread is finally | |
| terminated the `ZX_THREAD_TERMINATED` signal is set. | |
| ## Comparison with Posix (and Linux) | |
| This table shows equivalent terms, types, and function calls between | |
| Zircon and Posix/Linux for exceptions and the kinds of things exception | |
| handlers generally do. | |
| Zircon | Posix/Linux | |
| ------ | ----------- | |
| Exception/Signal | Signal | |
| ZX_EXCP_* | SIG* | |
| zx_task_create_exception_channel() | ptrace(ATTACH,DETACH) | |
| zx_task_suspend() | kill(SIGSTOP),ptrace(KILL(SIGSTOP)) | |
| zx_handle_close(suspend_token) | kill(SIGCONT),ptrace(CONT) | |
| zx_handle_close(exception) | kill(SIGCONT),ptrace(CONT) | |
| zx_task_kill() | kill(SIGKILL) | |
| N/A | kill(everything_else) | |
| TBD | signal()/sigaction() | |
| zx_port_wait() | wait*() | |
| various | W*() macros from sys/wait.h | |
| zx_exception_info_t | siginfo_t | |
| zx_exception_context_t | siginfo_t | |
| zx_thread_read_state() | ptrace(GETREGS,GETREGSET) | |
| zx_thread_write_state() | ptrace(SETREGS,SETREGSET) | |
| zx_process_read_memory() | ptrace(PEEKTEXT) | |
| zx_process_write_memory() | ptrace(POKETEXT) | |
| Zircon does not have asynchronous signals like `SIGINT`, `SIGQUIT`, `SIGTERM`, | |
| `SIGUSR1`, `SIGUSR2`, and so on. | |
| Another significant difference from Posix is that in Zircon a thread cannot | |
| handle its own exceptions, since Zircon exception handling is a synchronous | |
| operation driven by userspace rather than an asynchronous callback invoked by | |
| the kernel. | |
| ## Examples | |
| Zircon code that uses exceptions can be viewed for further examples, including: | |
| - `system/core/svchost/crashsvc`: system-level crash handler | |
| - `system/utest/exception`: exception unit tests | |
| - `system/utest/debugger`: debugger-related functionality unit tests | |
| ## See Also | |
| - [`zx_task_create_exception_channel()`] | |
| - [`zx_exception_get_thread()`] | |
| - [`zx_exception_get_process()`] | |
| - [`zx_object_set_property()`] | |
| - [`zx_port_wait()`] | |
| [`zx_exception_get_process()`]: /docs/reference/syscalls/exception_get_process.md | |
| [`zx_exception_get_thread()`]: /docs/reference/syscalls/exception_get_thread.md | |
| [`zx_object_get_info()`]: /docs/reference/syscalls/object_get_info.md | |
| [`zx_object_set_property()`]: /docs/reference/syscalls/object_set_property.md | |
| [`zx_object_wait_async()`]: /docs/reference/syscalls/object_wait_async.md | |
| [`zx_port_wait()`]: /docs/reference/syscalls/port_wait.md | |
| [`zx_task_create_exception_channel()`]: /docs/reference/syscalls/task_create_exception_channel.md | |
| [`zx_task_kill()`]: /docs/reference/syscalls/task_kill.md |