Install and Persistence¶
Something survives the build. Install hooks run as root at install time, which makes them the highest-privilege code a PKGBUILD carries (R007, H017, H023, H035), and units, pacman hooks and setuid bits keep running long after makepkg exits (R053, R054, R059, H039, H062, H084).
R017 and R053/R059 split the same operation by target: a setuid bit inside
$pkgdir is Electron packaging, the same bit on an absolute path is a
privilege change to the build host. R052 and H032 cover the user-profile
form, where the persistence is a dotfile rather than a unit.
See the rule system reference for the field table, the severity weights and the reserved identifier ranges.
Rules on this page¶
| Rule | Name | Severity |
|---|---|---|
| H017 | Install Hook Fetches Or Executes | HIGH |
| H023 | Install Hook Present | INFO |
| H032 | Write To User Home Or RC | HIGH |
| H035 | Foreign Package Manager In Install Hook | HIGH |
| H039 | Systemd ExecStart From Runtime-Writable Path | HIGH |
| H062 | Pacman Hook Installed | MEDIUM |
| H084 | Service ExecStart Targets Undeclared Binary | HIGH |
| H089 | Packaged File Names A Build-Only Path | HIGH |
| H093 | Committed Config Points At A Build-Only Path | HIGH |
| H095 | Boot Or Image Artifact Built From The Source Tree | HIGH |
| R007 | Install File Modification | MEDIUM |
| R017 | Setuid/Setgid Permission | HIGH |
| R052 | Dotfile Written To User Profile | HIGH |
| R053 | Setuid Or Setgid Bit Set In Package Root | MEDIUM |
| R054 | Persistence Unit Outside Package Root | HIGH |
| R059 | Setuid Or Setgid Bit Set Outside Package Root | HIGH |
| R144 | Packaged File Points At A World-Writable Path | HIGH |
R007: Install File Modification¶
- Target:
raw_line - Severity: MEDIUM (weight 15)
- Category:
installer - Pattern:
^\+.*\.install - Scope: All lines (no function-body restriction)
- Description: Fires when a
.installfile is added or modified in the diff. Install scripts run with root privileges and are a common vector for persistent backdoors. - Note: The pattern was
\+.*\.install.*before 0.13.2. The anchored form bounds the search to an added diff line and matches the same intended file paths.trustsight config sync-rules --updatereplaces the superseded pattern in an unmodified local rule file.
R017: Setuid/Setgid Permission¶
- Target:
raw_line - Severity: HIGH (weight 25)
- Category:
privilege - Pattern:
chmod.*\+s - Description: Detects symbolic
chmod ... +scommands that set a setuid or setgid bit. A setuid binary runs with its owner's privileges, the shape of many local-privilege-escalation backdoors. R053 and R059 own the target-specific symbolic and octal forms, so R017 defers to them rather than scoring one command twice.
R052: Dotfile Written To User Profile¶
- Target:
raw_line - Severity: HIGH (weight 25)
- Category:
persistence - Pattern:
\b(?:install|cp|mv|tee)\s+[^;&|]*(?:\$HOME|~|/root|/home/[^/\s]+)/\.\w+ - Description: Detects writes to a dotfile under
$HOME,~,/root, or/home/<user>, the shape of shell-profile persistence. Dotfiles written inside$pkgdir(such as/etc/skeltemplates) are ordinary packaging and do not match.
R053: Setuid Or Setgid Bit Set In Package Root¶
- Target:
raw_line - Severity: MEDIUM (weight 15)
- Category:
privilege - Pattern:
\bchmod\s+(?:-\S+\s+)*(?:(?:--mode=)?(?:[2467][0-7]{3}\b|[ugoa]*\+s\b))(?:\s+--\s+(?!["\x27]?/)|\s+(?!--\s)(?!["\x27]?/))|\bsetcap\s+(?:-\S+\s+)*["\x27]?cap_\w+[^\s]*\s+(?!["\x27]?/) - Description: Setuid or setgid applied to a path being staged into the package. Detects both octal (
4755,2755) and symbolic (u+s) forms; ordinary modes such as644,755and+xdo not match. Chromium's sandbox helper legitimately requires4755, so this fires on essentially every Electron package. Measured across the benign corpus, MEDIUM changes no package's risk band; the evidence stays visible in the tiered breakdown without reclassifying routine updates. At HIGH it would have reclassified every Electron package as Medium.
R059: Setuid Or Setgid Bit Set Outside Package Root¶
- Target:
raw_line - Severity: HIGH (weight 25)
- Category:
privilege - Pattern:
\bchmod\s+(?:-\S+\s+)*(?:(?:--mode=)?(?:[2467][0-7]{3}\b|[ugoa]*\+s\b))(?:\s+--\s+["\x27]?/|\s+(?!--\s)["\x27]?/)|\bsetcap\s+(?:-\S+\s+)*["\x27]?cap_\w+[^\s]*\s+["\x27]?/ - Description: The same operation against an absolute path. This touches the live filesystem rather than
$pkgdir, so it is a privilege change on the build host and not packaging. Split from R053 because the two are materially different:chmod u+s "$pkgdir/opt/x/chrome-sandbox"is ordinary Electron packaging, whilechmod u+s "/usr/bin/helper"is not.
R054: Persistence Unit Outside Package Root¶
- Target:
raw_line - Severity: HIGH (weight 25)
- Category:
persistence - Pattern:
(?:\b(?:install|cp|mv|ln|tee|dd|rsync|mkdir|cat|printf|echo)\b|>)[^;&|\n]*?(?:[\s"\x27]|\$\{?pkgdir\}?)(?:(?:/etc/(?:cron\.[a-z]+|cron\.d|systemd/(?:system|user)|profile\.d|bash\.bashrc\.d|zsh(?:/zshrc\.d|rc\.d)|X11/(?:Xsession|xinit/xinitrc)\.d|xdg/autostart|dbus-1/(?:system|session)\.d|sudoers\.d|ld\.so\.conf\.d|pam\.d|security/pam_\w+\.conf|NetworkManager/dispatcher\.d|xinetd\.d|(?:init|rc)\.d|logrotate\.d|tmpfiles\.d|sysusers\.d|binfmt\.d|sysctl\.d|environment\.d|polkit-1/(?:rules|actions)\.d|polkit-1/(?:rules|actions)|skel|update-motd\.d|systemd/(?:system|user)-preset)|/usr/lib/systemd/(?:system|user)|/usr/lib/systemd/(?:system|user)-(?:generators|sleep|shutdown)|/usr/share/dbus-1/(?:system|session)-services|/var/spool/cron)/|(?:/etc/(?:rc\.local|profile|bash\.bashrc|ld\.so\.preload|environment|csh\.cshrc|zsh/(?:zshrc|zprofile|zshenv)|X11/xinit/xinitrc|X11/Xsession))(?![\w./-])) - Description: Detects a cron job or systemd unit written to a system path. A unit staged into
$pkgdiris flagged too, in any quoting style ("${pkgdir}"/usr/lib/...,"${pkgdir}/usr/lib/...",$pkgdir/usr/lib/...): pacman installs what the recipe staged, so all three produce the same persistent root-level unit. Writing to the live filesystem during a build is the worse case of the same finding.
H017: Install Hook Fetches Or Executes¶
- Target: programmatic (defined in
src/trustsight/analysis/build.py) - Severity: HIGH (weight 25)
- Category:
installer - Description: A
.installhook body (post_install,post_upgrade,pre_install,pre_upgrade,pre_remove,post_remove) downloads something or performs a privileged operation:chmod u+s,systemctl enable,eval,useradd.
Hooks run as root at install time, which makes them the highest-privilege code a PKGBUILD carries. generate_diff() already includes *.install patches, and _classify_enclosing_function() recognises post_install() exactly as it recognises build(), so no separate parser is involved.
Comments are stripped before matching: one of the corpus hits was the line # systemctl enable input-remapper.
Overlaps R007, which matches any line mentioning .install at MEDIUM. R007 is left as it is because it is calibrated and in the baseline; H017 is the narrow, higher-severity companion.
H023: Install Hook Present¶
- Target: programmatic (diff-aware)
- Severity: INFO (weight 0)
- Category:
context - Condition: The PKGBUILD declares an
install=file, or the diff touches a*.installfile.
An .install scriptlet runs code as root at install time. H023 is pure
context - "this package has a root-time hook" - not an accusation. It is the
metadata a human wants when weighing other signals.
Origin: mirrors pnpm's allowBuilds/strictDepBuilds - every package
manager that distinguishes "declares a privileged post-install step" from
"does not" treats that distinction as primary metadata. pnpm blocks all build
scripts by default; H023 is the review-side equivalent - flagging .install
hooks so a human can weigh them.
Overlap guard: R007 already fires on install added. H023 fires on install present (existing or added). If R007 fires, H023 is redundant for that diff; the two must not both surface as separate findings for the same event.
H035: Foreign Package Manager In Install Hook¶
- Target: programmatic (resolved install hook lines, position-scoped)
- Severity: HIGH (weight 25)
- Category:
installer - Condition: An added line inside an install hook body (
post_install,post_upgrade,pre_install,pre_upgrade,pre_remove,post_remove) invokes a foreign package manager:pip install,npm install,cargo install,gem install,go install,dnf install,yum install,apt-get install,pacman -S/-U, ormake installwithoutDESTDIR.
Install hooks run as root at install time. Invoking another package manager from inside an AUR package's install hook modifies system state outside pacman's control, creating untracked dependencies and potential conflicts.
Kernel modules (dkms), initramfs rebuilds, and service restarts are the
expected scope of an install hook; foreign package managers are not.
H032: Write To User Home Or RC¶
- Severity: HIGH (weight 25)
- Category:
persistence - Condition: A build or install function writes into
$HOME,.bashrc,.zshrc,.profileor.config, outside$pkgdirstaging.
Fire rate: 1 of 3246 (0.03 %), a legitimate log path written from post_upgrade.
The severity is contextual. A write into a user's home during build() is
HIGH; the same write from an install scriptlet is CRITICAL, because pacman
runs scriptlets as root during the transaction. Nothing a package installs
belongs in somebody's home directory, and root reaching into one is
categorical rather than suspicious.
H039: Systemd ExecStart From Runtime-Writable Path¶
- Severity: HIGH (weight 25)
- Category:
persistence - Condition: A systemd unit the package installs has an
ExecStartpointing into a runtime-writable path (/tmp,/var/tmp,/dev/shm,$HOME,/run).
The rule reads the unit's content, including a heredoc body, not the unit's
filename. A name proves nothing; the ExecStart line is the fact.
Fire rate: 0 of 3246.
H062: Pacman Hook Installed¶
- Severity: MEDIUM (weight 15)
- Category:
persistence - Condition: A file is placed under
/usr/share/libalpm/hooks/.
A pacman hook runs on every later transaction, which is why it is reported; packages legitimately ship them, which is why it is MEDIUM.
Fire rate: 4 of 3246 (0.12 %), all packages that legitimately ship hooks.
H084: Service ExecStart Targets Undeclared Binary¶
- Severity: HIGH (weight 25)
- Category:
persistence - Condition: A systemd service unit's
ExecStartpoints at an absolute path, and the recipe installs an executable to that path whose source is neither declared insource=()nor present in the repository manifest.
Service units are read from the tree manifest when one is supplied, and from
added diff lines otherwise (a whole service file in the diff, parsed by
heuristic). The installed executable must come from an install with an
explicit 7xx mode or no -m flag (install's default is 755). Such files
arrive through the unseen source tarball, so their content cannot be audited.
Detected by _service_binary_findings() in src/trustsight/analysis/delivery.py.
R144: Packaged File Points At A World-Writable Path¶
- Target:
raw_line - Severity: HIGH (weight 25)
- Category:
persistence - Pattern:
^\+?(?=[^\n]*\$\{?pkgdir\}?)(?=[^\n]*(?:/tmp/|/var/tmp/|/dev/shm/))\S - Condition: A line that both references
$pkgdirand names a path under/tmp,/var/tmpor/dev/shm. - Description: A file staged into the package root names a program under a world-writable directory. Anyone can replace the target after the package is built, so the installed path is not under the packager's control.
A file staged into the package root that names a program under a world-writable directory. Those directories are writable by everyone, so whatever the config names can be replaced by any local user between the package being installed and the config being read - and the config is read as root for a unit, a PAM line or a cron entry.
It is both halves at once: an attacker who ships this is arranging for their own planted file to run, and a maintainer who ships it by accident has handed the same lever to anyone with a shell on the machine. The target is never in the diff, which is why every rule that looks for a payload found nothing here - the observable is the destination, not the code.
Order-free, because the recipe may write the config and then name the path or
the reverse, and anchored at ^ so each lookahead runs once. Zero
occurrences in the 3,246-diff benign corpus: a package pointing its own
config at /tmp is not something the ecosystem does.
H089: Packaged File Names A Build-Only Path¶
- Severity: HIGH (weight 25)
- Category:
persistence - Condition: Content written into
$pkgdir- a heredoc body or aprintf/echo/tee/catredirect - that names$srcdir,$startdir,$PWD,$BUILDDIRor$pkgdir.
The audit's largest silent family is a configuration file the recipe
generates into the package root whose exec slot names a script: an i3
bindsym … exec, a polybar exec =, a udev RUN+=, an acme RELOADCMD=,
a mutt macro … !bash. Every rule that looks for execution reads the
recipe's own commands, and none of these lines is a command the recipe
runs - they are text, and what runs them is the user's session, later, on a
different machine.
What separates them from the ordinary case is not the exec slot, which is
what those files are for: a .desktop with Exec=/usr/bin/p and a
bindsym $mod+d exec dmenu_run are exactly right, and both stay silent. It
is which path the slot names. $srcdir, $startdir, $PWD and
$BUILDDIR exist only while the package is being built, in a directory
pacman never ships and the user does not have. A shipped file naming one is
either broken on arrival - it points at nothing - or it is aimed at a
directory whoever wrote it expects to control at the moment it is read.
Neither reading is packaging. The rule is about the pairing of a write
into $pkgdir with content naming a build-only path, which is why it is not
a line pattern: install -Dm755 "$srcdir/x" "$pkgdir/usr/bin/x" names both
on one line and is the single most common line in the ecosystem. There
$srcdir is an argument to a copy; here it is inside the bytes being
written. The rule splits a single-line write at its redirect and reads only
the content half, and for a heredoc it reads the body against the target
named on the opener.
Zero occurrences in the 3,246-diff benign corpus.
H093: Committed Config Points At A Build-Only Path¶
- Severity: HIGH (weight 25)
- Category:
persistence - Condition: A committed
.service,.desktop,.rules,.conf(and the rest of the carrier set H090 reads) holding a directive that runs something, whose value names$srcdir,$startdir,$PWD,$BUILDDIRor$pkgdir.
The symmetric half of H089. That rule reads content the recipe
generates into $pkgdir; this one reads content the recipe committed
and then ships. The observable is identical and so is the reasoning: those
directories exist only while the package is being built, so a shipped file
naming one is either broken on arrival or aimed at a directory whoever wrote
it expects to control when it is read.
The value has to sit in a directive that runs something. A .desktop whose
Comment= mentions a build path is a cosmetic mistake; an Exec= naming one
is a command pointed at nothing.
How the directive is recognised. The rule does not carry a list of
exec-bearing keys. A shipped file that names a build directory is broken on
arrival whatever field holds the path, so the test is inverted: fields that
only describe are excluded, and those are few and stable - Comment,
Description, Name, Icon, URL, X-*, and comment lines. A .desktop
whose Comment= mentions the build tree is untidy; an Exec= naming one is
a command aimed at nothing.
The path, not the file extension, is the observable.
ExecStart=/usr/share/p/launcher.sh names a script the package itself
ships, and stays quiet.
Carriers include build manifests (build.ninja, Makefile,
BUILD.bazel, *.mk) for the same reason they include unit files: the
engine runs what they say. make spells its variables $(srcdir) with
parentheses, so an ordinary Makefile does not look like a build-only path.
Measured across all thirty audited verticals in their committed form: thirty fire. Measured across 249 committed files in 81 real AUR repositories: none does.
H095: Boot Or Image Artifact Built From The Source Tree¶
- Severity: HIGH (weight 25)
- Category:
persistence - Condition:
dracut,mkinitcpio,update-initramfs,grub-mkconfig,grub-install,guestfish,virt-customizeorbootctlinvoked with an argument naming$srcdir,$startdir,$PWDor$pkgdir.
dracut --include "$srcdir/x" /x injects a path from the build tree into
the initramfs, which runs before userspace exists and before any filesystem
the user can inspect is mounted. grub-mkconfig writes the boot menu.
guestfish and virt-customize edit a disk image's contents.
A package may legitimately ship kernel modules or a bootloader, and those
are installed like any other file. Generating boot material during a
build is different: the result captures the builder's machine, and any path
from the source tree that goes into it is code that will run at the earliest
moment there is.
The build-only path is the observable, as it is for H089 and H093. A bare relative filename establishes no provenance - if it is declared or committed, H090 and H093 read it; if it is neither, it is the W001 boundary.
None of these tools appear in the benign corpus with a build-tree argument.