Skip to content

Commit 86ecb2c

Browse files
nsis: install as --plain, so this plugin stops rewriting the system PATH
A plugin drops a library into a host's plugin folder; it has no reason to be on the system PATH. It passed --cli only because --cli was everything that was not --gui, and --cli is the mode that writes PATH. That write was unguarded: NSIS ReadRegStr returns EMPTY and sets its error flag when the value exceeds NSIS_MAX_STRLEN (1024), which a real Windows PATH often does, so the append wrote just ";$INSTDIR" over the whole system PATH. Confirmed on Windows: a 1500-char value reads back as 0 chars, error flag set. That is nib#2. Re-vendors release-lib.sh from stoatworks-backend main, which adds --plain and guards the --cli write. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 4bd67a4 commit 86ecb2c

2 files changed

Lines changed: 120 additions & 10 deletions

File tree

.github/workflows/release.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,9 @@ jobs:
168168
rm -rf "$stage"; mkdir -p "$stage"
169169
unzip -q "$zip" -d "$stage"
170170
cp README.md "$stage/" 2>/dev/null || true
171-
# --cli: a plugin has no executable to make a shortcut to.
172-
rl_nsis windows-x86_64 "$stage" --cli
171+
# --plain: a plugin needs neither a shortcut nor a PATH entry. It was --cli
172+
# until nib#2, where the unguarded PATH write wiped a system PATH.
173+
rl_nsis windows-x86_64 "$stage" --plain
173174
ls -l installers
174175
175176
- uses: actions/upload-artifact@v7

scripts/release-lib.sh

Lines changed: 117 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -483,9 +483,14 @@ rl_sign_windows() { # rl_sign_windows <stagedir-or-file> [...]
483483

484484
# ------------------------------------------------------------------- NSIS ---
485485
#
486-
# Two shapes of Windows installer:
487-
# --cli installs into Program Files and appends to the system PATH
488-
# --gui <exe> the above plus Start Menu and Desktop shortcuts
486+
# Three shapes of Windows installer:
487+
# --plain installs into Program Files; touches nothing else
488+
# --cli the above plus an append to the system PATH
489+
# --gui <exe> --plain plus Start Menu and Desktop shortcuts
490+
#
491+
# Pick --cli only for something the user types at a prompt. A plugin loaded by a
492+
# host application is --plain: putting it on PATH gains nothing and, before the
493+
# guard below existed, cost one reporter their entire system PATH (nib#2).
489494
#
490495
# Both write an uninstaller and the Add/Remove Programs registry keys. The file
491496
# list is generated from the staging directory so callers never hand-maintain
@@ -595,7 +600,7 @@ PS1
595600
printf '%s' "$work/Uninstall.exe"
596601
}
597602

