summaryrefslogtreecommitdiffhomepage
path: root/test/syscalls/linux/proc_pid_smaps.cc
diff options
context:
space:
mode:
authorJamie Liu <jamieliu@google.com>2019-01-07 15:16:37 -0800
committerShentubot <shentubot@google.com>2019-01-07 15:17:44 -0800
commit901ed5da44f1ef28c67ce942eef978342a5f8766 (patch)
treefba169cb810807104aef6ea98060fb6d4bb07bc2 /test/syscalls/linux/proc_pid_smaps.cc
parente44cb43b9c2292239c33fb77ddd6510ef52e1a7a (diff)
Implement /proc/[pid]/smaps.
PiperOrigin-RevId: 228245523 Change-Id: I5a4d0a6570b93958e51437e917e5331d83e23a7e
Diffstat (limited to 'test/syscalls/linux/proc_pid_smaps.cc')
-rw-r--r--test/syscalls/linux/proc_pid_smaps.cc467
1 files changed, 467 insertions, 0 deletions
diff --git a/test/syscalls/linux/proc_pid_smaps.cc b/test/syscalls/linux/proc_pid_smaps.cc
new file mode 100644
index 000000000..4aefc1b41
--- /dev/null
+++ b/test/syscalls/linux/proc_pid_smaps.cc
@@ -0,0 +1,467 @@
+// Copyright 2019 Google LLC
+//
+// 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 <stddef.h>
+#include <stdint.h>
+
+#include <algorithm>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "absl/container/flat_hash_set.h"
+#include "absl/strings/str_cat.h"
+#include "absl/strings/str_format.h"
+#include "absl/strings/str_split.h"
+#include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
+#include "test/util/file_descriptor.h"
+#include "test/util/fs_util.h"
+#include "test/util/memory_util.h"
+#include "test/util/posix_error.h"
+#include "test/util/proc_util.h"
+#include "test/util/temp_path.h"
+#include "test/util/test_util.h"
+
+using ::testing::Contains;
+using ::testing::ElementsAreArray;
+using ::testing::IsSupersetOf;
+using ::testing::Not;
+using ::testing::Optional;
+
+namespace gvisor {
+namespace testing {
+
+namespace {
+
+struct ProcPidSmapsEntry {
+ ProcMapsEntry maps_entry;
+
+ // These fields should always exist, as they were included in e070ad49f311
+ // "[PATCH] add /proc/pid/smaps".
+ size_t size_kb;
+ size_t rss_kb;
+ size_t shared_clean_kb;
+ size_t shared_dirty_kb;
+ size_t private_clean_kb;
+ size_t private_dirty_kb;
+
+ // These fields were added later and may not be present.
+ absl::optional<size_t> pss_kb;
+ absl::optional<size_t> referenced_kb;
+ absl::optional<size_t> anonymous_kb;
+ absl::optional<size_t> anon_huge_pages_kb;
+ absl::optional<size_t> shared_hugetlb_kb;
+ absl::optional<size_t> private_hugetlb_kb;
+ absl::optional<size_t> swap_kb;
+ absl::optional<size_t> swap_pss_kb;
+ absl::optional<size_t> kernel_page_size_kb;
+ absl::optional<size_t> mmu_page_size_kb;
+ absl::optional<size_t> locked_kb;
+
+ // Caution: "Note that there is no guarantee that every flag and associated
+ // mnemonic will be present in all further kernel releases. Things get
+ // changed, the flags may be vanished or the reverse -- new added." - Linux
+ // Documentation/filesystems/proc.txt, on VmFlags. Avoid checking for any
+ // flags that are not extremely well-established.
+ absl::optional<std::vector<std::string>> vm_flags;
+};
+
+// Given the value part of a /proc/[pid]/smaps field containing a value in kB
+// (for example, " 4 kB", returns the value in kB (in this example, 4).
+PosixErrorOr<size_t> SmapsValueKb(absl::string_view value) {
+ // TODO: let us use RE2 or <regex>
+ std::pair<absl::string_view, absl::string_view> parts =
+ absl::StrSplit(value, ' ', absl::SkipEmpty());
+ if (parts.second != "kB") {
+ return PosixError(EINVAL,
+ absl::StrCat("invalid smaps field value: ", value));
+ }
+ ASSIGN_OR_RETURN_ERRNO(auto val_kb, Atoi<size_t>(parts.first));
+ return val_kb;
+}
+
+PosixErrorOr<std::vector<ProcPidSmapsEntry>> ParseProcPidSmaps(
+ absl::string_view contents) {
+ std::vector<ProcPidSmapsEntry> entries;
+ absl::optional<ProcPidSmapsEntry> entry;
+ bool have_size_kb = false;
+ bool have_rss_kb = false;
+ bool have_shared_clean_kb = false;
+ bool have_shared_dirty_kb = false;
+ bool have_private_clean_kb = false;
+ bool have_private_dirty_kb = false;
+
+ auto const finish_entry = [&] {
+ if (entry) {
+ if (!have_size_kb) {
+ return PosixError(EINVAL, "smaps entry is missing Size");
+ }
+ if (!have_rss_kb) {
+ return PosixError(EINVAL, "smaps entry is missing Rss");
+ }
+ if (!have_shared_clean_kb) {
+ return PosixError(EINVAL, "smaps entry is missing Shared_Clean");
+ }
+ if (!have_shared_dirty_kb) {
+ return PosixError(EINVAL, "smaps entry is missing Shared_Dirty");
+ }
+ if (!have_private_clean_kb) {
+ return PosixError(EINVAL, "smaps entry is missing Private_Clean");
+ }
+ if (!have_private_dirty_kb) {
+ return PosixError(EINVAL, "smaps entry is missing Private_Dirty");
+ }
+ // std::move(entry.value()) instead of std::move(entry).value(), because
+ // otherwise tools may report a "use-after-move" warning, which is
+ // spurious because entry.emplace() below resets entry to a new
+ // ProcPidSmapsEntry.
+ entries.emplace_back(std::move(entry.value()));
+ }
+ entry.emplace();
+ have_size_kb = false;
+ have_rss_kb = false;
+ have_shared_clean_kb = false;
+ have_shared_dirty_kb = false;
+ have_private_clean_kb = false;
+ have_private_dirty_kb = false;
+ return NoError();
+ };
+
+ // Holds key/value pairs from smaps field lines. Declared here so it can be
+ // captured by reference by the following lambdas.
+ std::vector<absl::string_view> key_value;
+
+ auto const on_required_field_kb = [&](size_t* field, bool* have_field) {
+ if (*have_field) {
+ return PosixError(
+ EINVAL,
+ absl::StrFormat("smaps entry has duplicate %s line", key_value[0]));
+ }
+ ASSIGN_OR_RETURN_ERRNO(*field, SmapsValueKb(key_value[1]));
+ *have_field = true;
+ return NoError();
+ };
+
+ auto const on_optional_field_kb = [&](absl::optional<size_t>* field) {
+ if (*field) {
+ return PosixError(
+ EINVAL,
+ absl::StrFormat("smaps entry has duplicate %s line", key_value[0]));
+ }
+ ASSIGN_OR_RETURN_ERRNO(*field, SmapsValueKb(key_value[1]));
+ return NoError();
+ };
+
+ absl::flat_hash_set<std::string> unknown_fields;
+ auto const on_unknown_field = [&] {
+ absl::string_view key = key_value[0];
+ // Don't mention unknown fields more than once.
+ if (unknown_fields.count(key)) {
+ return;
+ }
+ unknown_fields.insert(std::string(key));
+ LOG(INFO) << "skipping unknown smaps field " << key;
+ };
+
+ auto lines = absl::StrSplit(contents, '\n', absl::SkipEmpty());
+ for (absl::string_view l : lines) {
+ // Is this line a valid /proc/[pid]/maps entry?
+ auto maybe_maps_entry = ParseProcMapsLine(l);
+ if (maybe_maps_entry.ok()) {
+ // This marks the beginning of a new /proc/[pid]/smaps entry.
+ RETURN_IF_ERRNO(finish_entry());
+ entry->maps_entry = std::move(maybe_maps_entry).ValueOrDie();
+ continue;
+ }
+ // Otherwise it's a field in an existing /proc/[pid]/smaps entry of the form
+ // "key:value" (where value in practice will be preceded by a variable
+ // amount of whitespace).
+ if (!entry) {
+ LOG(WARNING) << "smaps line not considered a maps line: "
+ << maybe_maps_entry.error_message();
+ return PosixError(
+ EINVAL,
+ absl::StrCat("smaps field line without preceding maps line: ", l));
+ }
+ key_value = absl::StrSplit(l, absl::MaxSplits(':', 1));
+ if (key_value.size() != 2) {
+ return PosixError(EINVAL, absl::StrCat("invalid smaps field line: ", l));
+ }
+ absl::string_view const key = key_value[0];
+ if (key == "Size") {
+ RETURN_IF_ERRNO(on_required_field_kb(&entry->size_kb, &have_size_kb));
+ } else if (key == "Rss") {
+ RETURN_IF_ERRNO(on_required_field_kb(&entry->rss_kb, &have_rss_kb));
+ } else if (key == "Shared_Clean") {
+ RETURN_IF_ERRNO(
+ on_required_field_kb(&entry->shared_clean_kb, &have_shared_clean_kb));
+ } else if (key == "Shared_Dirty") {
+ RETURN_IF_ERRNO(
+ on_required_field_kb(&entry->shared_dirty_kb, &have_shared_dirty_kb));
+ } else if (key == "Private_Clean") {
+ RETURN_IF_ERRNO(on_required_field_kb(&entry->private_clean_kb,
+ &have_private_clean_kb));
+ } else if (key == "Private_Dirty") {
+ RETURN_IF_ERRNO(on_required_field_kb(&entry->private_dirty_kb,
+ &have_private_dirty_kb));
+ } else if (key == "Pss") {
+ RETURN_IF_ERRNO(on_optional_field_kb(&entry->pss_kb));
+ } else if (key == "Referenced") {
+ RETURN_IF_ERRNO(on_optional_field_kb(&entry->referenced_kb));
+ } else if (key == "Anonymous") {
+ RETURN_IF_ERRNO(on_optional_field_kb(&entry->anonymous_kb));
+ } else if (key == "AnonHugePages") {
+ RETURN_IF_ERRNO(on_optional_field_kb(&entry->anon_huge_pages_kb));
+ } else if (key == "Shared_Hugetlb") {
+ RETURN_IF_ERRNO(on_optional_field_kb(&entry->shared_hugetlb_kb));
+ } else if (key == "Private_Hugetlb") {
+ RETURN_IF_ERRNO(on_optional_field_kb(&entry->private_hugetlb_kb));
+ } else if (key == "Swap") {
+ RETURN_IF_ERRNO(on_optional_field_kb(&entry->swap_kb));
+ } else if (key == "SwapPss") {
+ RETURN_IF_ERRNO(on_optional_field_kb(&entry->swap_pss_kb));
+ } else if (key == "KernelPageSize") {
+ RETURN_IF_ERRNO(on_optional_field_kb(&entry->kernel_page_size_kb));
+ } else if (key == "MMUPageSize") {
+ RETURN_IF_ERRNO(on_optional_field_kb(&entry->mmu_page_size_kb));
+ } else if (key == "Locked") {
+ RETURN_IF_ERRNO(on_optional_field_kb(&entry->locked_kb));
+ } else if (key == "VmFlags") {
+ if (entry->vm_flags) {
+ return PosixError(EINVAL, "duplicate VmFlags line");
+ }
+ entry->vm_flags = absl::StrSplit(key_value[1], ' ', absl::SkipEmpty());
+ } else {
+ on_unknown_field();
+ }
+ }
+ RETURN_IF_ERRNO(finish_entry());
+ return entries;
+};
+
+TEST(ParseProcPidSmapsTest, Correctness) {
+ auto entries = ASSERT_NO_ERRNO_AND_VALUE(
+ ParseProcPidSmaps("0-10000 rw-s 00000000 00:00 0 "
+ " /dev/zero (deleted)\n"
+ "Size: 0 kB\n"
+ "Rss: 1 kB\n"
+ "Pss: 2 kB\n"
+ "Shared_Clean: 3 kB\n"
+ "Shared_Dirty: 4 kB\n"
+ "Private_Clean: 5 kB\n"
+ "Private_Dirty: 6 kB\n"
+ "Referenced: 7 kB\n"
+ "Anonymous: 8 kB\n"
+ "AnonHugePages: 9 kB\n"
+ "Shared_Hugetlb: 10 kB\n"
+ "Private_Hugetlb: 11 kB\n"
+ "Swap: 12 kB\n"
+ "SwapPss: 13 kB\n"
+ "KernelPageSize: 14 kB\n"
+ "MMUPageSize: 15 kB\n"
+ "Locked: 16 kB\n"
+ "FutureUnknownKey: 17 kB\n"
+ "VmFlags: rd wr sh mr mw me ms lo ?? sd \n"));
+ ASSERT_EQ(entries.size(), 1);
+ auto& entry = entries[0];
+ EXPECT_EQ(entry.maps_entry.filename, "/dev/zero (deleted)");
+ EXPECT_EQ(entry.size_kb, 0);
+ EXPECT_EQ(entry.rss_kb, 1);
+ EXPECT_THAT(entry.pss_kb, Optional(2));
+ EXPECT_EQ(entry.shared_clean_kb, 3);
+ EXPECT_EQ(entry.shared_dirty_kb, 4);
+ EXPECT_EQ(entry.private_clean_kb, 5);
+ EXPECT_EQ(entry.private_dirty_kb, 6);
+ EXPECT_THAT(entry.referenced_kb, Optional(7));
+ EXPECT_THAT(entry.anonymous_kb, Optional(8));
+ EXPECT_THAT(entry.anon_huge_pages_kb, Optional(9));
+ EXPECT_THAT(entry.shared_hugetlb_kb, Optional(10));
+ EXPECT_THAT(entry.private_hugetlb_kb, Optional(11));
+ EXPECT_THAT(entry.swap_kb, Optional(12));
+ EXPECT_THAT(entry.swap_pss_kb, Optional(13));
+ EXPECT_THAT(entry.kernel_page_size_kb, Optional(14));
+ EXPECT_THAT(entry.mmu_page_size_kb, Optional(15));
+ EXPECT_THAT(entry.locked_kb, Optional(16));
+ EXPECT_THAT(entry.vm_flags,
+ Optional(ElementsAreArray({"rd", "wr", "sh", "mr", "mw", "me",
+ "ms", "lo", "??", "sd"})));
+}
+
+// Returns the unique entry in entries containing the given address.
+PosixErrorOr<ProcPidSmapsEntry> FindUniqueSmapsEntry(
+ std::vector<ProcPidSmapsEntry> const& entries, uintptr_t addr) {
+ auto const pred = [&](ProcPidSmapsEntry const& entry) {
+ return entry.maps_entry.start <= addr && addr < entry.maps_entry.end;
+ };
+ auto const it = std::find_if(entries.begin(), entries.end(), pred);
+ if (it == entries.end()) {
+ return PosixError(EINVAL,
+ absl::StrFormat("no entry contains address %#x", addr));
+ }
+ auto const it2 = std::find_if(it + 1, entries.end(), pred);
+ if (it2 != entries.end()) {
+ return PosixError(
+ EINVAL,
+ absl::StrFormat("overlapping entries [%#x-%#x) and [%#x-%#x) both "
+ "contain address %#x",
+ it->maps_entry.start, it->maps_entry.end,
+ it2->maps_entry.start, it2->maps_entry.end, addr));
+ }
+ return *it;
+}
+
+PosixErrorOr<std::vector<ProcPidSmapsEntry>> ReadProcSelfSmaps() {
+ ASSIGN_OR_RETURN_ERRNO(std::string contents, GetContents("/proc/self/smaps"));
+ return ParseProcPidSmaps(contents);
+}
+
+TEST(ProcPidSmapsTest, SharedAnon) {
+ // Map with MAP_POPULATE so we get some RSS.
+ Mapping const m = ASSERT_NO_ERRNO_AND_VALUE(MmapAnon(
+ 2 * kPageSize, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE));
+ auto const entries = ASSERT_NO_ERRNO_AND_VALUE(ReadProcSelfSmaps());
+ auto const entry =
+ ASSERT_NO_ERRNO_AND_VALUE(FindUniqueSmapsEntry(entries, m.addr()));
+
+ EXPECT_EQ(entry.size_kb, m.len() / 1024);
+ // It's possible that populated pages have been swapped out, so RSS might be
+ // less than size.
+ EXPECT_LE(entry.rss_kb, entry.size_kb);
+
+ if (entry.pss_kb) {
+ // PSS should be exactly equal to RSS since no other address spaces should
+ // be sharing our new mapping.
+ EXPECT_EQ(entry.pss_kb.value(), entry.rss_kb);
+ }
+
+ // "Shared" and "private" in smaps refers to whether or not *physical pages*
+ // are shared; thus all pages in our MAP_SHARED mapping should nevertheless
+ // be private.
+ EXPECT_EQ(entry.shared_clean_kb, 0);
+ EXPECT_EQ(entry.shared_dirty_kb, 0);
+ EXPECT_EQ(entry.private_clean_kb + entry.private_dirty_kb, entry.rss_kb)
+ << "Private_Clean = " << entry.private_clean_kb
+ << " kB, Private_Dirty = " << entry.private_dirty_kb << " kB";
+
+ // Shared anonymous mappings are implemented as a shmem file, so their pages
+ // are not PageAnon.
+ if (entry.anonymous_kb) {
+ EXPECT_EQ(entry.anonymous_kb.value(), 0);
+ }
+
+ if (entry.vm_flags) {
+ EXPECT_THAT(entry.vm_flags.value(),
+ IsSupersetOf({"rd", "wr", "sh", "mr", "mw", "me", "ms"}));
+ EXPECT_THAT(entry.vm_flags.value(), Not(Contains("ex")));
+ }
+}
+
+TEST(ProcPidSmapsTest, PrivateAnon) {
+ // Map with MAP_POPULATE so we get some RSS.
+ Mapping const m = ASSERT_NO_ERRNO_AND_VALUE(
+ MmapAnon(2 * kPageSize, PROT_WRITE, MAP_PRIVATE | MAP_POPULATE));
+ auto const entries = ASSERT_NO_ERRNO_AND_VALUE(ReadProcSelfSmaps());
+ auto const entry =
+ ASSERT_NO_ERRNO_AND_VALUE(FindUniqueSmapsEntry(entries, m.addr()));
+
+ // It's possible that our mapping was merged with another vma, so the smaps
+ // entry might be bigger than our original mapping.
+ EXPECT_GE(entry.size_kb, m.len() / 1024);
+ EXPECT_LE(entry.rss_kb, entry.size_kb);
+ if (entry.pss_kb) {
+ EXPECT_LE(entry.pss_kb.value(), entry.rss_kb);
+ }
+
+ if (entry.anonymous_kb) {
+ EXPECT_EQ(entry.anonymous_kb.value(), entry.rss_kb);
+ }
+
+ if (entry.vm_flags) {
+ EXPECT_THAT(entry.vm_flags.value(), IsSupersetOf({"wr", "mr", "mw", "me"}));
+ // We passed PROT_WRITE to mmap. On at least x86, the mapping is in
+ // practice readable because there is no way to configure the MMU to make
+ // pages writable but not readable. However, VmFlags should reflect the
+ // flags set on the VMA, so "rd" (VM_READ) should not appear in VmFlags.
+ EXPECT_THAT(entry.vm_flags.value(), Not(Contains("rd")));
+ EXPECT_THAT(entry.vm_flags.value(), Not(Contains("ex")));
+ EXPECT_THAT(entry.vm_flags.value(), Not(Contains("sh")));
+ EXPECT_THAT(entry.vm_flags.value(), Not(Contains("ms")));
+ }
+}
+
+TEST(ProcPidSmapsTest, SharedReadOnlyFile) {
+ size_t const kFileSize = kPageSize;
+
+ auto const temp_file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ ASSERT_THAT(truncate(temp_file.path().c_str(), kFileSize), SyscallSucceeds());
+ auto const fd = ASSERT_NO_ERRNO_AND_VALUE(Open(temp_file.path(), O_RDONLY));
+
+ auto const m = ASSERT_NO_ERRNO_AND_VALUE(Mmap(
+ nullptr, kFileSize, PROT_READ, MAP_SHARED | MAP_POPULATE, fd.get(), 0));
+ auto const entries = ASSERT_NO_ERRNO_AND_VALUE(ReadProcSelfSmaps());
+ auto const entry =
+ ASSERT_NO_ERRNO_AND_VALUE(FindUniqueSmapsEntry(entries, m.addr()));
+
+ // Most of the same logic as the SharedAnon case applies.
+ EXPECT_EQ(entry.size_kb, kFileSize / 1024);
+ EXPECT_LE(entry.rss_kb, entry.size_kb);
+ if (entry.pss_kb) {
+ EXPECT_EQ(entry.pss_kb.value(), entry.rss_kb);
+ }
+ EXPECT_EQ(entry.shared_clean_kb, 0);
+ EXPECT_EQ(entry.shared_dirty_kb, 0);
+ EXPECT_EQ(entry.private_clean_kb + entry.private_dirty_kb, entry.rss_kb)
+ << "Private_Clean = " << entry.private_clean_kb
+ << " kB, Private_Dirty = " << entry.private_dirty_kb << " kB";
+ if (entry.anonymous_kb) {
+ EXPECT_EQ(entry.anonymous_kb.value(), 0);
+ }
+
+ if (entry.vm_flags) {
+ EXPECT_THAT(entry.vm_flags.value(), IsSupersetOf({"rd", "mr", "me", "ms"}));
+ EXPECT_THAT(entry.vm_flags.value(), Not(Contains("wr")));
+ EXPECT_THAT(entry.vm_flags.value(), Not(Contains("ex")));
+ // Because the mapped file was opened O_RDONLY, the VMA is !VM_MAYWRITE and
+ // also !VM_SHARED.
+ EXPECT_THAT(entry.vm_flags.value(), Not(Contains("sh")));
+ EXPECT_THAT(entry.vm_flags.value(), Not(Contains("mw")));
+ }
+}
+
+// Tests that gVisor's /proc/[pid]/smaps provides all of the fields we expect it
+// to, which as of this writing is all fields provided by Linux 4.4.
+TEST(ProcPidSmapsTest, GvisorFields) {
+ SKIP_IF(!IsRunningOnGvisor());
+ auto const entries = ASSERT_NO_ERRNO_AND_VALUE(ReadProcSelfSmaps());
+ for (auto const& entry : entries) {
+ EXPECT_TRUE(entry.pss_kb);
+ EXPECT_TRUE(entry.referenced_kb);
+ EXPECT_TRUE(entry.anonymous_kb);
+ EXPECT_TRUE(entry.anon_huge_pages_kb);
+ EXPECT_TRUE(entry.shared_hugetlb_kb);
+ EXPECT_TRUE(entry.private_hugetlb_kb);
+ EXPECT_TRUE(entry.swap_kb);
+ EXPECT_TRUE(entry.swap_pss_kb);
+ EXPECT_THAT(entry.kernel_page_size_kb, Optional(kPageSize / 1024));
+ EXPECT_THAT(entry.mmu_page_size_kb, Optional(kPageSize / 1024));
+ EXPECT_TRUE(entry.locked_kb);
+ EXPECT_TRUE(entry.vm_flags);
+ }
+}
+
+} // namespace
+
+} // namespace testing
+} // namespace gvisor