Skip to content

Commit e420318

Browse files
committed
fix: validate android helper install args
1 parent 294a829 commit e420318

4 files changed

Lines changed: 41 additions & 3 deletions

File tree

android-snapshot-helper/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ unavailable, it falls back to the active-window root.
88

99
The helper is intentionally provider-neutral. Local `adb`, cloud ADB tunnels, and remote device
1010
providers can all install and run the same APK as long as they can execute ADB-style operations.
11+
Released helper APKs use the committed `debug.keystore`; do not rotate it casually, because Android
12+
requires a stable signing certificate for `adb install -r` upgrades.
1113

1214
## Build
1315

@@ -31,6 +33,9 @@ adb shell am instrument -w \
3133
```
3234

3335
`maxDepth` also caps recursive traversal depth inside the helper.
36+
The `-t` install flag is required because the helper is a debuggable instrumentation/test APK.
37+
Devices or providers that block test-package installs must allow this package before helper capture
38+
can run.
3439

3540
## Output Contract
3641

@@ -60,3 +65,8 @@ The final instrumentation result includes:
6065
- `elapsedMs`
6166

6267
Failures return `ok=false`, `errorType`, and `message` in the final result.
68+
69+
The release manifest is a stable provider contract for the current helper protocol. Providers should
70+
resolve the APK from `apkUrl`, verify `sha256`, install using `installArgs`, and run
71+
`instrumentationRunner`. `installArgs` must start with `install`; extra arguments are limited to adb
72+
install flags and the consumer appends the APK path.

android-snapshot-helper/src/main/java/com/callstack/agentdevice/snapshothelper/SnapshotInstrumentation.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ private static int appendInteractiveWindowRoots(
162162
if (root != null) {
163163
root.recycle();
164164
}
165+
// UiAutomation.getWindows() transfers recyclable AccessibilityWindowInfo instances.
165166
window.recycle();
166167
}
167168
}

src/platforms/android/__tests__/snapshot-helper.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,9 @@ test('parseAndroidSnapshotHelperManifest validates manifest shape', () => {
432432
assert.throws(() => parseAndroidSnapshotHelperManifest({ ...manifest, outputFormat: 'json' }), {
433433
message: 'Android snapshot helper manifest outputFormat must be "uiautomator-xml".',
434434
});
435+
assert.throws(() => parseAndroidSnapshotHelperManifest({ ...manifest, installArgs: ['shell'] }), {
436+
message: 'Android snapshot helper manifest installArgs must start with "install".',
437+
});
435438
});
436439

437440
function helperOutput(options: { chunks: string[]; result: Record<string, string> }): string {

src/platforms/android/snapshot-helper.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,10 @@ export async function ensureAndroidSnapshotHelper(options: {
154154
}
155155

156156
await verifyAndroidSnapshotHelperArtifact(artifact);
157-
const installArgs = [...artifact.manifest.installArgs, artifact.apkPath];
157+
const installArgs = [
158+
...readAndroidSnapshotHelperInstallArgs(artifact.manifest),
159+
artifact.apkPath,
160+
];
158161
const result = await installAndroidSnapshotHelper(adb, installArgs, {
159162
packageName,
160163
timeoutMs: options.timeoutMs,
@@ -182,7 +185,7 @@ export async function verifyAndroidSnapshotHelperArtifact(
182185
artifact: AndroidSnapshotHelperArtifact,
183186
): Promise<void> {
184187
const actual = await sha256File(artifact.apkPath);
185-
if (actual !== artifact.manifest.sha256.toLowerCase()) {
188+
if (actual !== artifact.manifest.sha256) {
186189
throw new AppError('COMMAND_FAILED', 'Android snapshot helper APK checksum mismatch', {
187190
apkPath: artifact.apkPath,
188191
expectedSha256: artifact.manifest.sha256,
@@ -503,10 +506,31 @@ export function parseAndroidSnapshotHelperManifest(value: unknown): AndroidSnaps
503506
'statusProtocol',
504507
'android-snapshot-helper-v1',
505508
),
506-
installArgs: readStringArray(record.installArgs, 'installArgs'),
509+
installArgs: readAndroidSnapshotHelperManifestInstallArgs(record.installArgs),
507510
};
508511
}
509512

513+
function readAndroidSnapshotHelperInstallArgs(manifest: AndroidSnapshotHelperManifest): string[] {
514+
return readAndroidSnapshotHelperManifestInstallArgs(manifest.installArgs);
515+
}
516+
517+
function readAndroidSnapshotHelperManifestInstallArgs(value: unknown): string[] {
518+
const installArgs = readStringArray(value, 'installArgs');
519+
if (installArgs[0] !== 'install') {
520+
throw new AppError(
521+
'INVALID_ARGS',
522+
'Android snapshot helper manifest installArgs must start with "install".',
523+
);
524+
}
525+
if (installArgs.some((arg) => arg.includes('\u0000'))) {
526+
throw new AppError(
527+
'INVALID_ARGS',
528+
'Android snapshot helper manifest installArgs must not contain null bytes.',
529+
);
530+
}
531+
return installArgs;
532+
}
533+
510534
async function readInstalledVersionCode(
511535
adb: AndroidAdbExecutor,
512536
packageName: string,

0 commit comments

Comments
 (0)