Files
ppsspp/Core/Util/KL4E.cpp
T
Henrik RydgårdandClaude Opus 5 917cb1b0fc Implement KL4E/KL3E decompression, so firmware modules that use it can load
KL4E is Sony's second compression scheme for ~PSP modules, alongside gzip:
LZ77 tokens where every bit is arithmetic-coded, structurally close to LZMA.
PPSSPP could detect it but not decode it, so any module packed with it failed
to load at all - the loader reached the gzip path and bailed there.

Which scheme a compressed module uses is now decided by the payload's own
magic rather than assuming gzip. On a 6.61 flash0 dump this takes the kd/
modules that load from 126 to 129 of 129; libmp3.prx, libaac.prx and
libmp4.prx were the ones affected, and libmp3.prx decompresses to exactly the
elf_size its PRX header declares.

Two bounds problems in the format are fixed rather than reproduced: the match
copy is unchecked against the output buffer on real hardware, so a crafted
stream can write up to 255 bytes past it, and a long enough distance code
indexes copyDistProbs out of range. Input reads are bounded too - the format
carries no length and trusts the stream to terminate itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 11:06:17 -06:00

420 lines
13 KiB
C++

// Copyright (c) 2026- PPSSPP Project.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0 or later versions.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
// KL4E / KL3E decompression. See docs/KL4E.md.
//
// LZ77 tokens - "emit this literal" or "repeat N bytes from M back" - where every bit is coded
// with an adaptive binary arithmetic coder rather than written directly. Structurally close to
// LZMA. KL3E differs from KL4E in exactly one constant (kPowLimitLong).
//
// Written from the format description, not ported from existing code.
#include <cstring>
#include "Common/Log.h"
#include "Core/HLE/ErrorCodes.h"
#include "Core/Util/KL4E.h"
namespace {
// Adaptation profiles. A probability is the chance the next bit is a 1, out of 256: on a 0 it
// just decays, on a 1 it decays and then gains the bonus back, so it climbs toward a ceiling of
// roughly (bonus << decay).
const int kDecayNormal = 3, kBonusNormal = 31;
const int kDecayFlag = 4, kBonusFlag = 15;
// Table sizes. Each is exactly large enough for the highest index its indexing scheme can
// produce - except copyDistProbs, see the bounds check in the distance decode.
const int kLitProbsSize = 2040; // 8 literal contexts * 255
const int kCopyCountBitsProbsSize = 64; // 8 states * 8 unary steps
const int kCopyCountProbsSize = 256;
const int kCopyDistBitsProbsSize = 304;
const int kCopyDistProbsSize = 144;
// The doubling walk that sizes a distance code stops once it reaches this. The single constant
// that separates the two formats.
const int kPowLimitKL4E = 256;
const int kPowLimitKL3E = 128;
// Short matches use a tighter limit in both formats.
const int kPowLimitShort = 64;
struct ArithDecoder {
const u8 *in;
const u8 *inEnd;
u32 range;
u32 code;
bool overrun;
u8 NextByte() {
if (in >= inEnd) {
// The format carries no length and relies on the stream terminating itself, so a
// truncated file would otherwise read forever. Feed zeroes and remember.
overrun = true;
return 0;
}
return *in++;
}
// One bit against an adaptive probability.
int ReadBit(u8 *prob, int decay, int bonus) {
u32 bound;
const u32 p = *prob;
if ((range >> 24) == 0) {
// Renormalize. The bound uses the range from *before* the shift.
code = (code << 8) + NextByte();
bound = range * p;
range <<= 8;
} else {
bound = (range >> 8) * p;
}
const u32 decayed = p - (p >> decay);
if (code >= bound) {
code -= bound;
range -= bound;
*prob = (u8)decayed;
return 0;
}
range = bound;
*prob = (u8)(decayed + bonus);
return 1;
}
// A bit the encoder judged incompressible: no probability, no adaptation.
int ReadBitUniform() {
if ((range >> 24) == 0) {
code = (code << 8) + NextByte();
range <<= 7;
} else {
range >>= 1;
}
if (code >= range) {
code -= range;
return 0;
}
return 1;
}
// Same, skipping the refill check. Only valid straight after a read that did refill, which
// is how runs of uniform bits are emitted.
int ReadBitUniformNoNorm() {
range >>= 1;
if (code >= range) {
code -= range;
return 0;
}
return 1;
}
// Eight bits into an accumulator that starts at 1, so the count is implicit. The context is
// a mix of the previous byte and the output position's alignment; `shift` slides the window
// between the two, the same choice LZMA's lc makes.
u32 ReadLiteral(u8 *litProbs, int outPos, u32 prevByte, int shift) {
const u32 ctx = ((u32)(outPos & 7) << 8) | (prevByte & 0xFF);
u8 *probs = litProbs + ((ctx >> shift) & 7) * 255 - 1;
u32 acc = 1;
while (acc < 0x100) {
acc = (acc << 1) | ReadBit(&probs[acc], kDecayNormal, kBonusNormal);
}
return acc & 0xFF;
}
};
} // namespace
bool IsKL4EMagic(const u8 *data, size_t size, bool *isKL3E) {
if (size < 4) {
return false;
}
if (!memcmp(data, "KL4E", 4)) {
if (isKL3E) {
*isKL3E = false;
}
return true;
}
if (!memcmp(data, "KL3E", 4)) {
if (isKL3E) {
*isKL3E = true;
}
return true;
}
return false;
}
int DecompressKL4E(u8 *out, int outSize, const u8 *in, size_t inSize, const u8 **end, bool isKL3E) {
if (!out || !in || outSize <= 0 || inSize < 5) {
return SCE_KERNEL_ERROR_INVALID_FORMAT;
}
// --- five-byte stream header ---
const u8 flags = in[0];
// Big-endian, which almost nothing else on the PSP is.
const u32 headerWord = ((u32)in[1] << 24) | ((u32)in[2] << 16) | ((u32)in[3] << 8) | in[4];
if (flags & 0x80) {
// Stored, not compressed: the header word is a length and the bytes follow verbatim.
// A payload that exactly fills the output buffer is rejected, not accepted.
if (headerWord >= (u32)outSize) {
return SCE_KERNEL_ERROR_INVALID_SIZE;
}
if (5 + (size_t)headerWord > inSize) {
return SCE_KERNEL_ERROR_INVALID_FORMAT;
}
memcpy(out, in + 5, headerWord);
if (end) {
*end = in + 5 + headerWord;
}
return (int)headerWord;
}
// bits 4-3 pick the starting probability, bits 2-0 the literal context selector.
const u8 seed = (u8)(0x80 - (((flags >> 3) & 3) << 4));
const int shift = flags & 7;
u8 litProbs[kLitProbsSize];
u8 copyCountBitsProbs[kCopyCountBitsProbsSize];
u8 copyCountProbs[kCopyCountProbsSize];
u8 copyDistBitsProbs[kCopyDistBitsProbsSize];
u8 copyDistProbs[kCopyDistProbsSize];
memset(litProbs, seed, sizeof(litProbs));
memset(copyCountBitsProbs, seed, sizeof(copyCountBitsProbs));
memset(copyCountProbs, seed, sizeof(copyCountProbs));
memset(copyDistBitsProbs, seed, sizeof(copyDistBitsProbs));
memset(copyDistProbs, seed, sizeof(copyDistProbs));
ArithDecoder dec;
dec.in = in + 5;
dec.inEnd = in + inSize;
dec.range = 0xFFFFFFFF;
dec.code = headerWord;
dec.overrun = false;
const int powLimitLong = isKL3E ? kPowLimitKL3E : kPowLimitKL4E;
// outPos indexes the byte being produced. The loop head advances it, which is why a match
// only advances it by copyCount after writing copyCount + 1 bytes.
int outPos = 0;
u32 prevByte = 0;
// An index into copyCountBitsProbs that persists across tokens - the "state". The unary walk
// below moves it in steps of 8 and does not put it back, so its low three bits are what the
// length code's context uses.
int countBitsIdx = 0;
// The first literal has no flag bit in front of it.
prevByte = dec.ReadLiteral(litProbs, outPos, prevByte, shift);
out[0] = (u8)prevByte;
while (true) {
outPos++;
if (dec.overrun) {
return SCE_KERNEL_ERROR_INVALID_FORMAT;
}
if (dec.ReadBit(&copyCountBitsProbs[countBitsIdx], kDecayFlag, kBonusFlag) == 0) {
// Literal.
countBitsIdx = countBitsIdx > 0 ? countBitsIdx - 1 : 0;
if (outPos >= outSize) {
return SCE_KERNEL_ERROR_INVALID_SIZE;
}
prevByte = dec.ReadLiteral(litProbs, outPos, prevByte, shift);
out[outPos] = (u8)prevByte;
continue;
}
// A match. First, how many bits the length code uses, as a unary run stepping the index
// by 8 so the low three bits stay put.
u32 copyCount = 1;
int copyCountBits = -1;
while (copyCountBits < 6) {
countBitsIdx += 8;
if (countBitsIdx >= kCopyCountBitsProbsSize) {
return SCE_KERNEL_ERROR_INVALID_FORMAT;
}
if (!dec.ReadBit(&copyCountBitsProbs[countBitsIdx], kDecayFlag, kBonusFlag)) {
break;
}
copyCountBits++;
}
// The length, and with it the distance code's parameters: the bit that lands in the
// length's low position also selects them, for short matches.
int powLimit = kPowLimitShort;
int distBase = copyCountBits;
if (copyCountBits >= 0) {
const int offset = (copyCountBits << 5)
| (((outPos & 3) << (copyCountBits + 3)) & 0x18)
| (countBitsIdx & 7);
u8 *probs = copyCountProbs + offset;
// High bits: two adaptive, then uniform for anything longer.
if (copyCountBits >= 3) {
copyCount = 2 + dec.ReadBit(probs + 24, kDecayNormal, kBonusNormal);
if (copyCountBits > 3) {
copyCount = (copyCount << 1) | dec.ReadBit(probs + 24, kDecayNormal, kBonusNormal);
if (copyCountBits > 4) {
copyCount = (copyCount << 1) | dec.ReadBitUniform();
}
for (int i = 5; i < copyCountBits; i++) {
copyCount = (copyCount << 1) | dec.ReadBitUniformNoNorm();
}
}
}
copyCount <<= 1;
if (dec.ReadBit(probs, kDecayNormal, kBonusNormal)) {
copyCount |= 1;
if (copyCountBits <= 0) {
powLimit = powLimitLong;
distBase = 56 + copyCountBits;
}
} else if (copyCountBits <= 0) {
powLimit = kPowLimitShort;
distBase = copyCountBits;
}
if (copyCountBits > 0) {
copyCount = (copyCount << 1) | dec.ReadBit(probs + 8, kDecayNormal, kBonusNormal);
if (copyCountBits != 1) {
copyCount <<= 1;
if (dec.ReadBit(probs + 16, kDecayNormal, kBonusNormal)) {
copyCount++;
// 0xFF ends the stream. There is no length in the header and no
// terminator byte; this is the only normal way out of the loop.
if (copyCount == 0xFF) {
if (end) {
*end = dec.in;
}
return outPos;
}
}
}
powLimit = powLimitLong;
distBase = 56 + copyCountBits;
}
}
// How many bits the distance code uses, by a doubling walk. curPow only picks up the
// extra 8 when the walk continues, which is what makes the deepest reachable index fit
// copyDistBitsProbs exactly.
u32 copyDist = 0;
bool distanceIsZero = false;
int curPow = 8;
int copyDistBits = 0;
while (true) {
const int probIndex = distBase + curPow - 7;
if (probIndex < 0 || probIndex >= kCopyDistBitsProbsSize) {
return SCE_KERNEL_ERROR_INVALID_FORMAT;
}
u8 *prob = &copyDistBitsProbs[probIndex];
curPow <<= 1;
copyDistBits = curPow - powLimit;
if (!dec.ReadBit(prob, kDecayNormal, kBonusNormal)) {
if (copyDistBits >= 0) {
if (copyDistBits != 0) {
copyDistBits -= 8;
break;
}
// No distance bits at all: a match against the byte just emitted, i.e. a run.
distanceIsZero = true;
break;
}
} else {
curPow += 8;
if (copyDistBits >= 0) {
break;
}
}
}
if (!distanceIsZero) {
// The distance value. Same shape as the length: two adaptive high bits, uniform in
// the middle, then three adaptive low bits whose +1/-1 adjustments keep the code
// ranges contiguous across bit counts.
if (copyDistBits < 0 || copyDistBits + 3 >= kCopyDistProbsSize) {
// Only reachable from a stream encoding a distance far larger than any real
// module needs - the hardware routine reads past its own table here.
return SCE_KERNEL_ERROR_INVALID_FORMAT;
}
u8 *probs = copyDistProbs + copyDistBits;
int readBits = copyDistBits / 8;
if (readBits < 3) {
copyDist = 1;
} else {
copyDist = 2 + dec.ReadBit(probs + 3, kDecayNormal, kBonusNormal);
if (readBits > 3) {
copyDist = (copyDist << 1) | dec.ReadBit(probs + 3, kDecayNormal, kBonusNormal);
if (readBits > 4) {
copyDist = (copyDist << 1) | dec.ReadBitUniform();
readBits--;
}
while (readBits > 4) {
copyDist = (copyDist << 1) + dec.ReadBitUniformNoNorm();
readBits--;
}
}
}
copyDist <<= 1;
if (dec.ReadBit(probs, kDecayNormal, kBonusNormal)) {
if (readBits > 0) {
copyDist++;
}
} else if (readBits <= 0) {
copyDist--;
}
if (readBits > 0) {
copyDist <<= 1;
if (dec.ReadBit(probs + 1, kDecayNormal, kBonusNormal)) {
if (readBits != 1) {
copyDist++;
}
} else if (readBits == 1) {
copyDist--;
}
if (readBits != 1) {
copyDist <<= 1;
if (!dec.ReadBit(probs + 2, kDecayNormal, kBonusNormal)) {
copyDist--;
}
}
}
if (copyDist >= (u32)outPos) {
return SCE_KERNEL_ERROR_INVALID_FORMAT;
}
}
const int copyLen = (int)copyCount + 1;
// The hardware routine bounds-checks only the literal path, so a crafted stream can
// write up to 255 bytes past the end of the output buffer here. Check it.
if (copyLen > outSize - outPos) {
return SCE_KERNEL_ERROR_INVALID_SIZE;
}
// Forward, byte at a time - overlap is legal and is how runs are encoded.
const u8 *src = out + outPos - copyDist - 1;
for (int i = 0; i < copyLen; i++) {
out[outPos + i] = src[i];
}
prevByte = out[outPos + (int)copyCount];
outPos += (int)copyCount;
countBitsIdx = 6 + (outPos & 1);
}
}