Skip to content

fix(linux): prevent infinite re-exec loop under AppArmor#2239

Open
anishesg wants to merge 4 commits into
google:mainfrom
proudhare:fix/ph-issue-2184
Open

fix(linux): prevent infinite re-exec loop under AppArmor#2239
anishesg wants to merge 4 commits into
google:mainfrom
proudhare:fix/ph-issue-2184

Conversation

@anishesg

@anishesg anishesg commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

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.

This fix implements a fork-based verification: before committing to re-exec the parent process, we fork a test child, exec it with a special argument, and have it report back whether ADDR_NO_RANDOMIZE actually survived the exec boundary. Only if the child confirms the flag persists do we proceed with re-executing the parent. This prevents infinite loops in environments where the LSM strips the personality flag during exec.

The test child is invoked with --benchmark_aslr_test_child=<fd>, checks its personality after exec, writes the result back through the provided file descriptor, and exits. This approach maintains hermeticity by using process arguments rather than environment variables.

Fixes #2184

The `MaybeReenterWithoutASLR()` function in `src/benchmark.cc` caused infinite execv() loops when running benchmarks under AppArmor-enabled LSMs. The existing fix from google#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 <anishesg@users.noreply.github.com>
Comment thread src/benchmark.cc
Comment thread src/benchmark.cc Outdated
Comment thread src/benchmark.cc Outdated
Comment on lines +907 to +924
// Build new argv with test argument (allocate on heap to survive exec)
char** new_argv = static_cast<char**>(
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<char*>(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);

@LebedevRI LebedevRI Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I haven't seen if ASAN splats on this,
but i really don't think this is allowed.
Can you even touch heap in a child?
Certainly you can't alloc before calling execv?

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.

good catch, switched to stack allocation which is safe before exec

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm not sure any (either stack or heap) is safe before calling execv.
It would only be safe if it copies the actual underlying strings, does it do that?
I'm hoping ASAN will answer this. If this isn't safe, we need to produce new_argv before fork().

@LebedevRI

Copy link
Copy Markdown
Collaborator

Thank you for taking a look!
Can you repro the original issue?

This does roughly look how i imagined it would look.
But i'm not yet confident the fix is safe.

Comment thread src/benchmark.cc Outdated
Comment on lines +903 to +914
// 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<char**>(
malloc(sizeof(char*) * (arg_count + 2)));
if (!new_argv) _exit(1);

for (int i = 0; i < arg_count; ++i) {
new_argv[i] = argv[i];
}

@LebedevRI LebedevRI Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm not sure there is a need to preserve existing arguments,
just the {argv[0], <magic>, nullptr} is enough.

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.

simplified to just {argv[0], magic, nullptr}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Err, i didn't actually mean a stack-based array, just that it only needs to have 3 elements.
(in principle, i'm not sure we need to pass argv[0], either)

LebedevRI and others added 2 commits July 2, 2026 22:20
…eap alloc

Signed-off-by: anish <anishesg@users.noreply.github.com>
@anishesg

anishesg commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

I haven't personally reproduced the original issue reported in #2184, but the reporter confirmed the infinite loop occurs on Ubuntu 24.04 with AppArmor enabled. The fork-based verification approach should be safe - if the personality doesn't survive exec, we just skip re-exec and continue normally.

Comment thread src/benchmark.cc
char result = ((test_personality != -1) &&
(internal::get_as_unsigned(test_personality) & ADDR_NO_RANDOMIZE))
? 1 : 0;
(void)write(write_fd, &result, 1);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks like GCC wants some better check than cast-to-void here.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] MaybeReenterWithoutASLR() infinite loop under AppArmor (not covered by #1984 fix)

2 participants