Cog/Frameworks/psflib/psflib/psf2fs.c
Christopher Snowhill f02a36fd08 [Highly Complete Input] Fix possible crash on OOM
If one condition returned memory successfully, but the other did not, it
would result in the cleanup code accessing an uninitialized pointer and
iterating over it, crashing.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-11 13:41:26 -07:00

480 lines
14 KiB
C

#include "psf2fs.h"
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <zlib.h>
/////////////////////////////////////////////////////////////////////////////
#define MYMAXPATH (1024)
struct SOURCE_FILE {
uint8_t * reserved_data;
int reserved_size;
struct SOURCE_FILE *next;
};
struct DIR_ENTRY {
char name[37];
struct DIR_ENTRY *subdir;
int length;
int block_size;
struct SOURCE_FILE *source;
int *offset_table;
struct DIR_ENTRY *next;
};
struct CACHEBLOCK {
struct SOURCE_FILE *from_source;
int from_offset;
char *uncompressed_data;
int uncompressed_size;
};
struct PSF2FS {
struct SOURCE_FILE *sources;
struct DIR_ENTRY *dir;
struct CACHEBLOCK cacheblock;
int adderror;
};
/////////////////////////////////////////////////////////////////////////////
static void source_cleanup_free(struct SOURCE_FILE *source) {
while(source) {
struct SOURCE_FILE *next = source->next;
if(source->reserved_data) free( source->reserved_data );
free( source );
source = next;
}
}
static void dir_cleanup_free(struct DIR_ENTRY *dir) {
while(dir) {
struct DIR_ENTRY *next = dir->next;
if(dir->subdir) dir_cleanup_free(dir->subdir);
free( dir );
dir = next;
}
}
static void cache_cleanup(struct CACHEBLOCK *cacheblock) {
if(!cacheblock) return;
if(cacheblock->uncompressed_data) free( cacheblock->uncompressed_data );
}
/////////////////////////////////////////////////////////////////////////////
void *psf2fs_create(void) {
struct PSF2FS *fs;
fs = ( struct PSF2FS * ) malloc( sizeof( struct PSF2FS ) );
if(!fs) return NULL;
memset(fs, 0, sizeof(struct PSF2FS));
return fs;
}
/////////////////////////////////////////////////////////////////////////////
void psf2fs_delete(void *psf2fs) {
struct PSF2FS *fs = (struct PSF2FS*)psf2fs;
if(fs->sources) source_cleanup_free(fs->sources);
if(fs->dir) dir_cleanup_free(fs->dir);
cache_cleanup(&(fs->cacheblock));
free( fs );
}
/////////////////////////////////////////////////////////////////////////////
static int isdirsep(char c) { return (c == '/' || c == '\\' || c == '|' || c == ':'); }
/////////////////////////////////////////////////////////////////////////////
static unsigned read32lsb(const uint8_t * foo) {
return (
((foo[0] & 0xFF) << 0) |
((foo[1] & 0xFF) << 8) |
((foo[2] & 0xFF) << 16) |
((foo[3] & 0xFF) << 24)
);
}
/////////////////////////////////////////////////////////////////////////////
static int __memicmp(const char * a, const char * b, int length)
{
int o, p;
for (o = 0; o < length; o++) {
p = tolower(a[o]) - tolower(b[o]);
if (p) return p;
}
return 0;
}
/////////////////////////////////////////////////////////////////////////////
static struct DIR_ENTRY *finddirentry(
struct DIR_ENTRY *dir,
const char *name,
int name_l
) {
if(name_l > 36) return NULL;
while(dir) {
if(!__memicmp(dir->name, name, name_l) && dir->name[name_l] == 0) return dir;
dir = dir->next;
}
return NULL;
}
/////////////////////////////////////////////////////////////////////////////
//
// Make a DIR_ENTRY list for a given file and Reserved offset.
// Recurses subdirectories also.
// All entries are set to point to the given SOURCE_FILE.
//
static struct DIR_ENTRY *makearchivedir(
struct PSF2FS *fs,
int offset,
struct SOURCE_FILE *source
) {
struct DIR_ENTRY *dir = NULL;
const uint8_t *file = source->reserved_data;
int n, num;
if(offset < 0) goto corrupt;
if(offset >= source->reserved_size) { goto corrupt; }
if((offset + 4) > source->reserved_size) { goto corrupt; }
num = read32lsb(file + offset);
offset += 4;
if(num < 0) goto corrupt;
for(n = 0; n < num; n++) {
int o, u, b;
if((offset + 48) > source->reserved_size) { goto corrupt; }
{ struct DIR_ENTRY *entry = ( struct DIR_ENTRY * ) malloc( sizeof( struct DIR_ENTRY ) );
if(!entry) goto outofmemory;
memset(entry, 0, sizeof(struct DIR_ENTRY));
entry->next = dir;
dir = entry;
}
memcpy(dir->name, file + offset, 36);
o = read32lsb(file + offset + 36);
u = read32lsb(file + offset + 40);
b = read32lsb(file + offset + 44);
offset += 48;
if(o < 0) goto corrupt;
if(u < 0) goto corrupt;
if(b < 0) goto corrupt;
if(o && o < offset) {
// char s[100];
// sprintf(s,"q[o=%08X offset=%08X]",o,offset);
// errormessageadd(fs, s);
goto corrupt;
}
// if this new entry describes a subdirectory:
if(u == 0 && b == 0 && o != 0) {
dir->subdir = makearchivedir(fs, o, source);
if(fs->adderror) goto error;
// if(!dir->subdir) goto error;
// if this new entry describes a zero-length file:
} else if(u == 0 || b == 0 || o == 0) {
// fields were zero anyway
// if this new entry describes a real source file:
} else {
int i;
int blocks = (u + (b-1)) / b;
int dataofs = o + 4 * blocks;
if(dataofs >= source->reserved_size) { goto corrupt; }
// record the info
dir->length = u;
dir->block_size = b;
dir->source = source;
dir->offset_table = (int *) malloc( ( blocks + 1 ) * sizeof( int ) );
if(!dir->offset_table) goto outofmemory;
for(i = 0; i < blocks; i++) {
int cbs;
if((o + 4) > source->reserved_size) { goto corrupt; }
cbs = read32lsb(file + o);
o += 4;
dir->offset_table[i] = dataofs;
dataofs += cbs;
}
dir->offset_table[i] = dataofs;
}
}
//success:
return dir;
corrupt:
goto error;
outofmemory:
goto error;
error:
dir_cleanup_free(dir);
fs->adderror = 1;
return NULL;
}
/////////////////////////////////////////////////////////////////////////////
//
// Merge two SOURCE_FILE lists.
// Guaranteed to succeed and not to free anything.
//
static struct SOURCE_FILE *mergesource(
struct SOURCE_FILE *to,
struct SOURCE_FILE *from
) {
struct SOURCE_FILE *to_tail;
if(!to && !from) return NULL;
if(!to) {
struct SOURCE_FILE *t;
t = to; to = from; from = t;
}
to_tail = to;
while(to_tail->next) { to_tail = to_tail->next; }
to_tail->next = from;
return to;
}
/////////////////////////////////////////////////////////////////////////////
//
// Merge two DIR_ENTRY lists.
// Guaranteed to succeed. May free some structures.
//
static struct DIR_ENTRY *mergedir(
struct DIR_ENTRY *to,
struct DIR_ENTRY *from
) {
// will traverse "from", and add to "to".
while(from) {
struct DIR_ENTRY *entry_to;
struct DIR_ENTRY *entry_from;
entry_from = from;
from = from->next;
// delink entry_from
entry_from->next = NULL;
// look for a duplicate entry in "to"
entry_to = finddirentry(to, entry_from->name, (int)strlen(entry_from->name));
// if there is one, do something fancy and then free entry_from.
if(entry_to) {
// if both are subdirs, merge the subdirs
if((entry_to->subdir) && (entry_from->subdir)) {
entry_to->subdir = mergedir(entry_to->subdir, entry_from->subdir);
entry_from->subdir = NULL;
// if both are files, copy over the info
} else if((!(entry_to->subdir)) && (!(entry_from->subdir))) {
entry_to->length = entry_from->length;
entry_to->block_size = entry_from->block_size;
entry_to->source = entry_from->source;
if(entry_to->offset_table) free( entry_to->offset_table );
entry_to->offset_table = entry_from->offset_table;
entry_from->offset_table = NULL;
// if one's a subdir but the other's not, we lose "from". this is fine.
}
dir_cleanup_free(entry_from);
entry_from = NULL;
// otherwise, just relink to the top of "to"
} else {
entry_from->next = to;
to = entry_from;
}
}
return to;
}
/////////////////////////////////////////////////////////////////////////////
//
// only modifies *psource and *pdir on success
//
static int addarchive(
struct PSF2FS *fs,
const uint8_t *reserved_data,
int reserved_size,
struct SOURCE_FILE **psource,
struct DIR_ENTRY **pdir
) {
struct SOURCE_FILE *source = *psource;
struct DIR_ENTRY *dir = *pdir;
// these relate to the current file
struct SOURCE_FILE *this_source = NULL;
struct DIR_ENTRY *this_dir = NULL;
// default to no error
fs->adderror = 0;
// create a source entry for this psf2
this_source = ( struct SOURCE_FILE * ) malloc( sizeof( struct SOURCE_FILE ) );
if(!this_source) goto outofmemory;
this_source->next = NULL;
this_source->reserved_data = ( uint8_t * ) malloc( reserved_size );
if(!this_source->reserved_data) goto outofmemory;
memcpy(this_source->reserved_data, reserved_data, reserved_size);
this_source->reserved_size = reserved_size;
this_dir = makearchivedir(fs, 0, this_source);
if(fs->adderror) goto error;
// success
// now merge everything
*psource = mergesource(source, this_source);
*pdir = mergedir(dir, this_dir);
//success:
return 0;
outofmemory:
goto error;
error:
if(dir) dir_cleanup_free(dir);
if(source) source_cleanup_free(source);
if(this_dir) dir_cleanup_free(this_dir);
if(this_source) source_cleanup_free(this_source);
return -1;
}
/////////////////////////////////////////////////////////////////////////////
//
//
//
int psf2fs_load_callback(void * psf2fs, const uint8_t * exe, size_t exe_size,
const uint8_t * reserved, size_t reserved_size) {
struct PSF2FS *fs = (struct PSF2FS*)psf2fs;
(void)exe;
(void)exe_size;
return addarchive(fs, reserved, (int)reserved_size, &(fs->sources), &(fs->dir));
}
/////////////////////////////////////////////////////////////////////////////
//
//
//
static int virtual_read(struct PSF2FS *fs, struct DIR_ENTRY *entry, int offset, char *buffer, int length) {
int length_read = 0;
int r;
unsigned long destlen;
if(offset >= entry->length) return 0;
if((offset + length) > entry->length) length = entry->length - offset;
while(length_read < length) {
// get info on the current block
int blocknum = offset / entry->block_size;
int ofs_within_block = offset % entry->block_size;
int canread;
int block_zofs = entry->offset_table[blocknum];
int block_zsize = entry->offset_table[blocknum+1] - block_zofs;
int block_usize;
if(block_zofs <= 0 || block_zofs >= entry->source->reserved_size) goto bounds;
if((block_zofs+block_zsize) > entry->source->reserved_size) goto bounds;
// get the actual uncompressed size of this block
block_usize = entry->length - (blocknum * entry->block_size);
if(block_usize > entry->block_size) block_usize = entry->block_size;
// if it's not already in the cache block, read it
if(
(fs->cacheblock.from_offset != block_zofs) ||
(fs->cacheblock.from_source != entry->source)
) {
// invalidate cache without freeing buffer
fs->cacheblock.from_source = NULL;
// make sure there's a buffer allocated
// but only reallocate if the size is different
if(fs->cacheblock.uncompressed_size != block_usize) {
fs->cacheblock.uncompressed_size = 0;
if(fs->cacheblock.uncompressed_data) {
free( fs->cacheblock.uncompressed_data );
fs->cacheblock.uncompressed_data = NULL;
}
fs->cacheblock.uncompressed_data = ( char * ) malloc( block_usize );
if(!fs->cacheblock.uncompressed_data) goto outofmemory;
fs->cacheblock.uncompressed_size = block_usize;
}
destlen = block_usize;
// attempt decompress
r = uncompress((unsigned char *) fs->cacheblock.uncompressed_data, &destlen, (const unsigned char *) entry->source->reserved_data + block_zofs, block_zsize);
if(r != Z_OK || destlen != block_usize) {
// char s[999];
// sprintf(s,"zdata=%02X %02X %02X blockz=%d blocku=%d destlenout=%d", zdata[0], zdata[1], zdata[2], block_zsize, block_usize, destlen);
// errormessageadd(fs, s);
goto error;
}
}
// at this point, we can read whatever we want out of the cacheblock
canread = fs->cacheblock.uncompressed_size - ofs_within_block;
if(canread > (length - length_read)) canread = length - length_read;
// copy
memcpy(buffer, fs->cacheblock.uncompressed_data + ofs_within_block, canread);
// advance pointers/counters
offset += canread;
length_read += canread;
buffer += canread;
}
//success:
return length_read;
bounds:
goto error;
outofmemory:
goto error;
error:
// if cacheblock was invalidated, we can free it
if(!fs->cacheblock.from_source) {
fs->cacheblock.uncompressed_size = 0;
if(fs->cacheblock.uncompressed_data) {
free( fs->cacheblock.uncompressed_data );
fs->cacheblock.uncompressed_data = NULL;
}
}
return -1;
}
/////////////////////////////////////////////////////////////////////////////
//
//
//
int psf2fs_virtual_readfile(void *psf2fs, const char *path, int offset, char *buffer, int length) {
struct PSF2FS *fs = (struct PSF2FS*)psf2fs;
struct DIR_ENTRY *entry = fs->dir;
if(!path) goto invalidarg;
if(offset < 0) goto invalidarg;
if(!buffer) goto invalidarg;
if(length < 0) goto invalidarg;
for(;;) {
int l;
int need_dir;
if(!entry) goto pathnotfound;
while(isdirsep(*path)) path++;
for(l = 0;; l++) {
if(!path[l]) { need_dir = 0; break; }
if(isdirsep(path[l])) { need_dir = 1; break; }
}
entry = finddirentry(entry, path, l);
if(!entry) goto pathnotfound;
if(!need_dir) break;
entry = entry->subdir;
path += l;
}
// if we "found" a file but it's a directory, then we didn't find it
if(entry->subdir) goto pathnotfound;
// special case: if requested length is 0, return the total file length
if(!length) return entry->length;
// otherwise, read from source
return virtual_read(fs, entry, offset, buffer, length);
pathnotfound:
goto error;
invalidarg:
goto error;
error:
return -1;
}
/////////////////////////////////////////////////////////////////////////////