fix(adb): report an unparsed push checksum as unverified (#6010)
fix(adb): derive push digest source from the parsed device digest A zero-exit sha256sum whose output held no recognizable digest produced device_sha256: null with device_digest_source: "device_sha256sum" and dropped the not-device-side-verified limitation. The push receipt now takes one tagged device digest, so the source can only name sha256sum when a digest was parsed; unrecognized output reports "unavailable", the unverified limitation, and the raw command output. The sha256sum parser moves to AdbShellOutput and accepts GNU's escaped-name prefix. Fixes #5924 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2319166c2e
commit
1f92c77398
4 files changed
+136
-40
No files matched your search
@@ -172,7 +172,9 @@ and during shutdown.
|
||||
transfer even with `overwrite`; only a clean `test -e` exit of 1 with no
|
||||
output is interpreted as absence. The check is not atomic, so a different
|
||||
process can create the path before `adb push` writes it. When available, the device's
|
||||
`sha256sum` verifies the transfer; installs preserve the
|
||||
`sha256sum` verifies the transfer. A missing command or output without a
|
||||
recognizable digest reports `device_digest_source: "unavailable"` with an
|
||||
unverified-transfer limitation; installs preserve the
|
||||
device's `Failure [...]` reason verbatim.
|
||||
- **Producer identity stays attached.** Every result embeds the producing
|
||||
adb client (`binary_path`, `path_source`, `version`, `installed_path`).
|
||||
|
||||
+69
-25
@@ -1049,34 +1049,78 @@ exit 0`,
|
||||
},
|
||||
);
|
||||
|
||||
it("pushes a file and verifies the device-side digest", async () => {
|
||||
const { binary, root } = await writeAdbStub(
|
||||
() => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
|
||||
const PUSHED_DIGEST =
|
||||
"8748b5d17d1ebcec7d46674fa6a8b92c0f95598a552f27c1c8a132eb58c04043";
|
||||
|
||||
it.each([
|
||||
{
|
||||
answer: "a digest line",
|
||||
stdout: `${PUSHED_DIGEST} /data/local/tmp/payload.bin\n`,
|
||||
exit: 0,
|
||||
verified: true,
|
||||
},
|
||||
{
|
||||
// GNU sha256sum prefixes a line whose echoed name it escaped.
|
||||
answer: "an escaped digest line",
|
||||
stdout: `\\${PUSHED_DIGEST} /data/local/tmp/pay\\\\load.bin\n`,
|
||||
exit: 0,
|
||||
verified: true,
|
||||
},
|
||||
{ answer: "empty output", stdout: "", exit: 0, verified: false },
|
||||
{
|
||||
answer: "a malformed line",
|
||||
stdout: `${PUSHED_DIGEST.slice(1)} /data/local/tmp/payload.bin\n`,
|
||||
exit: 0,
|
||||
verified: false,
|
||||
},
|
||||
{ answer: "a failed command", stdout: "", exit: 127, verified: false },
|
||||
])(
|
||||
"derives the push digest source from $answer",
|
||||
async ({ stdout, exit, verified }) => {
|
||||
const { binary, root } = await writeAdbStub(
|
||||
(stubRoot) => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
|
||||
if [ "$1" = "-s" ] && [ "$4" = "test" ]; then exit 1; fi
|
||||
if [ "$1" = "-s" ] && [ "$3" = "push" ]; then exit 0; fi
|
||||
if [ "$1" = "-s" ] && [ "$4" = "sha256sum" ]; then printf '%s %s\\n' "8748b5d17d1ebcec7d46674fa6a8b92c0f95598a552f27c1c8a132eb58c04043" "$5"; exit 0; fi
|
||||
if [ "$1" = "-s" ] && [ "$4" = "sha256sum" ]; then cat "${join(stubRoot, "sha256sum.out")}"; exit ${exit}; fi
|
||||
exit 1`,
|
||||
);
|
||||
const localFile = join(root, "payload.bin");
|
||||
await writeFile(localFile, "push-payload");
|
||||
const digest = createHash("sha256").update("push-payload").digest("hex");
|
||||
const pushed = await evidenceResult(providerOver(binary), {
|
||||
operation: "push_adb_file",
|
||||
input: {
|
||||
serial: "emulator-5554",
|
||||
local_path: localFile,
|
||||
device_path: "/data/local/tmp/payload.bin",
|
||||
overwrite: false,
|
||||
},
|
||||
});
|
||||
expect(pushed).toMatchObject({
|
||||
bytes: 12,
|
||||
sha256: digest,
|
||||
device_sha256: digest,
|
||||
overwritten: false,
|
||||
device_digest_source: "device_sha256sum",
|
||||
});
|
||||
});
|
||||
);
|
||||
await writeFile(join(root, "sha256sum.out"), stdout);
|
||||
const localFile = join(root, "payload.bin");
|
||||
await writeFile(localFile, "push-payload");
|
||||
const result = await providerOver(binary).execute({
|
||||
operation: "push_adb_file",
|
||||
input: {
|
||||
serial: "emulator-5554",
|
||||
local_path: localFile,
|
||||
device_path: "/data/local/tmp/payload.bin",
|
||||
overwrite: false,
|
||||
},
|
||||
});
|
||||
if (!result.ok) throw new Error(result.error.message);
|
||||
expect(result.value.result).toMatchObject({
|
||||
bytes: 12,
|
||||
sha256: PUSHED_DIGEST,
|
||||
overwritten: false,
|
||||
...(verified
|
||||
? {
|
||||
device_sha256: PUSHED_DIGEST,
|
||||
device_digest_source: "device_sha256sum",
|
||||
}
|
||||
: { device_sha256: null, device_digest_source: "unavailable" }),
|
||||
});
|
||||
expect(
|
||||
result.value.limitations.some((line) =>
|
||||
line.includes("not device-side verified"),
|
||||
),
|
||||
).toBe(!verified);
|
||||
// Unrecognized checksum output stays inspectable instead of being dropped.
|
||||
expect(result.value.rawResult).toEqual(
|
||||
!verified && exit === 0
|
||||
? { device_sha256sum: { stdout, stderr: "" } }
|
||||
: null,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("pushes the same private bytes that its reported digest identifies", async () => {
|
||||
const { binary, root } = await writeAdbStub(
|
||||
|
||||
+56
-14
@@ -117,6 +117,7 @@ import {
|
||||
parseResolveActivityOutput,
|
||||
parseServiceListOutput,
|
||||
parseSettingsGetOutput,
|
||||
parseSha256sumOutput,
|
||||
parseWindowFocusOutput,
|
||||
parseWmDensityOutput,
|
||||
parseWmSizeOutput,
|
||||
@@ -272,6 +273,21 @@ const adbDigestError = (
|
||||
|
||||
type AdbFileDigest = Awaited<ReturnType<typeof hashStableFile>>;
|
||||
|
||||
/**
|
||||
* A pushed file's device-side digest together with its source, so a receipt
|
||||
* cannot name `sha256sum` as the source of a digest that was never parsed.
|
||||
*/
|
||||
type AdbDeviceDigest =
|
||||
| {
|
||||
readonly device_sha256: string;
|
||||
readonly device_digest_source: "device_sha256sum";
|
||||
}
|
||||
| {
|
||||
readonly device_sha256: null;
|
||||
readonly device_digest_source: "unavailable";
|
||||
readonly reason: string;
|
||||
};
|
||||
|
||||
const partialObservationWithDigest = (
|
||||
observation:
|
||||
| AdbAcquiredFilePartialObservation
|
||||
@@ -2436,9 +2452,13 @@ export class AdbProvider {
|
||||
);
|
||||
if (!pushed.ok) return pushed;
|
||||
const receipt = (
|
||||
deviceSha256: string | null,
|
||||
source: "device_sha256sum" | "unavailable",
|
||||
extra: readonly string[] = [],
|
||||
deviceDigest: AdbDeviceDigest,
|
||||
rawResult?: {
|
||||
readonly device_sha256sum: {
|
||||
readonly stdout: string;
|
||||
readonly stderr: string;
|
||||
};
|
||||
},
|
||||
) =>
|
||||
createAnalysisExecution(
|
||||
{
|
||||
@@ -2448,15 +2468,18 @@ export class AdbProvider {
|
||||
device_path: input.device_path,
|
||||
bytes: digest.bytes,
|
||||
sha256: digest.sha256,
|
||||
device_sha256: deviceSha256,
|
||||
device_sha256: deviceDigest.device_sha256,
|
||||
overwritten: deviceExists,
|
||||
device_digest_source: source,
|
||||
device_digest_source: deviceDigest.device_digest_source,
|
||||
},
|
||||
provider,
|
||||
{
|
||||
...(rawResult === undefined ? {} : { rawResult }),
|
||||
limitations: [
|
||||
"Push writes a private snapshot of the caller's file to the device",
|
||||
...extra,
|
||||
...(deviceDigest.device_digest_source === "unavailable"
|
||||
? [deviceDigest.reason]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
);
|
||||
@@ -2471,11 +2494,13 @@ export class AdbProvider {
|
||||
if (!deviceDigest.ok) {
|
||||
const cancelled =
|
||||
deviceDigest.error instanceof AnalysisCancelledError;
|
||||
const completed = receipt(null, "unavailable", [
|
||||
cancelled
|
||||
const completed = receipt({
|
||||
device_sha256: null,
|
||||
device_digest_source: "unavailable",
|
||||
reason: cancelled
|
||||
? "The device-side digest check was cancelled after push completed"
|
||||
: "The device did not answer sha256sum, so the transfer is not device-side verified",
|
||||
]);
|
||||
});
|
||||
if (cancelled)
|
||||
deviceDigest.error.retainPartialObservation(
|
||||
createEvidence(
|
||||
@@ -2494,11 +2519,28 @@ export class AdbProvider {
|
||||
);
|
||||
return cancelled ? err(deviceDigest.error) : ok(completed);
|
||||
}
|
||||
const digestMatch = /^([a-f0-9]{64})\s/u.exec(
|
||||
deviceDigest.value.stdout,
|
||||
);
|
||||
const completed = receipt(digestMatch?.[1] ?? null, "device_sha256sum");
|
||||
if (digestMatch !== null && digestMatch[1] !== digest.sha256)
|
||||
const parsedDigest = parseSha256sumOutput(deviceDigest.value.stdout);
|
||||
const completed =
|
||||
parsedDigest === null
|
||||
? receipt(
|
||||
{
|
||||
device_sha256: null,
|
||||
device_digest_source: "unavailable",
|
||||
reason:
|
||||
"The device's sha256sum output contained no recognizable digest, so the transfer is not device-side verified",
|
||||
},
|
||||
{
|
||||
device_sha256sum: {
|
||||
stdout: deviceDigest.value.stdout,
|
||||
stderr: deviceDigest.value.stderr,
|
||||
},
|
||||
},
|
||||
)
|
||||
: receipt({
|
||||
device_sha256: parsedDigest,
|
||||
device_digest_source: "device_sha256sum",
|
||||
});
|
||||
if (parsedDigest !== null && parsedDigest !== digest.sha256)
|
||||
return err(
|
||||
new AnalysisArtifactChangedError(
|
||||
operation,
|
||||
|
||||
@@ -381,6 +381,14 @@ export const parseBugreportCopiedPath = (stdout: string): string | null => {
|
||||
return match === null ? null : match[1]!;
|
||||
};
|
||||
|
||||
/**
|
||||
* Read the digest from one `sha256sum FILE` line, or null when the output
|
||||
* names none. GNU sha256sum starts the line with a backslash when it escaped
|
||||
* the echoed name; the digest itself is never escaped.
|
||||
*/
|
||||
export const parseSha256sumOutput = (stdout: string): string | null =>
|
||||
/^\\?([a-f0-9]{64}) [ *]/u.exec(stdout)?.[1] ?? null;
|
||||
|
||||
/** Read the device's `Failure [REASON]` verdict from package-change output. */
|
||||
export const packageChangeFailureReason = (output: string): string | null =>
|
||||
/Failure \[([^\]]+)\]/u.exec(output)?.[1] ?? null;
|
||||
|
||||
Reference in new issue
Block a user