CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
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-"],
|
|
)
|