Skip to content

Instantly share code, notes, and snippets.

@tom-seddon
Last active January 14, 2024 16:09
Show Gist options
  • Save tom-seddon/87ff7c4ab5157d87a0f229b07cf6600f to your computer and use it in GitHub Desktop.
Save tom-seddon/87ff7c4ab5157d87a0f229b07cf6600f to your computer and use it in GitHub Desktop.
Random Mach notes

Random Mach notes

mach_thread_self increments ref count

Each call to mach_thread_self adds another MACH_PORT_RIGHT_SEND refcount. For each call to mach_thread_self, you need to call mach_port_deallocate on the result.

(This does not apply to mach_task_self.)

Port sets must be destroyed

mach_port_deallocate is no good for port sets. Use mach_port_destroy.

mach_port_deallocate is for send rights only

mach_port_deallocate will return KERN_INVALID_RIGHT for a receive right.

For receive rights, use mach_port_destroy.

(If you’ve got a send/receive port right, due perhaps to having called mach_port_insert_right, you need to call both…)

Message size

mach_msg_header_t::msgh_size includes the header size.

Processing MACH_NOTIFY_DEAD_NAME

See mach_dead_name_notification_t in /usr/include/mach/notify.h.

Check header’s msgh_id is MACH_NOTIFY_DEAD_NAME. If so, cast message buffer to mach_dead_name_notification_t.

Watch out for port refs in messages

If you have a message with a port name in it, you now have a reference to it. It must be unreferenced with mach_port_deallocate to avoid port name leaks.

This applies to mach_dead_name_notification_t!

NDR

There’s a global, NDR_record, holding the native settings for the current CPU. So compare message’s NDR record against this and swap stuff if necessary.

kqueues can’t watch port rights

The man page is fairly unambiguous:

Takes the name of a mach port, or port set, in ident and waits until a message is received on the port or port set.

But in fact, a kqueue may only watch a port set. You get ENOTSUP if trying to watch a port.

There’s even a test for this exact behaviour: http://www.opensource.apple.com/source/xnu/xnu-1456.1.26/tools/tests/xnu_quick_test/kqueue_tests.c

P.S. See also https://stackoverflow.com/questions/33115562 - can’t remember any of the details though.

Mach Exception Handlers

https://www.mikeash.com/pyblog/friday-qa-2013-01-11-mach-exception-handlers.html

https://llvm.org/bugs/show_bug.cgi?id=22868

Handling a dead process name

Once the dead name notification comes in, the process state - exit status or killing signal - can still be queried using wait4. (I’m pretty sure this is mandatory, to ensure the process gets reaped.)

Thread Stuff

Declaration of struct thread. Note:

uint64_t thread_id;	/*system wide unique thread-id*/

Handling SIGKILL when ptrace-ing

ptrace will trap SIGKILL.

When running a process under ptrace, sending that process SIGKILL will stop the process rather than kill it. Detect this using waitpid - WIFSTOPPED(status) will be true, and WSTOPSIG(status) will be SIGKILL.

Suggestion: pass the signal on to the process using PT_CONTINUE!

Killing a ptrace’d process

Use PT_KILL to kill threads that are stopped due to an exception. They will die without further ado. (If you are watching for dead names you will get a notification that their names have died.)

PT_KILL won’t kill other threads, so send SIGKILL to the process as well, to kill the other threads. Eventually, the process will end up stopped again in the SIGNALED state, with SIGKILL as the signal. Pass it through and the process will go away.

PT_SIGEXC

PT_SIGEXC is a ptrace code for use by the tracee after it’s called PT_TRACE_ME. It directs the OS to send it signals in the form of Mach exceptions, allowing the tracer to catch them via the exception port rather than having to bugger about with SIGCHLD.

For such exceptions, the message’s exception field will be EXC_SOFTWARE, code[0] will be EXC_SOFT_SIGNAL, and code[1] will be the signal number.

Start by using task_suspend to suspend the task.

Don’t do anything further until you’re ready for the process to continue.

Once you’re ready, use PT_THUPDATE. Pass your Mach thread port as the addr argument to ptrace. For data, as PT_CONTINUE, pass the signal number to deliver, or 0 to discard it.

ptrace(PT_THUPDATE,pid,(caddr_t)(uintptr_t)thread_port,exception->code[1])

Then, pass the exception on in the usual fashion, by forming an appropriate mig_error_reply_t (see the generated mig code) - this might best be done when the exception is caught, and saved for later. The reply’s RetCode should be KERN_SUCCESS, indicating that the exception should be delivered.

TODO: figure out exactly what the results are when deviating from this!

64-bit exception types

Certain exceptions use the raise message code fields to store addresses. This is no good for 64-bit tracees, because the code fields are 32 bit.

To fix this, when registering an exception port, include the value MACH_EXCEPTION_CODES in the behaviour argument. Exceptions will then be delivered in an alternative format (64-bit code fields), with different codes (starting at 2405 rather than 2401).

The MIG stuff for this is in /usr/include/mach/mach_exc.defs. Unlike exc.defs, there’s no pregenerated header for it though.

(It looks as if this works for 32-bit tracees as well.)

vm_read_overwrite

Despite what the docs say, the data_count argument is ignored on input, and its size is in bytes. There’s no need to set it before the call. (Of course, this means the buffer must be large enough… Mach doesn’t check…)

vm_read and unmapped regions

If the read touches an unmapped region, the result is KERN_INVALID_ADDRESS. Intersect the area to be read with the list of VM regions (e.g., using vm_region_64), and read each intersecting area.

mach_vm_read or vm_read and the 4GByte limit

mach_vm_read and vm_read return the actual size of the mapped region as a mach_msg_type_number_t - a 32-bit value. These calls are therefore limited to mapping 4GBytes at a time.

You get KERN_INVALID_ARGUMENT if this is breached.

See =mach_vm_read=; =vm_read=.

TBC? - vm_read will split regions

When using vm_read to map part of a region, that region may be split. For example:

Old mapping:

0x00000001008f2000: R+W+X-                16384 e772fa9949c60812cd1c6aa8c8e1b2848ff4ef16

Then after calling vm_read to duplicate 4K starting at 0x00000001008f4000:

0x00000001008f2000: R+W+X-                 4096 1d61359d785b37e7a97f5491733f72cf321dd8b2
0x00000001008f3000: R+W+X-                 4096 1d61359d785b37e7a97f5491733f72cf321dd8b2
0x00000001008f4000: R+W+X-                 8192 f2a97cb03760772953a411e7928fd6ff461152d2

vm_region_64’s out parameter

vm_region_64 (and friends) have a mystery out parameter, “no longer used” according to the documentation:

vm_region_64(vm_map_t                 map,
             /* ... */
             mach_port_t             *object_name)           /* OUT */

It’s always set to IPC_NULL, which is 0.

@tom-seddon
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment