Page MenuHomeFreeBSD

D50510.id155998.diff
No OneTemporary

D50510.id155998.diff

diff --git a/libexec/rc/rc.conf b/libexec/rc/rc.conf
--- a/libexec/rc/rc.conf
+++ b/libexec/rc/rc.conf
@@ -725,6 +725,9 @@
mixer_enable="YES" # Run the sound mixer.
opensm_enable="NO" # Opensm(8) for infiniband devices defaults to off
nuageinit_enable="NO" # Run nuageinit at startup
+mdoctl_enable="NO" # Load MAC/do at startup
+mdoctl_program="/sbin/mdoctl" # mdoctl executable
+mdoctl_config="/etc/mdo.conf" # Location of mdo.conf
# rctl(8) requires kernel options RACCT and RCTL
rctl_enable="YES" # Load rctl(8) rules on boot
diff --git a/libexec/rc/rc.d/Makefile b/libexec/rc/rc.d/Makefile
--- a/libexec/rc/rc.d/Makefile
+++ b/libexec/rc/rc.d/Makefile
@@ -46,6 +46,7 @@
mountlate \
mdconfig \
mdconfig2 \
+ mdoctl \
msgs \
netif \
netoptions \
diff --git a/libexec/rc/rc.d/mdoctl b/libexec/rc/rc.d/mdoctl
new file mode 100755
--- /dev/null
+++ b/libexec/rc/rc.d/mdoctl
@@ -0,0 +1,25 @@
+#!/bin/sh
+#
+# PROVIDE: mdoctl
+# REQUIRE: FILESYSTEMS
+# BEFORE: LOGIN
+
+. /etc/rc.subr
+
+name="mdoctl"
+desc="MAC/do configuration"
+rcvar="mdoctl_enable"
+start_cmd="mdoctl_start"
+
+load_rc_config $name
+
+# doesn't make sense to run in a svcj: configures kernel
+mdoctl_svcj="NO"
+
+mdoctl_start() {
+ sysctl -qn security.mac.do >/dev/null || kldload -qn mac_do || return 1
+ $mdoctl_program enable
+ $mdoctl_program load -f "$mdoctl_config"
+}
+
+run_rc_command "$1"
diff --git a/sbin/Makefile b/sbin/Makefile
--- a/sbin/Makefile
+++ b/sbin/Makefile
@@ -34,6 +34,7 @@
md5 \
mdconfig \
mdmfs \
+ mdoctl \
mknod \
mksnap_ffs \
mount \
diff --git a/sbin/mdoctl/Makefile b/sbin/mdoctl/Makefile
new file mode 100644
--- /dev/null
+++ b/sbin/mdoctl/Makefile
@@ -0,0 +1,7 @@
+PACKAGE= utilities
+PROG_CXX= mdoctl
+SRCS= mdoctl.cc
+CXXSTD= c++23
+MAN= mdoctl.8
+
+.include <bsd.prog.mk>
diff --git a/sbin/mdoctl/mdoctl.8 b/sbin/mdoctl/mdoctl.8
new file mode 100644
--- /dev/null
+++ b/sbin/mdoctl/mdoctl.8
@@ -0,0 +1,52 @@
+.\" SPDX-License-Identifier: ISC
+.\" Copyright (c) 2025 Lexi Winter.
+.\"
+.Dd May 24, 2025
+.Dt MDOCTL 8
+.Os
+.Sh NAME
+.Nm mdoctl
+.Nd manage MAC/do
+.Sh SYNOPSIS
+.Nm
+.Cm disable
+.Nm
+.Cm enable
+.Nm
+.Cm load
+.Op Fl f Ar path
+.Nm
+.Cm status
+.Sh DESCRIPTION
+The
+.Nm
+utility is used to manage the MAC/do framework,
+.Xr mac_do 4 .
+The behaviour of
+.Nm
+depends on the command:
+.Bl -tag -width disable
+.It Cm status
+Display the current status of MAC/do.
+.It Cm enable
+Enable MAC/do.
+.It Cm disable
+Disable MAC/do.
+.It Cm load
+Load the MAC/do ruleset from
+.Pa /etc/mdo.conf ,
+or from
+.Ar file
+if specified
+(see
+.Xr mdo.conf 5 ) .
+.El
+.Sh SEE ALSO
+.Xr mac_do 4 ,
+.Xr mdo 1 ,
+.Xr mdo.conf 5
+.Sh HISTORY
+The
+.Nm
+utility appeared in
+.Fx 15.0 .
diff --git a/sbin/mdoctl/mdoctl.cc b/sbin/mdoctl/mdoctl.cc
new file mode 100644
--- /dev/null
+++ b/sbin/mdoctl/mdoctl.cc
@@ -0,0 +1,825 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2025 Lexi Winter.
+ */
+
+/*
+ * mdoctl: manage MAC/do
+ */
+
+#include <sys/types.h>
+#include <sys/sysctl.h>
+
+#include <concepts>
+#include <format>
+#include <functional>
+#include <iostream>
+#include <optional>
+#include <print>
+#include <ranges>
+#include <stdexcept>
+#include <string>
+#include <unordered_map>
+
+#include <fcntl.h>
+#include <grp.h>
+#include <pwd.h>
+#include <unistd.h>
+
+using namespace std::literals;
+
+namespace {
+
+/*
+ * scope_guard: invoke a callable when this object is destroyed; this is similar
+ * to scope_exit from the library fundamentals TS, which LLVM doesn't implement.
+ */
+template<std::invocable F>
+struct guard final {
+ // Initialise the guard with a callable we will invoke later.
+ guard(F func) : _func(std::move(func)) {}
+
+ /*
+ * We are being destroyed, so call the callable.
+ * If the callable throws, std::terminate() will be called.
+ */
+ ~guard() {
+ if (_func)
+ std::invoke(*_func);
+ }
+
+ // Release the guard. This turns the destructor into a no-op.
+ void release() noexcept {
+ _func.release();
+ }
+
+ // Not default-constructible or copyable.
+ guard() = delete;
+ guard(guard const &) = delete;
+ guard(guard &&) noexcept = delete;
+ guard &operator=(guard const &) = delete;
+ guard &operator=(guard &&) noexcept = delete;
+
+private:
+ // The callable to be invoked when we are destroyed.
+ std::optional<F> _func;
+};
+
+/*
+ * The current status of MAC/do in the kernel. not_loaded means we couldn't
+ * load the MAC/do sysctl tree for some reason, which is usually because the
+ * kernel modules isn't loaded.
+ */
+enum struct mdo_status {
+ enabled, disabled, not_loaded
+};
+
+/* MAC/do constants */
+const std::string
+ sysctl_enabled = "security.mac.do.enabled",
+ sysctl_rules = "security.mac.do.rules",
+ default_config_path = "/etc/mdo.conf",
+
+ // What is considered "whitespace" when loading mdo.conf.
+ // This matches isspace() for the C locale.
+ whitespace = " \t\n\f\r\v";
+
+ /*
+ * These are the identifiers used in mdo.conf, and refer to nss names.
+ * They are NOT the identifiers used to configure the MAC/do kernel
+ * module; see the 'matcher' and 'target' classes for those.
+ */
+constexpr std::string_view
+ subject_type_user = "user",
+ subject_type_group = "group",
+ subject_type_sgroup = "+group",
+ subject_type_force_sgroup = "!group",
+ subject_type_deny_sgroup = "-group";
+
+/* Prototypes */
+void usage();
+auto mdo_enabled() -> mdo_status;
+int read_file(std::string filename, std::output_iterator<char> auto &&iter);
+auto skipws(std::string_view text) -> std::string_view;
+auto split(std::string_view text, std::string_view sep)
+ -> std::pair<std::string_view, std::string_view>;
+auto join(std::ranges::input_range auto &&elms, std::string_view sep)
+ -> std::string;
+auto next_word(std::string_view text)
+ -> std::pair<std::string_view, std::string_view>;
+auto uidofname(std::string_view name) -> std::optional<::uid_t>;
+auto gidofname(std::string_view name) -> std::optional<::gid_t>;
+/* Commands */
+int c_status(int argc, char **argv);
+int c_enable(int argc, char **argv);
+int c_disable(int argc, char **argv);
+int c_load(int argc, char **argv);
+
+/* The command table */
+std::unordered_map commands{
+ std::pair{"status"sv, &c_status},
+ std::pair{"enable"sv, &c_enable},
+ std::pair{"disable"sv, &c_disable},
+ std::pair{"load"sv, &c_load},
+};
+
+/**********************************************************************
+ * Utility functions.
+ */
+
+/*
+ * read_file: load the contents of a file into an output iterator.
+ * Return 0 on success, or an errno value on failure.
+ */
+int
+read_file(std::string filename, std::output_iterator<char> auto &&iter)
+{
+ constexpr std::size_t bufsize = 1024;
+ std::array<char, bufsize> buffer;
+ int fd, err;
+
+ if ((fd = ::open(filename.c_str(), O_RDONLY)) == -1)
+ return (errno);
+
+ auto fd_guard = guard([fd] { ::close(fd); });
+
+ while ((err = ::read(fd, &buffer[0], buffer.size())) > 0) {
+ auto data = std::span(buffer).subspan(0, err);
+ std::ranges::copy(data, iter);
+ }
+
+ return (err == -1 ? errno : 0);
+}
+
+/*
+ * skipws: Return the provided string with leading whitespace removed.
+ */
+std::string_view
+skipws(std::string_view text)
+{
+ auto pos = text.find_first_not_of(whitespace);
+ if (pos == std::string_view::npos)
+ return {};
+ return (text.substr(pos));
+}
+
+/*
+ * Split a string on a delimiter, returning a pair consisting of the string up
+ * to but not including the delimiter, and the remainder of the string.
+ * Only the first delimiter is significant, so multiple sequential delimiters
+ * will be treated as empty fields.
+ */
+std::pair<std::string_view, std::string_view>
+split(std::string_view text, std::string_view sep)
+{
+ auto pos = text.find_first_of(sep);
+ if (pos == std::string_view::npos)
+ // The entire string is the final token.
+ return {text, {}};
+
+ return {text.substr(0, pos), text.substr(pos + 1)};
+}
+
+/*
+ * Join the elements of a range into a string using the provided separator.
+ * Each element is converted to a string as if by std::format.
+ */
+std::string
+join(std::ranges::input_range auto &&elms, std::string_view sep)
+{
+ std::string ret;
+
+ for (auto const &elm : elms) {
+ if (!ret.empty())
+ ret += sep;
+ ret += std::format("{}", elm);
+ }
+
+ return (ret);
+}
+
+/*
+ * next_word: skip leading whitespace in the given string, then return
+ * the next whitespace-delimited word and the rest of the string.
+ */
+std::pair<std::string_view, std::string_view>
+next_word(std::string_view text)
+{
+ return (split(skipws(text), whitespace));
+}
+
+/*
+ * uidofname: convert a username into a uid.
+ */
+std::optional<::uid_t>
+uidofname(std::string_view name)
+{
+ std::string cname(std::from_range, name);
+ if (auto const *pw = ::getpwnam(cname.c_str()); pw != nullptr)
+ return {pw->pw_uid};
+ return {};
+}
+
+/*
+ * gidofname: convert a group name into a gid.
+ */
+std::optional<::gid_t>
+gidofname(std::string_view name)
+{
+ std::string cname(std::from_range, name);
+ if (auto const *gr = ::getgrnam(cname.c_str()); gr != nullptr)
+ return {gr->gr_gid};
+ return {};
+}
+
+/**********************************************************************
+ * The rule parser.
+ */
+
+// A generic exception indicating some sort of parsing error occurred.
+// what() should be informative.
+struct parse_error final : std::runtime_error {
+ template<typename... Args>
+ parse_error(std::format_string<Args...> fmt, Args &&...args)
+ : std::runtime_error(std::format(fmt,
+ std::forward<Args>(args)...))
+ {}
+};
+
+/*
+ * A matcher (uid or gid).
+ * This is what MAC/do calls the "from" part of the rule, i.e. a user or group
+ * which the rest of the rule applies to.
+ */
+struct matcher final {
+ std::string type; // The type of thing we match, e.g. "uid"
+ std::string id; // The subject we match, e.g. "0".
+
+ // Predefined values for 'type'.
+ static inline const std::string type_uid = "uid";
+ static inline const std::string type_gid = "gid";
+
+ // Format this matcher in MAC/do format.
+ std::string format() const {
+ return (std::format("{}={}", type, id));
+ }
+
+ /*
+ * Static constructors.
+ */
+
+ // A matcher that matches a specific uid.
+ static matcher uid(::uid_t userid) {
+ return matcher(type_uid, std::to_string(userid));
+ }
+
+ // A matcher that matches a specific gid.
+ static matcher gid(::gid_t grpid) {
+ return matcher(type_gid, std::to_string(grpid));
+ }
+
+ /*
+ * Constructors. To create new instances of this type, you must use
+ * one of the static constructors defined above.
+ */
+
+ matcher(matcher const &) = default;
+ matcher(matcher &&) noexcept = default;
+
+private:
+ matcher(std::string type_, std::string id_)
+ : type(std::move(type_))
+ , id(std::move(id_))
+ {}
+};
+
+/*
+ * Parse a matcher for a given subject. If the subject starts with '%',
+ * then it's a group name, otherwise it's a username. No restrictions
+ * are imposed on the format aside from what NSS requires.
+ *
+ * On error, throws parse_error.
+ */
+matcher
+parse_matcher(std::string_view text)
+{
+ // This should probably not happen.
+ if (text.empty())
+ throw parse_error("invalid empty subject");
+
+ // If the string starts with '%', then it's a group name.
+ if (text[0] == '%') {
+ auto group = text.substr(1);
+ if (auto gid = gidofname(group); gid)
+ return (matcher::gid(*gid));
+ else
+ throw parse_error("unknown group: '{}'", group);
+ }
+
+ // Otherwise, it's a username.
+ if (auto uid = uidofname(text); uid)
+ return (matcher::uid(*uid));
+ throw parse_error("unknown user: '{}'", text);
+}
+
+/*
+ * A target constraint.
+ * This is what MAC/do calls the "target" part of the rule; it describes what
+ * the subject is allowed to do once the rule matches.
+ */
+struct target final {
+ std::string type; // The type of thing we match, e.g. "uid"
+ std::string id; // The subject we match, e.g. "0".
+
+ // Predefined values for 'type'.
+ static inline const std::string type_uid = "uid";
+ static inline const std::string type_gid = "gid";
+ static inline const std::string type_sgid = "+gid";
+ static inline const std::string type_force_sgid = "!gid";
+ static inline const std::string type_deny_sgid = "-gid";
+ // Predefined values for 'id'.
+ static inline const std::string id_any = "*";
+
+ // Format this target in MAC/do format.
+ std::string format() const {
+ return (std::format("{}={}", type, id));
+ }
+
+ /*
+ * Static constructors.
+ */
+
+ // Permit a specific uid.
+ static target uid(::uid_t id) {
+ return target(type_uid, std::to_string(id));
+ }
+
+ // Permit any uid.
+ static target uid() {
+ return target(type_uid, id_any);
+ }
+
+ // Permit a specific gid.
+ static target gid(::gid_t id) {
+ return target(type_gid, std::to_string(id));
+ }
+
+ // Permit any gid.
+ static target gid() {
+ return target(type_gid, id_any);
+ }
+
+ // Permit a specific supplemental gid.
+ static target sgid(::gid_t id) {
+ return target(type_sgid, std::to_string(id));
+ }
+
+ // Permit any supplemental gid.
+ static target sgid() {
+ return target(type_sgid, id_any);
+ }
+
+ // Deny a specific supplemental gid.
+ static target deny_sgid(::gid_t id) {
+ return target(type_deny_sgid, std::to_string(id));
+ }
+
+ // Require a specific supplemental gid.
+ static target force_sgid(::gid_t id) {
+ return target(type_force_sgid, std::to_string(id));
+ }
+
+ /*
+ * Constructors. To create new instances of this type, you must use
+ * one of the static constructors defined above.
+ */
+
+ target(target const &) = default;
+ target(target &&) noexcept = default;
+
+private:
+ target(std::string type_, std::string id_)
+ : type(std::move(type_))
+ , id(std::move(id_))
+ {}
+};
+
+/*
+ * Parse a user target.
+ * Note that while MAC/do accepts both "*" and "any" to match any user, we only
+ * accept "*", because "any" is a valid username.
+ */
+target
+parse_user_target(std::string_view text)
+{
+ // "*" matches any user
+ if (text == target::id_any)
+ return (target::uid());
+
+ // Otherwise, we should have a username
+ if (auto uid = uidofname(text); uid)
+ return (target::uid(*uid));
+
+ throw parse_error("unknown user '{}'", text);
+}
+
+/*
+ * Parse a group target.
+ * Note that while MAC/do accepts both "*" and "any" to match any group, we only
+ * accept "*", because "any" is a valid username.
+ */
+target
+parse_group_target(std::string_view text)
+{
+ // "*" matches any group
+ if (text == target::id_any)
+ return target::gid();
+
+ // Otherwise, we should have a group name.
+ if (auto gid = gidofname(text); gid)
+ return target::gid(*gid);
+
+ throw parse_error("unknown group '{}'", text);
+}
+
+/*
+ * Parse a +group target.
+ */
+target
+parse_sgroup_target(std::string_view text)
+{
+ // "*" matches any group
+ if (text == "*")
+ return target::sgid();
+
+ // Otherwise we should have a group name
+ if (auto gid = gidofname(text); gid)
+ return target::sgid(*gid);
+
+ throw parse_error("unknown group '{}'", text);
+}
+
+/*
+ * Parse a "!group" target.
+ */
+target
+parse_forcegroup_target(std::string_view text)
+{
+ // "*" is not supported with !group
+ if (text == target::id_any)
+ throw parse_error("cannot use \"{}\" with \"{}\"",
+ text, subject_type_force_sgroup);
+
+ // So we must have a group name.
+ if (auto gid = gidofname(text); gid)
+ return (target::force_sgid(*gid));
+
+ throw parse_error("unknown group '{}'", text);
+}
+/*
+ * Parse a "-group" target.
+ */
+target
+parse_denygroup_target(std::string_view text)
+{
+ // "*" is not supported with -group
+ if (text == target::id_any)
+ throw parse_error("cannot use \"{}\" with \"{}\"",
+ text, subject_type_deny_sgroup);
+
+ // So we must have a group name.
+ if (auto gid = gidofname(text); gid)
+ return (target::deny_sgid(*gid));
+
+ throw parse_error("unknown group '{}'", text);
+}
+
+/*
+ * Parse a target of some sort.
+ */
+target
+parse_target(std::string_view text)
+{
+ // The target types we can parse.
+ static const std::unordered_map targets{
+ std::pair{subject_type_user, &parse_user_target},
+ std::pair{subject_type_group, &parse_group_target},
+ std::pair{subject_type_sgroup, &parse_sgroup_target},
+ std::pair{subject_type_force_sgroup, &parse_forcegroup_target},
+ std::pair{subject_type_deny_sgroup, &parse_denygroup_target},
+ };
+ auto [type, value] = split(text, "=");
+
+ if (auto it = targets.find(type); it != targets.end())
+ return (it->second(value));
+
+ throw parse_error("invalid target type '{}'", type);
+}
+
+/*
+ * A rule, containing a matcher and a list of targets
+ */
+struct rule final {
+ matcher subject;
+ std::vector<target> targets;
+
+ rule(matcher subject_) : subject(std::move(subject_)) {}
+
+ // Create a new rule from a subject and a range of targets.
+ rule(matcher subject_, std::ranges::range auto &&targets_)
+ : subject(subject_)
+ , targets(std::from_range, targets_)
+ {}
+
+ rule(rule const &) = default;
+ rule(rule &&) noexcept = default;
+
+ /*
+ * Format this rule in the MAC/do kernel format.
+ */
+ std::string format() const {
+ /*
+ * The mac_do rule format is:
+ * SUBJECT>TARGET[,TARGET...]
+ */
+ auto target_text = join(
+ std::views::transform(targets, &target::format),
+ ",");
+ auto rule_text = std::format("{}>{}",
+ subject.format(), target_text);
+ return (rule_text);
+ }
+};
+
+/*
+ * Parse a single rule and return a rule object.
+ *
+ * A rule consists of a single matcher, followed by one or more all targets, all
+ * separated by whitespace.
+ */
+rule
+parse_rule(std::string_view text)
+{
+ std::string_view subject_text, target_text;
+ std::vector<target> targets;
+
+ // The rule should start with a subject matcher.
+ std::tie(subject_text, text) = next_word(text);
+ if (subject_text.empty())
+ throw parse_error("missing subject");
+
+ // Parse the subject.
+ matcher subject = parse_matcher(subject_text);
+
+ // The target list cannot be empty.
+ text = skipws(text);
+ if (text.empty())
+ throw parse_error("no target specified");
+
+ // Parse whitespace-separated targets.
+ do {
+ std::tie(target_text, text) = next_word(text);
+ targets.push_back(parse_target(target_text));
+ text = skipws(text);
+ } while (!text.empty());
+
+ // Create the rule.
+ return (rule(std::move(subject), std::move(targets)));
+}
+
+/*
+ * mdo_enabled: return a constant indicating whether MAC/do is enabled
+ * mdo_status::enabled -> enabled
+ * mdo_status::disabled -> disabled
+ * mdo_status::not_loaded -> the mac_do module isn't loaded (or we couldn't
+ * fetch the sysctl for some reason)
+ */
+mdo_status
+mdo_enabled()
+{
+ int status_value = 0;
+ std::size_t status_size = sizeof(status_value);
+
+ if (sysctlbyname(sysctl_enabled.c_str(), &status_value, &status_size,
+ nullptr, 0) != 0)
+ return (mdo_status::not_loaded);
+
+ return (status_value ? mdo_status::enabled : mdo_status::disabled);
+}
+
+/**********************************************************************
+ * Command handlers.
+ */
+
+/*
+ * status: show the current status of MAC/do.
+ */
+int
+c_status(int argc, char **)
+{
+ if (argc != 1) {
+ usage();
+ return (1);
+ }
+
+ switch (mdo_enabled()) {
+ case mdo_status::enabled:
+ std::print("enabled\n");
+ break;
+ case mdo_status::disabled:
+ std::print("disabled\n");
+ break;
+ case mdo_status::not_loaded:
+ std::print("not loaded\n");
+ break;
+ default:
+ std::print("unknown\n");
+ }
+
+ return (0);
+}
+
+/*
+ * c_enable: turn MAC/do on
+ */
+int
+c_enable(int argc, char **)
+{
+ int value;
+ std::size_t size;
+
+ if (argc != 1) {
+ usage();
+ return (1);
+ }
+
+ value = 1;
+ size = sizeof(value);
+ if (sysctlbyname(sysctl_enabled.c_str(), nullptr, 0,
+ &value, size) == 0)
+ return (0);
+
+ if (errno == ENOENT)
+ std::print(std::cerr, "{}: MAC/do is not loaded\n",
+ getprogname());
+ else
+ std::print(std::cerr, "{}: {}\n",
+ getprogname(), strerror(errno));
+ return (1);
+}
+
+/*
+ * c_disable: turn MAC/do off
+ */
+int
+c_disable(int argc, char **)
+{
+ int value;
+ std::size_t size;
+
+ if (argc != 1) {
+ usage();
+ return (1);
+ }
+
+ value = 0;
+ size = sizeof(value);
+ if (sysctlbyname(sysctl_enabled.c_str(), nullptr, 0,
+ &value, size) == 0)
+ return (0);
+
+ if (errno == ENOENT)
+ std::print(std::cerr, "{}: MAC/do is not loaded\n",
+ getprogname());
+ else
+ std::print(std::cerr, "{}: {}\n",
+ getprogname(), strerror(errno));
+ return (1);
+}
+
+/*
+ * c_load: load MAC/do rules from a file into the kernel
+ */
+int
+c_load(int argc, char **argv)
+{
+ std::vector<rule> rules;
+ std::string config, rules_text, cfgpath;
+ std::string_view rest;
+ unsigned lineno;
+ int ch;
+
+ // Parse our arguments.
+
+ cfgpath = default_config_path;
+ while ((ch = getopt(argc, argv, "f:")) != -1) {
+ switch (ch) {
+ case 'f':
+ cfgpath = optarg;
+ break;
+ case '?':
+ default:
+ usage();
+ return (-1);
+ }
+ }
+
+ argc -= optind;
+ argv += optind;
+
+ if (argc != 0) {
+ usage();
+ return (1);
+ }
+
+ // Load the text of the configuration file.
+ if (auto err = read_file(cfgpath, std::back_inserter(config));
+ err != 0) {
+ std::print(std::cerr, "{}: {}\n", cfgpath, strerror(err));
+ return (1);
+ }
+
+ // Parse each line of the configuration file into a rule.
+ lineno = 0;
+ rest = std::string_view(config);
+ do {
+ std::string_view line;
+
+ // Fetch the next line.
+ std::tie(line, rest) = split(rest, "\n");
+ ++lineno;
+
+ // Remove leading whitespace
+ line = skipws(line);
+
+ // Skip comments and empty lines
+ if (line.empty() || line[0] == '#')
+ continue;
+
+ // Parse the rule.
+ try {
+ rules.push_back(parse_rule(line));
+ } catch (parse_error const &exc) {
+ std::print(std::cerr, "{}:{}: error: {}\n",
+ cfgpath, lineno, exc.what());
+ return (1);
+ }
+ } while (!rest.empty());
+
+ // We parsed all the rules successfully, so format and concat them
+ rules_text = join(std::views::transform(rules, &rule::format), ";");
+
+ // Load the new rules into the kernel.
+ if (sysctlbyname(sysctl_rules.c_str(), nullptr, 0,
+ rules_text.data(), rules_text.size()) == -1) {
+ std::print("{}: failed to configure kernel ruleset: {}\n",
+ getprogname(), strerror(errno));
+ return (1);
+ }
+
+ return (0);
+}
+
+/*
+ * usage: print usage
+ */
+void
+usage()
+{
+ std::print(std::cerr,
+"usage: {0} disable\n"
+" {0} enable\n"
+" {0} load [-f <path>]\n"
+" {0} status\n",
+ getprogname());
+}
+
+} // anonymous namespace
+
+int
+main(int argc, char **argv)
+{
+ if (argc < 2) {
+ usage();
+ return (1);
+ }
+
+ setprogname(argv[0]);
+
+ ++argv;
+ --argc;
+
+ /*
+ * Dispatch the top-level command.
+ */
+
+ if (auto c = commands.find(argv[0]); c != commands.end())
+ return (c->second(argc, argv));
+
+ std::print(std::cerr, "{}: unknown command '{}'\n",
+ getprogname(), argv[0]);
+ usage();
+ return (1);
+}
+
diff --git a/share/man/man5/Makefile b/share/man/man5/Makefile
--- a/share/man/man5/Makefile
+++ b/share/man/man5/Makefile
@@ -27,6 +27,7 @@
link.5 \
mailer.conf.5 \
make.conf.5 \
+ mdo.conf.5 \
moduli.5 \
motd.5 \
mount.conf.5 \
diff --git a/share/man/man5/mdo.conf.5 b/share/man/man5/mdo.conf.5
new file mode 100644
--- /dev/null
+++ b/share/man/man5/mdo.conf.5
@@ -0,0 +1,75 @@
+.\" SPDX-License-Identifier: ISC
+.\" Copyright (c) 2025 Lexi Winter.
+.\"
+.Dd May 24, 2025
+.Dt MDO.CONF 5
+.Os
+.Sh NAME
+.Nm mdo.conf
+.Nd MAC/do configuration file
+.Sh DESCRIPTION
+The
+.Nm
+file is used by
+.Xr mdoctl 8
+to configure the MAC/do framework (see
+.Xr mac_do 4 ) .
+.Pp
+The file is formatted as a list of rules, one per line.
+Empty lines and lines beginning with the
+.Sq #
+character are ignored.
+Rules consist of a whitespace-separated list of
+.Dq Ar name Ns Li = Ns Ar value
+pairs.
+The first field specifies the subject who is invoking
+.Xr setcred 2 .
+The remaining fields specify the targets permitted for that subject.
+.Pp
+The following subjects are supported:
+.Bl -tag -indent
+.It Cm user Ns = Ns Ar username
+This rule applies to the user
+.Ar username .
+.It Cm group Ns = Ns % Ns Ar groupname
+This rule applies to the group
+.Ar groupname .
+.El
+.Pp
+The following targets are supported:
+.Bl -tag -indent
+.It Cm user Ns = Ns Ar username
+Allow the user id to be changed to
+.Ar username .
+.It Cm group Ns = Ns Ar groupname
+Allow the primary group to be changed to
+.Ar groupname .
+.It Cm +group Ns = Ns Ar groupname
+Allow the supplemental group list to include
+.Ar groupname .
+.It Cm !group Ns = Ns Ar groupname
+Require the supplemental group list to include
+.Ar groupname .
+.It Cm -group Ns = Ns Ar groupname
+Forbid the supplemental group list to include
+.Ar groupname .
+.El
+.Sh EXAMPLE
+To allow anyone in the group
+.Dq wheel
+to change their user id to
+.Dq root ,
+with no restriction on the supplemental group list:
+.Bd -literal -offset indent
+%wheel user=root group=* +group=*
+.Ed
+.Sh SEE ALSO
+.Xr mac_do 4 ,
+.Xr mdoctl 8 ,
+.Xr mdo 1 ,
+.Sh HISTORY
+The
+.Nm
+file appeared in
+.Fx 15.0 .
+
diff --git a/share/man/man5/rc.conf.5 b/share/man/man5/rc.conf.5
--- a/share/man/man5/rc.conf.5
+++ b/share/man/man5/rc.conf.5
@@ -22,7 +22,7 @@
.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
.\" SUCH DAMAGE.
.\"
-.Dd May 21, 2025
+.Dd May 24, 2025
.Dt RC.CONF 5
.Os
.Sh NAME
@@ -4940,6 +4940,24 @@
.Dq Li YES ,
these are the flags to pass to the
.Xr sendmail 8
+.It Va mdoctl_enable
+.Pq Vt bool
+If set to
+.Dq Li YES ,
+run
+.Xr mdoctl 8
+at startup to configure MAC/do rules.
+.It Va mdoctl_program
+.Pq Vt str
+The path to
+.Pa mdoctl .
+The default path is
+.Pa /sbin/mdoctl .
+.It Va mdoctl_config
+.Pq Vt str
+The MAC/do configuration to load.
+The default configuration file is
+.Pa /etc/mdo.conf .
.It Va precious_machine
If set to
.Dq Li YES ,

File Metadata

Mime Type
text/plain
Expires
Sun, Dec 21, 12:36 PM (4 h, 54 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
27113034
Default Alt Text
D50510.id155998.diff (25 KB)

Event Timeline