Tencent/tinker · critical · IOException

old dex signature mismatch! expected: %s, actual: %s

Error message

old dex signature mismatch! expected: %s, actual: %s

What it means

IOException from DexPatchApplier.executeAndSaveTo: the SHA-1 signature computed over the old dex does not match the old-dex signature stored in the patch file at generation time. This is the applier's core safety check — the patch was diffed against different bytes than the dex being patched.

Source

Thrown at tinker-commons/src/main/java/com/tencent/tinker/commons/dexpatcher/DexPatchApplier.java:135

        // old dex we passed in.
        if (this.patchFile == null) {
            throw new IllegalArgumentException("patch file is null.");
        }
        if (this.patchFile.getVersion() > DexPatchFile.VERSION_02) {
            final int oldDexAPI = this.oldDex.getTableOfContents().api;
            final int expectedOldDexAPI = this.patchFile.getOldDexAPI();
            if (oldDexAPI != expectedOldDexAPI) {
                throw new IOException("old dex version mismatch! expetced: "
                        + expectedOldDexAPI + ", actual: " + oldDexAPI);
            }
        }
        byte[] oldDexSign = this.oldDex.computeSignature(false);
        if (oldDexSign == null) {
            throw new IOException("failed to compute old dex's signature.");
        }
        byte[] oldDexSignInPatchFile = this.patchFile.getOldDexSignature();
        if (CompareUtils.uArrCompare(oldDexSign, oldDexSignInPatchFile) != 0) {
            throw new IOException(
                    String.format(
                            "old dex signature mismatch! expected: %s, actual: %s",
                            Arrays.toString(oldDexSign),
                            Arrays.toString(oldDexSignInPatchFile)
                    )
            );
        }

        // Firstly, set sections' offset after patched, sort according to their offset so that
        // the dex lib of aosp can calculate section size.
        TableOfContents patchedToc = this.patchedDex.getTableOfContents();

        patchedToc.api = patchFile.getPatchedDexAPI();
        patchedToc.header.off = 0;
        patchedToc.header.size = 1;
        patchedToc.mapList.size = 1;

        patchedToc.stringIds.off

View on GitHub (pinned to 1b7ea02c23)

Solutions

  1. Regenerate the patch from the exact base apk currently installed/distributed
  2. Verify channel/build variants match between diff time and apply time
  3. Add an automated base-apk hash check in CI before releasing patches
Defensive patterns

Strategy: validation

Validate before calling

byte[] actual = oldDex.computeSignature(false);
if (actual == null || CompareUtils.uArrCompare(actual, patchFile.getOldDexSignature()) != 0) {
    throw new IOException("base dex differs from patch target; regenerate patch");
}

Try / catch

try {
    applier.executeAndSaveTo(out);
} catch (IOException e) {
    if (String.valueOf(e.getMessage()).startsWith("old dex signature mismatch")) {
        // halt rollout: installed base differs from patch base
    } else { throw e; }
}

Prevention

When it happens

Trigger: Applying a dex patch to any old dex whose content differs by even one byte from the dex used by tinker-patch when the patch was produced.

Common situations: Base apk re-built (even trivially) after patch generation; channels appending data to the dex; wrong base apk version in a multi-flavor setup; mixing debug and release artifacts.

Related errors


AI-assisted analysis of Tencent/tinker@1b7ea02c23 (2026-08-14). Data as JSON: /api/errors/06c4b552e96d4612. Report an issue: GitHub.