fix(apktool): treat an EPERM launcher spawn as not executable

A launcher whose exec is refused by an operating-system security policy
surfaces EPERM, not EACCES. It was reported as a missing launcher, so
the recovery pointed at installing apktool instead of fixing execute
permission. EPERM now joins EACCES as command_not_executable.

Squashed from #5180 and rebased onto main.

Supersedes #5180 (follow-up to #5177)

Co-Authored-By: gensokyo-dev <gensokyo-dev@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and Ysopwnable committed 2026-11-14 10:21:04 +01:00
1 parent 90c70ba770
commit 4bf5226724
2 files changed
+13 -5

No files matched your search

+12 -5
View File
@@ -522,17 +522,23 @@ const versionProbeFailure = (
...apktoolVersionProbeTimeoutGuidance(),
},
);
// EPERM is the other errno a refused exec can surface (for example a launcher
// blocked by an operating-system security policy), so it joins EACCES as a
// permission failure, not a missing launcher.
if (
cause instanceof Error &&
"code" in cause &&
(cause.code === "ENOENT" || cause.code === "EACCES")
)
(cause.code === "ENOENT" ||
cause.code === "EACCES" ||
cause.code === "EPERM")
) {
const execDenied = cause.code === "EACCES" || cause.code === "EPERM";
return new ApktoolConfigurationFailure(
cause.code === "EACCES" ? "command_not_executable" : "command_missing",
cause.code === "EACCES"
execDenied ? "command_not_executable" : "command_missing",
execDenied
? `Execution of the selected apktool launcher was denied: ${selection.command}`
: `The selected apktool launcher was not found: ${selection.command}`,
cause.code === "EACCES"
execDenied
? APKTOOL_NOT_EXECUTABLE_REMEDIATION
: APKTOOL_MISSING_REMEDIATION,
{
@@ -542,6 +548,7 @@ const versionProbeFailure = (
},
{ cause },
);
}
const snapshot = cause instanceof OwnedCommandFailure ? cause.snapshot : null;
if (cause instanceof OwnedCommandFailure && cause.reason === "output-limit")
return new AnalysisResourceConstraintError(
+1
View File
@@ -201,6 +201,7 @@ it.each(["linux", "win32"] as const)(
it.each([
["EACCES", "command_not_executable"],
["EPERM", "command_not_executable"],
["ENOENT", "command_missing"],
])("distinguishes launcher spawn %s", async (code, expected) => {
vi.mocked(runOwnedCommand).mockRejectedValueOnce(