Tencent/tinker · error · IOException

Bad oat magic: %x %x %x %x

Error message

Bad oat magic: %x %x %x %x

What it means

ShareOatUtil.readOatInstructionSet reads 8 bytes at the start of the .rodata section header offset of an ELF file and validates the OAT magic bytes 'o','a','t','\n'. If those four bytes do not spell the OAT magic, the file at that offset is not a valid OAT/odex container, so an IOException with the hex dump of the offending bytes is thrown. This is a hard integrity check: Tinker refuses to derive an instruction set from a file that is not a real oat file.

Source

Thrown at tinker-android/tinker-android-loader-no-op/src/main/java/com/tencent/tinker/loader/shareutil/ShareOatUtil.java:68

        String result = "";
        try {
            elfFile = new ShareElfFile(oatFile);
            final ShareElfFile.SectionHeader roDataHdr = elfFile.getSectionHeaderByName(".rodata");
            if (roDataHdr == null) {
                throw new IOException("Unable to find .rodata section.");
            }

            final FileChannel channel = elfFile.getChannel();
            channel.position(roDataHdr.shOffset);

            final byte[] oatMagicAndVersion = new byte[8];
            ShareElfFile.readUntilLimit(channel, ByteBuffer.wrap(oatMagicAndVersion), "Failed to read oat magic and version.");

            if (oatMagicAndVersion[0] != 'o'
                    || oatMagicAndVersion[1] != 'a'
                    || oatMagicAndVersion[2] != 't'
                    || oatMagicAndVersion[3] != '\n') {
                throw new IOException(
                        String.format("Bad oat magic: %x %x %x %x",
                                oatMagicAndVersion[0],
                                oatMagicAndVersion[1],
                                oatMagicAndVersion[2],
                                oatMagicAndVersion[3])
                );
            }

            final int versionOffsetFromOatBegin = 4;
            final int versionBytes = 3;

            final String oatVersion = new String(oatMagicAndVersion,
                    versionOffsetFromOatBegin, versionBytes, Charset.forName("ASCII"));
            try {
                Integer.parseInt(oatVersion);
            } catch (NumberFormatException e) {
                throw new IOException("Bad oat version: " + oatVersion);
            }

View on GitHub (pinned to 1b7ea02c23)

Solutions

  1. Verify the odex file is a real OAT file before parsing: check SharePatchFileUtil.isLegalFile(file) and that the ELF header/section parse succeeded.
  2. Delete the stale odex and re-trigger dex2oat (retry patch install or call TinkerDexOptimizer again) so a fresh odex is generated on-device.
  3. If the device consistently produces invalid oat files (known vendor bypass), skip the oat check path that this code guards (the code itself notes it is 'fine to skip the check' for such devices).
  4. Confirm the patch package was built with a compatible tinker version and the correct ABI so dex2oat emits a matching oat.

Example fix

// before
String isa = ShareOatUtil.readOatInstructionSet(oatFile); // throws on stub odex

// after
if (SharePatchFileUtil.isLegalFile(oatFile)) {
    String isa = null;
    try {
        isa = ShareOatUtil.readOatInstructionSet(oatFile);
    } catch (IOException e) {
        ShareTinkerLog.w(TAG, "bad oat, delete and re-optimize: " + oatFile, e);
        oatFile.delete();
    }
}
Defensive patterns

Strategy: try-catch

Validate before calling

File oat = new File(oatDir, name + ".odex");
if (!oat.exists() || oat.length() < 8) {
    // no valid oat to parse; trigger re-optimization instead
}

Try / catch

try { isa = ShareOatUtil.readOatInstructionSet(oatFile); } catch (IOException e) { oatFile.delete(); /* regenerate odex */ }

Prevention

When it happens

Trigger: Calling the oat-parsing path (e.g. ShareTinkerInternals/SharePatchFileUtil flows that read the odex generated for a patch dex) with: a truncated or corrupt odex; an ELF that is a plain dex/zip instead of an oat; a file whose section header table (roDataHdr.shOffset) pointed past the real data due to a mismatched ELF parse; a zero-length or placeholder odex written by a device that skipped dex2oat.

Common situations: Vendor ROMs (vivo/oppo-class, per the code comments) that bypass dex2oat and emit nothing or a stub file; patch packages rebuilt with a different dex2oat than the one on device; OTA updates changing the ART/oat format; manually copying an odex from another build.

Related errors


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