aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLev Rumyantsev <levarum@google.com>2024-04-18 21:37:21 -0700
committerLev Rumyantsev <levarum@google.com>2024-04-24 21:48:30 -0700
commit792349345448fcd6e832fec89eec8a380e46f18d (patch)
treeb65886d18a433f51188117061d3b5bf5e9d25fed
parent5fe6d56ce2ad491251912db30c958ab39d75aa70 (diff)
downloadbinary_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.h2
-rw-r--r--base/prctl_helpers.cc16
-rw-r--r--kernel_api/open_emulation.cc76
-rw-r--r--kernel_api/sys_mman_emulation.cc15
-rw-r--r--tests/ndk_program_tests/Android.bp4
-rw-r--r--tests/ndk_program_tests/proc_self_maps_test.cc109
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