[GH-ISSUE #7435] [Bug] ADB screen capture succeeds with zero dimensions outside its output contract #4442

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

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

Before submitting

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

Area

Process capture or runtime observation

Observed behavior

When a caller-selected adb executable returns a PNG header declaring width and height zero, capture-adb-screen emulator-5554 --output-directory <directory> --json succeeds and publishes the returned bytes as screen.png. I reproduced this through the production source CLI: the process exited 0 and emitted capture Evidence with width: 0, height: 0, and bytes: 33. Applying the operation's canonical result schema to that emitted normalized_result rejected both dimensions. The fixture has the PNG signature, correct IHDR length/type, and a correct IHDR checksum; it deliberately does not contain a valid PNG image.

Reproduction

Use Node.js 24.22.1 and a fresh clone at commit f8f7ac96de8a66a4ab9a23d87e04779bfcd11045. Install its locked dependencies with npm ci before running the following from the repository root. Dependencies were already installed in the verification checkout. These commands use the repository's source loader and production CLI factory, so no compiled runtime build is needed. No Android device or adb server is used.

work="$(mktemp -d)"
export REA_VERIFY_WORK="$work"
mkdir -p "$work/home" "$work/shots"

node --input-type=module <<'JS'
import { writeFileSync } from 'node:fs';
import { crc32 } from 'node:zlib';
const frame = Buffer.alloc(33);
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]).copy(frame);
frame.writeUInt32BE(13, 8);
frame.write('IHDR', 12);
frame[24] = 8;
frame[25] = 6;
frame.writeUInt32BE(crc32(frame.subarray(12, 29)), 29);
writeFileSync(process.env.REA_VERIFY_WORK + '/frame.png', frame);
JS

cat > "$work/adb" <<'SH'
#!/bin/sh
if [ "$1" = "version" ]; then
  printf 'Android Debug Bridge version 1.0.41\nVersion 34.0.5-fixture\nInstalled as %s\n' "$0"
  exit 0
fi
if [ "$1" = "-s" ]; then shift 2; fi
if [ "$1" = "exec-out" ] && [ "$2" = "screencap" ] && [ "$3" = "-p" ]; then
  cat "$(dirname "$0")/frame.png"
  exit 0
fi
exit 1
SH
chmod +x "$work/adb"

HOME="$work/home" REA_ADB_PATH="$work/adb" node --input-type=module > "$work/result.json" <<'JS'
import { pathToFileURL } from 'node:url';
import { importVerifierSource } from './scripts/lib/verifier-source-loader.mjs';
const { createCli } = await importVerifierSource(
  './src/cli.ts', pathToFileURL(process.cwd() + '/'),
);
await createCli().serve([
  'capture-adb-screen', 'emulator-5554',
  '--output-directory', process.env.REA_VERIFY_WORK + '/shots', '--json',
]);
JS
printf 'CLI exit=%s\n' "$?"

node --input-type=module <<'JS'
import { readFileSync } from 'node:fs';
import { pathToFileURL } from 'node:url';
import { importVerifierSource } from './scripts/lib/verifier-source-loader.mjs';
const { adbResultSchemas } = await importVerifierSource(
  './src/domain/adb/adbDeviceAnalysis.ts', pathToFileURL(process.cwd() + '/'),
);
const root = process.env.REA_VERIFY_WORK;
const result = JSON.parse(readFileSync(root + '/result.json', 'utf8')).normalized_result;
const validation = adbResultSchemas.capture_adb_screen.safeParse(result);
const published = readFileSync(root + '/shots/screen.png');
console.log(JSON.stringify({
  width: result.width, height: result.height, bytes: result.bytes,
  identical: published.equals(readFileSync(root + '/frame.png')),
  schemaSuccess: validation.success,
  invalidFields: validation.success ? [] : validation.error.issues.map(issue => issue.path),
}, null, 2));
JS

Two captures were independently reproduced, including execution of this reproduction block. The source loader can print Node's experimental TypeScript transformation warning on stderr; the result remains JSON on stdout.

Expected behavior

A successful capture must satisfy its own dimension contract: each non-null width and height must be a positive integer. Expected source: src/domain/adb/adbDeviceAnalysis.ts:673; the named tool binds that schema at src/contracts/adb/adbToolContracts.ts:152. The documented capture operation returns a screen frame with dimensions, as described at docs/adb-device-analysis.md:97. For the supplied zero-dimension header, reject the malformed capture through the existing protocol-error path before publishing a successful capture, rather than emit forbidden zero values.

