This is an archive of the discontinued LLVM Phabricator instance.

Change how debugserver clears auth bits from pc/fp/sp/lr with thread_get_state on Darwin
ClosedPublic

Authored by jasonmolenda on Oct 24 2022, 10:02 AM.

Details

Summary

When debugserver thread_get_state/thread_set_state's registers from an inferior, if the inferior is arm64e, debugserver must also be built arm64e, and debugserver passes the values through a series of macros provided by the kernel to authorize & clear auth bits off of the values that thread_get_state provides. When the inferior process has crashed -- jumping through an improperly signed function pointer, or jumped to invalid memory, the pc value will fail to authenticate in these kernel macros. On M2 era Mac hardware, this auth failure results in debugserver crashing.

We don't need to authenticate sp/pc/fp/lr, we only need to clear the auth bits from the address values. This patch replaces the kernel macro accesses after thread_get_state to do that. The macros like __darwin_arm_thread_state64_get_pc() are gated on __has_feature(ptrauth_calls) && defined(__LP64__), and in the case where we have ptrauth_calls, the register context structure in <mach/arm/_structs.h> are void * instead of uint64_t, so I needed to add a reinterpret cast of those values before clearing them.

It would probably be better to move my checks of __has_feature(ptrauth_calls) && defined(__LP64__) into this clear_pac_bits() function and call it unconditionally, instead of testing at all of the caller sites. (these two tests are distinguishing between arm64_32 v. arm64 v. arm64e)

In the case of thread_set_state, we will still use the kernel provided macros -- in this case, we are passing unsigned addresses and the signing will never fail.

With this patch, we still trigger the warning that the program has halted because of a PAC auth failure and show the most relevant pc value to explain it; no change in behavior there.

Diff Detail

Event Timeline

jasonmolenda created this revision.Oct 24 2022, 10:02 AM
Herald added a project: Restricted Project. · View Herald TranscriptOct 24 2022, 10:02 AM
jasonmolenda requested review of this revision.Oct 24 2022, 10:02 AM
JDevlieghere added inline comments.Oct 24 2022, 10:28 AM
lldb/tools/debugserver/source/DNB.cpp
1830–1837

A more C++ and thread safe way of doing this is:

static std::once_flag g_once_flag;
std::call_once(g_once_flag, [&](){
	size_t len = sizeof(uint32_t);
	if (::sysctlbyname("machdep.virtual_address_size", &g_addressing_bits, &len,
	                   NULL, 0) != 0) {
	  g_addressing_bits = 0;
	}
}
1840–1843
return g_addressing_bits > 0
lldb/tools/debugserver/source/MacOSX/arm64/DNBArchImplARM64.cpp
98–100

Doesn't DNBGetAddressingBits already cover the case of addressing_bits being 0? Why not have DNBGetAddressingBits return the addressing bits and do the check here?

Update patch to address Jonas' comments -- using a std::call_once to initialize the number of addressing bits, and making the return clearer.

This revision is now accepted and ready to land.Oct 25 2022, 8:25 AM
This revision was automatically updated to reflect the committed changes.