Skip to content

[asan] Re-exec without ASLR if needed on 32-bit Linux #131975

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 3 commits into from
Mar 20, 2025

Conversation

thurstond
Copy link
Contributor

@thurstond thurstond commented Mar 19, 2025

High-entropy ASLR allows up to 16-bits of entropy (2**16 4KB pages == 256MB; a bit more in practice because of implementation details), which is a significant chunk of the user address space on 32-bit systems (4GB or less). This, combined with ASan's shadow (512MB) and ASan's fixed shadow offset (512MB), makes it possible for large binaries to fail to map the shadow.

This patch changes ASan to do a one-time re-exec without ASLR if it cannot map the shadow, thus reclaiming the ~256MB of address space.

Alternatives considered:

  1. We don't lower ASan's fixed shadow offset, because that would limit non-PIE binaries.
  2. We don't switch to a dynamic shadow offset, because ASan for 32-bit Linux relies on the compile-time constant offset to optimize its instrumentation and compiler-rt.

This is loosely inspired by #78351, #85142, and #85674, though those were required because there were no static shadow mappings that could fully shadow the range of user mappings; this is not the case for ASan.

High-entropy ASLR allows up to 16-bits of entropy (256MB), which is a
significant chunk of the 32-bit address space (4GB, less if running with
a 32-bit kernel). This, combined with ASan's shadow (512MB) and ASan's
fixed shadow offset (512MB), makes it possible for large binaries to
fail to map the shadow.

This patch will re-exec without ASLR if it cannot map the shadow, thus
reclaiming the 256MB of address space.

Alternatives considered:
1) We don't attempt to lower ASan's fixed shadow offset, because that
   would limit non-PIE binaries.
2) We don't switch to a dynamic shadow offset, because ASan on 32-bit
   Linux relies on the constant offset to optimize its instrumentation
   and compiler-rt.

This is loosely inspired by
llvm#78351,
llvm#85142, and llvm#85674, though those were required because there were no static mappings that could fully shadow the range of user mappings; this is not the case for ASan.
@llvmbot
Copy link
Member

llvmbot commented Mar 19, 2025

@llvm/pr-subscribers-compiler-rt-sanitizer

Author: Thurston Dang (thurstond)

Changes

High-entropy ASLR allows up to 16-bits of entropy (256MB), which is a significant chunk of the 32-bit address space (4GB, less if running with a 32-bit kernel). This, combined with ASan's shadow (512MB) and ASan's fixed shadow offset (512MB), makes it possible for large binaries to fail to map the shadow.

This patch will re-exec without ASLR if it cannot map the shadow, thus reclaiming the 256MB of address space.

Alternatives considered:

  1. We don't attempt to lower ASan's fixed shadow offset, because that
    would limit non-PIE binaries.
  2. We don't switch to a dynamic shadow offset, because ASan on 32-bit
    Linux relies on the constant offset to optimize its instrumentation
    and compiler-rt.

This is loosely inspired by
#78351,
#85142, and #85674, though those were required because there were no static mappings that could fully shadow the range of user mappings; this is not the case for ASan.


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

5 Files Affected:

  • (modified) compiler-rt/lib/asan/asan_internal.h (+1)
  • (modified) compiler-rt/lib/asan/asan_linux.cpp (+28)
  • (modified) compiler-rt/lib/asan/asan_mac.cpp (+3)
  • (modified) compiler-rt/lib/asan/asan_shadow_setup.cpp (+9)
  • (modified) compiler-rt/lib/asan/asan_win.cpp (+4)
diff --git a/compiler-rt/lib/asan/asan_internal.h b/compiler-rt/lib/asan/asan_internal.h
index 06dfc4b177339..8205e19fdab7f 100644
--- a/compiler-rt/lib/asan/asan_internal.h
+++ b/compiler-rt/lib/asan/asan_internal.h
@@ -82,6 +82,7 @@ void ReplaceSystemMalloc();
 uptr FindDynamicShadowStart();
 void AsanCheckDynamicRTPrereqs();
 void AsanCheckIncompatibleRT();
+void TryReExecWithoutASLR();
 
 // Unpoisons platform-specific stacks.
 // Returns true if all stacks have been unpoisoned.
diff --git a/compiler-rt/lib/asan/asan_linux.cpp b/compiler-rt/lib/asan/asan_linux.cpp
index 4cabca388ca9a..7f502df3cf1b4 100644
--- a/compiler-rt/lib/asan/asan_linux.cpp
+++ b/compiler-rt/lib/asan/asan_linux.cpp
@@ -21,6 +21,7 @@
 #  include <pthread.h>
 #  include <stdio.h>
 #  include <sys/mman.h>
+#  include <sys/personality.h>
 #  include <sys/resource.h>
 #  include <sys/syscall.h>
 #  include <sys/time.h>