Impact and scope

P2: a bounded incorrect success result publishes unusable capture bytes and violates the operation's declared output contract. The reproduced result cannot pass canonical schema validation, so downstream consumers cannot rely on a successful capture satisfying that schema. This was tested with a synthetic caller-selected adb wrapper; no naturally corrupted response from a real device, frequency in ordinary use, or particular MCP client's handling was established. A normal positive-dimension screenshot is outside this reproduction. The caller can discard the bad artifact and obtain a new capture after correcting the producer; automatic recovery was not tested.

REA version

6.8.0, baseline commit f8f7ac96de8a66a4ab9a23d87e04779bfcd11045.

Runtime and platform

Node.js v24.22.1; Darwin 27.0.0; arm64.

Target and artifact context

Synthetic caller-selected adb executable reporting version 34.0.5-fixture. Its exec-out screencap -p response is a 33-byte zero-dimension IHDR fixture with a correct checksum. No emulator or physical device was contacted.

Interface

CLI

Earliest failure stage

Capture or observation

Relevant output or evidence

The source CLI returned exit 0. Independent inspection of its result and published file produced these selected facts:

{
  "width": 0,
  "height": 0,
  "bytes": 33,
  "identical": true,
  "schemaSuccess": false,
  "invalidFields": [["width"], ["height"]]
}

The published file's SHA-256 was ce4ec43db147bfd6a47476ff8289368e62cba8dbdc1912894b02e48e9d2f593b. The canonical validator returned too_small for each dimension with the message Too small: expected number to be >0. The wrapper's recorded calls were version and -s emulator-5554 exec-out screencap -p.

Failure source: src/adb/AdbShellOutput.ts:624 admits the signature and 24-byte minimum, then reads dimensions without a positivity check. src/adb/AdbProvider.ts:3107 rejects only a null width and subsequently publishes the capture. src/cli/adbCommands.ts:194 reaches that path through the shared service. The reproduction establishes source CLI behavior, not execution of the compiled dispatcher or an MCP transport.

Typed outcome or recovery expected

Return the existing malformed-screencap protocol error without publishing screen.png. Successful output must not contain zero dimensions.

Additional context

