Skip to content

[lldb] Restore register state if PrepareTrivialCall fails #129038

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 2 commits into from
Feb 28, 2025

Conversation

DavidSpickett
Copy link
Collaborator

@DavidSpickett DavidSpickett commented Feb 27, 2025

Fixes #124269

PrepareTrivalCall always had the possibility of failing, but given that it only wrote to general purpose registers, if it did, you had bigger problems.

When it failed, we did not mark the thread plan valid and when it was torn down we didn't try to restore the register state. This meant that if you tried to continue, the program was unlikely to work.

When I added AArch64 GCS support, I needed to handle the situation where the GCS pointer points to unmapped memory and we fail to write the extra entry we need. So I added code to restore the gcspr_el0 register specifically if this happened, and ordered the operations so that we tried this first.

In this change I've made the teardown of an invalid thread plan restore the register state if one was saved. It may be there isn't one if ConstructorSetup fails, but this is ok because that function does not modify anything.

Now that we're doing that, I don't need the GCS specific code anymore, and all thread plans are protected from this in the rare event something does fail.

Testing is done by the existing GCS test case that points the gcspr into unmapped memory which causes PrepareTrivialCall to fail. I tried adding a simulated test using a mock gdb server. This was not possible because they all use DynamicLoaderStatic which disables all JIT features.

@llvmbot
Copy link
Member

llvmbot commented Feb 27, 2025

@llvm/pr-subscribers-lldb

Author: David Spickett (DavidSpickett)

Changes

Fixes #124269

PrepareTrivalCall always had the possibility of failing, but given that it only wrote to general purpose registers, if it did, you had bigger problems.

When it failed, we did not mark the thread plan valid and when it was torn down we didn't try to restore the register state. This meant that if you tried to continue, the program unlikely to work.

When I added GCS, I needed to handle the situation where the GCS pointer points to unmapped memory and we fail to write the extra entry we need. So I added code to restore the gcspr_el0 register specifically if this happened, and ordered the operations such that we tried this first.

In this change I've made the teardown of an invalid thread plan restore the register state if one was saved. It may be there isn't one if ConstructorSetup fails, but this is ok because that function does not modify anything.

Now that we're doing that, I don't need the GCS specific code anymore, and all thread plans are protected from this in the rare event something does fail.

Testing is done by the existing GCS test case that points the gcspr into unmapped memory which causes PrepareTrivialCall to fail.

I tried adding a simulated test using a mock gdb server. This was not possible because they all use DynamicLoaderStatic which disables all JIT features.


Full diff: https://github.com/llvm/llvm-project/pull/129038.diff

2 Files Affected:

  • (modified) lldb/source/Plugins/ABI/AArch64/ABISysV_arm64.cpp (+1-6)
  • (modified) lldb/source/Target/ThreadPlanCallFunction.cpp (+12-1)
diff --git a/lldb/source/Plugins/ABI/AArch64/ABISysV_arm64.cpp b/lldb/source/Plugins/ABI/AArch64/ABISysV_arm64.cpp
index 280ec5ba37100..eb7c3a429f03d 100644
--- a/lldb/source/Plugins/ABI/AArch64/ABISysV_arm64.cpp
+++ b/lldb/source/Plugins/ABI/AArch64/ABISysV_arm64.cpp
@@ -102,12 +102,7 @@ static Status PushToLinuxGuardedControlStack(addr_t return_addr,
   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);
+    // gcspr_el0 will be restored when the ThreadPlan is destroyed.
     return Status("Failed to write new Guarded Control Stack entry.");
   }
 
