Studio: remove the Windows VBS launcher to clear the Kaspersky false positive (#6326)
* Studio: drop the VBS launcher to clear the Kaspersky false positive
The Windows shortcut launched Unsloth Studio through wscript.exe ->
launch-studio.vbs, and that VBS used CreateObject("WScript.Shell").Run to
start a hidden -ExecutionPolicy Bypass PowerShell. That wscript + .vbs +
bypass-powershell shape is the canonical trigger for generic VBS-dropper
heuristics (Kaspersky HEUR:Trojan.VBS.Agent.gen). The launcher is benign;
only its shape is the problem.
- install.ps1: stop generating launch-studio.vbs and point the Desktop /
Start Menu .lnk straight at powershell.exe -WindowStyle Hidden running
launch-studio.ps1. The shortcut is saved WindowStyle 7 (minimized) so the
brief console flash is muted. launch-studio.ps1 (health poll, port,
mutex, browser) is byte-for-byte unchanged.
- install.ps1: delete a pre-existing launch-studio.vbs on upgrade, so the
flagged file does not linger on machines that already installed it.
- install.ps1 / install.sh: run the heavier ie4uinit -ClearIconCache plus
StartMenuExperienceHost tile-cache rebuild only on a first install or a
real icon change, instead of on every no-op reinstall. That repeated
clear-cache plus kill cluster is itself a dropper-like behavioral pattern.
- tests: forbid VBS generation and require the legacy-VBS cleanup.
Linux, macOS and WSL install paths are unchanged. WSL already targets
wsl.exe from its .lnk and never used a VBS; its only change is the same
icon-cache gating.
* Studio: add launcher-chain smoke coverage to the Windows UI CI
The shortcut launch path was previously untested: studio-windows-ui-smoke
installed then booted `unsloth studio` directly, so a broken .lnk could ship
silently. After install the job now seeds a legacy launch-studio.vbs, asserts
the upgrade removed it, asserts the .lnk targets hidden powershell.exe (never
wscript.exe), and launches via the shortcut's stored command, waiting for
/api/health to report healthy.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
375350e0b6
commit
d50a2e2d07
4 changed files with 241 additions and 78 deletions
74
.github/workflows/studio-windows-ui-smoke.yml
vendored
74
.github/workflows/studio-windows-ui-smoke.yml
vendored
|
|
@ -136,6 +136,17 @@ jobs:
|
|||
}
|
||||
}
|
||||
|
||||
- name: Seed a legacy launch-studio.vbs (upgrade-cleanup check)
|
||||
# Simulate a pre-hardening install so the post-install assertion below
|
||||
# proves the installer DELETES an existing launch-studio.vbs (the exact
|
||||
# Kaspersky-flagged file), not merely stops generating it.
|
||||
shell: pwsh
|
||||
run: |
|
||||
$appDir = Join-Path $env:LOCALAPPDATA 'Unsloth Studio'
|
||||
New-Item -ItemType Directory -Force -Path $appDir | Out-Null
|
||||
Set-Content -LiteralPath (Join-Path $appDir 'launch-studio.vbs') -Value 'WScript.Echo "legacy"' -Encoding Unicode
|
||||
Write-Host "seeded legacy launch-studio.vbs at $appDir"
|
||||
|
||||
- name: Install Studio (--local, --no-torch)
|
||||
# install.ps1 is the supported Windows installer. install.sh
|
||||
# has no Windows branch (apt-get / brew calls). The PS1
|
||||
|
|
@ -192,6 +203,69 @@ jobs:
|
|||
echo "install.ps1 installed the Windows prebuilt llama.cpp:"
|
||||
cat "$INFO"
|
||||
|
||||
- name: Assert Studio launcher chain (no VBS, hidden PowerShell shortcut)
|
||||
# The shortcut launch path is otherwise untested here (the steps below
|
||||
# boot `unsloth studio` directly). Guard against re-introducing the VBS
|
||||
# that tripped Kaspersky HEUR:Trojan.VBS.Agent.gen and against the .lnk
|
||||
# pointing anywhere other than hidden PowerShell over launch-studio.ps1.
|
||||
shell: pwsh
|
||||
run: |
|
||||
$appDir = Join-Path $env:LOCALAPPDATA 'Unsloth Studio'
|
||||
if (Test-Path -LiteralPath (Join-Path $appDir 'launch-studio.vbs')) {
|
||||
throw "regression: launch-studio.vbs exists (the Kaspersky VBS-FP shape)"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $appDir 'launch-studio.ps1'))) {
|
||||
throw "missing launch-studio.ps1 in $appDir"
|
||||
}
|
||||
$lnk = Join-Path ([Environment]::GetFolderPath('Desktop')) 'Unsloth Studio.lnk'
|
||||
if (-not (Test-Path -LiteralPath $lnk)) {
|
||||
$lnk = Join-Path $env:APPDATA 'Microsoft\Windows\Start Menu\Programs\Unsloth Studio.lnk'
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $lnk)) { throw "no Unsloth Studio.lnk on Desktop or Start Menu" }
|
||||
$sc = (New-Object -ComObject WScript.Shell).CreateShortcut($lnk)
|
||||
Write-Host "shortcut target: $($sc.TargetPath)"
|
||||
Write-Host "shortcut args: $($sc.Arguments)"
|
||||
if ($sc.TargetPath -match 'wscript\.exe$') { throw "shortcut still targets wscript.exe (VBS host)" }
|
||||
if ($sc.TargetPath -notmatch 'powershell\.exe$') { throw "unexpected shortcut target: $($sc.TargetPath)" }
|
||||
if ($sc.Arguments -notmatch '-WindowStyle Hidden') {
|
||||
throw "shortcut must launch windowless (-WindowStyle Hidden)"
|
||||
}
|
||||
Write-Host "launcher chain OK (no VBS; hidden powershell over launch-studio.ps1)"
|
||||
|
||||
- name: Launch Studio via the shortcut and assert health
|
||||
# Run the exact command the .lnk stores (hidden PowerShell over
|
||||
# launch-studio.ps1) and confirm it brings the backend up. This is the
|
||||
# only step that proves the shortcut launch is not silently broken.
|
||||
# Default port range is 8888-8908; the later UI tests use 18896/18897, so
|
||||
# there is no conflict, and we tear this server down before they boot.
|
||||
shell: pwsh
|
||||
run: |
|
||||
$lnk = Join-Path ([Environment]::GetFolderPath('Desktop')) 'Unsloth Studio.lnk'
|
||||
if (-not (Test-Path -LiteralPath $lnk)) {
|
||||
$lnk = Join-Path $env:APPDATA 'Microsoft\Windows\Start Menu\Programs\Unsloth Studio.lnk'
|
||||
}
|
||||
$sc = (New-Object -ComObject WScript.Shell).CreateShortcut($lnk)
|
||||
Write-Host "launching: $($sc.TargetPath) $($sc.Arguments)"
|
||||
Start-Process -FilePath $sc.TargetPath -ArgumentList $sc.Arguments -WorkingDirectory $sc.WorkingDirectory
|
||||
$foundPort = 0
|
||||
foreach ($i in 1..180) {
|
||||
foreach ($port in 8888..8908) {
|
||||
try {
|
||||
$r = Invoke-RestMethod -Uri "http://127.0.0.1:$port/api/health" -TimeoutSec 1
|
||||
if ($r.status -eq 'healthy' -and $r.service -eq 'Unsloth UI Backend') { $foundPort = $port; break }
|
||||
} catch {}
|
||||
}
|
||||
if ($foundPort) { break }
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
# Tear down the shortcut-launched server before the main UI tests boot.
|
||||
try {
|
||||
$owner = (Get-NetTCPConnection -LocalPort $foundPort -State Listen -ErrorAction Stop | Select-Object -First 1).OwningProcess
|
||||
if ($owner) { taskkill /PID $owner /T /F 2>$null | Out-Null }
|
||||
} catch {}
|
||||
if (-not $foundPort) { throw "Studio did not become healthy when launched via the shortcut" }
|
||||
Write-Host "Studio healthy on port $foundPort (launched via the shortcut)"
|
||||
|
||||
- name: Add Studio shim to GITHUB_PATH
|
||||
# install.ps1 puts unsloth.exe at $StudioHome\bin\unsloth.exe
|
||||
# and adds that dir to the User PATH via the Windows registry.
|
||||
|
|
|
|||
126
install.ps1
126
install.ps1
|
|
@ -537,7 +537,6 @@ function Install-UnslothStudio {
|
|||
}
|
||||
$appDir = $StudioDataDir
|
||||
$launcherPs1 = Join-Path $appDir "launch-studio.ps1"
|
||||
$launcherVbs = Join-Path $appDir "launch-studio.vbs"
|
||||
$desktopDir = [Environment]::GetFolderPath("Desktop")
|
||||
$desktopLink = if ($desktopDir -and $desktopDir.Trim()) {
|
||||
Join-Path $desktopDir "Unsloth Studio.lnk"
|
||||
|
|
@ -830,19 +829,30 @@ exit 0
|
|||
# even when install.ps1 is executed from PowerShell 7.
|
||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllText($launcherPs1, $launcherContent, $utf8Bom)
|
||||
# shell.Run(cmd, 0, ...) already hides the window, so -WindowStyle Hidden
|
||||
# is redundant; omitting it trims an AV-heuristic token (Kaspersky FP).
|
||||
$vbsContent = @"
|
||||
Set shell = CreateObject("WScript.Shell")
|
||||
cmd = "powershell -NoProfile -ExecutionPolicy Bypass -File ""$launcherPs1"""
|
||||
shell.Run cmd, 0, False
|
||||
"@
|
||||
# WSH handles UTF-16LE reliably for .vbs files with non-ASCII paths.
|
||||
Set-Content -LiteralPath $launcherVbs -Value $vbsContent -Encoding Unicode -Force
|
||||
# No .vbs launcher is written. A WScript.Shell .vbs that spawns a hidden
|
||||
# ExecutionPolicy-Bypass PowerShell is exactly the shape VBS-dropper
|
||||
# heuristics score (e.g. Kaspersky HEUR:Trojan.VBS.Agent.gen). The .lnk
|
||||
# shortcuts instead point straight at powershell.exe running
|
||||
# launch-studio.ps1 with a hidden window (selected below).
|
||||
|
||||
# Delete any launch-studio.vbs left by a pre-hardening install. New
|
||||
# installs no longer generate it, but an upgrade that merely stopped
|
||||
# generating it would leave the exact file AV flags on disk, so remove
|
||||
# it explicitly. Covers default and env-mode installs (same $appDir).
|
||||
$legacyLauncherVbs = Join-Path $appDir "launch-studio.vbs"
|
||||
if (Test-Path -LiteralPath $legacyLauncherVbs) {
|
||||
Remove-Item -LiteralPath $legacyLauncherVbs -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Prefer bundled icon from local clone/dev installs.
|
||||
# If not available, best-effort download from raw GitHub.
|
||||
# We only attach the icon if the resulting file has a valid ICO header.
|
||||
# Snapshot the existing icon first so we can tell whether it actually
|
||||
# changed and gate the heavier icon-cache refresh on a real change.
|
||||
$preIconHash = $null
|
||||
if (Test-Path -LiteralPath $iconPath) {
|
||||
try { $preIconHash = (Get-FileHash -LiteralPath $iconPath -Algorithm SHA256).Hash } catch {}
|
||||
}
|
||||
$hasValidIcon = $false
|
||||
if ($bundledIcon -and (Test-Path -LiteralPath $bundledIcon)) {
|
||||
try {
|
||||
|
|
@ -878,6 +888,24 @@ shell.Run cmd, 0, False
|
|||
}
|
||||
}
|
||||
|
||||
# Did the icon content actually change vs the previous install?
|
||||
# Only a real change (or a first/removed icon) should trigger the heavy
|
||||
# refresh; a no-op reinstall with no icon at all must not.
|
||||
$iconChanged = $false
|
||||
if ($hasValidIcon) {
|
||||
if (-not $preIconHash) {
|
||||
$iconChanged = $true
|
||||
} else {
|
||||
try {
|
||||
$postIconHash = (Get-FileHash -LiteralPath $iconPath -Algorithm SHA256).Hash
|
||||
$iconChanged = ($postIconHash -ne $preIconHash)
|
||||
} catch { $iconChanged = $true }
|
||||
}
|
||||
} elseif ($preIconHash) {
|
||||
# A previously present icon was removed or invalidated.
|
||||
$iconChanged = $true
|
||||
}
|
||||
|
||||
# Env-mode: skip persistent Desktop / Start Menu .lnk shortcuts
|
||||
# that may point at a deleted workspace; launcher + icon stay.
|
||||
if ($StudioRedirectMode -eq 'env') {
|
||||
|
|
@ -885,8 +913,22 @@ shell.Run cmd, 0, False
|
|||
return
|
||||
}
|
||||
|
||||
$wscriptExe = Join-Path $env:SystemRoot "System32\wscript.exe"
|
||||
$shortcutArgs = "//B //Nologo `"$launcherVbs`""
|
||||
# Whether this is effectively a first install (no pre-existing .lnk).
|
||||
# Used to gate the heavier icon-cache refresh below so a no-op reinstall
|
||||
# does not repeatedly clear caches / restart StartMenuExperienceHost --
|
||||
# a behavioral cluster AV heuristics can score as dropper-like.
|
||||
$firstInstall = -not (
|
||||
($desktopLink -and (Test-Path -LiteralPath $desktopLink)) -or
|
||||
($startMenuLink -and (Test-Path -LiteralPath $startMenuLink))
|
||||
)
|
||||
|
||||
# Launch transport for the shortcuts: powershell.exe runs
|
||||
# launch-studio.ps1 with a hidden window. We deliberately avoid a
|
||||
# .vbs/WScript.Shell wrapper -- that script-engine shape is what AV
|
||||
# VBS-dropper heuristics score (Kaspersky HEUR:Trojan.VBS.Agent.gen).
|
||||
$powershellForLnk = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe"
|
||||
$shortcutTarget = $powershellForLnk
|
||||
$shortcutArgs = "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$launcherPs1`""
|
||||
|
||||
try {
|
||||
$wshell = New-Object -ComObject WScript.Shell
|
||||
|
|
@ -896,9 +938,11 @@ shell.Run cmd, 0, False
|
|||
if (-not $linkPath -or [string]::IsNullOrWhiteSpace($linkPath)) { continue }
|
||||
try {
|
||||
$shortcut = $wshell.CreateShortcut($linkPath)
|
||||
$shortcut.TargetPath = $wscriptExe
|
||||
$shortcut.TargetPath = $shortcutTarget
|
||||
$shortcut.Arguments = $shortcutArgs
|
||||
$shortcut.WorkingDirectory = $appDir
|
||||
# Start minimized so the brief PowerShell console flash is muted.
|
||||
$shortcut.WindowStyle = 7
|
||||
$shortcut.Description = "Launch Unsloth Studio"
|
||||
if ($hasValidIcon) {
|
||||
$shortcut.IconLocation = "$iconPath,0"
|
||||
|
|
@ -912,15 +956,13 @@ shell.Run cmd, 0, False
|
|||
}
|
||||
if ($createdShortcutCount -gt 0) {
|
||||
substep "Created Unsloth Studio shortcut"
|
||||
# Force Explorer to re-read each new shortcut's icon so it renders
|
||||
# immediately instead of a stale/generic entry (a same-name .lnk
|
||||
# recreated across reinstalls keeps Explorer's cached per-item icon).
|
||||
# The reliable, non-disruptive fix (no explorer restart) is a per-item
|
||||
# SHChangeNotify SHCNE_UPDATEITEM + SHCNF_PATHW per .lnk; the global
|
||||
# SHCNE_ASSOCCHANGED broadcast alone does NOT recover a stale item.
|
||||
# Also clear the on-disk icon cache (covers heavier staleness).
|
||||
try { & "$env:SystemRoot\System32\ie4uinit.exe" -ClearIconCache 2>$null } catch {}
|
||||
try { & "$env:SystemRoot\System32\ie4uinit.exe" -show 2>$null } catch {}
|
||||
# Always do the cheap, non-disruptive per-item refresh so a
|
||||
# rewritten same-name .lnk renders with its new target/icon
|
||||
# immediately (a same-name .lnk recreated across reinstalls keeps
|
||||
# Explorer's cached per-item icon). The reliable fix (no explorer
|
||||
# restart) is a per-item SHChangeNotify SHCNE_UPDATEITEM +
|
||||
# SHCNF_PATHW per .lnk; the global SHCNE_ASSOCCHANGED broadcast
|
||||
# alone does NOT recover a stale item.
|
||||
try {
|
||||
Add-Type -Namespace UnslothShell -Name IconRefresh -MemberDefinition '[System.Runtime.InteropServices.DllImport("shell32.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode)] public static extern void SHChangeNotify(int eventId, uint flags, string item1, System.IntPtr item2);' -ErrorAction SilentlyContinue
|
||||
# SHCNE_UPDATEITEM (0x00002000) + SHCNF_PATHW (0x0005) per shortcut
|
||||
|
|
@ -930,21 +972,31 @@ shell.Run cmd, 0, False
|
|||
# SHCNE_ASSOCCHANGED (0x08000000) global refresh (belt-and-suspenders)
|
||||
[UnslothShell.IconRefresh]::SHChangeNotify(0x08000000, 0, $null, [System.IntPtr]::Zero)
|
||||
} catch {}
|
||||
# Win11's Start Menu (StartMenuExperienceHost) keeps its OWN
|
||||
# pre-rendered tile-icon cache that ie4uinit/explorer restart do NOT
|
||||
# invalidate, so a rewritten same-name shortcut shows the old tile
|
||||
# until the host restarts. Drop only the render caches (NEVER
|
||||
# start2.bin -- the pinned layout) and let the host rebuild.
|
||||
# Best-effort; Win10 has no such host (Test-Path skips it).
|
||||
try {
|
||||
$smehTemp = Join-Path $env:LOCALAPPDATA "Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\TempState"
|
||||
if (Test-Path -LiteralPath $smehTemp) {
|
||||
Get-ChildItem -LiteralPath $smehTemp -Filter "TileCache_*" -ErrorAction SilentlyContinue |
|
||||
Remove-Item -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item -LiteralPath (Join-Path $smehTemp "StartUnifiedTileModelCache.dat") -Force -ErrorAction SilentlyContinue
|
||||
Stop-Process -Name StartMenuExperienceHost -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
} catch {}
|
||||
# Heavier on-disk icon-cache clear + StartMenuExperienceHost tile
|
||||
# rebuild only when the icon actually changed or this is a first
|
||||
# install. Running "clear icon cache + kill StartMenuExperienceHost"
|
||||
# on every no-op reinstall is a dropper-like behavioral cluster and
|
||||
# is unnecessary when the icon is unchanged (the per-item notify
|
||||
# above already refreshes the rewritten shortcut).
|
||||
if ($firstInstall -or $iconChanged) {
|
||||
try { & "$env:SystemRoot\System32\ie4uinit.exe" -ClearIconCache 2>$null } catch {}
|
||||
try { & "$env:SystemRoot\System32\ie4uinit.exe" -show 2>$null } catch {}
|
||||
# Win11's Start Menu (StartMenuExperienceHost) keeps its OWN
|
||||
# pre-rendered tile-icon cache that ie4uinit/explorer restart do NOT
|
||||
# invalidate, so a rewritten same-name shortcut shows the old tile
|
||||
# until the host restarts. Drop only the render caches (NEVER
|
||||
# start2.bin -- the pinned layout) and let the host rebuild.
|
||||
# Best-effort; Win10 has no such host (Test-Path skips it).
|
||||
try {
|
||||
$smehTemp = Join-Path $env:LOCALAPPDATA "Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\TempState"
|
||||
if (Test-Path -LiteralPath $smehTemp) {
|
||||
Get-ChildItem -LiteralPath $smehTemp -Filter "TileCache_*" -ErrorAction SilentlyContinue |
|
||||
Remove-Item -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item -LiteralPath (Join-Path $smehTemp "StartUnifiedTileModelCache.dat") -Force -ErrorAction SilentlyContinue
|
||||
Stop-Process -Name StartMenuExperienceHost -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
} else {
|
||||
substep "no Unsloth Studio shortcuts were created" "Yellow"
|
||||
}
|
||||
|
|
|
|||
54
install.sh
54
install.sh
|
|
@ -1261,6 +1261,10 @@ if (-not \$targetExe) { exit 1 }
|
|||
# native install if one exists) so the WSL shortcut shows the proper icon.
|
||||
\$iconDir = Join-Path \$env:LOCALAPPDATA 'Unsloth Studio'
|
||||
\$iconPath = Join-Path \$iconDir 'unsloth.ico'
|
||||
\$preIconHash = \$null
|
||||
if (Test-Path -LiteralPath \$iconPath) {
|
||||
try { \$preIconHash = (Get-FileHash -LiteralPath \$iconPath -Algorithm SHA256).Hash } catch {}
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath \$iconPath)) {
|
||||
try {
|
||||
New-Item -ItemType Directory -Force -Path \$iconDir | Out-Null
|
||||
|
|
@ -1276,9 +1280,11 @@ if (Test-Path -LiteralPath \$iconPath) {
|
|||
(Join-Path \$env:APPDATA 'Microsoft\Windows\Start Menu\Programs')
|
||||
)
|
||||
\$created = @()
|
||||
\$firstShortcut = \$false
|
||||
foreach (\$dir in \$locations) {
|
||||
if (-not \$dir -or -not (Test-Path \$dir)) { continue }
|
||||
\$linkPath = Join-Path \$dir '$_css_lnk_name_ps'
|
||||
if (-not (Test-Path -LiteralPath \$linkPath)) { \$firstShortcut = \$true }
|
||||
\$shortcut = \$WshShell.CreateShortcut(\$linkPath)
|
||||
\$shortcut.TargetPath = \$targetExe
|
||||
\$shortcut.Arguments = '$_css_sc_args_ps'
|
||||
|
|
@ -1287,27 +1293,43 @@ foreach (\$dir in \$locations) {
|
|||
\$shortcut.Save()
|
||||
\$created += \$linkPath
|
||||
}
|
||||
# Force Explorer to re-read EACH new shortcut's icon so it renders immediately
|
||||
# instead of a stale/blank (generic) icon. The reliable, NON-disruptive fix
|
||||
# (no explorer restart) is a PER-ITEM SHChangeNotify(SHCNE_UPDATEITEM,
|
||||
# SHCNF_PATHW, <lnk>) -- the global SHCNE_ASSOCCHANGED alone does not recover a
|
||||
# stale item. Also clear the on-disk icon cache for heavier staleness.
|
||||
try { & "\$env:SystemRoot\System32\ie4uinit.exe" -ClearIconCache } catch {}
|
||||
try { & "\$env:SystemRoot\System32\ie4uinit.exe" -show } catch {}
|
||||
\$iconChanged = \$false
|
||||
if (\$hasIcon) {
|
||||
if (-not \$preIconHash) {
|
||||
\$iconChanged = \$true
|
||||
} else {
|
||||
try {
|
||||
\$postIconHash = (Get-FileHash -LiteralPath \$iconPath -Algorithm SHA256).Hash
|
||||
\$iconChanged = (\$postIconHash -ne \$preIconHash)
|
||||
} catch { \$iconChanged = \$true }
|
||||
}
|
||||
} elseif (\$preIconHash) {
|
||||
\$iconChanged = \$true
|
||||
}
|
||||
# Per-item refresh always (cheap, non-disruptive) so the rewritten .lnk renders
|
||||
# immediately instead of a stale/blank (generic) icon. The reliable fix (no
|
||||
# explorer restart) is a PER-ITEM SHChangeNotify(SHCNE_UPDATEITEM, SHCNF_PATHW,
|
||||
# <lnk>) -- the global SHCNE_ASSOCCHANGED alone does not recover a stale item.
|
||||
try {
|
||||
Add-Type -Namespace UnslothShell -Name IconRefresh -MemberDefinition '[System.Runtime.InteropServices.DllImport("shell32.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode)] public static extern void SHChangeNotify(int e, uint f, string a, System.IntPtr b);' -ErrorAction SilentlyContinue
|
||||
foreach (\$p in \$created) { try { [UnslothShell.IconRefresh]::SHChangeNotify(0x00002000, 0x0005, \$p, [System.IntPtr]::Zero) } catch {} }
|
||||
[UnslothShell.IconRefresh]::SHChangeNotify(0x08000000, 0, \$null, [System.IntPtr]::Zero)
|
||||
} catch {}
|
||||
# Win11 Start Menu keeps its own tile-icon cache (preserve start2.bin).
|
||||
try {
|
||||
\$smeh = Join-Path \$env:LOCALAPPDATA 'Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\TempState'
|
||||
if (Test-Path -LiteralPath \$smeh) {
|
||||
Get-ChildItem -LiteralPath \$smeh -Filter 'TileCache_*' -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item -LiteralPath (Join-Path \$smeh 'StartUnifiedTileModelCache.dat') -Force -ErrorAction SilentlyContinue
|
||||
Stop-Process -Name StartMenuExperienceHost -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
} catch {}
|
||||
# Heavier on-disk icon-cache clear + StartMenuExperienceHost tile rebuild
|
||||
# (preserve start2.bin) only on first install or a real icon change, so a no-op
|
||||
# WSL reinstall does not run a dropper-like clear-cache + kill cluster each time.
|
||||
if (\$created.Count -gt 0 -and (\$firstShortcut -or \$iconChanged)) {
|
||||
try { & "\$env:SystemRoot\System32\ie4uinit.exe" -ClearIconCache } catch {}
|
||||
try { & "\$env:SystemRoot\System32\ie4uinit.exe" -show } catch {}
|
||||
try {
|
||||
\$smeh = Join-Path \$env:LOCALAPPDATA 'Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\TempState'
|
||||
if (Test-Path -LiteralPath \$smeh) {
|
||||
Get-ChildItem -LiteralPath \$smeh -Filter 'TileCache_*' -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item -LiteralPath (Join-Path \$smeh 'StartUnifiedTileModelCache.dat') -Force -ErrorAction SilentlyContinue
|
||||
Stop-Process -Name StartMenuExperienceHost -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
WSLPS1_EOF
|
||||
|
||||
# Convert WSL path to Windows path for powershell.exe
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
"""Guard install.ps1's launch-studio.vbs against re-introducing the AV-heuristic
|
||||
shape: a WScript .vbs spawning a hidden, ExecutionPolicy-Bypass PowerShell."""
|
||||
"""Guard install.ps1's Studio launcher against re-introducing the AV-heuristic
|
||||
shape: a WScript .vbs that spawns a hidden, ExecutionPolicy-Bypass PowerShell
|
||||
(Kaspersky HEUR:Trojan.VBS.Agent.gen). The shortcut must stay windowless via
|
||||
powershell.exe -WindowStyle Hidden over launch-studio.ps1 -- never a .vbs /
|
||||
WScript.Shell.Run wrapper. Any pre-existing .vbs from an older install must be
|
||||
deleted, not merely left behind."""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
|
@ -10,40 +14,51 @@ REPO_ROOT = Path(__file__).resolve().parents[3]
|
|||
INSTALL_PS1 = REPO_ROOT / "install.ps1"
|
||||
|
||||
|
||||
def _vbs_block() -> str:
|
||||
text = INSTALL_PS1.read_text(encoding = "utf-8")
|
||||
m = re.search(r'\$vbsContent\s*=\s*@"\r?\n(.*?)\r?\n"@', text, re.S)
|
||||
assert m, "could not locate the $vbsContent here-string in install.ps1"
|
||||
return m.group(1)
|
||||
def _text() -> str:
|
||||
return INSTALL_PS1.read_text(encoding = "utf-8")
|
||||
|
||||
|
||||
def test_install_ps1_present():
|
||||
assert INSTALL_PS1.is_file(), f"missing {INSTALL_PS1}"
|
||||
|
||||
|
||||
def test_vbs_does_not_pass_windowstyle_hidden():
|
||||
vbs = _vbs_block()
|
||||
assert "-WindowStyle Hidden" not in vbs, (
|
||||
"launch-studio.vbs must not pass -WindowStyle Hidden to PowerShell: the "
|
||||
"window is already hidden by shell.Run(cmd, 0, False); the redundant flag "
|
||||
"only adds the hidden-PowerShell token that AV heuristics flag."
|
||||
def test_no_vbs_launcher_generated():
|
||||
text = _text()
|
||||
# No here-string that builds a .vbs body, and no .vbs file written. (A
|
||||
# Remove-Item cleanup of the legacy .vbs is allowed and checked separately.)
|
||||
assert "$vbsContent" not in text, (
|
||||
"install.ps1 must not generate a launch-studio.vbs: a WScript.Shell .vbs "
|
||||
"spawning a hidden ExecutionPolicy-Bypass PowerShell is the exact shape "
|
||||
"VBS-dropper heuristics flag (Kaspersky HEUR:Trojan.VBS.Agent.gen)."
|
||||
)
|
||||
assert 'CreateObject("WScript.Shell")' not in text
|
||||
assert "shell.Run" not in text
|
||||
assert not re.search(r"Set-Content\s+-LiteralPath\s+\$launcherVbs", text)
|
||||
assert "//B //Nologo" not in text
|
||||
|
||||
|
||||
def test_vbs_stays_windowless_via_shell_run():
|
||||
vbs = _vbs_block()
|
||||
assert re.search(r"shell\.Run\s+cmd\s*,\s*0\s*,\s*False", vbs), (
|
||||
"launcher must remain windowless via shell.Run(cmd, 0, False) "
|
||||
"(intWindowStyle 0 = hidden)."
|
||||
)
|
||||
def test_legacy_vbs_removed_on_upgrade():
|
||||
# The whole point: an upgrade must DELETE a pre-existing launch-studio.vbs,
|
||||
# not just stop generating it, or AV keeps flagging the stale file.
|
||||
text = _text()
|
||||
assert re.search(
|
||||
r"Remove-Item\s+-LiteralPath\s+\$legacyLauncherVbs", text
|
||||
), "upgrades must remove a pre-existing launch-studio.vbs so AV stops flagging it"
|
||||
|
||||
|
||||
def test_vbs_keeps_bypass_and_file_invocation():
|
||||
# Bypass lets the unsigned local .ps1 run under the default Restricted policy.
|
||||
vbs = _vbs_block()
|
||||
assert "-ExecutionPolicy Bypass" in vbs
|
||||
assert "-File" in vbs
|
||||
assert "powershell" in vbs
|
||||
def test_shortcut_target_is_not_wscript():
|
||||
# The .lnk must not be launched through wscript.exe (the VBS script host).
|
||||
text = _text()
|
||||
assert "wscript.exe" not in text.lower()
|
||||
|
||||
|
||||
def test_launcher_is_windowless_powershell():
|
||||
# The shortcut runs powershell.exe with a hidden window over launch-studio.ps1.
|
||||
text = _text()
|
||||
assert re.search(
|
||||
r"-WindowStyle\s+Hidden", text
|
||||
), "the launcher must run powershell.exe with -WindowStyle Hidden over launch-studio.ps1."
|
||||
assert "launch-studio.ps1" in text
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue