Skip to content

[lldb][Linux] Add overlay and effective permissions to "memory region"#184115

Merged
DavidSpickett merged 2 commits into
llvm:mainfrom
DavidSpickett:lldb-poe-pr3
Apr 24, 2026
Merged

[lldb][Linux] Add overlay and effective permissions to "memory region"#184115
DavidSpickett merged 2 commits into
llvm:mainfrom
DavidSpickett:lldb-poe-pr3

Conversation

@DavidSpickett

Copy link
Copy Markdown
Contributor

In this change I'm extending the "memory region" command to show users the
overlay permissions that a protection key refers to, and the result of
applying that overlay to the page table permissions.

For example, protection key 0 refers to Perm0 in the por register.

(lldb) register read por 
             Perm0 = Read, Write, Execute

This is the default key, so many regions use it.

(lldb) memory region --all
<...>
[0x000ffffff7db0000-0x000ffffff7f40000) r-x /usr/lib/aarch64-linux-gnu/libc.so.6 PT_LOAD[0]
protection key: 0 (rwx, effective: r-x)

Protection keys can only change what was already enabled in the
page table. So we start with read and execute. Then a read/write/execute overlay
is applied. We cannot add write, so the result is read and execute.

Here's an example of its use with a real crash (output edited):

(lldb) c
* thread #1, name = 'test.o', stop reason = signal SIGSEGV: failed protection key checks (fault address=0xffffff7d60000)
-> 106    read_only_page[0] = '?';
(lldb) memory region 0xffffff7d60000
[0x000ffffff7d60000-0x000ffffff7d70000) rw- 
protection key: 6 (r--, effective: r--)
(lldb) register read por 
             Perm6 = Read

The calculation of permissions is implemented by a new ABI method.
It's in ABI for 2 reasons:

  • These overlays are usually in a register (X86 and AArch64 are)
    and that register name is architecture specific.
  • The way the overlay values apply may differ between architecture.
    AArch64 treats a set bit as adding a permission, but some may
    treat it as removing.

Technically this is dependent on operating system and architecture.
However, so are the methods for removing non-address bits, and those
are in ABI too.

To test this I have changed the allocations in the test program
to use read+execute permissions by default. With read+write+execute
I could not observe that the overlay only changes enabled permissions.

@llvmbot

llvmbot commented Mar 2, 2026

Copy link
Copy Markdown
Member

@llvm/pr-subscribers-lldb

Author: David Spickett (DavidSpickett)

Changes

In this change I'm extending the "memory region" command to show users the
overlay permissions that a protection key refers to, and the result of
applying that overlay to the page table permissions.

For example, protection key 0 refers to Perm0 in the por register.

(lldb) register read por 
             Perm0 = Read, Write, Execute

This is the default key, so many regions use it.

(lldb) memory region --all
&lt;...&gt;
[0x000ffffff7db0000-0x000ffffff7f40000) r-x /usr/lib/aarch64-linux-gnu/libc.so.6 PT_LOAD[0]
protection key: 0 (rwx, effective: r-x)

Protection keys can only change what was already enabled in the
page table. So we start with read and execute. Then a read/write/execute overlay
is applied. We cannot add write, so the result is read and execute.

Here's an example of its use with a real crash (output edited):

(lldb) c
* thread #<!-- -->1, name = 'test.o', stop reason = signal SIGSEGV: failed protection key checks (fault address=0xffffff7d60000)
-&gt; 106    read_only_page[0] = '?';
(lldb) memory region 0xffffff7d60000
[0x000ffffff7d60000-0x000ffffff7d70000) rw- 
protection key: 6 (r--, effective: r--)
(lldb) register read por 
             Perm6 = Read

The calculation of permissions is implemented by a new ABI method.
It's in ABI for 2 reasons:

  • These overlays are usually in a register (X86 and AArch64 are)
    and that register name is architecture specific.
  • The way the overlay values apply may differ between architecture.
    AArch64 treats a set bit as adding a permission, but some may
    treat it as removing.

Technically this is dependent on operating system and architecture.
However, so are the methods for removing non-address bits, and those
are in ABI too.

To test this I have changed the allocations in the test program
to use read+execute permissions by default. With read+write+execute
I could not observe that the overlay only changes enabled permissions.


Patch is 43.51 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/184115.diff

