Handles are kernel constructs that allows user-mode programs to reference a kernel object. A handle can be thought as a session or connection to a particular kernel object.
It is often the case that multiple processes concurrently access the same object via different handles. However, a single handle can only be either bound to a single process or be bound to kernel.
When it is bound to kernel we say it's ‘in-transit’.
In user-mode a handle is simply a specific number returned by some syscall. Only handles that are not in-transit are visible to user-mode.
The integer that represents a handle is only meaningful for that process. The same number in another process might not map to any handle or it might map to a handle pointing to a completely different kernel object.
The integer value for a handle is any 32-bit number except the value corresponding to ZX_HANDLE_INVALID.
For kernel-mode, a handle is a C++ object that contains three logical fields:
The ‘rights’ specify what operations on the kernel object are allowed. It is possible for a single process to have two different handles to the same kernel object with different rights.
There are many syscalls that create a new kernel object and which return a handle to it. To name a few:
These calls create both the kernel object and the first handle pointing to it. The handle is bound to the process that issued the syscall and the rights are the default rights for that type of kernel object.
There is only one syscall that can make a copy of a handle, which points to the same kernel object and is bound to the same process that issued the syscall:
There is one syscall that creates an equivalent handle (possibly with fewer rights), invalidating the original handle:
There is one syscall that just destroys a handle:
There are two syscalls that takes a handle bound to calling process and binds it into kernel (puts the handle in-transit):
There are three syscalls that takes an in-transit handle and binds it to the calling process:
The channel and socket syscalls above are used to transfer a handle from one process to another. For example it is possible to connect two processes with a channel. To transfer a handle the source process calls
zx_channel_call and then the destination process calls
zx_channel_read on the same channel.
Finally, there is a single syscall that gives a new process its bootstrapping handle, that is, the handle that it can use to request other handles:
The bootstrapping handle can be of any transferable kernel object but the most reasonable case is that it points to one end of a channel so this initial channel can be used to send further handles into the new process.
If a handle is valid, the kernel object it points to is guaranteed to be valid. This is ensured because kernel objects are ref-counted and each handle holds a reference to its kernel object.
The opposite does not hold. When a handle is destroyed it does not mean its object is destroyed. There could be other handles pointing to the object or the kernel itself could be holding a reference to the kernel object. An example of this is a handle to a thread; the fact that the last handle to a thread is closed it does not mean that the thread has been terminated.
When the last reference to a kernel object is released, the kernel object is either destroyed or the kernel marks the object for garbage collection; the object will be destroyed at a later time when the current set of pending operations on it are completed.
It is an error to pass to any syscall except for
zx_object_get_info the following values:
The kernel is free to re-use the integer values of closed handles for newly created objects. Therefore, it is important to make sure that proper handle hygiene is observed:
Detecting invalid handle usage can be automated by using the ZX_POL_BAD_HANDLE Job policy with ZX_POL_ACTION_EXCEPTION to generate an exception when a process under such job object attempts any of the of the mentioned invalid cases.