Skip to content

Commit 28d81f9

Browse files
committed
fix: address managed binary review feedback
1 parent 24d527e commit 28d81f9

7 files changed

Lines changed: 80 additions & 22 deletions

File tree

src/services/code-index/semble/semble-downloader.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const SEMBLE_ARCHIVES: Record<string, { archive: string; binary: string }> = {
2626
export const SEMBLE_VERSION = "v0.4.1"
2727
const DOWNLOAD_BASE_URL = `https://github.com/Zoo-Code-Org/sembleexec/releases/download/${SEMBLE_VERSION}`
2828
const VERSION_FILE = ".semble-version"
29+
const MAX_ARCHIVE_BYTES = 50 * 1024 * 1024
2930

3031
/**
3132
* SHA-256 checksums for each platform archive at SEMBLE_VERSION.
@@ -121,13 +122,16 @@ export async function downloadSemble(storageDir: string): Promise<string | undef
121122
name: "Semble",
122123
trustedDomains: TRUSTED_DOWNLOAD_DOMAINS,
123124
timeoutMs: 120_000,
125+
maxBytes: MAX_ARCHIVE_BYTES,
124126
}),
125127
verifyArchive: (archivePath) => verifyChecksum(archivePath, expectedChecksum),
126128
extractArchive: async (archivePath, stagingDir) => {
127129
if (info.archive.endsWith(".tar.gz")) {
128130
await extractTarGzArchive(archivePath, stagingDir)
129131
} else if (info.archive.endsWith(".zip")) {
130132
await extractZipArchive(archivePath, stagingDir)
133+
} else {
134+
throw new Error(`Unsupported semble archive format: ${info.archive}`)
131135
}
132136
},
133137
})

src/services/managed-binary/__tests__/archive.spec.ts

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,18 @@ describe("managed binary archive utilities", () => {
4444

4545
it("kills a process that exceeds its timeout", async () => {
4646
vi.useFakeTimers()
47-
const child = createChild()
48-
mockSpawn.mockReturnValue(child as unknown as ReturnType<typeof spawn>)
49-
const processResult = runProcess("tool", [], 100)
50-
const assertion = expect(processResult).rejects.toThrow("tool timed out")
51-
52-
await vi.advanceTimersByTimeAsync(100)
53-
await assertion
54-
expect(child.kill).toHaveBeenCalledWith("SIGKILL")
55-
vi.useRealTimers()
47+
try {
48+
const child = createChild()
49+
mockSpawn.mockReturnValue(child as unknown as ReturnType<typeof spawn>)
50+
const processResult = runProcess("tool", [], 100)
51+
const assertion = expect(processResult).rejects.toThrow("tool timed out")
52+
53+
await vi.advanceTimersByTimeAsync(100)
54+
await assertion
55+
expect(child.kill).toHaveBeenCalledWith("SIGKILL")
56+
} finally {
57+
vi.useRealTimers()
58+
}
5659
})
5760

5861
it("extracts tar.gz archives with hardened flags", async () => {
@@ -112,7 +115,7 @@ describe("managed binary archive utilities", () => {
112115
mockSpawn.mockReturnValueOnce(listing as unknown as ReturnType<typeof spawn>)
113116
mockSpawn.mockReturnValueOnce(extraction as unknown as ReturnType<typeof spawn>)
114117
const result = extractSingleFileTarXzArchive("/tmp/archive.tar.xz", "/tmp/output", "binary", "Tool")
115-
listing.stdout.write("./binary\n")
118+
listing.stdout.write("-rwxr-xr-x user/group 1 2026-01-01 00:00 ./binary\n")
116119
listing.emit("close", 0)
117120
await new Promise<void>((resolve) => setImmediate(resolve))
118121
extraction.emit("close", 0)
@@ -121,11 +124,36 @@ describe("managed binary archive utilities", () => {
121124
expect(mockSpawn).toHaveBeenNthCalledWith(
122125
2,
123126
"tar",
124-
["-xJf", "/tmp/archive.tar.xz", "-C", "/tmp/output", "./binary"],
127+
[
128+
"-xJf",
129+
"/tmp/archive.tar.xz",
130+
"-C",
131+
"/tmp/output",
132+
"--no-same-owner",
133+
...(process.platform === "linux" ? ["--no-overwrite-dir"] : []),
134+
"./binary",
135+
],
125136
expect.any(Object),
126137
)
127138
})
128139

140+
it.each([
141+
["-rwxr-xr-x user/group 1 2026-01-01 00:00 ./other\n", "an unexpected filename"],
142+
[
143+
"-rwxr-xr-x user/group 1 2026-01-01 00:00 ./binary\n-rwxr-xr-x user/group 1 2026-01-01 00:00 ./other\n",
144+
"multiple entries",
145+
],
146+
["lrwxrwxrwx user/group 0 2026-01-01 00:00 ./binary\n", "a non-regular entry"],
147+
])("rejects a tar.xz archive with %s", async (listingOutput) => {
148+
const listing = createChild()
149+
mockSpawn.mockReturnValue(listing as unknown as ReturnType<typeof spawn>)
150+
const result = extractSingleFileTarXzArchive("/tmp/archive.tar.xz", "/tmp/output", "binary", "Tool")
151+
listing.stdout.write(listingOutput)
152+
listing.emit("close", 0)
153+
154+
await expect(result).rejects.toThrow("Tool archive has an unexpected layout")
155+
})
156+
129157
it("builds a single-entry-validated PowerShell ZIP extraction", async () => {
130158
const child = createChild()
131159
mockSpawn.mockReturnValue(child as unknown as ReturnType<typeof spawn>)

src/services/managed-binary/__tests__/download.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,16 @@ describe("managed binary downloads", () => {
104104
await expect(verification).rejects.toThrow("checksum mismatch: actual-checksum")
105105
})
106106

107+
it("accepts a matching SHA-256 checksum", async () => {
108+
const input = new EventEmitter()
109+
mockCreateReadStream.mockReturnValue(input as ReturnType<typeof createReadStream>)
110+
const verification = verifySha256Checksum("/tmp/archive", "actual-checksum", () => new Error("should not fail"))
111+
input.emit("data", Buffer.from("archive"))
112+
input.emit("end")
113+
114+
await expect(verification).resolves.toBeUndefined()
115+
})
116+
107117
it("follows a trusted redirect and applies destination security options", async () => {
108118
const requestOne = createRequest()
109119
const requestTwo = createRequest()

src/services/managed-binary/__tests__/install.spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,10 @@ describe("managed binary installation", () => {
6868
expect(first).toBe(second)
6969
await vi.waitFor(() => expect(download).toHaveBeenCalledOnce())
7070
finishDownload?.()
71-
await Promise.all([first, second])
71+
await expect(Promise.all([first, second])).resolves.toEqual([
72+
getManagedBinaryPaths(options).binaryPath,
73+
getManagedBinaryPaths(options).binaryPath,
74+
])
7275
expect(download).toHaveBeenCalledOnce()
7376
})
7477

src/services/managed-binary/archive.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export async function extractSingleFileZipArchive(
7070
expectedFile: string,
7171
archiveName: string,
7272
): Promise<void> {
73+
// This deliberately uses PowerShell because it is only called for Windows release archives.
7374
const script = [
7475
"$ErrorActionPreference = 'Stop'",
7576
"$archivePath = $args[0]",
@@ -103,15 +104,21 @@ export async function extractSingleFileTarXzArchive(
103104
expectedFile: string,
104105
archiveName: string,
105106
): Promise<void> {
106-
const listing = await runProcess("tar", ["-tJf", archivePath])
107+
const listing = await runProcess("tar", ["-tvJf", archivePath])
107108
const entries = listing.stdout
108109
.split(/\r?\n/)
109110
.map((entry) => entry.trim())
110111
.filter(Boolean)
111112
const archiveEntry = entries[0]
112-
if (entries.length !== 1 || archiveEntry.replace(/^\.\//, "") !== expectedFile) {
113+
const entryName = archiveEntry?.split(/\s+/).at(-1)
114+
if (entries.length !== 1 || !archiveEntry.startsWith("-") || entryName?.replace(/^\.\//, "") !== expectedFile) {
113115
throw new Error(`${archiveName} archive has an unexpected layout`)
114116
}
115117

116-
await runProcess("tar", ["-xJf", archivePath, "-C", destination, archiveEntry])
118+
const args = ["-xJf", archivePath, "-C", destination, "--no-same-owner"]
119+
if (process.platform === "linux") {
120+
args.push("--no-overwrite-dir")
121+
}
122+
args.push(entryName)
123+
await runProcess("tar", args)
117124
}

src/services/managed-binary/download.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -125,27 +125,30 @@ function downloadBinaryFileWithRedirects(
125125
destination,
126126
options.exclusiveDestination ? { flags: "wx", mode: 0o600 } : undefined,
127127
)
128+
const abort = (error: Error) => {
129+
response.unpipe(output)
130+
output.destroy()
131+
response.destroy()
132+
request.destroy()
133+
reject(error)
134+
}
128135
response.on("data", (chunk: Buffer) => {
129136
received += chunk.length
130137
if (options.maxBytes !== undefined) {
131138
try {
132139
assertSizeWithinLimit(received, options.maxBytes, options.name)
133140
} catch (error) {
134-
response.unpipe(output)
135-
output.destroy()
136-
response.destroy()
137-
request.destroy()
138-
reject(error)
141+
abort(error instanceof Error ? error : new Error(String(error)))
139142
}
140143
}
141144
})
142-
response.on("error", reject)
145+
response.on("error", abort)
143146
response.pipe(output)
144147
output.on("finish", () => {
145148
output.close()
146149
resolve()
147150
})
148-
output.on("error", reject)
151+
output.on("error", abort)
149152
})
150153

151154
request.setTimeout(options.timeoutMs, () => request.destroy(new Error(`${options.name} download timed out`)))

src/services/managed-binary/install.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ async function installManagedBinaryWithLock(options: ManagedBinaryInstallOptions
124124
stale: 5 * 60_000,
125125
update: 30_000,
126126
retries: { retries: 10, factor: 1.5, minTimeout: 100, maxTimeout: 1_000 },
127+
onCompromised: (error) => {
128+
throw error
129+
},
127130
})
128131
try {
129132
return await installManagedBinary(options)

0 commit comments

Comments
 (0)