Re-check Bolt less often while there is no Thunderbolt controller (#14931)

Both Thunderbolt loops reconciled or rescanned every 2 s even on machines with
no controller: on an idle M2 Max that was about 1,590 process launches a
minute. While nothing is connected or boot-protected and no controller is
present, the daemon now reconciles every 10 s, inside a snapshot's 20 s
lifetime, and returns to every tick as soon as a controller, a policy change
or a boot recovery checkpoint appears. The watcher skips an unchanged, fresh,
device-free snapshot, and both loops wait without forking sleep.
This commit is contained in:
Marcelo Alcantara authored and GitHub committed 2026-10-10 18:03:22 -04:00
1 parent 77bfaca408
commit 6fee7105af
4 files changed
+150 -8

No files matched your search

+1 -1
View File
@@ -14,7 +14,7 @@ The Bolt startup drop-in sets the persisted mode before hardware enumeration whe
Secure-capable devices require both a stored Bolt record and a saved key for automatic trust. An explicit permanent approval pins the current sysfs directory and key descriptor, seeds an empty kernel key before authorization, and verifies Bolt imported it before saving consent. Automatic reconciliation cannot initialize a missing key and retains its read-only sysfs sandbox. First activation is deferred when a captured secure-capable device lacks a saved Bolt key: the `.pending` marker starts a setup notification, while existing Bolt configuration and services remain untouched. Installation can finish; only explicit setup after safe disconnection clears the pending state. Initial identity capture uses Bolt's numeric device/vendor fallback when descriptive sysfs attributes are absent.
The runtime is Bash, with `busctl --json=short` for Bolt's structured D-Bus replies and `jq` for JSON. The root daemon publishes a root-owned snapshot in `/run/omarchy-thunderbolt-authorization`. The user service polls that snapshot and refuses unavailable, stale, or mismatched generations. It does not infer final policy from presence signals. Requests use opaque tokens and bind the policy-service generation, Bolt bus owner, device path, connection timestamp, sysfs inode, and device identity. The inode distinguishes same-second reconnects. Device text is displayed as untrusted data. Failed delivery retries; lost approval responses retain intent and offer another notification. Watcher restarts preserve unfinished intent only while the exact connection and policy-service incarnation remain unchanged. A changed device or daemon requires a new request.
The runtime is Bash, with `busctl --json=short` for Bolt's structured D-Bus replies and `jq` for JSON. The root daemon publishes a root-owned snapshot in `/run/omarchy-thunderbolt-authorization`. The user service polls that snapshot and refuses unavailable, stale, or mismatched generations. Both loops tick every 2 s without forking. While Linux sees no Thunderbolt controller and nothing is connected or boot-protected, the daemon reconciles Bolt every 10 s instead of every tick, still well inside a snapshot's 20 s lifetime; a controller appearing, a policy change or a boot recovery checkpoint restores the 2 s cadence at once. The user service skips an unchanged, fresh snapshot with no devices, warnings or requests in flight. It does not infer final policy from presence signals. Requests use opaque tokens and bind the policy-service generation, Bolt bus owner, device path, connection timestamp, sysfs inode, and device identity. The inode distinguishes same-second reconnects. Device text is displayed as untrusted data. Failed delivery retries; lost approval responses retain intent and offer another notification. Watcher restarts preserve unfinished intent only while the exact connection and policy-service incarnation remain unchanged. A changed device or daemon requires a new request.
The terminal dialog saves an approval intent; the background watcher submits it through `pkexec` to the fixed installed `/usr/bin/omarchy-thunderbolt-authorization-approve` helper. Polkit grants active local wheel users this individual approval action. The helper validates its arguments and the live connection, then independently verifies authorization and any saved trust before returning success. Root entrypoints use protected Bash startup and fixed installed libraries; they discard caller-supplied D-Bus addresses. A shared lock serializes setup, reconciliation, and approvals. General setup uses a separate administrator command invoked with `sudo` from the terminal. Blanket Bolt management requires administrator authentication while protection is enabled. These controls protect against unsolicited accessories, not malicious software already running as an administrator. Identifiers can be forged; Bolt's secure connection keys provide additional verification on supported hardware.
+39 -2
View File
@@ -13,11 +13,29 @@ TB_DEVICE=org.freedesktop.bolt1.Device
TB_DOMAIN=org.freedesktop.bolt1.Domain
TB_PATH=/org/freedesktop/bolt
TB_SYSFS=/sys/bus/thunderbolt/devices
# How often the daemon re-checks Bolt while there is no controller and no boot
# protection: well inside the 20 s a published snapshot stays valid.
TB_IDLE_RECHECK=10
TB_TRUST_JQ='def trusted($state): $state.trusted[.Uid] == {Uid,Name,Vendor} and
((.Secure | not) or (.Stored and (.Key | IN("have","new"))));'
tb_fail() { printf '%s\n' "$*" >&2; return 1; }
# Sleeps without forking on each tick: a pipe open for reading and writing
# never reaches EOF, so a timed read on it returns only on timeout or a signal.
tb_wait() {
[[ -n ${TB_TICK:-} ]] || exec {TB_TICK}<> <(:)
read -r -t "$1" -u "$TB_TICK" _ || true
}
tb_controller_present() {
local domain
for domain in "$TB_SYSFS"/domain*; do
[[ -e $domain ]] && return 0
done
return 1
}
tb_json_write() {
local target=$1 mode=${2:-600} temporary
temporary=$(mktemp "${target%/*}/.thunderbolt.XXXXXX") || return 1
@@ -287,11 +305,24 @@ tb_reconcile() {
}
tb_daemon() {
local snapshot error
local snapshot error policy idle_policy='' idle_since=0
install -d -m 755 "$TB_RUNTIME"
cat /proc/sys/kernel/random/uuid > "$TB_RUNTIME/generation"
chmod 644 "$TB_RUNTIME/generation"
while [[ -f $TB_MARKER ]]; do
policy=''
[[ -r $TB_STATE ]] && IFS= read -r -d '' policy < "$TB_STATE" || true
# With no controller, no device and no boot protection there is nothing to
# restore or reapply, so Bolt is reconciled every TB_IDLE_RECHECK seconds
# instead of every tick. A controller appearing, a policy change or a boot
# recovery checkpoint brings back the full check at once.
if [[ -n $idle_policy && $policy == "$idle_policy" && ! -e ${TB_STATE%/*}/boot-recovery.json ]] &&
(( EPOCHSECONDS - idle_since >= 0 && EPOCHSECONDS - idle_since < TB_IDLE_RECHECK )) &&
! tb_controller_present; then
tb_wait 2
continue
fi
idle_policy=''
(
flock -x 9
if snapshot=$(tb_reconcile 2> "$TB_RUNTIME/error"); then
@@ -303,7 +334,13 @@ tb_daemon() {
tb_json_write "$TB_RUNTIME/snapshot.json" 644
fi
) 9> "$TB_LOCK"
sleep 2
snapshot=''
[[ -r $TB_RUNTIME/snapshot.json ]] && IFS= read -r -d '' snapshot < "$TB_RUNTIME/snapshot.json" || true
if [[ -n $policy ]] && jq -e '.available == true and .domains == [] and .devices == [] and
.warnings == [] and .error == "" and .boot_protection == false' <<< "$snapshot" >/dev/null 2>&1; then
idle_policy=$policy idle_since=$EPOCHSECONDS
fi
tb_wait 2
done
}
+20 -3
View File
@@ -139,15 +139,28 @@ tb_scan() {
}
tb_watch() {
local watch snapshot warning warned='' failures=0
local watch snapshot warning warned='' failures=0 text generation clean='' clean_generation='' clean_time=0
tb_requests
watch=$(cat /proc/sys/kernel/random/uuid)
while true; do
if [[ -f $TB_PENDING && ! -f $TB_MARKER ]]; then
if [[ $warned != "pending" ]] && tb_setup_notice; then warned=pending; fi
sleep 2
clean=''
tb_wait 2
continue
fi
# The last scan's snapshot, unchanged, still fresh and from the same daemon,
# with no device, warning or request in flight, has nothing to deliver.
text='' generation=''
[[ -r $TB_RUNTIME/snapshot.json ]] && IFS= read -r -d '' text < "$TB_RUNTIME/snapshot.json" || true
[[ -r $TB_RUNTIME/generation ]] && IFS= read -r generation < "$TB_RUNTIME/generation" || true
if [[ -n $clean && ${text%$'\n'} == "$clean" && $generation == "$clean_generation" ]] &&
(( EPOCHSECONDS - clean_time >= 0 && EPOCHSECONDS - clean_time < 20 )) &&
! compgen -G "$TB_REQUESTS/request-*.json" >/dev/null; then
tb_wait 2
continue
fi
clean=''
if snapshot=$(tb_scan "$watch"); then
failures=0
warning=$(jq -c '[.generation,.warnings,.error]' <<< "$snapshot")
@@ -157,6 +170,10 @@ tb_watch() {
fi
else
warned=''
if jq -e '.devices == []' <<< "$snapshot" >/dev/null; then
IFS=' ' read -r clean_generation clean_time <<< "$(jq -r '"\(.generation) \(.time)"' <<< "$snapshot")"
[[ $clean_time =~ ^[0-9]+$ ]] && clean=$snapshot
fi
fi
else
failures=$((failures + 1))
@@ -164,7 +181,7 @@ tb_watch() {
warned=unavailable
fi
fi
sleep 2
tb_wait 2
done
}
@@ -317,7 +317,7 @@ test_deferred_opt_out_retains_initial_enrollment() {
test_pending_notice_retries_without_claiming_protection() {
touch "$TB_PENDING"
local count=0 notices=0
sleep() { count=$((count + 1)); (( count < 3 )) || exit 99; }
tb_wait() { count=$((count + 1)); (( count < 3 )) || exit 99; }
tb_setup_notice() { notices=$((notices + 1)); (( notices > 1 )) && echo pending >> "$T/notifications"; }
reject tb_watch
[[ $(cat "$T/notifications") == "pending" ]]
@@ -567,7 +567,7 @@ test_trusted_reconnect_no_alert() {
}
test_unavailable_warns_once() {
tb_scan() { return 1; }
sleep() { count=$((count+1)); ((count<4)) || exit 99; }
tb_wait() { count=$((count+1)); ((count<4)) || exit 99; }
local count=0
reject tb_watch
[[ $(cat "$T/notifications") == "attention" ]]
@@ -629,6 +629,94 @@ test_snapshot_freshness_and_disabled_state() {
reject tb_public_snapshot
echo generation-b > "$TB_RUNTIME/generation"; reject tb_public_snapshot
}
test_wait_times_out_without_input() {
local start=$EPOCHSECONDS
TB_TICK='' tb_wait 1
(( EPOCHSECONDS - start >= 1 && EPOCHSECONDS - start < 10 ))
}
test_daemon_rechecks_controller_free_bolt_less_often() {
touch "$TB_MARKER"; TB_IDLE_RECHECK=600
local tick=0 extra='{}'
expect() { [[ $(wc -l < "$T/reconciles") == "$1" ]] || fail "tick $tick: expected $1 reconciles" "$(wc -l < "$T/reconciles")"; }
tb_reconcile() {
echo >> "$T/reconciles"
jq -cn --argjson time "$EPOCHSECONDS" --argjson extra "$extra" \
'{available:true,time:$time,generation:"g",domains:[],devices:[],warnings:[],error:"",boot_protection:false} + $extra'
}
tb_wait() {
tick=$((tick + 1))
case $tick in
1) expect 1 ;;
2) expect 1; mkdir "$TB_SYSFS/domain0" ;; # idle: no reconcile
3) expect 2; rmdir "$TB_SYSFS/domain0" ;; # a controller appeared
4) expect 2; edit_state '.trusted.x={}' ;;
5) expect 3; touch "${TB_STATE%/*}/boot-recovery.json" ;; # the policy changed
6) expect 4; rm "${TB_STATE%/*}/boot-recovery.json" # a recovery checkpoint
extra='{"devices":[{"Uid":"device-1"}]}'; edit_state '.trusted.y={}' ;;
7) expect 5 ;; # devices from here on
8) expect 6; rm "$TB_MARKER" ;; # so every tick reconciles
esac
}
tb_daemon
[[ $tick == 8 ]]
}
test_daemon_rechecks_after_the_idle_interval() {
touch "$TB_MARKER"; TB_IDLE_RECHECK=2
local tick=0
tb_reconcile() {
echo >> "$T/reconciles"
jq -cn --argjson time "$EPOCHSECONDS" '{available:true,time:$time,generation:"g",domains:[],devices:[],warnings:[],error:"",boot_protection:false}'
}
tb_wait() {
tick=$((tick + 1))
case $tick in
1) sleep 2.1 ;;
2) [[ $(wc -l < "$T/reconciles") == 2 ]] || fail "the idle check did not expire"; rm "$TB_MARKER" ;;
esac
}
tb_daemon
}
test_daemon_failed_reconcile_keeps_every_tick() {
touch "$TB_MARKER"; TB_IDLE_RECHECK=600
local tick=0
tb_reconcile() { echo >> "$T/reconciles"; tb_fail 'Bolt unavailable'; }
tb_wait() { tick=$((tick + 1)); (( tick < 3 )) || rm "$TB_MARKER"; }
tb_daemon 2>/dev/null
[[ $(wc -l < "$T/reconciles") == 3 ]]
check '.available==false' "$TB_RUNTIME/snapshot.json"
}
test_watch_skips_only_unchanged_clean_snapshots() {
local tick=0 start=$EPOCHSECONDS
publish() { jq -cnS --argjson time "$1" --arg generation generation-a --argjson devices "${2:-[]}" \
'{available:true,time:$time,generation:$generation,devices:$devices,warnings:[],error:""}' > "$TB_RUNTIME/snapshot.json"; }
expect() { [[ $(wc -l < "$T/scans") == "$1" ]] || fail "tick $tick: expected $1 scans" "$(wc -l < "$T/scans")"; }
tb_scan() { echo >> "$T/scans"; cat "$TB_RUNTIME/snapshot.json"; }
publish "$start"
tb_wait() {
tick=$((tick + 1))
case $tick in
1) expect 1 ;;
2) expect 1; touch "$TB_REQUESTS/request-x.json" ;; # unchanged: skipped
3) expect 2; rm "$TB_REQUESTS/request-x.json" ;; # a request in flight
4) expect 2; publish "$((start - 5))" ;;
5) expect 3; echo generation-b > "$TB_RUNTIME/generation" ;; # a new snapshot
6) expect 4; echo generation-a > "$TB_RUNTIME/generation" # another daemon
publish "$start" '[{"identity":{}}]' ;;
7) expect 5 ;; # devices from here on
8) expect 6; exit 0 ;; # so every tick scans
esac
}
(tb_watch)
}
test_watch_rescans_a_stale_snapshot() {
local tick=0
jq -cnS --argjson time "$((EPOCHSECONDS - 30))" \
'{available:true,time:$time,generation:"generation-a",devices:[],warnings:[],error:""}' > "$TB_RUNTIME/snapshot.json"
tb_scan() { echo >> "$T/scans"; cat "$TB_RUNTIME/snapshot.json"; }
tb_wait() { tick=$((tick + 1)); (( tick < 3 )) || exit 0; }
(tb_watch)
[[ $(wc -l < "$T/scans") == 3 ]]
}
test_invalid_state_not_skipped_by_migration() {
echo '{"enabled":false}' > "$TB_STATE"; reject tb_admin migrate
[[ ! -s $T/units ]]