From b4a05ce65b2257786b2e48300cdb07f425194196 Mon Sep 17 00:00:00 2001 From: Josh Seba Date: Mon, 18 Jul 2022 14:07:45 -0700 Subject: [PATCH 1/3] Add support for /proc/[pid]/limits --- pkg/sentry/fsimpl/proc/task.go | 1 + pkg/sentry/fsimpl/proc/task_files.go | 37 +++++++++++ pkg/sentry/fsimpl/proc/tasks_test.go | 1 + pkg/sentry/limits/limits.go | 97 ++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+) diff --git a/pkg/sentry/fsimpl/proc/task.go b/pkg/sentry/fsimpl/proc/task.go index 33111c003..a037037c0 100644 --- a/pkg/sentry/fsimpl/proc/task.go +++ b/pkg/sentry/fsimpl/proc/task.go @@ -64,6 +64,7 @@ func (fs *filesystem) newTaskInode(ctx context.Context, task *kernel.Task, pidns "fdinfo": fs.newFDInfoDirInode(ctx, task), "gid_map": fs.newTaskOwnedInode(ctx, task, fs.NextIno(), 0644, &idMapData{task: task, gids: true}), "io": fs.newTaskOwnedInode(ctx, task, fs.NextIno(), 0400, newIO(task, isThreadGroup)), + "limits": fs.newTaskOwnedInode(ctx, task, fs.NextIno(), 0444, &limitsData{task: task}), "maps": fs.newTaskOwnedInode(ctx, task, fs.NextIno(), 0444, &mapsData{task: task}), "mem": fs.newMemInode(ctx, task, fs.NextIno(), 0400), "mountinfo": fs.newTaskOwnedInode(ctx, task, fs.NextIno(), 0444, &mountInfoData{fs: fs, task: task}), diff --git a/pkg/sentry/fsimpl/proc/task_files.go b/pkg/sentry/fsimpl/proc/task_files.go index 156c420ba..c2794b5fe 100644 --- a/pkg/sentry/fsimpl/proc/task_files.go +++ b/pkg/sentry/fsimpl/proc/task_files.go @@ -529,6 +529,43 @@ func (fd *memFD) SetStat(context.Context, vfs.SetStatOptions) error { // Release implements vfs.FileDescriptionImpl.Release. func (fd *memFD) Release(context.Context) {} +// limitsData implements vfs.DynamicBytesSource for /proc/[pid]/limits. +// +// +stateify savable +type limitsData struct { + kernfs.DynamicBytesFile + + task *kernel.Task +} + +func (d *limitsData) Generate(ctx context.Context, buf *bytes.Buffer) error { + // formatting matches the kernel output from linux/fs/proc/base.c:proc_pid_limits() + fmt.Fprintf(buf, "Limit Soft Limit Hard Limit Units \n") + for _, lt := range limits.AllLimitTypes { + fmt.Fprintf(buf, "%-25s ", lt.Name()) + + l := d.task.Limits().Get(lt) + if l.Cur == limits.Infinity { + fmt.Fprintf(buf, "%-20s ", "unlimited") + } else { + fmt.Fprintf(buf, "%-20d ", l.Cur) + } + + if l.Max == limits.Infinity { + fmt.Fprintf(buf, "%-20s ", "unlimited") + } else { + fmt.Fprintf(buf, "%-20d ", l.Max) + } + + if u := lt.Unit(); u != "" { + fmt.Fprintf(buf, "%-10s", u) + } + + buf.WriteByte('\n') + } + return nil +} + // mapsData implements vfs.DynamicBytesSource for /proc/[pid]/maps. // // +stateify savable diff --git a/pkg/sentry/fsimpl/proc/tasks_test.go b/pkg/sentry/fsimpl/proc/tasks_test.go index d4cc2c507..6d0276670 100644 --- a/pkg/sentry/fsimpl/proc/tasks_test.go +++ b/pkg/sentry/fsimpl/proc/tasks_test.go @@ -78,6 +78,7 @@ var ( "fdinfo": linux.DT_DIR, "gid_map": linux.DT_REG, "io": linux.DT_REG, + "limits": linux.DT_REG, "maps": linux.DT_REG, "mem": linux.DT_REG, "mountinfo": linux.DT_REG, diff --git a/pkg/sentry/limits/limits.go b/pkg/sentry/limits/limits.go index 6dd726e02..c8a47fc31 100644 --- a/pkg/sentry/limits/limits.go +++ b/pkg/sentry/limits/limits.go @@ -43,6 +43,103 @@ const ( Rttime ) +var AllLimitTypes = []LimitType{ + CPU, + FileSize, + Data, + Stack, + Core, + Rss, + ProcessCount, + NumberOfFiles, + MemoryLocked, + AS, + Locks, + SignalsPending, + MessageQueueBytes, + Nice, + RealTimePriority, + Rttime, +} + +// Name returns the kernel name of the limit +func (lt LimitType) Name() string { + switch lt { + case CPU: + return "Max cpu time" + case FileSize: + return "Max file size" + case Data: + return "Max data size" + case Stack: + return "Max stack size" + case Core: + return "Max core file size" + case Rss: + return "Max resident set" + case ProcessCount: + return "Max processes" + case NumberOfFiles: + return "Max open files" + case MemoryLocked: + return "Max locked memory" + case AS: + return "Max address space" + case Locks: + return "Max file locks" + case SignalsPending: + return "Max pending signals" + case MessageQueueBytes: + return "Max msgqueue size" + case Nice: + return "Max nice priority" + case RealTimePriority: + return "Max realtime priority" + case Rttime: + return "Max realtime timeout" + } + return "unknown" +} + +// Unit returns the unit string for a limit +func (lt LimitType) Unit() string { + switch lt { + case CPU: + return "seconds" + case FileSize: + return "bytes" + case Data: + return "bytes" + case Stack: + return "bytes" + case Core: + return "bytes" + case Rss: + return "bytes" + case ProcessCount: + return "processes" + case NumberOfFiles: + return "files" + case MemoryLocked: + return "bytes" + case AS: + return "bytes" + case Locks: + return "locks" + case SignalsPending: + return "signals" + case MessageQueueBytes: + return "bytes" + case Nice: + return "" + case RealTimePriority: + return "" + case Rttime: + return "us" + } + return "" +} + // Infinity is a constant representing a resource with no limit. const Infinity = ^uint64(0) From db0dfd6a3f9da26154ade878ceb265080b34af24 Mon Sep 17 00:00:00 2001 From: Josh Seba Date: Mon, 12 Dec 2022 17:13:27 -0800 Subject: [PATCH 2/3] Get limits only once instead of each loop iteration --- pkg/sentry/fsimpl/proc/task_files.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/sentry/fsimpl/proc/task_files.go b/pkg/sentry/fsimpl/proc/task_files.go index c2794b5fe..8baaa44ed 100644 --- a/pkg/sentry/fsimpl/proc/task_files.go +++ b/pkg/sentry/fsimpl/proc/task_files.go @@ -539,12 +539,13 @@ type limitsData struct { } func (d *limitsData) Generate(ctx context.Context, buf *bytes.Buffer) error { + taskLimits := d.task.Limits() // formatting matches the kernel output from linux/fs/proc/base.c:proc_pid_limits() fmt.Fprintf(buf, "Limit Soft Limit Hard Limit Units \n") for _, lt := range limits.AllLimitTypes { fmt.Fprintf(buf, "%-25s ", lt.Name()) - l := d.task.Limits().Get(lt) + l := taskLimits.Get(lt) if l.Cur == limits.Infinity { fmt.Fprintf(buf, "%-20s ", "unlimited") } else { From 90873a990380be6c1d64498dde1020f01fb69971 Mon Sep 17 00:00:00 2001 From: Josh Seba Date: Wed, 14 Dec 2022 11:39:47 -0800 Subject: [PATCH 3/3] Add simple unit test checking limits --- test/syscalls/linux/BUILD | 1 + test/syscalls/linux/rlimits.cc | 27 ++++++ test/util/proc_util.cc | 153 +++++++++++++++++++++++++++++++++ test/util/proc_util.h | 43 +++++++++ 4 files changed, 224 insertions(+) diff --git a/test/syscalls/linux/BUILD b/test/syscalls/linux/BUILD index 12f44de26..a03979a49 100644 --- a/test/syscalls/linux/BUILD +++ b/test/syscalls/linux/BUILD @@ -2139,6 +2139,7 @@ cc_binary( linkstatic = 1, deps = [ "//test/util:capability_util", + "//test/util:proc_util", "//test/util:test_main", "//test/util:test_util", "//test/util:thread_util", diff --git a/test/syscalls/linux/rlimits.cc b/test/syscalls/linux/rlimits.cc index 13d15a529..323eb8ee4 100644 --- a/test/syscalls/linux/rlimits.cc +++ b/test/syscalls/linux/rlimits.cc @@ -21,8 +21,14 @@ #include #include +#include +#include +#include +#include "absl/strings/numbers.h" +#include "absl/strings/str_split.h" #include "test/util/capability_util.h" +#include "test/util/proc_util.h" #include "test/util/test_util.h" #include "test/util/thread_util.h" @@ -31,6 +37,21 @@ namespace testing { namespace { +PosixErrorOr GetProcLimitEntryByType(LimitType limit_type) { + ASSIGN_OR_RETURN_ERRNO(std::string proc_self_limits, + GetContents("/proc/self/limits")); + ASSIGN_OR_RETURN_ERRNO(auto entries, ParseProcLimits(proc_self_limits)); + auto it = + std::find_if(entries.begin(), entries.end(), + [limit_type](const ProcLimitsEntry& v) { + return v.limit_type == limit_type; + }); + if (it == entries.end()) { + return PosixError(ENOENT, "limit type not found"); + } + return *it; +} + TEST(RlimitTest, SetRlimitHigher) { SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_RESOURCE))); @@ -43,6 +64,12 @@ TEST(RlimitTest, SetRlimitHigher) { rl.rlim_max--; ASSERT_THAT(setrlimit(RLIMIT_NOFILE, &rl), SyscallSucceeds()); + // Now verify we can read the changed values via /proc/self/limits + const ProcLimitsEntry limit_entry = + ASSERT_NO_ERRNO_AND_VALUE(GetProcLimitEntryByType(LimitType::NumberOfFiles)); + EXPECT_EQ(rl.rlim_cur, limit_entry.cur_limit); + EXPECT_EQ(rl.rlim_max, limit_entry.max_limit); + rl.rlim_max++; EXPECT_THAT(setrlimit(RLIMIT_NOFILE, &rl), SyscallSucceeds()); } diff --git a/test/util/proc_util.cc b/test/util/proc_util.cc index 132c53953..6304744ec 100644 --- a/test/util/proc_util.cc +++ b/test/util/proc_util.cc @@ -331,5 +331,158 @@ bool IsTHPDisabled() { return StackTHPDisabled(maps.ValueOrDie()); } +PosixErrorOr ParseProcLimitsLine(absl::string_view line) { + ProcLimitsEntry limits_entry = {}; + + std::vector parts = + absl::StrSplit(line.substr(25), ' ', absl::SkipWhitespace()); + + // should have 3 parts (soft, hard, units) + // the name is ignored since the whole line is space separated and + // the name has spaces in but not a consistent number of spaces per + // name (e.g. 'Max cpu time' vs 'Max processes') + // however, since units are optional, ignore them as well + if (parts.size() < 2 || parts.size() > 3) { + return PosixError(EINVAL, absl::StrCat("Invalid line: ", line)); + } + + // parse the limit type + auto limitType = line.substr(0,25); + if (absl::StrContains(limitType, "cpu time")) { + limits_entry.limit_type = LimitType::CPU; + } else if (absl::StrContains(limitType, "file size")) { + limits_entry.limit_type = LimitType::FileSize; + } else if (absl::StrContains(limitType, "data size")) { + limits_entry.limit_type = LimitType::Data; + } else if (absl::StrContains(limitType, "stack size")) { + limits_entry.limit_type = LimitType::Stack; + } else if (absl::StrContains(limitType, "core file size")) { + limits_entry.limit_type = LimitType::Core; + } else if (absl::StrContains(limitType, "resident set")) { + limits_entry.limit_type = LimitType::RSS; + } else if (absl::StrContains(limitType, "processes")) { + limits_entry.limit_type = LimitType::ProcessCount; + } else if (absl::StrContains(limitType, "open files")) { + limits_entry.limit_type = LimitType::NumberOfFiles; + } else if (absl::StrContains(limitType, "locked memory")) { + limits_entry.limit_type = LimitType::MemoryLocked; + } else if (absl::StrContains(limitType, "address space")) { + limits_entry.limit_type = LimitType::AS; + } else if (absl::StrContains(limitType, "file locks")) { + limits_entry.limit_type = LimitType::Locks; + } else if (absl::StrContains(limitType, "pending signals")) { + limits_entry.limit_type = LimitType::SignalsPending; + } else if (absl::StrContains(limitType, "msgqueue size")) { + limits_entry.limit_type = LimitType::MessageQueueBytes; + } else if (absl::StrContains(limitType, "nice priority")) { + limits_entry.limit_type = LimitType::Nice; + } else if (absl::StrContains(limitType, "realtime priority")) { + limits_entry.limit_type = LimitType::RealTimePriority; + } else if (absl::StrContains(limitType, "realtime timeout")) { + limits_entry.limit_type = LimitType::Rttime; + } else { + return PosixError(EINVAL, absl::StrCat("Invalid limit type: ", limitType)); + } + + // parse soft limit + if (parts[0] == "unlimited") { + limits_entry.cur_limit = ~0ULL; + } else { + ASSIGN_OR_RETURN_ERRNO(limits_entry.cur_limit, Atoi(parts[0])); + } + + // parse hard limit + if (parts[1] == "unlimited") { + limits_entry.max_limit = ~0ULL; + } else { + ASSIGN_OR_RETURN_ERRNO(limits_entry.max_limit, Atoi(parts[1])); + } + + // ignore units + + return limits_entry; +} + +PosixErrorOr> ParseProcLimits( + absl::string_view contents) { + std::vector entries; + std::vector lines = absl::StrSplit(contents, '\n', absl::SkipEmpty()); + // skip first line (headers) + for (size_t i = 1U; i < lines.size(); ++i) { + std::cout << "line: " << lines[i] << std::endl; + ASSIGN_OR_RETURN_ERRNO(auto entry, ParseProcLimitsLine(lines[i])); + entries.push_back(entry); + } + return entries; +} + +std::ostream& operator<<(std::ostream& os, const ProcLimitsEntry& entry) { + std::string str = "Max "; + + switch (entry.limit_type) { + case LimitType::CPU: absl::StrAppend(&str, absl::StrFormat("%-25s ", "cpu time")); break; + case LimitType::FileSize: absl::StrAppend(&str, absl::StrFormat("%-25s ", "file size")); break; + case LimitType::Data: absl::StrAppend(&str, absl::StrFormat("%-25s ", "data size")); break; + case LimitType::Stack: absl::StrAppend(&str, absl::StrFormat("%-25s ", "stack size")); break; + case LimitType::Core: absl::StrAppend(&str, absl::StrFormat("%-25s ", "core file size")); break; + case LimitType::RSS: absl::StrAppend(&str, absl::StrFormat("%-25s ", "resident set")); break; + case LimitType::ProcessCount: absl::StrAppend(&str, absl::StrFormat("%-25s ", "processes")); break; + case LimitType::NumberOfFiles: absl::StrAppend(&str, absl::StrFormat("%-25s ", "open files")); break; + case LimitType::MemoryLocked: absl::StrAppend(&str, absl::StrFormat("%-25s ", "locked memory")); break; + case LimitType::AS: absl::StrAppend(&str, absl::StrFormat("%-25s ", "address space")); break; + case LimitType::Locks: absl::StrAppend(&str, absl::StrFormat("%-25s ", "file locks")); break; + case LimitType::SignalsPending: absl::StrAppend(&str, absl::StrFormat("%-25s ", "pending signals")); break; + case LimitType::MessageQueueBytes: absl::StrAppend(&str, absl::StrFormat("%-25s ", "msgqueue size")); break; + case LimitType::Nice: absl::StrAppend(&str, absl::StrFormat("%-25s ", "nice priority")); break; + case LimitType::RealTimePriority: absl::StrAppend(&str, absl::StrFormat("%-25s ", "realtime priority")); break; + case LimitType::Rttime: absl::StrAppend(&str, absl::StrFormat("%-25s ", "realtime timeout")); break; + } + + if (entry.cur_limit == ~0ULL) { + absl::StrAppend(&str, absl::StrFormat("%-20s ", "unlimited")); + } else { + absl::StrAppend(&str, absl::StrFormat("%-20d ", entry.cur_limit)); + } + + if (entry.max_limit == ~0ULL) { + absl::StrAppend(&str, absl::StrFormat("%-20s ", "unlimited")); + } else { + absl::StrAppend(&str, absl::StrFormat("%-20d ", entry.max_limit)); + } + + switch (entry.limit_type) { + case LimitType::CPU: absl::StrAppend(&str, absl::StrFormat("%-10s", "seconds")); break; + case LimitType::FileSize: absl::StrAppend(&str, absl::StrFormat("%-10s ", "bytes")); break; + case LimitType::Data: absl::StrAppend(&str, absl::StrFormat("%-10s ", "bytes")); break; + case LimitType::Stack: absl::StrAppend(&str, absl::StrFormat("%-10s ", "bytes")); break; + case LimitType::Core: absl::StrAppend(&str, absl::StrFormat("%-10s ", "bytes")); break; + case LimitType::RSS: absl::StrAppend(&str, absl::StrFormat("%-10s ", "bytes")); break; + case LimitType::ProcessCount: absl::StrAppend(&str, absl::StrFormat("%-10s ", "processes")); break; + case LimitType::NumberOfFiles: absl::StrAppend(&str, absl::StrFormat("%-10s ", "files")); break; + case LimitType::MemoryLocked: absl::StrAppend(&str, absl::StrFormat("%-10s ", "bytes")); break; + case LimitType::AS: absl::StrAppend(&str, absl::StrFormat("%-10s ", "bytes")); break; + case LimitType::Locks: absl::StrAppend(&str, absl::StrFormat("%-10s ", "locks")); break; + case LimitType::SignalsPending: absl::StrAppend(&str, absl::StrFormat("%-10s ", "signals")); break; + case LimitType::MessageQueueBytes: absl::StrAppend(&str, absl::StrFormat("%-10s ", "bytes")); break; + case LimitType::Nice: absl::StrAppend(&str, absl::StrFormat("%-10s ", "")); break; + case LimitType::RealTimePriority: absl::StrAppend(&str, absl::StrFormat("%-10s ", "")); break; + case LimitType::Rttime: absl::StrAppend(&str, absl::StrFormat("%-10s ", "us")); break; + } + + os << str; + return os; +} + +std::ostream& operator<<(std::ostream& os, const std::vector& vec) { + os << "Limit Soft Limit Hard Limit Units \n"; + for (unsigned int i = 0; i < vec.size(); i++) { + os << vec[i]; + if (i != vec.size() - 1) { + os << "\n"; + } + } + return os; +} + } // namespace testing } // namespace gvisor diff --git a/test/util/proc_util.h b/test/util/proc_util.h index 2ec4e61be..106f44f7e 100644 --- a/test/util/proc_util.h +++ b/test/util/proc_util.h @@ -195,6 +195,49 @@ MATCHER_P(ContainsMappings, mappings, return all_present; } +// LimitType is an rlimit type +enum class LimitType { + CPU, + FileSize, + Data, + Stack, + Core, + RSS, + ProcessCount, + NumberOfFiles, + MemoryLocked, + AS, + Locks, + SignalsPending, + MessageQueueBytes, + Nice, + RealTimePriority, + Rttime, +}; + +// ProcLimitsEntry contains the data from a single line in /proc/xxx/limits. +struct ProcLimitsEntry { + LimitType limit_type; + uint64_t cur_limit; + uint64_t max_limit; +}; + +// Parses a single line from /proc/xxx/limits +PosixErrorOr ParseProcLimitsLine(absl::string_view line); + +// Parses an entire /proc/xxx/limits file into lines +PosixErrorOr> ParseProcLimits(absl::string_view contents); + +// Printer for ProcLimitsEntry. +std::ostream& operator<<(std::ostream& os, const ProcLimitsEntry& entry); + +// Printer for std::vector. +std::ostream& operator<<(std::ostream& os, const std::vector& vec); + +// GMock printer for std::vector. +inline void PrintTo(const std::vector& vec, std::ostream* os) { + *os << vec; +} } // namespace testing } // namespace gvisor