{"record":{"id":"b18919f00c6eb3dd","repo":"Tencent/tinker","slug":"local-file-header-offset-is-after-central-director","errorCode":null,"errorMessage":"Local file header offset is after central directory","messagePattern":"Local file header offset is after central directory","errorType":"exception","errorClass":"ZipException","httpStatus":null,"severity":"error","filePath":"third-party/tinker-ziputils/src/main/java/com/tencent/tinker/ziputils/ziputil/TinkerZipFile.java","lineNumber":484,"sourceCode":"\n        if (commentLength > 0) {\n            byte[] commentBytes = new byte[commentLength];\n            raf.readFully(commentBytes);\n            comment = new String(commentBytes, 0, commentBytes.length, StandardCharsets.UTF_8);\n        }\n\n        // Seek to the first CDE and read all entries.\n        // We have to do this now (from the constructor) rather than lazily because the\n        // public API doesn't allow us to throw IOException except from the constructor\n        // or from getInputStream.\n        RAFStream rafStream = new RAFStream(raf, centralDirOffset);\n        BufferedInputStream bufferedStream = new BufferedInputStream(rafStream, 4096);\n        byte[] hdrBuf = new byte[CENHDR]; // Reuse the same buffer for each entry.\n        for (int i = 0; i < numEntries; ++i) {\n            TinkerZipEntry newEntry = new TinkerZipEntry(hdrBuf, bufferedStream, StandardCharsets.UTF_8,\n                (false) /* isZip64 */);\n            if (newEntry.localHeaderRelOffset >= centralDirOffset) {\n                throw new ZipException(\"Local file header offset is after central directory\");\n            }\n            String entryName = newEntry.getName();\n            if (entries.put(entryName, newEntry) != null) {\n                throw new ZipException(\"Duplicate entry name: \" + entryName);\n            }\n        }\n\n    }\n\n    // private final CloseGuard guard = CloseGuard.get();\n    static class EocdRecord {\n        final long numEntries;\n        final long centralDirOffset;\n        final int commentLength;\n        EocdRecord(long numEntries, long centralDirOffset, int commentLength) {\n            this.numEntries = numEntries;\n            this.centralDirOffset = centralDirOffset;\n            this.commentLength = commentLength;","sourceCodeStart":466,"sourceCodeEnd":502,"githubUrl":"https://github.com/Tencent/tinker/blob/1b7ea02c239840f563ea64fb5bd286eb98d4011e/third-party/tinker-ziputils/src/main/java/com/tencent/tinker/ziputils/ziputil/TinkerZipFile.java#L466-L502","documentation":"While reading the central directory in the constructor, TinkerZipFile sanity-checks each entry's localHeaderRelOffset against centralDirOffset: a local file header must live before the central directory in a well-formed zip. An offset >= centralDirOffset means the archive is internally inconsistent (offsets were rewritten without fixing the central directory, or the CD was moved), so it throws ZipException('Local file header offset is after central directory') instead of reading garbage later.","triggerScenarios":"Opening a zip whose central-directory entries carry stale/inflated local header offsets — typically after a tool deleted or inserted entries and rewrote only part of the metadata, after zip64->zip32 conversion bugs, or after naive binary splicing of two archives.","commonSituations":"APK/patch post-processing steps that strip entries (signature removal, dex pruning) but corrupt offsets; archives edited by buggy repackagers; hand-rolled zip merge tools.","solutions":["Rebuild the archive from its contents with a trusted tool (unzip to a temp dir, re-zip) so all offsets are regenerated consistently.","Identify which upstream step rewrote the file (repackager, obfuscator, signer) and fix or update it.","Run 'unzip -t' / 'zip -T' on inputs before processing to catch inconsistent archives early."],"exampleFix":"// before\nTinkerZipFile zf = new TinkerZipFile(editedApk); // offsets corrupted by a repack tool\n\n// after\nFile rebuilt = rebuildArchive(editedApk); // unzip to tmp, re-zip with a standard tool\nTinkerZipFile zf = new TinkerZipFile(rebuilt);","handlingStrategy":"try-catch","validationCode":"// full pre-validation is costly; rely on independent verification instead:\n// unzip -t returns nonzero on inconsistent archives — run it on untrusted inputs\nProcess p = Runtime.getRuntime().exec(new String[]{\"unzip\", \"-t\", file.getPath()});\nif (p.waitFor() != 0) {\n    throw new IOException(\"Archive fails integrity check: \" + file);\n}","typeGuard":null,"tryCatchPattern":"try {\n    new TinkerZipFile(file);\n} catch (ZipException e) {\n    if (e.getMessage().contains(\"after central directory\")) {\n        throw new IOException(\"Archive has inconsistent offsets (bad repack?): \" + file, e);\n    }\n    throw e;\n}","preventionTips":["Do not strip/insert zip entries with hand-rolled offset math — use a mature zip library.","Run 'unzip -t' on archives produced by custom build tooling before shipping.","After any repackaging step, regenerate the central directory from scratch."],"tags":["zip","corruption","offset","tinker"],"backgroundTag":null,"analyzedSha":"1b7ea02c239840f563ea64fb5bd286eb98d4011e","analyzedAt":"2026-08-14T15:16:52.110Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}