From: OpenImprenta spike Subject: [PATCH] Clamp Escher container and block lengths to the stream (32-bit long) parseEscherContainer() trusts the 32-bit length read from the file. Callers then do input->seek(info.contentsOffset + info.contentsLength, ...), and RVNGInputStream::seek() takes a `long`. Where long is 32 bits (WebAssembly, any ILP32 target) a large bogus length makes that offset negative: RVNGStringStream::seek() then resets the position to 0, and parseEscherDelay() starts over from the beginning of the stream, inflating the same pictures again on every pass. On x86-64 the same value is a valid positive offset past EOF, so the loop simply ends. parseBlock() has the same problem through skipBlock(): the block parsers re-read the same blocks forever. Seen with truncated/mutated copies of Apache POI's SampleNewsletter.pub: native x86-64 finishes in ~0.2 s, the wasm build took 18-24 s (1.3 GB of memory) or did not finish within 120 s. --- a/src/lib/MSPUBParser.cpp 2026-10-01 15:16:17.806402700 -0300 +++ b/src/lib/MSPUBParser.cpp 2026-10-01 15:16:17.807401500 -0300 @@ -2507,6 +2507,16 @@ info.type = readU16(input); info.contentsLength = readU32(input); info.contentsOffset = input->tell(); + // Keep contentsOffset + contentsLength inside the stream. Callers seek to + // that sum through RVNGInputStream::seek(long): where long is 32 bits + // (WebAssembly, 32-bit builds) a bogus length wraps it negative, the + // stream rewinds to 0 and loops like parseEscherDelay() start over again + // and again (minutes, GBs of memory). On LP64 the same file just hits EOF. + const unsigned long streamLength = getLength(input); + if (info.contentsOffset > streamLength) + info.contentsLength = 0; + else if (info.contentsLength > streamLength - info.contentsOffset) + info.contentsLength = streamLength - info.contentsOffset; MSPUB_DEBUG_MSG(("Parsed escher container: type 0x%x, contentsOffset 0x%lx, contentsLength 0x%lx\n", info.type, info.contentsOffset, info.contentsLength)); return info; } @@ -2523,6 +2533,13 @@ if (varLen) { info.dataLength = readU32(input); + // Same as in parseEscherContainer(): skipBlock() seeks to + // dataOffset + dataLength, which must not wrap a 32-bit long. + const unsigned long streamLength = getLength(input); + if (info.dataOffset > streamLength) + info.dataLength = 0; + else if (info.dataLength > streamLength - info.dataOffset) + info.dataLength = streamLength - info.dataOffset; if (isBlockDataString(info.type)) { info.stringData = std::vector();