Observed through the source CLI with a wrapper standing in for adb; no physical device or MCP client was tested. Related, distinct: #4251 (closed) concerned ADB screen capture publishing after cancellation.

Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7435 ### 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 Process capture or runtime observation ### Observed behavior When a caller-selected adb executable returns a PNG header declaring width and height zero, `capture-adb-screen emulator-5554 --output-directory <directory> --json` succeeds and publishes the returned bytes as `screen.png`. I reproduced this through the production source CLI: the process exited 0 and emitted capture Evidence with `width: 0`, `height: 0`, and `bytes: 33`. Applying the operation's canonical result schema to that emitted `normalized_result` rejected both dimensions. The fixture has the PNG signature, correct IHDR length/type, and a correct IHDR checksum; it deliberately does not contain a valid PNG image. ### Reproduction Use Node.js 24.22.1 and a fresh clone at commit `f8f7ac96de8a66a4ab9a23d87e04779bfcd11045`. Install its locked dependencies with `npm ci` before running the following from the repository root. Dependencies were already installed in the verification checkout. These commands use the repository's source loader and production CLI factory, so no compiled runtime build is needed. No Android device or adb server is used. ```sh work="$(mktemp -d)" export REA_VERIFY_WORK="$work" mkdir -p "$work/home" "$work/shots" node --input-type=module <<'JS' import { writeFileSync } from 'node:fs'; import { crc32 } from 'node:zlib'; const frame = Buffer.alloc(33); Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]).copy(frame); frame.writeUInt32BE(13, 8); frame.write('IHDR', 12); frame[24] = 8; frame[25] = 6; frame.writeUInt32BE(crc32(frame.subarray(12, 29)), 29); writeFileSync(process.env.REA_VERIFY_WORK + '/frame.png', frame); JS cat > "$work/adb" <<'SH' #!/bin/sh if [ "$1" = "version" ]; then printf 'Android Debug Bridge version 1.0.41\nVersion 34.0.5-fixture\nInstalled as %s\n' "$0" exit 0 fi if [ "$1" = "-s" ]; then shift 2; fi if [ "$1" = "exec-out" ] && [ "$2" = "screencap" ] && [ "$3" = "-p" ]; then cat "$(dirname "$0")/frame.png" exit 0 fi exit 1 SH chmod +x "$work/adb" HOME="$work/home" REA_ADB_PATH="$work/adb" node --input-type=module > "$work/result.json" <<'JS' import { pathToFileURL } from 'node:url'; import { importVerifierSource } from './scripts/lib/verifier-source-loader.mjs'; const { createCli } = await importVerifierSource( './src/cli.ts', pathToFileURL(process.cwd() + '/'), ); await createCli().serve([ 'capture-adb-screen', 'emulator-5554', '--output-directory', process.env.REA_VERIFY_WORK + '/shots', '--json', ]); JS printf 'CLI exit=%s\n' "$?" node --input-type=module <<'JS' import { readFileSync } from 'node:fs'; import { pathToFileURL } from 'node:url'; import { importVerifierSource } from './scripts/lib/verifier-source-loader.mjs'; const { adbResultSchemas } = await importVerifierSource( './src/domain/adb/adbDeviceAnalysis.ts', pathToFileURL(process.cwd() + '/'), ); const root = process.env.REA_VERIFY_WORK; const result = JSON.parse(readFileSync(root + '/result.json', 'utf8')).normalized_result; const validation = adbResultSchemas.capture_adb_screen.safeParse(result); const published = readFileSync(root + '/shots/screen.png'); console.log(JSON.stringify({ width: result.width, height: result.height, bytes: result.bytes, identical: published.equals(readFileSync(root + '/frame.png')), schemaSuccess: validation.success, invalidFields: validation.success ? [] : validation.error.issues.map(issue => issue.path), }, null, 2)); JS ``` Two captures were independently reproduced, including execution of this reproduction block. The source loader can print Node's experimental TypeScript transformation warning on stderr; the result remains JSON on stdout. ### Expected behavior A successful capture must satisfy its own dimension contract: each non-null width and height must be a positive integer. Expected source: `src/domain/adb/adbDeviceAnalysis.ts:673`; the named tool binds that schema at `src/contracts/adb/adbToolContracts.ts:152`. The documented capture operation returns a screen frame with dimensions, as described at `docs/adb-device-analysis.md:97`. For the supplied zero-dimension header, reject the malformed capture through the existing protocol-error path before publishing a successful capture, rather than emit forbidden zero values. ### Impact and scope P2: a bounded incorrect success result publishes unusable capture bytes and violates the operation's declared output contract. The reproduced result cannot pass canonical schema validation, so downstream consumers cannot rely on a successful capture satisfying that schema. This was tested with a synthetic caller-selected adb wrapper; no naturally corrupted response from a real device, frequency in ordinary use, or particular MCP client's handling was established. A normal positive-dimension screenshot is outside this reproduction. The caller can discard the bad artifact and obtain a new capture after correcting the producer; automatic recovery was not tested. ### REA version 6.8.0, baseline commit `f8f7ac96de8a66a4ab9a23d87e04779bfcd11045`. ### Runtime and platform Node.js v24.22.1; Darwin 27.0.0; arm64. ### Target and artifact context Synthetic caller-selected adb executable reporting version 34.0.5-fixture. Its `exec-out screencap -p` response is a 33-byte zero-dimension IHDR fixture with a correct checksum. No emulator or physical device was contacted. ### Interface CLI ### Earliest failure stage Capture or observation ### Relevant output or evidence The source CLI returned exit 0. Independent inspection of its result and published file produced these selected facts: ```json { "width": 0, "height": 0, "bytes": 33, "identical": true, "schemaSuccess": false, "invalidFields": [["width"], ["height"]] } ``` The published file's SHA-256 was `ce4ec43db147bfd6a47476ff8289368e62cba8dbdc1912894b02e48e9d2f593b`. The canonical validator returned `too_small` for each dimension with the message `Too small: expected number to be >0`. The wrapper's recorded calls were `version` and `-s emulator-5554 exec-out screencap -p`. Failure source: `src/adb/AdbShellOutput.ts:624` admits the signature and 24-byte minimum, then reads dimensions without a positivity check. `src/adb/AdbProvider.ts:3107` rejects only a null width and subsequently publishes the capture. `src/cli/adbCommands.ts:194` reaches that path through the shared service. The reproduction establishes source CLI behavior, not execution of the compiled dispatcher or an MCP transport. ### Typed outcome or recovery expected Return the existing malformed-screencap protocol error without publishing `screen.png`. Successful output must not contain zero dimensions. ### Additional context Observed through the source CLI with a wrapper standing in for adb; no physical device or MCP client was tested. Related, distinct: #4251 (closed) concerned ADB screen capture publishing after cancellation.
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#4442
No description provided.