@@ -107,6 +108,33 @@ void FlushUnneededASanShadowMemory(uptr p, uptr size) {
   ReleaseMemoryPagesToOS(MemToShadow(p), MemToShadow(p + size));
 }
 
+void ReExecWithoutASLR() {
+  // ASLR personality check.
+  // Caution: 'personality' is sometimes forbidden by sandboxes, so only call
+  // this function as a last resort (when the memory mapping is incompatible
+  // and ASan would fail anyway).
+  int old_personality = personality(0xffffffff);
+  bool aslr_on =
+      (old_personality != -1) && ((old_personality & ADDR_NO_RANDOMIZE) == 0);
+
+  if (aslr_on) {
+    // Disable ASLR if the memory layout was incompatible.
+    // Alternatively, we could just keep re-execing until we get lucky
+    // with a compatible randomized layout, but the risk is that if it's
+    // not an ASLR-related issue, we will be stuck in an infinite loop of
+    // re-execing (unless we change ReExec to pass a parameter of the
+    // number of retries allowed.)
+    VReport(1,
+            "WARNING: AddressSanitizer: memory layout is incompatible, "
+            "possibly due to high-entropy ASLR.\n"
+            "Re-execing with fixed virtual address space.\n"
+            "N.B. reducing ASLR entropy is preferable.\n");
+    CHECK_NE(personality(old_personality | ADDR_NO_RANDOMIZE), -1);
+
+    ReExec();
+  }
+}
+
 #  if SANITIZER_ANDROID
 // FIXME: should we do anything for Android?
 void AsanCheckDynamicRTPrereqs() {}
diff --git a/compiler-rt/lib/asan/asan_mac.cpp b/compiler-rt/lib/asan/asan_mac.cpp
index bfc349223258b..be513a03ed5cd 100644
--- a/compiler-rt/lib/asan/asan_mac.cpp
+++ b/compiler-rt/lib/asan/asan_mac.cpp
@@ -55,6 +55,9 @@ uptr FindDynamicShadowStart() {
                           GetMmapGranularity());
 }
 
+// Not used.
+void TryReExecWithoutASLR() {}
+
 // No-op. Mac does not support static linkage anyway.
 void AsanCheckDynamicRTPrereqs() {}
 
diff --git a/compiler-rt/lib/asan/asan_shadow_setup.cpp b/compiler-rt/lib/asan/asan_shadow_setup.cpp
index fc6de39622b51..ad24f63985f8a 100644
--- a/compiler-rt/lib/asan/asan_shadow_setup.cpp
+++ b/compiler-rt/lib/asan/asan_shadow_setup.cpp
@@ -109,6 +109,15 @@ void InitializeShadowMemory() {
     ProtectGap(kShadowGap2Beg, kShadowGap2End - kShadowGap2Beg + 1);
     ProtectGap(kShadowGap3Beg, kShadowGap3End - kShadowGap3Beg + 1);
   } else {
+#  if SANITIZER_LINUX
+    // The shadow mappings can shadow the entire user address space. However,
+    // on 32-bit systems, the maximum ASLR entropy (currently up to 16-bits
+    // == 256MB) is a significant chunk of the address space; reclaiming it by
+    // disabling ASLR might allow chonky binaries to run.
+    if (sizeof(uptr) == 32)
+      TryReExecWithoutASLR();
+#  endif
+
     Report(
         "Shadow memory range interleaves with an existing memory mapping. "
         "ASan cannot proceed correctly. ABORTING.\n");
diff --git a/compiler-rt/lib/asan/asan_win.cpp b/compiler-rt/lib/asan/asan_win.cpp
index 027340280e068..7e6792c78d5e6 100644
--- a/compiler-rt/lib/asan/asan_win.cpp
+++ b/compiler-rt/lib/asan/asan_win.cpp
@@ -43,6 +43,7 @@ uptr __asan_get_shadow_memory_dynamic_address() {
   __asan_init();
   return __asan_shadow_memory_dynamic_address;
 }
+
 }  // extern "C"
 
 // ---------------------- Windows-specific interceptors ---------------- {{{
@@ -279,6 +280,9 @@ uptr FindDynamicShadowStart() {
                           GetMmapGranularity());
 }
 
+// Not used
+void TryReExecWithoutASLR() {}
+
 void AsanCheckDynamicRTPrereqs() {}
 
 void AsanCheckIncompatibleRT() {}

// Caution: 'personality' is sometimes forbidden by sandboxes, so only call
// this function as a last resort (when the memory mapping is incompatible
// and ASan would fail anyway).
int old_personality = personality(0xffffffff);
Copy link
Contributor

Choose a reason for hiding this comment

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

I would have

