From 6bde9daed52200edf541b80438e9d7156822d74a Mon Sep 17 00:00:00 2001 From: anish Date: Thu, 2 Jul 2026 17:50:19 +0000 Subject: [PATCH 1/4] fix(linux): prevent infinite re-exec loop under AppArmor The `MaybeReenterWithoutASLR()` function in `src/benchmark.cc` caused infinite execv() loops when running benchmarks under AppArmor-enabled LSMs. The existing fix from #1985 only checked whether personality(ADDR_NO_RANDOMIZE) succeeded in the current process before calling execv(). However, some LSMs like AppArmor can silently reset personality flags during the execve() system call transition, even though the flag was successfully set in the parent process. Signed-off-by: anish --- src/benchmark.cc | 84 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 80 insertions(+), 4 deletions(-) diff --git a/src/benchmark.cc b/src/benchmark.cc index 22817851a..5e3f5162c 100644 --- a/src/benchmark.cc +++ b/src/benchmark.cc @@ -36,6 +36,7 @@ #ifdef BENCHMARK_OS_LINUX #include +#include #endif #include @@ -43,6 +44,7 @@ #include #include #include +#include #include #include #include @@ -835,11 +837,26 @@ std::make_unsigned_t get_as_unsigned(T v) { } // end namespace internal -void MaybeReenterWithoutASLR(int /*argc*/, char** argv) { +void MaybeReenterWithoutASLR(int argc, char** argv) { // On e.g. Hexagon simulator, argv may be NULL. if (!argv) return; #ifdef BENCHMARK_OS_LINUX + // Check if we are a test child process that should report personality and exit + for (int i = 1; i < argc; ++i) { + if (std::strncmp(argv[i], "--benchmark_aslr_test_child=", 28) == 0) { + const int write_fd = std::atoi(argv[i] + 28); + const auto test_personality = personality(0xffffffff); + // Write 1 if ADDR_NO_RANDOMIZE is set, 0 otherwise + char result = ((test_personality != -1) && + (internal::get_as_unsigned(test_personality) & ADDR_NO_RANDOMIZE)) + ? 1 : 0; + write(write_fd, &result, 1); + close(write_fd); + std::exit(0); + } + } + const auto curr_personality = personality(0xffffffff); // We should never fail to read-only query the current personality, @@ -864,9 +881,68 @@ void MaybeReenterWithoutASLR(int /*argc*/, char** argv) { if ((internal::get_as_unsigned(new_personality) & ADDR_NO_RANDOMIZE) == 0) return; - execv(argv[0], argv); - // The exec() functions return only if an error has occurred, - // in which case we want to just continue as-is. + // Verify that the personality change survives exec() by testing in a child. + // Some LSMs (e.g., AppArmor) may reset personality flags during execve(), + // even though they were successfully set in the parent process. + // This prevents infinite re-exec loops when the kernel silently resets + // the personality after each exec. + int pipefd[2]; + if (pipe(pipefd) == -1) return; + + pid_t pid = fork(); + if (pid == -1) { + close(pipefd[0]); + close(pipefd[1]); + return; + } + + if (pid == 0) { + // Child: prepare to exec with test argument + close(pipefd[0]); + + // Count existing arguments + int arg_count = 0; + while (argv[arg_count] != nullptr) ++arg_count; + + // Build new argv with test argument (allocate on heap to survive exec) + char** new_argv = static_cast( + malloc(sizeof(char*) * (arg_count + 2))); + if (!new_argv) _exit(1); + + for (int i = 0; i < arg_count; ++i) { + new_argv[i] = argv[i]; + } + + // Add test argument with pipe write fd (allocate on heap) + char* test_arg = static_cast(malloc(64)); + if (!test_arg) _exit(1); + std::snprintf(test_arg, 64, + "--benchmark_aslr_test_child=%d", pipefd[1]); + new_argv[arg_count] = test_arg; + new_argv[arg_count + 1] = nullptr; + + execv(argv[0], new_argv); + // If exec fails, exit (no need to free since we're exiting) + _exit(1); + } + + // Parent: wait for child to report back + close(pipefd[1]); + + char result = 0; + ssize_t nread = read(pipefd[0], &result, 1); + close(pipefd[0]); + + int status; + waitpid(pid, &status, 0); + + // If we successfully read the result and it indicates ADDR_NO_RANDOMIZE + // survived exec, proceed with re-exec. Otherwise, skip to avoid infinite loop. + if (nread == 1 && result == 1) { + execv(argv[0], argv); + } + + // Personality doesn't survive exec boundary, skip re-exec. #else return; #endif From 47b38bd6125be8c70874584f414dcb367b14fa00 Mon Sep 17 00:00:00 2001 From: Roman Lebedev Date: Thu, 2 Jul 2026 22:20:57 +0300 Subject: [PATCH 2/4] Update src/benchmark.cc --- src/benchmark.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/benchmark.cc b/src/benchmark.cc index 5e3f5162c..6edc01444 100644 --- a/src/benchmark.cc +++ b/src/benchmark.cc @@ -838,6 +838,7 @@ std::make_unsigned_t get_as_unsigned(T v) { } // end namespace internal void MaybeReenterWithoutASLR(int argc, char** argv) { + (void)argc; // On e.g. Hexagon simulator, argv may be NULL. if (!argv) return; From da0a954dc3c755c0a7c9314d337f05e185a05218 Mon Sep 17 00:00:00 2001 From: anish Date: Thu, 2 Jul 2026 19:50:27 +0000 Subject: [PATCH 3/4] address review feedback: fix CI warning, simplify child argv, avoid heap alloc Signed-off-by: anish --- src/benchmark.cc | 39 ++++++++++++++------------------------- 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/src/benchmark.cc b/src/benchmark.cc index 6edc01444..5a1c29d5a 100644 --- a/src/benchmark.cc +++ b/src/benchmark.cc @@ -838,21 +838,23 @@ std::make_unsigned_t get_as_unsigned(T v) { } // end namespace internal void MaybeReenterWithoutASLR(int argc, char** argv) { - (void)argc; // On e.g. Hexagon simulator, argv may be NULL. if (!argv) return; #ifdef BENCHMARK_OS_LINUX + static constexpr const char kTestChildArg[] = "--benchmark_aslr_test_child="; + static constexpr size_t kTestChildArgLen = sizeof(kTestChildArg) - 1; + // Check if we are a test child process that should report personality and exit for (int i = 1; i < argc; ++i) { - if (std::strncmp(argv[i], "--benchmark_aslr_test_child=", 28) == 0) { - const int write_fd = std::atoi(argv[i] + 28); + if (std::strncmp(argv[i], kTestChildArg, kTestChildArgLen) == 0) { + const int write_fd = std::atoi(argv[i] + kTestChildArgLen); const auto test_personality = personality(0xffffffff); // Write 1 if ADDR_NO_RANDOMIZE is set, 0 otherwise char result = ((test_personality != -1) && (internal::get_as_unsigned(test_personality) & ADDR_NO_RANDOMIZE)) ? 1 : 0; - write(write_fd, &result, 1); + (void)write(write_fd, &result, 1); close(write_fd); std::exit(0); } @@ -901,29 +903,16 @@ void MaybeReenterWithoutASLR(int argc, char** argv) { // Child: prepare to exec with test argument close(pipefd[0]); - // Count existing arguments - int arg_count = 0; - while (argv[arg_count] != nullptr) ++arg_count; - - // Build new argv with test argument (allocate on heap to survive exec) - char** new_argv = static_cast( - malloc(sizeof(char*) * (arg_count + 2))); - if (!new_argv) _exit(1); - - for (int i = 0; i < arg_count; ++i) { - new_argv[i] = argv[i]; - } - - // Add test argument with pipe write fd (allocate on heap) - char* test_arg = static_cast(malloc(64)); - if (!test_arg) _exit(1); - std::snprintf(test_arg, 64, + // Build test argument on stack (safe before exec) + char test_arg[64]; + std::snprintf(test_arg, sizeof(test_arg), "--benchmark_aslr_test_child=%d", pipefd[1]); - new_argv[arg_count] = test_arg; - new_argv[arg_count + 1] = nullptr; - execv(argv[0], new_argv); - // If exec fails, exit (no need to free since we're exiting) + // Simple argv with just the executable and test argument + char* child_argv[] = {argv[0], test_arg, nullptr}; + + execv(argv[0], child_argv); + // If exec fails, exit _exit(1); } From fca823eb2375caafb3ddb8deaf19e19191bad4d7 Mon Sep 17 00:00:00 2001 From: Roman Lebedev Date: Thu, 2 Jul 2026 23:00:16 +0300 Subject: [PATCH 4/4] Apply suggestion from @LebedevRI --- src/benchmark.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/benchmark.cc b/src/benchmark.cc index 5a1c29d5a..a86ef8829 100644 --- a/src/benchmark.cc +++ b/src/benchmark.cc @@ -838,6 +838,7 @@ std::make_unsigned_t get_as_unsigned(T v) { } // end namespace internal void MaybeReenterWithoutASLR(int argc, char** argv) { + (void)argc; // On e.g. Hexagon simulator, argv may be NULL. if (!argv) return;