From dc103b260b2dfd8b94ccdd23aaaaa54000d8d56c Mon Sep 17 00:00:00 2001 From: "claude (dev556 4bdc306f-7204-4d7f-ba68-0c897dcf0291)" Date: Mon, 14 Sep 2026 09:35:22 -0400 Subject: [PATCH 1/8] Also skip install checks on dist_build_iso; house-style - Skip the sudo/console-user install checks when dist_build_iso=true (an ISO/live build) in addition to the existing skip_install_check file, so the derivative-maker build no longer needs to write that marker file. The manual file-based bypass is kept for non-derivative-maker users. - Modernize to dist-ai house style: strict-mode block, printf not echo, has not command -v, braced + nounset-guarded variables. --- debian/security-misc-shared.preinst | 118 ++++++++++++++++------------ 1 file changed, 66 insertions(+), 52 deletions(-) diff --git a/debian/security-misc-shared.preinst b/debian/security-misc-shared.preinst index 93d8b2f5..22873fe4 100755 --- a/debian/security-misc-shared.preinst +++ b/debian/security-misc-shared.preinst @@ -6,12 +6,19 @@ if [ -f /usr/libexec/helper-scripts/pre.bsh ]; then source /usr/libexec/helper-scripts/pre.bsh fi +source /usr/libexec/helper-scripts/has.bsh -set -e +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose +export LC_ALL=C true " ##################################################################### -## INFO: BEGIN: $DPKG_MAINTSCRIPT_PACKAGE $DPKG_MAINTSCRIPT_NAME $@ +## INFO: BEGIN: ${DPKG_MAINTSCRIPT_PACKAGE:-} ${DPKG_MAINTSCRIPT_NAME:-} $* ##################################################################### " @@ -60,14 +67,14 @@ user_groups_modifications() { } output_skip_checks() { - echo "security-misc-shared '$0' INFO: Allow installation of security-misc-shared anyway." >&2 - echo "security-misc-shared '$0' INFO: (technical reason: $@)" >&2 - echo "security-misc-shared '$0' INFO: If this is a chroot this is probably OK." >&2 - echo "security-misc-shared '$0' INFO: Otherwise you might not be able to login." >&2 + printf '%s\n' "security-misc-shared '${0}' INFO: Allow installation of security-misc-shared anyway." >&2 + printf '%s\n' "security-misc-shared '${0}' INFO: (technical reason: $*)" >&2 + printf '%s\n' "security-misc-shared '${0}' INFO: If this is a chroot this is probably OK." >&2 + printf '%s\n' "security-misc-shared '${0}' INFO: Otherwise you might not be able to login." >&2 } sudo_users_check () { - if command -v "qubesdb-read" &>/dev/null; then + if has qubesdb-read; then ## Qubes users can use dom0 to get a root terminal emulator. ## For example: ## qvm-run -u root debian-10 xterm @@ -80,12 +87,14 @@ sudo_users_check () { ## example sudo_users: ## user,root - OLD_IFS="$IFS" + OLD_IFS="${IFS}" IFS="," export IFS - for user_with_sudo in $sudo_users ; do - if [ "$user_with_sudo" = "root" ]; then + ## Intentional word-split of the comma-separated group member list. + # shellcheck disable=SC2086 + for user_with_sudo in ${sudo_users} ; do + if [ "${user_with_sudo}" = "root" ]; then ## root login is also restricted. ## Therefore user "root" being member of group "sudo" is ## considered insufficient. @@ -95,31 +104,32 @@ sudo_users_check () { break done - IFS="$OLD_IFS" + IFS="${OLD_IFS}" export IFS - if [ "$are_there_any_sudo_users" = "yes" ]; then + if [ "${are_there_any_sudo_users:-}" = "yes" ]; then return 0 fi ## Prevent users from locking themselves out. ## https://forums.whonix.org/t/is-security-misc-suitable-for-hardening-bridges-and-relays/8299/4 - echo "$0: ERROR: No account is a member of group 'sudo'. Installation aborted." >&2 - echo "$0: ERROR: You probably want to run:" >&2 - echo "$0: NOTE: Replace account 'user' with your actual Linux user account name." >&2 - echo "" >&2 - echo "sudo adduser user sudo" >&2 - echo "sudo adduser user console" >&2 - echo "" >&2 - echo "$0: ERROR: See also installation instructions:" >&2 - echo "https://www.kicksecure.com/wiki/security-misc#install" >&2 - - if [ "$SECURITY_MISC_INSTALL" = "force" ]; then + printf '%s\n' "${0}: ERROR: No account is a member of group 'sudo'. Installation aborted." >&2 + printf '%s\n' "${0}: ERROR: You probably want to run:" >&2 + printf '%s\n' "${0}: NOTE: Replace account 'user' with your actual Linux user account name." >&2 + printf '%s\n' "" >&2 + printf '%s\n' "sudo adduser user sudo" >&2 + printf '%s\n' "sudo adduser user console" >&2 + printf '%s\n' "" >&2 + printf '%s\n' "${0}: ERROR: See also installation instructions:" >&2 + printf '%s\n' "https://www.kicksecure.com/wiki/security-misc#install" >&2 + + if [ "${SECURITY_MISC_INSTALL:-}" = "force" ]; then output_skip_checks "Environment variable SECURITY_MISC_INSTALL is set to 'force'." return 0 fi - if test -f "/var/lib/security-misc/skip_install_check" ; then - output_skip_checks "File '/var/lib/security-misc/skip_install_check' exists." + if test -f "/var/lib/security-misc/skip_install_check" \ + || [ "${dist_build_iso:-}" = "true" ] ; then + output_skip_checks "File '/var/lib/security-misc/skip_install_check' exists or this is an ISO/live build (dist_build_iso=true)." return 0 fi @@ -127,13 +137,14 @@ sudo_users_check () { } console_users_check() { - if [ "$SECURITY_MISC_INSTALL" = "force" ]; then + if [ "${SECURITY_MISC_INSTALL:-}" = "force" ]; then return 0 fi - if test -f "/var/lib/security-misc/skip_install_check" ; then + if test -f "/var/lib/security-misc/skip_install_check" \ + || [ "${dist_build_iso:-}" = "true" ] ; then return 0 fi - if command -v "qubesdb-read" &>/dev/null; then + if has qubesdb-read; then ## Qubes users can use dom0 to get a root terminal emulator. ## For example: ## qvm-run -u root debian-10 xterm @@ -147,12 +158,14 @@ console_users_check() { ## user console_unrestricted_users="$(getent group console-unrestricted | cut -d: -f4)" - OLD_IFS="$IFS" + OLD_IFS="${IFS}" IFS="," export IFS - for user_with_console in $console_users $console_unrestricted_users ; do - if [ "$user_with_console" = "root" ]; then + ## Intentional word-split of the comma-separated group member lists. + # shellcheck disable=SC2086 + for user_with_console in ${console_users} ${console_unrestricted_users} ; do + if [ "${user_with_console}" = "root" ]; then ## root login is also restricted. ## Therefore account "root" being member of group "console" is ## considered insufficient. @@ -162,29 +175,30 @@ console_users_check() { break done - IFS="$OLD_IFS" + IFS="${OLD_IFS}" export IFS ## Prevent users from locking themselves out. ## https://forums.whonix.org/t/is-security-misc-suitable-for-hardening-bridges-and-relays/8299/4 - if [ "$are_there_any_console_users" = "yes" ]; then + if [ "${are_there_any_console_users:-}" = "yes" ]; then return 0 fi - echo "$0: ERROR: No account is a member of group 'console'. Installation aborted." >&2 - echo "$0: ERROR: You probably want to run:" >&2 - echo "" >&2 - echo "sudo adduser user console" >&2 - echo "" >&2 - echo "$0: ERROR: See also installation instructions:" >&2 - echo "https://www.whonix.org/wiki/security-misc#install" >&2 + printf '%s\n' "${0}: ERROR: No account is a member of group 'console'. Installation aborted." >&2 + printf '%s\n' "${0}: ERROR: You probably want to run:" >&2 + printf '%s\n' "" >&2 + printf '%s\n' "sudo adduser user console" >&2 + printf '%s\n' "" >&2 + printf '%s\n' "${0}: ERROR: See also installation instructions:" >&2 + printf '%s\n' "https://www.whonix.org/wiki/security-misc#install" >&2 - if [ "$SECURITY_MISC_INSTALL" = "force" ]; then + if [ "${SECURITY_MISC_INSTALL:-}" = "force" ]; then output_skip_checks "Environment variable SECURITY_MISC_INSTALL is set to 'force'." return 0 fi - if test -f "/var/lib/security-misc/skip_install_check" ; then - output_skip_checks "File '/var/lib/security-misc/skip_install_check' exists." + if test -f "/var/lib/security-misc/skip_install_check" \ + || [ "${dist_build_iso:-}" = "true" ] ; then + output_skip_checks "File '/var/lib/security-misc/skip_install_check' exists or this is an ISO/live build (dist_build_iso=true)." return 0 fi @@ -192,7 +206,7 @@ console_users_check() { } legacy() { - if [ -f "/var/lib/legacy/do_once/${FUNCNAME}_version_1" ]; then + if [ -f "/var/lib/legacy/do_once/${FUNCNAME[0]}_version_1" ]; then return 0 fi @@ -205,11 +219,11 @@ legacy() { continue_yes=true fi - if [ ! "$continue_yes" = "true" ]; then + if [ ! "${continue_yes:-}" = "true" ]; then return 0 fi - if command -v "qubesdb-read" &>/dev/null; then + if has qubesdb-read; then ## Qubes users can use dom0 to get a root terminal emulator. ## For example: ## qvm-run -u root debian-10 xterm @@ -220,23 +234,23 @@ legacy() { user_to_be_created=user - if ! id "$user_to_be_created" &>/dev/null ; then - true "INFO: Account '$user_to_be_created' does not exist. Skipping adding account '$user_to_be_created' to group 'console' and also skipping 'pam-auth-update --enable console-lockdown-security-misc'." + if ! id "${user_to_be_created}" &>/dev/null ; then + true "INFO: Account '${user_to_be_created}' does not exist. Skipping adding account '${user_to_be_created}' to group 'console' and also skipping 'pam-auth-update --enable console-lockdown-security-misc'." return 0 fi - adduser "$user_to_be_created" console + adduser "${user_to_be_created}" console pam-auth-update --enable console-lockdown-security-misc mkdir --parents "/var/lib/legacy/do_once" - touch "/var/lib/legacy/do_once/${FUNCNAME}_version_1" + touch "/var/lib/legacy/do_once/${FUNCNAME[0]}_version_1" } user_groups_modifications legacy -if [ "$1" = "install" ] || [ "$1" = "upgrade" ]; then +if [ "${1:-}" = "install" ] || [ "${1:-}" = "upgrade" ]; then sudo_users_check console_users_check fi @@ -249,7 +263,7 @@ true "INFO: Done with debhelper." true " ##################################################################### -## INFO: END : $DPKG_MAINTSCRIPT_PACKAGE $DPKG_MAINTSCRIPT_NAME $@ +## INFO: END : ${DPKG_MAINTSCRIPT_PACKAGE:-} ${DPKG_MAINTSCRIPT_NAME:-} $* ##################################################################### " From 88abbcaa436a86dcd15a37c8ba4f4351b565c050 Mon Sep 17 00:00:00 2001 From: "claude (dev556 4bdc306f-7204-4d7f-ba68-0c897dcf0291)" Date: Mon, 14 Sep 2026 11:13:01 -0400 Subject: [PATCH 2/8] Detect primary-GID members in the install checks - sudo_users_check / console_users_check now count a non-root account whose PRIMARY group is sudo/console (via a vendored group_has_nonroot_member), not only supplementary members; a supplementary-only scan falsely aborted install for such an admin. - Revert the qubesdb-read probe to 'command -v' (no-has waiver): a preinst runs at unpack time, before helper-scripts is guaranteed configured, so it must not source has.bsh. - group_has_nonroot_member is VENDORED from helper-scripts accountctl.sh (same reason it cannot be sourced); a dist-ai drift guard keeps them in sync. --- debian/security-misc-shared.preinst | 104 ++++++++++++---------------- 1 file changed, 44 insertions(+), 60 deletions(-) diff --git a/debian/security-misc-shared.preinst b/debian/security-misc-shared.preinst index 22873fe4..bd5783dd 100755 --- a/debian/security-misc-shared.preinst +++ b/debian/security-misc-shared.preinst @@ -3,10 +3,12 @@ ## Copyright (C) 2012 - 2025 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. +## style-ok: no-has -- a preinst runs at unpack time, before helper-scripts (a +## plain Depends) is guaranteed configured, so it must not source has.bsh; it +## uses 'command -v' and a vendored group_has_nonroot_member instead. if [ -f /usr/libexec/helper-scripts/pre.bsh ]; then source /usr/libexec/helper-scripts/pre.bsh fi -source /usr/libexec/helper-scripts/has.bsh set -o errexit set -o nounset @@ -73,41 +75,48 @@ output_skip_checks() { printf '%s\n' "security-misc-shared '${0}' INFO: Otherwise you might not be able to login." >&2 } +## VENDORED verbatim from helper-scripts accountctl.sh group_has_nonroot_member. +## A preinst runs before helper-scripts is guaranteed installed, so it cannot be +## sourced. Keep in sync with accountctl.sh; drift is caught by the dist-ai +## 'group_has_nonroot_member_drift' test. +group_has_nonroot_member() { + local group group_gid members member entry_name entry_gid + local -a member_list + group="${1:-}" + [ -n "${group}" ] || return 1 + group_gid="$(getent group -- "${group}" 2>/dev/null | cut -d: -f3)" || true + [ -n "${group_gid}" ] || return 1 + + ## Primary-GID members: a passwd account whose GID equals the group's GID + ## (such accounts are absent from the group's supplementary member field). + while IFS=":" read -r entry_name _ _ entry_gid _; do + if [ "${entry_gid}" = "${group_gid}" ] && [ "${entry_name}" != "root" ]; then + return 0 + fi + done < <(getent passwd) + + ## Supplementary members (comma-separated member field). + members="$(getent group -- "${group}" 2>/dev/null | cut -d: -f4)" || true + IFS="," read -r -a member_list <<< "${members}" || true + for member in "${member_list[@]}"; do + if [ -n "${member}" ] && [ "${member}" != "root" ]; then + return 0 + fi + done + return 1 +} + sudo_users_check () { - if has qubesdb-read; then + if command -v qubesdb-read >/dev/null 2>&1; then ## Qubes users can use dom0 to get a root terminal emulator. ## For example: ## qvm-run -u root debian-10 xterm return 0 fi - local sudo_users user_with_sudo are_there_any_sudo_users OLD_IFS - - sudo_users="$(getent group sudo | cut -d: -f4)" - ## example sudo_users: - ## user,root - - OLD_IFS="${IFS}" - IFS="," - export IFS - - ## Intentional word-split of the comma-separated group member list. - # shellcheck disable=SC2086 - for user_with_sudo in ${sudo_users} ; do - if [ "${user_with_sudo}" = "root" ]; then - ## root login is also restricted. - ## Therefore user "root" being member of group "sudo" is - ## considered insufficient. - continue - fi - are_there_any_sudo_users=yes - break - done - - IFS="${OLD_IFS}" - export IFS - - if [ "${are_there_any_sudo_users:-}" = "yes" ]; then + ## Any non-root account with sudo access (supplementary member OR primary + ## GID) prevents a lockout, so it satisfies the check. + if group_has_nonroot_member sudo; then return 0 fi @@ -144,43 +153,18 @@ console_users_check() { || [ "${dist_build_iso:-}" = "true" ] ; then return 0 fi - if has qubesdb-read; then + if command -v qubesdb-read >/dev/null 2>&1; then ## Qubes users can use dom0 to get a root terminal emulator. ## For example: ## qvm-run -u root debian-10 xterm return 0 fi - local console_users console_unrestricted_users user_with_console are_there_any_console_users OLD_IFS - - console_users="$(getent group console | cut -d: -f4)" - ## example console_users: - ## user - console_unrestricted_users="$(getent group console-unrestricted | cut -d: -f4)" - - OLD_IFS="${IFS}" - IFS="," - export IFS - - ## Intentional word-split of the comma-separated group member lists. - # shellcheck disable=SC2086 - for user_with_console in ${console_users} ${console_unrestricted_users} ; do - if [ "${user_with_console}" = "root" ]; then - ## root login is also restricted. - ## Therefore account "root" being member of group "console" is - ## considered insufficient. - continue - fi - are_there_any_console_users=yes - break - done - - IFS="${OLD_IFS}" - export IFS - - ## Prevent users from locking themselves out. + ## A non-root member (supplementary OR primary GID) of either console group + ## prevents a lockout. ## https://forums.whonix.org/t/is-security-misc-suitable-for-hardening-bridges-and-relays/8299/4 - if [ "${are_there_any_console_users:-}" = "yes" ]; then + if group_has_nonroot_member console \ + || group_has_nonroot_member console-unrestricted; then return 0 fi @@ -223,7 +207,7 @@ legacy() { return 0 fi - if has qubesdb-read; then + if command -v qubesdb-read >/dev/null 2>&1; then ## Qubes users can use dom0 to get a root terminal emulator. ## For example: ## qvm-run -u root debian-10 xterm From 497a23fbb31e2550fc858e6139e84b595be49d20 Mon Sep 17 00:00:00 2001 From: "claude (dev556 4bdc306f-7204-4d7f-ba68-0c897dcf0291)" Date: Mon, 14 Sep 2026 12:49:30 -0400 Subject: [PATCH 3/8] Sync vendored group_has_nonroot_member (reject numeric arg) --- debian/security-misc-shared.preinst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/debian/security-misc-shared.preinst b/debian/security-misc-shared.preinst index bd5783dd..df09f7f1 100755 --- a/debian/security-misc-shared.preinst +++ b/debian/security-misc-shared.preinst @@ -84,6 +84,13 @@ group_has_nonroot_member() { local -a member_list group="${1:-}" [ -n "${group}" ] || return 1 + ## A group NAME starts with a letter or underscore (per is_name_valid). + ## Reject anything else so a numeric argument is not silently reinterpreted + ## by getent as a GID lookup (answering about the wrong group). Kept + ## self-contained -- no is_name_valid call -- for vendoring. + if [[ "${group}" != [a-z_]* ]]; then + return 1 + fi group_gid="$(getent group -- "${group}" 2>/dev/null | cut -d: -f3)" || true [ -n "${group_gid}" ] || return 1 From 239532762966b9fc57f158bb02fb19b04bf1e1bc Mon Sep 17 00:00:00 2001 From: "claude (dev556 4bdc306f-7204-4d7f-ba68-0c897dcf0291)" Date: Tue, 15 Sep 2026 02:16:06 -0400 Subject: [PATCH 4/8] Pre-Depend on helper-scripts; source accountctl.sh, drop vendored copy - helper-scripts moved from Depends to Pre-Depends: it is configured before this package is unpacked, so the preinst can source its libraries. Reuse the real group_has_nonroot_member from accountctl.sh instead of a vendored copy (and its drift guard). - accountctl.sh pulls in has.bsh, so 'has' is available: use it, drop the no-has waiver and the command -v fallbacks. --- debian/control | 2 +- debian/security-misc-shared.preinst | 56 +++++------------------------ 2 files changed, 10 insertions(+), 48 deletions(-) diff --git a/debian/control b/debian/control index 4a944d1b..1673ab33 100644 --- a/debian/control +++ b/debian/control @@ -30,11 +30,11 @@ Rules-Requires-Root: no ## > security-misc is to be removed Package: security-misc-shared Architecture: all +Pre-Depends: helper-scripts Depends: adduser, apparmor-profile-dist, build-essential, dmsetup, - helper-scripts, libcap2-bin, libdbus-1-dev, libglib2.0-bin, diff --git a/debian/security-misc-shared.preinst b/debian/security-misc-shared.preinst index df09f7f1..c7108b08 100755 --- a/debian/security-misc-shared.preinst +++ b/debian/security-misc-shared.preinst @@ -3,12 +3,12 @@ ## Copyright (C) 2012 - 2025 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. -## style-ok: no-has -- a preinst runs at unpack time, before helper-scripts (a -## plain Depends) is guaranteed configured, so it must not source has.bsh; it -## uses 'command -v' and a vendored group_has_nonroot_member instead. -if [ -f /usr/libexec/helper-scripts/pre.bsh ]; then - source /usr/libexec/helper-scripts/pre.bsh -fi +## helper-scripts is a Pre-Depends: it is fully configured before this preinst +## runs, so its libraries are guaranteed present and source-able here. Source +## pre.bsh (error handler) and accountctl.sh (group_has_nonroot_member; also +## pulls in has.bsh, so 'has' is available). +source /usr/libexec/helper-scripts/pre.bsh +source /usr/libexec/helper-scripts/accountctl.sh set -o errexit set -o nounset @@ -75,46 +75,8 @@ output_skip_checks() { printf '%s\n' "security-misc-shared '${0}' INFO: Otherwise you might not be able to login." >&2 } -## VENDORED verbatim from helper-scripts accountctl.sh group_has_nonroot_member. -## A preinst runs before helper-scripts is guaranteed installed, so it cannot be -## sourced. Keep in sync with accountctl.sh; drift is caught by the dist-ai -## 'group_has_nonroot_member_drift' test. -group_has_nonroot_member() { - local group group_gid members member entry_name entry_gid - local -a member_list - group="${1:-}" - [ -n "${group}" ] || return 1 - ## A group NAME starts with a letter or underscore (per is_name_valid). - ## Reject anything else so a numeric argument is not silently reinterpreted - ## by getent as a GID lookup (answering about the wrong group). Kept - ## self-contained -- no is_name_valid call -- for vendoring. - if [[ "${group}" != [a-z_]* ]]; then - return 1 - fi - group_gid="$(getent group -- "${group}" 2>/dev/null | cut -d: -f3)" || true - [ -n "${group_gid}" ] || return 1 - - ## Primary-GID members: a passwd account whose GID equals the group's GID - ## (such accounts are absent from the group's supplementary member field). - while IFS=":" read -r entry_name _ _ entry_gid _; do - if [ "${entry_gid}" = "${group_gid}" ] && [ "${entry_name}" != "root" ]; then - return 0 - fi - done < <(getent passwd) - - ## Supplementary members (comma-separated member field). - members="$(getent group -- "${group}" 2>/dev/null | cut -d: -f4)" || true - IFS="," read -r -a member_list <<< "${members}" || true - for member in "${member_list[@]}"; do - if [ -n "${member}" ] && [ "${member}" != "root" ]; then - return 0 - fi - done - return 1 -} - sudo_users_check () { - if command -v qubesdb-read >/dev/null 2>&1; then + if has qubesdb-read; then ## Qubes users can use dom0 to get a root terminal emulator. ## For example: ## qvm-run -u root debian-10 xterm @@ -160,7 +122,7 @@ console_users_check() { || [ "${dist_build_iso:-}" = "true" ] ; then return 0 fi - if command -v qubesdb-read >/dev/null 2>&1; then + if has qubesdb-read; then ## Qubes users can use dom0 to get a root terminal emulator. ## For example: ## qvm-run -u root debian-10 xterm @@ -214,7 +176,7 @@ legacy() { return 0 fi - if command -v qubesdb-read >/dev/null 2>&1; then + if has qubesdb-read; then ## Qubes users can use dom0 to get a root terminal emulator. ## For example: ## qvm-run -u root debian-10 xterm From c894bbcf75bbe3c63f17b75f0ffc8a0ae2f98ef3 Mon Sep 17 00:00:00 2001 From: "claude (dev556 4bdc306f-7204-4d7f-ba68-0c897dcf0291)" Date: Tue, 15 Sep 2026 02:37:02 -0400 Subject: [PATCH 5/8] preinst: fail closed loudly if helper-scripts library missing - source failures were swallowed (errexit not set yet); abort with the real cause instead. - a missing group_has_nonroot_member now aborts with the true reason rather than degrading the lockout check into a misleading 'no account is a member of sudo' abort. --- debian/security-misc-shared.preinst | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/debian/security-misc-shared.preinst b/debian/security-misc-shared.preinst index c7108b08..50dbc7fc 100755 --- a/debian/security-misc-shared.preinst +++ b/debian/security-misc-shared.preinst @@ -7,8 +7,18 @@ ## runs, so its libraries are guaranteed present and source-able here. Source ## pre.bsh (error handler) and accountctl.sh (group_has_nonroot_member; also ## pulls in has.bsh, so 'has' is available). -source /usr/libexec/helper-scripts/pre.bsh -source /usr/libexec/helper-scripts/accountctl.sh +## +## Fail CLOSED, loudly: a source failure (unreadable/partial/syntax) here would +## otherwise be swallowed (errexit is not set yet), and a missing +## group_has_nonroot_member would degrade the lockout check into a misleading +## 'no account is a member of sudo' abort. A security check that cannot load its +## verifier must stop with the TRUE cause, not skip or mislead. +source /usr/libexec/helper-scripts/pre.bsh || { printf '%s\n' "${0}: ERROR: failed to source /usr/libexec/helper-scripts/pre.bsh; helper-scripts is broken or missing." >&2; exit 1; } +source /usr/libexec/helper-scripts/accountctl.sh || { printf '%s\n' "${0}: ERROR: failed to source /usr/libexec/helper-scripts/accountctl.sh; helper-scripts is broken or missing." >&2; exit 1; } +if ! type -t group_has_nonroot_member >/dev/null 2>&1; then + printf '%s\n' "${0}: ERROR: helper-scripts accountctl.sh did not provide group_has_nonroot_member; cannot verify account lockout safety. helper-scripts is too old or incomplete." >&2 + exit 1 +fi set -o errexit set -o nounset From 888a5f679d816aeb8c50fc14ab604d27d158863d Mon Sep 17 00:00:00 2001 From: "claude (dev556 4bdc306f-7204-4d7f-ba68-0c897dcf0291)" Date: Tue, 15 Sep 2026 04:27:13 -0400 Subject: [PATCH 6/8] permission-hardener: restore octal-owner filename disambiguation - the right-to-left octal scan mis-anchors on a numeric octal-looking owner (e.g. 0755), folding the real mode into the filename; the nonexistent path is then silently dropped, leaving a SUID binary un-hardened. - restore fixed-width tail anchoring + disambiguation by on-disk existence; test_whitespace_filename.sh (numowner, 'x y') parses again. --- .../permission-hardener#security-misc-shared | 70 +++++++++++++------ 1 file changed, 47 insertions(+), 23 deletions(-) diff --git a/usr/bin/permission-hardener#security-misc-shared b/usr/bin/permission-hardener#security-misc-shared index ec71c610..cc7de229 100755 --- a/usr/bin/permission-hardener#security-misc-shared +++ b/usr/bin/permission-hardener#security-misc-shared @@ -431,7 +431,8 @@ load_state() { ## to mean that all whitelisting should be ignored. local config_file line field_list policy_nosuid_file_item policy_file_item - local one_field field_count fso_name meta_list field_index hit_barrier + local one_field field_count fso_name meta_list + local split_index candidate_index fallback_index ## Load configuration, deferring whitelist handling until later for config_file in \ @@ -471,16 +472,12 @@ load_state() { processed_config_line="${line}" - ## We can handle filenames with spaces. This is because the first item - ## after a filename is either an octal file mode or a keyword, and we - ## only support user/group names, not UIDs and GIDs, as owner - ## identifiers. This means that we can scan from right to left for the - ## furthest-right octal integer or keyword field and use it to separate - ## the filename from the rest of the options. (If usernames could - ## themselves be octal integers, this would break, but adduser.conf's - ## default NAME_REGEX forbids this. This is definitely a hack, but the - ## alternative is to change the format of the policy files, which would - ## break custom config for users. + ## We can handle filenames with spaces. The options after a filename are a + ## fixed-width tail: either a lone whitelist keyword, or an octal mode + + ## owner + group + optional capability. The filename is whatever precedes + ## that tail, recovered by anchoring the tail from the right (see the field + ## split below). An owner that itself looks octal is disambiguated by which + ## recovered filename exists on disk. ## ## 'read' will collapse consecutive spaces when splitting on spaces. ## Convert spaces into newlines first, then split on newlines with a @@ -491,21 +488,48 @@ load_state() { done < <(printf '%s\n' "${line}" | tr ' ' '\n') field_count="${#field_list[@]}" - hit_barrier='false' + ## The option tail is fixed-width: a lone trailing whitelist keyword, or a + ## mode/owner/group[/capability] tail whose FIRST field is the octal mode. + ## Anchor the tail from the RIGHT. Two readings can be lexically valid when + ## a filename component OR a numeric owner looks octal (mode at field + ## count-3 vs count-4), so disambiguate by which recovered filename exists + ## on disk -- the tool skips nonexistent targets anyway. Fall back to the + ## rightmost octal candidate when neither recovered name exists. fso_name="" meta_list=() - for (( field_index = field_count - 1; field_index >= 0; \ - field_index-- )); do - if [ "${hit_barrier}" = 'false' ]; then - if [[ "${field_list[field_index]}" =~ ^[0-7]{3,4}$ ]] \ - || [[ "${field_list[field_index]}" =~ ^(nosuid|exactwhitelist|matchwhitelist|disablewhitelist)$ ]]; then - hit_barrier='true' + split_index='' + case "${field_list[field_count-1]}" in + nosuid|exactwhitelist|matchwhitelist|disablewhitelist) + split_index=$(( field_count - 1 )) + ;; + *) + fallback_index='' + for candidate_index in "$(( field_count - 3 ))" "$(( field_count - 4 ))"; do + (( candidate_index >= 1 )) || continue + [[ "${field_list[candidate_index]}" =~ ^[0-7]{3,4}$ ]] || continue + if [ -z "${fallback_index}" ]; then + fallback_index="${candidate_index}" fi - meta_list=( "${field_list[field_index]}" "${meta_list[@]}" ) - else - fso_name="${field_list[field_index]}${fso_name:+" ${fso_name}"}" - fi - done + fso_name="$(IFS=' '; printf '%s' "${field_list[*]:0:candidate_index}")" + if (( candidate_index == 1 )) || [ -e "${fso_name}" ]; then + split_index="${candidate_index}" + break + fi + done + if [ -z "${split_index}" ]; then + split_index="${fallback_index}" + fi + ;; + esac + if [ -n "${split_index}" ]; then + fso_name="$(IFS=' '; printf '%s' "${field_list[*]:0:split_index}")" + meta_list=( "${field_list[@]:split_index}" ) + else + ## No recoverable mode/keyword tail: hand every field to the field-count + ## check below, which rejects the line loudly (exit 200). + fso_name="" + meta_list=( "${field_list[@]}" ) + fi case "${#meta_list[@]}" in 1|3|4) From 57a6530574d21307d77a371a1fb53259bfbe22fa Mon Sep 17 00:00:00 2001 From: "claude (dev556 4bdc306f-7204-4d7f-ba68-0c897dcf0291)" Date: Tue, 15 Sep 2026 10:52:46 -0400 Subject: [PATCH 7/8] security-misc: simplify preinst helper-scripts sourcing - helper-scripts is a Pre-Depends, configured before this preinst; strict options first, then plain source -- errexit turns any load failure into a hard stop, so the bespoke fail-closed handling + type -t check are redundant --- debian/security-misc-shared.preinst | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/debian/security-misc-shared.preinst b/debian/security-misc-shared.preinst index 50dbc7fc..1fa5b35f 100755 --- a/debian/security-misc-shared.preinst +++ b/debian/security-misc-shared.preinst @@ -3,23 +3,6 @@ ## Copyright (C) 2012 - 2025 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. -## helper-scripts is a Pre-Depends: it is fully configured before this preinst -## runs, so its libraries are guaranteed present and source-able here. Source -## pre.bsh (error handler) and accountctl.sh (group_has_nonroot_member; also -## pulls in has.bsh, so 'has' is available). -## -## Fail CLOSED, loudly: a source failure (unreadable/partial/syntax) here would -## otherwise be swallowed (errexit is not set yet), and a missing -## group_has_nonroot_member would degrade the lockout check into a misleading -## 'no account is a member of sudo' abort. A security check that cannot load its -## verifier must stop with the TRUE cause, not skip or mislead. -source /usr/libexec/helper-scripts/pre.bsh || { printf '%s\n' "${0}: ERROR: failed to source /usr/libexec/helper-scripts/pre.bsh; helper-scripts is broken or missing." >&2; exit 1; } -source /usr/libexec/helper-scripts/accountctl.sh || { printf '%s\n' "${0}: ERROR: failed to source /usr/libexec/helper-scripts/accountctl.sh; helper-scripts is broken or missing." >&2; exit 1; } -if ! type -t group_has_nonroot_member >/dev/null 2>&1; then - printf '%s\n' "${0}: ERROR: helper-scripts accountctl.sh did not provide group_has_nonroot_member; cannot verify account lockout safety. helper-scripts is too old or incomplete." >&2 - exit 1 -fi - set -o errexit set -o nounset set -o pipefail @@ -28,6 +11,11 @@ shopt -s inherit_errexit shopt -s shift_verbose export LC_ALL=C +## helper-scripts is a Pre-Depends, so it is fully configured and source-able +## here (accountctl.sh provides group_has_nonroot_member; pulls in has.bsh). +source /usr/libexec/helper-scripts/pre.bsh +source /usr/libexec/helper-scripts/accountctl.sh + true " ##################################################################### ## INFO: BEGIN: ${DPKG_MAINTSCRIPT_PACKAGE:-} ${DPKG_MAINTSCRIPT_NAME:-} $* From 00bad98497794d461fe4923e4ff182a61b918df7 Mon Sep 17 00:00:00 2001 From: "claude (dev556 4bdc306f-7204-4d7f-ba68-0c897dcf0291)" Date: Tue, 15 Sep 2026 10:56:10 -0400 Subject: [PATCH 8/8] security-misc: source has.bsh explicitly in preinst --- debian/security-misc-shared.preinst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/debian/security-misc-shared.preinst b/debian/security-misc-shared.preinst index 1fa5b35f..1c2856cc 100755 --- a/debian/security-misc-shared.preinst +++ b/debian/security-misc-shared.preinst @@ -12,8 +12,9 @@ shopt -s shift_verbose export LC_ALL=C ## helper-scripts is a Pre-Depends, so it is fully configured and source-able -## here (accountctl.sh provides group_has_nonroot_member; pulls in has.bsh). +## here (accountctl.sh provides group_has_nonroot_member). source /usr/libexec/helper-scripts/pre.bsh +source /usr/libexec/helper-scripts/has.bsh source /usr/libexec/helper-scripts/accountctl.sh true "