Page Menu
Home
FreeBSD
Search
Configure Global Search
Log In
Files
F140120601
D50510.id155998.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
25 KB
Referenced Files
None
Subscribers
None
D50510.id155998.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D50510: mdoctl(8): a configuration utility for MAC/do
Attached
Detach File
Event Timeline
Log In to Comment