Page MenuHomeFreeBSD

D50510.id155972.diff
No OneTemporary

D50510.id155972.diff

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,50 @@
+.\" 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
+status
+.Pp
+.Nm
+enable
+.Pp
+.Nm
+disable
+.Pp
+.Nm
+load
+.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
+(see
+.Xr mdo.conf 5 ) .
+.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,513 @@
+/*
+ * 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} status\n"
+" {0} enable\n"
+" {0} disable\n",
+" {0} load\n",
+ getprogname());
+}
+
+/*
+ * status: show the current status of MAC/do.
+ */
+int
+c_status(int, char **)
+{
+ 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, char **)
+{
+ int value;
+ std::size_t size;
+
+ 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, char **)
+{
+ int value;
+ std::size_t size;
+
+ 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, char **)
+{
+ std::FILE *fp;
+ std::vector<std::string> lines;
+ std::vector<rule> rules;
+ std::string rules_text;
+ unsigned lineno;
+ bool first;
+ static std::array<char, 2048> buf;
+
+ if ((fp = std::fopen(config_path.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("{}, line {}: {}\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 == 0) {
+ usage();
+ return (1);
+ }
+
+ setprogname(argv[0]);
+
+ ++argv;
+ --argc;
+
+ /*
+ * Dispatch the top-level command.
+ */
+ if (argc == 0) {
+ usage();
+ return (1);
+ }
+
+ 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]);
+ return (1);
+}

File Metadata

Mime Type
text/plain
Expires
Sun, Apr 26, 11:10 PM (12 h, 2 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
32200304
Default Alt Text
D50510.id155972.diff (12 KB)

Event Timeline