blob: 782c7c41775005fa5c01d87d76895b478ec44119 [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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 <fts.h>
#include "linux/cgroups2.hpp"
#include <iterator>
#include <ostream>
#include <set>
#include <string>
#include <vector>
#include <process/after.hpp>
#include <process/loop.hpp>
#include <process/pid.hpp>
#include <stout/adaptor.hpp>
#include <stout/none.hpp>
#include <stout/numify.hpp>
#include <stout/os.hpp>
#include <stout/path.hpp>
#include <stout/unreachable.hpp>
#include <stout/stringify.hpp>
#include <stout/try.hpp>
#include "linux/ebpf.hpp"
#include "linux/fs.hpp"
using std::ostream;
using std::set;
using std::string;
using std::vector;
using process::Break;
using process::Continue;
using process::ControlFlow;
using process::Failure;
using process::Future;
using process::loop;
using mesos::internal::fs::MountTable;
namespace cgroups2 {
// Name of the cgroups v2 filesystem as found in /proc/filesystems.
const string FILE_SYSTEM = "cgroup2";
// Mount point for the cgroups2 file system.
const string MOUNT_POINT = "/sys/fs/cgroup";
template <typename T>
Try<T> read(const string& cgroup, const string& control);
template <>
Try<string> read(const string& cgroup, const string& control)
{
return os::read(path::join(cgroups2::path(cgroup), control));
}
template <>
Try<uint64_t> read(const string& cgroup, const string& control)
{
Try<string> content = read<string>(cgroup, control);
if (content.isError()) {
return Error(content.error());
}
return numify<uint64_t>(strings::trim(*content));
}
Try<Nothing> write(
const string& cgroup,
const string& control,
const string& value)
{
return os::write(path::join(cgroups2::path(cgroup), control), value);
}
Try<Nothing> write(
const string& cgroup,
const string& control,
const uint64_t& value)
{
return write(cgroup, control, stringify(value));
}
namespace control {
// Interface files found in all cgroups.
const std::string CONTROLLERS = "cgroup.controllers";
const std::string EVENTS = "cgroup.events";
const std::string FREEZE = "cgroup.freeze";
const std::string IRQ_PRESSURE = "irq.pressure";
const std::string KILL = "cgroup.kill";
const std::string MAX_DEPTH = "cgroup.max.depth";
const std::string MAX_DESCENDANTS = "cgroup.max.descendants";
const std::string PRESSURE = "cgroup.pressure";
const std::string PROCESSES = "cgroup.procs";
const std::string STATS = "cgroup.stat";
const std::string SUBTREE_CONTROLLERS = "cgroup.subtree_control";
const std::string THREADS = "cgroup.threads";
const std::string TYPE = "cgroup.type";
namespace subtree_control {
struct State
{
State() = default;
// We don't return errors here because enabling something
// unknown will fail when writing it back out.
void enable(const vector<string>& controllers)
{
foreach (const string& controller, controllers) {
enable(controller);
}
}
// We don't return errors here because enabling something
// unknown will fail when writing it back out.
void enable(const string& controller)
{
_disabled.erase(controller);
_enabled.insert(controller);
}
// We don't return errors here since disabling something
// unknown will fail when writing it back out.
void disable(const string& controller)
{
_enabled.erase(controller);
_disabled.insert(controller);
}
void disable(const set<string>& controllers)
{
foreach (const string& controller, controllers) {
disable(controller);
}
}
set<string> enabled() const { return _enabled; }
set<string> disabled() const { return _disabled; }
bool enabled(const string& controller) const
{
return _enabled.find(controller) != _enabled.end();
}
static State parse(const string& contents)
{
State control;
// Trim trailing newline.
const string trimmed = strings::trim(contents);
if (trimmed.empty()) {
return control;
}
vector<string> controllers = strings::split(trimmed, " ");
control._enabled.insert(
std::make_move_iterator(controllers.begin()),
std::make_move_iterator(controllers.end()));
return control;
}
private:
set<string> _enabled;
set<string> _disabled;
};
std::ostream& operator<<(std::ostream& stream, const State& state)
{
foreach (const string& system, state.enabled()) {
stream << "+" << system << " ";
}
foreach (const string& system, state.disabled()) {
stream << "-" << system << " ";
}
return stream;
}
Try<State> read(const string& cgroup)
{
Try<string> contents =
cgroups2::read<string>(cgroup, cgroups2::control::SUBTREE_CONTROLLERS);
if (contents.isError()) {
return Error(
"Failed to read 'cgroup.subtree_control' for cgroup '" + cgroup + "': "
+ contents.error());
}
return State::parse(*contents);
}
Try<Nothing> write(const string& cgroup, const State& state)
{
return cgroups2::write(
cgroup, control::SUBTREE_CONTROLLERS, stringify(state));
}
} // namespace subtree_control {
} // namespace control {
bool enabled()
{
Try<bool> supported = mesos::internal::fs::supported(cgroups2::FILE_SYSTEM);
return supported.isSome() && *supported;
}
Try<Nothing> mount()
{
if (!cgroups2::enabled()) {
return Error("cgroups2 is not enabled");
}
Try<bool> mounted = cgroups2::mounted();
if (mounted.isError()) {
return Error("Failed to check if cgroups2 filesystem is mounted: "
+ mounted.error());
}
if (*mounted) {
return Error("cgroup2 filesystem is already mounted at"
" '" + cgroups2::MOUNT_POINT + "'");
}
Try<Nothing> mkdir = os::mkdir(cgroups2::MOUNT_POINT);
if (mkdir.isError()) {
return Error("Failed to create cgroups2 directory"
" '" + cgroups2::MOUNT_POINT + "': " + mkdir.error());
}
return mesos::internal::fs::mount(
None(),
cgroups2::MOUNT_POINT,
cgroups2::FILE_SYSTEM,
0,
None());
}
Try<bool> mounted()
{
Try<MountTable> mountTable = MountTable::read("/proc/mounts");
if (mountTable.isError()) {
return Error("Failed to read /proc/mounts: " + mountTable.error());
}
foreach (MountTable::Entry entry, mountTable->entries) {
if (entry.type == cgroups2::FILE_SYSTEM) {
if (entry.dir == MOUNT_POINT) {
return true;
}
return Error("Found cgroups2 mount at an unexpected location"
" '" + entry.dir + "'");
}
}
return false;
}
Try<Nothing> unmount()
{
Try<bool> mounted = cgroups2::mounted();
if (mounted.isError()) {
return Error("Failed to check if the cgroup2 filesystem is mounted: "
+ mounted.error());
}
if (!*mounted) {
return Error("cgroups2 filesystem is not mounted");
}
Try<Nothing> result = mesos::internal::fs::unmount(MOUNT_POINT);
if (result.isError()) {
return Error("Failed to unmount the cgroup2 hierarchy"
" '" + cgroups2::MOUNT_POINT + "': " + result.error());
}
Try<Nothing> rmdir = os::rmdir(cgroups2::MOUNT_POINT);
if (rmdir.isError()) {
return Error("Failed to remove directory '" + cgroups2::MOUNT_POINT + "': "
+ rmdir.error());
}
return Nothing();
}
bool exists(const string& cgroup)
{
return os::exists(cgroups2::path(cgroup));
}
Try<set<string>> get(const string& cgroup)
{
const string& path = cgroups2::path(cgroup);
char* paths[] = {const_cast<char*>(path.c_str()), nullptr};
FTS* tree = fts_open(paths, FTS_NOCHDIR, nullptr);
if (tree == nullptr) {
return ErrnoError("Failed to start traversing filesystem");
}
FTSENT* node;
set<string> cgroups;
while ((node = fts_read(tree)) != nullptr) {
// Use post-order walk here. fts_level is the depth of the traversal,
// numbered from -1 to N, where the file/dir was found. The traversal root
// itself is numbered 0. fts_info includes flags for the current node.
// FTS_DP indicates a directory being visited in postorder.
if (node->fts_level > 0 && node->fts_info & FTS_DP) {
string _cgroup = strings::trim(
node->fts_path + MOUNT_POINT.length(), "/");
cgroups.insert(_cgroup);
}
}
if (errno != 0) {
Error error =
ErrnoError("Failed to read a node while traversing the filesystem");
fts_close(tree);
return error;
}
if (fts_close(tree) != 0) {
return ErrnoError("Failed to stop traversing file system");
}
return cgroups;
}
Try<Nothing> create(const string& cgroup, bool recursive)
{
const string path = cgroups2::path(cgroup);
Try<Nothing> mkdir = os::mkdir(path, recursive);
if (mkdir.isError()) {
return Error("Failed to create directory '" + path + "': " + mkdir.error());
}
return Nothing();
}
Try<Nothing> kill(const std::string& cgroup)
{
if (!cgroups2::exists(cgroup)) {
return Error("Cgroup does not exist");
}
return cgroups2::write(cgroup, cgroups2::control::KILL, "1");
}
Future<Nothing> destroy(const string& cgroup)
{
if (!cgroups2::exists(cgroup)) {
return Failure("Cgroup '" + cgroup + "' does not exist");
}
// To destroy a subtree of cgroups we first kill all of the processes inside
// of the cgroup and then remove all of the cgroup directories, removing
// the most deeply nested directories first.
Try<Nothing> kill = cgroups2::kill(cgroup);
if (kill.isError()) {
return Failure("Failed to kill processes in cgroup: " + kill.error());
}
// Wait until all of the processes have been killed.
int retries = 50;
Future<Nothing> emptied = loop(
[]() { return process::after(Milliseconds(1)); },
[=](const Nothing&) mutable -> Future<ControlFlow<Nothing>> {
Try<set<pid_t>> pids = cgroups2::processes(cgroup, true);
if (pids.isError()) {
return Failure("Failed to fetch pids in cgroup: " + pids.error());
}
if (pids->empty()) {
return Break();
}
--retries;
if (retries == 0) {
return Failure("Processes were still found: " + stringify(*pids));
}
return Continue();
});
return emptied
.then([=]() -> Future<Nothing> {
Try<set<string>> cgroups = cgroups2::get(cgroup);
if (cgroups.isError()) {
return Failure("Failed to get nested cgroups: " + cgroups.error());
}
cgroups->insert(cgroup);
// Remove the cgroups in bottom-up order.
foreach (const string& cgroup, adaptor::reverse(*cgroups)) {
const string path = cgroups2::path(cgroup);
// Remove the cgroup's directory. If the directory does not exist,
// ignore the error to protect against races.
if (::rmdir(path.c_str()) < 0) {
ErrnoError error = ErrnoError();
if (error.code != ENOENT) {
return Failure(
"Failed to remove directory '" + path + "': " + error.message);
}
}
}
return Nothing();
});
}
Try<Nothing> assign(const string& cgroup, pid_t pid)
{
if (!cgroups2::exists(cgroup)) {
return Error("Cgroup '" + cgroup + "' does not exist");
}
return cgroups2::write(cgroup, control::PROCESSES, stringify(pid));
}
Try<string> cgroup(pid_t pid)
{
// A process's cgroup membership is listed in /proc/{pid}/cgroup.
// The format, e.g if the process belongs to /sys/fs/cgroup/foo/bar, is:
//
// 0::/foo/bar
// or
// 0::/foo/bar (deleted)
//
// See: https://docs.kernel.org/admin-guide/cgroup-v2.html#processes
// https://man7.org/linux/man-pages/man7/cgroups.7.html
const string& cgroupFile = path::join("/proc", stringify(pid), "cgroup");
if (!os::exists(cgroupFile)) {
return Error("'" + cgroupFile + "' does not exist");
}
Try<string> read = os::read(cgroupFile);
if (read.isError()) {
return Error("Failed to read '" + cgroupFile + "': " + read.error());
}
string content = strings::trim(*read);
if (!strings::startsWith(content, "0::/")) {
return Error("process belongs to a v1 cgroup: " + content);
}
content = strings::remove(content, "0::/", strings::Mode::PREFIX);
content = strings::remove(content, " (deleted)", strings::Mode::SUFFIX);
return content;
}
Try<set<pid_t>> processes(const string& cgroup, bool recursive)
{
if (!cgroups2::exists(cgroup)) {
return Error("Cgroup '" + cgroup + "' does not exist");
}
set<string> cgroups = {cgroup};
if (recursive) {
Try<set<string>> descendants = cgroups2::get(cgroup);
if (descendants.isError()) {
return Error("Failed to list cgroups: " + descendants.error());
}
cgroups.insert(descendants->begin(), descendants->end());
}
set<pid_t> pids;
foreach (const string& cgroup, cgroups) {
Try<string> contents = cgroups2::read<string>(cgroup, control::PROCESSES);
if (contents.isError() && !exists(cgroup)) {
continue; // Ignore missing cgroups due to races.
}
if (contents.isError()) {
return Error("Failed to read cgroup.procs in '" + cgroup + "': "
+ contents.error());
}
foreach (const string& line, strings::split(*contents, "\n")) {
if (line.empty()) continue;
Try<pid_t> pid = numify<pid_t>(line);
if (pid.isError()) {
return Error("Failed to parse '" + line + "' as a pid: " + pid.error());
}
pids.insert(*pid);
}
}
return pids;
}
Try<set<pid_t>> threads(const string& cgroup)
{
Try<string> contents = cgroups2::read<string>(cgroup, control::THREADS);
if (contents.isError()) {
return Error("Failed to read 'cgroup.threads' in"
" '" + cgroup + "': " + contents.error());
}
set<pid_t> tids;
foreach (const string& line, strings::split(*contents, "\n")) {
if (line.empty()) continue;
Try<pid_t> tid = numify<pid_t>(line);
if (tid.isError()) {
return Error("Failed to parse '" + line + "' as a tid: " + tid.error());
}
tids.insert(*tid);
}
return tids;
}
string path(const string& cgroup)
{
return path::join(cgroups2::MOUNT_POINT, cgroup);
}
namespace controllers {
Try<set<string>> available(const string& cgroup)
{
Try<string> read =
cgroups2::read<string>(cgroup, cgroups2::control::CONTROLLERS);
if (read.isError()) {
return Error("Failed to read cgroup.controllers in '" + cgroup + "': "
+ read.error());
}
// Trim trailing newline.
const string contents = strings::trim(*read);
if (contents.empty()) {
return set<string>();
}
vector<string> controllers = strings::split(contents, " ");
return set<string>(
std::make_move_iterator(controllers.begin()),
std::make_move_iterator(controllers.end()));
}
Try<Nothing> enable(const string& cgroup, const vector<string>& controllers)
{
using State = control::subtree_control::State;
Try<State> control = cgroups2::control::subtree_control::read(cgroup);
if (control.isError()) {
return Error(control.error());
}
control->enable(controllers);
return cgroups2::control::subtree_control::write(cgroup, *control);
}
Try<Nothing> disable(const string& cgroup, const set<string>& controllers)
{
using State = control::subtree_control::State;
Try<State> control = cgroups2::control::subtree_control::read(cgroup);
if (control.isError()) {
return Error(control.error());
}
control->disable(controllers);
return cgroups2::control::subtree_control::write(cgroup, *control);
}
Try<set<string>> enabled(const string& cgroup)
{
Try<string> contents =
cgroups2::read<string>(cgroup, cgroups2::control::SUBTREE_CONTROLLERS);
if (contents.isError()) {
return Error("Failed to read 'cgroup.subtree_control' in '" + cgroup + "'"
": " + contents.error());
}
using State = control::subtree_control::State;
State control = State::parse(*contents);
return control.enabled();
}
} // namespace controllers {
namespace cpu {
BandwidthLimit::BandwidthLimit(Duration _limit, Duration _period)
: limit{_limit},
period{_period} {}
Try<BandwidthLimit> parse_bandwidth(const string& content)
{
// Format
// -----------------------------
// $MAX $PERIOD
// -----------------------------
// $MAX Maximum CPU time, in microseconds, processes in the cgroup can
// collectively use during one $PERIOD. If set to "max" then there
// is no limit.
//
// $PERIOD Length of one period, in microseconds.
vector<string> split = strings::split(strings::trim(content), " ");
if (split.size() != 2) {
return Error("Expected format '$MAX $PERIOD'"
" but received '" + content + "'");
}
if (split[0] == "max") {
return cpu::BandwidthLimit();
}
Try<Duration> limit = Duration::parse(split[0] + "us");
if (limit.isError()) {
return Error("Failed to parse cpu.max's limit of '" + split[0] + "': "
+ limit.error());
}
Try<Duration> period = Duration::parse(split[1] + "us");
if (period.isError()) {
return Error("Failed to parse cpu.max's period of '" + split[1] + "': "
+ period.error());
}
return BandwidthLimit(*limit, *period);
}
namespace control {
const std::string IDLE = "cpu.idle";
const std::string MAX = "cpu.max";
const std::string MAX_BURST = "cpu.max.burst";
const std::string PRESSURE = "cpu.pressure";
const std::string STATS = "cpu.stat";
const std::string UCLAMP_MAX = "cpu.uclamp.max";
const std::string UCLAMP_MIN = "cpu.uclamp.min";
const std::string WEIGHT = "cpu.weight";
const std::string WEIGHT_NICE = "cpu.weight.nice";
namespace stat {
Try<Stats> parse(const string& content)
{
const vector<string> lines = strings::split(content, "\n");
cpu::Stats stats;
foreach (const string& line, lines) {
if (line.empty()) {
continue;
}
vector<string> tokens = strings::split(line, " ");
if (tokens.size() != 2) {
return Error("Invalid line format in 'cpu.stat' expected "
"<key> <value> received: '" + line + "'");
}
const string& field = tokens[0];
const string& value = tokens[1];
Try<uint64_t> number = numify<uint64_t>(value);
if (number.isError()) {
return Error("Failed to parse '" + field + "': " + number.error());
}
Duration duration = Microseconds(static_cast<int64_t>(*number));
if (field == "usage_usec") { stats.usage = duration; }
else if (field == "user_usec") { stats.user_time = duration; }
else if (field == "system_usec") { stats.system_time = duration; }
else if (field == "nr_periods") { stats.periods = *number; }
else if (field == "nr_throttled") { stats.throttled = *number; }
else if (field == "throttled_usec") { stats.throttle_time = duration; }
else if (field == "nr_burst") { stats.bursts = *number; }
else if (field == "burst_usec") { stats.bursts_time = duration; }
}
return stats;
}
} // namespace stat {
} // namespace control {
Try<Nothing> weight(const string& cgroup, uint64_t weight)
{
if (cgroup == ROOT_CGROUP) {
return Error("Operation not supported for the root cgroup");
}
return cgroups2::write(cgroup, cpu::control::WEIGHT, weight);
}
Try<uint64_t> weight(const string& cgroup)
{
if (cgroup == ROOT_CGROUP) {
return Error("Operation not supported for the root cgroup");
}
return cgroups2::read<uint64_t>(cgroup, cpu::control::WEIGHT);
}
Try<cpu::Stats> stats(const string& cgroup)
{
Try<string> content = cgroups2::read<string>(
cgroup, cgroups2::cpu::control::STATS);
if (content.isError()) {
return Error("Failed to read 'cpu.stat' for the cgroup '" + cgroup + "': "
+ content.error());
}
return cpu::control::stat::parse(*content);
}
Try<Nothing> set_max(const string& cgroup, const cpu::BandwidthLimit& limit)
{
if (cgroup == ROOT_CGROUP) {
return Error("Operation not supported for the root cgroup");
}
if (limit.limit.isNone()) {
return cgroups2::write(cgroup, cpu::control::MAX, "max");
}
if (limit.period.isNone()) {
return Error("Invalid bandwidth limit: period can only be None"
" for a limitless bandwidth limit");
}
if (limit.period->ns() < 0 || limit.limit->ns() < 0
|| limit.period->ns() % 1000 > 0 || limit.limit->ns() % 1000 > 0) {
return Error("Invalid bandwidth limit: period and limit must be"
" positive and microsecond level granularity, received"
" period=" + stringify(*limit.period)
+ " limit=" + stringify(*limit.limit));
}
return cgroups2::write(
cgroup,
cpu::control::MAX,
stringify(static_cast<uint64_t>(limit.limit->us()))
+ " "
+ stringify(static_cast<uint64_t>(limit.period->us())));
}
Try<cpu::BandwidthLimit> max(const string& cgroup)
{
if (cgroup == ROOT_CGROUP) {
return Error("Operation not supported for the root cgroup");
}
Try<string> content = cgroups2::read<string>(cgroup, cpu::control::MAX);
if (content.isError()) {
return Error("Failed the read 'cpu.max' for cgroup '" + cgroup + "': "
+ content.error());
}
Try<BandwidthLimit> limit = parse_bandwidth(*content);
if (limit.isError()) {
return Error("Failed to parse '" + *content + "' as a bandwidth limit: "
+ limit.error());
}
return *limit;
}
} // namespace cpu {
namespace memory {
namespace internal {
// Parse a byte limit from a string.
//
// Format: "max" OR a u64_t string representing bytes.
Result<Bytes> parse_bytelimit(const string& value)
{
const string trimmed = strings::trim(value);
if (trimmed == "max") {
return None();
}
Try<uint64_t> bytes = numify<uint64_t>(trimmed);
if (bytes.isError()) {
return Error("Failed to numify '" + trimmed + "': " + bytes.error());
}
return Bytes(*bytes);
}
} // namespace internal {
namespace control {
const string CURRENT = "memory.current";
const string EVENTS = "memory.events";
const string LOW = "memory.low";
const string HIGH = "memory.high";
const string MAX = "memory.max";
const string MIN = "memory.min";
const string STAT = "memory.stat";
namespace stat {
Try<Stats> parse(const string& content)
{
Stats stats;
foreach (const string& line, strings::split(content, "\n")) {
if (line.empty()) {
continue;
}
vector<string> tokens = strings::split(line, " ");
if (tokens.size() != 2) {
return Error("Invalid line format in 'memory.stat'; expected "
"<key> <value> received: '" + line + "'");
}
const string& key = tokens[0];
const string& value = tokens[1];
Try<uint64_t> n = numify<uint64_t>(value);
if (n.isError()) {
return Error("Failed to numify '" + value + "': " + n.error());
}
const Bytes bytes(*n);
if (key == "anon") { stats.anon = bytes; }
else if (key == "file") { stats.file = bytes; }
else if (key == "kernel") { stats.kernel = bytes; }
else if (key == "kernel_stack") { stats.kernel_stack = bytes; }
else if (key == "pagetables") { stats.pagetables = bytes; }
else if (key == "sock") { stats.sock = bytes; }
else if (key == "vmalloc") { stats.vmalloc = bytes; }
else if (key == "file_mapped") { stats.file_mapped = bytes; }
}
return stats;
}
} // namespace stat {
} // namespace control {
namespace events {
Try<Events> parse(const string& content)
{
Events events;
foreach (const string& line, strings::split(content, "\n")) {
if (line.empty()) {
continue;
}
vector<string> tokens = strings::split(line, " ");
if (tokens.size() != 2) {
return Error("Invalid line format in 'memory.events' expected "
"<key> <value> received: '" + line + "'");
}
const string& field = tokens[0];
const string& value = tokens[1];
Try<uint64_t> count = numify<uint64_t>(value);
if (count.isError()) {
return Error("Failed to numify '" + value + "': " + count.error());
}
if (field == "low") { events.low = *count; }
else if (field == "high") { events.high = *count; }
else if (field == "max") { events.max = *count; }
else if (field == "oom") { events.oom = *count; }
else if (field == "oom_kill") { events.oom_kill = *count; }
else if (field == "oom_group_kill") { events.oom_group_kill = *count; }
}
return events;
}
} // namespace events {
Future<Nothing> oom(const string& cgroup)
{
// TODO(dleamy): Update this to use inotify, rather than polling.
return loop(
[]() {
return process::after(Milliseconds(100));
},
[=](const Nothing&) -> Future<ControlFlow<Nothing>> {
Try<string> content = cgroups2::read<string>(cgroup, control::EVENTS);
if (content.isError()) {
return Failure("Failed to read 'memory.events': " + content.error());
}
Try<Events> events = events::parse(strings::trim(*content));
if (events.isError()) {
return Failure("Failed to parse 'memory.events': " + events.error());
}
if (events->oom > 0) {
return Break(Nothing());
}
return Continue();
});
}
Try<Bytes> usage(const string& cgroup)
{
Try<uint64_t> contents = cgroups2::read<uint64_t>(
cgroup, memory::control::CURRENT);
if (contents.isError()) {
return Error("Failed to read 'memory.current': " + contents.error());
}
return Bytes(*contents);
}
Try<Nothing> set_low(const string& cgroup, const Bytes& bytes)
{
return cgroups2::write(cgroup, control::LOW, bytes.bytes());
}
Try<Bytes> low(const string& cgroup)
{
Try<uint64_t> contents = cgroups2::read<uint64_t>(cgroup, control::LOW);
if (contents.isError()) {
return Error("Failed to read 'memory.low': " + contents.error());
}
return Bytes(*contents);
}
Try<Nothing> set_min(const string& cgroup, const Bytes& bytes)
{
return cgroups2::write(cgroup, control::MIN, bytes.bytes());
}
Try<Bytes> min(const string& cgroup)
{
Try<uint64_t> contents = cgroups2::read<uint64_t>(cgroup, control::MIN);
if (contents.isError()) {
return Error("Failed to read 'memory.min': " + contents.error());
}
return Bytes(*contents);
}
Try<Nothing> set_max(const string& cgroup, const Option<Bytes>& limit)
{
return cgroups2::write(
cgroup,
control::MAX,
limit.isNone() ? "max" : stringify(limit->bytes()));
}
Result<Bytes> max(const string& cgroup)
{
Try<string> contents = cgroups2::read<string>(cgroup, control::MAX);
if (contents.isError()) {
return Error("Failed to read 'memory.max': " + contents.error());
}
return internal::parse_bytelimit(*contents);
}
Try<Nothing> set_high(const string& cgroup, const Option<Bytes>& limit)
{
return cgroups2::write(
cgroup,
control::HIGH,
limit.isNone() ? "max" : stringify(limit->bytes()));
}
Result<Bytes> high(const string& cgroup)
{
Try<string> contents = cgroups2::read<string>(cgroup, control::HIGH);
if (contents.isError()) {
return Error("Failed to read 'memory.high': " + contents.error());
}
return internal::parse_bytelimit(*contents);
}
Try<Stats> stats(const string& cgroup)
{
Try<string> contents = cgroups2::read<string>(cgroup, control::STAT);
if (contents.isError()) {
return Error("Failed to read 'memory.stat': " + contents.error());
}
return control::stat::parse(*contents);
}
} // namespace memory {
namespace devices {
// Utility class to construct an eBPF program to whitelist or blacklist
// select device accesses.
class DeviceProgram
{
public:
DeviceProgram() : program{ebpf::Program(BPF_PROG_TYPE_CGROUP_DEVICE)}
{
// The BPF_PROG_TYPE_CGROUP_DEVICE program takes in
// `struct bpf_cgroup_dev_ctx*` as input. We extract the fields into
// registers r2-5.
//
// The device type is encoded in the first 16 bits of `access_type` and
// the access type is encoded in the last 16 bits of `access_type`.
program.append({
// r2: Type ('c', 'b', '?')
BPF_LDX_MEM(
BPF_W, BPF_REG_2, BPF_REG_1, offsetof(bpf_cgroup_dev_ctx, access_type)),
BPF_ALU32_IMM(BPF_AND, BPF_REG_2, 0xFFFF),
// r3: Access ('r', 'w', 'm')
BPF_LDX_MEM(BPF_W, BPF_REG_3, BPF_REG_1,
offsetof(bpf_cgroup_dev_ctx, access_type)),
BPF_ALU32_IMM(BPF_RSH, BPF_REG_3, 16),
// r4: Major Version
BPF_LDX_MEM(BPF_W, BPF_REG_4, BPF_REG_1,
offsetof(bpf_cgroup_dev_ctx, major)),
// r5: Minor Version
BPF_LDX_MEM(BPF_W, BPF_REG_5, BPF_REG_1,
offsetof(bpf_cgroup_dev_ctx, minor)),
});
}
Try<Nothing> allow(const Entry entry) { return addDevice(entry, true); }
Try<Nothing> deny(const Entry entry) { return addDevice(entry, false); }
ebpf::Program build()
{
if (!hasCatchAll) {
// Exit instructions.
// If no entry granted access, then deny the access.
program.append({
BPF_MOV64_IMM (BPF_REG_0, DENY_ACCESS),
BPF_EXIT_INSN(),
});
}
return program;
}
private:
Try<Nothing> addDevice(const Entry entry, bool allow)
{
if (hasCatchAll) {
return Nothing();
}
// We create a block of bytecode with the format:
// 1. Major Version Check
// 2. Minor Version Check
// 3. Type Check
// 4. Access Check
// 5. Allow/Deny Access
//
// 6. NEXT BLOCK
//
// Either:
// 1. The device access is matched by (1,2,3,4) and the Allow/Deny access
// block (5) is executed.
// 2. One of (1,2,3,4) does not match the requested access and we skip
// to the next block (6).
const Entry::Selector& selector = entry.selector;
const Entry::Access& access = entry.access;
bool check_major = selector.major.isSome();
bool check_minor = selector.minor.isSome();
bool check_type = selector.type != Entry::Selector::Type::ALL;
bool check_access = !access.mknod || !access.read || !access.write;
// Number of instructions to the [NEXT BLOCK]. This is used if a check
// fails (meaning this entry does not apply) and we want to skip the
// subsequent checks.
short jmp_size = 1 + (check_major ? 1 : 0) + (check_minor ? 1 : 0) +
(check_access ? 3 : 0) + (check_type ? 1 : 0);
// Check major version (r4) against entry.
if (check_major) {
program.append({
BPF_JMP_IMM(BPF_JNE, BPF_REG_4, (int)selector.major.get(), jmp_size),
});
--jmp_size;
}
// Check minor version (r5) against entry.
if (check_minor) {
program.append({
BPF_JMP_IMM(BPF_JNE, BPF_REG_5, (int)selector.minor.get(), jmp_size),
});
--jmp_size;
}
// Check type (r2) against entry.
if (check_type) {
int bpf_type = [selector]() {
switch (selector.type) {
case Entry::Selector::Type::BLOCK: return BPF_DEVCG_DEV_BLOCK;
case Entry::Selector::Type::CHARACTER: return BPF_DEVCG_DEV_CHAR;
case Entry::Selector::Type::ALL: UNREACHABLE();
}
}();
program.append({
BPF_JMP_IMM(BPF_JNE, BPF_REG_2, bpf_type, jmp_size),
});
--jmp_size;
}
// Check access (r3) against entry.
if (check_access) {
int bpf_access = 0;
bpf_access |= access.read ? BPF_DEVCG_ACC_READ : 0;
bpf_access |= access.write ? BPF_DEVCG_ACC_WRITE : 0;
bpf_access |= access.mknod ? BPF_DEVCG_ACC_MKNOD : 0;
program.append({
BPF_MOV32_REG(BPF_REG_1, BPF_REG_3),
BPF_ALU32_IMM(BPF_AND, BPF_REG_1, bpf_access),
BPF_JMP_REG(
BPF_JNE, BPF_REG_1, BPF_REG_3, static_cast<short>(jmp_size - 2)),
});
jmp_size -= 3;
}
if (!check_major && !check_minor && !check_type && !check_access) {
// The exit instructions as well as any additional device entries would
// generate unreachable blocks.
hasCatchAll = true;
}
// Allow/Deny access block.
program.append({
BPF_MOV64_IMM(BPF_REG_0, allow ? ALLOW_ACCESS : DENY_ACCESS),
BPF_EXIT_INSN(),
});
return Nothing();
}
ebpf::Program program;
// Whether the program has a device entry that allows or denies ALL accesses.
// Such cases need to be specially handled because any instructions added
// after it will be unreachable, and thus will cause the eBPF verifier to
// reject the program.
bool hasCatchAll = false;
static const int ALLOW_ACCESS = 1;
static const int DENY_ACCESS = 0;
};
Try<Nothing> configure(
const string& cgroup,
const vector<Entry>& allow,
const vector<Entry>& deny)
{
DeviceProgram program = DeviceProgram();
foreach (const Entry entry, allow) {
program.allow(entry);
}
foreach (const Entry entry, deny) {
program.deny(entry);
}
Try<Nothing> attach = ebpf::cgroups2::attach(
cgroups2::path(cgroup),
program.build());
if (attach.isError()) {
return Error("Failed to attach BPF_PROG_TYPE_CGROUP_DEVICE program: " +
attach.error());
}
return Nothing();
}
} // namespace devices {
} // namespace cgroups2 {