The features and locked registers hold the same bits, the latter is a lock for the former. Tested with core files and live processes. I thought about setting a non-zero lock register in the core file, however: * We can be pretty sure it's reading correctly because its between the 2 other GCS registers in the same core file note. * I can't make the test case modify lock bits because userspace can't clear them (without using ptrace) and we don't know what the libc has locked (probably all feature bits).
422 lines
16 KiB
Python
422 lines
16 KiB
Python
"""
|
|
Check that lldb features work when the AArch64 Guarded Control Stack (GCS)
|
|
extension is enabled.
|
|
"""
|
|
|
|
import lldb
|
|
from lldbsuite.test.decorators import *
|
|
from lldbsuite.test.lldbtest import *
|
|
from lldbsuite.test import lldbutil
|
|
|
|
|
|
class AArch64LinuxGCSTestCase(TestBase):
|
|
NO_DEBUG_INFO_TESTCASE = True
|
|
|
|
@skipUnlessArch("aarch64")
|
|
@skipUnlessPlatform(["linux"])
|
|
def test_gcs_region(self):
|
|
if not self.isAArch64GCS():
|
|
self.skipTest("Target must support GCS.")
|
|
|
|
# This test assumes that we have /proc/<PID>/smaps files
|
|
# that include "VmFlags:" lines.
|
|
# AArch64 kernel config defaults to enabling smaps with
|
|
# PROC_PAGE_MONITOR and "VmFlags" was added in kernel 3.8,
|
|
# before GCS was supported at all.
|
|
|
|
self.build()
|
|
self.runCmd("file " + self.getBuildArtifact("a.out"), CURRENT_EXECUTABLE_SET)
|
|
|
|
lldbutil.run_break_set_by_file_and_line(
|
|
self,
|
|
"main.c",
|
|
line_number("main.c", "// Set break point at this line."),
|
|
num_expected_locations=1,
|
|
)
|
|
|
|
self.runCmd("run", RUN_SUCCEEDED)
|
|
|
|
if self.process().GetState() == lldb.eStateExited:
|
|
self.fail("Test program failed to run.")
|
|
|
|
self.expect(
|
|
"thread list",
|
|
STOPPED_DUE_TO_BREAKPOINT,
|
|
substrs=["stopped", "stop reason = breakpoint"],
|
|
)
|
|
|
|
# By now either the program or the system C library enabled GCS and there
|
|
# should be one region marked for use by it (we cannot predict exactly
|
|
# where it will be).
|
|
self.runCmd("memory region --all")
|
|
found_ss = False
|
|
for line in self.res.GetOutput().splitlines():
|
|
if line.strip() == "shadow stack: yes":
|
|
if found_ss:
|
|
self.fail("Found more than one shadow stack region.")
|
|
found_ss = True
|
|
|
|
self.assertTrue(found_ss, "Failed to find a shadow stack region.")
|
|
|
|
# Note that we must let the debugee get killed here as it cannot exit
|
|
# cleanly if GCS was manually enabled.
|
|
|
|
@skipUnlessArch("aarch64")
|
|
@skipUnlessPlatform(["linux"])
|
|
def test_gcs_fault(self):
|
|
if not self.isAArch64GCS():
|
|
self.skipTest("Target must support GCS.")
|
|
|
|
self.build()
|
|
self.runCmd("file " + self.getBuildArtifact("a.out"), CURRENT_EXECUTABLE_SET)
|
|
self.runCmd("run", RUN_SUCCEEDED)
|
|
|
|
if self.process().GetState() == lldb.eStateExited:
|
|
self.fail("Test program failed to run.")
|
|
|
|
self.expect(
|
|
"thread list",
|
|
"Expected stopped by SIGSEGV.",
|
|
substrs=[
|
|
"stopped",
|
|
"stop reason = signal SIGSEGV: control protection fault",
|
|
],
|
|
)
|
|
|
|
# This helper reads all the GCS registers and optionally compares them
|
|
# against a previous state, then returns the current register values.
|
|
def check_gcs_registers(
|
|
self,
|
|
expected_gcs_features_enabled=None,
|
|
expected_gcs_features_locked=None,
|
|
expected_gcspr_el0=None,
|
|
):
|
|
thread = self.dbg.GetSelectedTarget().process.GetThreadAtIndex(0)
|
|
registerSets = thread.GetFrameAtIndex(0).GetRegisters()
|
|
gcs_registers = registerSets.GetFirstValueByName(
|
|
r"Guarded Control Stack Registers"
|
|
)
|
|
|
|
gcs_features_enabled = gcs_registers.GetChildMemberWithName(
|
|
"gcs_features_enabled"
|
|
).GetValueAsUnsigned()
|
|
if expected_gcs_features_enabled is not None:
|
|
self.assertEqual(expected_gcs_features_enabled, gcs_features_enabled)
|
|
|
|
gcs_features_locked = gcs_registers.GetChildMemberWithName(
|
|
"gcs_features_locked"
|
|
).GetValueAsUnsigned()
|
|
if expected_gcs_features_locked is not None:
|
|
self.assertEqual(expected_gcs_features_locked, gcs_features_locked)
|
|
|
|
gcspr_el0 = gcs_registers.GetChildMemberWithName(
|
|
"gcspr_el0"
|
|
).GetValueAsUnsigned()
|
|
if expected_gcspr_el0 is not None:
|
|
self.assertEqual(expected_gcspr_el0, gcspr_el0)
|
|
|
|
return gcs_features_enabled, gcs_features_locked, gcspr_el0
|
|
|
|
@skipUnlessArch("aarch64")
|
|
@skipUnlessPlatform(["linux"])
|
|
def test_gcs_registers(self):
|
|
if not self.isAArch64GCS():
|
|
self.skipTest("Target must support GCS.")
|
|
|
|
self.build()
|
|
self.runCmd("file " + self.getBuildArtifact("a.out"), CURRENT_EXECUTABLE_SET)
|
|
|
|
self.runCmd("b test_func")
|
|
self.runCmd("b test_func2")
|
|
self.runCmd("run", RUN_SUCCEEDED)
|
|
|
|
if self.process().GetState() == lldb.eStateExited:
|
|
self.fail("Test program failed to run.")
|
|
|
|
self.expect(
|
|
"thread list",
|
|
STOPPED_DUE_TO_BREAKPOINT,
|
|
substrs=["stopped", "stop reason = breakpoint"],
|
|
)
|
|
|
|
self.expect("register read --all", substrs=["Guarded Control Stack Registers:"])
|
|
|
|
enabled, locked, spr_el0 = self.check_gcs_registers()
|
|
|
|
# Features enabled should have at least the enable bit set, it could have
|
|
# others depending on what the C library did, but we can't rely on always
|
|
# having them.
|
|
self.assertTrue(enabled & 1, "Expected GCS enable bit to be set.")
|
|
|
|
# Features locked we cannot predict, we will just assert that it remains
|
|
# the same as we continue.
|
|
|
|
# spr_el0 will point to some memory region that is a shadow stack region.
|
|
self.expect(f"memory region {spr_el0}", substrs=["shadow stack: yes"])
|
|
|
|
# Continue into test_func2, where the GCS pointer should have been
|
|
# decremented, and the other registers remain the same.
|
|
self.runCmd("continue")
|
|
|
|
self.expect(
|
|
"thread list",
|
|
STOPPED_DUE_TO_BREAKPOINT,
|
|
substrs=["stopped", "stop reason = breakpoint"],
|
|
)
|
|
|
|
_, _, spr_el0 = self.check_gcs_registers(enabled, locked, spr_el0 - 8)
|
|
|
|
# Any combination of GCS feature lock bits might have been set by the C
|
|
# library, and could be set to 0 or 1. To check that we can modify them,
|
|
# invert one of those bits then write it back to the lock register.
|
|
# The stack pushing feature is bit 2 of that register.
|
|
STACK_PUSH = 2
|
|
# Get the original value of the stack push lock bit.
|
|
stack_push = bool((locked >> STACK_PUSH) & 1)
|
|
# Invert the value and put it back into the set of lock bits.
|
|
new_locked = (locked & ~(1 << STACK_PUSH)) | (int(not stack_push) << STACK_PUSH)
|
|
# Write the new lock bits, which are the same as before, only with stack
|
|
# push locked (if it was previously unlocked), or unlocked (if it was
|
|
# previously locked).
|
|
self.runCmd(f"register write gcs_features_locked 0x{new_locked:x}")
|
|
# We should be able to read back this new set of lock bits.
|
|
self.expect(
|
|
f"register read gcs_features_locked",
|
|
substrs=[f"gcs_features_locked = 0x{new_locked:016x}"],
|
|
)
|
|
|
|
# We could prove the write made it to hardware by trying to prctl() to
|
|
# enable or disable the stack push feature here, but because the libc
|
|
# may or may not have locked it, it's tricky to coordinate this. Given
|
|
# that we know the other registers can be written and their values are
|
|
# seen by the process, we can assume this is too.
|
|
|
|
# Restore the original lock bits, as the libc may rely on being able
|
|
# to use certain features during program execution.
|
|
self.runCmd(f"register write gcs_features_locked 0x{locked:x}")
|
|
|
|
# Modify the guarded control stack pointer to cause a fault.
|
|
spr_el0 += 8
|
|
self.runCmd(f"register write gcspr_el0 {spr_el0}")
|
|
self.expect(
|
|
"register read gcspr_el0", substrs=[f"gcspr_el0 = 0x{spr_el0:016x}"]
|
|
)
|
|
|
|
# If we wrote it back correctly, we will now fault. Don't pass this signal
|
|
# to the application, as we will continue past it later.
|
|
self.runCmd("process handle SIGSEGV --pass false")
|
|
self.runCmd("continue")
|
|
|
|
self.expect(
|
|
"thread list",
|
|
"Expected stopped by SIGSEGV.",
|
|
substrs=[
|
|
"stopped",
|
|
"stop reason = signal SIGSEGV: control protection fault",
|
|
],
|
|
)
|
|
|
|
# Now to prove we can write gcs_features_enabled, disable GCS and continue
|
|
# past the fault we caused. Note that although the libc likely locked the
|
|
# ability to disable GCS, ptrace bypasses the lock bits.
|
|
enabled &= ~1
|
|
self.runCmd(f"register write gcs_features_enabled {enabled}")
|
|
self.expect(
|
|
"register read gcs_features_enabled",
|
|
substrs=[
|
|
f"gcs_features_enabled = 0x{enabled:016x}",
|
|
f"= (PUSH = {(enabled >> 2) & 1}, WRITE = {(enabled >> 1) & 1}, ENABLE = {enabled & 1})",
|
|
],
|
|
)
|
|
|
|
# With GCS disabled, the invalid guarded control stack pointer is not
|
|
# checked, so the program can finish normally.
|
|
self.runCmd("continue")
|
|
self.expect(
|
|
"process status",
|
|
substrs=[
|
|
"exited with status = 0",
|
|
],
|
|
)
|
|
|
|
@skipUnlessPlatform(["linux"])
|
|
def test_gcs_expression_simple(self):
|
|
if not self.isAArch64GCS():
|
|
self.skipTest("Target must support GCS.")
|
|
|
|
self.build()
|
|
self.runCmd("file " + self.getBuildArtifact("a.out"), CURRENT_EXECUTABLE_SET)
|
|
|
|
# Break before GCS has been enabled.
|
|
self.runCmd("b main")
|
|
# And after it has been enabled.
|
|
lldbutil.run_break_set_by_file_and_line(
|
|
self,
|
|
"main.c",
|
|
line_number("main.c", "// Set break point at this line."),
|
|
num_expected_locations=1,
|
|
)
|
|
|
|
self.runCmd("run", RUN_SUCCEEDED)
|
|
|
|
if self.process().GetState() == lldb.eStateExited:
|
|
self.fail("Test program failed to run.")
|
|
|
|
self.expect(
|
|
"thread list",
|
|
STOPPED_DUE_TO_BREAKPOINT,
|
|
substrs=["stopped", "stop reason = breakpoint"],
|
|
)
|
|
|
|
# GCS has not been enabled yet and the ABI plugin should know not to
|
|
# attempt pushing to the control stack.
|
|
before = self.check_gcs_registers()
|
|
expr_cmd = "p get_gcs_status()"
|
|
self.expect(expr_cmd, substrs=["(unsigned long) 0"])
|
|
self.check_gcs_registers(*before)
|
|
|
|
# Continue to when GCS has been enabled.
|
|
self.runCmd("continue")
|
|
self.expect(
|
|
"thread list",
|
|
STOPPED_DUE_TO_BREAKPOINT,
|
|
substrs=["stopped", "stop reason = breakpoint"],
|
|
)
|
|
|
|
# If we fail to setup the GCS entry, we should not leave any of the GCS registers
|
|
# changed. The last thing we do is write a new GCS entry to memory and
|
|
# to simulate the failure of that, temporarily point the GCS to the zero page.
|
|
#
|
|
# We use the value 8 here because LLDB will decrement it by 8 so it points to
|
|
# what we think will be an empty entry on the guarded control stack.
|
|
_, _, original_gcspr = self.check_gcs_registers()
|
|
self.runCmd("register write gcspr_el0 8")
|
|
before = self.check_gcs_registers()
|
|
self.expect(expr_cmd, error=True)
|
|
self.check_gcs_registers(*before)
|
|
# Point to the valid shadow stack region again.
|
|
self.runCmd(f"register write gcspr_el0 {original_gcspr}")
|
|
|
|
# This time we do need to push to the GCS and having done so, we can
|
|
# return from this expression without causing a fault.
|
|
before = self.check_gcs_registers()
|
|
self.expect(expr_cmd, substrs=["(unsigned long) 1"])
|
|
self.check_gcs_registers(*before)
|
|
|
|
@skipUnlessPlatform(["linux"])
|
|
def test_gcs_expression_disable_gcs(self):
|
|
if not self.isAArch64GCS():
|
|
self.skipTest("Target must support GCS.")
|
|
|
|
self.build()
|
|
self.runCmd("file " + self.getBuildArtifact("a.out"), CURRENT_EXECUTABLE_SET)
|
|
|
|
# Break after GCS is enabled.
|
|
lldbutil.run_break_set_by_file_and_line(
|
|
self,
|
|
"main.c",
|
|
line_number("main.c", "// Set break point at this line."),
|
|
num_expected_locations=1,
|
|
)
|
|
|
|
self.runCmd("run", RUN_SUCCEEDED)
|
|
|
|
if self.process().GetState() == lldb.eStateExited:
|
|
self.fail("Test program failed to run.")
|
|
|
|
self.expect(
|
|
"thread list",
|
|
STOPPED_DUE_TO_BREAKPOINT,
|
|
substrs=["stopped", "stop reason = breakpoint"],
|
|
)
|
|
|
|
# Unlock all features so the expression can enable them again.
|
|
self.runCmd("register write gcs_features_locked 0")
|
|
# Disable all features, but keep GCS itself enabled.
|
|
PR_SHADOW_STACK_ENABLE = 1
|
|
self.runCmd(f"register write gcs_features_enabled 0x{PR_SHADOW_STACK_ENABLE:x}")
|
|
|
|
enabled, locked, spr_el0 = self.check_gcs_registers()
|
|
# We restore everything apart GCS being enabled, as we are not allowed to
|
|
# go from disabled -> enabled via ptrace.
|
|
self.expect("p change_gcs_config(false)", substrs=["true"])
|
|
enabled &= ~1
|
|
self.check_gcs_registers(enabled, locked, spr_el0)
|
|
|
|
@skipUnlessPlatform(["linux"])
|
|
def test_gcs_expression_enable_gcs(self):
|
|
if not self.isAArch64GCS():
|
|
self.skipTest("Target must support GCS.")
|
|
|
|
self.build()
|
|
self.runCmd("file " + self.getBuildArtifact("a.out"), CURRENT_EXECUTABLE_SET)
|
|
|
|
# Break before GCS is enabled.
|
|
self.runCmd("b main")
|
|
|
|
self.runCmd("run", RUN_SUCCEEDED)
|
|
|
|
if self.process().GetState() == lldb.eStateExited:
|
|
self.fail("Test program failed to run.")
|
|
|
|
self.expect(
|
|
"thread list",
|
|
STOPPED_DUE_TO_BREAKPOINT,
|
|
substrs=["stopped", "stop reason = breakpoint"],
|
|
)
|
|
|
|
# Unlock all features so the expression can enable them again.
|
|
self.runCmd("register write gcs_features_locked 0")
|
|
# Disable all features. The program needs PR_SHADOW_STACK_PUSH, but it
|
|
# will enable that itself.
|
|
self.runCmd(f"register write gcs_features_enabled 0")
|
|
|
|
enabled, locked, spr_el0 = self.check_gcs_registers()
|
|
self.expect("p change_gcs_config(true)", substrs=["true"])
|
|
# Though we could disable GCS with ptrace, we choose not to to be
|
|
# consistent with the disabled -> enabled behaviour.
|
|
enabled |= 1
|
|
self.check_gcs_registers(enabled, locked, spr_el0)
|
|
|
|
@skipIfLLVMTargetMissing("AArch64")
|
|
def test_gcs_core_file(self):
|
|
# To re-generate the core file, build the test file and run it on a
|
|
# machine with GCS enabled. Note that because the kernel decides where
|
|
# the GCS is stored, the value of gcspr_el0 and which memory region it
|
|
# points to may change between runs.
|
|
|
|
self.runCmd("target create --core corefile")
|
|
|
|
self.expect(
|
|
"bt",
|
|
substrs=["stop reason = SIGSEGV: control protection fault"],
|
|
)
|
|
|
|
self.expect(
|
|
"register read --all",
|
|
substrs=[
|
|
"Guarded Control Stack Registers:",
|
|
"gcs_features_enabled = 0x0000000000000001",
|
|
"gcs_features_locked = 0x0000000000000000",
|
|
"gcspr_el0 = 0x0000ffffa83ffff0",
|
|
],
|
|
)
|
|
|
|
# Should get register fields for both. They have the same fields.
|
|
self.expect(
|
|
"register read gcs_features_enabled",
|
|
substrs=["= (PUSH = 0, WRITE = 0, ENABLE = 1)"],
|
|
)
|
|
self.expect(
|
|
"register read gcs_features_locked",
|
|
substrs=["= (PUSH = 0, WRITE = 0, ENABLE = 0)"],
|
|
)
|
|
|
|
# Core files do not include /proc/pid/smaps, so we cannot see the
|
|
# shadow stack "ss" flag. gcspr_el0 should at least point to some mapped
|
|
# region.
|
|
self.expect(
|
|
"memory region $gcspr_el0",
|
|
substrs=["[0x0000ffffa8000000-0x0000ffffa8400000) rw-"],
|
|
)
|