if (old_personality == -1) {
  [log error]
  return;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@@ -109,6 +109,15 @@ void InitializeShadowMemory() {
ProtectGap(kShadowGap2Beg, kShadowGap2End - kShadowGap2Beg + 1);
ProtectGap(kShadowGap3Beg, kShadowGap3End - kShadowGap3Beg + 1);
} else {
# if SANITIZER_LINUX
Copy link
Contributor

Choose a reason for hiding this comment

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

if the call is within this guard, why do we need a dummy for macos and windows?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@@ -43,6 +43,7 @@ uptr __asan_get_shadow_memory_dynamic_address() {
__asan_init();
return __asan_shadow_memory_dynamic_address;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

revert unrelated change

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@thurstond thurstond requested a review from fmayer March 19, 2025 20:59
@thurstond thurstond merged commit 3b3f8c5 into llvm:main Mar 20, 2025
10 checks passed
@rorth
Copy link
Collaborator

rorth commented Mar 20, 2025

This patch broke (at least) the Solaris/sparcv9 and Solaris/amd64 bots: <sys/personality.h> is a Linux-only feature, while asan_linux.cpp, despite its name, is used by serveral Unix OSes.

Please fix.

thurstond added a commit that referenced this pull request Mar 20, 2025
thurstond added a commit that referenced this pull request Mar 20, 2025
@thurstond
Copy link
Contributor Author

This patch broke (at least) the Solaris/sparcv9 and Solaris/amd64 bots: <sys/personality.h> is a Linux-only feature, while asan_linux.cpp, despite its name, is used by serveral Unix OSes.

Please fix.

Sorry, my bad. Fixed in 89a1197c1f4f and adb57757b964

llvm-sync bot pushed a commit to arm/arm-toolchain that referenced this pull request Mar 20, 2025
llvm-sync bot pushed a commit to arm/arm-toolchain that referenced this pull request Mar 20, 2025
thurstond added a commit to thurstond/llvm-project that referenced this pull request Mar 20, 2025
asan_(malloc_)?linux.cpp are misleadingly named because they cover
many non-Linux OSes, such as BSDs, Fuchsia and Solaris. This is a
footgun where changes may be made to these files without remembering it
is not Linux-specific (e.g., I broke the Solaris build - see llvm#131975 (comment)).

This patch mitigates the issue by renaming the file from ...linux to ...unix, which should hopefully give pause to anyone (me) when making Linux-specific changes.
thurstond added a commit that referenced this pull request Mar 24, 2025
Function definition added in #131975 was missing 'Try' (do or do not ...). My guess is buildbots
mostly didn't trip because usage was gated by 'if (sizeof(uptr) ==
32)', which is rare among buildbots.
thurstond added a commit to thurstond/llvm-project that referenced this pull request Mar 24, 2025
This generalizes llvm#131975 to
non-32-bit Linux (i.e., 64-bit Linux).

This works around an edge case in 64-bit Linux, where the memory layout
is incompatible if the stack size is unlimited AND ASLR entropy is 32
bits (see
google/sanitizers#856 (comment)).

More generally, this "re-exec if layout is incompatible" is a hammer
that can work around most shadow mapping issues, without the overhead of
using a dynamic shadow.
thurstond added a commit that referenced this pull request Mar 24, 2025
This generalizes #131975 to non-32-bit Linux (i.e., 64-bit Linux).

This works around an edge case in 64-bit Linux, whereby the memory layout is incompatible if the stack size is unlimited AND ASLR entropy is 31+ bits (see google/sanitizers#856 (comment)).

More generally, this "re-exec without ASLR if layout is incompatible" is a hammer that can work around most shadow mapping issues, without incurring the overhead of using a dynamic shadow.
llvm-sync bot pushed a commit to arm/arm-toolchain that referenced this pull request Mar 24, 2025
…2682)

This generalizes llvm/llvm-project#131975 to non-32-bit Linux (i.e., 64-bit Linux).

This works around an edge case in 64-bit Linux, whereby the memory layout is incompatible if the stack size is unlimited AND ASLR entropy is 31+ bits (see google/sanitizers#856 (comment)).

More generally, this "re-exec without ASLR if layout is incompatible" is a hammer that can work around most shadow mapping issues, without incurring the overhead of using a dynamic shadow.
// on 32-bit systems, the maximum ASLR entropy (currently up to 16-bits
// == 256MB) is a significant chunk of the address space; reclaiming it by
// disabling ASLR might allow chonky binaries to run.
if (sizeof(uptr) == 32)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this correct? From what I can see, uptr is an alias of uintptr_t, meaning sizeof is 4 on 32bit systems and this branch is never taken on any platform.

(Yes, I'm late.)

Copy link
Contributor

Choose a reason for hiding this comment

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

...oh, #132682 deleted that check. This question no longer matters.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

From what I can see, uptr is an alias of uintptr_t, meaning sizeof is 4 on 32bit systems and this branch is never taken on any platform.

You're right, that was a bug. Nice catch :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants