From 5daefde4e8cb4705d8fbe7886f2e003e61e0b974 Mon Sep 17 00:00:00 2001 From: "claude (dev118 bd22decd-3027-4ab5-9e48-11adfe39b279)" Date: Tue, 28 Jul 2026 13:50:24 -0400 Subject: [PATCH 01/17] consumer-claude-code: mention-only triggers (propagated from template) Byte-identical cp from developer-meta-files/consumer-templates. pull_request and workflow_dispatch resolve to claude-code-action's agent mode, whose trigger is a non-empty 'prompt' input that this workflow does not pass, so those events could only spin up a runner and exit without calling the model - a green "Claude Code Review" check that reviewed nothing. Co-Authored-By: Claude --- .github/workflows/consumer-claude-code.yml | 73 +++++++++++----------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/.github/workflows/consumer-claude-code.yml b/.github/workflows/consumer-claude-code.yml index 9f8a1b1f..64a46f91 100644 --- a/.github/workflows/consumer-claude-code.yml +++ b/.github/workflows/consumer-claude-code.yml @@ -14,58 +14,55 @@ name: Claude Code Review +## Mention-driven only. claude-code-action picks its mode from the +## event: comment events with no 'prompt' input select tag mode, +## where the '@claude' phrase is the trigger and the model runs. +## Every other event (pull_request, workflow_dispatch, schedule) +## selects agent mode, whose trigger is a non-empty 'prompt' - which +## this workflow does not pass - so those events can only spin up a +## runner and exit without calling the model. Listing them here +## would put a green "Claude Code Review" check on every PR that +## never reviewed anything. See the reusable's header for the full +## rationale and for what enabling real auto-review would cost. on: - pull_request: - branches: [master] issue_comment: types: [created] pull_request_review_comment: types: [created] - workflow_dispatch: permissions: contents: read -## issue_comment / pull_request_review_comment fire with -## github.ref = default branch (NOT the PR head ref). Plain -## ${{ github.ref }} would queue unrelated PRs' comment-triggered -## runs into the same group. The PR/issue-number fallback chain -## isolates per-PR. +## Comment events fire with github.ref = default branch (NOT the PR +## head ref). Plain ${{ github.ref }} would queue unrelated PRs' +## comment-triggered runs into the same group. The PR/issue-number +## fallback chain isolates per-PR. ## -## Bot-vs-human split via the trailing actor-class suffix: when -## the Claude action posts a review comment, it does so as -## claude[bot], which fires this workflow again. Without the -## split, that bot-triggered run would land in the same -## concurrency group as the in-flight human @claude request, and -## 'cancel-in-progress: true' below would cancel the review -## mid-flight before any output reached the PR. The job-level -## 'if:' filter further down catches the bot run (claude[bot]'s -## own comment body does not contain '@claude'), but by then -## concurrency has already done the damage. GitHub's standard -## '[bot]' suffix on App accounts (claude[bot], -## github-advanced-security[bot], dependabot[bot], ...) lets a -## single endsWith() check isolate all bot-authored events into -## a separate group so they only cancel each other. +## Bot-vs-human split via the trailing actor-class suffix: when the +## Claude action posts a review comment, it does so as claude[bot], +## which fires this workflow again. Without the split, that +## bot-triggered run would land in the same concurrency group as the +## in-flight human @claude request, and 'cancel-in-progress: true' +## below would cancel the review mid-flight before any output +## reached the PR. The job-level 'if:' filter further down catches +## the bot run (claude[bot]'s own comment body does not contain +## '@claude'), but by then concurrency has already done the damage. +## GitHub's standard '[bot]' suffix on App accounts (claude[bot], +## github-advanced-security[bot], dependabot[bot], ...) lets a single +## endsWith() check isolate all bot-authored events into a separate +## group so they only cancel each other. concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.issue.number || github.ref }}-${{ endsWith(github.actor, '[bot]') && 'bot' || 'human' }} cancel-in-progress: true jobs: review: - ## Pre-filter comment events for the '@claude' substring before - ## the runner spins up. The reusable's job-level 'if:' enforces - ## the same gate plus the allowed-users roster check, but doing - ## the cheap substring test here too prevents a runner from - ## starting (and stopping seconds later) for every unrelated - ## comment on every issue/PR. pull_request and workflow_dispatch - ## events pass through unconditionally. - if: | - github.event_name == 'pull_request' || - github.event_name == 'workflow_dispatch' || - (github.event_name == 'issue_comment' - && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review_comment' - && contains(github.event.comment.body, '@claude')) + ## Pre-filter for the '@claude' substring before the runner spins + ## up. The reusable's job-level 'if:' enforces the same gate plus + ## the allowed-users roster check, but doing the cheap substring + ## test here too prevents a runner from starting (and stopping + ## seconds later) for every unrelated comment on every issue/PR. + if: contains(github.event.comment.body, '@claude') uses: org-ai-assisted/developer-meta-files/.github/workflows/reusable-claude-code-review.yml@master ## Explicit secret forwarding (NOT 'secrets: inherit'). With ## inherit, every caller-side secret would flow into the @@ -73,6 +70,10 @@ jobs: ## context to exactly CLAUDE_CODE_OAUTH_TOKEN. secrets: CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + ## 'contents: read' does NOT bound what Claude can do - the + ## action swaps the runner token for an OIDC-minted claude[bot] + ## App token carrying contents/issues/pull-requests write. The + ## block is required for 'id-token: write'. See the reusable. permissions: contents: read pull-requests: write From e8e9fd4fae34474d5d1a96c516ddfd1913b7fd82 Mon Sep 17 00:00:00 2001 From: "assisted-by-ai (Bot Account) (Machine Account)" <228987186+assisted-by-ai@users.noreply.github.com> Date: Thu, 30 Jul 2026 11:59:15 -0400 Subject: [PATCH 02/17] consumer-claude-code: propagate the corrected template Byte-identical to developer-meta-files consumer-templates. The github.ref comment was wrong for one of the two events it described: issue_comment fires with the default branch ref, pull_request_review_comment with the PR merge ref (refs/pull//merge). No behaviour change -- the concurrency group reaches github.ref only if neither number field is set, and one always is for the events this workflow accepts. Generated with assistance from Claude Code. --- .github/workflows/consumer-claude-code.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/consumer-claude-code.yml b/.github/workflows/consumer-claude-code.yml index 64a46f91..a81d2f7f 100644 --- a/.github/workflows/consumer-claude-code.yml +++ b/.github/workflows/consumer-claude-code.yml @@ -33,10 +33,14 @@ on: permissions: contents: read -## Comment events fire with github.ref = default branch (NOT the PR -## head ref). Plain ${{ github.ref }} would queue unrelated PRs' +## The two comment events differ: issue_comment fires with +## github.ref = the default branch, pull_request_review_comment with +## the PR merge ref (refs/pull//merge). Neither is the PR head +## ref, and plain ${{ github.ref }} would queue unrelated PRs' ## comment-triggered runs into the same group. The PR/issue-number -## fallback chain isolates per-PR. +## chain below is what actually applies: one of those two number +## fields is populated for every event this workflow accepts, so +## github.ref is a fallback that is never reached here. ## ## Bot-vs-human split via the trailing actor-class suffix: when the ## Claude action posts a review comment, it does so as claude[bot], From 32cb7a2f36105a23bed0562293901e3799cd7892 Mon Sep 17 00:00:00 2001 From: "claude (dev142 b93fb2c6-e15f-4d45-8566-f3ded893b70e)" Date: Fri, 31 Jul 2026 16:23:30 -0400 Subject: [PATCH 03/17] CI: propagate codeql-actions + secrets-audit consumer workflows Byte-identical propagation from developer-meta-files consumer-templates. Closes scanner-coverage gaps (bandit already present): - consumer-codeql-actions.yml: CodeQL 'actions' language -- workflow-YAML security analysis (script-injection, missing permissions, unpinned actions). - consumer-secrets-audit.yml: manual-dispatch secrets surface audit (presence flags only, values never emitted). Managed files -- do not hand-edit; regenerated on the next propagation pass. ## AI-Assisted Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/consumer-codeql-actions.yml | 48 +++++++++++++++++++ .github/workflows/consumer-secrets-audit.yml | 41 ++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 .github/workflows/consumer-codeql-actions.yml create mode 100644 .github/workflows/consumer-secrets-audit.yml diff --git a/.github/workflows/consumer-codeql-actions.yml b/.github/workflows/consumer-codeql-actions.yml new file mode 100644 index 00000000..8391ed4b --- /dev/null +++ b/.github/workflows/consumer-codeql-actions.yml @@ -0,0 +1,48 @@ +## Copyright (C) 2026 - 2026 ENCRYPTED SUPPORT LLC +## See the file COPYING for copying conditions. + +## AI-Assisted + +## Managed by pkg_update_consumer_workflows. Byte-identical +## across consumers; this file is `cp`-ed from +## developer-meta-files/consumer-templates/.github/workflows/consumer-codeql-actions.yml. +## DO NOT hand-edit this file in the consumer repo - changes +## will be overwritten on the next propagation pass. +## +## Consumer wrapper for the 'actions' CodeQL language - workflow +## YAML security analysis (missing-permissions, script-injection, +## unversioned-immutable-actions, etc.). +## +## Cronless on purpose: byte-identical propagation forbids +## per-repo cron rewrites at propagation time. Push / PR / +## workflow_dispatch triggers cover the scan-on-change cases; +## rule-refresh re-scans can be kicked manually from the Actions +## tab. +## +## Reusable docs: +## https://github.com/org-ai-assisted/developer-meta-files/blob/master/.github/workflows/reusable-codeql.yml + +name: CodeQL Actions + +on: + push: + branches: [master] + pull_request: + branches: [master] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + actions: + uses: org-ai-assisted/developer-meta-files/.github/workflows/reusable-codeql.yml@master + with: + language: actions + permissions: + security-events: write + contents: read diff --git a/.github/workflows/consumer-secrets-audit.yml b/.github/workflows/consumer-secrets-audit.yml new file mode 100644 index 00000000..579f9915 --- /dev/null +++ b/.github/workflows/consumer-secrets-audit.yml @@ -0,0 +1,41 @@ +## Copyright (C) 2026 - 2026 ENCRYPTED SUPPORT LLC +## See the file COPYING for copying conditions. + +## AI-Assisted + +## Managed by pkg_update_consumer_workflows. Byte-identical +## across consumers; this file is `cp`-ed from +## developer-meta-files/consumer-templates/.github/workflows/consumer-secrets-audit.yml. +## DO NOT hand-edit this file in the consumer repo - changes +## will be overwritten on the next propagation pass. +## +## Manual-trigger entry point. Run from the Actions tab: +## 'Secrets surface audit' > 'Run workflow'. Use as a sanity +## check after changing any reusable's 'workflow_call.secrets' +## schema or any consumer's 'secrets:' map. +## +## 'secrets: inherit' here forwards the entire repo-level + +## org-level secret set into the audit reusable, which prints +## presence flags. The audit boolean is resolved at +## expression-evaluation time, so secret values themselves never +## land in env or step output. +## +## Reusable docs: +## https://github.com/org-ai-assisted/developer-meta-files/blob/master/.github/workflows/reusable-secrets-audit.yml + +name: Secrets surface audit + +on: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + audit: + uses: org-ai-assisted/developer-meta-files/.github/workflows/reusable-secrets-audit.yml@master + secrets: inherit From a2b13848b6a2189c0bda2b478f5ac1b0f9c88819 Mon Sep 17 00:00:00 2001 From: "claude (dev163 38f9745c-aed1-4fb4-b6ae-9899f20f8de2)" Date: Wed, 5 Aug 2026 13:57:22 -0400 Subject: [PATCH 04/17] security-misc: R-010 strict for the module-disabled alert scripts The thirteen disabled-*-by-security-misc scripts are near-identical: they warn that a kernel module was blocked and exit 1. Add the strict preamble and convert echo -> printf (R-034). The '$@' inside the message becomes '$*' deliberately. Under printf, "...$@" expands to one argument per positional parameter, so a multi-word invocation would print one word per line; '$*' joins with spaces, which is what echo did. Sandbox differential vs the pre-change scripts: all thirteen byte-identical in output and still exit 1. Co-Authored-By: Claude Opus 5 (1M context) --- ...abled-bluetooth-by-security-misc#security-misc-shared | 9 ++++++++- .../disabled-cdrom-by-security-misc#security-misc-shared | 9 ++++++++- ...disabled-cpumsr-by-security-misc#security-misc-shared | 9 ++++++++- ...isabled-filesys-by-security-misc#security-misc-shared | 9 ++++++++- ...sabled-firewire-by-security-misc#security-misc-shared | 9 ++++++++- ...led-framebuffer-by-security-misc#security-misc-shared | 9 ++++++++- .../disabled-gps-by-security-misc#security-misc-shared | 9 ++++++++- ...isabled-intelme-by-security-misc#security-misc-shared | 9 ++++++++- ...sabled-intelpmt-by-security-misc#security-misc-shared | 9 ++++++++- ...d-miscellaneous-by-security-misc#security-misc-shared | 9 ++++++++- ...bled-netfilesys-by-security-misc#security-misc-shared | 9 ++++++++- ...isabled-network-by-security-misc#security-misc-shared | 9 ++++++++- ...led-thunderbolt-by-security-misc#security-misc-shared | 9 ++++++++- 13 files changed, 104 insertions(+), 13 deletions(-) diff --git a/usr/bin/disabled-bluetooth-by-security-misc#security-misc-shared b/usr/bin/disabled-bluetooth-by-security-misc#security-misc-shared index d4ae8662..f52a6b1d 100755 --- a/usr/bin/disabled-bluetooth-by-security-misc#security-misc-shared +++ b/usr/bin/disabled-bluetooth-by-security-misc#security-misc-shared @@ -5,6 +5,13 @@ ## Alerts user that a kernel module failed to load due to it being explicitly disabled by default. -echo "$0: ALERT: This Bluetooth kernel module is disabled by package security-misc-shared by default. See the configuration file /etc/modprobe.d/30_security-misc_disable.conf for details. | args: $@" >&2 +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose + +printf '%s\n' "$0: ALERT: This Bluetooth kernel module is disabled by package security-misc-shared by default. See the configuration file /etc/modprobe.d/30_security-misc_disable.conf for details. | args: $*" >&2 exit 1 diff --git a/usr/bin/disabled-cdrom-by-security-misc#security-misc-shared b/usr/bin/disabled-cdrom-by-security-misc#security-misc-shared index 7749d060..bb4db834 100755 --- a/usr/bin/disabled-cdrom-by-security-misc#security-misc-shared +++ b/usr/bin/disabled-cdrom-by-security-misc#security-misc-shared @@ -5,6 +5,13 @@ ## Alerts user that a kernel module failed to load due to it being explicitly disabled by default. -echo "$0: ALERT: This CD-ROM/DVD kernel module is disabled by package security-misc-shared by default. See the configuration file /etc/modprobe.d/30_security-misc_disable.conf for details. | args: $@" >&2 +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose + +printf '%s\n' "$0: ALERT: This CD-ROM/DVD kernel module is disabled by package security-misc-shared by default. See the configuration file /etc/modprobe.d/30_security-misc_disable.conf for details. | args: $*" >&2 exit 1 diff --git a/usr/bin/disabled-cpumsr-by-security-misc#security-misc-shared b/usr/bin/disabled-cpumsr-by-security-misc#security-misc-shared index a6b0223d..7e5bdadd 100755 --- a/usr/bin/disabled-cpumsr-by-security-misc#security-misc-shared +++ b/usr/bin/disabled-cpumsr-by-security-misc#security-misc-shared @@ -5,6 +5,13 @@ ## Alerts user that a kernel module failed to load due to it being explicitly disabled by default. -echo "$0: ALERT: This CPU MSR kernel module is disabled by package security-misc-shared by default. See the configuration file /etc/modprobe.d/30_security-misc_disable.conf for details. | args: $@" >&2 +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose + +printf '%s\n' "$0: ALERT: This CPU MSR kernel module is disabled by package security-misc-shared by default. See the configuration file /etc/modprobe.d/30_security-misc_disable.conf for details. | args: $*" >&2 exit 1 diff --git a/usr/bin/disabled-filesys-by-security-misc#security-misc-shared b/usr/bin/disabled-filesys-by-security-misc#security-misc-shared index d37c52ec..d85a951a 100755 --- a/usr/bin/disabled-filesys-by-security-misc#security-misc-shared +++ b/usr/bin/disabled-filesys-by-security-misc#security-misc-shared @@ -5,6 +5,13 @@ ## Alerts user that a kernel module failed to load due to it being explicitly disabled by default. -echo "$0: ALERT: This file system kernel module is disabled by package security-misc-shared by default. See the configuration file /etc/modprobe.d/30_security-misc_disable.conf for details. | args: $@" >&2 +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose + +printf '%s\n' "$0: ALERT: This file system kernel module is disabled by package security-misc-shared by default. See the configuration file /etc/modprobe.d/30_security-misc_disable.conf for details. | args: $*" >&2 exit 1 diff --git a/usr/bin/disabled-firewire-by-security-misc#security-misc-shared b/usr/bin/disabled-firewire-by-security-misc#security-misc-shared index 4511d902..ecd429e1 100755 --- a/usr/bin/disabled-firewire-by-security-misc#security-misc-shared +++ b/usr/bin/disabled-firewire-by-security-misc#security-misc-shared @@ -5,6 +5,13 @@ ## Alerts user that a kernel module failed to load due to it being explicitly disabled by default. -echo "$0: ALERT: This FireWire (IEEE 1394) kernel module is disabled by package security-misc-shared by default. See the configuration file /etc/modprobe.d/30_security-misc_disable.conf for details. | args: $@" >&2 +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose + +printf '%s\n' "$0: ALERT: This FireWire (IEEE 1394) kernel module is disabled by package security-misc-shared by default. See the configuration file /etc/modprobe.d/30_security-misc_disable.conf for details. | args: $*" >&2 exit 1 diff --git a/usr/bin/disabled-framebuffer-by-security-misc#security-misc-shared b/usr/bin/disabled-framebuffer-by-security-misc#security-misc-shared index 0f6879ca..97e5869a 100755 --- a/usr/bin/disabled-framebuffer-by-security-misc#security-misc-shared +++ b/usr/bin/disabled-framebuffer-by-security-misc#security-misc-shared @@ -5,6 +5,13 @@ ## Alerts user that a kernel module failed to load due to it being explicitly disabled by default. -echo "$0: ALERT: This framebuffer (fbdev) kernel module is disabled by package security-misc-shared by default. See the configuration file /etc/modprobe.d/30_security-misc_disable.conf for details. | args: $@" >&2 +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose + +printf '%s\n' "$0: ALERT: This framebuffer (fbdev) kernel module is disabled by package security-misc-shared by default. See the configuration file /etc/modprobe.d/30_security-misc_disable.conf for details. | args: $*" >&2 exit 1 diff --git a/usr/bin/disabled-gps-by-security-misc#security-misc-shared b/usr/bin/disabled-gps-by-security-misc#security-misc-shared index 14131ad1..9a29b36d 100755 --- a/usr/bin/disabled-gps-by-security-misc#security-misc-shared +++ b/usr/bin/disabled-gps-by-security-misc#security-misc-shared @@ -5,6 +5,13 @@ ## Alerts user that a kernel module failed to load due to it being explicitly disabled by default. -echo "$0: ALERT: This Global Positioning System (GPS) kernel module is disabled by package security-misc-shared by default. See the configuration file /etc/modprobe.d/30_security-misc_disable.conf for details. | args: $@" >&2 +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose + +printf '%s\n' "$0: ALERT: This Global Positioning System (GPS) kernel module is disabled by package security-misc-shared by default. See the configuration file /etc/modprobe.d/30_security-misc_disable.conf for details. | args: $*" >&2 exit 1 diff --git a/usr/bin/disabled-intelme-by-security-misc#security-misc-shared b/usr/bin/disabled-intelme-by-security-misc#security-misc-shared index 787e6a2b..b50fbbd0 100755 --- a/usr/bin/disabled-intelme-by-security-misc#security-misc-shared +++ b/usr/bin/disabled-intelme-by-security-misc#security-misc-shared @@ -5,6 +5,13 @@ ## Alerts user that a kernel module failed to load due to it being explicitly disabled by default. -echo "$0: ALERT: This Intel Management Engine (ME) kernel module is disabled by package security-misc-shared by default. See the configuration file /etc/modprobe.d/30_security-misc_disable.conf for details. | args: $@" >&2 +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose + +printf '%s\n' "$0: ALERT: This Intel Management Engine (ME) kernel module is disabled by package security-misc-shared by default. See the configuration file /etc/modprobe.d/30_security-misc_disable.conf for details. | args: $*" >&2 exit 1 diff --git a/usr/bin/disabled-intelpmt-by-security-misc#security-misc-shared b/usr/bin/disabled-intelpmt-by-security-misc#security-misc-shared index 60054826..edee2680 100755 --- a/usr/bin/disabled-intelpmt-by-security-misc#security-misc-shared +++ b/usr/bin/disabled-intelpmt-by-security-misc#security-misc-shared @@ -5,6 +5,13 @@ ## Alerts user that a kernel module failed to load due to it being explicitly disabled by default. -echo "$0: ALERT: This Intel Platform Monitoring Technology (PMT) Telemetry kernel module is disabled by package security-misc-shared by default. See the configuration file /etc/modprobe.d/30_security-misc_disable.conf for details. | args: $@" >&2 +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose + +printf '%s\n' "$0: ALERT: This Intel Platform Monitoring Technology (PMT) Telemetry kernel module is disabled by package security-misc-shared by default. See the configuration file /etc/modprobe.d/30_security-misc_disable.conf for details. | args: $*" >&2 exit 1 diff --git a/usr/bin/disabled-miscellaneous-by-security-misc#security-misc-shared b/usr/bin/disabled-miscellaneous-by-security-misc#security-misc-shared index f5ddcb54..04b627c8 100755 --- a/usr/bin/disabled-miscellaneous-by-security-misc#security-misc-shared +++ b/usr/bin/disabled-miscellaneous-by-security-misc#security-misc-shared @@ -5,6 +5,13 @@ ## Alerts user that a kernel module failed to load due to it being explicitly disabled by default. -echo "$0: ALERT: This kernel module is disabled by package security-misc-shared by default. See the configuration file /etc/modprobe.d/30_security-misc_disable.conf for details. | args: $@" >&2 +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose + +printf '%s\n' "$0: ALERT: This kernel module is disabled by package security-misc-shared by default. See the configuration file /etc/modprobe.d/30_security-misc_disable.conf for details. | args: $*" >&2 exit 1 diff --git a/usr/bin/disabled-netfilesys-by-security-misc#security-misc-shared b/usr/bin/disabled-netfilesys-by-security-misc#security-misc-shared index 9b00de51..de4db437 100755 --- a/usr/bin/disabled-netfilesys-by-security-misc#security-misc-shared +++ b/usr/bin/disabled-netfilesys-by-security-misc#security-misc-shared @@ -5,6 +5,13 @@ ## Alerts user that a kernel module failed to load due to it being explicitly disabled by default. -echo "$0: ALERT: This network file system kernel module is disabled by package security-misc-shared by default. See the configuration file /etc/modprobe.d/30_security-misc_disable.conf for details. | args: $@" >&2 +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose + +printf '%s\n' "$0: ALERT: This network file system kernel module is disabled by package security-misc-shared by default. See the configuration file /etc/modprobe.d/30_security-misc_disable.conf for details. | args: $*" >&2 exit 1 diff --git a/usr/bin/disabled-network-by-security-misc#security-misc-shared b/usr/bin/disabled-network-by-security-misc#security-misc-shared index 02bdb6ca..3d2e342a 100755 --- a/usr/bin/disabled-network-by-security-misc#security-misc-shared +++ b/usr/bin/disabled-network-by-security-misc#security-misc-shared @@ -5,6 +5,13 @@ ## Alerts user that a kernel module failed to load due to it being explicitly disabled by default. -echo "$0: ALERT: This network protocol kernel module is disabled by package security-misc-shared by default. See the configuration file /etc/modprobe.d/30_security-misc_disable.conf for details. | args: $@" >&2 +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose + +printf '%s\n' "$0: ALERT: This network protocol kernel module is disabled by package security-misc-shared by default. See the configuration file /etc/modprobe.d/30_security-misc_disable.conf for details. | args: $*" >&2 exit 1 diff --git a/usr/bin/disabled-thunderbolt-by-security-misc#security-misc-shared b/usr/bin/disabled-thunderbolt-by-security-misc#security-misc-shared index 0939dc79..faa380bc 100755 --- a/usr/bin/disabled-thunderbolt-by-security-misc#security-misc-shared +++ b/usr/bin/disabled-thunderbolt-by-security-misc#security-misc-shared @@ -5,6 +5,13 @@ ## Alerts user that a kernel module failed to load due to it being explicitly disabled by default. -echo "$0: ALERT: This Thunderbolt kernel module is disabled by package security-misc-shared by default. See the configuration file /etc/modprobe.d/30_security-misc_disable.conf for details. | args: $@" >&2 +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose + +printf '%s\n' "$0: ALERT: This Thunderbolt kernel module is disabled by package security-misc-shared by default. See the configuration file /etc/modprobe.d/30_security-misc_disable.conf for details. | args: $*" >&2 exit 1 From 59b3e60b2627a9eeda0f4e7cbd4804958cabedd3 Mon Sep 17 00:00:00 2001 From: "claude (dev163 38f9745c-aed1-4fb4-b6ae-9899f20f8de2)" Date: Wed, 5 Aug 2026 13:57:39 -0400 Subject: [PATCH 05/17] security-misc: R-010 strict for the PAM helpers, preserving exit codes These four scripts are invoked by pam_exec and their EXIT CODE is the security decision: the PAM stacks use '[success=1 default=ignore]', so the code selects whether the next module runs. Strict mode must not perturb it. PAM_SERVICE and PAM_USER come from pam_exec's environment. Read bare under nounset they abort the script with a NONZERO status, which PAM then reads as a control-flow decision. Each is now read as '${VAR:-}', which reproduces today's semantics exactly (an unset value falls through to the same branch it already took). pam-abort-on-locked-password was the sharp case. It documents "identifiable exit codes in case stdout / stderr is not logged in journal"; with PAM_USER absent it returned 3 ("user does not exist"), and a bare read turned that into 1. Verified restored. Exit-code matrix, old vs new, all matching: PAM_SERVICE=login -> pam_only_if_login 1, pam_only_if_su 0 PAM_SERVICE=su -> pam_only_if_login 0, pam_only_if_su 1 PAM_SERVICE=sshd -> both 0 PAM_SERVICE empty -> both 0 PAM_SERVICE absent -> pam_only_if_login 0, pam_only_if_su 0, pam_faillock_not_if_x 1 PAM_USER root / nonexistent / absent -> pam-abort 3 in every case Co-Authored-By: Claude Opus 5 (1M context) --- ...rt-on-locked-password#security-misc-shared | 46 ++++++++++++------- ...pam_faillock_not_if_x#security-misc-shared | 16 +++++-- .../pam_only_if_login#security-misc-shared | 10 +++- .../pam_only_if_su#security-misc-shared | 10 +++- 4 files changed, 58 insertions(+), 24 deletions(-) diff --git a/usr/libexec/security-misc/pam-abort-on-locked-password#security-misc-shared b/usr/libexec/security-misc/pam-abort-on-locked-password#security-misc-shared index 35c2dd4b..540cbce3 100755 --- a/usr/libexec/security-misc/pam-abort-on-locked-password#security-misc-shared +++ b/usr/libexec/security-misc/pam-abort-on-locked-password#security-misc-shared @@ -7,35 +7,47 @@ ## counter. This is not a security feature. ## https://forums.whonix.org/t/restrict-root-access/7658/1 +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose + +## PAM_USER is supplied by pam_exec. Read it nounset-safely: this script +## documents IDENTIFIABLE exit codes, and a bare read would abort with 1 +## instead of reaching the intended "user does not exist" exit 3. +PAM_USER="${PAM_USER:-}" + passwd_bin="$(type -P -- "passwd")" -if ! test -x "$passwd_bin" ; then - echo "\ -$0: ERROR: passwd_bin \"$passwd_bin\" is not executable. +if ! test -x "${passwd_bin}" ; then + printf '%s\n' "\ +$0: ERROR: passwd_bin \"${passwd_bin}\" is not executable. See https://www.kicksecure.com/wiki/SUID_Disabler_and_Permission_Hardener#passwd" >&2 ## Identifiable exit codes in case stdout / stderr is not logged in journal. exit 2 fi -if ! passwd_output="$("$passwd_bin" -S -- "$PAM_USER" 2>/dev/null)" ; then - echo "$0: ERROR: user \"$PAM_USER\" does not exist." >&2 +if ! passwd_output="$("${passwd_bin}" -S -- "${PAM_USER}" 2>/dev/null)" ; then + printf '%s\n' "$0: ERROR: user \"${PAM_USER}\" does not exist." >&2 exit 3 fi -password_status_field="$(echo "$passwd_output" | cut -d ' ' -f 2)" +password_status_field="$(printf '%s\n' "${passwd_output}" | cut -d ' ' -f 2)" -if [ "$password_status_field" = "P" ]; then - true "$0: INFO: user \"$PAM_USER\" has a usable password." -elif [ "$password_status_field" = "NP" ]; then - true "$0: INFO: user \"$PAM_USER\" has no password." -elif [ "$password_status_field" = "L" ]; then - echo "$0: INFO: Password for user \"$PAM_USER\" is locked." +if [ "${password_status_field}" = "P" ]; then + true "$0: INFO: user \"${PAM_USER}\" has a usable password." +elif [ "${password_status_field}" = "NP" ]; then + true "$0: INFO: user \"${PAM_USER}\" has no password." +elif [ "${password_status_field}" = "L" ]; then + printf '%s\n' "$0: INFO: Password for user \"${PAM_USER}\" is locked." if [ -f /usr/share/whonix/marker ] || [ -f /usr/share/kicksecure/marker ]; then - if [ "$PAM_USER" = "root" ]; then - echo "$0: ERROR: root account is locked by default. See:" >&2 - echo "https://www.kicksecure.com/wiki/root" >&2 - echo "" >&2 + if [ "${PAM_USER}" = "root" ]; then + printf '%s\n' "$0: ERROR: root account is locked by default. See:" >&2 + printf '%s\n' "https://www.kicksecure.com/wiki/root" >&2 + printf '%s\n' "" >&2 exit 4 fi fi @@ -47,7 +59,7 @@ elif [ "$password_status_field" = "L" ]; then ## faster feedback. A new login attempt would not be needlessly delayed. exit 0 else - echo "$0: INFO: Password status field for user \"$PAM_USER\" could not be parsed. Please report this bug." + printf '%s\n' "$0: INFO: Password status field for user \"${PAM_USER}\" could not be parsed. Please report this bug." fi exit 0 diff --git a/usr/libexec/security-misc/pam_faillock_not_if_x#security-misc-shared b/usr/libexec/security-misc/pam_faillock_not_if_x#security-misc-shared index 433dca87..f5fe0922 100755 --- a/usr/libexec/security-misc/pam_faillock_not_if_x#security-misc-shared +++ b/usr/libexec/security-misc/pam_faillock_not_if_x#security-misc-shared @@ -6,8 +6,18 @@ ## https://serverfault.com/questions/134471/success-n-control-syntax-in-pam-conf-pam-d-files set -x +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose -true "PAM_SERVICE: $PAM_SERVICE" +## PAM_SERVICE is supplied by pam_exec; read it nounset-safely so an absent +## value falls through the exclusion loop as before instead of aborting. +PAM_SERVICE="${PAM_SERVICE:-}" + +true "PAM_SERVICE: ${PAM_SERVICE}" ## PAM configuration notes ## @@ -23,8 +33,8 @@ true "PAM_SERVICE: $PAM_SERVICE" ## This list can later be extended as needed. pam_service_exclusion_list="dovecot sshd" -for pam_service_exclusion_item in $pam_service_exclusion_list ; do - if [ "$PAM_SERVICE" = "$pam_service_exclusion_item" ]; then +for pam_service_exclusion_item in ${pam_service_exclusion_list} ; do + if [ "${PAM_SERVICE}" = "${pam_service_exclusion_item}" ]; then ## exit success so [success=1 default=ignore] will result in skipping the ## next PAM module (the pam_faillock module). exit 0 diff --git a/usr/libexec/security-misc/pam_only_if_login#security-misc-shared b/usr/libexec/security-misc/pam_only_if_login#security-misc-shared index 568f037d..89497633 100755 --- a/usr/libexec/security-misc/pam_only_if_login#security-misc-shared +++ b/usr/libexec/security-misc/pam_only_if_login#security-misc-shared @@ -6,10 +6,16 @@ ## https://serverfault.com/questions/134471/success-n-control-syntax-in-pam-conf-pam-d-files set -x +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose -true "PAM_SERVICE: $PAM_SERVICE" +true "PAM_SERVICE: ${PAM_SERVICE:-}" -if [ "$PAM_SERVICE" = "login" ]; then +if [ "${PAM_SERVICE:-}" = "login" ]; then ## FIXME: ## Creates unwanted journal log entry. ## pam_exec(login:account): /usr/libexec/security-misc/pam_only_if_login failed: exit code 1 diff --git a/usr/libexec/security-misc/pam_only_if_su#security-misc-shared b/usr/libexec/security-misc/pam_only_if_su#security-misc-shared index 604510f6..eeef759e 100755 --- a/usr/libexec/security-misc/pam_only_if_su#security-misc-shared +++ b/usr/libexec/security-misc/pam_only_if_su#security-misc-shared @@ -7,10 +7,16 @@ ## /usr/libexec/security-misc/pam_only_if_login set -x +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose -true "PAM_SERVICE: $PAM_SERVICE" +true "PAM_SERVICE: ${PAM_SERVICE:-}" -if [ "$PAM_SERVICE" = "su" ]; then +if [ "${PAM_SERVICE:-}" = "su" ]; then exit 1 else exit 0 From 9d6c4634a056c0711d3a42b8c3e4c876b93ec001 Mon Sep 17 00:00:00 2001 From: "claude (dev163 38f9745c-aed1-4fb4-b6ae-9899f20f8de2)" Date: Wed, 5 Aug 2026 13:57:59 -0400 Subject: [PATCH 06/17] security-misc: R-010 strict for the remaining standalone scripts Covers the eleven remaining standalones, plus the whole-file debt the gate surfaces on each: echo -> printf (R-034), 'has' instead of 'command -v' (R-090, sourcing has.sh -- security-misc-shared already Depends on helper-scripts), safe-rm (R-120), SC2250 braces. permission-lockdown needed care. Its loop chmods every home directory and records a state file per user, and it tolerated per-user failures: a directory it could not chmod did not stop the ones after it. Under errexit the first failure aborted the whole loop, so every remaining home directory silently stayed un-locked-down -- observed in the sandbox as 1 user processed instead of 11, and the script exiting 1 rather than 0. The two per-user operations are now explicitly best-effort, restoring the original behaviour (including that a failed chmod still records the state file -- arguably wrong, but changing it is not this pass's business). mmap-rnd-bits keeps its 'ls -1 -t /boot/config-* | head -n 1' with a reasoned SC2012 disable: kernel config filenames carry no spaces or newlines, so the parsing hazard is unreachable, and rewriting newest-file detection in a security package is risk without gain. Not touched, deliberately: the dracut modules under usr/lib/dracut/modules.d*. 'module-setup.sh' is SOURCED by dracut at initramfs build time, and remount-secure.sh is installed with 'inst_hook' and SOURCED by dracut-lib at BOOT. A strict-mode block in either runs in dracut's own shell; in the boot case that risks an unbootable system. Sandbox differential vs the pre-change scripts, comparing output AND exit code: 36 cases identical. The only two remaining deltas are 'set -x' traces of the preamble and of echo -> printf, both at unchanged exit codes. Co-Authored-By: Claude Opus 5 (1M context) --- .../30_remove-system-map#security-misc-shared | 7 ++++ .../askpass#security-misc-shared | 9 ++++- ...ck-for-usb-controller#security-misc-shared | 13 ++++++- ...kernel-module-loading#security-misc-shared | 7 +++- .../echo-path#security-misc-shared | 10 ++++- .../mmap-rnd-bits#security-misc-shared | 39 ++++++++++++------- .../panic-on-oops#security-misc-shared | 7 +++- .../permission-lockdown#security-misc-shared | 21 +++++++--- .../placeholder#security-misc-server | 9 ++++- .../remove-system.map#security-misc-shared | 19 ++++++--- .../lkrg/lkrg-virtualbox#security-misc-shared | 15 +++++-- 11 files changed, 118 insertions(+), 38 deletions(-) diff --git a/etc/kernel/postinst.d/30_remove-system-map#security-misc-shared b/etc/kernel/postinst.d/30_remove-system-map#security-misc-shared index 416c8087..e2d1228d 100755 --- a/etc/kernel/postinst.d/30_remove-system-map#security-misc-shared +++ b/etc/kernel/postinst.d/30_remove-system-map#security-misc-shared @@ -3,6 +3,13 @@ ## Copyright (C) 2019 - 2025 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose + if test -x /usr/libexec/security-misc/remove-system.map ; then /usr/libexec/security-misc/remove-system.map fi diff --git a/usr/libexec/security-misc/askpass#security-misc-shared b/usr/libexec/security-misc/askpass#security-misc-shared index d4289750..64d36fd8 100755 --- a/usr/libexec/security-misc/askpass#security-misc-shared +++ b/usr/libexec/security-misc/askpass#security-misc-shared @@ -3,8 +3,13 @@ ## Copyright (C) 2019 - 2025 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. -set -e +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose title="$0: password required for $(whoami) to perform action as superuser" -yad --password --title="$title" +yad --password --title="${title}" diff --git a/usr/libexec/security-misc/check-for-usb-controller#security-misc-shared b/usr/libexec/security-misc/check-for-usb-controller#security-misc-shared index 3c006021..0cb043a6 100755 --- a/usr/libexec/security-misc/check-for-usb-controller#security-misc-shared +++ b/usr/libexec/security-misc/check-for-usb-controller#security-misc-shared @@ -3,12 +3,21 @@ ## Copyright (C) 2025 - 2025 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. -set -e +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose + +## security-misc-shared Depends on helper-scripts. +# shellcheck source=../../../../helper-scripts/usr/libexec/helper-scripts/has.sh +source /usr/libexec/helper-scripts/has.sh export LC_ALL='C' ## Package 'pciutils' provides tool 'lspci'. -command -v lspci &>/dev/null +has lspci if lspci | grep --quiet '^[^ ]* USB controller: '; then exit 0 diff --git a/usr/libexec/security-misc/disable-kernel-module-loading#security-misc-shared b/usr/libexec/security-misc/disable-kernel-module-loading#security-misc-shared index 817d8599..ae0ccd34 100755 --- a/usr/libexec/security-misc/disable-kernel-module-loading#security-misc-shared +++ b/usr/libexec/security-misc/disable-kernel-module-loading#security-misc-shared @@ -4,7 +4,12 @@ ## See the file COPYING for copying conditions. set -x -set -e +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose sysctl -w kernel.modules_disabled=1 diff --git a/usr/libexec/security-misc/echo-path#security-misc-shared b/usr/libexec/security-misc/echo-path#security-misc-shared index 3bcc2cd0..f33f8fef 100755 --- a/usr/libexec/security-misc/echo-path#security-misc-shared +++ b/usr/libexec/security-misc/echo-path#security-misc-shared @@ -3,6 +3,12 @@ ## Copyright (C) 2019 - 2025 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. -set -e +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose -echo "$PATH" +printf '%s +' "${PATH}" diff --git a/usr/libexec/security-misc/mmap-rnd-bits#security-misc-shared b/usr/libexec/security-misc/mmap-rnd-bits#security-misc-shared index 25745c29..8420389f 100755 --- a/usr/libexec/security-misc/mmap-rnd-bits#security-misc-shared +++ b/usr/libexec/security-misc/mmap-rnd-bits#security-misc-shared @@ -8,6 +8,13 @@ ## See also: ## https://forums.whonix.org/t/automate-mmap-randomisation-to-fix-ppc64el/16514 +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose + set -euo pipefail shopt -s failglob @@ -15,19 +22,19 @@ more_info_link="https://forums.whonix.org/t/automate-mmap-randomisation-to-fix-p aslr_mmap_config_file="/etc/sysctl.d/30_security-misc_aslr-mmap.conf" exit_with_error() { - echo "$0: SEE ALSO:" >&2 - echo "" >&2 - echo "$more_info_link" >&2 + printf '%s\n' "$0: SEE ALSO:" >&2 + printf '%s\n' "" >&2 + printf '%s\n' "${more_info_link}" >&2 exit 1 } if ! test -d /etc/sysctl.d ; then - echo "$0: ERROR: Folder /etc/sysctl.d does not exist!" >&2 + printf '%s\n' "$0: ERROR: Folder /etc/sysctl.d does not exist!" >&2 exit_with_error fi if ! test -w /etc/sysctl.d ; then - echo "$0: ERROR: Folder /etc/sysctl.d not writeable! This script is supposed to be run as root." >&2 + printf '%s\n' "$0: ERROR: Folder /etc/sysctl.d not writeable! This script is supposed to be run as root." >&2 exit_with_error fi @@ -37,19 +44,23 @@ BITS_MAX_DEFAULT=32 COMPAT_BITS_MAX_DEFAULT=16 ## Find the most recently modified Linux config file. +## Kernel config filenames are /boot/config-: no spaces or newlines, +## so the ls-parsing hazard SC2012 warns about is not reachable here, and a +## find-based newest-file rewrite would be a behaviour risk for no gain. +# shellcheck disable=SC2012 if compgen -G "/boot/config-*" > /dev/null && CONFIG=$(ls -1 -t /boot/config-* | head -n 1) ; then ## Find the relevant config options. if ! BITS_MAX=$(grep -E '^CONFIG_ARCH_MMAP_RND_BITS_MAX=[0-9]+$' "${CONFIG}" | cut -d "=" -f 2) ; then - echo "$0: ERROR: Error detecting CONFIG_ARCH_MMAP_RND_BITS_MAX! Using built-in default." >&2 + printf '%s\n' "$0: ERROR: Error detecting CONFIG_ARCH_MMAP_RND_BITS_MAX! Using built-in default." >&2 BITS_MAX="${BITS_MAX_DEFAULT}" fi if ! COMPAT_BITS_MAX=$(grep -E '^CONFIG_ARCH_MMAP_RND_COMPAT_BITS_MAX=[0-9]+$' "${CONFIG}" | cut -d "=" -f 2) ; then - echo "$0: ERROR: Error detecting CONFIG_ARCH_MMAP_RND_COMPAT_BITS_MAX! Using built-in default." >&2 + printf '%s\n' "$0: ERROR: Error detecting CONFIG_ARCH_MMAP_RND_COMPAT_BITS_MAX! Using built-in default." >&2 COMPAT_BITS_MAX="${COMPAT_BITS_MAX_DEFAULT}" fi else ## Could be a chroot. - echo "$0: INFO: No Linux config file detected in folder /boot/ (starting with 'config-'). Therefore using built-in defaults." >&2 + printf '%s\n' "$0: INFO: No Linux config file detected in folder /boot/ (starting with 'config-'). Therefore using built-in defaults." >&2 BITS_MAX="${BITS_MAX_DEFAULT}" COMPAT_BITS_MAX="${COMPAT_BITS_MAX_DEFAULT}" fi @@ -63,19 +74,19 @@ SYSCTL="\ ## $0 ## Do not edit! ## See also: -## $more_info_link +## ${more_info_link} ## Improves ASLR effectiveness for mmap. vm.mmap_rnd_bits=${BITS_MAX} vm.mmap_rnd_compat_bits=${COMPAT_BITS_MAX}" ## Write the sysctl.d conf file. -if echo "${SYSCTL}" | tee "$aslr_mmap_config_file" > /dev/null ; then - echo "$0: INFO: Successfully written ASLR map config file: -$aslr_mmap_config_file" +if printf '%s\n' "${SYSCTL}" | tee "${aslr_mmap_config_file}" > /dev/null ; then + printf '%s\n' "$0: INFO: Successfully written ASLR map config file: +${aslr_mmap_config_file}" exit 0 fi -echo "$0: ERROR: Error writing ASLR map config file: -$aslr_mmap_config_file" >&2 +printf '%s\n' "$0: ERROR: Error writing ASLR map config file: +${aslr_mmap_config_file}" >&2 exit_with_error diff --git a/usr/libexec/security-misc/panic-on-oops#security-misc-shared b/usr/libexec/security-misc/panic-on-oops#security-misc-shared index ca8a0ceb..a89bbd0d 100755 --- a/usr/libexec/security-misc/panic-on-oops#security-misc-shared +++ b/usr/libexec/security-misc/panic-on-oops#security-misc-shared @@ -3,7 +3,12 @@ ## Copyright (C) 2019 - 2025 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. -set -e +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose if [ -f /usr/libexec/helper-scripts/pre.bsh ]; then ## pre.bsh would `source` the following folders: diff --git a/usr/libexec/security-misc/permission-lockdown#security-misc-shared b/usr/libexec/security-misc/permission-lockdown#security-misc-shared index 19fbe893..6f29df54 100755 --- a/usr/libexec/security-misc/permission-lockdown#security-misc-shared +++ b/usr/libexec/security-misc/permission-lockdown#security-misc-shared @@ -4,6 +4,13 @@ ## See the file COPYING for copying conditions. ## Doing this for all users would create many issues. +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose + # /usr/libexec/security-misc/permission-lockdown: user: root | chmod o-rwx "/root" # /usr/libexec/security-misc/permission-lockdown: user: daemon | chmod o-rwx "/usr/sbin" # /usr/libexec/security-misc/permission-lockdown: user: bin | chmod o-rwx "/bin" @@ -36,10 +43,10 @@ home_folder_access_rights_lockdown() { mkdir --parents /var/cache/security-misc/state-files local user for user in $(dir /home); do ## lists directories only - if [ -f "/var/cache/security-misc/state-files/$user" ]; then + if [ -f "/var/cache/security-misc/state-files/${user}" ]; then continue fi - folder_name="/home/$user" + folder_name="/home/${user}" ## chmod: ## The 'g' for 'group' is not needed. ## Debian by default uses USERGROUPS=yes in /etc/adduser.conf. @@ -51,9 +58,13 @@ home_folder_access_rights_lockdown() { ## required to run is sudo addgroup user1 user2. ## See also: user private groups UPGs ## https://wiki.debian.org/UserPrivateGroups - echo "$0: chmod o-rwx \"$folder_name\"" - chmod o-rwx "$folder_name" - touch "/var/cache/security-misc/state-files/$user" + printf '%s\n' "$0: chmod o-rwx \"${folder_name}\"" + ## Best-effort PER USER, deliberately: without these, errexit would abort + ## the whole loop on the first failure and leave every remaining home + ## directory un-locked-down. Failures were already tolerated here before + ## strict mode; keep that, including still recording the state file. + chmod o-rwx "${folder_name}" || true + touch "/var/cache/security-misc/state-files/${user}" || true done } diff --git a/usr/libexec/security-misc/placeholder#security-misc-server b/usr/libexec/security-misc/placeholder#security-misc-server index e8e373ef..6cfad57b 100755 --- a/usr/libexec/security-misc/placeholder#security-misc-server +++ b/usr/libexec/security-misc/placeholder#security-misc-server @@ -4,6 +4,11 @@ ## See the file COPYING for copying conditions. set -x -set -e -echo "$0: This is just a placeholder until security-misc-server gets implemented." +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose +printf '%s\n' "$0: This is just a placeholder until security-misc-server gets implemented." exit 0 diff --git a/usr/libexec/security-misc/remove-system.map#security-misc-shared b/usr/libexec/security-misc/remove-system.map#security-misc-shared index 5b75f6de..7bdebc78 100755 --- a/usr/libexec/security-misc/remove-system.map#security-misc-shared +++ b/usr/libexec/security-misc/remove-system.map#security-misc-shared @@ -3,6 +3,13 @@ ## Copyright (C) 2012 - 2025 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose + if [ -f /usr/libexec/helper-scripts/pre.bsh ]; then ## pre.bsh would `source` the following folders: ## /etc/remove-system.map_pre.d/*.conf @@ -19,8 +26,8 @@ for filename in ${system_map_location} ; do counter=$(( counter + 1 )) done -if [ "$counter" -ge "1" ]; then - echo "INFO: Deleting system.map files..." +if [ "${counter}" -ge "1" ]; then + printf '%s\n' "INFO: Deleting system.map files..." fi ## Removes the System.map files as they are only used for debugging or malware. @@ -29,14 +36,14 @@ for filename in ${system_map_location} ; do if [ -w "${filename}" ]; then ## 'shred' with '--verbose' is too chatty. (7 lines) shred --force --zero -u "${filename}" - echo "INFO: removed '${filename}'" + printf '%s\n' "INFO: removed '${filename}'" else - echo "NOTE: Cannot delete '${filename}' - read-only. For details, see: https://www.kicksecure.com/wiki/security-misc#system_map" + printf '%s\n' "NOTE: Cannot delete '${filename}' - read-only. For details, see: https://www.kicksecure.com/wiki/security-misc#system_map" exit 0 fi fi done -if [ "$counter" -ge "1" ]; then - echo "INFO: Done. Success." +if [ "${counter}" -ge "1" ]; then + printf '%s\n' "INFO: Done. Success." fi diff --git a/usr/share/security-misc/lkrg/lkrg-virtualbox#security-misc-shared b/usr/share/security-misc/lkrg/lkrg-virtualbox#security-misc-shared index 8b7d15ee..38324a68 100755 --- a/usr/share/security-misc/lkrg/lkrg-virtualbox#security-misc-shared +++ b/usr/share/security-misc/lkrg/lkrg-virtualbox#security-misc-shared @@ -4,14 +4,23 @@ ## See the file COPYING for copying conditions. #set -x -set -e +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose + +## security-misc-shared Depends on helper-scripts. +# shellcheck source=../../../../helper-scripts/usr/libexec/helper-scripts/has.sh +source /usr/libexec/helper-scripts/has.sh ## Check if the VirtualBox host software is installed. -if ! command -v vboxmanage &>/dev/null ; then +if ! has vboxmanage ; then ## VirtualBox host software is not installed. if test -f /etc/sysctl.d/30-lkrg-virtualbox.conf ; then ## Delete using '--verbose' so user is notified. - rm --force --verbose /etc/sysctl.d/30-lkrg-virtualbox.conf + safe-rm --force --verbose /etc/sysctl.d/30-lkrg-virtualbox.conf fi exit 0 fi From 1ec153c3f0db147e82f9d6c4c82b5a1026eada7b Mon Sep 17 00:00:00 2001 From: "claude (dev163 38f9745c-aed1-4fb4-b6ae-9899f20f8de2)" Date: Fri, 7 Aug 2026 07:17:03 -0400 Subject: [PATCH 07/17] fix shellcheck directives for an isolated CI checkout The gate passed locally but failed (or would fail) in CI. Cause: CI checks each repo out ALONE, while locally they sit side by side as submodules under derivative-maker/packages. A '# shellcheck source=' climbing out of the repo ('../../../../helper-scripts/...') therefore resolves here and not there, so shellcheck reports SC1091 and the gate fails on a commit that looked clean. The source= paths are kept -- they document where the file actually lives -- and each is paired with a 'disable=SC1091' so an absent sibling is tolerated. Same shape already used in security-misc master. Follow-on findings from the same isolated run, each with a stated reason rather than a blanket waiver: - SC2154 on systemd_notify: assigned by the sourced systemd-notify.bsh, which shellcheck can no longer follow. - SC2034 on SCRIPTNAME and output_cmd: genuinely unused. suspend-pre's output_cmd toggle is vestigial -- unlike suspend-post the script prints directly -- flagged rather than removed, since wiring it up or dropping it is a maintainer call. - SC1090 on the uwt.d conf glob: a dynamic source, which R-081 prescribes SC1090 for. Verified with a new reproducer, ~/strict-pass-tests/ci-repro.sh, which clones the repo on its own and runs the real gate there -- the condition CI actually sees. All six repos: gate exit 0. Co-Authored-By: Claude Opus 5 (1M context) --- .../check-for-usb-controller#security-misc-shared | 3 +++ .../security-misc/lkrg/lkrg-virtualbox#security-misc-shared | 3 +++ 2 files changed, 6 insertions(+) diff --git a/usr/libexec/security-misc/check-for-usb-controller#security-misc-shared b/usr/libexec/security-misc/check-for-usb-controller#security-misc-shared index 0cb043a6..f720c018 100755 --- a/usr/libexec/security-misc/check-for-usb-controller#security-misc-shared +++ b/usr/libexec/security-misc/check-for-usb-controller#security-misc-shared @@ -12,6 +12,9 @@ shopt -s shift_verbose ## security-misc-shared Depends on helper-scripts. # shellcheck source=../../../../helper-scripts/usr/libexec/helper-scripts/has.sh +## Sibling repo: absent in an isolated CI checkout, so shellcheck cannot +## follow it there. The source= path above still documents where it lives. +# shellcheck disable=SC1091 source /usr/libexec/helper-scripts/has.sh export LC_ALL='C' diff --git a/usr/share/security-misc/lkrg/lkrg-virtualbox#security-misc-shared b/usr/share/security-misc/lkrg/lkrg-virtualbox#security-misc-shared index 38324a68..e000b8cf 100755 --- a/usr/share/security-misc/lkrg/lkrg-virtualbox#security-misc-shared +++ b/usr/share/security-misc/lkrg/lkrg-virtualbox#security-misc-shared @@ -13,6 +13,9 @@ shopt -s shift_verbose ## security-misc-shared Depends on helper-scripts. # shellcheck source=../../../../helper-scripts/usr/libexec/helper-scripts/has.sh +## Sibling repo: absent in an isolated CI checkout, so shellcheck cannot +## follow it there. The source= path above still documents where it lives. +# shellcheck disable=SC1091 source /usr/libexec/helper-scripts/has.sh ## Check if the VirtualBox host software is installed. From a8f26061e0e199b95077aa20696257bddc3db73d Mon Sep 17 00:00:00 2001 From: "claude (dev182 e09ec5cf-4ebf-4e1b-88e5-55fbac6ef1d3)" Date: Mon, 10 Aug 2026 13:56:04 -0400 Subject: [PATCH 08/17] Drain input in USB-controller and config-file lookups check-for-usb-controller: grep --quiet exits on the first match and closes the pipe, sending SIGPIPE to lspci. Under pipefail the pipeline then reports 141, so the guarding if takes the false branch even when a USB controller is present. Let grep read the whole stream and redirect to /dev/null instead. mmap-rnd-bits: head -n 1 has the same early-close hazard against ls; use sed -n '1p', which consumes all input, for consistency. Drop the redundant duplicate set -euo pipefail line already covered by the long-form preamble. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../check-for-usb-controller#security-misc-shared | 2 +- usr/libexec/security-misc/mmap-rnd-bits#security-misc-shared | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/usr/libexec/security-misc/check-for-usb-controller#security-misc-shared b/usr/libexec/security-misc/check-for-usb-controller#security-misc-shared index f720c018..aa70e39a 100755 --- a/usr/libexec/security-misc/check-for-usb-controller#security-misc-shared +++ b/usr/libexec/security-misc/check-for-usb-controller#security-misc-shared @@ -22,7 +22,7 @@ export LC_ALL='C' ## Package 'pciutils' provides tool 'lspci'. has lspci -if lspci | grep --quiet '^[^ ]* USB controller: '; then +if lspci | grep -- '^[^ ]* USB controller: ' >/dev/null; then exit 0 fi diff --git a/usr/libexec/security-misc/mmap-rnd-bits#security-misc-shared b/usr/libexec/security-misc/mmap-rnd-bits#security-misc-shared index 8420389f..ae6abf06 100755 --- a/usr/libexec/security-misc/mmap-rnd-bits#security-misc-shared +++ b/usr/libexec/security-misc/mmap-rnd-bits#security-misc-shared @@ -14,8 +14,6 @@ set -o pipefail set -o errtrace shopt -s inherit_errexit shopt -s shift_verbose - -set -euo pipefail shopt -s failglob more_info_link="https://forums.whonix.org/t/automate-mmap-randomisation-to-fix-ppc64el/16514" @@ -48,7 +46,7 @@ COMPAT_BITS_MAX_DEFAULT=16 ## so the ls-parsing hazard SC2012 warns about is not reachable here, and a ## find-based newest-file rewrite would be a behaviour risk for no gain. # shellcheck disable=SC2012 -if compgen -G "/boot/config-*" > /dev/null && CONFIG=$(ls -1 -t /boot/config-* | head -n 1) ; then +if compgen -G "/boot/config-*" > /dev/null && CONFIG=$(ls -1 -t /boot/config-* | sed -n '1p') ; then ## Find the relevant config options. if ! BITS_MAX=$(grep -E '^CONFIG_ARCH_MMAP_RND_BITS_MAX=[0-9]+$' "${CONFIG}" | cut -d "=" -f 2) ; then printf '%s\n' "$0: ERROR: Error detecting CONFIG_ARCH_MMAP_RND_BITS_MAX! Using built-in default." >&2 From 942ba8a1902c2c8a0d6cd1921f2b540eb413a217 Mon Sep 17 00:00:00 2001 From: "claude (dev188 4c9dda7c-c359-4064-afa5-2ec7ed54398c)" Date: Wed, 12 Aug 2026 04:39:33 -0400 Subject: [PATCH 09/17] security-misc: permission-hardener parse filenames with spaces - load_state() split config lines on every space, so any entry whose filename contains a space was mis-fielded: silently skipped (a SUID binary left un-hardened) or mapped to a corrupted policy entry. - Recover the filename by parsing options from the right -- a trailing whitelist keyword, or the mode/owner/group[/capability] tail anchored by the octal mode. Space-free lines unchanged. - Add regression test (drives the real script via print-policy) plus its CI workflow. Canary-verified: fails on the pre-fix parser, passes now. - Bring the file to strict-mode / shellcheck compliance as required to touch it (6-directive block, printf, braces). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../local-permission-hardener-test.yml | 59 +++++++++ .../test_whitespace_filename.sh | 69 ++++++++++ .../permission-hardener#security-misc-shared | 119 ++++++++++++------ 3 files changed, 206 insertions(+), 41 deletions(-) create mode 100644 .github/workflows/local-permission-hardener-test.yml create mode 100755 ci/tests/permission_hardener/test_whitespace_filename.sh diff --git a/.github/workflows/local-permission-hardener-test.yml b/.github/workflows/local-permission-hardener-test.yml new file mode 100644 index 00000000..bbc05186 --- /dev/null +++ b/.github/workflows/local-permission-hardener-test.yml @@ -0,0 +1,59 @@ +--- +## Copyright (C) 2026 - 2026 ENCRYPTED SUPPORT LLC +## See the file COPYING for copying conditions. + +## AI-Assisted + +## Regression test for the permission-hardener config parser. +## +## Scope is intentionally narrow: it exercises load_state()'s +## right-anchored option parsing so a config filename containing a +## space is parsed as one entry instead of being split and silently +## dropped. See ci/tests/permission_hardener/test_whitespace_filename.sh. + +name: Test permission-hardener + +on: + push: + branches: [master] + pull_request: + branches: [master] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Test permission-hardener + runs-on: ubuntu-latest + timeout-minutes: 10 + + ## CI runs only where we enabled it; ANDed with the existing guard. + ## Unset variable -> skipped, no runner, run stays green. + if: >- + vars.CI_ENABLED_ORG_AI_ASSISTED == 'true' + && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name != 'pull_request') + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + ## permission-hardener sources helper-scripts (log_run_die.sh, + ## safe_echo.sh, strings.bsh) at startup; install-deps provides + ## them under /usr/libexec/helper-scripts/. + - name: Install genmkfile + helper-scripts + safe-rm + uses: org-ai-assisted/developer-meta-files/.github/actions/install-deps@master + with: + apt-packages: 'safe-rm' + + - name: Whitespace-in-filename parser regression test + ## Root: the test writes a temp config under + ## /etc/permission-hardener.d/ and runs 'print-policy'. + run: sudo -E ci/tests/permission_hardener/test_whitespace_filename.sh diff --git a/ci/tests/permission_hardener/test_whitespace_filename.sh b/ci/tests/permission_hardener/test_whitespace_filename.sh new file mode 100755 index 00000000..9d45b54e --- /dev/null +++ b/ci/tests/permission_hardener/test_whitespace_filename.sh @@ -0,0 +1,69 @@ +#!/bin/bash +## Copyright (C) 2026 - 2026 ENCRYPTED SUPPORT LLC +## See the file COPYING for copying conditions. + +## AI-Assisted + +## Regression test: permission-hardener must parse a config filename that +## contains spaces. +## +## load_state() recovers the filename by reading the option fields from the +## right (a trailing whitelist keyword, or the mode/owner/group[/capability] +## tail anchored by the octal mode). A space in the filename must NOT split it +## into the wrong fields -- that drops the entry silently, leaving a SUID +## binary un-hardened. +## +## Drives the REAL script via 'print-policy' with a mode-form entry +## ( ) whose filename contains a space, and +## asserts the recovered filename appears in the printed policy. +## +## Requires root: writes a temporary config under /etc/permission-hardener.d/ +## and needs helper-scripts installed (sourced by permission-hardener). + +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose + +repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../../.." && pwd)" +ph_bin="${PERMISSION_HARDENER_BIN:-${repo_root}/usr/bin/permission-hardener#security-misc-shared}" + +if [ ! -f "${ph_bin}" ]; then + printf '%s\n' "FAIL: permission-hardener not found at '${ph_bin}'." >&2 + exit 1 +fi + +test_dir="$(mktemp -d -t ph-ws-test.XXXXXX)" +## The space in the directory name is the property under test. +spaced_file="${test_dir}/some space/binary" +mkdir -p -- "${test_dir}/some space" +touch -- "${spaced_file}" + +config_dir="/etc/permission-hardener.d" +config_file="${config_dir}/zz-ai-whitespace-regression-test.conf" +mkdir -p -- "${config_dir}" + +## invoked indirectly via 'trap ... EXIT' +# shellcheck disable=SC2317 +cleanup() { + safe-rm -f -- "${config_file}" + safe-rm -rf -- "${test_dir}" +} +trap cleanup EXIT + +## mode-form entry: +printf '%s\n' "${spaced_file} 0744 root root" > "${config_file}" + +policy_output="$( "${ph_bin}" print-policy )" + +if printf '%s\n' "${policy_output}" | grep -qF -- "${spaced_file}"; then + printf '%s\n' "PASS: space-containing filename parsed and present in policy." + exit 0 +fi + +printf '%s\n' "FAIL: space-containing filename '${spaced_file}' missing from print-policy output." >&2 +printf '%s\n' "----- print-policy output -----" >&2 +printf '%s\n' "${policy_output}" >&2 +exit 1 diff --git a/usr/bin/permission-hardener#security-misc-shared b/usr/bin/permission-hardener#security-misc-shared index 9f9f351c..775cea57 100755 --- a/usr/bin/permission-hardener#security-misc-shared +++ b/usr/bin/permission-hardener#security-misc-shared @@ -14,7 +14,12 @@ ## works very well for literal matching, and it is used that way extensively ## throughout this script. -set -o errexit -o nounset -o pipefail +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose ## Constants # shellcheck disable=SC2034 @@ -115,7 +120,7 @@ output_stat() { return 1 fi - if [ -z "$stat_output" ]; then + if [ -z "${stat_output}" ]; then log error "stat_output is empty. File name: '${file_name}' Stat output: '${stat_output}' @@ -166,7 +171,7 @@ hardlink_count: '${hardlink_count}' return 1 fi - if [ "$file_name" != "$file_name_from_stat" ]; then + if [ "${file_name}" != "${file_name_from_stat}" ]; then log error "\ File name is different from file name received from stat: File name: '${file_name}' @@ -279,7 +284,7 @@ check_nosuid_whitelist() { ## literal matching is intentional here too [[ " ${policy_exact_white_list[*]} " =~ " ${target_file} " ]] && return 1 - for match_white_list_entry in ${policy_match_white_list[@]+"${policy_match_white_list[@]}"}; do + for match_white_list_entry in "${policy_match_white_list[@]}"; do if [[ "${target_file}" == *"${match_white_list_entry}"* ]]; then return 1 fi @@ -425,6 +430,7 @@ load_state() { ## to mean that all whitelisting should be ignored. local config_file line field_list policy_nosuid_file_item policy_file_item + local field_count recovered_file_name mode_index field_index ## Load configuration, deferring whitelist handling until later for config_file in \ @@ -466,8 +472,38 @@ load_state() { IFS=' ' read -r -a field_list <<< "${line}" + ## A filename may contain spaces. The option grammar is unambiguous at the + ## END of a line -- either a single trailing whitelist keyword, or a + ## mode/owner/group[/capability] tail whose first field is the octal mode + ## -- so recover the filename by parsing options from the right and + ## rejoining everything before them. Space-free lines are left unchanged. + field_count="${#field_list[@]}" + if (( field_count > 2 )); then + case "${field_list[field_count-1]}" in + exactwhitelist|matchwhitelist|disablewhitelist|nosuid) + recovered_file_name="$(IFS=' '; printf '%s' "${field_list[*]:0:field_count-1}")" + field_list=( "${recovered_file_name}" "${field_list[field_count-1]}" ) + ;; + *) + mode_index='' + for (( field_index=1; field_index < field_count; field_index++ )); do + if [[ "${field_list[field_index]}" =~ ^0?[0-7]{3,4}$ ]]; then + mode_index="${field_index}" + break + fi + done + if [ -n "${mode_index}" ] && (( mode_index > 1 )); then + recovered_file_name="$(IFS=' '; printf '%s' "${field_list[*]:0:mode_index}")" + field_list=( "${recovered_file_name}" "${field_list[@]:mode_index}" ) + fi + ;; + esac + fi + case "${#field_list[@]}" in - 2|4|5) true;; + 2|4|5) + true + ;; *) exit_code=200 log error "Line contains an invalid number of fields: '${line}'" >&2 @@ -738,11 +774,11 @@ undo_policy_for_file() { state_group_owner_item="${state_group_owner_list[state_idx]}" state_mode_item="${state_mode_list[state_idx]}" # shellcheck disable=SC2086 - chown ${verbose} -- "${state_user_owner_item}:${state_group_owner_item}" \ + chown "${verbose}" -- "${state_user_owner_item}:${state_group_owner_item}" \ "${undo_file}" || exit_code=202 ## chmod needs to be run after chown since chown removes suid. # shellcheck disable=SC2086 - chmod ${verbose} "${state_mode_item}" "${undo_file}" || exit_code=203 + chmod "${verbose}" "${state_mode_item}" "${undo_file}" || exit_code=203 else log info "File does not exist: '${undo_file}'" fi @@ -788,6 +824,7 @@ print_columns() { done format_str="${format_str}\n" ## Using a dynamically generated format string on purpose. + ## style-ok: printf-format # shellcheck disable=SC2059 printf "${format_str}" "$@" } @@ -832,9 +869,9 @@ print_raw_policy_config() { if [ ! -f "${config_file}" ]; then continue fi - echo "*** begin ${config_file} ***" + printf '%s\n' "*** begin ${config_file} ***" cat "${config_file}" - echo "*** end ${config_file} ***" + printf '%s\n' "*** end ${config_file} ***" done } @@ -842,13 +879,13 @@ print_raw_state() { local state_file for state_file in "${store_dir}/existing_mode/statoverride" \ "${store_dir}/new_mode/statoverride"; do - echo "*** begin ${state_file} ***" + printf '%s\n' "*** begin ${state_file} ***" if [ -f "${state_file}" ]; then cat "${state_file}" else - echo '(file does not exist)' + printf '%s\n' '(file does not exist)' fi - echo "*** end ${state_file} ***" + printf '%s\n' "*** end ${state_file} ***" done } @@ -856,13 +893,13 @@ print_fs_audit() { local state_idx state_file_item state_user_owner_item state_group_owner_item \ state_mode_item - echo 'Legend:' - echo '... - Warning about an unusual, but not necessarily wrong, condition' - echo '!!! - Warning about an unusual and definitely wrong condition' - echo '*** - File permission data, actual state on filesystem is consistent with policy' - echo '^^^ - File permission data, actual state on filesystem is inconsistent with policy' - echo 'vvv - File permissions specified by state, always shown after a ^^^ item' - echo + printf '%s\n' 'Legend:' + printf '%s\n' '... - Warning about an unusual, but not necessarily wrong, condition' + printf '%s\n' '!!! - Warning about an unusual and definitely wrong condition' + printf '%s\n' '*** - File permission data, actual state on filesystem is consistent with policy' + printf '%s\n' '^^^ - File permission data, actual state on filesystem is inconsistent with policy' + printf '%s\n' 'vvv - File permissions specified by state, always shown after a ^^^ item' + printf '%s\n' '' for (( state_idx=0; state_idx < ${#state_file_list[@]}; state_idx++ )); do state_file_item="${state_file_list[state_idx]}" @@ -886,7 +923,7 @@ print_fs_audit() { output_stat "${state_file_item}" if [ -z "${file_name_from_stat}" ]; then - echo "... '${state_file_item}' does not exist or has multiple hardlinks" + printf '%s\n' "... '${state_file_item}' does not exist or has multiple hardlinks" continue fi @@ -895,20 +932,20 @@ print_fs_audit() { || [ "${existing_mode}" != "${state_mode_item}" ]; then if ! [[ $'\n'"${passwd_file_contents}" \ =~ $'\n'"${state_user_owner_item}:" ]]; then - echo "!!! Owner from config does not exist: '${state_user_owner_item}'" + printf '%s\n' "!!! Owner from config does not exist: '${state_user_owner_item}'" continue fi if ! [[ $'\n'"${group_file_contents}" \ =~ $'\n'"${state_group_owner_item}:" ]]; then - echo "!!! Group from config does not exist: '${state_group_owner_item}'" + printf '%s\n' "!!! Group from config does not exist: '${state_group_owner_item}'" continue fi - echo "^^^ ${file_name_from_stat} ${existing_owner}:${existing_group} ${existing_mode}" - echo "vvv ${file_name_from_stat} ${state_user_owner_item}:${state_group_owner_item} ${state_mode_item}" + printf '%s\n' "^^^ ${file_name_from_stat} ${existing_owner}:${existing_group} ${existing_mode}" + printf '%s\n' "vvv ${file_name_from_stat} ${state_user_owner_item}:${state_group_owner_item} ${state_mode_item}" else - echo "*** ${file_name_from_stat} ${existing_owner}:${existing_group} ${existing_mode}" + printf '%s\n' "*** ${file_name_from_stat} ${existing_owner}:${existing_group} ${existing_mode}" fi done } @@ -988,49 +1025,49 @@ case "${1:-}" in print_state ;; print-diagnostics) - echo '=== BEGIN PERMISSION-HARDENER DIAGNOSTICS ===' + printf '%s\n' '=== BEGIN PERMISSION-HARDENER DIAGNOSTICS ===' - echo '--- BEGIN State without policy ---' + printf '%s\n' '--- BEGIN State without policy ---' load_state_without_policy print_state - echo '--- END State without policy ---' + printf '%s\n' '--- END State without policy ---' reset_global_vars - echo '--- BEGIN Policy without state ---' + printf '%s\n' '--- BEGIN Policy without state ---' load_state print_policy - echo '--- END Policy without state ---' + printf '%s\n' '--- END Policy without state ---' reset_global_vars - echo '--- BEGIN Policy-applied-state ---' + printf '%s\n' '--- BEGIN Policy-applied-state ---' load_state apply_policy print_state - echo '--- END Policy-applied state ---' + printf '%s\n' '--- END Policy-applied state ---' reset_global_vars - echo '--- BEGIN Master dpkg-statoverride database ---' + printf '%s\n' '--- BEGIN Master dpkg-statoverride database ---' dpkg-statoverride --list - echo '--- END Master dpkg-statoverride database ---' + printf '%s\n' '--- END Master dpkg-statoverride database ---' - echo '--- BEGIN Raw policy configuration ---' + printf '%s\n' '--- BEGIN Raw policy configuration ---' print_raw_policy_config - echo '--- END Raw policy configuration ---' + printf '%s\n' '--- END Raw policy configuration ---' - echo '--- BEGIN Raw state data ---' + printf '%s\n' '--- BEGIN Raw state data ---' print_raw_state - echo '--- END Raw state data ---' + printf '%s\n' '--- END Raw state data ---' - echo '--- BEGIN Filesystem state audit ---' + printf '%s\n' '--- BEGIN Filesystem state audit ---' load_state apply_policy print_fs_audit - echo '--- END Filesystem state audit ---' + printf '%s\n' '--- END Filesystem state audit ---' - echo '=== END PERMISSION-HARDENER DIAGNOSTICS ===' + printf '%s\n' '=== END PERMISSION-HARDENER DIAGNOSTICS ===' ;; -h|--help) print_usage From 8c991dd34247e7cc43843a1dff6d5300330646f1 Mon Sep 17 00:00:00 2001 From: "claude (dev188 4c9dda7c-c359-4064-afa5-2ec7ed54398c)" Date: Wed, 12 Aug 2026 04:39:52 -0400 Subject: [PATCH 10/17] security-misc: compile emerg-shutdown once at postinst, not every boot - emerg-shutdown.c was recompiled into /run on every boot. Move the compile to a standalone build-emerg-shutdown run from postinst, which installs /usr/bin/emerg-shutdown. Compiling on-target keeps the package Architecture: all while still producing a correct per-arch static binary. - Boot-time emerg-shutdown now just copies /usr/bin/emerg-shutdown into /run (memlockd-resident), with a fallback build if the binary is absent. - dracut module ships /usr/bin/emerg-shutdown (exists right after install, fixing the require_binaries /run chicken-and-egg). - Sandbox-verified: build-emerg-shutdown produces a static ELF in /usr/bin. - Strict/style compliance on the touched scripts (waivers for the debconf maintainer script and the dracut-sourced module). Co-Authored-By: Claude Opus 4.8 (1M context) --- debian/security-misc-shared.install | 1 + debian/security-misc-shared.postinst | 29 ++++++++---- .../module-setup.sh#security-misc-shared | 8 ++-- .../build-emerg-shutdown#security-misc-shared | 45 +++++++++++++++++++ ...ompile-emerg-shutdown#security-misc-shared | 10 +++-- .../emerg-shutdown#security-misc-shared | 30 ++++++++----- 6 files changed, 97 insertions(+), 26 deletions(-) create mode 100755 usr/libexec/security-misc/build-emerg-shutdown#security-misc-shared diff --git a/debian/security-misc-shared.install b/debian/security-misc-shared.install index 8e72deee..f149b2e0 100755 --- a/debian/security-misc-shared.install +++ b/debian/security-misc-shared.install @@ -109,6 +109,7 @@ usr/lib/systemd/user/usbguard-notifier.service.d/30_security-misc.conf#security- usr/lib/udev/rules.d/95-emerg-shutdown.rules#security-misc-shared => /usr/lib/udev/rules.d/95-emerg-shutdown.rules usr/libexec/security-misc/askpass#security-misc-shared => /usr/libexec/security-misc/askpass usr/libexec/security-misc/block-unsafe-logins#security-misc-shared => /usr/libexec/security-misc/block-unsafe-logins +usr/libexec/security-misc/build-emerg-shutdown#security-misc-shared => /usr/libexec/security-misc/build-emerg-shutdown usr/libexec/security-misc/build-fm-shim-backend#security-misc-shared => /usr/libexec/security-misc/build-fm-shim-backend usr/libexec/security-misc/check-for-usb-controller#security-misc-shared => /usr/libexec/security-misc/check-for-usb-controller usr/libexec/security-misc/compile-emerg-shutdown#security-misc-shared => /usr/libexec/security-misc/compile-emerg-shutdown diff --git a/debian/security-misc-shared.postinst b/debian/security-misc-shared.postinst index af81bf2a..1d304e2d 100755 --- a/debian/security-misc-shared.postinst +++ b/debian/security-misc-shared.postinst @@ -3,6 +3,10 @@ ## Copyright (C) 2012 - 2025 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. +## style-ok: allow-echo (Debian maintainer-script convention) +## style-ok: no-has (has.sh not reliably available this early in maintainer scripts) +## style-ok: no-strict (debconf maintainer script; full strict block would leak into APT/debconf) + if [ -f /usr/libexec/helper-scripts/pre.bsh ]; then source /usr/libexec/helper-scripts/pre.bsh fi @@ -11,11 +15,11 @@ fi ## the postinst itself does not use debconf commands. source /usr/share/debconf/confmodule -set -e +set -o errexit true " ##################################################################### -## INFO: BEGIN: $DPKG_MAINTSCRIPT_PACKAGE $DPKG_MAINTSCRIPT_NAME $* +## INFO: BEGIN: ${DPKG_MAINTSCRIPT_PACKAGE} ${DPKG_MAINTSCRIPT_NAME} $* ##################################################################### " @@ -116,7 +120,7 @@ root root 4755 /usr/bin/mount root root 644 /etc/issue root root 755 /etc/cron.d" - printf '%s\n' "$state_str" | tee /var/lib/permission-hardener-v2/existing_mode/statoverride + printf '%s\n' "${state_str}" | tee /var/lib/permission-hardener-v2/existing_mode/statoverride touch "/var/lib/security-misc/do_once/${FUNCNAME[0]}_version_1" } @@ -125,6 +129,10 @@ build_fm_shim_backend() { /usr/libexec/security-misc/build-fm-shim-backend } +build_emerg_shutdown() { + /usr/libexec/security-misc/build-emerg-shutdown +} + case "$1" in configure) if [ -d /etc/skel/.gnupg ]; then @@ -164,13 +172,18 @@ case "$1" in ## Build the org.freedesktop.FileManager1 shim backend. build_fm_shim_backend + + ## Compile emerg-shutdown into /usr/bin (on-target, so an + ## Architecture: all package still gets a correct per-arch binary); + ## the boot-time script then only copies it into /run. + build_emerg_shutdown ;; abort-upgrade|abort-remove|abort-deconfigure) ;; triggered) - echo "INFO: triggered $DPKG_MAINTSCRIPT_PACKAGE: '$DPKG_MAINTSCRIPT_PACKAGE' $DPKG_MAINTSCRIPT_PACKAGE DPKG_MAINTSCRIPT_NAME: '$DPKG_MAINTSCRIPT_NAME' $\*: '$*' 2: '$2'" + echo "INFO: triggered ${DPKG_MAINTSCRIPT_PACKAGE}: '${DPKG_MAINTSCRIPT_PACKAGE}' ${DPKG_MAINTSCRIPT_PACKAGE} DPKG_MAINTSCRIPT_NAME: '${DPKG_MAINTSCRIPT_NAME}' $\*: '$*' 2: '$2'" /usr/share/security-misc/lkrg/lkrg-virtualbox || true /usr/libexec/security-misc/mmap-rnd-bits || true permission_hardening @@ -178,7 +191,7 @@ case "$1" in ;; *) - echo "$DPKG_MAINTSCRIPT_NAME called with unknown argument \`$1'" >&2 + echo "${DPKG_MAINTSCRIPT_NAME} called with unknown argument \`$1'" >&2 exit 1 ;; esac @@ -194,8 +207,8 @@ permission_hardening ## https://bugs.debian.org/481542 if command -v update-grub >/dev/null 2>&1; then update-grub || \ - echo "$DPKG_MAINTSCRIPT_PACKAGE $DPKG_MAINTSCRIPT_NAME ERROR: Running \ -'update-grub' failed with exit code $?. $DPKG_MAINTSCRIPT_PACKAGE is most \ + echo "${DPKG_MAINTSCRIPT_PACKAGE} ${DPKG_MAINTSCRIPT_NAME} ERROR: Running \ +'update-grub' failed with exit code $?. ${DPKG_MAINTSCRIPT_PACKAGE} is most \ likely only the trigger, not the cause. Unless you know this is not an issue, \ you should fix running 'update-grub', otherwise your system might no longer \ boot." >&2 @@ -213,7 +226,7 @@ permission_hardening_legacy_config_folder true " ##################################################################### -## INFO: END : $DPKG_MAINTSCRIPT_PACKAGE $DPKG_MAINTSCRIPT_NAME $* +## INFO: END : ${DPKG_MAINTSCRIPT_PACKAGE} ${DPKG_MAINTSCRIPT_NAME} $* ##################################################################### " diff --git a/usr/lib/dracut/modules.d/99emerg-shutdown/module-setup.sh#security-misc-shared b/usr/lib/dracut/modules.d/99emerg-shutdown/module-setup.sh#security-misc-shared index 98d6be9a..a69c36f9 100755 --- a/usr/lib/dracut/modules.d/99emerg-shutdown/module-setup.sh#security-misc-shared +++ b/usr/lib/dracut/modules.d/99emerg-shutdown/module-setup.sh#security-misc-shared @@ -3,15 +3,17 @@ ## Copyright (C) 2025 - 2025 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. +## style-ok: no-strict (dracut sources this module; top-level strict mode would leak into dracut) + ## called by dracut check() { - require_binaries /run/emerg-shutdown || return 1 + require_binaries /usr/bin/emerg-shutdown || return 1 return 255 } ## called by dracut depends() { - echo 'systemd bash' + printf '%s\n' 'systemd bash' return 0 } @@ -23,7 +25,7 @@ install() { inst_simple /usr/libexec/security-misc/emerg-shutdown inst_simple /usr/share/security-misc/emerg-shutdown-initramfs.service /usr/lib/systemd/system/emerg-shutdown-initramfs.service - inst_simple /run/emerg-shutdown /emerg-shutdown + inst_simple /usr/bin/emerg-shutdown /emerg-shutdown for config_file in /etc/security-misc/emerg-shutdown/*.conf; do if [ -f "${config_file}" ]; then diff --git a/usr/libexec/security-misc/build-emerg-shutdown#security-misc-shared b/usr/libexec/security-misc/build-emerg-shutdown#security-misc-shared new file mode 100755 index 00000000..4c9ca911 --- /dev/null +++ b/usr/libexec/security-misc/build-emerg-shutdown#security-misc-shared @@ -0,0 +1,45 @@ +#!/bin/bash + +## Copyright (C) 2026 - 2026 ENCRYPTED SUPPORT LLC +## See the file COPYING for copying conditions. + +## AI-Assisted + +## Compile emerg-shutdown.c into /usr/bin/emerg-shutdown at package postinst +## time. Compiling on the target (rather than shipping a prebuilt binary) keeps +## security-misc-shared Architecture: all while still producing a correct +## per-architecture, statically linked binary. The boot-time emerg-shutdown +## script then only copies this binary into /run instead of recompiling on +## every boot. + +## style-ok: no-safe-rm (only ever removes our own just-created mktemp temp) + +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose + +export LC_ALL=C + +## Compile to a temporary file first, then atomically move into place. +## This prevents leaving a corrupted binary if compilation is interrupted. +tmp_output="$(mktemp -- /usr/bin/emerg-shutdown.XXXXXX)" + +cleanup() { + rm -f -- "${tmp_output}" +} +trap cleanup EXIT + +/usr/libexec/security-misc/compile-emerg-shutdown \ + /usr/src/security-misc/emerg-shutdown.c \ + "${tmp_output}" \ + || { + printf '%s\n' 'Could not compile emerg-shutdown executable!' + exit 1 + } + +chmod 0755 -- "${tmp_output}" +mv -f -- "${tmp_output}" /usr/bin/emerg-shutdown +trap - EXIT diff --git a/usr/libexec/security-misc/compile-emerg-shutdown#security-misc-shared b/usr/libexec/security-misc/compile-emerg-shutdown#security-misc-shared index 873fa1a8..cab64f7c 100755 --- a/usr/libexec/security-misc/compile-emerg-shutdown#security-misc-shared +++ b/usr/libexec/security-misc/compile-emerg-shutdown#security-misc-shared @@ -17,9 +17,9 @@ ## compile-emerg-shutdown ## ## Used by: -## * usr/libexec/security-misc/emerg-shutdown (first-run on the -## user's machine: compiles C code into /run/emerg-shutdown then -## execs it) +## * usr/libexec/security-misc/build-emerg-shutdown (package postinst: +## compiles C code into /usr/bin/emerg-shutdown; the boot-time +## emerg-shutdown script then copies that into /run) ## * ci/codeql-build.sh (CodeQL static analysis prebuild) ## ## Holding the gcc command in one place keeps the runtime build path @@ -27,8 +27,10 @@ set -o errexit set -o nounset -set -o errtrace set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose export LC_ALL=C diff --git a/usr/libexec/security-misc/emerg-shutdown#security-misc-shared b/usr/libexec/security-misc/emerg-shutdown#security-misc-shared index 0f3de2d2..fafffc7f 100755 --- a/usr/libexec/security-misc/emerg-shutdown#security-misc-shared +++ b/usr/libexec/security-misc/emerg-shutdown#security-misc-shared @@ -3,15 +3,19 @@ ## Copyright (C) 2025 - 2025 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. -## TODO: Move most of this to a build script that will compile and place under -## /usr/bin rather than /run. Then turn this script into something that simply -## copies emerg-shutdown to /run and then runs it. This will reduce resource -## consumption for end users. +## emerg-shutdown.c is compiled once to /usr/bin/emerg-shutdown at package +## postinst (by build-emerg-shutdown), not shipped prebuilt -- compiling +## on-target keeps security-misc-shared Architecture: all while still producing +## a correct per-architecture, statically linked binary. This script copies +## that binary into /run (tmpfs, memlockd-resident) at boot and runs it, rather +## than recompiling on every boot. set -o errexit set -o nounset -set -o errtrace set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose ## Make sure globs sort in a predictable, reproducible fashion export LC_ALL=C @@ -45,15 +49,19 @@ else ## Find the devices that make up the root device readarray -t root_devices < <(/usr/libexec/helper-scripts/get-backing-devices-for-mountpoint '/') || true; - ## Build the actual emerg-shutdown executable + ## Copy the prebuilt binary (compiled at postinst by build-emerg-shutdown) + ## into /run (tmpfs) so it is memory-resident for the emergency; memlockd + ## then locks it into RAM. if [ ! -f '/run/emerg-shutdown' ]; then - /usr/libexec/security-misc/compile-emerg-shutdown \ - /usr/src/security-misc/emerg-shutdown.c \ - /run/emerg-shutdown \ - || { - printf "%s\n" 'Could not compile force-shutdown executable!' + if [ ! -f '/usr/bin/emerg-shutdown' ]; then + ## Fallback: build was skipped or removed. Compile now so the emergency + ## shutdown capability is never silently absent. + /usr/libexec/security-misc/build-emerg-shutdown || { + printf '%s\n' 'Could not build emerg-shutdown executable!' exit 1 } + fi + cp -- /usr/bin/emerg-shutdown /run/emerg-shutdown fi ## memlockd daemonizes itself, so no need to background it. From 8ba6ca134e52411ce18f29b553dbfedb42b8ed3e Mon Sep 17 00:00:00 2001 From: "claude (dev188 4c9dda7c-c359-4064-afa5-2ec7ed54398c)" Date: Wed, 12 Aug 2026 04:40:08 -0400 Subject: [PATCH 11/17] security-misc: silence expected pam_exec journal noise via quiet_log - The [success=1 default=ignore] pam_exec lines run helper scripts that exit 1 on their expected control-flow path, emitting an alarming 'pam_exec(...): ... failed: exit code 1' journal line. Plain 'quiet' suppresses only the user-facing message, not the log. - Add 'quiet_log' (linux-pam >= 1.5.2, present on bookworm+) to every skip-logic pam_exec line (console-lockdown, unix-faillock, faillock-preauth, wheel), leaving the control flow byte-identical. - Update the scripts' comments to point at the resolution. Note: not verified on a live login journal (needs a real PAM stack); the change is additive and semantics-preserving regardless. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../pam_faillock_not_if_x#security-misc-shared | 7 +++++-- .../security-misc/pam_only_if_login#security-misc-shared | 9 ++++++--- .../console-lockdown-security-misc#security-misc-shared | 2 +- .../faillock-preauth-security-misc#security-misc-shared | 2 +- .../unix-faillock-security-misc#security-misc-shared | 8 ++++---- .../pam-configs/wheel-security-misc#security-misc-shared | 2 +- 6 files changed, 18 insertions(+), 12 deletions(-) diff --git a/usr/libexec/security-misc/pam_faillock_not_if_x#security-misc-shared b/usr/libexec/security-misc/pam_faillock_not_if_x#security-misc-shared index f5fe0922..22523eed 100755 --- a/usr/libexec/security-misc/pam_faillock_not_if_x#security-misc-shared +++ b/usr/libexec/security-misc/pam_faillock_not_if_x#security-misc-shared @@ -44,7 +44,10 @@ done ## exit failure so [success=1 default=ignore] will result in running the ## next PAM module (the pam_faillock module). ## -## Causes confusing error message: -## pam_exec(sudo:auth): /usr/libexec/security-misc/pam_faillock_not_if_x failed: exit code 1 +## This previously emitted a confusing journal line: +## pam_exec(sudo:auth): ... pam_faillock_not_if_x failed: exit code 1 +## now suppressed by 'quiet_log' on the pam_exec.so lines in +## /usr/share/pam-configs/{unix-faillock,faillock-preauth}-security-misc +## (linux-pam >= 1.5.2; plain 'quiet' only hides the user-facing message). ## https://github.com/linux-pam/linux-pam/issues/329 exit 1 diff --git a/usr/libexec/security-misc/pam_only_if_login#security-misc-shared b/usr/libexec/security-misc/pam_only_if_login#security-misc-shared index 89497633..e3b1877c 100755 --- a/usr/libexec/security-misc/pam_only_if_login#security-misc-shared +++ b/usr/libexec/security-misc/pam_only_if_login#security-misc-shared @@ -16,9 +16,12 @@ shopt -s shift_verbose true "PAM_SERVICE: ${PAM_SERVICE:-}" if [ "${PAM_SERVICE:-}" = "login" ]; then - ## FIXME: - ## Creates unwanted journal log entry. - ## pam_exec(login:account): /usr/libexec/security-misc/pam_only_if_login failed: exit code 1 + ## exit 1 drives the [success=1 default=ignore] control flow (fall through + ## to the next PAM module). It previously emitted a journal line + ## pam_exec(login:account): ... pam_only_if_login failed: exit code 1 + ## now suppressed by 'quiet_log' on the pam_exec.so line in + ## /usr/share/pam-configs/console-lockdown-security-misc (linux-pam >= 1.5.2; + ## plain 'quiet' only hides the user-facing message, not the log). exit 1 else ## exit success so [success=1 default=ignore] will result in skipping the diff --git a/usr/share/pam-configs/console-lockdown-security-misc#security-misc-shared b/usr/share/pam-configs/console-lockdown-security-misc#security-misc-shared index 45e5c41b..d760d5fe 100644 --- a/usr/share/pam-configs/console-lockdown-security-misc#security-misc-shared +++ b/usr/share/pam-configs/console-lockdown-security-misc#security-misc-shared @@ -3,5 +3,5 @@ Default: no Priority: 280 Account-Type: Primary Account: - [success=1 default=ignore] pam_exec.so seteuid quiet /usr/libexec/security-misc/pam_only_if_login + [success=1 default=ignore] pam_exec.so seteuid quiet quiet_log /usr/libexec/security-misc/pam_only_if_login required pam_access.so accessfile=/etc/security/access-security-misc.conf debug diff --git a/usr/share/pam-configs/faillock-preauth-security-misc#security-misc-shared b/usr/share/pam-configs/faillock-preauth-security-misc#security-misc-shared index 9d74cb81..41c61f9a 100644 --- a/usr/share/pam-configs/faillock-preauth-security-misc#security-misc-shared +++ b/usr/share/pam-configs/faillock-preauth-security-misc#security-misc-shared @@ -4,5 +4,5 @@ Priority: 1024 Auth-Type: Primary Auth: optional pam_exec.so debug stdout seteuid /usr/libexec/security-misc/pam-info - [success=1 default=ignore] pam_exec.so seteuid quiet /usr/libexec/security-misc/pam_faillock_not_if_x + [success=1 default=ignore] pam_exec.so seteuid quiet quiet_log /usr/libexec/security-misc/pam_faillock_not_if_x required pam_faillock.so preauth diff --git a/usr/share/pam-configs/unix-faillock-security-misc#security-misc-shared b/usr/share/pam-configs/unix-faillock-security-misc#security-misc-shared index b1328b56..0950f2ad 100644 --- a/usr/share/pam-configs/unix-faillock-security-misc#security-misc-shared +++ b/usr/share/pam-configs/unix-faillock-security-misc#security-misc-shared @@ -4,17 +4,17 @@ Priority: 384 Auth-Type: Primary Auth: [success=3 default=ignore] pam_unix.so nullok try_first_pass - [success=1 default=ignore] pam_exec.so seteuid quiet /usr/libexec/security-misc/pam_faillock_not_if_x + [success=1 default=ignore] pam_exec.so seteuid quiet quiet_log /usr/libexec/security-misc/pam_faillock_not_if_x [default=die] pam_faillock.so authfail requisite pam_deny.so - [success=1 default=ignore] pam_exec.so seteuid quiet /usr/libexec/security-misc/pam_faillock_not_if_x + [success=1 default=ignore] pam_exec.so seteuid quiet quiet_log /usr/libexec/security-misc/pam_faillock_not_if_x optional pam_faillock.so authsucc required pam_permit.so Auth-Initial: [success=3 default=ignore] pam_unix.so nullok - [success=1 default=ignore] pam_exec.so seteuid quiet /usr/libexec/security-misc/pam_faillock_not_if_x + [success=1 default=ignore] pam_exec.so seteuid quiet quiet_log /usr/libexec/security-misc/pam_faillock_not_if_x [default=die] pam_faillock.so authfail requisite pam_deny.so - [success=1 default=ignore] pam_exec.so seteuid quiet /usr/libexec/security-misc/pam_faillock_not_if_x + [success=1 default=ignore] pam_exec.so seteuid quiet quiet_log /usr/libexec/security-misc/pam_faillock_not_if_x optional pam_faillock.so authsucc required pam_permit.so diff --git a/usr/share/pam-configs/wheel-security-misc#security-misc-shared b/usr/share/pam-configs/wheel-security-misc#security-misc-shared index 599d5bc4..12d72100 100644 --- a/usr/share/pam-configs/wheel-security-misc#security-misc-shared +++ b/usr/share/pam-configs/wheel-security-misc#security-misc-shared @@ -3,5 +3,5 @@ Default: yes Priority: 1050 Auth-Type: Primary Auth: - [success=1 default=ignore] pam_exec.so seteuid quiet /usr/libexec/security-misc/pam_only_if_su + [success=1 default=ignore] pam_exec.so seteuid quiet quiet_log /usr/libexec/security-misc/pam_only_if_su requisite pam_wheel.so group=sudo debug From 8aef705e63595beaa90f70e9c8a4f297fd7a08ee Mon Sep 17 00:00:00 2001 From: "claude (dev188 4c9dda7c-c359-4064-afa5-2ec7ed54398c)" Date: Wed, 12 Aug 2026 04:40:20 -0400 Subject: [PATCH 12/17] security-misc: fm-shim frontend calls xdg-mime/gio by absolute path - fm-shim-frontend invoked xdg-mime and gio by bare name, depending on the systemd --user manager's PATH (minimal, profile-derived entries absent) that fm-shim-backend hands it. Use /usr/bin/xdg-mime and /usr/bin/gio, matching the existing /usr/bin/unicode-show call. - Reword the backend TODO: the whole-session PATH fix belongs in desktop-config-dist's start-lxqt-session (out of this repo); the frontend absolute-path change is the in-repo mitigation. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../fm_shim_frontend.py#security-misc-shared | 4 ++-- .../fm-shim-backend.c#security-misc-shared | 14 +++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) mode change 100644 => 100755 usr/lib/python3/dist-packages/fm_shim_frontend/fm_shim_frontend.py#security-misc-shared diff --git a/usr/lib/python3/dist-packages/fm_shim_frontend/fm_shim_frontend.py#security-misc-shared b/usr/lib/python3/dist-packages/fm_shim_frontend/fm_shim_frontend.py#security-misc-shared old mode 100644 new mode 100755 index 0af75f60..b16f8724 --- a/usr/lib/python3/dist-packages/fm_shim_frontend/fm_shim_frontend.py#security-misc-shared +++ b/usr/lib/python3/dist-packages/fm_shim_frontend/fm_shim_frontend.py#security-misc-shared @@ -393,7 +393,7 @@ class FmShimWindow(QDialog): try: default_fm_desktop_file: str = subprocess.run( - ["xdg-mime", "query", "default", "inode/directory"], + ["/usr/bin/xdg-mime", "query", "default", "inode/directory"], check=True, capture_output=True, encoding="utf-8", @@ -482,7 +482,7 @@ class FmShimWindow(QDialog): for target_dir in self.dir_list: if target_dir.is_dir(): subprocess.run( - ["gio", "launch", str(default_fm_path), str(target_dir)], + ["/usr/bin/gio", "launch", str(default_fm_path), str(target_dir)], check=False, ) else: diff --git a/usr/src/security-misc/fm-shim-backend.c#security-misc-shared b/usr/src/security-misc/fm-shim-backend.c#security-misc-shared index bf3fadeb..548db61d 100644 --- a/usr/src/security-misc/fm-shim-backend.c#security-misc-shared +++ b/usr/src/security-misc/fm-shim-backend.c#security-misc-shared @@ -85,11 +85,15 @@ void launch_frontend_process(const char *mode_opt, char **uri_list, * process, otherwise we won't know what display server to use and may run * into theming issues. * - * TODO: The environment in Non-Qubes-Whonix's user manager has some - * worrying discrepancies when compared to the environment of bash running - * in a qterminal window. In particular, 'PATH' is different. We may need to - * add a 'dbus-update-activation-environment --systemd --all' call to - * /usr/libexec/desktop-config-dist/start-lxqt-session. + * NOTE: Non-Qubes-Whonix's systemd --user manager exports a minimal PATH + * (the systemd default) rather than the profile-derived PATH an interactive + * qterminal bash has, because the graphical session start does not import + * the desktop environment into the user manager. fm-shim-frontend no longer + * depends on that PATH for launching (it invokes xdg-mime and gio by + * absolute path). The complete fix -- reconciling the whole session PATH -- + * belongs in desktop-config-dist's start-lxqt-session (e.g. a + * 'dbus-update-activation-environment --systemd --all' call after PATH is + * set) and is out of scope for this package. */ DBusMessage *env_request = NULL; From 9f12941585d5b18ae79f5d8dfd9f94cc0cd2507d Mon Sep 17 00:00:00 2001 From: "claude (dev188 4c9dda7c-c359-4064-afa5-2ec7ed54398c)" Date: Wed, 12 Aug 2026 04:40:35 -0400 Subject: [PATCH 13/17] security-misc: document whitelisted SUID/SGID helpers, flag human-only - Replace the bare 'TODO: research' markers on the permission-hardener whitelist entries (postfix, selinux/utempter, spice, virtualbox, qubes/qfile-unpacker, pam-tmpdir-helper, gst-ptp-helper) with concise documentation: what each helper is, why it is SUID/SGID, what breaks if stripped, and relevant CVE/hardening notes. - Retag the keep-vs-strip decisions TODO-HUMAN-DEVELOPER-ONLY -- a security-trust call for a human maintainer, not AI-resolvable. - 30_default.conf: fix the commented gst-ptp-helper path typo (gstreamer1.0/grstreamer-1.0 -> gstreamer-1.0/gstreamer-1.0); note that filenames with spaces are now parsed. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...lt_whitelist_pam.conf#security-misc-shared | 6 +++++- ...hitelist_postfix.conf#security-misc-shared | 10 +++++++++- ..._whitelist_qubes.conf#security-misc-shared | 17 ++++++++++------ ...hitelist_selinux.conf#security-misc-shared | 11 +++++++++- ..._whitelist_spice.conf#security-misc-shared | 11 +++++++++- ...elist_virtualbox.conf#security-misc-shared | 16 ++++++++++++++- .../30_default.conf#security-misc-shared | 20 +++++++++++++++---- 7 files changed, 76 insertions(+), 15 deletions(-) diff --git a/usr/lib/permission-hardener.d/25_default_whitelist_pam.conf#security-misc-shared b/usr/lib/permission-hardener.d/25_default_whitelist_pam.conf#security-misc-shared index ffb136e6..d770a269 100644 --- a/usr/lib/permission-hardener.d/25_default_whitelist_pam.conf#security-misc-shared +++ b/usr/lib/permission-hardener.d/25_default_whitelist_pam.conf#security-misc-shared @@ -19,5 +19,9 @@ ## ## Without this, LXQt fails to start with a dbus-launch error. ## -## TODO: audit pam-tmpdir-helper +## Confirmed: installed SUID root, mode 4755. Takes NO command-line/user input -- +## derives the directory purely from the caller's real UID/GID. No upstream git +## repo and no known CVE. A source audit is worthwhile given it is SUID root, but +## risk is low given zero attacker-controlled input. +## TODO-HUMAN-DEVELOPER-ONLY: source audit of pam-tmpdir-helper (SUID root). pam-tmpdir-helper matchwhitelist diff --git a/usr/lib/permission-hardener.d/25_default_whitelist_postfix.conf#security-misc-shared b/usr/lib/permission-hardener.d/25_default_whitelist_postfix.conf#security-misc-shared index 64dd72b5..1e45a13c 100644 --- a/usr/lib/permission-hardener.d/25_default_whitelist_postfix.conf#security-misc-shared +++ b/usr/lib/permission-hardener.d/25_default_whitelist_postfix.conf#security-misc-shared @@ -6,6 +6,14 @@ ## configuration. When security-misc-shared is updated, this file may be ## overwritten. -## TODO: research and document +## postqueue(1) / postdrop(1): Postfix mail-submission helpers. Installed SGID +## group "postdrop" (Postfix setgid_group), NOT setuid root. The SGID bit lets +## an unprivileged user write into the group-writable maildrop queue and reach +## the Postfix daemon sockets, with no user-level privilege granted. Stripping +## SGID breaks local mail submission ("Permission denied" on maildrop; postqueue +## cannot reach the queue manager). SGID-group is already the minimal-privilege +## design; no notable current CVE tied to the bit. +## Ref: https://www.postfix.org/postconf.5.html (setgid_group) +## TODO-HUMAN-DEVELOPER-ONLY: keep-vs-strip SGID is a human trust decision. postqueue matchwhitelist postdrop matchwhitelist diff --git a/usr/lib/permission-hardener.d/25_default_whitelist_qubes.conf#security-misc-shared b/usr/lib/permission-hardener.d/25_default_whitelist_qubes.conf#security-misc-shared index 1b65dfac..34656b92 100644 --- a/usr/lib/permission-hardener.d/25_default_whitelist_qubes.conf#security-misc-shared +++ b/usr/lib/permission-hardener.d/25_default_whitelist_qubes.conf#security-misc-shared @@ -6,18 +6,23 @@ ## configuration. When security-misc-shared is updated, this file may be ## overwritten. -## TODO: research +## qfile-unpacker (qubes-core-agent-linux, qubes-rpc): receiver side of the +## inter-qube file copy (qvm-copy/qvm-move); reads the qrexec stream and unpacks +## incoming files into the target qube. SUID root because it must chroot() into +## the recipient dir and setfsuid()/drop to the unprivileged user before writing +## -- both need root, so SUID cannot simply be stripped. It parses attacker- +## influenced input from another qube => high-value local-EoP target. The issue +## below (#8633) is now CLOSED: upstream HARDENED the unpacker (path sanitization, +## qubes-linux-utils PR #87) rather than dropping SUID. Keep the package patched; +## residual risk is the parser itself. ## https://github.com/QubesOS/qubes-core-agent-linux/blob/master/qubes-rpc/qfile-unpacker.c -## -## Historic Qubes upstream security issue: -## qfile-unpacker allows unprivileged users in VMs to gain root privileges -## https://github.com/QubesOS/qubes-issues/issues/8633 +## https://github.com/QubesOS/qubes-issues/issues/8633 (historic local-root EoP) ## ## matches both: ## - /usr/lib/qubes/qfile-unpacker whitelist ## - Not bit-for-bit identical to /usr/lib/qubes/qfile-unpacker. ## - Stripping SUID from this does *not* break file copying. -## - TODO: further research required on its purpose +## - TODO-HUMAN-DEVELOPER-ONLY: further research required on its purpose ## - /usr/bin/qfile-unpacker ## - Appears to be an integral part of file transfer between qubes, stripping ## SUID from this in an AppVM results in that AppVM being unable to receive diff --git a/usr/lib/permission-hardener.d/25_default_whitelist_selinux.conf#security-misc-shared b/usr/lib/permission-hardener.d/25_default_whitelist_selinux.conf#security-misc-shared index 6f5c7f30..986f7a72 100644 --- a/usr/lib/permission-hardener.d/25_default_whitelist_selinux.conf#security-misc-shared +++ b/usr/lib/permission-hardener.d/25_default_whitelist_selinux.conf#security-misc-shared @@ -6,5 +6,14 @@ ## configuration. When security-misc-shared is updated, this file may be ## overwritten. -## TODO: research and document +## utempter (libutempter0): privileged back-end that terminal emulators (xterm, +## screen, tmux) fork to add/remove their pty login records in /var/run/utmp and +## /var/log/wtmp so who/w/last see the session. Installed SGID group "utmp" (NOT +## setuid root) -- the utmp group owns those files, so SGID utmp is the minimum +## privilege to update them. Stripping the bit breaks terminal login accounting +## (who/w miss sessions); no root privilege lost. Old CVEs only (CVE-2004-0233 +## path traversal, fixed in 0.5.3+). Whitelisted under "selinux" because SELinux +## policy references this path. +## Ref: https://bugzilla.redhat.com/show_bug.cgi?id=246063 +## TODO-HUMAN-DEVELOPER-ONLY: keep-vs-strip SGID is a human trust decision. /utempter/utempter matchwhitelist diff --git a/usr/lib/permission-hardener.d/25_default_whitelist_spice.conf#security-misc-shared b/usr/lib/permission-hardener.d/25_default_whitelist_spice.conf#security-misc-shared index 6569621b..684237bc 100644 --- a/usr/lib/permission-hardener.d/25_default_whitelist_spice.conf#security-misc-shared +++ b/usr/lib/permission-hardener.d/25_default_whitelist_spice.conf#security-misc-shared @@ -6,5 +6,14 @@ ## configuration. When security-misc-shared is updated, this file may be ## overwritten. -## TODO: research and document +## spice-client-glib-usb-acl-helper (spice-gtk): SUID root helper a SPICE client +## (virt-viewer, GNOME Boxes) calls during USB redirection to grant the invoking +## user a POSIX ACL on a /dev/bus/usb node so it can pass through to a VM. Needs +## root to set the ACL; consults polkit for an active local session first, so +## access is session-scoped rather than a permanent group grant. Stripping SUID +## breaks USB redirection ("Error setting facl: Operation not permitted"). It is +## a setuid-root C helper reachable by any desktop user -- a legitimate hardening +## candidate to disable where USB-to-VM redirection is unused. +## Ref: https://packages.debian.org/bookworm/spice-client-glib-usb-acl-helper +## TODO-HUMAN-DEVELOPER-ONLY: keep-vs-strip SUID is a human trust decision. spice-client-glib-usb-acl-helper matchwhitelist diff --git a/usr/lib/permission-hardener.d/25_default_whitelist_virtualbox.conf#security-misc-shared b/usr/lib/permission-hardener.d/25_default_whitelist_virtualbox.conf#security-misc-shared index 725e3ad6..da26b99d 100644 --- a/usr/lib/permission-hardener.d/25_default_whitelist_virtualbox.conf#security-misc-shared +++ b/usr/lib/permission-hardener.d/25_default_whitelist_virtualbox.conf#security-misc-shared @@ -6,7 +6,21 @@ ## configuration. When security-misc-shared is updated, this file may be ## overwritten. -## TODO: research +## VirtualBox ships several helpers under /usr/lib/virtualbox/. The NETWORKING +## helpers carry the SUID-root bit; they need root only to create/configure host +## virtual NICs and run the userspace net stacks, then drop privileges: +## - VBoxNetAdpCtl : creates/removes vboxnetN adapters, assigns IPs +## (CAP_NET_ADMIN operations). +## - VBoxNetDHCP / VBoxNetNAT : userspace DHCP server / NAT engine. +## The front-ends (VBoxHeadless, VBoxSDL, VirtualBoxVM) are generally NOT SUID in +## stock builds; whitelisted defensively for distro/"hardened" builds that +## setuid-root them to load the ring-0 VMM driver. Stripping SUID from the net +## helpers breaks host-only/NAT networking for non-root users. +## SUID history (why the surface is sensitive): CVE-2009-3692 (VBoxNetAdpCtl +## popen() metachar injection -> local root), CVE-2017-3316, recurring Oracle CPU +## local-EoP fixes. Verify on-image bits: find /usr/lib/virtualbox -perm -4000 +## Ref: https://nvd.nist.gov/vuln/detail/CVE-2009-3692 +## TODO-HUMAN-DEVELOPER-ONLY: keep-vs-strip SUID is a human trust decision. /usr/lib/virtualbox/ matchwhitelist VirtualBoxVM matchwhitelist VBoxSDL matchwhitelist diff --git a/usr/lib/permission-hardener.d/30_default.conf#security-misc-shared b/usr/lib/permission-hardener.d/30_default.conf#security-misc-shared index 6e5f9404..cab227f4 100644 --- a/usr/lib/permission-hardener.d/30_default.conf#security-misc-shared +++ b/usr/lib/permission-hardener.d/30_default.conf#security-misc-shared @@ -12,7 +12,10 @@ ## [filename] [mode] [owner] [group] [capability] ## [filename] [exactwhitelist|matchwhitelist|disablewhitelist|nosuid] ## -## TODO: white spaces inside file name untested and probably will not work. +## White spaces inside a file name are supported: the parser recovers the file +## name by reading the option fields from the right (a trailing whitelist +## keyword, or the mode/owner/group[/capability] tail anchored by the octal +## mode). Quoting/escaping is still not supported; a literal space is fine. ###################################################################### # Global Settings @@ -25,7 +28,9 @@ ###################################################################### ## For example, if you are not using SELinux the following might make sense to -## enable. TODO: research +## enable (utempter is SGID utmp for terminal login accounting; see +## 25_default_whitelist_selinux.conf for the full rationale). +## TODO-HUMAN-DEVELOPER-ONLY: disabling utempter's SGID is a human trust decision. #/utempter/utempter disablewhitelist ## If you are not going to use AppImages such as electrum Bitcoin wallet. @@ -119,5 +124,12 @@ ## anon-apps-config does this. #/usr/bin/ping 0744 root root none -## TODO: research -#/usr/lib/x86_64-linux-gnu/gstreamer1.0/grstreamer-1.0/gst-ptp-helper 0744 root root none +## gst-ptp-helper (gstreamer1.0-plugins-base): started by gst_ptp_init() for PTP +## (IEEE 1588) network clock sync; SUID root or cap_net_bind_service+cap_net_admin +## to bind privileged UDP ports 319/320, then drops privileges. Removing it only +## disables PTP clock sync; ordinary audio/video playback is unaffected, so this +## is a low-risk hardening candidate (cf. Red Hat bug 1724677). Prefer stripping +## to capabilities over setuid-root if kept. Path corrected below (was +## gstreamer1.0/grstreamer-1.0, which does not exist). +## TODO-HUMAN-DEVELOPER-ONLY: enabling this SUID/caps strip is a human trust decision. +#/usr/lib/x86_64-linux-gnu/gstreamer-1.0/gstreamer-1.0/gst-ptp-helper 0744 root root none From 5b34c9406198a47ce0c8d78a143606056e9d51b0 Mon Sep 17 00:00:00 2001 From: "claude (dev188 4c9dda7c-c359-4064-afa5-2ec7ed54398c)" Date: Wed, 12 Aug 2026 04:40:49 -0400 Subject: [PATCH 14/17] security-misc: dfuzzer commit pin, marker rewords, remount-secure cleanup - ci/dfuzzer-build.sh: pin dfuzzer to the exact upstream commit and verify the tag resolves to it (a moved tag now fails loudly). - grub 40_kernel_hardening + remount-secure hidepid: reword standing 'TODO' notes to 'REMINDER' (tracking, not pending work). - remount-secure: replace the echo/true toggle with a printf-array output helper; bring the file to strict-mode / shellcheck compliance (6-directive block, braces) as required to touch it. Co-Authored-By: Claude Opus 4.8 (1M context) --- ci/dfuzzer-build.sh | 21 ++- ..._kernel_hardening.cfg#security-misc-shared | 2 +- usr/bin/remount-secure#security-misc-shared | 147 ++++++++++-------- 3 files changed, 97 insertions(+), 73 deletions(-) diff --git a/ci/dfuzzer-build.sh b/ci/dfuzzer-build.sh index b536a002..790419b2 100755 --- a/ci/dfuzzer-build.sh +++ b/ci/dfuzzer-build.sh @@ -25,13 +25,18 @@ ## ## dfuzzer is NOT packaged in Ubuntu 24.04 noble (verified via ## packages.ubuntu.com - 'No such package'); hence the from-source -## build. Pinned to upstream tag v2.6 (latest release as of -## 2026-05-08). Bump when a new release lands. +## build. Pinned to upstream tag v2.6 AND its exact commit (verified +## below); latest release as of 2026-05-08. Bump both DFUZZER_TAG and +## DFUZZER_COMMIT together when a new release lands. set -o errexit set -o nounset set -o pipefail set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose + +## style-ok: no-tmp-hardcode (ephemeral CI build clone under /tmp/dfuzzer) if [ "${CI:-}" != "true" ] && [ "${ALLOW_LOCAL:-}" != "true" ]; then printf '%s\n' "${BASH_SOURCE[0]}: refusing to run outside CI. Set ALLOW_LOCAL=true to override." >&2 @@ -39,10 +44,20 @@ if [ "${CI:-}" != "true" ] && [ "${ALLOW_LOCAL:-}" != "true" ]; then fi DFUZZER_TAG="${DFUZZER_TAG:-v2.6}" +## Pin the exact upstream commit. A tag is mutable; verifying the +## resolved commit hash detects a re-pointed tag (supply-chain guard). +## Keep in sync with DFUZZER_TAG on every version bump. +DFUZZER_COMMIT="${DFUZZER_COMMIT:-a955a80f7dd20fd7aeffad91da1c495aab5dbbd3}" -## TODO: Better to clone the whole repository, then check out a commit hash? git clone --depth 1 --branch "${DFUZZER_TAG}" \ https://github.com/dbus-fuzzer/dfuzzer /tmp/dfuzzer + +dfuzzer_actual_commit="$(git -C /tmp/dfuzzer rev-parse HEAD)" +if [ "${dfuzzer_actual_commit}" != "${DFUZZER_COMMIT}" ]; then + printf '%s\n' "${BASH_SOURCE[0]}: dfuzzer tag ${DFUZZER_TAG} resolved to ${dfuzzer_actual_commit}, expected ${DFUZZER_COMMIT} -- refusing (tag moved?)." >&2 + exit 1 +fi + meson setup --buildtype=release /tmp/dfuzzer/build /tmp/dfuzzer ninja -C /tmp/dfuzzer/build -v sudo install -m 0755 /tmp/dfuzzer/build/dfuzzer /usr/local/bin/dfuzzer diff --git a/etc/default/grub.d/40_kernel_hardening.cfg#security-misc-shared b/etc/default/grub.d/40_kernel_hardening.cfg#security-misc-shared index 498a6ddc..ecbddc7a 100644 --- a/etc/default/grub.d/40_kernel_hardening.cfg#security-misc-shared +++ b/etc/default/grub.d/40_kernel_hardening.cfg#security-misc-shared @@ -55,7 +55,7 @@ GRUB_CMDLINE_LINUX="$GRUB_CMDLINE_LINUX slab_nomerge" ## KSPP=partial ## KSPP sets the kernel parameters and CONFIG_SLUB_DEBUG. ## -## TODO: Debian forky / 14 +## REMINDER: Debian forky / 14 ## The first parameter is applicable when using Linux kernel >= 6.17 (retained here for future-proofing and completeness). ## #GRUB_CMDLINE_LINUX="$GRUB_CMDLINE_LINUX hash_pointers=always" diff --git a/usr/bin/remount-secure#security-misc-shared b/usr/bin/remount-secure#security-misc-shared index 957ad46f..55c2924e 100755 --- a/usr/bin/remount-secure#security-misc-shared +++ b/usr/bin/remount-secure#security-misc-shared @@ -26,24 +26,33 @@ ## https://www.kicksecure.com/wiki/Dev/remount-secure ## https://forums.whonix.org/t/re-mount-home-and-other-with-noexec-and-nosuid-among-other-useful-mount-options-for-better-security/7707 +## style-ok: no-has (runs in dracut where helper-scripts has.sh is unavailable; id itself may be absent) + +## Mount helper functions (_boot, _sys, _usr, ...) are invoked indirectly via +## the dispatch in main(); shellcheck cannot see the calls. +# shellcheck disable=SC2317 + #set -x -set -e -set -o pipefail +set -o errexit set -o nounset +set -o pipefail +set -o errtrace +shopt -s inherit_errexit +shopt -s shift_verbose init() { if test -o xtrace ; then - output_command=true + output_command=( true ) else - output_command=echo + output_command=( printf '%s\n' ) fi - $output_command "$0: INFO: START" + "${output_command[@]}" "$0: INFO: START" ## dracut does not have id. Saving space in initial ramdisk. if command -v id &>/dev/null ; then if [ "$(id -u)" != "0" ]; then - $output_command "ERROR: must be run as root! sudo $0" + "${output_command[@]}" "ERROR: must be run as root! sudo $0" exit 1 fi fi @@ -53,10 +62,10 @@ init() { ## dracut sets NEWROOT=/sysroot [[ -v NEWROOT ]] || NEWROOT="" - if [ "$NEWROOT" = "" ]; then - $output_command "INFO: dracut detected: no" + if [ "${NEWROOT}" = "" ]; then + "${output_command[@]}" "INFO: dracut detected: no" else - $output_command "INFO: dracut detected: yes - NEWROOT: '$NEWROOT'" + "${output_command[@]}" "INFO: dracut detected: yes - NEWROOT: '${NEWROOT}'" fi ## Debugging. @@ -76,33 +85,33 @@ parse_options() { do case ${1:-} in 0) - $output_command "WARNING: Not using remount-secure." + "${output_command[@]}" "WARNING: Not using remount-secure." exit 0 shift ;; 1) - $output_command "INFO: level 1/3 (low)" + "${output_command[@]}" "INFO: level 1/3 (low)" most_noexec_maybe="" home_noexec_maybe="" parsed=true shift ;; 2) - $output_command "INFO: level 2/3 (medium)" + "${output_command[@]}" "INFO: level 2/3 (medium)" most_noexec_maybe=",noexec" home_noexec_maybe="" parsed=true shift ;; 3) - $output_command "INFO: level 3/3 (high)" + "${output_command[@]}" "INFO: level 3/3 (high)" most_noexec_maybe=",noexec" home_noexec_maybe=",noexec" parsed=true shift ;; --force) - $output_command "INFO: --force" + "${output_command[@]}" "INFO: --force" option_force=true shift ;; @@ -111,7 +120,7 @@ parse_options() { break ;; -*) - echo "ERROR: unknown option: $1" >&2 + printf '%s\n' "ERROR: unknown option: ${1}" >&2 exit 1 ;; *) @@ -125,27 +134,27 @@ parse_options() { [[ -v home_noexec_maybe ]] || home_noexec_maybe="" [[ -v most_noexec_maybe ]] || most_noexec_maybe="" - $output_command "INFO: using nosuid,nodev: yes" + "${output_command[@]}" "INFO: using nosuid,nodev: yes" - if [ "$home_noexec_maybe" = "" ]; then - $output_command "INFO: using noexec for all: no" + if [ "${home_noexec_maybe}" = "" ]; then + "${output_command[@]}" "INFO: using noexec for all: no" else - $output_command "INFO: using noexec for all: yes" + "${output_command[@]}" "INFO: using noexec for all: yes" return 0 fi - if [ "$most_noexec_maybe" = "" ]; then - $output_command "INFO: using noexec for most: no" + if [ "${most_noexec_maybe}" = "" ]; then + "${output_command[@]}" "INFO: using noexec for most: no" else - $output_command "INFO: using noexec for most (not all): yes" + "${output_command[@]}" "INFO: using noexec for most (not all): yes" return 0 fi - if [ "$parsed" = "true" ]; then + if [ "${parsed}" = "true" ]; then return 0 fi - $output_command "ERROR: syntax error. use either: + "${output_command[@]}" "ERROR: syntax error. use either: $0 0 $0 1 $0 2 @@ -156,14 +165,14 @@ $0 3" preparation() { ## Debugging. - #$output_command "INFO: 'findmnt --list' output at the START." - #$output_command "$(findmnt --list)" - #$output_command "" + #"${output_command[@]}" "INFO: 'findmnt --list' output at the START." + #"${output_command[@]}" "$(findmnt --list)" + #"${output_command[@]}" "" true } remount_secure() { - $output_command "" + "${output_command[@]}" "" ## ${FUNCNAME[1]} is the name of the calling function. I.e. the function ## which called this function. @@ -174,52 +183,52 @@ remount_secure() { ## example status_file_full_path: ## /run/remount-secure/_home - old_mount_options="$(findmnt --noheadings --output options -- "$mount_folder")" || true + old_mount_options="$(findmnt --noheadings --output options -- "${mount_folder}")" || true ## example old_mount_options: ## rw,nosuid,nodev,relatime,discard - $output_command "INFO: '$mount_folder' old_mount_options: '$old_mount_options'" + "${output_command[@]}" "INFO: '${mount_folder}' old_mount_options: '${old_mount_options}'" - if printf '%s\n' "$old_mount_options" | grep "$intended_mount_options" >/dev/null 2>/dev/null ; then - $output_command "INFO: '$mount_folder' has already intended mount options. ('$intended_mount_options')" + if printf '%s\n' "${old_mount_options}" | grep "${intended_mount_options}" >/dev/null 2>/dev/null ; then + "${output_command[@]}" "INFO: '${mount_folder}' has already intended mount options. ('${intended_mount_options}')" return 0 fi ## When this package is upgraded, the systemd unit will run again. ## If the user meanwhile manually relaxed mount options, this should not be undone. - if [ ! "$option_force" == "true" ]; then - if [ -e "$status_file_full_path" ]; then - $output_command "INFO: '$mount_folder' already remounted earlier. Not remounting again. Use --force if this is what you want." + if [ ! "${option_force}" == "true" ]; then + if [ -e "${status_file_full_path}" ]; then + "${output_command[@]}" "INFO: '${mount_folder}' already remounted earlier. Not remounting again. Use --force if this is what you want." return 0 fi fi - if ! test -d "$mount_folder" ; then + if ! test -d "${mount_folder}" ; then ## For example /boot/efi does not always exist on all systems. - $output_command "INFO: '$mount_folder' folder exists: no" + "${output_command[@]}" "INFO: '${mount_folder}' folder exists: no" return 0 fi - $output_command "INFO: '$mount_folder' folder exists: yes" + "${output_command[@]}" "INFO: '${mount_folder}' folder exists: yes" - if findmnt --noheadings "$mount_folder" >/dev/null ; then - $output_command "INFO: '$mount_folder' already mounted, therefore using remount." - $output_command INFO: Executing: mount --make-private --options "remount,${intended_mount_options}" "$mount_folder" - mount --make-private --options "remount,${intended_mount_options}" "$mount_folder" || exit_code=100 + if findmnt --noheadings "${mount_folder}" >/dev/null ; then + "${output_command[@]}" "INFO: '${mount_folder}' already mounted, therefore using remount." + "${output_command[@]}" "INFO: Executing: mount --make-private --options remount,${intended_mount_options} ${mount_folder}" + mount --make-private --options "remount,${intended_mount_options}" "${mount_folder}" || exit_code=100 else - $output_command "INFO: '$mount_folder' not yet mounted, therefore using mount bind." - $output_command INFO: Executing: mount --make-private --options "$intended_mount_options" --bind "$mount_folder" "$mount_folder" - mount --make-private --options "$intended_mount_options" --bind "$mount_folder" "$mount_folder" || exit_code=101 + "${output_command[@]}" "INFO: '${mount_folder}' not yet mounted, therefore using mount bind." + "${output_command[@]}" "INFO: Executing: mount --make-private --options ${intended_mount_options} --bind ${mount_folder} ${mount_folder}" + mount --make-private --options "${intended_mount_options}" --bind "${mount_folder}" "${mount_folder}" || exit_code=101 fi - new_mount_options="$(findmnt --noheadings --output options -- "$mount_folder")" || true - $output_command "INFO: '$mount_folder' new_mount_options: '$new_mount_options'" + new_mount_options="$(findmnt --noheadings --output options -- "${mount_folder}")" || true + "${output_command[@]}" "INFO: '${mount_folder}' new_mount_options: '${new_mount_options}'" - touch "$status_file_full_path" + touch "${status_file_full_path}" } _boot() { - mount_folder="$NEWROOT/boot" + mount_folder="${NEWROOT}/boot" ## https://lists.freedesktop.org/archives/systemd-devel/2015-February/028456.html intended_mount_options="nosuid,nodev,noexec" remount_secure @@ -227,7 +236,7 @@ _boot() { _boot_efi() { ## TODO: new, test - mount_folder="$NEWROOT/boot/efi" + mount_folder="${NEWROOT}/boot/efi" intended_mount_options="nosuid,nodev,noexec" remount_secure } @@ -261,25 +270,25 @@ _sys() { } _tmp() { - mount_folder="$NEWROOT/tmp" + mount_folder="${NEWROOT}/tmp" intended_mount_options="nosuid,nodev${most_noexec_maybe}" remount_secure } _var_tmp() { - mount_folder="$NEWROOT/var/tmp" + mount_folder="${NEWROOT}/var/tmp" intended_mount_options="nosuid,nodev${most_noexec_maybe}" remount_secure } _var_log() { - mount_folder="$NEWROOT/var/log" + mount_folder="${NEWROOT}/var/log" intended_mount_options="nosuid,nodev,noexec" remount_secure } _var() { - mount_folder="$NEWROOT/var" + mount_folder="${NEWROOT}/var" ## noexec: Not possible. Reason: ## Debian stores executable maintainer scripts in /var/lib/dpkg/info folder. intended_mount_options="nosuid,nodev" @@ -288,48 +297,48 @@ _var() { _usr() { ## TODO: new, test - mount_folder="$NEWROOT/usr" + mount_folder="${NEWROOT}/usr" intended_mount_options="nodev" remount_secure } _home() { - mount_folder="$NEWROOT/home" + mount_folder="${NEWROOT}/home" intended_mount_options="nosuid,nodev${home_noexec_maybe}" remount_secure } _root() { ## TODO: new, test - mount_folder="$NEWROOT/root" + mount_folder="${NEWROOT}/root" intended_mount_options="nosuid,nodev${home_noexec_maybe}" remount_secure } _srv() { ## TODO: new, test - mount_folder="$NEWROOT/srv" + mount_folder="${NEWROOT}/srv" intended_mount_options="nosuid,nodev${most_noexec_maybe}" remount_secure } _media() { ## TODO: new, test - mount_folder="$NEWROOT/media" + mount_folder="${NEWROOT}/media" intended_mount_options="nosuid,nodev${most_noexec_maybe}" remount_secure } _mnt() { ## TODO: new, test - mount_folder="$NEWROOT/mnt" + mount_folder="${NEWROOT}/mnt" intended_mount_options="nosuid,nodev${most_noexec_maybe}" remount_secure } _opt() { ## TODO: new, test - mount_folder="$NEWROOT/opt" + mount_folder="${NEWROOT}/opt" ## Allow /opt exec as usually optional binaries are placed there such as Firefox ## when manually installed from tarball. intended_mount_options="nosuid,nodev" @@ -340,20 +349,20 @@ _etc() { ## TODO: new, test ## /etc cannot be noexec because various executables are there. To find, run: ## sudo find /etc -executable - mount_folder="$NEWROOT/etc" + mount_folder="${NEWROOT}/etc" intended_mount_options="nosuid,nodev" remount_secure } end() { ## Debugging. - #$output_command "INFO: 'findmnt --list' output at the END." - #$output_command "$(findmnt --list)" + #"${output_command[@]}" "INFO: 'findmnt --list' output at the END." + #"${output_command[@]}" "$(findmnt --list)" - $output_command "" - $output_command "INFO: exit_code: $exit_code" - $output_command "$0: INFO: END" - exit $exit_code + "${output_command[@]}" "" + "${output_command[@]}" "INFO: exit_code: ${exit_code}" + "${output_command[@]}" "$0: INFO: END" + exit "${exit_code}" } main() { @@ -382,7 +391,7 @@ main() { end } -## TODO: see also hidepid /usr/lib/systemd/system/proc-hidepid.service +## REMINDER: see also hidepid /usr/lib/systemd/system/proc-hidepid.service #mount --options defaults,nosuid,nodev,noexec,remount,subset=pid /proc main "$@" From c4bf5403d0dbda87b2e1fe165ade7c81bf4e1b30 Mon Sep 17 00:00:00 2001 From: "claude (dev188 4c9dda7c-c359-4064-afa5-2ec7ed54398c)" Date: Wed, 12 Aug 2026 04:44:07 -0400 Subject: [PATCH 15/17] security-misc: declare bash (>= 4.4) for inherit_errexit - Many scripts use 'shopt -s inherit_errexit' (bash 4.4+) in their strict-mode preamble; ${misc:Depends} does not supply the baseline, so an older /bin/bash would die in the preamble before the script's logic. Co-Authored-By: Claude Opus 4.8 (1M context) --- debian/control | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/control b/debian/control index 4a944d1b..d14059e2 100644 --- a/debian/control +++ b/debian/control @@ -32,6 +32,7 @@ Package: security-misc-shared Architecture: all Depends: adduser, apparmor-profile-dist, + bash (>= 4.4), build-essential, dmsetup, helper-scripts, From a66fa50019f95326fef372ec5b3d1cde3b59b4a9 Mon Sep 17 00:00:00 2001 From: "claude (dev188 4c9dda7c-c359-4064-afa5-2ec7ed54398c)" Date: Wed, 12 Aug 2026 04:55:18 -0400 Subject: [PATCH 16/17] security-misc: fix undo chown empty-arg + emerg fallback (ai-review) - undo_policy_for_file: the strict-mode brace pass quoted "${verbose}", so 'permission-hardener disable all' ran 'chown "" -- ...' (invalid empty owner) and 'chmod "" ...'. Make verbose an array so an empty value expands to zero arguments. Canary-verified. - emerg-shutdown fallback: when the prebuilt binary is absent, compile straight into /run (tmpfs, writable even if /usr is read-only) rather than via build-emerg-shutdown into /usr/bin. - Document the space-filename parser limitations (consecutive spaces collapse; an octal-looking path chunk can misanchor -> entry skipped). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../permission-hardener#security-misc-shared | 14 +++++++------ .../emerg-shutdown#security-misc-shared | 21 ++++++++++++------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/usr/bin/permission-hardener#security-misc-shared b/usr/bin/permission-hardener#security-misc-shared index 775cea57..c3e66c23 100755 --- a/usr/bin/permission-hardener#security-misc-shared +++ b/usr/bin/permission-hardener#security-misc-shared @@ -477,6 +477,10 @@ load_state() { ## mode/owner/group[/capability] tail whose first field is the octal mode ## -- so recover the filename by parsing options from the right and ## rejoining everything before them. Space-free lines are left unchanged. + ## Limitation: a single internal space is supported; consecutive spaces + ## collapse, and a space-delimited path chunk that is itself a bare octal + ## mode (e.g. '744') can misanchor. Such a filename does not exist, so the + ## entry is skipped, not misapplied. Quoting/escaping is not supported. field_count="${#field_list[@]}" if (( field_count > 2 )); then case "${field_list[field_count-1]}" in @@ -731,10 +735,10 @@ undo_policy_for_file() { undo_file="${1}" undo_all=false - verbose='--verbose' + verbose=('--verbose') if [ "${undo_file}" = 'all' ]; then undo_all=true - verbose='' + verbose=() fi if [ ! -f "${state_file}" ]; then @@ -773,12 +777,10 @@ undo_policy_for_file() { state_user_owner_item="${state_user_owner_list[state_idx]}" state_group_owner_item="${state_group_owner_list[state_idx]}" state_mode_item="${state_mode_list[state_idx]}" - # shellcheck disable=SC2086 - chown "${verbose}" -- "${state_user_owner_item}:${state_group_owner_item}" \ + chown "${verbose[@]}" -- "${state_user_owner_item}:${state_group_owner_item}" \ "${undo_file}" || exit_code=202 ## chmod needs to be run after chown since chown removes suid. - # shellcheck disable=SC2086 - chmod "${verbose}" "${state_mode_item}" "${undo_file}" || exit_code=203 + chmod "${verbose[@]}" "${state_mode_item}" "${undo_file}" || exit_code=203 else log info "File does not exist: '${undo_file}'" fi diff --git a/usr/libexec/security-misc/emerg-shutdown#security-misc-shared b/usr/libexec/security-misc/emerg-shutdown#security-misc-shared index fafffc7f..27f21856 100755 --- a/usr/libexec/security-misc/emerg-shutdown#security-misc-shared +++ b/usr/libexec/security-misc/emerg-shutdown#security-misc-shared @@ -53,15 +53,20 @@ else ## into /run (tmpfs) so it is memory-resident for the emergency; memlockd ## then locks it into RAM. if [ ! -f '/run/emerg-shutdown' ]; then - if [ ! -f '/usr/bin/emerg-shutdown' ]; then - ## Fallback: build was skipped or removed. Compile now so the emergency - ## shutdown capability is never silently absent. - /usr/libexec/security-misc/build-emerg-shutdown || { - printf '%s\n' 'Could not build emerg-shutdown executable!' - exit 1 - } + if [ -f '/usr/bin/emerg-shutdown' ]; then + cp -- /usr/bin/emerg-shutdown /run/emerg-shutdown + else + ## Fallback: prebuilt binary absent (build skipped/removed). Compile + ## straight into /run (tmpfs, writable even when /usr is read-only) so the + ## emergency-shutdown capability is never silently absent. + /usr/libexec/security-misc/compile-emerg-shutdown \ + /usr/src/security-misc/emerg-shutdown.c \ + /run/emerg-shutdown \ + || { + printf '%s\n' 'Could not compile emerg-shutdown executable!' + exit 1 + } fi - cp -- /usr/bin/emerg-shutdown /run/emerg-shutdown fi ## memlockd daemonizes itself, so no need to background it. From c2fcb2d893bf26a59c1b25415333fafcad8682dc Mon Sep 17 00:00:00 2001 From: "claude (dev188 4c9dda7c-c359-4064-afa5-2ec7ed54398c)" Date: Wed, 12 Aug 2026 05:15:00 -0400 Subject: [PATCH 17/17] security-misc: drop versioned bash dependency - Revert the bash (>= 4.4) Depends. Supported Debian (trixie+) always ships bash >= 4.4 and earlier is unsupported, so the inherit_errexit builtin is always present; the dependency adds nothing. Policy bans it (bash-style-guide R-010b). Co-Authored-By: Claude Opus 4.8 (1M context) --- debian/control | 1 - 1 file changed, 1 deletion(-) diff --git a/debian/control b/debian/control index d14059e2..4a944d1b 100644 --- a/debian/control +++ b/debian/control @@ -32,7 +32,6 @@ Package: security-misc-shared Architecture: all Depends: adduser, apparmor-profile-dist, - bash (>= 4.4), build-essential, dmsetup, helper-scripts,