18 Files Affected:

  • (modified) lldb/docs/resources/lldbgdbremote.md (+2)
  • (modified) lldb/include/lldb/API/SBMemoryRegionInfo.h (+13)
  • (modified) lldb/include/lldb/Target/ABI.h (+25)
  • (modified) lldb/include/lldb/Target/MemoryRegionInfo.h (+11-3)
  • (modified) lldb/source/API/SBMemoryRegionInfo.cpp (+12)
  • (modified) lldb/source/Commands/CommandObjectMemory.cpp (+23)
  • (modified) lldb/source/Plugins/ABI/AArch64/ABISysV_arm64.cpp (+46)
  • (modified) lldb/source/Plugins/ABI/AArch64/ABISysV_arm64.h (+5)
  • (modified) lldb/source/Plugins/Process/Utility/LinuxProcMaps.cpp (+4)
  • (modified) lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp (+4)
  • (modified) lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp (+3)
  • (modified) lldb/source/Target/MemoryRegionInfo.cpp (+8-8)
  • (modified) lldb/test/API/linux/aarch64/permission_overlay/TestAArch64LinuxPOE.py (+67)
  • (modified) lldb/test/API/linux/aarch64/permission_overlay/main.c (+9-3)
  • (modified) lldb/unittests/Process/Utility/LinuxProcMapsTest.cpp (+92-34)
  • (modified) lldb/unittests/Process/Utility/MemoryTagManagerAArch64MTETest.cpp (+1-1)
  • (modified) lldb/unittests/Process/gdb-remote/GDBRemoteCommunicationClientTest.cpp (+20)
  • (modified) lldb/unittests/Process/minidump/MinidumpParserTest.cpp (+59-55)
diff --git a/lldb/docs/resources/lldbgdbremote.md b/lldb/docs/resources/lldbgdbremote.md
index 9aa7ad2259a6a..148b3a03aff86 100644
--- a/lldb/docs/resources/lldbgdbremote.md
+++ b/lldb/docs/resources/lldbgdbremote.md
@@ -1443,6 +1443,8 @@ tuples to return are:
   listed (`dirty-pages:;`) indicates no dirty pages in
   this memory region.  The *absence* of this key means
   that this stub cannot determine dirty pages.
+* `protection-key:<key>`- where `<key>` is an unsigned integer memory protection
+  key.
 
 If the address requested is not in a mapped region (e.g. we've jumped through
 a NULL pointer and are at 0x0) currently lldb expects to get back the size
