I have been working on a SELinux solution for Fedora GNOME Workstation systems where a normal desktop account is mapped to:
id -Z : user_u:user_r:user_t:s0
The original problem was that GNOME/Nautilus image icons and thumbnails would fail to display under a restricted user_u desktop.
Confining a desktop account to SELinux user_u reduces the damage that a compromised application or user process can do by placing it in a much more restricted SELinux role than an unconfined account. Processes running as user_u:user_r:user_t are prevented from performing many privileged actions, accessing sensitive system resources, creating broad persistence mechanisms, or crossing into domains they were never meant to control unless policy explicitly permits it. The main benefit is containment: even if an application is exploited, the attacker is still constrained by SELinux mandatory access controls instead of inheriting the full authority of the logged-in user. If you believe in not using the admin account for daily driver, you should implement user_u on a SELinux platform.
You constrain a user account to user_u by using
The previous incarnation of the solution was that rights assigned were too permissive, as reviewed by @Bot This time permissions were assigned tightly. It allow bwrap and glycin to work and no more.
Usage: script_name_you_choose.sh install <account name> --online ( or --offline)
When the --online parameter is given, it will tell dnf to install the necessary packages.
id -Z : user_u:user_r:user_t:s0
The original problem was that GNOME/Nautilus image icons and thumbnails would fail to display under a restricted user_u desktop.
Confining a desktop account to SELinux user_u reduces the damage that a compromised application or user process can do by placing it in a much more restricted SELinux role than an unconfined account. Processes running as user_u:user_r:user_t are prevented from performing many privileged actions, accessing sensitive system resources, creating broad persistence mechanisms, or crossing into domains they were never meant to control unless policy explicitly permits it. The main benefit is containment: even if an application is exploited, the attacker is still constrained by SELinux mandatory access controls instead of inheriting the full authority of the logged-in user. If you believe in not using the admin account for daily driver, you should implement user_u on a SELinux platform.
You constrain a user account to user_u by using
Sass:
semanage login -a -s user_u <account name>
restorecon -RFv /home/<account name>/
The previous incarnation of the solution was that rights assigned were too permissive, as reviewed by @Bot This time permissions were assigned tightly. It allow bwrap and glycin to work and no more.
Usage: script_name_you_choose.sh install <account name> --online ( or --offline)
When the --online parameter is given, it will tell dnf to install the necessary packages.
Code:
#!/usr/bin/bash
# FedoraGnome Glycin Domain v1.20a stable candidate
#
# Purpose:
# Confine Fedora GNOME Glycin sandbox construction and image decoding to
# dedicated SELinux domains without returning namespace/mount authority to
# ordinary user_t.
#
# Architecture:
# user_t
# -> fedoragnome_glycin_gate_t
# -> fedoragnome_glycin_bwrap_t
# -> fedoragnome_glycin_loader_t (ENFORCING)
# -> fedoragnome_glycin_frame_t for decoder-created frame memfds
#
# v1.20a preserves v1.20 and adds one measured inherited-frame mmap permission:
# * gate_t, bwrap_t and loader_t are enforcing.
# * approved Glycin calls use root-owned private copies of Fedora's four
# Glycin loaders and transition bwrap_t -> loader_t.
# * loader-created tmpfs frame files transition to a dedicated
# fedoragnome_glycin_frame_t; user_t receives only the frame access needed
# to consume decoded output, not generic tmpfs_t write access.
# * the measured loader_t -> tmpfs_t:lnk_file read permission is retained.
# * user_t receives no bwrap namespace/mount/netlink authority and cannot
# directly execute the private loader copies.
# * the development observation collector/verbose OBS argv/env/fd logging
# has been removed. Security routing/rejection logging remains.
# * selftest is root-only and verifies loader_t is enforcing plus the frame
# transition and generic-tmpfs negative checks.
#
# Dependency modes:
# install USER --online : install missing Fedora prerequisites, then install.
# install USER --offline : never invoke dnf/network; fail if prerequisites are missing.
#
# Usage:
# sudo -r sysadm_r /usr/bin/bash
# ./FedoraGnome-GlycinDomain-v1.20a.sh install yyy --online (--offline)
# ./FedoraGnome-GlycinDomain-v1.20a.sh selftest yyy
# ./FedoraGnome-GlycinDomain-v1.20a.sh status yyy
# ./FedoraGnome-GlycinDomain-v1.20a.sh uninstall
#
set -Eeuo pipefail
IFS=$'\n\t'
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
export PATH
umask 077
NAME="FedoraGnome-GlycinDomain"
VERSION="1.20a-stable-candidate"
SYSTEM_BWRAP="/usr/bin/bwrap"
GATE="/usr/local/bin/bwrap"
PRIVATE_DIR="/usr/local/libexec/fedoragnome-glycin-domain"
DOMAIN_BWRAP="$PRIVATE_DIR/bwrap-domain"
USER_BWRAP="$PRIVATE_DIR/bwrap-user"
USER_TEST_HELPER="/usr/local/bin/fedoragnome-glycin-user-test"
PRIVATE_REAL_LOADERS="$PRIVATE_DIR/loaders-real"
STATE_DIR="/var/lib/fedoragnome-glycin-domain-test"
SRC="$STATE_DIR/glycin-bwrap-gate.c"
POLICY_DIR="$STATE_DIR/policy"
POLICY_MODULE="fedoragnome_glycin_domain_test"
POLICY_TE="$POLICY_DIR/$POLICY_MODULE.te"
POLICY_FC="$POLICY_DIR/$POLICY_MODULE.fc"
POLICY_PP="$POLICY_DIR/$POLICY_MODULE.pp"
FAPOLICY_TRUST="/etc/fapolicyd/trust.d/fedoragnome-glycin-domain-test"
SYSTEM_LOADER_DIR="/usr/libexec/glycin-loaders/2+"
LOADERS=(glycin-image-rs glycin-svg glycin-heif glycin-jxl)
log() { printf '[%s-v%s] %s\n' "$NAME" "$VERSION" "$*"; }
die() { printf '[%s-v%s] ERROR: %s\n' "$NAME" "$VERSION" "$*" >&2; exit 1; }
require_root() {
[[ ${EUID:-$(id -u)} -eq 0 ]] || die "run as root"
}
have_module() {
semodule -l 2>/dev/null | awk '{print $1}' | grep -Fxq "$1"
}
source_type_of() {
stat -c '%C' "$1" 2>/dev/null | awk -F: '{print $3}'
}
REQUIRED_PACKAGES=(
gcc
make
selinux-policy-devel
checkpolicy
policycoreutils-devel
policycoreutils
policycoreutils-python-utils
setools-console
libselinux-utils
bubblewrap
glycin-loaders
coreutils
diffutils
util-linux
systemd
gawk
grep
sed
)
required_commands_missing() {
local c missing=0
for c in \
gcc make checkmodule semodule_package semodule semanage restorecon sesearch seinfo getenforce \
sha256sum stat cmp bwrap rpm systemctl \
mktemp getent awk grep sed install id runuser; do
if ! command -v "$c" >/dev/null 2>&1; then
printf '%s\n' "$c"
missing=1
fi
done
[[ -r /usr/share/selinux/devel/Makefile ]] || {
printf '%s\n' '/usr/share/selinux/devel/Makefile'
missing=1
}
return "$missing"
}
require_commands() {
local missing
missing="$(required_commands_missing || true)"
[[ -z "$missing" ]] || {
printf '[%s-v%s] ERROR: missing required commands/files:\n%s\n' \
"$NAME" "$VERSION" "$missing" >&2
die "install prerequisites are incomplete"
}
}
prepare_dependencies() {
local mode="$1"
case "$mode" in
--offline)
log "offline mode: package installation is disabled"
require_commands
;;
--online)
command -v dnf >/dev/null 2>&1 ||
die "--online requires Fedora's dnf command"
local pkg
local -a missing_pkgs=()
for pkg in "${REQUIRED_PACKAGES[@]}"; do
rpm -q -- "$pkg" >/dev/null 2>&1 || missing_pkgs+=("$pkg")
done
if ((${#missing_pkgs[@]})); then
log "online mode: installing missing Fedora prerequisite packages: ${missing_pkgs[*]}"
dnf -y install "${missing_pkgs[@]}" ||
die "dnf failed while installing prerequisites"
else
log "online mode: all prerequisite packages are already installed"
fi
require_commands
;;
*)
die "install mode must be --online or --offline"
;;
esac
}
conflicting_test_loaded() {
local m
for m in \
fedora_gnome_user_t_compat_test \
fedoragnome_user_t_bwrap_netlink_test \
fedoragnome_user_t_hwdb_compat \
fedoragnome_icon_debug_netlink \
fedoragnome_glycin_thumb_gap_test
do
if have_module "$m"; then
printf '%s\n' "$m"
return 0
fi
done
return 1
}
gate_is_ours() {
[[ -x "$GATE" ]] || return 1
"$GATE" --fedoragnome-glycin-gate-version 2>/dev/null |
grep -Fxq 'FedoraGnome-GlycinDomain-v1.20a-stable-candidate'
}
installed_gate_version() {
[[ -x "$GATE" ]] || return 1
"$GATE" --fedoragnome-glycin-gate-version 2>/dev/null | head -1
}
assert_old_user_t_namespace_grants_absent() {
local bad=0 out
check_absent() {
local desc="$1"; shift
out="$("$@" 2>/dev/null || true)"
if [[ -n ${out//[[:space:]]/} ]]; then
printf 'FAIL %s\n%s\n' "$desc" "$out" >&2
bad=1
else
printf 'PASS absent: %s\n' "$desc"
fi
}
check_absent "user_t -> proc_t:dir mounton" \
sesearch -A -s user_t -t proc_t -c dir -p mounton
check_absent "user_t -> proc_t:filesystem mount" \
sesearch -A -s user_t -t proc_t -c filesystem -p mount
check_absent "user_t -> self:netlink_route_socket nlmsg_write" \
sesearch -A -s user_t -c netlink_route_socket -p nlmsg_write
check_absent "user_t -> tmpfs_t:filesystem mount" \
sesearch -A -s user_t -t tmpfs_t -c filesystem -p mount
check_absent "user_t -> fs_t:filesystem remount" \
sesearch -A -s user_t -t fs_t -c filesystem -p remount
check_absent "user_t -> user_tmp_t:file mounton" \
sesearch -A -s user_t -t user_tmp_t -c file -p mounton
check_absent "user_t -> devpts_t:filesystem mount" \
sesearch -A -s user_t -t devpts_t -c filesystem -p mount
check_absent "user_t -> fs_t:filesystem unmount" \
sesearch -A -s user_t -t fs_t -c filesystem -p unmount
check_absent "user_t -> fonts_cache_t:dir mounton" \
sesearch -A -s user_t -t fonts_cache_t -c dir -p mounton
check_absent "user_t -> tmpfs_t:filesystem unmount" \
sesearch -A -s user_t -t tmpfs_t -c filesystem -p unmount
check_absent "user_t -> root_t:dir mounton" \
sesearch -A -s user_t -t root_t -c dir -p mounton
check_absent "user_t -> tmp_t:dir mounton" \
sesearch -A -s user_t -t tmp_t -c dir -p mounton
((bad == 0))
}
write_gate_source() {
local uid="$1" home="$2"
[[ "$home" =~ ^/[A-Za-z0-9._/+:-]+$ ]] ||
die "target home contains unsupported characters for compiled gate: $home"
install -d -o root -g root -m 0700 "$STATE_DIR"
cat >"$SRC" <<'EOF_C'
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <pwd.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/prctl.h>
#include <syslog.h>
#include <unistd.h>
#ifndef TARGET_UID
#define TARGET_UID 1001
#endif
#define SYSTEM_BWRAP "/usr/bin/bwrap"
#define FALLBACK_BWRAP "/usr/local/libexec/fedoragnome-glycin-domain/bwrap-user"
#define SPECIAL_BWRAP "/usr/local/libexec/fedoragnome-glycin-domain/bwrap-domain"
#define PRIVATE_LOADER_DIR "/usr/local/libexec/fedoragnome-glycin-domain/loaders-real"
#define GATE_VERSION "FedoraGnome-GlycinDomain-v1.20a-stable-candidate"
#ifndef TARGET_HOME
#define TARGET_HOME "/home/yyy"
#endif
static bool digits_only(const char *s)
{
if (!s || !*s) return false;
for (const unsigned char *p = (const unsigned char *)s; *p; ++p)
if (*p < '0' || *p > '9') return false;
return true;
}
static bool has_dotdot_component(const char *p)
{
if (!p || *p != '/') return true;
const char *s = p;
while ((s = strstr(s, "..")) != NULL) {
bool left = (s == p || s[-1] == '/');
bool right = (s[2] == '\0' || s[2] == '/');
if (left && right) return true;
s += 2;
}
return false;
}
static bool prefix_path(const char *path, const char *prefix)
{
size_t n = strlen(prefix);
if (strncmp(path, prefix, n) != 0) return false;
return path[n] == '\0' || path[n] == '/';
}
static bool own_home_path(const char *path)
{
return path && TARGET_HOME[0] == '/' && prefix_path(path, TARGET_HOME);
}
static bool allowed_ro_path(const char *path)
{
if (!path || path[0] != '/' || has_dotdot_component(path)) return false;
if (!strcmp(path, "/usr") ||
!strcmp(path, "/etc/ld.so.cache") ||
prefix_path(path, "/etc/fonts") ||
prefix_path(path, "/usr/share/fonts") ||
prefix_path(path, "/usr/local/share/fonts") ||
prefix_path(path, "/var/cache/fontconfig") ||
prefix_path(path, "/usr/lib/fontconfig/cache") ||
prefix_path(path, "/nix/store"))
return true;
/*
* Glycin may expose user font-cache/base-directory content read-only.
* This does not confer DAC access beyond the invoking target UID.
*/
if (own_home_path(path))
return true;
return false;
}
static bool allowed_tmpfs(const char *p)
{
return p && (!strcmp(p, "/tmp-home") || !strcmp(p, "/tmp-run"));
}
static bool allowed_symlink_pair(const char *src, const char *dst)
{
return
(!strcmp(src, "/usr/lib") && !strcmp(dst, "/lib")) ||
(!strcmp(src, "/usr/lib64") && !strcmp(dst, "/lib64")) ||
(!strcmp(src, "/usr/lib32") && !strcmp(dst, "/lib32"));
}
static bool allowed_setenv(const char *key, const char *val)
{
if (!key || !val) return false;
if (!strcmp(key, "HOME"))
return !strcmp(val, "/tmp-home");
if (!strcmp(key, "XDG_RUNTIME_DIR")) {
if (!strcmp(val, "/tmp-run")) return true;
char expected[64];
snprintf(expected, sizeof(expected), "/run/user/%u", (unsigned)TARGET_UID);
return !strcmp(val, expected);
}
if (!strcmp(key, "XDG_CACHE_HOME"))
return own_home_path(val) && strstr(val, "/.cache/glycin/") != NULL;
return false;
}
static bool safe_loader(const char *path)
{
if (!path || !prefix_path(path, "/usr/libexec/glycin-loaders"))
return false;
char resolved[PATH_MAX];
if (!realpath(path, resolved))
return false;
if (!prefix_path(resolved, "/usr/libexec/glycin-loaders"))
return false;
const char *base = strrchr(resolved, '/');
base = base ? base + 1 : resolved;
if (strncmp(base, "glycin-", 7) != 0)
return false;
struct stat st;
if (stat(resolved, &st) != 0)
return false;
if (!S_ISREG(st.st_mode) || st.st_uid != 0)
return false;
if (st.st_mode & (S_IWGRP | S_IWOTH))
return false;
/*
* Deliberately do NOT call access(X_OK) here. The validator domain is
* intentionally denied execute on bin_t. Executability is an installer
* property; this runtime check validates path, ownership and writability.
*/
return (st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)) != 0;
}
static bool private_loader_for(const char *system_loader, char out[PATH_MAX])
{
const char *base = strrchr(system_loader ? system_loader : "", '/');
base = base ? base + 1 : system_loader;
if (!base ||
(strcmp(base, "glycin-image-rs") &&
strcmp(base, "glycin-svg") &&
strcmp(base, "glycin-heif") &&
strcmp(base, "glycin-jxl")))
return false;
int n = snprintf(out, PATH_MAX, "%s/%s", PRIVATE_LOADER_DIR, base);
if (n <= 0 || n >= PATH_MAX)
return false;
struct stat st;
if (stat(out, &st) != 0)
return false;
if (!S_ISREG(st.st_mode) || st.st_uid != 0 || (st.st_mode & (S_IWGRP | S_IWOTH)))
return false;
/*
* Metadata-only validation: gate_t must never need execute permission on
* the private real-loader type. Only the dedicated bwrap domain may execute
* these files; SELinux then transitions the loader back to user_t.
*/
return (st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)) != 0;
}
static bool looks_like_glycin(int argc, char **argv, const char **loader_out, int *loader_index_out)
{
bool unshare_all = false, die_parent = false, clearenv = false, seccomp = false;
int i = 1;
while (i < argc) {
const char *a = argv[i];
if (!strcmp(a, "--")) {
++i;
break;
}
if (a[0] != '-')
break;
if (!strcmp(a, "--unshare-all")) {
unshare_all = true; ++i; continue;
}
if (!strcmp(a, "--die-with-parent")) {
die_parent = true; ++i; continue;
}
if (!strcmp(a, "--clearenv")) {
clearenv = true; ++i; continue;
}
if (!strcmp(a, "--chdir")) {
if (i + 1 >= argc || strcmp(argv[i+1], "/")) return false;
i += 2; continue;
}
if (!strcmp(a, "--dev")) {
if (i + 1 >= argc || strcmp(argv[i+1], "/dev")) return false;
i += 2; continue;
}
if (!strcmp(a, "--tmpfs")) {
if (i + 1 >= argc || !allowed_tmpfs(argv[i+1])) return false;
i += 2; continue;
}
if (!strcmp(a, "--seccomp")) {
if (i + 1 >= argc || !digits_only(argv[i+1])) return false;
seccomp = true;
i += 2; continue;
}
if (!strcmp(a, "--ro-bind") || !strcmp(a, "--ro-bind-try")) {
if (i + 2 >= argc) return false;
if (strcmp(argv[i+1], argv[i+2])) return false;
if (!allowed_ro_path(argv[i+1])) return false;
i += 3; continue;
}
if (!strcmp(a, "--bind-try")) {
if (i + 2 >= argc) return false;
if (strcmp(argv[i+1], argv[i+2])) return false;
if (!own_home_path(argv[i+1]) || strstr(argv[i+1], "/.cache/glycin/") == NULL)
return false;
i += 3; continue;
}
if (!strcmp(a, "--setenv")) {
if (i + 2 >= argc || !allowed_setenv(argv[i+1], argv[i+2]))
return false;
i += 3; continue;
}
if (!strcmp(a, "--symlink")) {
if (i + 2 >= argc || !allowed_symlink_pair(argv[i+1], argv[i+2]))
return false;
i += 3; continue;
}
/* Unknown bwrap option: never route it into thumb_t. */
return false;
}
if (!(unshare_all && die_parent && clearenv && seccomp))
return false;
if (i >= argc || !safe_loader(argv[i]))
return false;
int loader_index = i;
const char *loader = argv[i++];
/* Current Glycin loaders take only a D-Bus FD after the loader path. */
if (i + 2 != argc || strcmp(argv[i], "--dbus-fd") || !digits_only(argv[i+1]))
return false;
*loader_out = loader;
*loader_index_out = loader_index;
return true;
}
static void log_rejected_argv(int argc, char **argv)
{
/*
* Keep diagnostics bounded. Arguments are not interpreted by syslog;
* control characters are replaced so each rejection remains one journal line.
*/
enum { CAP = 7000 };
char buf[CAP];
size_t used = 0;
int n = snprintf(buf, sizeof(buf),
"rejected argv uid=%u ppid=%ld argc=%d:",
(unsigned)getuid(), (long)getppid(), argc);
if (n < 0) return;
used = (size_t)n < sizeof(buf) ? (size_t)n : sizeof(buf) - 1;
for (int i = 0; i < argc && used + 8 < sizeof(buf); ++i) {
n = snprintf(buf + used, sizeof(buf) - used, " [%d]=", i);
if (n < 0) break;
if ((size_t)n >= sizeof(buf) - used) {
used = sizeof(buf) - 1;
break;
}
used += (size_t)n;
const unsigned char *p = (const unsigned char *)(argv[i] ? argv[i] : "");
while (*p && used + 2 < sizeof(buf)) {
unsigned char c = *p++;
if (c < 0x20 || c == 0x7f)
c = '?';
buf[used++] = (char)c;
}
buf[used] = '\0';
}
if (used + 16 < sizeof(buf))
snprintf(buf + used, sizeof(buf) - used, " [end]");
openlog("fedoragnome-glycin-gate", LOG_PID, LOG_USER);
syslog(LOG_WARNING, "%s", buf);
closelog();
}
static int read_proc_attr(const char *path, char *buf, size_t buflen)
{
int fd = open(path, O_RDONLY | O_CLOEXEC);
if (fd < 0)
return -1;
ssize_t n = read(fd, buf, buflen - 1);
int saved = errno;
close(fd);
errno = saved;
if (n <= 0)
return -1;
while (n > 0 && (buf[n - 1] == '\n' || buf[n - 1] == '\0'))
--n;
buf[n] = '\0';
return 0;
}
static int build_context_with_type(const char *current,
const char *new_type,
char *out,
size_t outlen)
{
const char *c1 = strchr(current, ':');
if (!c1) return -1;
const char *c2 = strchr(c1 + 1, ':');
if (!c2) return -1;
const char *c3 = strchr(c2 + 1, ':');
if (!c3) return -1;
size_t prefix_len = (size_t)(c2 - current + 1);
int n = snprintf(out, outlen, "%.*s%s%s",
(int)prefix_len, current, new_type, c3);
return (n > 0 && (size_t)n < outlen) ? 0 : -1;
}
static int set_next_exec_type(const char *new_type,
char *requested,
size_t requested_len)
{
char current[512];
if (read_proc_attr("/proc/self/attr/current", current, sizeof(current)) != 0)
return -1;
if (build_context_with_type(current, new_type,
requested, requested_len) != 0) {
errno = EINVAL;
return -1;
}
int fd = open("/proc/self/attr/exec", O_WRONLY | O_CLOEXEC);
if (fd < 0)
return -1;
size_t len = strlen(requested) + 1;
ssize_t n = write(fd, requested, len);
int saved = errno;
close(fd);
errno = saved;
return n == (ssize_t)len ? 0 : -1;
}
static void set_next_exec_type_or_die(const char *type, const char *path)
{
char requested[512];
if (set_next_exec_type(type, requested, sizeof(requested)) != 0) {
int saved = errno;
openlog("fedoragnome-glycin-gate", LOG_PID, LOG_USER);
syslog(LOG_ERR,
"refusing exec: explicit SELinux exec-context request failed; type=%s path=%s errno=%d",
type, path, saved);
closelog();
fprintf(stderr,
"%s: explicit SELinux exec-context request failed for %s -> %s: %s\n",
GATE_VERSION, path, type, strerror(saved));
_exit(126);
}
openlog("fedoragnome-glycin-gate", LOG_PID, LOG_USER);
syslog(LOG_NOTICE,
"explicit SELinux exec context armed; next=%s path=%s",
requested, path);
closelog();
}
static void exec_system_bwrap(int argc, char **argv)
{
(void)argc;
argv[0] = (char *)SYSTEM_BWRAP;
execv(SYSTEM_BWRAP, argv);
fprintf(stderr, "%s: exec %s failed: %s\n",
GATE_VERSION, SYSTEM_BWRAP, strerror(errno));
_exit(127);
}
static void exec_user_bwrap(int argc, char **argv)
{
(void)argc;
set_next_exec_type_or_die("user_t", FALLBACK_BWRAP);
argv[0] = (char *)FALLBACK_BWRAP;
execv(FALLBACK_BWRAP, argv);
fprintf(stderr, "%s: exec %s failed: %s\n",
GATE_VERSION, FALLBACK_BWRAP, strerror(errno));
_exit(127);
}
static void exec_domain_bwrap(int argc, char **argv)
{
(void)argc;
set_next_exec_type_or_die("fedoragnome_glycin_bwrap_t", SPECIAL_BWRAP);
argv[0] = (char *)SPECIAL_BWRAP;
execv(SPECIAL_BWRAP, argv);
fprintf(stderr, "%s: exec %s failed: %s\n",
GATE_VERSION, SPECIAL_BWRAP, strerror(errno));
_exit(127);
}
int main(int argc, char **argv)
{
if (argc == 2 && !strcmp(argv[1], "--fedoragnome-glycin-gate-version")) {
puts(GATE_VERSION);
return 0;
}
if (geteuid() != getuid()) {
fprintf(stderr, "%s: refusing setuid/seteuid mismatch\n", GATE_VERSION);
return 126;
}
if (getuid() != (uid_t)TARGET_UID)
exec_system_bwrap(argc, argv);
const char *loader = NULL;
int loader_index = -1;
bool approved = looks_like_glycin(argc, argv, &loader, &loader_index);
char private_loader[PATH_MAX];
if (approved && !private_loader_for(loader, private_loader))
approved = false;
if (getenv("FEDORAGNOME_GLYCIN_GATE_DRYRUN")) {
printf("decision=%s\n", approved ? "dedicated-bwrap" : "user-bwrap");
if (loader) printf("loader=%s\n", loader);
if (approved) printf("private_loader=%s\n", private_loader);
return approved ? 0 : 3;
}
if (!approved) {
/*
* Only log rejected calls that appear to mention Glycin, avoiding
* noise from unrelated bwrap users. v1.2 records the bounded argv so
* legitimate Glycin variants can be added deliberately.
*/
for (int i = 1; i < argc; ++i) {
if (strstr(argv[i], "glycin")) {
log_rejected_argv(argc, argv);
break;
}
}
exec_user_bwrap(argc, argv);
}
if (loader_index < 0 || loader_index >= argc)
exec_user_bwrap(argc, argv);
argv[loader_index] = private_loader;
int nnp = prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0);
openlog("fedoragnome-glycin-gate", LOG_PID, LOG_USER);
if (nnp >= 0) {
syslog(LOG_NOTICE,
"routing approved Glycin sandbox to fedoragnome_glycin_bwrap_t; nnp=%d; loader=%s",
nnp, loader);
} else {
syslog(LOG_NOTICE,
"routing approved Glycin sandbox to fedoragnome_glycin_bwrap_t; nnp=unknown errno=%d; loader=%s",
errno, loader);
}
closelog();
exec_domain_bwrap(argc, argv);
return 127;
}
EOF_C
gcc \
-O2 -pipe \
-Wall -Wextra -Werror \
-fstack-protector-strong \
-D_FORTIFY_SOURCE=3 \
-fPIE -pie \
-Wl,-z,relro,-z,now \
-DTARGET_UID="$uid" \
-DTARGET_HOME="\"$home\"" \
-o "$STATE_DIR/bwrap.gate.new" "$SRC"
chown root:root "$STATE_DIR/bwrap.gate.new"
chmod 0755 "$STATE_DIR/bwrap.gate.new"
}
write_policy_source() {
install -d -o root -g root -m 0700 "$POLICY_DIR"
cat >"$POLICY_TE" <<'EOF_TE'
policy_module(fedoragnome_glycin_domain_test, 1.20.1)
gen_require(`
role user_r;
type user_t;
type staff_t;
type sysadm_t;
type proc_t;
type tmpfs_t;
type fs_t;
type user_tmp_t;
type devpts_t;
type fonts_cache_t;
type root_t;
type tmp_t;
type nsfs_t;
type init_var_run_t;
type systemd_hwdb_etc_t;
type dma_device_t;
type dri_device_t;
type sound_device_t;
')
#
# Entry/gate domain.
#
type fedoragnome_glycin_gate_t;
type fedoragnome_glycin_gate_exec_t;
domain_type(fedoragnome_glycin_gate_t)
domain_entry_file(fedoragnome_glycin_gate_t, fedoragnome_glycin_gate_exec_t)
#
# Dedicated capability-bearing bwrap domain.
#
type fedoragnome_glycin_bwrap_t;
type fedoragnome_glycin_bwrap_exec_t;
domain_type(fedoragnome_glycin_bwrap_t)
domain_entry_file(fedoragnome_glycin_bwrap_t, fedoragnome_glycin_bwrap_exec_t)
#
# Non-privileged fallback bwrap entrypoint and private loader copies.
#
type fedoragnome_glycin_user_bwrap_exec_t;
files_type(fedoragnome_glycin_user_bwrap_exec_t)
type fedoragnome_glycin_loader_t;
type fedoragnome_glycin_loader_real_exec_t;
domain_type(fedoragnome_glycin_loader_t)
domain_entry_file(fedoragnome_glycin_loader_t, fedoragnome_glycin_loader_real_exec_t)
# Dedicated type for frame memfds created by the decoder.
type fedoragnome_glycin_frame_t;
files_type(fedoragnome_glycin_frame_t)
# All three custom process domains remain under user_r.
role user_r types fedoragnome_glycin_gate_t;
role user_r types fedoragnome_glycin_bwrap_t;
role user_r types fedoragnome_glycin_loader_t;
# user_t may enter only the compiled validator gate.
domtrans_pattern(user_t, fedoragnome_glycin_gate_exec_t, fedoragnome_glycin_gate_t)
# Rejected/non-Glycin target-user calls drop back to ordinary user_t bwrap.
domtrans_pattern(fedoragnome_glycin_gate_t, fedoragnome_glycin_user_bwrap_exec_t, user_t)
# Only the gate domain can enter the capability-bearing bwrap domain.
domtrans_pattern(fedoragnome_glycin_gate_t, fedoragnome_glycin_bwrap_exec_t, fedoragnome_glycin_bwrap_t)
# Real Fedora GNOME/Glycin execution proved the approved bwrap exec stayed in
# gate_t. SELinux has a separate process2 permission for automatic transitions
# while no_new_privs is set. Scope it to this single trusted transition only.
allow fedoragnome_glycin_gate_t fedoragnome_glycin_bwrap_t:process2 nnp_transition;
# The administrator's staff_t/sysadm_t PATH may also encounter /usr/local/bin/bwrap.
# They execute the gate without a transition; the gate detects non-target UID and
# execs the stock /usr/bin/bwrap normally.
allow staff_t fedoragnome_glycin_gate_exec_t:file { getattr open read execute execute_no_trans map };
allow sysadm_t fedoragnome_glycin_gate_exec_t:file { getattr open read execute execute_no_trans map };
#
# Gate runtime: no namespace capability is granted here.
#
libs_use_ld_so(fedoragnome_glycin_gate_t)
libs_use_shared_libs(fedoragnome_glycin_gate_t)
files_read_usr_files(fedoragnome_glycin_gate_t)
files_read_etc_files(fedoragnome_glycin_gate_t)
logging_send_syslog_msg(fedoragnome_glycin_gate_t)
userdom_use_inherited_user_terminals(fedoragnome_glycin_gate_t)
# v1.13: preserve Glycin's inherited writable seccomp memfd across the
# user_t -> gate_t transition. This is intentionally inherited-FD-only:
# no open/create/manage permission is added to gate_t for user_tmp_t.
userdom_write_inherited_user_tmp_files(fedoragnome_glycin_gate_t)
# journald's /run/systemd parent is init_var_run_t on this Fedora build.
allow fedoragnome_glycin_gate_t init_var_run_t:dir search;
# The gate may inspect private loader metadata but may NOT execute loaders.
allow fedoragnome_glycin_gate_t fedoragnome_glycin_loader_real_exec_t:file getattr;
# The fallback returns to user_t. The real boot path additionally showed
# the ELF mapping check on this private executable, so grant exactly map plus
# the already-required entrypoint permission.
allow user_t fedoragnome_glycin_user_bwrap_exec_t:file { getattr open read execute entrypoint map };
allow fedoragnome_glycin_gate_t self:process setexec;
allow fedoragnome_glycin_gate_t user_t:fd use;
allow fedoragnome_glycin_gate_t user_t:fifo_file { getattr ioctl read write };
allow fedoragnome_glycin_gate_t user_t:unix_stream_socket { getattr ioctl read write getopt setopt shutdown };
allow fedoragnome_glycin_gate_t user_t:unix_dgram_socket { getattr ioctl read write getopt setopt shutdown };
#
# Dedicated bwrap runtime mechanics carried over from the earlier proven
# bravevault_bwrap_t design, but without generic bin_t/shell execution.
#
allow fedoragnome_glycin_bwrap_t user_t:fd use;
allow fedoragnome_glycin_bwrap_t fedoragnome_glycin_gate_t:fd use;
allow fedoragnome_glycin_bwrap_t user_t:fifo_file { getattr ioctl read write };
allow fedoragnome_glycin_bwrap_t user_t:unix_stream_socket { getattr ioctl read write getopt setopt shutdown };
allow fedoragnome_glycin_bwrap_t user_t:unix_dgram_socket { getattr ioctl read write getopt setopt shutdown };
allow fedoragnome_glycin_bwrap_t self:process { setcap setrlimit setsched signal signull };
allow fedoragnome_glycin_bwrap_t self:fifo_file manage_fifo_file_perms;
allow fedoragnome_glycin_bwrap_t self:unix_stream_socket create_stream_socket_perms;
allow fedoragnome_glycin_bwrap_t self:unix_dgram_socket create_socket_perms;
allow fedoragnome_glycin_bwrap_t self:netlink_route_socket rw_netlink_socket_perms;
allow fedoragnome_glycin_bwrap_t self:shm create_shm_perms;
allow fedoragnome_glycin_bwrap_t self:sem create_sem_perms;
allow fedoragnome_glycin_bwrap_t self:cap_userns { net_admin setpcap sys_admin sys_ptrace };
allow fedoragnome_glycin_bwrap_t self:user_namespace create;
allow fedoragnome_glycin_bwrap_t nsfs_t:file getattr;
# Glycin's caller-created seccomp memfd is inherited by FD. Do not grant open
# or directory search here: only operate on an inherited user_tmp_t object.
allow fedoragnome_glycin_bwrap_t user_tmp_t:file { getattr read write };
libs_use_ld_so(fedoragnome_glycin_bwrap_t)
libs_use_shared_libs(fedoragnome_glycin_bwrap_t)
files_read_non_security_files(fedoragnome_glycin_bwrap_t)
files_map_non_security_files(fedoragnome_glycin_bwrap_t)
files_mounton_rootfs(fedoragnome_glycin_bwrap_t)
files_mounton_generic_tmp_dirs(fedoragnome_glycin_bwrap_t)
fs_getattr_all_fs(fedoragnome_glycin_bwrap_t)
fs_rw_inherited_tmpfs_files(fedoragnome_glycin_bwrap_t)
fs_mounton_tmpfs(fedoragnome_glycin_bwrap_t)
fs_all_mount_fs_perms_xattr_fs(fedoragnome_glycin_bwrap_t)
fs_all_mount_fs_perms_tmpfs(fedoragnome_glycin_bwrap_t)
fs_manage_tmpfs_dirs(fedoragnome_glycin_bwrap_t)
fs_manage_tmpfs_files(fedoragnome_glycin_bwrap_t)
fs_manage_tmpfs_symlinks(fedoragnome_glycin_bwrap_t)
allow fedoragnome_glycin_bwrap_t tmpfs_t:file mounton;
miscfiles_read_fonts(fedoragnome_glycin_bwrap_t)
term_mount_pty_fs(fedoragnome_glycin_bwrap_t)
userdom_read_user_home_content_files(fedoragnome_glycin_bwrap_t)
optional_policy(`
miscfiles_mounton_fonts_cache_dirs(fedoragnome_glycin_bwrap_t)
')
optional_policy(`
userdom_mounton_tmp_files(fedoragnome_glycin_bwrap_t)
')
# The validator rewrites approved Glycin loader paths directly to a root-owned
# private real-loader file. Execution of that typed file is now the transition
# point from capability-bearing bwrap_t into the dedicated decoder domain.
domtrans_pattern(fedoragnome_glycin_bwrap_t, fedoragnome_glycin_loader_real_exec_t, fedoragnome_glycin_loader_t)
# The decoder must be able to map/enter its own executable image. user_t gets
# none of these permissions, preventing direct bypass of gate_t/bwrap_t.
allow fedoragnome_glycin_loader_t fedoragnome_glycin_loader_real_exec_t:file {
getattr open read execute entrypoint map
};
# Final loader transition may occur with no_new_privs and/or from a nosuid
# sandbox mount. Scope both exceptions only to bwrap_t -> loader_t.
allow fedoragnome_glycin_bwrap_t fedoragnome_glycin_loader_t:process2 {
nnp_transition nosuid_transition
};
# Minimal measured loader runtime. loader_t is enforcing; do not broaden this set
# without a reproduced AVC or a separately justified protocol requirement.
libs_use_ld_so(fedoragnome_glycin_loader_t)
libs_use_shared_libs(fedoragnome_glycin_loader_t)
files_read_usr_files(fedoragnome_glycin_loader_t)
files_read_etc_files(fedoragnome_glycin_loader_t)
dev_read_urand(fedoragnome_glycin_loader_t)
miscfiles_read_fonts(fedoragnome_glycin_loader_t)
userdom_read_inherited_user_home_content_files(fedoragnome_glycin_loader_t)
userdom_rw_inherited_user_tmp_files(fedoragnome_glycin_loader_t)
# Measured under enforcing v1.20: a caller-created /memfd:glycin-frame can
# arrive as user_tmp_t. The generic domain->file_type map rule is conditional
# on domain_can_mmap_files, which is intentionally OFF. Permit only this
# decoder domain to map the inherited user_tmp_t frame; do not enable the
# broad boolean and do not grant generic user_t/tmpfs access.
allow fedoragnome_glycin_loader_t user_tmp_t:file map;
fs_rw_inherited_tmpfs_files(fedoragnome_glycin_loader_t)
# Measured in the c5 learning run across JXL, SVG and image-rs: the only
# remaining loader_t denial was tmpfs_t:lnk_file read. Inside the bwrap
# namespace /lib64 is a tmpfs_t symlink; the dynamic loader must read that link.
allow fedoragnome_glycin_loader_t tmpfs_t:lnk_file read;
# Measured failure showed enforcing user_t gly-global-exec denied read/write
# on /memfd:glycin-frame objects that loader_t created as generic tmpfs_t.
# Give only loader-created tmpfs files a dedicated frame type.
fs_tmpfs_filetrans(fedoragnome_glycin_loader_t, fedoragnome_glycin_frame_t, file)
manage_files_pattern(fedoragnome_glycin_loader_t, fedoragnome_glycin_frame_t, fedoragnome_glycin_frame_t)
allow fedoragnome_glycin_loader_t fedoragnome_glycin_frame_t:file map;
# The user_t-side Glycin coordinator receives these memfds by descriptor.
# Permit it to consume only the dedicated frame type; do not grant generic
# tmpfs_t access.
allow user_t fedoragnome_glycin_frame_t:file { getattr read write map };
# Preserve inherited/passed descriptors used by the Glycin protocol.
allow fedoragnome_glycin_loader_t user_t:fd use;
allow fedoragnome_glycin_loader_t fedoragnome_glycin_gate_t:fd use;
allow fedoragnome_glycin_loader_t fedoragnome_glycin_bwrap_t:fd use;
allow fedoragnome_glycin_loader_t user_t:fifo_file { getattr ioctl read write };
allow fedoragnome_glycin_loader_t user_t:unix_stream_socket {
getattr ioctl read write getopt setopt shutdown
};
allow fedoragnome_glycin_loader_t user_t:unix_dgram_socket {
getattr ioctl read write getopt setopt shutdown
};
#
# Complete v2.4j-v2.4r + v2.4u-v2.4v bwrap/Glycin AVC-confirmed chain.
# These permissions used to be placed on user_t; they now live ONLY here.
#
allow fedoragnome_glycin_bwrap_t proc_t:dir mounton;
allow fedoragnome_glycin_bwrap_t proc_t:filesystem mount;
# netlink_route_socket is already granted above via rw_netlink_socket_perms.
allow fedoragnome_glycin_bwrap_t tmpfs_t:filesystem mount;
allow fedoragnome_glycin_bwrap_t fs_t:filesystem remount;
allow fedoragnome_glycin_bwrap_t user_tmp_t:file mounton;
allow fedoragnome_glycin_bwrap_t devpts_t:filesystem mount;
allow fedoragnome_glycin_bwrap_t fs_t:filesystem unmount;
allow fedoragnome_glycin_bwrap_t fonts_cache_t:dir mounton;
allow fedoragnome_glycin_bwrap_t tmpfs_t:filesystem unmount;
allow fedoragnome_glycin_bwrap_t root_t:dir mounton;
allow fedoragnome_glycin_bwrap_t tmp_t:dir mounton;
#
# Earlier Fedora GNOME user_u compatibility findings from the known-working
# savepoint-07 baseline. These are desktop/render/device permissions, not
# namespace privileges, and remain on user_t for functional equivalence.
#
allow user_t systemd_hwdb_etc_t:file { getattr open read map };
allow user_t dma_device_t:chr_file { getattr ioctl open read write map };
allow user_t dri_device_t:chr_file { getattr ioctl open read write map };
allow user_t sound_device_t:chr_file { getattr ioctl open read write map };
EOF_TE
cat >"$POLICY_FC" <<'EOF_FC'
/usr/local/bin/bwrap -- gen_context(system_u:object_r:fedoragnome_glycin_gate_exec_t,s0)
/usr/local/libexec/fedoragnome-glycin-domain/bwrap-domain -- gen_context(system_u:object_r:fedoragnome_glycin_bwrap_exec_t,s0)
/usr/local/libexec/fedoragnome-glycin-domain/bwrap-user -- gen_context(system_u:object_r:fedoragnome_glycin_user_bwrap_exec_t,s0)
/usr/local/libexec/fedoragnome-glycin-domain/loaders-real/glycin-image-rs -- gen_context(system_u:object_r:fedoragnome_glycin_loader_real_exec_t,s0)
/usr/local/libexec/fedoragnome-glycin-domain/loaders-real/glycin-svg -- gen_context(system_u:object_r:fedoragnome_glycin_loader_real_exec_t,s0)
/usr/local/libexec/fedoragnome-glycin-domain/loaders-real/glycin-heif -- gen_context(system_u:object_r:fedoragnome_glycin_loader_real_exec_t,s0)
/usr/local/libexec/fedoragnome-glycin-domain/loaders-real/glycin-jxl -- gen_context(system_u:object_r:fedoragnome_glycin_loader_real_exec_t,s0)
EOF_FC
}
build_policy() {
rm -f -- "$POLICY_PP" "$POLICY_DIR/$POLICY_MODULE.mod"
(
cd "$POLICY_DIR"
make -f /usr/share/selinux/devel/Makefile "$POLICY_MODULE.pp"
)
[[ -s "$POLICY_PP" ]] || die "SELinux module build produced no $POLICY_PP"
}
install_private_files() {
local loader
install -d -o root -g root -m 0755 \
"$PRIVATE_DIR" "$PRIVATE_REAL_LOADERS"
install -o root -g root -m 0755 "$STATE_DIR/bwrap.gate.new" "$GATE"
install -o root -g root -m 0755 "$SYSTEM_BWRAP" "$DOMAIN_BWRAP"
install -o root -g root -m 0755 "$SYSTEM_BWRAP" "$USER_BWRAP"
for loader in "${LOADERS[@]}"; do
[[ -f "$SYSTEM_LOADER_DIR/$loader" && ! -L "$SYSTEM_LOADER_DIR/$loader" ]] ||
die "missing Fedora Glycin loader: $SYSTEM_LOADER_DIR/$loader"
[[ "$(stat -c '%u' "$SYSTEM_LOADER_DIR/$loader")" == 0 ]] ||
die "system Glycin loader not root-owned: $loader"
(( (8#$(stat -c '%a' "$SYSTEM_LOADER_DIR/$loader") & 8#022) == 0 )) ||
die "system Glycin loader is group/other writable: $loader"
install -o root -g root -m 0755 \
"$SYSTEM_LOADER_DIR/$loader" "$PRIVATE_REAL_LOADERS/$loader"
done
}
install_user_test_helper() {
local user="$1" uid="$2" home="$3"
cat >"$STATE_DIR/user-test.new" <<EOF_USER_TEST
#!/usr/bin/bash
set -euo pipefail
IFS=\$'\\n\\t'
EXPECTED_USER='$user'
EXPECTED_UID='$uid'
EXPECTED_HOME='$home'
GATE='/usr/local/bin/bwrap'
IMAGE_LOADER='/usr/libexec/glycin-loaders/2+/glycin-image-rs'
SVG_LOADER='/usr/libexec/glycin-loaders/2+/glycin-svg'
fail() {
printf 'FAIL %s\\n' "\$*" >&2
exit 1
}
pass() {
printf 'PASS %s\\n' "\$*"
}
actual_user="\$(id -un)"
actual_uid="\$(id -u)"
actual_ctx="\$(id -Z 2>/dev/null || true)"
[[ "\$actual_user" == "\$EXPECTED_USER" ]] ||
fail "must be run directly as \$EXPECTED_USER; current user=\$actual_user"
[[ "\$actual_uid" == "\$EXPECTED_UID" ]] ||
fail "unexpected uid=\$actual_uid expected=\$EXPECTED_UID"
case "\$actual_ctx" in
user_u:user_r:user_t:*)
pass "real desktop SELinux context: \$actual_ctx"
;;
*)
fail "not running in real user_u:user_r:user_t context: \${actual_ctx:-unknown}"
;;
esac
[[ -x "\$GATE" ]] || fail "installed gate missing: \$GATE"
run_normal_test() {
local out rc
set +e
out="\$(
FEDORAGNOME_GLYCIN_GATE_DRYRUN=1 "\$GATE" \
--unshare-all \
--die-with-parent \
--chdir / \
--ro-bind /usr /usr \
--dev /dev \
--ro-bind-try /etc/ld.so.cache /etc/ld.so.cache \
--ro-bind-try /nix/store /nix/store \
--tmpfs /tmp-home \
--tmpfs /tmp-run \
--clearenv \
--setenv HOME /tmp-home \
--setenv XDG_RUNTIME_DIR /tmp-run \
--setenv XDG_RUNTIME_DIR "/run/user/\$EXPECTED_UID" \
--symlink /usr/lib /lib \
--symlink /usr/lib64 /lib64 \
--seccomp 73 \
"\$IMAGE_LOADER" \
--dbus-fd 72 \
2>&1
)"
rc=\$?
set -e
printf '%s\\n' "\$out"
[[ \$rc -eq 0 ]] || fail "real user_t normal gate test rc=\$rc"
grep -Fq 'decision=dedicated-bwrap' <<<"\$out" ||
fail "real user_t normal invocation was not approved"
grep -Fq 'private_loader=/usr/local/libexec/fedoragnome-glycin-domain/loaders-real/glycin-image-rs' <<<"\$out" ||
fail "normal invocation did not map to private image loader"
pass "real user_t normal Glycin invocation approved by gate"
}
run_svg_test() {
local cache_path out rc
cache_path="\$EXPECTED_HOME/.cache/glycin/usr/libexec/glycin-loaders/2+/glycin-svg"
set +e
out="\$(
FEDORAGNOME_GLYCIN_GATE_DRYRUN=1 "\$GATE" \
--unshare-all \
--die-with-parent \
--chdir / \
--ro-bind /usr /usr \
--dev /dev \
--ro-bind-try /etc/ld.so.cache /etc/ld.so.cache \
--ro-bind-try /nix/store /nix/store \
--tmpfs /tmp-home \
--tmpfs /tmp-run \
--clearenv \
--setenv HOME /tmp-home \
--setenv XDG_RUNTIME_DIR /tmp-run \
--setenv XDG_RUNTIME_DIR "/run/user/\$EXPECTED_UID" \
--symlink /usr/lib /lib \
--symlink /usr/lib64 /lib64 \
--ro-bind-try /etc/fonts/conf.d /etc/fonts/conf.d \
--ro-bind-try /etc/fonts/fonts.conf /etc/fonts/fonts.conf \
--ro-bind-try "\$EXPECTED_HOME/.cache/fontconfig" "\$EXPECTED_HOME/.cache/fontconfig" \
--ro-bind-try /usr/lib/fontconfig/cache /usr/lib/fontconfig/cache \
--bind-try "\$cache_path" "\$cache_path" \
--setenv XDG_CACHE_HOME "\$cache_path" \
--seccomp 79 \
"\$SVG_LOADER" \
--dbus-fd 78 \
2>&1
)"
rc=\$?
set -e
printf '%s\\n' "\$out"
[[ \$rc -eq 0 ]] || fail "real user_t SVG gate test rc=\$rc"
grep -Fq 'decision=dedicated-bwrap' <<<"\$out" ||
fail "real user_t SVG invocation was not approved"
grep -Fq 'private_loader=/usr/local/libexec/fedoragnome-glycin-domain/loaders-real/glycin-svg' <<<"\$out" ||
fail "SVG invocation did not map to private SVG loader"
pass "real user_t SVG/fontconfig Glycin invocation approved by gate"
}
echo "===== FedoraGnome Glycin Domain v1.20a REAL USER TEST ====="
run_normal_test
run_svg_test
echo
echo "RESULT: PASS"
echo "The gate parser/validator has now been exercised directly from \$actual_ctx."
echo "This test does not fake the desktop context with runuser."
EOF_USER_TEST
install -o root -g root -m 0755 "$STATE_DIR/user-test.new" "$USER_TEST_HELPER"
}
write_fapolicyd_trust() {
command -v fapolicyd-cli >/dev/null 2>&1 || return 0
[[ -d /etc/fapolicyd ]] || return 0
install -d -o root -g root -m 0750 /etc/fapolicyd/trust.d
local tmp path size hash loader
tmp="$(mktemp /etc/fapolicyd/trust.d/.fedoragnome-glycin-domain-test.XXXXXX)"
: >"$tmp"
for path in "$GATE" "$DOMAIN_BWRAP" "$USER_BWRAP" "$USER_TEST_HELPER"; do
size="$(stat -c '%s' "$path")"
hash="$(sha256sum "$path" | awk '{print $1}')"
printf '%s %s %s\n' "$path" "$size" "$hash" >>"$tmp"
done
for loader in "${LOADERS[@]}"; do
path="$PRIVATE_REAL_LOADERS/$loader"
size="$(stat -c '%s' "$path")"
hash="$(sha256sum "$path" | awk '{print $1}')"
printf '%s %s %s\n' "$path" "$size" "$hash" >>"$tmp"
done
chown root:root "$tmp"
chmod 0644 "$tmp"
mv -f "$tmp" "$FAPOLICY_TRUST"
if systemctl is-active --quiet fapolicyd.service 2>/dev/null; then
fapolicyd-cli --update >/dev/null || die "fapolicyd trust refresh failed"
fi
}
remove_fapolicyd_trust() {
rm -f -- "$FAPOLICY_TRUST"
if command -v fapolicyd-cli >/dev/null 2>&1 &&
systemctl is-active --quiet fapolicyd.service 2>/dev/null; then
fapolicyd-cli --update >/dev/null 2>&1 || true
fi
}
trust_matches() {
command -v fapolicyd-cli >/dev/null 2>&1 || return 0
[[ -d /etc/fapolicyd ]] || return 0
[[ -r "$FAPOLICY_TRUST" ]] || return 1
local path size hash loader
local -a paths=("$GATE" "$DOMAIN_BWRAP" "$USER_BWRAP" "$USER_TEST_HELPER")
for loader in "${LOADERS[@]}"; do
paths+=("$PRIVATE_REAL_LOADERS/$loader")
done
for path in "${paths[@]}"; do
[[ -f "$path" ]] || return 1
size="$(stat -c '%s' "$path")"
hash="$(sha256sum "$path" | awk '{print $1}')"
awk -v p="$path" -v s="$size" -v h="$hash" \
'$1==p && $2==s && $3==h {found=1} END{exit !found}' \
"$FAPOLICY_TRUST" || return 1
done
}
remove_policy_module() {
if have_module "$POLICY_MODULE"; then
semodule -r "$POLICY_MODULE" >/dev/null 2>&1 || true
fi
}
uninstall_impl() {
remove_fapolicyd_trust
remove_policy_module
rm -f -- "$GATE" "$USER_TEST_HELPER"
rm -rf -- "$PRIVATE_DIR"
rm -rf -- "$STATE_DIR"
}
private_files_match_system() {
local loader
cmp -s "$SYSTEM_BWRAP" "$DOMAIN_BWRAP" || return 1
cmp -s "$SYSTEM_BWRAP" "$USER_BWRAP" || return 1
for loader in "${LOADERS[@]}"; do
cmp -s "$SYSTEM_LOADER_DIR/$loader" "$PRIVATE_REAL_LOADERS/$loader" || return 1
done
}
gate_dryrun_test() {
local user="$1" uid="$2" loader="$3"
local out rc
set +e
out="$(
runuser -u "$user" -- env FEDORAGNOME_GLYCIN_GATE_DRYRUN=1 "$GATE" \
--unshare-all \
--die-with-parent \
--chdir / \
--ro-bind /usr /usr \
--dev /dev \
--ro-bind-try /etc/ld.so.cache /etc/ld.so.cache \
--ro-bind-try /nix/store /nix/store \
--tmpfs /tmp-home \
--tmpfs /tmp-run \
--clearenv \
--setenv HOME /tmp-home \
--setenv XDG_RUNTIME_DIR /tmp-run \
--setenv XDG_RUNTIME_DIR "/run/user/$uid" \
--symlink /usr/lib /lib \
--symlink /usr/lib64 /lib64 \
--seccomp 73 \
"$loader" \
--dbus-fd 72 \
2>&1
)"
rc=$?
set -e
if [[ $rc -eq 0 && "$out" == *"decision=dedicated-bwrap"* ]]; then
return 0
fi
printf 'DRYRUN failure rc=%s\n%s\n' "$rc" "$out" >&2
return 1
}
gate_svg_dryrun_test() {
local user="$1" uid="$2" loader="$3"
local home cache_path out rc
home="$(getent passwd "$user" | awk -F: '{print $6}')"
cache_path="$home/.cache/glycin/usr/libexec/glycin-loaders/2+/glycin-svg"
set +e
out="$(
runuser -u "$user" -- env FEDORAGNOME_GLYCIN_GATE_DRYRUN=1 "$GATE" \
--unshare-all \
--die-with-parent \
--chdir / \
--ro-bind /usr /usr \
--dev /dev \
--ro-bind-try /etc/ld.so.cache /etc/ld.so.cache \
--ro-bind-try /nix/store /nix/store \
--tmpfs /tmp-home \
--tmpfs /tmp-run \
--clearenv \
--setenv HOME /tmp-home \
--setenv XDG_RUNTIME_DIR /tmp-run \
--setenv XDG_RUNTIME_DIR "/run/user/$uid" \
--symlink /usr/lib /lib \
--symlink /usr/lib64 /lib64 \
--ro-bind-try /etc/fonts/conf.d /etc/fonts/conf.d \
--ro-bind-try /etc/fonts/fonts.conf /etc/fonts/fonts.conf \
--ro-bind-try "$home/.cache/fontconfig" "$home/.cache/fontconfig" \
--ro-bind-try /usr/lib/fontconfig/cache /usr/lib/fontconfig/cache \
--bind-try "$cache_path" "$cache_path" \
--setenv XDG_CACHE_HOME "$cache_path" \
--seccomp 79 \
"$loader" \
--dbus-fd 78 \
2>&1
)"
rc=$?
set -e
if [[ $rc -eq 0 && "$out" == *"decision=dedicated-bwrap"* ]]; then
return 0
fi
printf 'SVG dry-run failure rc=%s\n%s\n' "$rc" "$out" >&2
return 1
}
policy_has() {
local src="$1" tgt="$2" cls="$3" perm="$4"
local out
if [[ "$tgt" == self ]]; then
out="$(sesearch -A -s "$src" -c "$cls" -p "$perm" 2>/dev/null || true)"
else
out="$(sesearch -A -s "$src" -t "$tgt" -c "$cls" -p "$perm" 2>/dev/null || true)"
fi
[[ -n ${out//[[:space:]]/} ]]
}
selftest() {
require_root
local user="${1:-}"
[[ -n "$user" ]] || die "selftest requires target username"
local uid
uid="$(id -u "$user" 2>/dev/null)" || die "no such user: $user"
local failures=0 loader typ
pass() { printf 'PASS %s\n' "$*"; }
fail() { printf 'FAIL %s\n' "$*" >&2; failures=$((failures+1)); }
echo "===== FedoraGnome Glycin Domain v1.20a stable candidate ====="
have_module "$POLICY_MODULE" &&
pass "dedicated SELinux module loaded" ||
fail "dedicated SELinux module missing"
gate_is_ours &&
pass "compiled validator gate installed" ||
fail "compiled validator gate missing/not ours"
[[ -x "$USER_TEST_HELPER" && "$(stat -c '%U:%G:%a' "$USER_TEST_HELPER" 2>/dev/null)" == "root:root:755" ]] &&
pass "root-owned real-user test helper installed" ||
fail "real-user test helper missing/wrong metadata"
private_files_match_system &&
pass "private bwrap/loaders match current Fedora binaries" ||
fail "private bwrap/loaders differ from current Fedora binaries"
local -A expected_types=(
["$GATE"]="fedoragnome_glycin_gate_exec_t"
["$DOMAIN_BWRAP"]="fedoragnome_glycin_bwrap_exec_t"
["$USER_BWRAP"]="fedoragnome_glycin_user_bwrap_exec_t"
)
for loader in "${LOADERS[@]}"; do
expected_types["$PRIVATE_REAL_LOADERS/$loader"]="fedoragnome_glycin_loader_real_exec_t"
done
local path
for path in "${!expected_types[@]}"; do
typ="$(source_type_of "$path")"
[[ "$typ" == "${expected_types[$path]}" ]] &&
pass "$path label=$typ" ||
fail "$path label=${typ:-missing} expected=${expected_types[$path]}"
done
sesearch -T -s user_t -c process 2>/dev/null |
grep -Fq 'type_transition user_t fedoragnome_glycin_gate_exec_t:process fedoragnome_glycin_gate_t;' &&
pass "user_t transitions only into validator gate" ||
fail "user_t -> gate transition missing"
if sesearch -A -s user_t -t fedoragnome_glycin_gate_exec_t -c file -p execute_no_trans 2>/dev/null |
grep -q '^allow '; then
fail "user_t has forbidden execute_no_trans on gate entrypoint"
else
pass "user_t has NO execute_no_trans on gate entrypoint"
fi
sesearch -T -s fedoragnome_glycin_gate_t -c process 2>/dev/null |
grep -Fq 'type_transition fedoragnome_glycin_gate_t fedoragnome_glycin_bwrap_exec_t:process fedoragnome_glycin_bwrap_t;' &&
pass "gate transitions approved calls into dedicated bwrap domain" ||
fail "gate -> dedicated bwrap transition missing"
sesearch -T -s fedoragnome_glycin_gate_t -c process 2>/dev/null |
grep -Fq 'type_transition fedoragnome_glycin_gate_t fedoragnome_glycin_user_bwrap_exec_t:process user_t;' &&
pass "gate fallback returns rejected calls to user_t" ||
fail "gate -> user_t fallback transition missing"
policy_has fedoragnome_glycin_gate_t self process setexec &&
pass "validator gate may explicitly set next exec context" ||
fail "validator gate missing self:process setexec"
policy_has user_t fedoragnome_glycin_user_bwrap_exec_t file entrypoint &&
pass "user_t fallback entrypoint permission present" ||
fail "user_t fallback entrypoint permission missing"
for perm in getattr open read execute entrypoint map; do
policy_has user_t fedoragnome_glycin_user_bwrap_exec_t file "$perm" &&
pass "user_t fallback executable $perm permission present" ||
fail "user_t fallback executable $perm permission missing"
done
policy_has fedoragnome_glycin_gate_t fedoragnome_glycin_bwrap_t process2 nnp_transition &&
pass "gate -> dedicated bwrap no_new_privs transition permission present" ||
fail "gate -> dedicated bwrap nnp_transition missing"
policy_has fedoragnome_glycin_gate_t fedoragnome_glycin_loader_real_exec_t file getattr &&
pass "gate has metadata-only real-loader getattr" ||
fail "gate missing real-loader getattr"
if policy_has fedoragnome_glycin_gate_t fedoragnome_glycin_loader_real_exec_t file execute; then
fail "gate has forbidden execute permission on private Glycin loaders"
else
pass "gate has NO execute permission on private real Glycin loaders"
fi
policy_has fedoragnome_glycin_gate_t init_var_run_t dir search &&
pass "gate has minimal /run/systemd traversal for logging" ||
fail "gate missing /run/systemd traversal for logging"
if sesearch -A -s user_t -t fedoragnome_glycin_bwrap_t -c process -p transition 2>/dev/null |
grep -q '^allow '; then
fail "user_t has forbidden direct transition to dedicated bwrap domain"
else
pass "user_t has NO direct transition to dedicated bwrap domain"
fi
for perm in execute execute_no_trans entrypoint; do
if sesearch -A -s user_t -t fedoragnome_glycin_bwrap_exec_t -c file -p "$perm" 2>/dev/null |
grep -q '^allow '; then
fail "user_t has forbidden $perm on dedicated bwrap entrypoint"
else
pass "user_t has no $perm on dedicated bwrap entrypoint"
fi
done
policy_has fedoragnome_glycin_bwrap_t self netlink_route_socket nlmsg_write &&
pass "dedicated bwrap has route-netlink setup" ||
fail "dedicated bwrap missing nlmsg_write"
local -a checks=(
"proc_t:dir:mounton"
"proc_t:filesystem:mount"
"tmpfs_t:filesystem:mount"
"fs_t:filesystem:remount"
"user_tmp_t:file:mounton"
"devpts_t:filesystem:mount"
"fs_t:filesystem:unmount"
"fonts_cache_t:dir:mounton"
"tmpfs_t:filesystem:unmount"
"root_t:dir:mounton"
"tmp_t:dir:mounton"
)
local spec tgt cls perm
for spec in "${checks[@]}"; do
tgt="${spec%%:*}"; spec="${spec#*:}"
cls="${spec%%:*}"; perm="${spec#*:}"
policy_has fedoragnome_glycin_bwrap_t "$tgt" "$cls" "$perm" &&
pass "dedicated bwrap -> $tgt:$cls $perm" ||
fail "dedicated bwrap missing $tgt:$cls $perm"
done
policy_has fedoragnome_glycin_bwrap_t fedoragnome_glycin_loader_real_exec_t file execute &&
pass "dedicated bwrap may execute private real-loader transition point" ||
fail "dedicated bwrap cannot execute private real loader"
policy_has fedoragnome_glycin_bwrap_t fedoragnome_glycin_loader_t process transition &&
pass "bwrap -> loader_t process transition permission present" ||
fail "bwrap -> loader_t process transition missing"
if sesearch -T -s fedoragnome_glycin_bwrap_t -t fedoragnome_glycin_loader_real_exec_t -c process 2>/dev/null |
grep -Fq 'type_transition fedoragnome_glycin_bwrap_t fedoragnome_glycin_loader_real_exec_t:process fedoragnome_glycin_loader_t;'; then
pass "real private loader automatically transitions bwrap_t -> loader_t"
else
fail "missing bwrap_t -> loader_t type_transition on real private loader"
fi
if sesearch -A -s fedoragnome_glycin_bwrap_t -t fedoragnome_glycin_bwrap_t -c process -p setexec 2>/dev/null | grep -q '^allow '; then
fail "dedicated bwrap unexpectedly retains self:process setexec"
else
pass "dedicated bwrap has NO setexec; final handoff uses type_transition"
fi
policy_has fedoragnome_glycin_bwrap_t fedoragnome_glycin_loader_t process2 nnp_transition &&
pass "bwrap -> loader_t nnp_transition permission present" ||
fail "bwrap -> loader_t nnp_transition missing"
policy_has fedoragnome_glycin_bwrap_t fedoragnome_glycin_loader_t process2 nosuid_transition &&
pass "bwrap -> loader_t nosuid_transition permission present" ||
fail "bwrap -> loader_t nosuid_transition missing"
if sesearch -A -s fedoragnome_glycin_bwrap_t -t fedoragnome_glycin_loader_real_exec_t -c file -p execute_no_trans 2>/dev/null |
grep -q '^allow '; then
fail "dedicated bwrap unexpectedly has execute_no_trans on real private loader"
else
pass "dedicated bwrap has NO execute_no_trans on real private loader"
fi
for perm in getattr open read execute entrypoint map; do
policy_has fedoragnome_glycin_loader_t fedoragnome_glycin_loader_real_exec_t file "$perm" &&
pass "loader_t real-loader $perm permission present" ||
fail "loader_t real-loader $perm permission missing"
done
typ="fedoragnome_glycin_loader_real_exec_t"
for perm in execute execute_no_trans entrypoint; do
if sesearch -A -s user_t -t "$typ" -c file -p "$perm" 2>/dev/null |
grep -q '^allow '; then
fail "user_t unexpectedly has $perm on $typ"
else
pass "user_t has NO $perm on $typ"
fi
done
if sesearch -T -s user_t -t "$typ" -c process 2>/dev/null |
grep -q '^type_transition '; then
fail "user_t unexpectedly has a type_transition on $typ"
else
pass "user_t has NO type_transition on $typ"
fi
if seinfo --permissive 2>/dev/null | grep -qw 'fedoragnome_glycin_loader_t'; then
fail "loader_t is unexpectedly permissive in v1.20a"
else
pass "loader_t is enforcing"
fi
policy_has fedoragnome_glycin_loader_t tmpfs_t lnk_file read &&
pass "loader_t measured tmpfs symlink read permission present" ||
fail "loader_t measured tmpfs symlink read permission missing"
policy_has fedoragnome_glycin_loader_t user_tmp_t file map &&
pass "loader_t measured inherited user_tmp_t frame map permission present" ||
fail "loader_t measured inherited user_tmp_t frame map permission missing"
if sesearch -T -s fedoragnome_glycin_loader_t -t tmpfs_t -c file 2>/dev/null |
grep -Fq 'type_transition fedoragnome_glycin_loader_t tmpfs_t:file fedoragnome_glycin_frame_t;'; then
pass "loader_t tmpfs files transition to dedicated glycin frame type"
else
fail "loader_t -> dedicated glycin frame tmpfs type_transition missing"
fi
for perm in read write; do
policy_has user_t fedoragnome_glycin_frame_t file "$perm" &&
pass "user_t glycin-frame $perm permission present" ||
fail "user_t glycin-frame $perm permission missing"
done
if sesearch -A -s user_t -t tmpfs_t -c file -p write 2>/dev/null | grep -q '^allow '; then
fail "user_t unexpectedly has generic tmpfs_t:file write"
else
pass "user_t has NO generic tmpfs_t:file write"
fi
if sesearch -A -s fedoragnome_glycin_loader_t -t fedoragnome_glycin_loader_t -c user_namespace -p create 2>/dev/null | grep -q '^allow '; then
fail "loader_t unexpectedly may create user namespaces"
else
pass "loader_t has NO user_namespace create"
fi
if sesearch -A -s fedoragnome_glycin_loader_t -t fedoragnome_glycin_loader_t -c netlink_route_socket -p nlmsg_write 2>/dev/null | grep -q '^allow '; then
fail "loader_t unexpectedly has route-netlink write"
else
pass "loader_t has NO route-netlink write"
fi
for capclass in capability cap_userns; do
if sesearch -A -s fedoragnome_glycin_loader_t -t fedoragnome_glycin_loader_t -c "$capclass" -p sys_admin 2>/dev/null | grep -q '^allow '; then
fail "loader_t unexpectedly has $capclass:sys_admin"
else
pass "loader_t has NO $capclass:sys_admin"
fi
done
local generic
for generic in bin_t shell_exec_t usr_t python_exec_t; do
if seinfo -t"$generic" >/dev/null 2>&1; then
if sesearch -A -s fedoragnome_glycin_bwrap_t -t "$generic" -c file -p execute_no_trans 2>/dev/null |
grep -q '^allow '; then
fail "dedicated bwrap has forbidden generic execute_no_trans on $generic"
else
pass "dedicated bwrap has no execute_no_trans on $generic"
fi
if sesearch -A -s fedoragnome_glycin_loader_t -t "$generic" -c file -p execute_no_trans 2>/dev/null |
grep -q '^allow '; then
fail "loader_t has forbidden generic execute_no_trans on $generic"
else
pass "loader_t has no execute_no_trans on $generic"
fi
fi
done
if sesearch -A -s fedoragnome_glycin_gate_t -t bin_t -c file -p execute 2>/dev/null |
grep -q '^allow '; then
fail "gate unexpectedly has execute on bin_t"
else
pass "gate has NO execute on bin_t (validator must not require it)"
fi
if sesearch -A -s fedoragnome_glycin_gate_t -t user_tmp_t -c file -p write 2>/dev/null |
grep -q '^allow '; then
pass "gate may write inherited user_tmp_t seccomp memfd"
else
fail "gate missing inherited user_tmp_t write needed to preserve seccomp FD"
fi
if sesearch -A -s fedoragnome_glycin_gate_t -t user_tmp_t -c file -p create 2>/dev/null |
grep -q '^allow '; then
fail "gate unexpectedly has user_tmp_t file create permission"
else
pass "gate has NO user_tmp_t file create permission"
fi
if sesearch -A -s fedoragnome_glycin_gate_t -t user_tmp_t -c file -p mounton 2>/dev/null |
grep -q '^allow '; then
fail "gate unexpectedly has user_tmp_t file mounton permission"
else
pass "gate has NO user_tmp_t file mounton permission"
fi
if assert_old_user_t_namespace_grants_absent; then
pass "bwrap mount/netlink authority remains absent from user_t"
else
fail "bwrap mount/netlink authority leaked back into user_t"
fi
local -a desktop_checks=(
"systemd_hwdb_etc_t:file:map"
"dma_device_t:chr_file:open"
"dma_device_t:chr_file:map"
"dri_device_t:chr_file:open"
"dri_device_t:chr_file:map"
"sound_device_t:chr_file:open"
"sound_device_t:chr_file:map"
)
for spec in "${desktop_checks[@]}"; do
tgt="${spec%%:*}"; spec="${spec#*:}"
cls="${spec%%:*}"; perm="${spec#*:}"
policy_has user_t "$tgt" "$cls" "$perm" &&
pass "known-working GNOME user_t rule: $tgt:$cls $perm" ||
fail "known-working GNOME user_t rule missing: $tgt:$cls $perm"
done
loader="$SYSTEM_LOADER_DIR/glycin-image-rs"
gate_dryrun_test "$user" "$uid" "$loader" &&
pass "gate parser recognizes normal Glycin invocation (UID dry-run)" ||
fail "gate normal dry-run failed"
loader="$SYSTEM_LOADER_DIR/glycin-svg"
gate_svg_dryrun_test "$user" "$uid" "$loader" &&
pass "gate parser recognizes SVG/fontconfig Glycin invocation (UID dry-run)" ||
fail "gate SVG dry-run failed"
trust_matches &&
pass "fapolicyd exact-hash trust matches private executables" ||
fail "fapolicyd trust missing/mismatched"
if [[ $failures -eq 0 ]]; then
echo
echo "RESULT: PASS"
return 0
fi
echo
echo "RESULT: FAIL ($failures)"
return 1
}
install_cmd() {
local user="${1:-}" mode="${2:-}"
[[ -n "$user" ]] || die "install requires target username"
[[ "$mode" == --online || "$mode" == --offline ]] ||
die "install requires exactly one mode: --online or --offline"
require_root
prepare_dependencies "$mode"
[[ "$(getenforce 2>/dev/null || true)" != Disabled ]] || die "SELinux is disabled"
[[ -x "$SYSTEM_BWRAP" ]] || die "missing $SYSTEM_BWRAP"
local uid home conflict
uid="$(id -u "$user" 2>/dev/null)" || die "no such user: $user"
home="$(getent passwd "$user" | awk -F: '{print $6}')"
[[ -n "$home" && "$home" == /* ]] || die "could not determine absolute home for $user"
conflict="$(conflicting_test_loaded || true)"
[[ -z "$conflict" ]] ||
die "conflicting compatibility/debug module is loaded: $conflict (uninstall it first)"
local prior_version=""
prior_version="$(installed_gate_version 2>/dev/null || true)"
if [[ "$prior_version" =~ ^FedoraGnome-GlycinDomain-test-v1\.(5|6|7|8|9|10|11|12|13|14|15|16|17|18|19a|19b|19c-loader-learning|19c1-loader-learning|19c2-user_t-control|19c3-loader-learning-inbound-process|19c4-loader-learning-inbound-dbus|19c4a-loader-learning-inbound-dbus|19c5-loader-learning-frame-type|19c6-loader-enforcing)$ ]] ||
[[ "$prior_version" == "FedoraGnome-GlycinDomain-v1.20-stable-candidate" ]] ||
[[ "$prior_version" == "FedoraGnome-GlycinDomain-v1.20a-stable-candidate" ]]; then
log "removing previously installed ${prior_version} before v1.20a install"
uninstall_impl
elif have_module "$POLICY_MODULE"; then
die "$POLICY_MODULE is already installed but the installed gate is not a recognized GlycinDomain predecessor"
elif [[ -e "$GATE" ]] && ! gate_is_ours; then
die "$GATE already exists and is not a recognized earlier GlycinDomain test"
fi
# v1.5 could leave only build-state files behind when policy compilation
# failed before its rollback trap was armed. At this point no module and no
# foreign gate are active, so remove only this test's stale build directory.
if [[ -d "$STATE_DIR" ]]; then
log "removing stale pre-install build state from an earlier failed test"
rm -rf -- "$STATE_DIR"
fi
trap 'rc=$?; trap - ERR; uninstall_impl; exit "$rc"' ERR
log "checking broad bwrap namespace permissions are absent from user_t"
assert_old_user_t_namespace_grants_absent ||
die "old broad user_t bwrap permissions are still present"
log "building validator gate"
write_gate_source "$uid" "$home"
log "writing and building dedicated SELinux policy"
write_policy_source
build_policy
log "installing private bwrap and four real Glycin loader copies"
install_private_files
log "installing SELinux module"
semodule -i "$POLICY_PP"
restorecon -RF "$GATE" "$PRIVATE_DIR"
log "installing real-user test helper"
install_user_test_helper "$user" "$uid" "$home"
log "installing fapolicyd exact-hash trust"
write_fapolicyd_trust
log "running selftest"
if ! selftest "$user"; then
trap - ERR
log "selftest failed; removing all v1.20a state"
uninstall_impl
die "installation rolled back"
fi
trap - ERR
echo
echo "Installed dedicated Fedora GNOME Glycin bwrap domain ($mode)."
echo "The old user_t mount/netlink chain remains absent."
echo "The earlier known-working GNOME hwdb/DMA/DRI/sound user_t rules are restored."
echo
echo "Before rebooting, log into $user normally and run this command with NO sudo:"
echo " $USER_TEST_HELPER"
echo
echo "It must report the real user_u:user_r:user_t context and RESULT: PASS."
echo "Then reboot, log directly into $user, and check icons."
echo
echo "Then inspect:"
echo " journalctl -b -t fedoragnome-glycin-gate --no-pager"
echo
echo "Approved calls should say:"
echo " routing approved Glycin sandbox to fedoragnome_glycin_bwrap_t; nnp=..."
echo
echo "v1.20a explicitly arms bwrap_t before the private bwrap exec."
echo "Approved Glycin calls execute typed private real loaders that transition into enforcing fedoragnome_glycin_loader_t."
echo "Decoder frame memfds transition to fedoragnome_glycin_frame_t; user_t does not receive generic tmpfs_t write access."
echo "gate_t, bwrap_t and loader_t are enforcing; user_t retains no bwrap mount/netlink rights."
echo
echo "After exercising PNG/JPEG/SVG/JXL/HEIF thumbnails, check for loader-domain AVCs with:"
echo " ausearch -m AVC -ts recent -i | grep 'scontext=user_u:user_r:fedoragnome_glycin_loader_t:'"
}
status_cmd() {
local user="${1:-}"
[[ -n "$user" ]] || die "status requires target username"
require_root
require_commands
echo "===== MODULE ====="
semodule -l | grep -E "^${POLICY_MODULE}[[:space:]]" || true
echo
echo "===== FILES ====="
ls -lZ "$GATE" "$DOMAIN_BWRAP" "$USER_BWRAP" "$PRIVATE_REAL_LOADERS"/* 2>/dev/null || true
echo
echo "===== SELFTEST ====="
selftest "$user"
}
uninstall_cmd() {
require_root
uninstall_impl
if have_module "$POLICY_MODULE"; then
die "uninstall verification failed: SELinux module remains"
fi
if [[ -e "$GATE" || -e "$USER_TEST_HELPER" || -e "$PRIVATE_DIR" || -e "$STATE_DIR" || -e "$FAPOLICY_TRUST" ]]; then
die "uninstall verification failed: v1.20a artifacts remain"
fi
log "full uninstall complete; no v1.20a state retained"
}
usage() {
cat <<EOF
Usage:
$0 install USER --online
$0 install USER --offline
$0 selftest USER
$0 status USER
$0 uninstall
EOF
}
case "${1:-}" in
install)
[[ $# -eq 3 ]] || { usage; exit 2; }
install_cmd "$2" "$3"
;;
selftest)
[[ $# -eq 2 ]] || { usage; exit 2; }
selftest "$2"
;;
status)
[[ $# -eq 2 ]] || { usage; exit 2; }
status_cmd "$2"
;;
uninstall)
[[ $# -eq 1 ]] || { usage; exit 2; }
uninstall_cmd
;;
*)
usage
exit 2
;;
esac
Last edited: