engine: add sprite model loading fuzzing, decouple fuzzing from tests

This commit is contained in:
Alibek Omarov
2026-06-30 13:53:52 +05:00
parent 34467bfe9a
commit 8ccb9dea3f
9 changed files with 79 additions and 17 deletions

View File

@@ -274,7 +274,7 @@ void FS_FreeStream( stream_t *stream )
stream->format->freefunc( stream );
}
#if XASH_ENGINE_TESTS
#if XASH_LLVM_LIBFUZZER
#define IMPLEMENT_SOUNDLIB_FUZZ_TARGET( export, target ) \
int EXPORT export( const uint8_t *Data, size_t Size ); \
int EXPORT export( const uint8_t *Data, size_t Size ) \
@@ -294,4 +294,4 @@ int EXPORT export( const uint8_t *Data, size_t Size ) \
IMPLEMENT_SOUNDLIB_FUZZ_TARGET( Fuzz_Sound_LoadMPG, Sound_LoadMPG )
IMPLEMENT_SOUNDLIB_FUZZ_TARGET( Fuzz_Sound_LoadWAV, Sound_LoadWAV )
#endif
#endif // XASH_LLVM_LIBFUZZER

View File

@@ -185,7 +185,7 @@ static qboolean Sound_ParseID3Tag( const byte *buffer, fs_offset_t filesize )
return true;
}
#if XASH_ENGINE_TESTS
#if XASH_LLVM_LIBFUZZER
int EXPORT Fuzz_Sound_ParseID3Tag( const uint8_t *Data, size_t Size );
int EXPORT Fuzz_Sound_ParseID3Tag( const uint8_t *Data, size_t Size )
{
@@ -193,7 +193,7 @@ int EXPORT Fuzz_Sound_ParseID3Tag( const uint8_t *Data, size_t Size )
Sound_ParseID3Tag( Data, Size );
return 0;
}
#endif
#endif // XASH_LLVM_LIBFUZZER
/*
=================================================================

View File

@@ -701,6 +701,10 @@ void Test_RunImagelib( void )
Z_Free( rgb.buffer );
}
#endif // XASH_ENGINE_TESTS
#if XASH_LLVM_LIBFUZZER
#include "sprite.h"
#define IMPLEMENT_IMAGELIB_FUZZ_TARGET( export, target ) \
int EXPORT export( const uint8_t *Data, size_t Size ); \
@@ -724,4 +728,29 @@ IMPLEMENT_IMAGELIB_FUZZ_TARGET( Fuzz_Image_LoadPNG, Image_LoadPNG )
IMPLEMENT_IMAGELIB_FUZZ_TARGET( Fuzz_Image_LoadDDS, Image_LoadDDS )
IMPLEMENT_IMAGELIB_FUZZ_TARGET( Fuzz_Image_LoadTGA, Image_LoadTGA )
#endif /* XASH_ENGINE_TESTS */
int EXPORT Fuzz_Image_LoadSPR( const uint8_t *Data, size_t Size );
int EXPORT Fuzz_Image_LoadSPR( const uint8_t *Data, size_t Size )
{
rgbdata_t *rgb;
// the real caller passes only the pixel data size, the frame header is extra
if( Size < sizeof( dspriteframe_t ))
return 0;
host.type = HOST_NORMAL;
Memory_Init();
Image_Init();
image.hint = IL_HINT_Q1; // installs the Quake palette so the decoder runs
if( Image_LoadSPR( "#internal.spr", Data, Size - sizeof( dspriteframe_t )))
{
rgb = ImagePack( "#internal.spr" );
FS_FreeImage( rgb );
}
Image_Shutdown();
return 0;
}
#endif // XASH_LLVM_LIBFUZZER

View File

@@ -302,3 +302,34 @@ void Mod_LoadSpriteModel( model_t *mod, void *buffer, size_t buffersize, qboolea
Mod_SpriteLoadTextures( mod, buffer );
#endif
}
#if XASH_LLVM_LIBFUZZER
int EXPORT Fuzz_Mod_LoadSpriteModel( const uint8_t *Data, size_t Size );
int EXPORT Fuzz_Mod_LoadSpriteModel( const uint8_t *Data, size_t Size )
{
model_t mod = { .name = "#internal.spr", .needload = NL_NEEDS_LOADED };
qboolean loaded = false;
byte *buf;
if( Size == 0 )
return 0;
Memory_Init();
// dedicated mode exercises the parser/validator without needing a renderer
host.type = HOST_DEDICATED;
host.mempool = Mem_AllocPool( "fuzzing pool" );
// the loader byteswaps the buffer in place, so hand it a writable copy
buf = Mem_Malloc( host.mempool, Size );
memcpy( buf, Data, Size );
Mod_LoadSpriteModel( &mod, buf, Size, &loaded );
if( mod.mempool )
Mem_FreePool( &mod.mempool );
Mem_FreePool( &host.mempool );
return 0;
}
#endif // XASH_LLVM_LIBFUZZER

