[GH-ISSUE #5924] [Bug] ADB push claims a checksum source when no device digest was parsed #3599

Closed
opened 2026-10-31 00:42:16 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 21, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/5924

Originally assigned to: @kaoru0822-kitauji on GitHub.

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Evidence, artifacts, or reconstruction

Observed behavior

After a successful push_adb_file transfer, a device checksum command that exits zero with unrecognized stdout produces successful Evidence with device_sha256: null but device_digest_source: "device_sha256sum". The limitations omit the warning that the transfer is not device-side verified. In contrast, a checksum command that exits nonzero returns null with source "unavailable" and that warning.

This was executed through AdbDeviceAnalysisService with the production AdbProvider and a simulated adb executable. Both empty successful stdout and a checksum line prefixed with a backslash reproduced the inconsistent receipt. No Android device or emulator was contacted.

Reproduction

In an installed source checkout at the revision below, save the following focused test as src/adb/pushDigestReceipt.test.ts, then run node_modules/.bin/vitest run src/adb/pushDigestReceipt.test.ts --maxWorkers 1 --reporter verbose. The executable simulation follows the pattern used by the existing ADB provider tests. It copies the actual staged input locally and controls only adb's responses. Remove the test after running it.

import { createHash } from "node:crypto";
import { mkdtemp, writeFile, rm, readFile } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { expect, test } from "vitest";
import { AdbProvider } from "./AdbProvider.js";
import { AdbDeviceAnalysisService } from "../application/adb/AdbDeviceAnalysisService.js";

test.each(["valid", "empty", "escaped", "missing"] as const)(
  "push checksum receipt: %s",
  async (mode) => {
    const root = await mkdtemp(join(process.env.REA_PROBE_ROOT ?? tmpdir(), "adb-digest-"));
    const binary = join(root, "adb");
    const source = join(root, "payload.bin");
    const copied = join(root, "copied.bin");
    const digest = createHash("sha256").update("push-payload").digest("hex");
    const provider = new AdbProvider({ environment: { REA_ADB_PATH: binary } });
    try {
      await writeFile(source, "push-payload");
      const stdout = mode === "valid" ? `${digest}  /data/local/payload.bin\n`
        : mode === "escaped" ? `\\${digest}  /data/local/payload\\\\name.bin\n` : "";
      await writeFile(join(root, "checksum-output"), stdout);
      await writeFile(binary, `#!/bin/sh
if [ "$1" = "version" ]; then
  printf 'Android Debug Bridge version 1.0.41\\nVersion 34.0.5-stub\\nInstalled as %s\\n' "$0"
  exit 0
fi
if [ "$1" = "-s" ] && [ "$4" = "test" ]; then exit 1; fi
if [ "$1" = "-s" ] && [ "$3" = "push" ]; then cp "$4" '${copied}'; exit 0; fi
if [ "$1" = "-s" ] && [ "$4" = "sha256sum" ]; then
  cat '${join(root, "checksum-output")}'
  exit ${mode === "missing" ? 127 : 0}
fi
exit 99
`, { mode: 0o755 });
      const response = await new AdbDeviceAnalysisService(provider).execute("push_adb_file", {
        serial: "emulator-5554", local_path: source,
        device_path: mode === "escaped" ? "/data/local/payload\\name.bin" : "/data/local/payload.bin",
        overwrite: false,
      });
      expect(response.ok).toBe(true);
      if (!response.ok) throw new Error(response.error.message);
      expect(await readFile(copied, "utf8")).toBe("push-payload");
      const result = response.value.normalized_result as Record<string, unknown>;
      const limitations = response.value.limitations;
      console.log(JSON.stringify({ mode, device_sha256: result.device_sha256,
        device_digest_source: result.device_digest_source, limitations }));
      expect(result.sha256).toBe(digest);
      expect(result.device_sha256).toBe(mode === "valid" ? digest : null);
      expect(result.device_digest_source).toBe(mode === "valid" ? "device_sha256sum" : "unavailable");
      expect(limitations.some((line) => line.includes("not device-side verified"))).toBe(mode !== "valid");
    } finally {
      await provider.close();
      await rm(root, { recursive: true, force: true });
    }
  },
);

The expected-contract assertions fail for empty and escaped; valid and missing pass. Both simulated successful-but-unparseable cases reproduced in two observation runs. This tests the shared application/provider boundary, including request validation and Evidence construction, rather than the CLI or MCP transport.

Expected behavior

When no device digest can be parsed, preserve the completed push but explicitly report that device-side verification is unavailable, or return an appropriate typed failure retaining the completed transfer. A successful command exit alone does not establish that a checksum was obtained or compared. Keep valid matching checksums verified and the missing-command control explicitly unverified.

The tool description promises verification by comparing the device checksum with the local digest. The ADB guide describes the same behavior. The existing unavailable-checksum branch supplies the expected null/unavailable receipt and verification limitation.

Impact and scope

P2: callers receive misleading checksum provenance and lose the explicit verification limitation for successful but unparseable responses. The digest remains visibly null; this report does not establish that callers were actually misled or that transferred bytes were corrupted. Ordinary matching checksum output works. Callers can conservatively treat any null device digest as unverified regardless of its source field, which distinguishes both affected cases in the reproduction.

The executed scope is macOS with simulated adb output. Actual device frequency and particular Android checksum implementations are unknown. No security boundary crossing was observed.

REA version

5241597594; package manifest version 6.7.0.

Runtime and platform

macOS 27.0.1 arm64; Node.js v26.12.0; Vitest 5.0.3. Simulated adb identity: Android Debug Bridge 1.0.41, 34.0.5-stub.

Target and artifact context

Synthetic serial emulator-5554; local payload push-payload; synthetic absolute device destinations. The simulated push copies the private input snapshot into a local stand-in. The test checks the copied bytes and reported host digest. The escaped-output case supplies a backslash-prefixed checksum line; it does not claim that output was observed from an Android checksum implementation.

Interface

Other

Earliest failure stage

Transport or result projection

Relevant output or evidence

Observed projections from the test:

{"mode":"valid","device_sha256":"8748b5d17d1ebcec7d46674fa6a8b92c0f95598a552f27c1c8a132eb58c04043","device_digest_source":"device_sha256sum"}
{"mode":"empty","device_sha256":null,"device_digest_source":"device_sha256sum"}
{"mode":"escaped","device_sha256":null,"device_digest_source":"device_sha256sum"}
{"mode":"missing","device_sha256":null,"device_digest_source":"unavailable"}

All four service requests succeeded and copied push-payload. The empty and escaped cases had only Artifact identity is unavailable for this observation. and Push writes a private snapshot of the caller's file to the device as limitations. The missing-command control additionally reported The device did not answer sha256sum, so the transfer is not device-side verified.

The contract-checking run finished with 2 failed | 2 passed (4). Both failures were expected 'device_sha256sum' to be 'unavailable'.

Source-derived public reachability: CLI push command and MCP registration both expose this shared workflow. Those transports were not separately executed.

Typed outcome or recovery expected

For a completed transfer with no usable checksum, return device_sha256: null, device_digest_source: "unavailable", and an explicit unverified-transfer limitation, or an appropriate typed failure that preserves the completed transfer. Keep valid matching checksum responses unchanged.

Analysis or suspected cause

Source-derived: the parsing and receipt construction derive the digest from an optional regex match but hard-code its source to device_sha256sum. With no match, comparison is skipped. The source and warning should reflect whether a digest was actually obtained, independently of process exit status.

The result schema currently permits a null device digest with either source, so schema validation does not detect this contradiction.

Additional context

Related reports #3369 and PR #3458 concern capture overflow and resource failures. This reproduction uses small responses that exit successfully, so fixing their capture budget or resource-error handling does not fix this parsing outcome. Report #4252 concerns host input identity, and #4574 concerns the earlier existence probe; neither covers this receipt inconsistency.

After fetching upstream main at bcfcd8b6e2ee0c44c7ac656688db5ceb61a847d5, the relevant provider, service, CLI/MCP, schema, contract, and documentation files were unchanged from the tested baseline. No fix was applied or tested.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5924 Originally assigned to: @kaoru0822-kitauji on GitHub. ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Evidence, artifacts, or reconstruction ### Observed behavior After a successful `push_adb_file` transfer, a device checksum command that exits zero with unrecognized stdout produces successful Evidence with `device_sha256: null` but `device_digest_source: "device_sha256sum"`. The limitations omit the warning that the transfer is not device-side verified. In contrast, a checksum command that exits nonzero returns null with source `"unavailable"` and that warning. This was executed through `AdbDeviceAnalysisService` with the production `AdbProvider` and a simulated adb executable. Both empty successful stdout and a checksum line prefixed with a backslash reproduced the inconsistent receipt. No Android device or emulator was contacted. ### Reproduction In an installed source checkout at the revision below, save the following focused test as `src/adb/pushDigestReceipt.test.ts`, then run `node_modules/.bin/vitest run src/adb/pushDigestReceipt.test.ts --maxWorkers 1 --reporter verbose`. The executable simulation follows the pattern used by the existing ADB provider tests. It copies the actual staged input locally and controls only adb's responses. Remove the test after running it. ```ts import { createHash } from "node:crypto"; import { mkdtemp, writeFile, rm, readFile } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { expect, test } from "vitest"; import { AdbProvider } from "./AdbProvider.js"; import { AdbDeviceAnalysisService } from "../application/adb/AdbDeviceAnalysisService.js"; test.each(["valid", "empty", "escaped", "missing"] as const)( "push checksum receipt: %s", async (mode) => { const root = await mkdtemp(join(process.env.REA_PROBE_ROOT ?? tmpdir(), "adb-digest-")); const binary = join(root, "adb"); const source = join(root, "payload.bin"); const copied = join(root, "copied.bin"); const digest = createHash("sha256").update("push-payload").digest("hex"); const provider = new AdbProvider({ environment: { REA_ADB_PATH: binary } }); try { await writeFile(source, "push-payload"); const stdout = mode === "valid" ? `${digest} /data/local/payload.bin\n` : mode === "escaped" ? `\\${digest} /data/local/payload\\\\name.bin\n` : ""; await writeFile(join(root, "checksum-output"), stdout); await writeFile(binary, `#!/bin/sh if [ "$1" = "version" ]; then printf 'Android Debug Bridge version 1.0.41\\nVersion 34.0.5-stub\\nInstalled as %s\\n' "$0" exit 0 fi if [ "$1" = "-s" ] && [ "$4" = "test" ]; then exit 1; fi if [ "$1" = "-s" ] && [ "$3" = "push" ]; then cp "$4" '${copied}'; exit 0; fi if [ "$1" = "-s" ] && [ "$4" = "sha256sum" ]; then cat '${join(root, "checksum-output")}' exit ${mode === "missing" ? 127 : 0} fi exit 99 `, { mode: 0o755 }); const response = await new AdbDeviceAnalysisService(provider).execute("push_adb_file", { serial: "emulator-5554", local_path: source, device_path: mode === "escaped" ? "/data/local/payload\\name.bin" : "/data/local/payload.bin", overwrite: false, }); expect(response.ok).toBe(true); if (!response.ok) throw new Error(response.error.message); expect(await readFile(copied, "utf8")).toBe("push-payload"); const result = response.value.normalized_result as Record<string, unknown>; const limitations = response.value.limitations; console.log(JSON.stringify({ mode, device_sha256: result.device_sha256, device_digest_source: result.device_digest_source, limitations })); expect(result.sha256).toBe(digest); expect(result.device_sha256).toBe(mode === "valid" ? digest : null); expect(result.device_digest_source).toBe(mode === "valid" ? "device_sha256sum" : "unavailable"); expect(limitations.some((line) => line.includes("not device-side verified"))).toBe(mode !== "valid"); } finally { await provider.close(); await rm(root, { recursive: true, force: true }); } }, ); ``` The expected-contract assertions fail for `empty` and `escaped`; `valid` and `missing` pass. Both simulated successful-but-unparseable cases reproduced in two observation runs. This tests the shared application/provider boundary, including request validation and Evidence construction, rather than the CLI or MCP transport. ### Expected behavior When no device digest can be parsed, preserve the completed push but explicitly report that device-side verification is unavailable, or return an appropriate typed failure retaining the completed transfer. A successful command exit alone does not establish that a checksum was obtained or compared. Keep valid matching checksums verified and the missing-command control explicitly unverified. The [tool description](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/contracts/adb/adbToolContracts.ts#L127-L133) promises verification by comparing the device checksum with the local digest. The [ADB guide](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/docs/adb-device-analysis.md#L167-L175) describes the same behavior. The [existing unavailable-checksum branch](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/adb/AdbProvider.ts#L2484-L2491) supplies the expected null/unavailable receipt and verification limitation. ### Impact and scope P2: callers receive misleading checksum provenance and lose the explicit verification limitation for successful but unparseable responses. The digest remains visibly null; this report does not establish that callers were actually misled or that transferred bytes were corrupted. Ordinary matching checksum output works. Callers can conservatively treat any null device digest as unverified regardless of its source field, which distinguishes both affected cases in the reproduction. The executed scope is macOS with simulated adb output. Actual device frequency and particular Android checksum implementations are unknown. No security boundary crossing was observed. ### REA version 52415975942a4b9c53a817915b313ccc404f280e; package manifest version 6.7.0. ### Runtime and platform macOS 27.0.1 arm64; Node.js v26.12.0; Vitest 5.0.3. Simulated adb identity: Android Debug Bridge 1.0.41, 34.0.5-stub. ### Target and artifact context Synthetic serial `emulator-5554`; local payload `push-payload`; synthetic absolute device destinations. The simulated push copies the private input snapshot into a local stand-in. The test checks the copied bytes and reported host digest. The escaped-output case supplies a backslash-prefixed checksum line; it does not claim that output was observed from an Android checksum implementation. ### Interface Other ### Earliest failure stage Transport or result projection ### Relevant output or evidence Observed projections from the test: ```jsonl {"mode":"valid","device_sha256":"8748b5d17d1ebcec7d46674fa6a8b92c0f95598a552f27c1c8a132eb58c04043","device_digest_source":"device_sha256sum"} {"mode":"empty","device_sha256":null,"device_digest_source":"device_sha256sum"} {"mode":"escaped","device_sha256":null,"device_digest_source":"device_sha256sum"} {"mode":"missing","device_sha256":null,"device_digest_source":"unavailable"} ``` All four service requests succeeded and copied `push-payload`. The empty and escaped cases had only `Artifact identity is unavailable for this observation.` and `Push writes a private snapshot of the caller's file to the device` as limitations. The missing-command control additionally reported `The device did not answer sha256sum, so the transfer is not device-side verified`. The contract-checking run finished with `2 failed | 2 passed (4)`. Both failures were `expected 'device_sha256sum' to be 'unavailable'`. Source-derived public reachability: [CLI push command](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/cli/adbCommands.ts#L158-L178) and [MCP registration](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/server/registerAdbTools.ts#L25-L35) both expose this shared workflow. Those transports were not separately executed. ### Typed outcome or recovery expected For a completed transfer with no usable checksum, return `device_sha256: null`, `device_digest_source: "unavailable"`, and an explicit unverified-transfer limitation, or an appropriate typed failure that preserves the completed transfer. Keep valid matching checksum responses unchanged. ### Analysis or suspected cause Source-derived: [the parsing and receipt construction](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/adb/AdbProvider.ts#L2510-L2514) derive the digest from an optional regex match but hard-code its source to `device_sha256sum`. With no match, comparison is skipped. The source and warning should reflect whether a digest was actually obtained, independently of process exit status. The [result schema](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/adb/adbDeviceAnalysis.ts#L598-L611) currently permits a null device digest with either source, so schema validation does not detect this contradiction. ### Additional context Related reports #3369 and PR #3458 concern capture overflow and resource failures. This reproduction uses small responses that exit successfully, so fixing their capture budget or resource-error handling does not fix this parsing outcome. Report #4252 concerns host input identity, and #4574 concerns the earlier existence probe; neither covers this receipt inconsistency. After fetching upstream main at `bcfcd8b6e2ee0c44c7ac656688db5ceb61a847d5`, the relevant provider, service, CLI/MCP, schema, contract, and documentation files were unchanged from the tested baseline. No fix was applied or tested.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#3599
No description provided.