## Summary * **What is the goal of this PR?** Potential stack buffer overflow from untrusted ZIP entry name length * **What changes are included?** If nameLen >= 256 , this writes past the stack buffer. Risk: memory corruption/crash on malformed EPUB/ZIP. ## Additional Context * Add any other information that might be helpful for the reviewer (e.g., performance implications, potential risks, specific areas to focus on). --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _** PARTIALLY **_ Issue identified by AI
636 lines
16 KiB
C++
636 lines
16 KiB
C++
#include "ZipFile.h"
|
|
|
|
#include <HalStorage.h>
|
|
#include <InflateReader.h>
|
|
#include <Logging.h>
|
|
|
|
#include <algorithm>
|
|
|
|
struct ZipInflateCtx {
|
|
InflateReader reader; // Must be first — callback casts uzlib_uncomp* to ZipInflateCtx*
|
|
FsFile* file = nullptr;
|
|
size_t fileRemaining = 0;
|
|
uint8_t* readBuf = nullptr;
|
|
size_t readBufSize = 0;
|
|
};
|
|
|
|
namespace {
|
|
constexpr uint16_t ZIP_METHOD_STORED = 0;
|
|
constexpr uint16_t ZIP_METHOD_DEFLATED = 8;
|
|
|
|
int zipReadCallback(uzlib_uncomp* uncomp) {
|
|
auto* ctx = reinterpret_cast<ZipInflateCtx*>(uncomp);
|
|
if (ctx->fileRemaining == 0) return -1;
|
|
|
|
const size_t toRead = ctx->fileRemaining < ctx->readBufSize ? ctx->fileRemaining : ctx->readBufSize;
|
|
const size_t bytesRead = ctx->file->read(ctx->readBuf, toRead);
|
|
ctx->fileRemaining -= bytesRead;
|
|
|
|
if (bytesRead == 0) return -1;
|
|
|
|
uncomp->source = ctx->readBuf + 1;
|
|
uncomp->source_limit = ctx->readBuf + bytesRead;
|
|
return ctx->readBuf[0];
|
|
}
|
|
} // namespace
|
|
|
|
bool ZipFile::loadAllFileStatSlims() {
|
|
const bool wasOpen = isOpen();
|
|
if (!wasOpen && !open()) {
|
|
return false;
|
|
}
|
|
|
|
if (!loadZipDetails()) {
|
|
if (!wasOpen) {
|
|
close();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
file.seek(zipDetails.centralDirOffset);
|
|
|
|
uint32_t sig;
|
|
char itemName[256];
|
|
fileStatSlimCache.clear();
|
|
fileStatSlimCache.reserve(zipDetails.totalEntries);
|
|
|
|
while (file.available()) {
|
|
file.read(&sig, 4);
|
|
if (sig != 0x02014b50) break; // End of list
|
|
|
|
FileStatSlim fileStat = {};
|
|
|
|
file.seekCur(6);
|
|
file.read(&fileStat.method, 2);
|
|
file.seekCur(8);
|
|
file.read(&fileStat.compressedSize, 4);
|
|
file.read(&fileStat.uncompressedSize, 4);
|
|
uint16_t nameLen, m, k;
|
|
file.read(&nameLen, 2);
|
|
file.read(&m, 2);
|
|
file.read(&k, 2);
|
|
file.seekCur(8);
|
|
file.read(&fileStat.localHeaderOffset, 4);
|
|
|
|
if (nameLen < sizeof(itemName)) {
|
|
file.read(itemName, nameLen);
|
|
itemName[nameLen] = '\0';
|
|
fileStatSlimCache.emplace(itemName, fileStat);
|
|
} else {
|
|
// Skip over oversized entry names to avoid writing past fixed buffer.
|
|
file.seekCur(nameLen);
|
|
}
|
|
|
|
// Skip the rest of this entry (extra field + comment)
|
|
file.seekCur(m + k);
|
|
}
|
|
|
|
// Set cursor to start of central directory for sequential access
|
|
lastCentralDirPos = zipDetails.centralDirOffset;
|
|
lastCentralDirPosValid = true;
|
|
|
|
if (!wasOpen) {
|
|
close();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool ZipFile::loadFileStatSlim(const char* filename, FileStatSlim* fileStat) {
|
|
if (!fileStatSlimCache.empty()) {
|
|
const auto it = fileStatSlimCache.find(filename);
|
|
if (it != fileStatSlimCache.end()) {
|
|
*fileStat = it->second;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const bool wasOpen = isOpen();
|
|
if (!wasOpen && !open()) {
|
|
return false;
|
|
}
|
|
|
|
if (!loadZipDetails()) {
|
|
if (!wasOpen) {
|
|
close();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Phase 1: Try scanning from cursor position first
|
|
uint32_t startPos = lastCentralDirPosValid ? lastCentralDirPos : zipDetails.centralDirOffset;
|
|
bool wrapped = false;
|
|
bool found = false;
|
|
|
|
file.seek(startPos);
|
|
|
|
uint32_t sig;
|
|
char itemName[256];
|
|
|
|
while (true) {
|
|
uint32_t entryStart = file.position();
|
|
|
|
if (file.read(&sig, 4) != 4 || sig != 0x02014b50) {
|
|
// End of central directory
|
|
if (!wrapped && lastCentralDirPosValid && startPos != zipDetails.centralDirOffset) {
|
|
// Wrap around to beginning
|
|
file.seek(zipDetails.centralDirOffset);
|
|
wrapped = true;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// If we've wrapped and reached our start position, stop
|
|
if (wrapped && entryStart >= startPos) {
|
|
break;
|
|
}
|
|
|
|
file.seekCur(6);
|
|
file.read(&fileStat->method, 2);
|
|
file.seekCur(8);
|
|
file.read(&fileStat->compressedSize, 4);
|
|
file.read(&fileStat->uncompressedSize, 4);
|
|
uint16_t nameLen, m, k;
|
|
file.read(&nameLen, 2);
|
|
file.read(&m, 2);
|
|
file.read(&k, 2);
|
|
file.seekCur(8);
|
|
file.read(&fileStat->localHeaderOffset, 4);
|
|
|
|
if (nameLen < 256) {
|
|
file.read(itemName, nameLen);
|
|
itemName[nameLen] = '\0';
|
|
|
|
if (strcmp(itemName, filename) == 0) {
|
|
// Found it! Update cursor to next entry
|
|
file.seekCur(m + k);
|
|
lastCentralDirPos = file.position();
|
|
lastCentralDirPosValid = true;
|
|
found = true;
|
|
break;
|
|
}
|
|
} else {
|
|
// Name too long, skip it
|
|
file.seekCur(nameLen);
|
|
}
|
|
|
|
// Skip extra field + comment
|
|
file.seekCur(m + k);
|
|
}
|
|
|
|
if (!wasOpen) {
|
|
close();
|
|
}
|
|
return found;
|
|
}
|
|
|
|
long ZipFile::getDataOffset(const FileStatSlim& fileStat) {
|
|
const bool wasOpen = isOpen();
|
|
if (!wasOpen && !open()) {
|
|
return -1;
|
|
}
|
|
|
|
constexpr auto localHeaderSize = 30;
|
|
|
|
uint8_t pLocalHeader[localHeaderSize];
|
|
const uint64_t fileOffset = fileStat.localHeaderOffset;
|
|
|
|
file.seek(fileOffset);
|
|
const size_t read = file.read(pLocalHeader, localHeaderSize);
|
|
if (!wasOpen) {
|
|
close();
|
|
}
|
|
|
|
if (read != localHeaderSize) {
|
|
LOG_ERR("ZIP", "Something went wrong reading the local header");
|
|
return -1;
|
|
}
|
|
|
|
if (pLocalHeader[0] + (pLocalHeader[1] << 8) + (pLocalHeader[2] << 16) + (pLocalHeader[3] << 24) !=
|
|
0x04034b50 /* ZIP local file header signature */) {
|
|
LOG_ERR("ZIP", "Not a valid zip file header");
|
|
return -1;
|
|
}
|
|
|
|
const uint16_t filenameLength = pLocalHeader[26] + (pLocalHeader[27] << 8);
|
|
const uint16_t extraOffset = pLocalHeader[28] + (pLocalHeader[29] << 8);
|
|
return fileOffset + localHeaderSize + filenameLength + extraOffset;
|
|
}
|
|
|
|
bool ZipFile::loadZipDetails() {
|
|
if (zipDetails.isSet) {
|
|
return true;
|
|
}
|
|
|
|
const bool wasOpen = isOpen();
|
|
if (!wasOpen && !open()) {
|
|
return false;
|
|
}
|
|
|
|
const size_t fileSize = file.size();
|
|
if (fileSize < 22) {
|
|
LOG_ERR("ZIP", "File too small to be a valid zip");
|
|
if (!wasOpen) {
|
|
close();
|
|
}
|
|
return false; // Minimum EOCD size is 22 bytes
|
|
}
|
|
|
|
// We scan the last 1KB (or the whole file if smaller) for the EOCD signature
|
|
// 0x06054b50 is stored as 0x50, 0x4b, 0x05, 0x06 in little-endian
|
|
const int scanRange = fileSize > 1024 ? 1024 : fileSize;
|
|
const auto buffer = static_cast<uint8_t*>(malloc(scanRange));
|
|
if (!buffer) {
|
|
LOG_ERR("ZIP", "Failed to allocate memory for EOCD scan buffer");
|
|
if (!wasOpen) {
|
|
close();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
file.seek(fileSize - scanRange);
|
|
file.read(buffer, scanRange);
|
|
|
|
// Scan backwards for the signature
|
|
int foundOffset = -1;
|
|
for (int i = scanRange - 22; i >= 0; i--) {
|
|
constexpr uint32_t signature = 0x06054b50;
|
|
if (*reinterpret_cast<uint32_t*>(&buffer[i]) == signature) {
|
|
foundOffset = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (foundOffset == -1) {
|
|
LOG_ERR("ZIP", "EOCD signature not found in zip file");
|
|
free(buffer);
|
|
if (!wasOpen) {
|
|
close();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Now extract the values we need from the EOCD record
|
|
// Relative positions within EOCD:
|
|
// Offset 10: Total number of entries (2 bytes)
|
|
// Offset 16: Offset of start of central directory with respect to the starting disk number (4 bytes)
|
|
zipDetails.totalEntries = *reinterpret_cast<uint16_t*>(&buffer[foundOffset + 10]);
|
|
zipDetails.centralDirOffset = *reinterpret_cast<uint32_t*>(&buffer[foundOffset + 16]);
|
|
zipDetails.isSet = true;
|
|
|
|
free(buffer);
|
|
if (!wasOpen) {
|
|
close();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool ZipFile::open() {
|
|
if (!Storage.openFileForRead("ZIP", filePath, file)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool ZipFile::close() {
|
|
if (file) {
|
|
file.close();
|
|
}
|
|
lastCentralDirPos = 0;
|
|
lastCentralDirPosValid = false;
|
|
return true;
|
|
}
|
|
|
|
bool ZipFile::getInflatedFileSize(const char* filename, size_t* size) {
|
|
FileStatSlim fileStat = {};
|
|
if (!loadFileStatSlim(filename, &fileStat)) {
|
|
return false;
|
|
}
|
|
|
|
*size = static_cast<size_t>(fileStat.uncompressedSize);
|
|
return true;
|
|
}
|
|
|
|
int ZipFile::fillUncompressedSizes(std::vector<SizeTarget>& targets, std::vector<uint32_t>& sizes) {
|
|
if (targets.empty()) {
|
|
return 0;
|
|
}
|
|
|
|
const bool wasOpen = isOpen();
|
|
if (!wasOpen && !open()) {
|
|
return 0;
|
|
}
|
|
|
|
if (!loadZipDetails()) {
|
|
if (!wasOpen) {
|
|
close();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
file.seek(zipDetails.centralDirOffset);
|
|
|
|
int matched = 0;
|
|
uint32_t sig;
|
|
char itemName[256];
|
|
|
|
while (file.available()) {
|
|
file.read(&sig, 4);
|
|
if (sig != 0x02014b50) break;
|
|
|
|
file.seekCur(6);
|
|
uint16_t method;
|
|
file.read(&method, 2);
|
|
file.seekCur(8);
|
|
uint32_t compressedSize, uncompressedSize;
|
|
file.read(&compressedSize, 4);
|
|
file.read(&uncompressedSize, 4);
|
|
uint16_t nameLen, m, k;
|
|
file.read(&nameLen, 2);
|
|
file.read(&m, 2);
|
|
file.read(&k, 2);
|
|
file.seekCur(8);
|
|
uint32_t localHeaderOffset;
|
|
file.read(&localHeaderOffset, 4);
|
|
|
|
if (nameLen < 256) {
|
|
file.read(itemName, nameLen);
|
|
itemName[nameLen] = '\0';
|
|
|
|
uint64_t hash = fnvHash64(itemName, nameLen);
|
|
SizeTarget key = {hash, nameLen, 0};
|
|
|
|
auto it = std::lower_bound(targets.begin(), targets.end(), key, [](const SizeTarget& a, const SizeTarget& b) {
|
|
return a.hash < b.hash || (a.hash == b.hash && a.len < b.len);
|
|
});
|
|
|
|
while (it != targets.end() && it->hash == hash && it->len == nameLen) {
|
|
if (it->index < sizes.size()) {
|
|
sizes[it->index] = uncompressedSize;
|
|
matched++;
|
|
}
|
|
++it;
|
|
}
|
|
} else {
|
|
file.seekCur(nameLen);
|
|
}
|
|
|
|
file.seekCur(m + k);
|
|
}
|
|
|
|
if (!wasOpen) {
|
|
close();
|
|
}
|
|
|
|
return matched;
|
|
}
|
|
|
|
uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const bool trailingNullByte) {
|
|
const bool wasOpen = isOpen();
|
|
if (!wasOpen && !open()) {
|
|
return nullptr;
|
|
}
|
|
|
|
FileStatSlim fileStat = {};
|
|
if (!loadFileStatSlim(filename, &fileStat)) {
|
|
if (!wasOpen) {
|
|
close();
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
const long fileOffset = getDataOffset(fileStat);
|
|
if (fileOffset < 0) {
|
|
if (!wasOpen) {
|
|
close();
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
file.seek(fileOffset);
|
|
|
|
const auto deflatedDataSize = fileStat.compressedSize;
|
|
const auto inflatedDataSize = fileStat.uncompressedSize;
|
|
const auto dataSize = trailingNullByte ? inflatedDataSize + 1 : inflatedDataSize;
|
|
const auto data = static_cast<uint8_t*>(malloc(dataSize));
|
|
if (data == nullptr) {
|
|
LOG_ERR("ZIP", "Failed to allocate memory for output buffer (%zu bytes)", dataSize);
|
|
if (!wasOpen) {
|
|
close();
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
if (fileStat.method == ZIP_METHOD_STORED) {
|
|
// no deflation, just read content
|
|
const size_t dataRead = file.read(data, inflatedDataSize);
|
|
if (!wasOpen) {
|
|
close();
|
|
}
|
|
|
|
if (dataRead != inflatedDataSize) {
|
|
LOG_ERR("ZIP", "Failed to read data");
|
|
free(data);
|
|
return nullptr;
|
|
}
|
|
|
|
// Continue out of block with data set
|
|
} else if (fileStat.method == ZIP_METHOD_DEFLATED) {
|
|
// Read out deflated content from file
|
|
const auto deflatedData = static_cast<uint8_t*>(malloc(deflatedDataSize));
|
|
if (deflatedData == nullptr) {
|
|
LOG_ERR("ZIP", "Failed to allocate memory for decompression buffer");
|
|
if (!wasOpen) {
|
|
close();
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
const size_t dataRead = file.read(deflatedData, deflatedDataSize);
|
|
if (!wasOpen) {
|
|
close();
|
|
}
|
|
|
|
if (dataRead != deflatedDataSize) {
|
|
LOG_ERR("ZIP", "Failed to read data, expected %d got %d", deflatedDataSize, dataRead);
|
|
free(deflatedData);
|
|
free(data);
|
|
return nullptr;
|
|
}
|
|
|
|
bool success = false;
|
|
{
|
|
InflateReader r;
|
|
r.init(false);
|
|
r.setSource(deflatedData, deflatedDataSize);
|
|
success = r.read(data, inflatedDataSize);
|
|
}
|
|
free(deflatedData);
|
|
|
|
if (!success) {
|
|
LOG_ERR("ZIP", "Failed to inflate file");
|
|
free(data);
|
|
return nullptr;
|
|
}
|
|
|
|
// Continue out of block with data set
|
|
} else {
|
|
LOG_ERR("ZIP", "Unsupported compression method");
|
|
if (!wasOpen) {
|
|
close();
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
if (trailingNullByte) data[inflatedDataSize] = '\0';
|
|
if (size) *size = inflatedDataSize;
|
|
return data;
|
|
}
|
|
|
|
bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t chunkSize) {
|
|
const bool wasOpen = isOpen();
|
|
if (!wasOpen && !open()) {
|
|
return false;
|
|
}
|
|
|
|
FileStatSlim fileStat = {};
|
|
if (!loadFileStatSlim(filename, &fileStat)) {
|
|
return false;
|
|
}
|
|
|
|
const long fileOffset = getDataOffset(fileStat);
|
|
if (fileOffset < 0) {
|
|
return false;
|
|
}
|
|
|
|
file.seek(fileOffset);
|
|
const auto deflatedDataSize = fileStat.compressedSize;
|
|
const auto inflatedDataSize = fileStat.uncompressedSize;
|
|
|
|
if (fileStat.method == ZIP_METHOD_STORED) {
|
|
// no deflation, just read content
|
|
const auto buffer = static_cast<uint8_t*>(malloc(chunkSize));
|
|
if (!buffer) {
|
|
LOG_ERR("ZIP", "Failed to allocate memory for buffer");
|
|
if (!wasOpen) {
|
|
close();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
size_t remaining = inflatedDataSize;
|
|
while (remaining > 0) {
|
|
const size_t dataRead = file.read(buffer, remaining < chunkSize ? remaining : chunkSize);
|
|
if (dataRead == 0) {
|
|
LOG_ERR("ZIP", "Could not read more bytes");
|
|
free(buffer);
|
|
if (!wasOpen) {
|
|
close();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
out.write(buffer, dataRead);
|
|
remaining -= dataRead;
|
|
}
|
|
|
|
if (!wasOpen) {
|
|
close();
|
|
}
|
|
free(buffer);
|
|
return true;
|
|
}
|
|
|
|
if (fileStat.method == ZIP_METHOD_DEFLATED) {
|
|
auto* fileReadBuffer = static_cast<uint8_t*>(malloc(chunkSize));
|
|
if (!fileReadBuffer) {
|
|
LOG_ERR("ZIP", "Failed to allocate memory for zip file read buffer");
|
|
if (!wasOpen) {
|
|
close();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
auto* outputBuffer = static_cast<uint8_t*>(malloc(chunkSize));
|
|
if (!outputBuffer) {
|
|
LOG_ERR("ZIP", "Failed to allocate memory for output buffer");
|
|
free(fileReadBuffer);
|
|
if (!wasOpen) {
|
|
close();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
ZipInflateCtx ctx;
|
|
ctx.file = &file;
|
|
ctx.fileRemaining = deflatedDataSize;
|
|
ctx.readBuf = fileReadBuffer;
|
|
ctx.readBufSize = chunkSize;
|
|
|
|
if (!ctx.reader.init(true)) {
|
|
LOG_ERR("ZIP", "Failed to init inflate reader");
|
|
free(outputBuffer);
|
|
free(fileReadBuffer);
|
|
if (!wasOpen) {
|
|
close();
|
|
}
|
|
return false;
|
|
}
|
|
ctx.reader.setReadCallback(zipReadCallback);
|
|
|
|
bool success = false;
|
|
size_t totalProduced = 0;
|
|
|
|
while (true) {
|
|
size_t produced;
|
|
const InflateStatus status = ctx.reader.readAtMost(outputBuffer, chunkSize, &produced);
|
|
|
|
totalProduced += produced;
|
|
if (totalProduced > static_cast<size_t>(inflatedDataSize)) {
|
|
LOG_ERR("ZIP", "Decompressed size exceeds expected (%zu > %zu)", totalProduced,
|
|
static_cast<size_t>(inflatedDataSize));
|
|
break;
|
|
}
|
|
|
|
if (produced > 0) {
|
|
if (out.write(outputBuffer, produced) != produced) {
|
|
LOG_ERR("ZIP", "Failed to write all output bytes to stream");
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (status == InflateStatus::Done) {
|
|
if (totalProduced != static_cast<size_t>(inflatedDataSize)) {
|
|
LOG_ERR("ZIP", "Decompressed size mismatch (expected %zu, got %zu)", static_cast<size_t>(inflatedDataSize),
|
|
totalProduced);
|
|
break;
|
|
}
|
|
LOG_DBG("ZIP", "Decompressed %d bytes into %d bytes", deflatedDataSize, inflatedDataSize);
|
|
success = true;
|
|
break;
|
|
}
|
|
|
|
if (status == InflateStatus::Error) {
|
|
LOG_ERR("ZIP", "Decompression failed");
|
|
break;
|
|
}
|
|
// InflateStatus::Ok: output buffer full, continue
|
|
}
|
|
|
|
if (!wasOpen) {
|
|
close();
|
|
}
|
|
free(outputBuffer);
|
|
free(fileReadBuffer);
|
|
return success; // ctx.reader destructor frees the ring buffer
|
|
}
|
|
|
|
if (!wasOpen) {
|
|
close();
|
|
}
|
|
|
|
LOG_ERR("ZIP", "Unsupported compression method");
|
|
return false;
|
|
}
|