View File

@@ -653,7 +653,7 @@ void Mod_NeedCRC( const char *name, qboolean needCRC )
else ClearBits( p->flags, FCRC_SHOULD_CHECKSUM );
}
#if XASH_ENGINE_TESTS
#if XASH_LLVM_LIBFUZZER
static const uint8_t *fuzz_data;
static size_t fuzz_size;
@@ -689,4 +689,4 @@ int EXPORT Fuzz_Mod_LoadModel( const uint8_t *Data, size_t Size )
return 0;
}
#endif // XASH_ENGINE_TESTS
#endif // XASH_LLVM_LIBFUZZER

View File

@@ -47,9 +47,6 @@ def options(opt):
grp.add_option('--enable-static-binary', action = 'store_true', dest = 'STATIC', default = False,
help = 'build static binary(not recommended, --single-binary required) [default: %(default)s]')
grp.add_option('--enable-engine-fuzz', action = 'store_true', dest = 'ENGINE_FUZZ', default = False,
help = 'add LLVM libFuzzer [default: %(default)s]' )
grp.add_option('--enable-ffmpeg', action = 'store_true', dest = 'FFMPEG', default = False,
help = 'enable ffmpeg based movie playback [default: %(default)s')
@@ -89,9 +86,10 @@ def configure(conf):
elif conf.env.DEST_OS == 'dos':
conf.options.STATIC = True
if conf.options.ENGINE_FUZZ:
if conf.options.ENABLE_FUZZER:
conf.define('XASH_LLVM_LIBFUZZER', 1)
conf.env.append_unique('CFLAGS', '-fsanitize=fuzzer-no-link')
conf.env.append_unique('LINKFLAGS', '-fsanitize=fuzzer')
conf.env.append_unique('LINKFLAGS', '-fsanitize=fuzzer-no-link')
# Client-only dependencies
if conf.env.CLIENT:

View File

@@ -1,4 +1,5 @@
#include <dlfcn.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
@@ -6,12 +7,13 @@
#error
#endif
typedef int (*FuzzFunc)(const char *Data, size_t Size);
typedef int (*FuzzFunc)(const uint8_t *Data, size_t Size);
void *handle = NULL;
FuzzFunc f = NULL;
static void *handle = NULL;
static FuzzFunc f = NULL;
int LLVMFuzzerTestOneInput( const char *Data, size_t Size )
int LLVMFuzzerTestOneInput( const uint8_t *Data, size_t Size );
int LLVMFuzzerTestOneInput( const uint8_t *Data, size_t Size )
{
if( !handle )
handle = dlopen( LIB, RTLD_NOW );

View File

@@ -37,4 +37,6 @@ def build(bld):
add_runner_target(bld, 'libxash.so', 'Image_LoadPNG')
add_runner_target(bld, 'libxash.so', 'Image_LoadDDS')
add_runner_target(bld, 'libxash.so', 'Image_LoadTGA')
add_runner_target(bld, 'libxash.so', 'Image_LoadSPR')
add_runner_target(bld, 'libxash.so', 'Mod_LoadModel')
add_runner_target(bld, 'libxash.so', 'Mod_LoadSpriteModel')

View File

@@ -86,6 +86,7 @@ SUBDIRS = [
Subproject('3rdparty/libbacktrace'),
Subproject('3rdparty/library_suffix'),
Subproject('3rdparty/yy-thunks'),
Subproject('utils/run-fuzzer', lambda x: x.env.ENABLE_FUZZER),
# disable only by engine feature, makes no sense to even parse subprojects in dedicated mode
Subproject('3rdparty/extras', lambda x: x.env.CLIENT and x.env.DEST_OS != 'android'),
@@ -114,7 +115,6 @@ SUBDIRS = [
# enabled optionally
Subproject('utils/mdldec', lambda x: x.env.ENABLE_UTILS),
Subproject('utils/xar', lambda x: x.env.ENABLE_UTILS and x.env.ENABLE_XAR),
Subproject('utils/run-fuzzer', lambda x: x.env.ENABLE_FUZZER),
# enabled on PSVita only
Subproject('ref/gl/vgl_shim', lambda x: x.env.DEST_OS == 'psvita'),