598-
rl_nsis() { # rl_nsis <label> <stagedir> --cli | --gui <exe>
603+
rl_nsis() { # rl_nsis <label> <stagedir> --plain | --cli | --gui <exe>
599604
# RL_EULA (optional, from rl_eula) adds a licence page. Required when the NDI
600605
# runtime is bundled — that is the condition Vizrt's redistribution grant
601606
# rests on, so it is not cosmetic.
@@ -650,6 +655,11 @@ rl_nsis() { # rl_nsis <label> <stagedir> --cli | --gui <exe>
650655
fi
651656

652657
local shortcuts="" unshortcuts="" pathblock="" unpathblock=""
658+
# ☠️ This used to be `if --gui ... else <write the system PATH>`, so EVERY mode
659+
# that was not --gui got the PATH block — including the ~33 Resolume plugins,
660+
# which drop a .dll into a plugin folder and have no business on PATH at all.
661+
# That is how nib#2 (a wiped system PATH) reached a user. Modes are explicit
662+
# now, and an unknown one is a hard error rather than "probably CLI".
653663
if [[ "$mode" == "--gui" ]]; then
654664
shortcuts=$(cat <<SC
655665
CreateDirectory "\$SMPROGRAMS\\${RL_NAME}"
@@ -663,11 +673,27 @@ SC
663673
Delete "\$DESKTOP\\${RL_NAME}.lnk"
664674
SC
665675
)
666-
else
676+
elif [[ "$mode" == "--plain" ]]; then
677+
# Installs files, an uninstaller and the Add/Remove Programs keys, and
678+
# touches nothing else. This is what a plugin wants.
679+
:
680+
elif [[ "$mode" == "--cli" ]]; then
667681
# CLI: put the install dir on the machine PATH via EnvVarUpdate-lite.
668682
pathblock=$(cat <<'SC'
669-
; Append to the system PATH (idempotent: only if not already present)
683+
; Append to the system PATH (idempotent: only if not already present).
684+
;
685+
; ☠️ ReadRegStr returns "" AND sets the error flag when the value is longer
686+
; than NSIS_MAX_STRLEN -- 1024 in the stock makensis build, and a real
687+
; Windows system PATH very often exceeds it. Writing "$0;$INSTDIR" on that
688+
; empty $0 does not append, it REPLACES the whole system PATH with this one
689+
; directory: System32 included, so `ping` and everything else stops
690+
; resolving. That is nib#2. A build machine's PATH is short, which is why it
691+
; survived testing. Never write unless the read demonstrably succeeded and
692+
; came back non-empty.
693+
ClearErrors
670694
ReadRegStr $0 HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Environment" "Path"
695+
IfErrors pathdone
696+
StrCmp $0 "" pathdone
671697
Push $0
672698
Push "$INSTDIR"
673699
Call StrStr
@@ -678,6 +704,9 @@ SC
678704
pathdone:
679705
SC
680706
)
707+
else
708+
echo "rl_nsis: unknown mode '${mode}' (expected --cli, --gui <exe>, or --plain)" >&2
709+
return 1
681710
fi
682711

683712
# The uninstall section is assembled separately because the signed-uninstaller
@@ -878,12 +907,58 @@ rl_notary_args() {
878907
# Helpers/chrome_crashpad_handler in there, and codesign validates
879908
# subcomponents, so signing the version directory fails with "code object is
880909
# not signed at all" until that binary is signed first.
910+
# The JIT exceptions, for a loose binary that needs them. Written once per run,
911+
# next to the other scratch files.
912+
rl_jit_entitlements() {
913+
local f="${RL_OUT_DIR:-${TMPDIR:-/tmp}}/rl-jit-entitlements.plist"
914+
[[ -f "$f" ]] && { printf '%s' "$f"; return 0; }
915+
mkdir -p "$(dirname "$f")"
916+
cat >"$f" <<'PLIST'
917+
<?xml version="1.0" encoding="UTF-8"?>
918+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
919+
<plist version="1.0">
920+
<dict>
921+
<key>com.apple.security.cs.allow-jit</key><true/>
922+
<key>com.apple.security.cs.allow-unsigned-executable-memory</key><true/>
923+
<key>com.apple.security.cs.disable-library-validation</key><true/>
924+
</dict>
925+
</plist>
926+
PLIST
927+
printf '%s' "$f"
928+
}
929+
930+
# Does this loose binary carry a JIT of its own? V8's symbol strings say so for
931+
# a bundled Node runtime, which is the case that matters here.
932+
rl_is_jit_binary() { # rl_is_jit_binary <file>
933+
# LC_ALL=C: a 100 MB runtime is full of bytes that are not valid UTF-8, and
934+
# grep in a UTF-8 locale abandons the whole file rather than the line.
935+
LC_ALL=C grep -qa 'v8::internal::' "$1" 2>/dev/null
936+
}
937+
881938
rl_mac_sign_loose() { # rl_mac_sign_loose <root> [<path-prefix-to-skip>]
882-
local root="$1" skip="${2:-}" f
939+
local root="$1" skip="${2:-}" f ent
883940
while IFS= read -r f; do
884941
[[ -n "$skip" && "$f" == $skip* ]] && continue
885942
rl_grep 'Mach-O' "$(file -b "$f" 2>/dev/null || true)" || continue
886-
codesign --force --options runtime --timestamp \
943+
# Entitlements do NOT flow from the enclosing bundle to a binary signed on
944+
# its own, and they are read per *process*: a launcher that spawns
945+
# Contents/Resources/node gets node's entitlements for that process, not
946+
# the app's. So an embedded runtime signed bare aborts on its first JIT
947+
# allocation while the app around it verifies and notarises perfectly —
948+
# BlackMatrix shipped three releases that way, dying with SIGTRAP before it
949+
# could print a line. A JIT-carrying loose binary is signed with the JIT
950+
# exceptions: the run's own entitlements when it has some, or the standard
951+
# set when it does not.
952+
ent=()
953+
if rl_is_jit_binary "$f"; then
954+
if [[ -n "${RL_MAC_ENTITLEMENTS:-}" && -f "${RL_MAC_ENTITLEMENTS:-}" ]]; then
955+
ent=(--entitlements "$RL_MAC_ENTITLEMENTS")
956+
else
957+
ent=(--entitlements "$(rl_jit_entitlements)")
958+
fi
959+
rl_note "JIT runtime, entitled: ${f#$root/}"
960+
fi
961+
codesign --force --options runtime --timestamp ${ent[@]+"${ent[@]}"} \
887962
--sign "$RL_MAC_SIGN_IDENTITY" "$f" 2>"$RL_CS_ERR" >/dev/null \
888963
|| { echo "codesign failed on nested $f: $(cat "$RL_CS_ERR")" >&2; return 1; }
889964
done < <(find "$root" \( -type d \( -name '*.app' -o -name '*.framework' \) \) -prune \
@@ -984,6 +1059,40 @@ rl_mac_sign() { # rl_mac_sign <path-to-.app-or-binary>
9841059
# executable on its own makes codesign validate the whole enclosing bundle
9851060
# early — which fails on a PyInstaller app, where Contents/Frameworks holds
9861061
# base_library.zip and codesign treats that as unsigned nested code.
1062+
#
1063+
# ...with one exception: a SIDECAR. Tauri's `externalBin` puts helper
1064+
# binaries in Contents/MacOS alongside the main executable, and the notary
1065+
# service rejects those — the bundle seal covers them for integrity, but
1066+
# each is its own Mach-O and needs its own Developer ID signature, hardened
1067+
# runtime and secure timestamp. Burrow v0.1.0 was refused with exactly
1068+
# that, three times over, for Contents/MacOS/burrow-helper:
1069+
#
1070+
# The binary is not signed with a valid Developer ID certificate.
1071+
# The signature does not include a secure timestamp.
1072+
# The executable does not have the hardened runtime enabled.
1073+
#
1074+
# Burrow is the fleet's first app with a sidecar, which is why this has
1075+
# never come up before. The main executable is still left alone, so the
1076+
# PyInstaller reasoning above is untouched.
1077+
#
1078+
# The comparison is exact rather than a prefix: rl_mac_sign_loose's skip
1079+
# argument is a prefix match, and "$app/Contents/MacOS/burrow" is a prefix
1080+
# of "burrow-helper" — reusing it here would skip the very binary this
1081+
# exists to sign.
1082+
local mainexe sidecar
1083+
mainexe="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' \
1084+
"$app/Contents/Info.plist" 2>/dev/null || true)"
1085+
if [[ -n "$mainexe" ]]; then
1086+
while IFS= read -r sidecar; do
1087+
[[ "$sidecar" == "$app/Contents/MacOS/$mainexe" ]] && continue
1088+
rl_grep 'Mach-O' "$(file -b "$sidecar" 2>/dev/null || true)" || continue
1089+
rl_note "sidecar: ${sidecar#$app/}"
1090+
codesign --force --options runtime --timestamp \
1091+
--sign "$RL_MAC_SIGN_IDENTITY" "$sidecar" \
1092+
2>"$RL_CS_ERR" >/dev/null \
1093+
|| { echo "codesign failed on sidecar $sidecar: $(cat "$RL_CS_ERR")" >&2; return 1; }
1094+
done < <(find "$app/Contents/MacOS" -maxdepth 1 -type f 2>/dev/null | sort)
1095+
fi
9871096
fi
9881097
# ${a[@]+"${a[@]}"}: bash 3.2 — still /bin/bash on macOS, and what launchd
9891098
# runs — treats "${a[@]}" on an EMPTY array as an unbound variable under

0 commit comments

Comments
 (0)