Skip to content

[lldb][AArch64] Fix expression evaluation with Guarded Control Stacks #123918

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions lldb/source/Plugins/ABI/AArch64/ABISysV_arm64.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,69 @@ ABISysV_arm64::CreateInstance(lldb::ProcessSP process_sp, const ArchSpec &arch)
return ABISP();
}

static Status PushToLinuxGuardedControlStack(addr_t return_addr,
RegisterContext *reg_ctx,
Thread &thread) {
Status err;

// If the Guarded Control Stack extension is present we may need to put the
// return address onto that stack.
const RegisterInfo *gcs_features_enabled_info =
reg_ctx->GetRegisterInfoByName("gcs_features_enabled");
if (!gcs_features_enabled_info)
return err;

uint64_t gcs_features_enabled = reg_ctx->ReadRegisterAsUnsigned(
gcs_features_enabled_info, LLDB_INVALID_ADDRESS);
if (gcs_features_enabled == LLDB_INVALID_ADDRESS)
return Status("Could not read GCS features enabled register.");

// Only attempt this if GCS is enabled. If it's not enabled then gcspr_el0
// may point to unmapped memory.
if ((gcs_features_enabled & 1) == 0)
return err;

const RegisterInfo *gcspr_el0_info =
reg_ctx->GetRegisterInfoByName("gcspr_el0");
if (!gcspr_el0_info)
return Status("Could not get register info for gcspr_el0.");

uint64_t gcspr_el0 =
reg_ctx->ReadRegisterAsUnsigned(gcspr_el0_info, LLDB_INVALID_ADDRESS);
if (gcspr_el0 == LLDB_INVALID_ADDRESS)
return Status("Could not read gcspr_el0.");

// A link register entry on the GCS is 8 bytes.
gcspr_el0 -= 8;
if (!reg_ctx->WriteRegisterFromUnsigned(gcspr_el0_info, gcspr_el0))
return Status(
"Attempted to decrement gcspr_el0, but could not write to it.");

Status error;
size_t wrote = thread.GetProcess()->WriteMemory(gcspr_el0, &return_addr,
sizeof(return_addr), error);
if ((wrote != sizeof(return_addr) || error.Fail())) {
// When PrepareTrivialCall fails, the register context is not restored,
// unlike when an expression fails to execute. This is arguably a bug,
// see https://github.com/llvm/llvm-project/issues/124269.
// For now we are handling this here specifically. We can assume this
// write will work as the one to decrement the register did.
reg_ctx->WriteRegisterFromUnsigned(gcspr_el0_info, gcspr_el0 + 8);
return Status("Failed to write new Guarded Control Stack entry.");
}

Log *log = GetLog(LLDBLog::Expressions);
LLDB_LOGF(log,
"Pushed return address 0x%" PRIx64 " to Guarded Control Stack. "
"gcspr_el0 was 0%" PRIx64 ", is now 0x%" PRIx64 ".",
return_addr, gcspr_el0 - 8, gcspr_el0);

// gcspr_el0 will be restored to the original value by lldb-server after
// the call has finished, which serves as the "pop".

return err;
}

bool ABISysV_arm64::PrepareTrivialCall(Thread &thread, addr_t sp,
addr_t func_addr, addr_t return_addr,
llvm::ArrayRef<addr_t> args) const {
Expand Down Expand Up @@ -87,6 +150,18 @@ bool ABISysV_arm64::PrepareTrivialCall(Thread &thread, addr_t sp,
if (args.size() > 8)
return false;

// Do this first, as it's got the most chance of failing (though still very
// low).
if (GetProcessSP()->GetTarget().GetArchitecture().GetTriple().isOSLinux()) {
Status err = PushToLinuxGuardedControlStack(return_addr, reg_ctx, thread);
// If we could not manage the GCS, the expression will certainly fail,
// and if we just carried on, that failure would be a lot more cryptic.
if (err.Fail()) {
LLDB_LOGF(log, "Failed to setup Guarded Call Stack: %s", err.AsCString());
return false;
}
}

for (size_t i = 0; i < args.size(); ++i) {
const RegisterInfo *reg_info = reg_ctx->GetRegisterInfo(
eRegisterKindGeneric, LLDB_REGNUM_GENERIC_ARG1 + i);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1063,9 +1063,27 @@ Status NativeRegisterContextLinux_arm64::WriteAllRegisterValues(
std::bind(&NativeRegisterContextLinux_arm64::WriteFPMR, this));
break;
case RegisterSetType::GCS:
// It is not permitted to enable GCS via ptrace. We can disable it, but
// to keep things simple we will not revert any change to the
// PR_SHADOW_STACK_ENABLE bit. Instead patch in the current enable bit
// into the registers we are about to restore.
m_gcs_is_valid = false;
error = ReadGCS();
if (error.Fail())
return error;

uint64_t enable_bit = m_gcs_regs.features_enabled & 1UL;
gcs_regs new_gcs_regs = *reinterpret_cast<const gcs_regs *>(src);
new_gcs_regs.features_enabled =
(new_gcs_regs.features_enabled & ~1UL) | enable_bit;

const uint8_t *new_gcs_src =
reinterpret_cast<const uint8_t *>(&new_gcs_regs);
error = RestoreRegisters(
GetGCSBuffer(), &src, GetGCSBufferSize(), m_gcs_is_valid,
GetGCSBuffer(), &new_gcs_src, GetGCSBufferSize(), m_gcs_is_valid,
std::bind(&NativeRegisterContextLinux_arm64::WriteGCS, this));
src += GetGCSBufferSize();

break;
}

Expand Down
211 changes: 175 additions & 36 deletions lldb/test/API/linux/aarch64/gcs/TestAArch64LinuxGCS.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
extension is enabled.
"""


import lldb
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
Expand Down Expand Up @@ -84,6 +83,40 @@ def test_gcs_fault(self):
],
)

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

# This helper reads all the GCS registers and optionally compares them
# against a previous state, then returns the current register values.
@skipUnlessArch("aarch64")
@skipUnlessPlatform(["linux"])
def test_gcs_registers(self):
Expand All @@ -108,40 +141,7 @@ def test_gcs_registers(self):

self.expect("register read --all", substrs=["Guarded Control Stack Registers:"])

# 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(
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

enabled, locked, spr_el0 = check_gcs_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
Expand All @@ -164,7 +164,7 @@ def check_gcs_registers(
substrs=["stopped", "stop reason = breakpoint"],
)

_, _, spr_el0 = check_gcs_registers(enabled, locked, spr_el0 - 8)
_, _, 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,
Expand Down Expand Up @@ -235,3 +235,142 @@ def check_gcs_registers(
"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)
Loading
Loading