diff --git a/lldb/source/Target/ThreadPlanCallFunction.cpp b/lldb/source/Target/ThreadPlanCallFunction.cpp
index 50dcb66b9719f..218111d4faf60 100644
--- a/lldb/source/Target/ThreadPlanCallFunction.cpp
+++ b/lldb/source/Target/ThreadPlanCallFunction.cpp
@@ -174,8 +174,20 @@ void ThreadPlanCallFunction::ReportRegisterState(const char *message) {
 
 void ThreadPlanCallFunction::DoTakedown(bool success) {
   Log *log = GetLog(LLDBLog::Step);
+  Thread &thread = GetThread();
 
   if (!m_valid) {
+    // If ConstructorSetup was succesfull but PrepareTrivialCall was not,
+    // we will have a saved register state and potentially modified registers.
+    // Restore those.
+    if (m_stored_thread_state.register_backup_sp)
+      if (!thread.RestoreRegisterStateFromCheckpoint(m_stored_thread_state))
+        LLDB_LOGF(
+            log,
+            "ThreadPlanCallFunction(%p): Failed to restore register state from "
+            "invalid plan that contained a saved register state.",
+            static_cast<void *>(this));
+
     // Don't call DoTakedown if we were never valid to begin with.
     LLDB_LOGF(log,
               "ThreadPlanCallFunction(%p): Log called on "
@@ -185,7 +197,6 @@ void ThreadPlanCallFunction::DoTakedown(bool success) {
   }
 
   if (!m_takedown_done) {
-    Thread &thread = GetThread();
     if (success) {
       SetReturnValue();
     }

@DavidSpickett
Copy link
Collaborator Author

Existing GCS test case is here -

# If we fail to setup the GCS entry, we should not leave any of the GCS registers

These don't run on bots yet, only Arm's simulator.

@DavidSpickett
Copy link
Collaborator Author

Our policy for the GCS registers is documented here - https://lldb.llvm.org/use/aarch64-linux.html#id3 - but for the GCS enable bit, we restore all of them after an expression.

Copy link
Collaborator

@jimingham jimingham left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Fixes llvm#124269

PrepareTrivalCall always had the possibility of failing, but
given that it only wrote to general purpose registers, if it
did, you had bigger problems.

When it failed, we did not mark the thread plan valid and
when it was torn down we didn't try to restore the register
state. This meant that if you tried to continue, the program
unlikely to work.

When I added GCS, I needed to handle the situation where the
GCS pointer points to unmapped memory and we fail to write
the extra entry we need. So I added code to restore the gcspr_el0
register specifically if this happened, and ordered the operations
such that we tried this first.

In this change I've made the teardown of an invalid thread plan
restore the register state if one was saved. It may be there isn't
one if ConstructorSetup fails, but this is ok because that function
does not modify anything.

Now that we're doing that, I don't need the GCS specific code
anymore, and all thread plans are protected from this in the rare
event something does fail.

Testing is done by the existing GCS test case that points
the gcspr into unmapped memory which causes PrepareTrivialCall
to fail.

I tried adding a simulated test using a mock gdb server. This
was not possible because they all use DynamicLoaderStatic which
disables all JIT features.
@DavidSpickett DavidSpickett merged commit 09c64e5 into llvm:main Feb 28, 2025
5 of 6 checks passed
@DavidSpickett DavidSpickett deleted the lldb-trivial branch February 28, 2025 15:58
cheezeburglar pushed a commit to cheezeburglar/llvm-project that referenced this pull request Feb 28, 2025
Fixes llvm#124269

PrepareTrivalCall always had the possibility of failing, but given that
it only wrote to general purpose registers, if it did, you had bigger
problems.

When it failed, we did not mark the thread plan valid and when it was
torn down we didn't try to restore the register state. This meant that
if you tried to continue, the program was unlikely to work.

When I added AArch64 GCS support, I needed to handle the situation where
the GCS pointer points to unmapped memory and we fail to write the extra
entry we need. So I added code to restore the gcspr_el0 register
specifically if this happened, and ordered the operations so that we
tried this first.

In this change I've made the teardown of an invalid thread plan restore
the register state if one was saved. It may be there isn't one if
ConstructorSetup fails, but this is ok because that function does not
modify anything.

Now that we're doing that, I don't need the GCS specific code anymore,
and all thread plans are protected from this in the rare event something
does fail.

Testing is done by the existing GCS test case that points the gcspr into
unmapped memory which causes PrepareTrivialCall to fail. I tried adding
a simulated test using a mock gdb server. This was not possible because
they all use DynamicLoaderStatic which disables all JIT features.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

lldb - Registers are not restored if PrepareTrivialCall fails
3 participants