Add privileged mount helper + polkit policy

POSIX sh helper (pkexec target) that mounts/unmounts CIFS shares with strict
validation of the unprivileged caller's arguments: mountpoint confined to
$HOME or /run/media/<user>, //host/share source, credfile under the caller's
runtime dir at mode 0600, whitelisted SMB version/options/modes. Polkit
action authorizes it with auth_admin_keep.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jaroslav Beneš
2026-07-15 14:28:49 +02:00
parent fab2d65dd2
commit 6448ea4f22
2 changed files with 138 additions and 0 deletions
+118
View File
@@ -0,0 +1,118 @@
#!/bin/sh
#
# synology-nas-helper — privileged mount/unmount helper for the Synology NAS
# plasmoid. It is invoked as root via pkexec (see the accompanying
# sk.houmeres.synologynas.policy). The CALLER IS UNPRIVILEGED, so every argument
# is treated as hostile and strictly validated before any mount happens.
#
# Usage (all arguments positional):
# synology-nas-helper mount <source> <mountpoint> <credfile> <vers> <opts> <fmode> <dmode>
# synology-nas-helper unmount <mountpoint>
#
# Guarantees enforced below:
# * mountpoint must resolve to a path under the CALLING user's $HOME or
# /run/media/<user> (rejects .. traversal via realpath -m).
# * source must look like //host/share.
# * credfile must live under /run/user/<caller-uid>/, be a regular file,
# owned by the caller, mode 0600.
# * vers, permission modes and extra options are charset/whitelist checked.
#
set -eu
die() { echo "synology-nas-helper: $1" >&2; exit 1; }
# --- identify the calling (unprivileged) user ---------------------------------
[ -n "${PKEXEC_UID:-}" ] || die "must be run through pkexec (PKEXEC_UID unset)"
case "$PKEXEC_UID" in
''|*[!0-9]*) die "invalid PKEXEC_UID" ;;
esac
pwline=$(getent passwd "$PKEXEC_UID") || die "unknown caller uid $PKEXEC_UID"
CALLER_USER=$(printf '%s\n' "$pwline" | cut -d: -f1)
CALLER_GID=$(printf '%s\n' "$pwline" | cut -d: -f4)
CALLER_HOME=$(printf '%s\n' "$pwline" | cut -d: -f6)
[ -n "$CALLER_HOME" ] || die "caller has no home directory"
# --- shared validators --------------------------------------------------------
validate_mountpoint() {
# Canonicalise (resolves .. lexically, no need for the path to exist yet).
canon=$(realpath -m -- "$1") || die "cannot canonicalise mountpoint"
case "$canon" in
"$CALLER_HOME"/*) : ;;
"/run/media/$CALLER_USER"/*) : ;;
*) die "mountpoint must be under \$HOME or /run/media/$CALLER_USER" ;;
esac
printf '%s' "$canon"
}
# --- subcommands --------------------------------------------------------------
do_mount() {
[ $# -eq 7 ] || die "mount: expected 7 arguments"
source=$1; mountpoint=$2; credfile=$3; vers=$4; extra=$5; fmode=$6; dmode=$7
case "$source" in
//?*/?*) : ;;
*) die "source must be //host/share" ;;
esac
mp=$(validate_mountpoint "$mountpoint")
# Credentials file: under the caller's runtime dir, their file, mode 0600.
cf=$(realpath -m -- "$credfile") || die "cannot canonicalise credfile"
case "$cf" in
"/run/user/$PKEXEC_UID"/*) : ;;
*) die "credfile must be under /run/user/$PKEXEC_UID" ;;
esac
[ -f "$cf" ] || die "credfile not found"
[ "$(stat -c %u "$cf")" = "$PKEXEC_UID" ] || die "credfile not owned by caller"
[ "$(stat -c %a "$cf")" = "600" ] || die "credfile must be mode 0600"
case "$vers" in
default|1.0|2.0|2.1|3.0|3.02|3.1.1) : ;;
*) die "unsupported SMB version: $vers" ;;
esac
case "$fmode" in 0[0-7][0-7][0-7]) : ;; *) die "bad file mode" ;; esac
case "$dmode" in 0[0-7][0-7][0-7]) : ;; *) die "bad dir mode" ;; esac
case "$extra" in
'') : ;;
*[!A-Za-z0-9=,_.:-]*) die "extra mount options contain illegal characters" ;;
esac
if findmnt -rn -o TARGET --mountpoint "$mp" >/dev/null 2>&1; then
die "already mounted: $mp"
fi
# Create the mount point and hand the freshly-created dirs to the caller.
mkdir -p "$mp" || die "cannot create mount point"
chown "$PKEXEC_UID:$CALLER_GID" "$mp" 2>/dev/null || true
p=$(dirname "$mp")
while [ "$p" != "$CALLER_HOME" ] && [ "$p" != "/" ] && [ "$p" != "/run/media" ]; do
chown "$PKEXEC_UID:$CALLER_GID" "$p" 2>/dev/null || true
p=$(dirname "$p")
done
opts="credentials=$cf,uid=$PKEXEC_UID,gid=$CALLER_GID,forceuid,forcegid,file_mode=$fmode,dir_mode=$dmode,vers=$vers"
[ -n "$extra" ] && opts="$opts,$extra"
exec mount.cifs "$source" "$mp" -o "$opts"
}
do_unmount() {
[ $# -eq 1 ] || die "unmount: expected 1 argument"
mp=$(validate_mountpoint "$1")
findmnt -rn -t cifs -o TARGET --mountpoint "$mp" >/dev/null 2>&1 \
|| die "not a cifs mount: $mp"
umount "$mp" || die "umount failed"
# Tidy up empty leaf + host dirs (never recursive, never touches files).
rmdir "$mp" 2>/dev/null || true
rmdir "$(dirname "$mp")" 2>/dev/null || true
}
[ $# -ge 1 ] || die "no subcommand"
cmd=$1
shift
case "$cmd" in
mount) do_mount "$@" ;;
unmount) do_unmount "$@" ;;
*) die "unknown subcommand: $cmd" ;;
esac