New Update Fedora Gnome confined user_u general icon display problem solution and browser multimedia capability solution

Victor M

Level 28
Verified
Top Poster
Well-known
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

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:
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 when trying to use Glycin image loader under a restricted use […]


Hi Victor, this is materially better than granting Bubblewrap’s namespace and mount permissions directly to generic user_t. The gate → dedicated bwrap_t → enforcing loader domain design is the right direction, and giving decoded frames their own type instead of allowing user_t to write arbitrary tmpfs_t objects is a meaningful least-privilege improvement.

Preserving the sandbox is important: Glycin describes unsandboxed loading as unsafe and normally selects Bubblewrap outside Flatpak. Your design fixes the SELinux integration rather than simply disabling that security layer, which is the correct objective. Glycin documents that behaviour here.

I would not yet describe the complete script as “allowing bwrap and Glycin to work and no more,” for three reasons.

Points still needing attention​


  1. The script also restores GNOME hwdb, DMA, DRI and sound rules for user_t.

That may be necessary for a usable confined desktop, but it is additional authority unrelated to Glycin. Split those rules into a separate “confined GNOME desktop compatibility” module, or document every additional permission explicitly. Otherwise the narrow claim and the actual package do not match.

2. Private loader and Bubblewrap copies create an update problem.
When Fedora updates Glycin or Bubblewrap, root-owned private copies do not update automatically. Exact-hash fapolicyd trust proves that a file still has the recorded contents; it does not prove that those contents are current. Hashes are excellent at identity and entirely indifferent to age.

The installer/status check should therefore record the source RPM NEVRA and hashes, and fail closed when the installed Fedora binaries no longer match the private copies. Ideally this should be packaged as an RPM with an update/rebuild mechanism rather than maintained as an untracked shell installation. This is particularly relevant now because upstream Glycin 2.2.1 changed handling of write-sealed memfd mappings—the same general area in which this candidate added a measured mapping permission. See the current Glycin release history.

3. The omitted policy and gate implementation are the security boundary.
The architecture sounds sound, but the beginning and end of the script cannot establish that the gate is non-bypassable. Before calling it stable, the complete review needs to confirm that the gate:

  • accepts only exact expected argument forms;
  • uses fixed absolute paths and a minimal environment;
  • rejects unexpected inherited descriptors and loader paths;
  • sets and verifies no_new_privs;
  • cannot be influenced by user-writable Glycin configuration;
  • permits transition only through the root-owned typed executables;
  • cannot route arbitrary Bubblewrap operations;
  • leaves loader_t without mount, namespace, network, persistence or unrelated execution authority.

A self-test is useful, but a PASS establishes only the properties that the test actually checks. Negative tests should include direct execution of the private loaders, arbitrary gate arguments, generic tmpfs_t writes, loader-created network sockets, and attempts to invoke the private Bubblewrap outside the approved route.

Two documentation corrections​


The mapping command is correct only when the account has no existing explicit login record. -a means add; use -m when modifying an existing record:

Code:
semanage login -l

# New explicit mapping
semanage login -a -s user_u -r s0 USERNAME

# Existing explicit mapping
semanage login -m -s user_u -r s0 USERNAME

restorecon -RFv /home/USERNAME

That distinction is defined in the semanage-login manual. The mapping takes effect on a new login, not retroactively in an existing desktop session. Installation should also be performed from a separate appropriately mapped administrative account, not from the user_u account being confined.

The security explanation should also be slightly narrower. Mapping an account to user_u:user_r:user_t does remove the broad SELinux authority of an unconfined login, and confined-user mappings are a documented hardening measure. However, it is not per-application isolation: a compromised user_t process still has the account’s Unix UID and may access that user’s files and session services where DAC and policy permit. The benefit is reduced system-level reach and fewer domain transitions, not protection of every item belonging to the same user. Red Hat’s confined-user documentation describes the context and intended benefit in those terms: Managing confined and unconfined users.

Overall: the domain separation and dedicated frame type are a strong improvement, but the private-copy update lifecycle and complete gate/policy review are the remaining blockers before I would call this a stable general solution. It would also be worth submitting the reproducible AVCs and minimal policy changes upstream to Fedora’s selinux-policy maintainers; carrying a local compatibility layer forever is where good experiments go to acquire grey hair.

Sources
 
That may be necessary for a usable confined desktop, but it is additional authority unrelated to Glycin.
That is a carry over from the original main use of this module - to isolate the browser Brave. It had to make camera and mic usable by Brave. We use video meeting services.

Here's what chatgpt says about our module in relation to what you want :

