Align Windows release bundle mode validation
CI / tests-and-images (pull_request) Successful in 2m14s
Runner smoke test / runner-smoke (pull_request) Successful in 2s

This commit is contained in:
codex committed 2026-11-03 10:12:52 +03:00
1 parent 9d553744a1
commit 8748760594
4 files changed
+126 -4

No files matched your search

+9
View File
@@ -80,6 +80,15 @@ manifest и Git blob IDs в памяти, затем копирует тольк
Дополнительно installer проверяет цепочку Git objects `commit → tree → файлы`.
На elevated этапе Git не запускается; `.git/config`, hooks и состояние checkout
не используются. Не изменяйте ZIP между подготовкой и установкой.
Упаковщик ставит всем ZIP entries тип regular file и mode `0100600`; installer
проверяет тот же mode (`0x8180`) до чтения содержимого. Для проверки стыка
упаковщика и фактической функции mode-валидации на Windows используйте
`powershell.exe -NoProfile -File infra/deploy/test_windows_release_bundle_mode.ps1 -PythonPath C:\Python313\python.exe`;
`git.exe` должен уже разрешаться обычным `PATH`. Harness создаёт синтетический
Git репозиторий и временный ZIP, извлекает только эту функцию из PowerShell AST и
проверяет принятие штатного пакета и отказ после изменения mode. Он не запускает
installer, не требует elevation, не меняет ACL и не регистрирует задачи; политику
выполнения PowerShell менять для него не следует.
Проверенный bootstrap installer проверяет подпись PSF у
`C:\Python313\python.exe` и защищённые ACL, затем размещает файлы клиента в
@@ -173,6 +173,10 @@ function Read-BoundedZipEntry($Entry, [long]$Maximum) {
$buffer.Dispose()
}
}
function Assert-ReceiverBundleEntryMode($Entry) {
$entryMode = (([long]$Entry.ExternalAttributes -shr 16) -band 0xFFFF)
if ($entryMode -ne 0x8180) { throw "Receiver bundle entry has an unsafe file mode: $($Entry.FullName)" }
}
function Get-LowerHex([byte[]]$Bytes, [bool]$UseSha1 = $false) {
$algorithm = if ($UseSha1) { [Security.Cryptography.SHA1]::Create() } else { [Security.Cryptography.SHA256]::Create() }
try { return [BitConverter]::ToString($algorithm.ComputeHash($Bytes)).Replace('-', '').ToLowerInvariant() }
@@ -241,8 +245,7 @@ try {
if ($entryNames -cnotcontains $expectedEntry) { throw "Receiver bundle is missing an exact required entry: $expectedEntry" }
}
foreach ($zipEntry in $zip.Entries) {
$entryMode = (([long]$zipEntry.ExternalAttributes -shr 16) -band 0xFFFF)
if ($entryMode -ne 0x81A0) { throw "Receiver bundle entry has an unsafe file mode: $($zipEntry.FullName)" }
Assert-ReceiverBundleEntryMode -Entry $zipEntry
}
$manifestBytes = Read-BoundedZipEntry -Entry $zip.GetEntry('manifest.json') -Maximum 16384
$utf8Strict = [Text.UTF8Encoding]::new($false, $true)
@@ -0,0 +1,85 @@
param(
[Parameter(Mandatory = $true)]
[string]$PythonPath
)
$ErrorActionPreference = 'Stop'
$repository = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
$installerPath = Join-Path $PSScriptRoot 'install_windows_release_client.ps1'
$fixtureScript = Join-Path $PSScriptRoot 'test_windows_release_launcher.py'
$temporaryRoot = Join-Path ([IO.Path]::GetTempPath()) ('akyldash-bundle-mode-' + [guid]::NewGuid().ToString('N'))
[void][IO.Directory]::CreateDirectory($temporaryRoot)
$bundlePath = Join-Path $temporaryRoot 'fixture.zip'
try {
$fixtureOutput = & $PythonPath -E -s -S $fixtureScript --write-bundle-fixture $bundlePath
if ($LASTEXITCODE -ne 0) { throw 'The temporary packager fixture failed.' }
$fixture = ($fixtureOutput -join "`n") | ConvertFrom-Json
$tokens = $null
$parseErrors = $null
$installerAst = [System.Management.Automation.Language.Parser]::ParseFile(
$installerPath, [ref]$tokens, [ref]$parseErrors)
if ($parseErrors.Count -gt 0) { throw 'Installer PowerShell syntax is invalid.' }
$validatorAst = $installerAst.Find({
param($node)
$node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and
$node.Name -ceq 'Assert-ReceiverBundleEntryMode'
}, $true)
if ($null -eq $validatorAst) { throw 'Installer bundle-mode validator was not found.' }
. ([scriptblock]::Create($validatorAst.Extent.Text))
Add-Type -AssemblyName System.IO.Compression
$stream = [IO.File]::OpenRead($bundlePath)
try {
$archive = [System.IO.Compression.ZipArchive]::new(
$stream, [System.IO.Compression.ZipArchiveMode]::Read, $true)
try {
foreach ($entry in $archive.Entries) {
Assert-ReceiverBundleEntryMode -Entry $entry
}
} finally { $archive.Dispose() }
} finally { $stream.Dispose() }
$alteredPath = Join-Path $temporaryRoot 'altered-mode.zip'
$sourceStream = [IO.File]::OpenRead($bundlePath)
$targetStream = [IO.File]::Create($alteredPath)
try {
$sourceArchive = [System.IO.Compression.ZipArchive]::new(
$sourceStream, [System.IO.Compression.ZipArchiveMode]::Read, $true)
$targetArchive = [System.IO.Compression.ZipArchive]::new(
$targetStream, [System.IO.Compression.ZipArchiveMode]::Create, $true)
try {
$changed = $false
foreach ($entry in $sourceArchive.Entries) {
$copy = $targetArchive.CreateEntry($entry.FullName, [System.IO.Compression.CompressionLevel]::NoCompression)
$copy.ExternalAttributes = $entry.ExternalAttributes
if (-not $changed) {
$copy.ExternalAttributes = [int]([long]0x81A0 -shl 16)
$changed = $true
}
$input = $entry.Open()
$output = $copy.Open()
try { $input.CopyTo($output) }
finally { $output.Dispose(); $input.Dispose() }
}
if (-not $changed) { throw 'Packager fixture had no entries to mutate.' }
} finally { $targetArchive.Dispose(); $sourceArchive.Dispose() }
} finally { $targetStream.Dispose(); $sourceStream.Dispose() }
$alteredStream = [IO.File]::OpenRead($alteredPath)
try {
$alteredArchive = [System.IO.Compression.ZipArchive]::new(
$alteredStream, [System.IO.Compression.ZipArchiveMode]::Read, $true)
try {
$rejected = $false
try { Assert-ReceiverBundleEntryMode -Entry $alteredArchive.Entries[0] }
catch { $rejected = $true }
if (-not $rejected) { throw 'Installer validator accepted an altered ZIP entry mode.' }
} finally { $alteredArchive.Dispose() }
} finally { $alteredStream.Dispose() }
Write-Output ("PASS: installer validator accepted the actual packager bundle for commit {0} (SHA-256 {1}) and rejected altered mode." -f $fixture.commit, $fixture.sha256)
} finally {
Remove-Item -LiteralPath $temporaryRoot -Recurse -Force
}
+27 -2
View File
@@ -92,7 +92,8 @@ class WindowsReleaseBootstrapTests(unittest.TestCase):
script = (ROOT / "infra/deploy/install_windows_release_client.ps1").read_text(encoding="utf-8")
self.assertIn("[IO.FileShare]::Read", script)
self.assertIn("[System.IO.Compression.ZipArchive]::new($bundleStream", script)
self.assertIn("$entryMode -ne 0x81A0", script)
self.assertIn("Assert-ReceiverBundleEntryMode -Entry $zipEntry", script)
self.assertIn("$entryMode -ne 0x8180", script)
self.assertIn("$bundleHash -ne $ExpectedBundleSha256", script)
self.assertIn("manifest.commit -cne $ExpectedCommit", script)
self.assertIn("Get-GitObjectHex -Kind 'blob' -Bytes $data", script)
@@ -185,6 +186,8 @@ class WindowsReleaseBootstrapTests(unittest.TestCase):
self.assertEqual(set(member.filename for member in bundle.infolist()),
{"manifest.json", *(f"files/{name}" for name in release_bundle.FILES),
*release_bundle.TREE_OBJECTS.values(), "objects/commit.bin"})
self.assertEqual({(member.external_attr >> 16) & 0xFFFF for member in bundle.infolist()},
{0o100600})
self.assertEqual(release_bundle._git_object_sha("commit", bundle.read("objects/commit.bin")), commit)
for tree in manifest["trees"]:
object_name = release_bundle.TREE_OBJECTS[tree["path"]]
@@ -358,4 +361,26 @@ class WindowsReleaseBootstrapTests(unittest.TestCase):
if __name__ == "__main__":
unittest.main()
if len(sys.argv) == 3 and sys.argv[1] == "--write-bundle-fixture":
output_path = Path(sys.argv[2])
repository = output_path.parent / "fixture-repository"
repository.mkdir()
subprocess.run(["git", "-C", str(repository), "init", "-b", "main"], check=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
subprocess.run(["git", "-C", str(repository), "config", "user.name", "Akyldash Test"], check=True)
subprocess.run(["git", "-C", str(repository), "config", "user.email", "test@example.invalid"], check=True)
for name, relative in release_bundle.FILES.items():
path = repository / relative
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(f"fixture:{name}\n".encode("ascii"))
subprocess.run(["git", "-C", str(repository), "add", "--", *release_bundle.FILES.values()], check=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
subprocess.run(["git", "-C", str(repository), "commit", "-m", "bundle fixture"], check=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
commit = subprocess.run(["git", "-C", str(repository), "rev-parse", "HEAD"], check=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True).stdout.strip()
payload = release_bundle.create_bundle(repository, commit)
output_path.write_bytes(payload)
print(json.dumps({"commit": commit, "sha256": hashlib.sha256(payload).hexdigest()}))
else:
unittest.main()