diff options
author | Lev Rumyantsev <levarum@google.com> | 2024-04-18 21:37:21 -0700 |
---|---|---|
committer | Lev Rumyantsev <levarum@google.com> | 2024-04-24 21:48:30 -0700 |
commit | 792349345448fcd6e832fec89eec8a380e46f18d (patch) | |
tree | b65886d18a433f51188117061d3b5bf5e9d25fed | |
parent | 5fe6d56ce2ad491251912db30c958ab39d75aa70 (diff) | |
download | binary_translation-792349345448fcd6e832fec89eec8a380e46f18d.tar.gz |
kernel_api: Emulate /proc/self/maps
Test: tree-hugger
Bug: 322873334
Change-Id: I94a6bae6aa5b452166162efd93e307cbd19fec78
Merged-In: I94a6bae6aa5b452166162efd93e307cbd19fec78
-rw-r--r-- | base/include/berberis/base/file.h | 2 | ||||
-rw-r--r-- | base/prctl_helpers.cc | 16 | ||||
-rw-r--r-- | kernel_api/open_emulation.cc | 76 | ||||
-rw-r--r-- | kernel_api/sys_mman_emulation.cc | 15 | ||||
-rw-r--r-- | tests/ndk_program_tests/Android.bp | 4 | ||||
-rw-r--r-- | tests/ndk_program_tests/proc_self_maps_test.cc | 109 |
6 files changed, 214 insertions, 8 deletions
diff --git a/base/include/berberis/base/file.h b/base/include/berberis/base/file.h index 6c756d57..92c3d45d 100644 --- a/base/include/berberis/base/file.h +++ b/base/include/berberis/base/file.h @@ -28,6 +28,8 @@ using android::base::Dirname; using android::base::GetExecutableDirectory; +using android::base::ReadFileToString; + } // namespace berberis #endif // BERBERIS_BASE_FILE_H_ diff --git a/base/prctl_helpers.cc b/base/prctl_helpers.cc index 1e0049a6..8c441758 100644 --- a/base/prctl_helpers.cc +++ b/base/prctl_helpers.cc @@ -16,21 +16,21 @@ #include "berberis/base/prctl_helpers.h" -#if defined(__BIONIC__) #include <sys/prctl.h> -#else -#include "berberis/base/macros.h" + +// These are not defined in our glibc, but are supported in kernel since 5.17 (but not necessarily +// enabled). It's always enabled in Android kernerls, but otherwise on Linux may be disabled +// depending on CONFIG_ANON_VMA_NAME boot config flag. So the caller needs to check the result to +// see if it actually worked. +#if defined(__GLIBC__) +#define PR_SET_VMA 0x53564d41 +#define PR_SET_VMA_ANON_NAME 0 #endif namespace berberis { int SetVmaAnonName(void* addr, size_t size, const char* name) { -#if defined(__BIONIC__) return prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, addr, size, name); -#else - UNUSED(addr, size, name); - return 0; -#endif } } // namespace berberis diff --git a/kernel_api/open_emulation.cc b/kernel_api/open_emulation.cc index fee0bf56..483a47b3 100644 --- a/kernel_api/open_emulation.cc +++ b/kernel_api/open_emulation.cc @@ -18,13 +18,89 @@ #include <fcntl.h> +#include <cstdio> +#include <cstring> +#include <string> +#include <vector> + +#include "berberis/base/fd.h" +#include "berberis/base/file.h" +#include "berberis/base/strings.h" +#include "berberis/base/tracing.h" +#include "berberis/guest_os_primitives/guest_map_shadow.h" +#include "berberis/guest_state/guest_addr.h" #include "berberis/kernel_api/main_executable_real_path_emulation.h" namespace berberis { +namespace { + +// It's macro since we use it as string literal below. +#define PROC_SELF_MAPS "/proc/self/maps" + +// Note that dirfd, flags and mode are only used to fallback to +// host's openat in case of failure. +int OpenatProcSelfMapsForGuest(int dirfd, int flags, mode_t mode) { + TRACE("Openat for " PROC_SELF_MAPS); + + std::string file_data; + bool success = ReadFileToString(PROC_SELF_MAPS, &file_data); + if (!success) { + TRACE("Cannot read " PROC_SELF_MAPS ", falling back to host's openat"); + return openat(dirfd, PROC_SELF_MAPS, flags, mode); + } + + int mem_fd = CreateMemfdOrDie("[guest " PROC_SELF_MAPS "]"); + + auto* maps_shadow = GuestMapShadow::GetInstance(); + + std::vector<std::string> lines = Split(file_data, "\n"); + std::string guest_maps; + for (size_t i = 0; i < lines.size(); i++) { + uintptr_t start; + uintptr_t end; + int prot_offset; + if (sscanf(lines.at(i).c_str(), "%" SCNxPTR "-%" SCNxPTR " %n", &start, &end, &prot_offset) != + 2) { + if (!lines[i].empty()) { + TRACE("Cannot parse " PROC_SELF_MAPS " line : %s", lines.at(i).c_str()); + } + guest_maps.append(lines.at(i) + "\n"); + continue; + } + BitValue exec_status = maps_shadow->GetExecutable(GuestAddr(start), end - start); + if (exec_status == kBitMixed) { + // When we strip guest executable bit from host mappings the kernel may merge r-- and r-x + // mappings, resulting in kBitMixed executability state. We are avoiding such merging by + // SetVmaAnonName in MmapForGuest/MprotectForGuest. This isn't strictly guaranteed to work, so + // issue a warning if it doesn't, or if we got kBitMixed for another reason to investigate. + // TODO(b/322873334): Instead split such host mapping into several guest mappings. + TRACE("Unexpected " PROC_SELF_MAPS " mapping with mixed guest executability"); + } + // prot_offset points to "rwxp", so offset of "x" is 2 symbols away. + lines.at(i).at(prot_offset + 2) = (exec_status == kBitSet) ? 'x' : '-'; + + guest_maps.append(lines.at(i) + "\n"); + } + + TRACE("--------\n%s\n--------", guest_maps.c_str()); + + WriteFullyOrDie(mem_fd, guest_maps.c_str(), guest_maps.size()); + + lseek(mem_fd, 0, 0); + + return mem_fd; +} + +} // namespace + int OpenatForGuest(int dirfd, const char* path, int guest_flags, mode_t mode) { int host_flags = ToHostOpenFlags(guest_flags); + if (strcmp(path, PROC_SELF_MAPS) == 0) { + return OpenatProcSelfMapsForGuest(dirfd, host_flags, mode); + } + const char* real_path = nullptr; if ((host_flags & AT_SYMLINK_NOFOLLOW) == 0) { real_path = TryReadLinkToMainExecutableRealPath(path); diff --git a/kernel_api/sys_mman_emulation.cc b/kernel_api/sys_mman_emulation.cc index 61ad88e6..63249efd 100644 --- a/kernel_api/sys_mman_emulation.cc +++ b/kernel_api/sys_mman_emulation.cc @@ -20,6 +20,8 @@ #include <cerrno> +#include "berberis/base/mmap.h" +#include "berberis/base/prctl_helpers.h" #include "berberis/base/tracing.h" #include "berberis/guest_os_primitives/guest_map_shadow.h" #include "berberis/guest_state/guest_addr.h" @@ -36,10 +38,23 @@ int ToHostProt(int guest_prot) { return guest_prot; } +// Clobbers errno. void UpdateGuestProt(int guest_prot, void* addr, size_t length) { GuestAddr guest_addr = ToGuestAddr(addr); GuestMapShadow* shadow = GuestMapShadow::GetInstance(); if (guest_prot & PROT_EXEC) { + // Since we strip guest executable bit from host mappings kernel may merge r-- and r-x guest + // mappings together, which is difficult to split back when emulating /proc/self/maps. Setting + // region name helps to prevent regions merging. It helps even if it's a file backed mapping, + // even though filename isn't visibly changed in /proc/self/maps in this case. + // Note that this name can be overridden by the app, which is fine as long as it's + // unique for this mapping. We do not remove this name if executable bit is + // removed which also should be fine since it's just a hint. + int res = SetVmaAnonName(addr, AlignUpPageSize(length), "[guest exec mapping hint]"); + if (res == -1) { + TRACE("PR_SET_VMA_ANON_NAME failed with errno=%s", std::strerror(errno)); + } + shadow->SetExecutable(guest_addr, length); } else { shadow->ClearExecutable(guest_addr, length); diff --git a/tests/ndk_program_tests/Android.bp b/tests/ndk_program_tests/Android.bp index f7141639..36e95616 100644 --- a/tests/ndk_program_tests/Android.bp +++ b/tests/ndk_program_tests/Android.bp @@ -87,6 +87,7 @@ cc_test { defaults: ["berberis_ndk_program_tests_defaults"], shared_libs: ["libz"], srcs: [ + "proc_self_maps_test.cc", // TODO(b/187471779): Signal stress test is unstable with high number of repetitions. // TODO(b/188086209): Errno emulation is not thread-safe - some checks fail. "signal_stress_test.cc", @@ -202,6 +203,9 @@ cc_test { srcs: [ // TODO(b/187471779): fix for static executable. // "handle_not_executable_test.cc", + // TODO(b/297942688): We do not support accurate exec bit emulation in /proc/self/maps + // on systems without CONFIG_ANON_VMA_NAME. + // "proc_self_maps_test.cc", // TODO(b/187471779): Signal stress test is unstable with high number of repeatitions. // TODO(b/188086209): Errno emulation is not thread-safe - some checks fail. // "signal_stress_test.cc", diff --git a/tests/ndk_program_tests/proc_self_maps_test.cc b/tests/ndk_program_tests/proc_self_maps_test.cc new file mode 100644 index 00000000..78629714 --- /dev/null +++ b/tests/ndk_program_tests/proc_self_maps_test.cc @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "gtest/gtest.h" + +#include <sys/mman.h> +#include <unistd.h> // sysconf(_SC_PAGESIZE) + +#include <cinttypes> +#include <cstdint> +#include <cstdio> +#include <memory> + +namespace { + +constexpr bool kExactMapping = true; + +template <bool kIsExactMapping = false> +bool IsExecutable(void* ptr, size_t size) { + uintptr_t addr = reinterpret_cast<uintptr_t>(ptr); + std::unique_ptr<FILE, decltype(&fclose)> fp(fopen("/proc/self/maps", "re"), fclose); + if (fp == nullptr) { + ADD_FAILURE() << "Cannot open /proc/self/maps"; + return false; + } + + char line[BUFSIZ]; + while (fgets(line, sizeof(line), fp.get()) != nullptr) { + uintptr_t start; + uintptr_t end; + char prot[5]; // sizeof("rwxp") + if (sscanf(line, "%" SCNxPTR "-%" SCNxPTR " %4s", &start, &end, prot) == 3) { + bool is_match; + if constexpr (kIsExactMapping) { + is_match = (addr == start); + if (is_match) { + EXPECT_EQ(start + size, end); + } + } else { + is_match = (addr >= start) && (addr < end); + if (is_match) { + EXPECT_LE(addr + size, end); + } + } + if (is_match) { + return prot[2] == 'x'; + } + } + } + ADD_FAILURE() << "Didn't find address " << reinterpret_cast<void*>(addr) << " in /proc/self/maps"; + return false; +} + +TEST(ProcSelfMaps, ExecutableFromMmap) { + const size_t kPageSize = sysconf(_SC_PAGESIZE); + uint8_t* mapping = reinterpret_cast<uint8_t*>( + mmap(0, 3 * kPageSize, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + ASSERT_NE(mapping, nullptr); + + ASSERT_FALSE(IsExecutable(mapping, 3 * kPageSize)); + + void* exec_mapping = mmap(mapping + kPageSize, + kPageSize, + PROT_READ | PROT_EXEC, + MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS, + -1, + 0); + ASSERT_NE(exec_mapping, nullptr); + + ASSERT_FALSE(IsExecutable(mapping, kPageSize)); + // Surrounding mappings can be merged with adjacent mappings. But this one must match exactly. + ASSERT_TRUE(IsExecutable<kExactMapping>(mapping + kPageSize, kPageSize)); + ASSERT_FALSE(IsExecutable(mapping + 2 * kPageSize, kPageSize)); + + ASSERT_EQ(munmap(mapping, 3 * kPageSize), 0); +} + +TEST(ProcSelfMaps, ExecutableFromMprotect) { + const size_t kPageSize = sysconf(_SC_PAGESIZE); + uint8_t* mapping = reinterpret_cast<uint8_t*>( + mmap(0, 3 * kPageSize, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + ASSERT_NE(mapping, nullptr); + + ASSERT_FALSE(IsExecutable(mapping, 3 * kPageSize)); + + ASSERT_EQ(mprotect(mapping + kPageSize, kPageSize, PROT_READ | PROT_EXEC), 0); + + ASSERT_FALSE(IsExecutable(mapping, kPageSize)); + // Surrounding mappings can be merged with adjacent mappings. But this one must match exactly. + ASSERT_TRUE(IsExecutable<kExactMapping>(mapping + kPageSize, kPageSize)); + ASSERT_FALSE(IsExecutable(mapping + 2 * kPageSize, kPageSize)); + + ASSERT_EQ(munmap(mapping, 3 * kPageSize), 0); +} + +} // namespace |