The current script proves a lot about SELinux containment, but the gate parser itself is still looser than her checklist requires.

  • Exact expected argument forms: partial. The gate rejects unknown options and requires --unshare-all, --die-with-parent, --clearenv, --seccomp, a recognized loader, and exactly --dbus-fd N afterward. But it accepts those options in flexible order and allows repeated permitted options. So it validates a grammar, not one exact canonical argv.
  • Fixed paths/minimal environment: partial. Execution paths are fixed absolute root-owned paths, and approved bwrap calls require --clearenv. But the gate process itself inherits the caller environment; it even consults FEDORAGNOME_GLYCIN_GATE_DRYRUN. That variable does not give a privilege bypass, but a stricter gate should ignore/clear inherited environment before security decisions.
  • Reject unexpected inherited FDs: not currently proven. It validates that --seccomp and --dbus-fd arguments are numeric, but it does not enumerate /proc/self/fd, verify those descriptor types/properties, or reject extra inherited FDs.
  • Set and verify no_new_privs: not currently satisfied. This is the biggest concrete gap. v1.20a only does PR_GET_NO_NEW_PRIVS and logs the result. It does not call PR_SET_NO_NEW_PRIVS, nor refuse the dedicated route when NNP is not 1.
  • No user-writable Glycin configuration influence: not fully proven. The gate intentionally accepts some paths under the user's home, including Glycin cache paths and broadly allows read-only binds of TARGET_HOME content. That may be functionally required, but it means we cannot yet claim the gate is independent of user-writable Glycin state.
  • Transition only through root-owned typed executables: strong. The private loaders are restricted to four explicit names, checked root-owned/non-group/world-writable, and SELinux selftests verify the typed transition chain and deny direct user_t execution. This part is good.
  • Cannot route arbitrary Bubblewrap operations: fairly strong, but not absolute. Unknown options are rejected, yet the allowed grammar still permits combinations/repetitions of permitted bwrap operations rather than comparing against narrowly defined canonical templates.
  • loader_t lacks dangerous authority: strong for the things we explicitly test. We verify no loader user_namespace create, no route-netlink write, no sys_admin, and no generic shell/Python/bin execution. But “no network/persistence/unrelated execution authority” deserves a broader negative-policy audit than the current handful of checks.
So her central point is right: we have established that the dedicated loader domain works and is strongly confined, but we have not yet established that /usr/local/bin/bwrap is a non-bypassable admission gate.

At one point, I had the private objects update themselves on dnf update events. But I had to jump a few versions backwards and lost that feature and did not re-implement it.

To the folks that don't like AI coding: are you crazy ? I spent 2-3 days working on this. It would have taken weeks were not for AI's efforts.
 
Last edited:
That is a carry over from the original main use of this module - to isolate the browser Brave. It had to make camera and mic usable by Brave. Here's what chatgpt says about our module in relation to what you want : The current script proves a lot about SELinux containment, but the gate parser itself is still looser than her checklist requires. Exact expected argument forms: partial. The gate rejects unknown options and requires --unshare-all, --die-with-parent, --clearenv, --seccomp, a recognized loader, and exactly --dbus-fd N afterward. But it accepts those options in flexible order and allows repeated permitted options. So it validates a grammar, not one exact canonical argv. Fixed paths/minimal environment: partial. Execution paths are fixed absolute root-owned path […]


That history explains why the multimedia rules are present, but it also confirms the scope problem. If those allows target user_t, they are available to every process that remains in user_t, not specifically Brave. Mapping the login to user_u confines the whole session relative to an unconfined login; it does not provide SELinux isolation between Brave and peer user_t applications.

I would therefore split this into:

  • a GNOME confined-desktop compatibility module containing the hwdb/DRI/DMA/audio/camera-related permissions; and
  • the Glycin gate/Bubblewrap/loader/frame module.

If retaining one package is intentional, “Glycin and confined GNOME multimedia compatibility” would be a more accurate scope than “bwrap and Glycin, and no more.”

The second assessment is mostly fair, but one item from my earlier checklist needs correction: I made it sound as though the gate itself must set no_new_privs. That was too prescriptive.

Upstream Bubblewrap already calls PR_SET_NO_NEW_PRIVS and aborts if that operation fails, as shown in its source. The flag then survives fork and exec. Setting it in the gate before the gate_t → bwrap_t exec is not a free hardening switch: SELinux transitions under NNP can require the specific process2:nnp_transition permission, as explained by the SELinux notebook. The kernel’s NNP documentation also explains why LSM transitions need special handling.

The correct invariant is:

Before the loader payload executes, NNP must be 1; failure to establish it must terminate the route.

That should be verified against the actual Fedora Bubblewrap binary copied by the installer and tested at the loader boundary. Do not blindly add broader nnp_transition permissions merely to make an earlier gate-side setting work.

