fix(core): chmod the Core Tools binaries that are actually present - #1008
Open
om singhal (Om-singhaI) wants to merge 1 commit into
Open
fix(core): chmod the Core Tools binaries that are actually present#1008om singhal (Om-singhaI) wants to merge 1 commit into
om singhal (Om-singhaI) wants to merge 1 commit into
Conversation
Azure Functions Core Tools v4 packages no longer ship gozip at the root of the archive; it now sits under the in-proc6 and in-proc8 host folders. downloadCoreTools() still ran chmod on a hardcoded <dest>/gozip path, so a fresh install on Linux or macOS crashed with ENOENT immediately after the archive extracted fine, which left swa start unusable. Look up each known binary and chmod only the ones present on disk. These archives carry no Unix permission bits at all, so guarding the old root path alone would stop the crash but leave the relocated binaries non executable. The list covers the in-proc6 and in-proc8 host executables as well, matching what the official azure-functions-core-tools npm installer sets on the very same archive; the existence check also handles builds that ship no in process folders, so no architecture detection is needed. Fixes Azure#1007
om singhal (Om-singhaI)
requested review from
Long Hao (LongOddCode),
Jikun (cjk7989) and
jessieyyt
as code owners
August 24, 2026 02:18
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix(core): chmod the Core Tools binaries that are actually present
Fixes #1007
What breaks
On Linux and macOS, a fresh
swa startthat has to download Azure Functions Core Tools v4 dies immediately after the archive extracts successfully:downloadCoreTools()insrc/core/func-core-tools.tsextracts the package, then unconditionally runs chmod on two fixed paths:The second call is the crash. Nothing catches it, so the CLI fails at startup even though the download and extraction both worked.
Why it happens
gozipwas not removed from Core Tools. It moved.I checked the shipped artifacts directly rather than going by the file listing of a local install. I replicated the selection logic in
getLatestCoreToolsRelease()against the live feed (cli-feed-v4.json, keys reversed, stable 4.x only,coreToolsentry matching the OS withsize: "full"), then parsed each archive's ZIP central directory over HTTP range requests, so no full download was needed.The release that selection resolves to today is 4.131.0:
Azure.Functions.Cli.linux-x64.4.13.0.zip, 8703 central directory entries:funcpresent at the root, nogozipat the root,gozippresent atin-proc6/gozipandin-proc8/gozip.Azure.Functions.Cli.osx-x64.4.13.0.zip, 8698 central directory entries: identical in this respect.So
path.join(dest, "gozip")resolves to a path that no longer exists, whilefuncis still where the code expects it.Walking the stable 4.x Linux releases in the feed pins when this changed. The
in-proc6andin-proc8folders appeared first and theirgozipcopies coexisted with the root copy for a long stretch; the root copy is what went away:goziponly at the root, noin-proc6orin-proc8folder at allgozipat the root and atin-proc6/gozipandin-proc8/gozipin-proc6/gozipandin-proc8/gozipThat makes 4.127.0 the first release that breaks this code path.
Why guarding the old path alone is not enough
The same central directory dump shows a second thing worth acting on: in every one of these archives, every entry has an external attribute field of
0. Counted across all 8703 Linux entries and all 8698 macOS entries, zero entries carry a non zero value.These packages therefore ship no Unix permission bits at all. That is exactly why the explicit chmod on
funcexists in the first place, and it means every other binary in the package also lands without the executable bit.Wrapping the existing
path.join(dest, "gozip")call in anfs.existsSynccheck would stop the crash, but it would leave the real binaries non executable. That trades a loud failure for a quiet one.Which binaries actually need the flag
The same 4.102.0 release that introduced
in-proc6/gozipandin-proc8/gozipalso introducedin-proc6/funcandin-proc8/func. Those two are the in process host executables: each sits next to its ownfunc.dll,func.dll.config,createdumpandlibhostfxr.so, which is the same self contained apphost layout as the package root wherefuncis already chmodded today. Core Tools launches one of them for .NET in process function apps.The authoritative reference for which files need the flag is Microsoft's own installer for the
azure-functions-core-toolsnpm package, which unpacks the identical archive. Itslib/install.jsat 4.13.0 does exactly this:So the set this file has to cover is
funcplus the two in process hosts, and thegozipcopies it already tries to handle. Note also that upstream skips the in process folders entirely on builds that do not ship them, which is the behaviour the existence check below gives us for free.What the fix does
Enumerate the binaries that need the executable flag and chmod the ones that are actually on disk:
Notes on the shape of this:
gozipentry stays in the list on purpose. Releases up to 4.126.0 still ship it there, and this code path is reached for any 4.x version the feed resolves to, so dropping it would regress those.fs.existsSyncguarding is the pattern already used elsewhere in this file, ingetDownloadedCoreToolsVersion()and at the top ofdownloadCoreTools(). Here it also covers packages built without the in process folders, without needing to know the architecture.EXECUTABLE_BINARIESsits with the other module constants, and uses forward slashes the same wayCORE_TOOLS_FOLDERdoes, since both are fed throughpath.join.Testing
The download path tests in
src/core/func-core-tools.spec.tswere inert: theadm-zipmock was commented out, and both tests that exercisedownloadCoreTools()wereit.skip. The only mention ofgozipin the file was inside the dead comment block.I replaced that commented block with a working
adm-zipmock. It writes a caller supplied set of files into memfs at the extraction destination usingvol.fromJSON, which leaves them at memfs's default mode of0o666, matching the real archives that carry no permission bits. That default is what makes the mode assertions meaningful rather than vacuous.Both previously skipped tests now work and are un skipped and passing. Three new tests cover the layouts:
should make the binaries executable when gozip sits at the package root(the pre 4.127.0 layout)should make the binaries executable when gozip sits in the in process host folders(the current layout, assertingfunc, bothin-proc*/funcand bothin-proc*/gozip)should skip the in process host binaries when the package does not ship them(a package with only a rootfunc, which must still complete)Result on the changed file:
The new tests fail without the source change
Reverting only
src/core/func-core-tools.tsand keeping the tests reproduces the reported crash:Dropping only
in-proc6/funcandin-proc8/funcfromEXECUTABLE_BINARIES, keeping everything else, fails the same test on the mode assertion withexpected 438 to be 493, that is0o666against0o755. The in process host coverage is therefore load bearing on its own.Full suite
npm testgoes from 488 passed / 14 skipped to 493 passed / 12 skipped, with no change in what fails.Two suites already fail on unmodified
mainin my environment, both from running Node 25 against a repo that targets Node 18 (.nvmrc), and both are untouched by this change:src/cli/index.spec.tsfails to load, viajsonwebtokentojwatobuffer-equal-constant-time, which readsSlowBuffer.prototype.SlowBufferwas removed in recent Node.funcCoreTools > getCoreToolsBinary > should return the system binary if it's compatibleexpectsisCoreToolsVersionCompatible(4, <node major>)to be true, but the table in this file caps v4 at Node 22, so Node 25 returns false.I confirmed both by stashing the change and rerunning: identical failure counts before and after. CI covers Node 18, 20 and 22 on Linux, macOS and Windows, which is where this should get its real check.
npx tsc --noEmitis clean andnpx prettier --checkpasses on both changed files.