Page MenuHomeFreeBSD

D50510.id155977.diff
No OneTemporary

D50510.id155977.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,547 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2025 Lexi Winter.
+ */
+
+/*
+ * mdoctl: manage MAC/do
+ */
+
+#include <sys/types.h>
+#include <sys/sysctl.h>
+
+#include <cctype>
+#include <format>
+#include <functional>
+#include <iostream>
+#include <optional>
+#include <print>
+#include <ranges>
+#include <stdexcept>
+#include <string>
+#include <unordered_map>
+
+#include <unistd.h>
+#include <pwd.h>
+#include <grp.h>
+
+using namespace std::literals;
+
+namespace {
+
+/* Prototypes */
+void usage();
+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);
+
+/* MAC/do constants */
+const std::string
+ sysctl_enabled = "security.mac.do.enabled",
+ sysctl_rules = "security.mac.do.rules",
+ config_path = "/etc/mdo.conf",
+
+ // These are used in mdo.conf, and refer to nss names.
+ subject_type_user = "user",
+ subject_type_group = "group",
+ subject_type_sgroup = "+group",
+ subject_type_forcesgroup = "!group",
+ subject_type_denysgroup = "-group",
+
+ // These are used in the sysctl interface, and refer to ids.
+ category_uid = "uid",
+ category_gid = "gid",
+ category_sgid = "+gid",
+ category_forcesgid = "!gid",
+ category_denysgid = "-gid",
+ subject_any_uid = "*",
+ subject_any_gid = "*";
+
+/* The command table */
+using handler_t = std::function<int (int, char **)>;
+auto commands = std::unordered_map<std::string_view, handler_t>{
+ { "status"sv, c_status },
+ { "enable"sv, c_enable },
+ { "disable"sv, c_disable },
+ { "load"sv, c_load },
+};
+
+/*
+ * skipws: Return the provided string with leading whitespace removed.
+ */
+std::string_view
+skipws(std::string_view text)
+{
+ auto n = text.find_first_not_of(" \t"sv);
+ if (n == std::string_view::npos)
+ return {};
+ return text.substr(n);
+}
+
+/*
+ * next_word: skip leading whitespace in the given string, then return
+ * the next whitespace-delimited word and the rest of the string.
+ * If 'sep' is provided, use this as the separator instead of whitespace.
+ */
+std::pair<std::string_view, std::string_view>
+next_word(std::string_view text, std::string_view sep = " \t"sv)
+{
+ text = skipws(text);
+
+ auto n = text.find_first_of(sep);
+ if (n == std::string_view::npos)
+ // The entire string is the next word.
+ return {text, {}};
+
+ return {text.substr(0, n), text.substr(n + 1)};
+}
+
+/*
+ * 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
+ */
+
+struct parse_error : 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) */
+struct matcher final {
+ std::string category; // The type of thing we match, e.g. "uid"
+ std::string subject; // The subject we match, e.g. "0".
+
+ // Format this matcher in MAC/do format.
+ std::string format() const {
+ return std::format("{}={}", category, subject);
+ }
+};
+
+/*
+ * Parse a user matcher.
+ */
+std::optional<matcher>
+parse_user_matcher(std::string_view text)
+{
+ if (auto uid = uidofname(text); uid)
+ return matcher(category_uid, std::to_string(*uid));
+ return {};
+}
+
+/*
+ * Parse a group matcher.
+ */
+std::optional<matcher>
+parse_group_matcher(std::string_view text)
+{
+ std::string_view p = text;
+
+ // A group name must start with a % symbol.
+ if (p.empty() || p[0] != '%')
+ return {};
+ p = p.substr(1);
+
+ // See if this group exists.
+ if (auto gid = gidofname(p); gid)
+ return matcher(category_gid, std::to_string(*gid));
+ return {};
+}
+
+/*
+ * Parse a matcher of some type (group or user).
+ */
+matcher
+parse_subject(std::string_view text)
+{
+ if (auto matcher = parse_group_matcher(text); matcher)
+ return *matcher;
+ if (auto matcher = parse_user_matcher(text); matcher)
+ return *matcher;
+ // Not a username or a group name.
+ throw parse_error("invalid subject '{}'"sv, text);
+}
+
+/* A target constraint */
+struct constraint {
+ std::string category; // The type of thing we match, e.g. "uid"
+ std::string subject; // The subject we match, e.g. "0".
+
+ // Format this constraint in MAC/do format.
+ std::string format() const {
+ return std::format("{}={}", category, subject);
+ }
+};
+
+/*
+ * Parse a user constraint.
+ */
+constraint
+parse_user_constraint(std::string_view text)
+{
+ if (text == "*"sv)
+ return {constraint(category_uid, subject_any_uid)};
+ if (auto uid = uidofname(text); uid)
+ return {constraint(category_uid, std::to_string(*uid))};
+ throw parse_error("unknown user '{}'"sv, text);
+}
+
+/*
+ * Parse a group constraint.
+ */
+constraint
+parse_group_constraint(std::string category, std::string_view text)
+{
+ if (text == "*"sv)
+ return {constraint(std::move(category), subject_any_gid)};
+ if (auto gid = gidofname(text); gid)
+ return {constraint(std::move(category), std::to_string(*gid))};
+ throw parse_error("unknown group '{}'"sv, text);
+}
+
+/*
+ * Parse a constraint of some sort.
+ */
+constraint
+parse_constraint(std::string_view text)
+{
+ auto [type, value] = next_word(text, "=");
+
+ if (type == subject_type_user)
+ return parse_user_constraint(value);
+
+ if (type == subject_type_group)
+ return parse_group_constraint(category_gid, value);
+
+ if (type == subject_type_sgroup)
+ return parse_group_constraint(category_sgid, value);
+
+ if (type == subject_type_forcesgroup)
+ return parse_group_constraint(category_forcesgid, value);
+
+ if (type == subject_type_denysgroup)
+ return parse_group_constraint(category_denysgid, value);
+
+ throw parse_error("invalid constraint type '{}'"sv, type);
+}
+
+/* A rule, containing a matcher and a list of targets */
+struct rule final {
+ matcher subject;
+ std::vector<constraint> constraints;
+
+ rule(matcher subject_) : subject(std::move(subject_)) {}
+
+ std::string format() const {
+ std::string rule_text;
+ bool first;
+
+ rule_text = subject.format();
+ rule_text += ">"sv;
+
+ first = true;
+ for (auto const &cst : this->constraints) {
+ if (!first)
+ rule_text += ","sv;
+ first = false;
+ rule_text += cst.format();
+ }
+
+ return rule_text;
+ }
+};
+
+/*
+ * Parse a single rule and return a rule object.
+ */
+rule
+parse_rule(std::string_view text)
+{
+ std::string_view rest, subject, constraint_text;
+
+ // The rule should start with a subject matcher.
+ std::tie(subject, rest) = next_word(text);
+ if (subject.empty())
+ throw parse_error("missing subject");
+
+ auto subject_matcher = parse_subject(subject);
+ rule ret(std::move(subject_matcher));
+
+ // Parse space-separated constraints.
+ rest = skipws(rest);
+ while (!rest.empty()) {
+ std::tie(constraint_text, rest) = next_word(rest);
+ ret.constraints.push_back(parse_constraint(constraint_text));
+ }
+
+ return (ret);
+}
+
+/*
+ * 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 loader (or we couldn't
+ * fetch the sysctl for some reason)
+ */
+enum struct mdo_status {
+ enabled, disabled, not_loaded
+};
+
+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;
+}
+
+/*
+ * 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());
+}
+
+/*
+ * 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;
+ }
+
+ 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::FILE *fp;
+ std::vector<std::string> lines;
+ std::vector<rule> rules;
+ std::string rules_text, cfgpath;
+ unsigned lineno;
+ int ch;
+ bool first;
+ static std::array<char, 2048> buf;
+
+ cfgpath = 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);
+ }
+
+ if ((fp = std::fopen(cfgpath.c_str(), "r")) == NULL) {
+ std::print(std::cerr, "{}: {}\n", config_path, strerror(errno));
+ return (1);
+ }
+
+ while (fgets(buf.data(), buf.size(), fp) != NULL) {
+ std::string_view line(buf.data());
+ if (!line.empty() && line[line.size() - 1] == '\n')
+ line = line.substr(0, line.size() - 1);
+ lines.emplace_back(line);
+ }
+
+ if (ferror(fp)) {
+ std::print(std::cerr, "{}: {}\n", config_path, strerror(errno));
+ return (1);
+ }
+
+ lineno = 0;
+ for (auto line : lines) {
+ ++lineno;
+
+ // Remove leading whitespace
+ line = skipws(line);
+
+ // Skip comments and empty line
+ if (line.empty() || line[0] == '#')
+ continue;
+
+ try {
+ rules.push_back(parse_rule(line));
+ } catch (parse_error const &exc) {
+ std::print(std::cerr, "{}:{}: error: {}\n",
+ config_path, lineno, exc.what());
+ return (1);
+ }
+ }
+
+ /* We parsed all the rules successfully, so concat them */
+ first = true;
+ for (auto const &rl : rules) {
+ if (!first)
+ rules_text += ";";
+ first = false;
+ rules_text += rl.format();
+ }
+
+ 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);
+}
+
+} // 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/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
Fri, May 29, 10:36 AM (20 h, 26 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
33609891
Default Alt Text
D50510.id155977.diff (15 KB)

Event Timeline