# QKiSS AppArmor profile - /etc/apparmor.d/qkiss
#
# Goal: contain the external LZH extractors lha and unar, which parse untrusted
# archives. QKiSS itself is NOT confined (`allow all,`): a GUI app needs too much
# (X11 / Wayland, D-Bus, input methods, audio, fonts) to be confined without breaking.
# It only gets the denies in qkiss-deny-high-value.
#
# Needs AppArmor 4.1+: on 4.0.x, `allow all,` together with `/** pux` fails with
# "conflicting x modifiers".
# Not flags=(default_allow): aa-complain / aa-enforce replace that flag, which turns
# this profile into a default-deny profile that breaks QKiSS.
# Not flags=(unconfined): it ignores the deny rules and the Cx transitions.
#
# Tested in enforce mode on openSUSE Tumbleweed (apparmor_parser 5.0.2, Linux 7.2.8):
# lha, unar and "Open this item" work, lha and unar run in their child profiles, and
# the settings are saved.
# Only compiled, not run, with apparmor_parser 4.1.0 (Debian 13).

abi <abi/4.0>,

include <tunables/global>

# Two kinds of files that AppArmor cannot name are denied whatever the rules say, even with
# `allow all` (both seen on openSUSE Tumbleweed):
# - attach_disconnected: QKiSS logs to journald, whose socket can be a "disconnected path"
#   ("systemd/journal/socket"). This flag attaches it to /. The man page calls it unsafe: an
#   attached path can alias another one and slip past the deny rules. Getting a disconnected
#   path needs mounts or namespaces, which are denied.
# - mediate_deleted: Qt saves files (e.g. ~/.config/qkiss/qkiss.conf) as an unnamed O_TMPFILE
#   that it then links into place. Without this flag, that link is denied ("deleted entry")
#   and the settings are never saved.
profile qkiss /usr/{,local/}bin/qkiss flags=(attach_disconnected, mediate_deleted) {
    include <abstractions/qkiss-deny-high-value>

    allow all,

    # Stacked transitions ("&"): QKiSS's own Landlock / seccomp sandbox
    # (src/extractorsandbox.cpp) sets no_new_privs on lha / unar, and under no_new_privs
    # a plain "Cx -> lha" fails with EPERM.
    # Write the full name: the parser puts "qkiss//" in front of a Cx target, so "&lha"
    # would become "qkiss//&lha" (a stack with a top-level profile "lha", which does not
    # exist) and the exec would be denied. Upstream's plasmashell profile does the same.
    # Matched by the file name, in any folder (e.g. ~/.local/bin/lha too).
    # `priority=1`: these globs and `/** pux` below match the same files, which the parser
    # rejects as "conflicting x modifiers" unless one has a higher priority. A higher
    # priority completely replaces the lower-priority rules on these files, `allow all`
    # included, hence the `r`.
    # AppArmor matches the resolved path: /usr/bin/lha can be a symlink to lhasa or jlha
    # (e.g. made by update-alternatives). jlha (jlha-utils) is a Java program and is not
    # covered: it falls through to `pux` below, which no_new_privs does not allow. So it
    # cannot start while this profile is loaded.
    priority=1 /**/{lha,lhasa} rCx -> &qkiss//lha,
    priority=1 /**/unar        rCx -> &qkiss//unar,

    # "Open this item" (QDesktopServices::openUrl) launches arbitrary apps: they use
    # their own profile if they have one, else they run unconfined.
    /** pux,

    profile lha {
        include <abstractions/base>
        include <abstractions/qkiss-deny-high-value>

        /**/{lha,lhasa} mr,

        # The archive can be anywhere the user can read, so reads are broad. This is a
        # tradeoff, not secrecy: there is no network, exec or IPC, but whatever is read
        # can still flow back to QKiSS through the extractor's output.
        /** r,

        audit deny network,
        # "deny unix," is not enforced on kernels without fine-grained AF_UNIX mediation
        # (e.g. Linux 6.12), "deny network unix," is. Blocks X11 / Wayland / D-Bus.
        audit deny network unix,

        # QProcess::kill() / terminate()
        signal (receive) peer=qkiss,

        # Site-specific additions and overrides.
        include if exists <local/qkiss-lha>
    }

    profile unar {
        include <abstractions/base>
        include <abstractions/qkiss-deny-high-value>

        /**/unar mr,

        # Same reason as in the lha profile.
        /** r,
        # The only writable place: the QTemporaryDir of
        # AbstractArchiveProcessor::generateTempDirPath(). Keep the name in sync.
        # unar fails if TMPDIR points somewhere else. The glob also matches the user's
        # other qtkiss-extracted-* dirs, not only the one of the current run.
        owner /tmp/qtkiss-extracted-*/{,**} rw,

        audit deny network,
        audit deny network unix,

        signal (receive) peer=qkiss,

        # Site-specific additions and overrides.
        include if exists <local/qkiss-unar>
    }

    # Site-specific additions and overrides.
    include if exists <local/qkiss>
}