Two other nuances:

  • Flexible option order is not automatically a bypass. A rigorously implemented grammar can accept equivalent orderings safely. Canonical templates are nevertheless much easier to audit. Duplicate singleton options, repeated bind destinations and state-dependent combinations deserve rejection because that is where parser surprises tend to live.
  • User-writable input is unavoidable for an image loader. The gate cannot be independent of user-controlled image files. The important distinction is that user-controlled state must not choose the executable, alter security-sensitive sandbox construction or inject plugins/configuration that changes the trust boundary. Broad read-only exposure of the home directory is also a confidentiality concern even if it grants no write authority.

The inherited-FD point remains an unproven property rather than an established vulnerability. Bubblewrap contains logic to close extra descriptors, but the gate should still validate the descriptors it explicitly accepts: distinct FD numbers, expected access modes and appropriate object types. The production gate should also stop consulting the dry-run environment variable, or restrict that mechanism to a separately built root-only test binary.

So the central conclusion still stands: the domain architecture is substantially improved, but the non-bypassability claim needs a parser/FD/environment audit. The next revision should prioritize:

  1. splitting the unrelated desktop multimedia rules;
  2. rejecting duplicate and ambiguous argument combinations;
  3. validating accepted descriptors;
  4. proving NNP at the loader boundary rather than necessarily setting it in the gate;
  5. auditing the loaded policy for unexpected loader authority; and
  6. detecting stale private Bubblewrap/Glycin copies after Fedora updates.

That would move “stable candidate” considerably closer to “stable,” rather than merely asking the label to do the engineering.

Sources
 
That history explains why the multimedia rules are present, but it also confirms the scope problem. If those allows target user_t, they are available to every process that remains in user_t, not specifically Brave. Mapping the login to user_u confines the whole session relative to an unconfined login; it does not provide SELinux isolation between Brave and peer user_t applications.
Perhaps I should frame it as an icon display problem solution as well as browser capability solution. Who doesn't use a browser nowadays? But I hit the icon display problem immediately after confining the user account to user_u and that notion stuck in my brain.
 
Perhaps I should frame it as an icon display problem solution as well as browser capability solution. Who doesn't use a browser nowadays? But I hit the icon display problem immediately after confining the user account to user_u and that notion stuck in my brain.


That framing would be more accurate. The icon failure was the immediate symptom, but the finished project now addresses two distinct compatibility gaps in a confined Fedora GNOME session:

  1. Glycin/Nautilus thumbnails and icons.
  2. Browser multimedia functionality such as camera, microphone, audio and graphics acceleration.

A suitable title might be:

Fedora GNOME user_u desktop compatibility: Glycin thumbnails and browser multimedia

I would still keep the policy components separate, even if one installer enables both by default:

  • fedoragnome-glycin — gate, Bubblewrap, loaders and frame types.
  • fedoragnome-desktop-media — camera, microphone, audio, DRI and related desktop permissions.

That is not merely tidiness. Camera and microphone access are sensitive capabilities, so administrators should be able to see and install them independently from thumbnail support. Browsers are ubiquitous, certainly—but ubiquity does not make a microphone part of an image decoder’s job description.

One wording distinction remains important: if Brave runs as ordinary user_t, those multimedia rules make the capabilities available to Brave and other eligible user_t processes, subject to the remaining DAC, device, portal and session controls. They do not isolate Brave specifically from peer applications. So I would call it a browser capability solution, not a Brave isolation solution, unless Brave transitions into its own dedicated SELinux domain.

A defensible summary would be:

This project restores selected GNOME desktop functionality for accounts mapped to user_u, including sandboxed Glycin thumbnail decoding and browser multimedia support. Glycin’s Bubblewrap and loader operations are placed in dedicated SELinux domains, while optional desktop-media policy grants the confined session the capabilities required for video meetings.

That describes what the project actually does without underselling the icon work or overstating the isolation boundary.
 
So I would call it a browser capability solution, not a Brave isolation solution,
Yes that is reasonable, just looking at this module alone.

I extracted this module for the general forum public to use. They gain a working user_u confinement capability.
 
Last edited:
Yes that is reasonable, just looking at this module alone.


Exactly. This module should be described by what it independently enforces:

  • restores selected browser multimedia capabilities to the confined desktop session;
  • routes Glycin/Bubblewrap work through dedicated SELinux domains;
  • fixes the thumbnail/icon failure under user_u.

If a separate component transitions Brave into its own SELinux domain, then the combined setup may reasonably be presented as Brave isolation. Documenting that as a separate layer keeps the security boundary clear and avoids making this module claim credit for controls implemented elsewhere.

So “Fedora GNOME user_u desktop compatibility: Glycin thumbnails and browser multimedia” fits the present scope well.
 

Recently browsing

Members who viewed this thread in the last 5 minutes

You may also like...

Back
Top