diff --git a/lldb/include/lldb/API/SBMemoryRegionInfo.h b/lldb/include/lldb/API/SBMemoryRegionInfo.h
index dc5aa0858e1e3..9174675d2a11c 100644
--- a/lldb/include/lldb/API/SBMemoryRegionInfo.h
+++ b/lldb/include/lldb/API/SBMemoryRegionInfo.h
@@ -111,6 +111,19 @@ class LLDB_API SBMemoryRegionInfo {
   ///     or 0 if this information is unavailable.
   int GetPageSize();
 
+  /// Returns whether this memory region has a memory protection key.
+  ///
+  /// \return
+  ///     True if the region memory region has a memory protection key.
+  bool HasProtectionKey();
+
+  /// Returns the memory protection key of the memory region.
+  ///
+  /// \return
+  ///     The memory protection key of the region. This value is only valid if
+  ///     HasProtectionKey() is true.
+  uint32_t GetProtectionKey();
+
   bool operator==(const lldb::SBMemoryRegionInfo &rhs) const;
 
   bool operator!=(const lldb::SBMemoryRegionInfo &rhs) const;
diff --git a/lldb/include/lldb/Target/ABI.h b/lldb/include/lldb/Target/ABI.h
index 1a1f1724222e3..4eb38d33c9eed 100644
--- a/lldb/include/lldb/Target/ABI.h
+++ b/lldb/include/lldb/Target/ABI.h
@@ -152,6 +152,31 @@ class ABI : public PluginInterface {
 
   static lldb::ABISP FindPlugin(lldb::ProcessSP process_sp, const ArchSpec &arch);
 
+  struct MemoryPermissions {
+    // Both of these are sets of lldb::Permissions values.
+    // Overlay are the permissions being applied to the original permissions.
+    uint32_t overlay;
+    // Effective is the result of applying the overlay to the original
+    // permissions. Calculating this is done by the plugin because some
+    // permission overlays are done as positive (add permissions) and some as
+    // negative (remove permissions).
+    uint32_t effective;
+  };
+
+  /// Get the permissions being overlayed for a given memory key, and the
+  /// resulting permissions after applying the overlay. Typically the protection
+  /// key is used to look up in some architecture specific set of permissions.
+  /// On AArch64, this is the POR register, used by the Permission Overlay
+  /// Extension.
+  ///
+  /// Returns std::nullopt if the current target does not have such an overlay
+  /// system, or if the protection key is not valid.
+  virtual std::optional<MemoryPermissions>
+  GetMemoryPermissions(lldb_private::RegisterContext &reg_ctx,
+                       unsigned protection_key, uint32_t original_permissions) {
+    return std::nullopt;
+  }
+
 protected:
   ABI(lldb::ProcessSP process_sp, std::unique_ptr<llvm::MCRegisterInfo> info_up)
       : m_process_wp(process_sp), m_mc_register_info_up(std::move(info_up)) {
diff --git a/lldb/include/lldb/Target/MemoryRegionInfo.h b/lldb/include/lldb/Target/MemoryRegionInfo.h
index dc37a7dbeda52..1513d93f1ab22 100644
--- a/lldb/include/lldb/Target/MemoryRegionInfo.h
+++ b/lldb/include/lldb/Target/MemoryRegionInfo.h
@@ -29,11 +29,13 @@ class MemoryRegionInfo {
                    OptionalBool execute, OptionalBool shared,
                    OptionalBool mapped, ConstString name, OptionalBool flash,
                    lldb::offset_t blocksize, OptionalBool memory_tagged,
-                   OptionalBool stack_memory, OptionalBool shadow_stack)
+                   OptionalBool stack_memory, OptionalBool shadow_stack,
+                   std::optional<unsigned> protection_key)
       : m_range(range), m_read(read), m_write(write), m_execute(execute),
         m_shared(shared), m_mapped(mapped), m_name(name), m_flash(flash),
         m_blocksize(blocksize), m_memory_tagged(memory_tagged),
-        m_is_stack_memory(stack_memory), m_is_shadow_stack(shadow_stack) {}
+        m_is_stack_memory(stack_memory), m_is_shadow_stack(shadow_stack),
+        m_protection_key(protection_key) {}
 
   RangeType &GetRange() { return m_range; }
 
@@ -57,6 +59,8 @@ class MemoryRegionInfo {
 
   OptionalBool IsShadowStack() const { return m_is_shadow_stack; }
 
+  std::optional<unsigned> GetProtectionKey() const { return m_protection_key; }
+
   void SetReadable(OptionalBool val) { m_read = val; }
 
   void SetWritable(OptionalBool val) { m_write = val; }
@@ -81,6 +85,8 @@ class MemoryRegionInfo {
 
   void SetIsShadowStack(OptionalBool val) { m_is_shadow_stack = val; }
 
+  void SetProtectionKey(std::optional<unsigned> key) { m_protection_key = key; }
+
   // Get permissions as a uint32_t that is a mask of one or more bits from the
   // lldb::Permissions
   uint32_t GetLLDBPermissions() const {
@@ -111,7 +117,8 @@ class MemoryRegionInfo {
            m_memory_tagged == rhs.m_memory_tagged &&
            m_pagesize == rhs.m_pagesize &&
            m_is_stack_memory == rhs.m_is_stack_memory &&
-           m_is_shadow_stack == rhs.m_is_shadow_stack;
+           m_is_shadow_stack == rhs.m_is_shadow_stack &&
+           m_protection_key == rhs.m_protection_key;
   }
 
   bool operator!=(const MemoryRegionInfo &rhs) const { return !(*this == rhs); }
@@ -154,6 +161,7 @@ class MemoryRegionInfo {
   OptionalBool m_memory_tagged = eDontKnow;
   OptionalBool m_is_stack_memory = eDontKnow;
   OptionalBool m_is_shadow_stack = eDontKnow;
+  std::optional<unsigned> m_protection_key = std::nullopt;
   int m_pagesize = 0;
   std::optional<std::vector<lldb::addr_t>> m_dirty_pages;
 };
diff --git a/lldb/source/API/SBMemoryRegionInfo.cpp b/lldb/source/API/SBMemoryRegionInfo.cpp
index cd25be5d52769..99bb6f0de4915 100644
--- a/lldb/source/API/SBMemoryRegionInfo.cpp
+++ b/lldb/source/API/SBMemoryRegionInfo.cpp
@@ -160,6 +160,18 @@ int SBMemoryRegionInfo::GetPageSize() {
   return m_opaque_up->GetPageSize();
 }
 
+bool SBMemoryRegionInfo::HasProtectionKey() {
+  LLDB_INSTRUMENT_VA(this);
+
+  return m_opaque_up->GetProtectionKey() != std::nullopt;
+}
+
+uint32_t SBMemoryRegionInfo::GetProtectionKey() {
+  LLDB_INSTRUMENT_VA(this);
+
+  return m_opaque_up->GetProtectionKey().value_or(0);
+}
+
 bool SBMemoryRegionInfo::GetDescription(SBStream &description) {
   LLDB_INSTRUMENT_VA(this, description);
 
diff --git a/lldb/source/Commands/CommandObjectMemory.cpp b/lldb/source/Commands/CommandObjectMemory.cpp
index 93b6c1751b121..f1e3479fc8f69 100644
--- a/lldb/source/Commands/CommandObjectMemory.cpp
+++ b/lldb/source/Commands/CommandObjectMemory.cpp
@@ -1694,6 +1694,29 @@ class CommandObjectMemoryRegion : public CommandObjectParsed {
     MemoryRegionInfo::OptionalBool is_shadow_stack = range_info.IsShadowStack();
     if (is_shadow_stack == MemoryRegionInfo::OptionalBool::eYes)
       result.AppendMessage("shadow stack: yes");
+    if (std::optional<unsigned> protection_key =
+            range_info.GetProtectionKey()) {
+      result.AppendMessageWithFormat("protection key: %" PRIu32,
+                                     *protection_key);
+
+      if (const lldb::ABISP &abi = target.GetProcessSP()->GetABI()) {
+        uint32_t base_permissions = range_info.GetLLDBPermissions();
+        if (auto permissions =
+                abi->GetMemoryPermissions(*m_exe_ctx.GetRegisterContext(),
+                                          *protection_key, base_permissions)) {
+          result.AppendMessageWithFormatv(
+              " ({0}{1}{2}, effective: {3}{4}{5})",
+              permissions->overlay & lldb::ePermissionsReadable ? 'r' : '-',
+              permissions->overlay & lldb::ePermissionsWritable ? 'w' : '-',
+              permissions->overlay & lldb::ePermissionsExecutable ? 'x' : '-',
+              permissions->effective & lldb::ePermissionsReadable ? 'r' : '-',
+              permissions->effective & lldb::ePermissionsWritable ? 'w' : '-',
+              permissions->effective & lldb::ePermissionsExecutable ? 'x'
+                                                                    : '-');
+        }
+      } else
+        result.AppendMessage("");
+    }
 
     const std::optional<std::vector<addr_t>> &dirty_page_list =
         range_info.GetDirtyPageList();
diff --git a/lldb/source/Plugins/ABI/AArch64/ABISysV_arm64.cpp b/lldb/source/Plugins/ABI/AArch64/ABISysV_arm64.cpp
index aa9c20b6bb2cf..83a777da3237e 100644
--- a/lldb/source/Plugins/ABI/AArch64/ABISysV_arm64.cpp
+++ b/lldb/source/Plugins/ABI/AArch64/ABISysV_arm64.cpp
@@ -884,3 +884,49 @@ void ABISysV_arm64::Initialize() {
 void ABISysV_arm64::Terminate() {
   PluginManager::UnregisterPlugin(CreateInstance);
 }
+
+std::optional<ABISysV_arm64::MemoryPermissions>
+ABISysV_arm64::GetMemoryPermissions(lldb_private::RegisterContext &reg_ctx,
+                                    unsigned protection_key,
+                                    uint32_t original_permissions) {
+  // The presence of the POR register means we have the Permission Overlay
+  // Extension.
+  // See Arm Architecture Reference manual "POR_EL0, Permission Overlay Register
+  // 0 (EL0)".
+  const RegisterInfo *por_info = reg_ctx.GetRegisterInfoByName("por");
+  if (!por_info)
+    return std::nullopt;
+
+  uint64_t por_value =
+      reg_ctx.ReadRegisterAsUnsigned(por_info, LLDB_INVALID_ADDRESS);
+  if (por_value == LLDB_INVALID_ADDRESS)
+    return std::nullopt;
+
+  // POR contains 16, 4-bit permission sets (though Linux limits this to 8
+  // useable sets).
+  if (protection_key >= 16)
+    return std::nullopt;
+
+  // Bit 3 - reserved, bit 2 - write, bit 1 - execute, bit 0 - read.
+  const uint64_t por_permissions = (por_value >> (protection_key * 4)) & 0xf;
+  uint32_t overlay = 0;
+  if (por_permissions & 4)
+    overlay |= lldb::ePermissionsWritable;
+  if (por_permissions & 2)
+    overlay |= lldb::ePermissionsExecutable;
+  if (por_permissions & 1)
+    overlay |= lldb::ePermissionsReadable;
+
+  uint32_t effective = original_permissions;
+
+  // Permission overlays cannot add permissions, they can only keep, or disable,
+  // what was originally set.
+  if (!(overlay & lldb::ePermissionsWritable))
+    effective &= ~lldb::ePermissionsWritable;
+  if (!(overlay & lldb::ePermissionsExecutable))
+    effective &= ~lldb::ePermissionsExecutable;
+  if (!(overlay & lldb::ePermissionsReadable))
+    effective &= ~lldb::ePermissionsReadable;
+
+  return MemoryPermissions{overlay, effective};
+}
diff --git a/lldb/source/Plugins/ABI/AArch64/ABISysV_arm64.h b/lldb/source/Plugins/ABI/AArch64/ABISysV_arm64.h
index 213fbf7417b2c..53c79ee2d6eb1 100644
--- a/lldb/source/Plugins/ABI/AArch64/ABISysV_arm64.h
+++ b/lldb/source/Plugins/ABI/AArch64/ABISysV_arm64.h
@@ -81,6 +81,11 @@ class ABISysV_arm64 : public ABIAArch64 {
   lldb::addr_t FixCodeAddress(lldb::addr_t pc) override;
   lldb::addr_t FixDataAddress(lldb::addr_t pc) override;
 
+  virtual std::optional<MemoryPermissions>
+  GetMemoryPermissions(lldb_private::RegisterContext &reg_ctx,
+                       unsigned protection_key,
+                       uint32_t original_permissions) override;
+
 protected:
   lldb::ValueObjectSP
   GetReturnValueObjectImpl(lldb_private::Thread &thread,
diff --git a/lldb/source/Plugins/Process/Utility/LinuxProcMaps.cpp b/lldb/source/Plugins/Process/Utility/LinuxProcMaps.cpp
index 2ed896327a2f8..eea0bf245877d 100644
--- a/lldb/source/Plugins/Process/Utility/LinuxProcMaps.cpp
+++ b/lldb/source/Plugins/Process/Utility/LinuxProcMaps.cpp
@@ -174,6 +174,10 @@ void lldb_private::ParseLinuxSMapRegions(llvm::StringRef linux_smap,
               region->SetMemoryTagged(MemoryRegionInfo::eYes);
             else if (flag == "ss")
               region->SetIsShadowStack(MemoryRegionInfo::eYes);
+        } else if (name == "ProtectionKey") {
+          unsigned key = 0;
+          if (!value.ltrim().getAsInteger(10, key))
+            region->SetProtectionKey(key);
         }
       } else {
         // Orphaned settings line
diff --git a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp
index 738e4013b6154..c639090ebe9af 100644
--- a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp
+++ b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp
@@ -1677,6 +1677,10 @@ Status GDBRemoteCommunicationClient::GetMemoryRegionInfo(
               dirty_page_list.push_back(page);
           }
           region_info.SetDirtyPageList(dirty_page_list);
+        } else if (name == "protection-key") {
+          unsigned protection_key = 0;
+          if (!value.getAsInteger(10, protection_key))
+            region_info.SetProtectionKey(protection_key);
         }
       }
 
diff --git a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp
index 2f62415446b7a..72b6d7adbbf96 100644
--- a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp
+++ b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp
@@ -2907,6 +2907,9 @@ GDBRemoteCommunicationServerLLGS::Handle_qMemoryRegionInfo(
       response.PutStringAsRawHex8(name.GetStringRef());
       response.PutChar(';');
     }
+
+    if (std::optional<unsigned> protection_key = region_info.GetProtectionKey())
+      response.Printf("protection-key:%" PRIu32 ";", *protection_key);
   }
 
   return SendPacketNoLock(response.GetString());
diff --git a/lldb/source/Target/MemoryRegionInfo.cpp b/lldb/source/Target/MemoryRegionInfo.cpp
index 979e45ad023af..7bdb3dc4f3168 100644
--- a/lldb/source/Target/MemoryRegionInfo.cpp
+++ b/lldb/source/Target/MemoryRegionInfo.cpp
@@ -12,14 +12,14 @@ using namespace lldb_private;
 
 llvm::raw_ostream &lldb_private::operator<<(llvm::raw_ostream &OS,
                                             const MemoryRegionInfo &Info) {
-  return OS << llvm::formatv("MemoryRegionInfo([{0}, {1}), {2:r}{3:w}{4:x}, "
-                             "{5}, `{6}`, {7}, {8}, {9}, {10}, {11})",
-                             Info.GetRange().GetRangeBase(),
-                             Info.GetRange().GetRangeEnd(), Info.GetReadable(),
-                             Info.GetWritable(), Info.GetExecutable(),
-                             Info.GetMapped(), Info.GetName(), Info.GetFlash(),
-                             Info.GetBlocksize(), Info.GetMemoryTagged(),
-                             Info.IsStackMemory(), Info.IsShadowStack());
+  return OS << llvm::formatv(
+             "MemoryRegionInfo([{0}, {1}), {2:r}{3:w}{4:x}, "
+             "{5}, `{6}`, {7}, {8}, {9}, {10}, {11}, {12})",
+             Info.GetRange().GetRangeBase(), Info.GetRange().GetRangeEnd(),
+             Info.GetReadable(), Info.GetWritable(), Info.GetExecutable(),
+             Info.GetMapped(), Info.GetName(), Info.GetFlash(),
+             Info.GetBlocksize(), Info.GetMemoryTagged(), Info.IsStackMemory(),
+             Info.IsShadowStack(), Info.GetProtectionKey());
 }
 
 void llvm::format_provider<MemoryRegionInfo::OptionalBool>::format(
diff --git a/lldb/test/API/linux/aarch64/permission_overlay/TestAArch64LinuxPOE.py b/lldb/test/API/linux/aarch64/permission_overlay/TestAArch64LinuxPOE.py
index 056267a2dc900..9b20b39095789 100644
--- a/lldb/test/API/linux/aarch64/permission_overlay/TestAArch64LinuxPOE.py
+++ b/lldb/test/API/linux/aarch64/permission_overlay/TestAArch64LinuxPOE.py
@@ -79,6 +79,63 @@ def test_poe_live(self):
         self.expect("expression expr_function()", substrs=["$0 = 1"])
         self.expect("register read por", substrs=[self.EXPECTED_POR])
 
+        # Unmapped region has no key (not even default).
+        self.expect("memory region 0", substrs=["protection key:"], matching=False)
+
+        # The region has base permissions r-x, and overlay is r--. The result
+        # is that execution is disabled.
+        self.expect(
+            "memory region read_only_page",
+            substrs=["rw-", "protection key: 6 (r--, effective: r--)"],
+        )
+        # A region not assigned to a protection key has the default key 0. This
+        # key is rwx, but overlays cannot add permissions not already in the
+        # page table. So the execute permission is not enabled.
+        self.expect(
+            "memory region key_zero_page",
+            substrs=["rw-", "protection key: 0 (rwx, effective: rw-)"],
+        )
+
+        # Overlay permissions are on their own line.
+        self.expect(
+            "memory region --all",
+            patterns=["\nprotection key: [0-9]+ \([rwx-]{3}, effective: [rwx-]{3}\)\n"],
+        )
+
+        # Protection keys are also in SBMemoryRegionInfo.
+        process = self.dbg.GetSelectedTarget().GetProcess()
+        info = lldb.SBMemoryRegionInfo()
+
+        frame = (
+            self.dbg.GetSelectedTarget()
+            .GetProcess()
+            .GetSelectedThread()
+            .GetSelectedFrame()
+        )
+
+        err = lldb.SBError()
+        read_only_addr = frame.GetValueForVariablePath(
+            "read_only_page"
+        ).GetValueAsUnsigned(err)
+        self.assertTrue(err.Success())
+        key_zero_addr = frame.GetValueForVariablePath(
+            "key_zero_page"
+        ).GetValueAsUnsigned(err)
+        self.assertTrue(err.Success())
+
+        region_api_info = [
+            # An unmapped region will have no key at all.
+            # The getter returns 0 as a default, but should not be trusted.
+            (0, False, 0),
+            (read_only_addr, True, 6),
+            (key_zero_addr, True, 0),
+        ]
+        for addr, valid, key in region_api_info:
+            err = process.GetMemoryRegionInfo(addr, info)
+            self.assertTrue(err.Success())
+            self.assertEqual(info.HasProtectionKey(), valid)
+            self.assertEqual(info.GetProtectionKey(), key)
+
         # Not passing this to the application allows us to fix the permissions
         # using lldb, then continue to a normal exit.
         self.runCmd("process handle SIGSEGV --pass false")
@@ -127,3 +184,13 @@ def test_poe_core(self):
                 "register read por",
                 substrs=[f"     {self.EXPECTED_POR}\n" + self.EXPECTED_POR_FIELDS],
             )
+
+        # Protection keys are listed in /proc/<pid>/smaps, which is not included
+        # in core files.
+        self.expect("memory region --all", substrs=["protection key:"], matching=False)
+
+        # No region should have a key at all, not even a default.
+        process = self.dbg.GetSelectedTarget().GetProcess()
+        for region in self.dbg.GetSelectedTarget().GetProcess().GetMemoryRegions():
+            self.assertEqual(region.HasProtectionKey(), False)
+            self.assertEqual(region.GetProtectionKey(), 0)
diff --git a/lldb/test/API/linux/aarch64/permission_overlay/main.c b/lldb/test/API/linux/aarch64/permission_overlay/main.c
index 6f47ba9d774da..5eb4782b9a6ca 100644
--- a/lldb/test/API/linux/aarch64/permission_overlay/main.c
+++ b/lldb/test/API/linux/aarch64/permission_overlay/main.c
@@ -76,11 +76,17 @@ int main(void) {
   // Which leaves 7 keys available for programs to allocate.
 
   const size_t page_size = (size_t)sysconf(_SC_PAGESIZE);
-  // pkeys can only subtract from the set of permissions in the page table,
-  // so we set the page table to allow everything.
-  const int prot = PROT_READ | PROT_WRITE | PROT_EXEC;
+  // pkeys can only subtract from the set of permissions in the page table.
+  // So we leave out execute here to check later that an overlay does not
+  // enable execution.
+  const int prot = PROT_READ | PROT_WRITE;
   const int flags = MAP_PRIVATE | MAP_ANONYMOUS;
 
+  // This page will have the default key 0.
+  char *key_zero_page = mmap(NULL, page_size, prot, flags, -1, 0);
+  if (key_zero_page == MAP_FAILED)
+    exit(2);
+
   // Later we will use this to cause a protection key fault.
   char *read_only_page = NULL;
 
diff --git a/lldb/unittests/Process/Utility/LinuxProcMapsTest.cpp b/lldb/unittests/Process/Utility/LinuxProcMapsTest.cpp
index d94bb4f4db982..f3f40cbc2f19d 100644
--- a/l...
[truncated]

@DavidSpickett

Copy link
Copy Markdown
Contributor Author

First commit here is #182246, please review the following commits.

Comment thread lldb/source/Commands/CommandObjectMemory.cpp Outdated

if (const lldb::ABISP &abi = target.GetProcessSP()->GetABI()) {
uint32_t base_permissions = range_info.GetLLDBPermissions();
if (auto permissions =

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

X86 Linux also has memory protection keys, but because its ABI plugin doesn't override this method, it will just show protection key: N.

To make that work, someone would need to override that method, and add the x86 overlay register (but I have no intention to do so).

@DavidSpickett

Copy link
Copy Markdown
Contributor Author

Just rebased but the first commit here is still #182246, please review the second.

Comment thread lldb/include/lldb/Target/ABI.h Outdated
@DavidSpickett

Copy link
Copy Markdown
Contributor Author

ping @omjavaid

@omjavaid omjavaid left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This looks good to me. Thanks for waiting on the review.

In this change I'm extending the "memory region" command to show users the
overlay permissions that a protection key refers to, and the result of
applying that overlay to the page table permissions.

For example, protection key 0 refers to Perm0 in the por register.

(lldb) register read por
             Perm0 = Read, Write, Execute

This is the default key, so many regions use it.

(lldb) memory region --all
<...>
[0x000ffffff7db0000-0x000ffffff7f40000) r-x /usr/lib/aarch64-linux-gnu/libc.so.6 PT_LOAD[0]
protection key: 0 (rwx, effective: r-x)

Protection keys can only change what was already enabled in the
page table. So we start with read and execute. Then a read/write/execute overlay
is applied. We cannot add write, so the result is read and execute.

Here's an example of its use with a real crash (output edited):
(lldb) c
* thread #1, name = 'test.o', stop reason = signal SIGSEGV: failed protection key checks (fault address=0xffffff7d60000)
-> 106 	  read_only_page[0] = '?';
(lldb) memory region 0xffffff7d60000
[0x000ffffff7d60000-0x000ffffff7d70000) rw-
protection key: 6 (r--, effective: r--)
(lldb) register read por
             Perm6 = Read

The calculation of permissions is implemented by a new ABI method.
It's in ABI for 2 reasons:
* These overlays are usually in a register (X86 and AArch64 are)
  and that register name is architecture specific.
* The way the overlay values apply may differ between architecture.
  AArch64 treats a set bit as adding a permission, but some may
  treat it as removing.

Technically this is dependent on operating system and architecture.
However, so are the methods for removing non-address bits, and those
are in ABI too.

To test this I have changed the allocations in the test program
to use read+execute permissions by default. With read+write+execute
I could not observe that the overlay only changes enabled permissions.
@DavidSpickett DavidSpickett enabled auto-merge (squash) April 24, 2026 12:23
@DavidSpickett DavidSpickett merged commit 4c66205 into llvm:main Apr 24, 2026
11 checks passed
@DavidSpickett DavidSpickett deleted the lldb-poe-pr3 branch April 24, 2026 13:03
yingopq pushed a commit to yingopq/llvm-project that referenced this pull request Apr 29, 2026
llvm#184115)

In this change I'm extending the "memory region" command to show users
the
overlay permissions that a protection key refers to, and the result of
applying that overlay to the page table permissions.

For example, protection key 0 refers to Perm0 in the por register.
```
(lldb) register read por 
             Perm0 = Read, Write, Execute
```
This is the default key, so many regions use it. 
```
(lldb) memory region --all
<...>
[0x000ffffff7db0000-0x000ffffff7f40000) r-x /usr/lib/aarch64-linux-gnu/libc.so.6 PT_LOAD[0]
protection key: 0 (rwx, effective: r-x)
```
Protection keys can only change what was already enabled in the 
page table. So we start with read and execute. Then a read/write/execute
overlay
is applied. We cannot add write, so the result is read and execute.

Here's an example of its use with a real crash (output edited):
```
(lldb) c
* thread llvm#1, name = 'test.o', stop reason = signal SIGSEGV: failed protection key checks (fault address=0xffffff7d60000)
-> 106    read_only_page[0] = '?';
(lldb) memory region 0xffffff7d60000
[0x000ffffff7d60000-0x000ffffff7d70000) rw- 
protection key: 6 (r--, effective: r--)
(lldb) register read por 
             Perm6 = Read
```
The calculation of permissions is implemented by a new ABI method.
It's in ABI for 2 reasons:
* These overlays are usually in a register (X86 and AArch64 are)
  and that register name is architecture specific.
* The way the overlay values apply may differ between architecture.
  AArch64 treats a set bit as adding a permission, but some may 
  treat it as removing.

Technically this is dependent on operating system and architecture.
However, so are the methods for removing non-address bits, and those
are in ABI too.

To test this I have changed the allocations in the test program
to use read+execute permissions by default. With read+write+execute
I could not observe that the overlay only changes enabled permissions.
KHicketts pushed a commit to KHicketts/llvm-project that referenced this pull request Apr 30, 2026
llvm#184115)

In this change I'm extending the "memory region" command to show users
the
overlay permissions that a protection key refers to, and the result of
applying that overlay to the page table permissions.

For example, protection key 0 refers to Perm0 in the por register.
```
(lldb) register read por 
             Perm0 = Read, Write, Execute
```
This is the default key, so many regions use it. 
```
(lldb) memory region --all
<...>
[0x000ffffff7db0000-0x000ffffff7f40000) r-x /usr/lib/aarch64-linux-gnu/libc.so.6 PT_LOAD[0]
protection key: 0 (rwx, effective: r-x)
```
Protection keys can only change what was already enabled in the 
page table. So we start with read and execute. Then a read/write/execute
overlay
is applied. We cannot add write, so the result is read and execute.

Here's an example of its use with a real crash (output edited):
```
(lldb) c
* thread llvm#1, name = 'test.o', stop reason = signal SIGSEGV: failed protection key checks (fault address=0xffffff7d60000)
-> 106    read_only_page[0] = '?';
(lldb) memory region 0xffffff7d60000
[0x000ffffff7d60000-0x000ffffff7d70000) rw- 
protection key: 6 (r--, effective: r--)
(lldb) register read por 
             Perm6 = Read
```
The calculation of permissions is implemented by a new ABI method.
It's in ABI for 2 reasons:
* These overlays are usually in a register (X86 and AArch64 are)
  and that register name is architecture specific.
* The way the overlay values apply may differ between architecture.
  AArch64 treats a set bit as adding a permission, but some may 
  treat it as removing.

Technically this is dependent on operating system and architecture.
However, so are the methods for removing non-address bits, and those
are in ABI too.

To test this I have changed the allocations in the test program
to use read+execute permissions by default. With read+write+execute
I could not observe that the overlay only changes enabled permissions.
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.

3 participants