From f3f67990f612e478f902155e308fd0884f5ce99c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Thomas?= Date: Tue, 21 Mar 2017 17:20:44 +0100 Subject: [PATCH 1/2] Fix CVE-2017-6903 This fixes the issues mentioned in CVE-2017-6903 and adds more general security improvements as well. - Don't load q3config.cfg or autoexec.cfg from any pk3 file - Don't load QVMs from untrusted pk3 files in the download directory (this was previously detected but not actually blocked) - Disallow loading of DLL/so/dylib with a .pk3 extension - Add FS_CheckFilenameIsMutable from ioquake3 - Ensure condump can only write to .txt files - Ensure writeconfig can only write to .cfg files - Improve malicious pk3 reporting (and add check for .cfg files as well) Fixes #71 --- code/client/cl_console.c | 36 ++++++++++++--- code/client/cl_curl.c | 8 +++- code/client/cl_parse.c | 22 +++++----- code/client/snd_openal.c | 7 +++ code/qcommon/common.c | 7 +++ code/qcommon/files.c | 95 +++++++++++++++++++++++++++++++++------- code/qcommon/q_shared.c | 24 ++++++++++ code/qcommon/q_shared.h | 1 + code/qcommon/qcommon.h | 4 +- code/sys/sys_main.c | 7 +++ code/unix/linux_glimp.c | 6 +++ code/unix/sdl_glimp.c | 6 +++ code/win32/win_glimp.c | 6 +++ 13 files changed, 192 insertions(+), 37 deletions(-) diff --git a/code/client/cl_console.c b/code/client/cl_console.c index b8553e771..a3586e8a6 100644 --- a/code/client/cl_console.c +++ b/code/client/cl_console.c @@ -191,7 +191,9 @@ void Con_Dump_f (void) int l, x, i; short *line; fileHandle_t f; - char buffer[1024]; + int bufferlen; + char *buffer; + char filename[MAX_QPATH]; if (Cmd_Argc() != 2) { @@ -199,15 +201,24 @@ void Con_Dump_f (void) return; } - Com_Printf ("Dumped console text to %s.\n", Cmd_Argv(1) ); + Q_strncpyz( filename, Cmd_Argv( 1 ), sizeof( filename ) ); + COM_DefaultExtension( filename, sizeof( filename ), ".txt" ); - f = FS_FOpenFileWrite( Cmd_Argv( 1 ) ); + if (!COM_CompareExtension(filename, ".txt")) + { + Com_Printf("Con_Dump_f: Only the \".txt\" extension is supported by this command!\n"); + return; + } + + f = FS_FOpenFileWrite( filename ); if (!f) { - Com_Printf ("ERROR: couldn't open.\n"); + Com_Printf ("ERROR: couldn't open %s.\n", filename); return; } + Com_Printf ("Dumped console text to %s.\n", filename ); + // skip empty lines for (l = consoles[CONSOLE_ALL].current - consoles[CONSOLE_ALL].totallines + 1 ; l <= consoles[CONSOLE_ALL].current ; l++) { @@ -219,8 +230,16 @@ void Con_Dump_f (void) break; } +#ifdef _WIN32 + bufferlen = consoles[CONSOLE_ALL].linewidth + 3 * sizeof ( char ); +#else + bufferlen = consoles[CONSOLE_ALL].linewidth + 2 * sizeof ( char ); +#endif + + buffer = Hunk_AllocateTempMemory( bufferlen ); + // write the remaining lines - buffer[consoles[CONSOLE_ALL].linewidth] = 0; + buffer[bufferlen-1] = 0; for ( ; l <= consoles[CONSOLE_ALL].current ; l++) { line = consoles[CONSOLE_ALL].text + (l%consoles[CONSOLE_ALL].totallines)*consoles[CONSOLE_ALL].linewidth; @@ -233,10 +252,15 @@ void Con_Dump_f (void) else break; } - strcat( buffer, "\n" ); +#ifdef _WIN32 + Q_strcat(buffer, bufferlen, "\r\n"); +#else + Q_strcat(buffer, bufferlen, "\n"); +#endif FS_Write(buffer, strlen(buffer), f); } + Hunk_FreeTempMemory( buffer ); FS_FCloseFile( f ); } diff --git a/code/client/cl_curl.c b/code/client/cl_curl.c index 962c4aa74..3231ef0fc 100644 --- a/code/client/cl_curl.c +++ b/code/client/cl_curl.c @@ -122,8 +122,14 @@ qboolean CL_cURL_Init() if(cURLLib) return qtrue; - Com_Printf("Loading \"%s\"...", cl_cURLLib->string); + + if ( COM_CompareExtension( cl_cURLLib->string, ".pk3" ) ) + { + Com_Printf( S_COLOR_RED "Rejecting cl_cURLLib named \"%s\"\n", cl_cURLLib->string ); + return qfalse; + } + if( (cURLLib = OBJLOAD(cl_cURLLib->string)) == 0 ) { #ifdef _WIN32 diff --git a/code/client/cl_parse.c b/code/client/cl_parse.c index 063519e88..67fab50c0 100644 --- a/code/client/cl_parse.c +++ b/code/client/cl_parse.c @@ -709,21 +709,21 @@ void CL_ParseGamestate( msg_t *msg ) { // reinitialize the filesystem if the game directory has changed FS_ConditionalRestart( clc.checksumFeed ); - if (foreignQVMsFound) { - char QVMList[MAX_STRING_CHARS]; - for (i = 0; i < foreignQVMsFound; i++) { - strcat(QVMList, va("%s.pk3, ", foreignQVMNames[i])); + if (dangerousPaksFound) { + char PakList[MAX_STRING_CHARS]; + for (i = 0; i < dangerousPaksFound; i++) { + Q_strcat(PakList, sizeof(PakList), va("%s.pk3, ", dangerousPakNames[i])); } - QVMList[strlen(QVMList) - 2] = 0; + PakList[strlen(PakList) - 2] = 0; Cvar_Set("com_errorMessage", va( - "^1WARNING! ^7QVM found in downloaded pk3%s\n\n%s\n\n" - "You should go delete %s immediately. %s could contain malicious code.", - foreignQVMsFound == 1 ? ":" : "s:", - QVMList, - foreignQVMsFound == 1 ? "that file" : "those files", - foreignQVMsFound == 1 ? "It" : "They")); + "^1WARNING! ^7Dangerous file(s) found in downloaded pk3%s:\n\n%s\n\n" + "You should go delete %s immediately. %s could lead to malicious code execution.", + dangerousPaksFound == 1 ? "" : "s", + PakList, + dangerousPaksFound == 1 ? "that file" : "those files", + dangerousPaksFound == 1 ? "It" : "They")); VM_Call(uivm, UI_SET_ACTIVE_MENU, UIMENU_MAIN); return; diff --git a/code/client/snd_openal.c b/code/client/snd_openal.c index 4dbaa65d2..097b30c97 100644 --- a/code/client/snd_openal.c +++ b/code/client/snd_openal.c @@ -1878,6 +1878,13 @@ qboolean S_AL_Init( soundInterface_t *si ) s_alDriver = Cvar_Get( "s_alDriver", ALDRIVER_DEFAULT, CVAR_ARCHIVE ); + + if ( COM_CompareExtension( s_alDriver->string, ".pk3" ) ) + { + Com_Printf( S_COLOR_RED "Rejecting s_alDriver named \"%s\"\n", s_alDriver->string ); + return qfalse; + } + // Load QAL if( !QAL_Init( s_alDriver->string ) ) { diff --git a/code/qcommon/common.c b/code/qcommon/common.c index 7a57504d5..d3bbb6df6 100644 --- a/code/qcommon/common.c +++ b/code/qcommon/common.c @@ -2637,6 +2637,13 @@ void Com_WriteConfig_f( void ) { Q_strncpyz( filename, Cmd_Argv(1), sizeof( filename ) ); COM_DefaultExtension( filename, sizeof( filename ), ".cfg" ); + + if (!COM_CompareExtension(filename, ".cfg")) + { + Com_Printf("Com_WriteConfig_f: Only the \".cfg\" extension is supported by this command!\n"); + return; + } + Com_Printf( "Writing %s.\n", filename ); Com_WriteConfigToFile( filename ); } diff --git a/code/qcommon/files.c b/code/qcommon/files.c index eea2238b9..512c1980c 100644 --- a/code/qcommon/files.c +++ b/code/qcommon/files.c @@ -193,8 +193,8 @@ static const unsigned pak_checksums[] = { static int pak_purechecksums[1]; -int foreignQVMsFound; -char foreignQVMNames[MAX_ZPATH][MAX_SEARCH_PATHS]; +int dangerousPaksFound; +char dangerousPakNames[MAX_ZPATH][MAX_SEARCH_PATHS]; // if this is defined, the executable positively won't work with any paks other // than the demo pak, even if productid is present. This is only used for our @@ -501,6 +501,26 @@ static qboolean FS_CreatePath (char *OSPath) { return qfalse; } +/* +================= +FS_CheckFilenameIsMutable + +ERR_FATAL if trying to maniuplate a file with the platform library, QVM, or pk3 extension +================= + */ +static void FS_CheckFilenameIsMutable( const char *filename, + const char *function ) +{ + // Check if the filename ends with the library, QVM, or pk3 extension + if( COM_CompareExtension( filename, DLL_EXT ) + || COM_CompareExtension( filename, ".qvm" ) + || COM_CompareExtension( filename, ".pk3" ) ) + { + Com_Error( ERR_FATAL, "%s: Not allowed to manipulate '%s' due " + "to %s extension", function, filename, COM_GetExtension( filename ) ); + } +} + /* ================= FS_CopyFile @@ -556,6 +576,8 @@ FS_Remove =========== */ void FS_Remove( const char *osPath ) { + FS_CheckFilenameIsMutable( osPath, __func__ ); + remove( osPath ); } @@ -566,6 +588,8 @@ FS_HomeRemove =========== */ void FS_HomeRemove( const char *homePath ) { + FS_CheckFilenameIsMutable( homePath, __func__ ); + remove( FS_BuildOSPath( fs_homepath->string, fs_gamedir, homePath ) ); } @@ -643,6 +667,8 @@ fileHandle_t FS_SV_FOpenFileWrite( const char *filename ) { Com_Printf( "FS_SV_FOpenFileWrite: %s\n", ospath ); } + FS_CheckFilenameIsMutable( ospath, __func__ ); + if( FS_CreatePath( ospath ) ) { return 0; } @@ -751,6 +777,8 @@ void FS_SV_Rename( const char *from, const char *to ) { Com_Printf( "FS_SV_Rename: %s --> %s\n", from_ospath, to_ospath ); } + FS_CheckFilenameIsMutable( to_ospath, __func__ ); + if (rename( from_ospath, to_ospath )) { // Failed, try copying it and deleting the original FS_CopyFile ( from_ospath, to_ospath ); @@ -783,6 +811,8 @@ void FS_Rename( const char *from, const char *to ) { Com_Printf( "FS_Rename: %s --> %s\n", from_ospath, to_ospath ); } + FS_CheckFilenameIsMutable( to_ospath, __func__ ); + if (rename( from_ospath, to_ospath )) { // Failed, try copying it and deleting the original FS_CopyFile ( from_ospath, to_ospath ); @@ -844,6 +874,8 @@ fileHandle_t FS_FOpenFileWrite( const char *filename ) { Com_Printf( "FS_FOpenFileWrite: %s\n", ospath ); } + FS_CheckFilenameIsMutable( ospath, __func__ ); + if( FS_CreatePath( ospath ) ) { return 0; } @@ -890,6 +922,8 @@ fileHandle_t FS_FOpenFileAppend( const char *filename ) { Com_Printf( "FS_FOpenFileAppend: %s\n", ospath ); } + FS_CheckFilenameIsMutable( ospath, __func__ ); + if( FS_CreatePath( ospath ) ) { return 0; } @@ -961,6 +995,7 @@ int FS_FOpenFileRead( const char *filename, fileHandle_t *file, qboolean uniqueF FILE *temp; int l; char demoExt[16]; + qboolean isLocalConfig, isQVM; hash = 0; @@ -968,11 +1003,21 @@ int FS_FOpenFileRead( const char *filename, fileHandle_t *file, qboolean uniqueF Com_Error( ERR_FATAL, "Filesystem call made without initialization\n" ); } + isLocalConfig = !Q_stricmp(filename, "autoexec.cfg") || !Q_stricmp(filename, "q3config.cfg"); + isQVM = COM_CompareExtension(filename, ".qvm"); + if ( file == NULL ) { // just wants to see if file is there for ( search = fs_searchpaths ; search ; search = search->next ) { - // if ( search->pack ) { + // autoexec.cfg and q3config.cfg can only be loaded outside of pk3 files. + if (isLocalConfig) + continue; + + // QVMs can't be loaded from pk3 in the "download" directory + if (isQVM && !Q_stricmp(search->pack->pakGamename, "download")) + continue; + hash = FS_HashFileName(filename, search->pack->hashSize); } // is the element a pak file? @@ -1041,8 +1086,15 @@ int FS_FOpenFileRead( const char *filename, fileHandle_t *file, qboolean uniqueF fsh[*file].handleFiles.unique = uniqueFILE; for ( search = fs_searchpaths ; search ; search = search->next ) { - // if ( search->pack ) { + // autoexec.cfg and q3config.cfg can only be loaded outside of pk3 files. + if (isLocalConfig) + continue; + + // QVMs can't be loaded from pk3 in the "download" directory + if (isQVM && !Q_stricmp(search->pack->pakGamename, "download")) + continue; + hash = FS_HashFileName(filename, search->pack->hashSize); } // is the element a pak file? @@ -1647,7 +1699,7 @@ Creates a new pak_t in the search chain for the contents of a zip file. ================= */ -static pack_t *FS_LoadZipFile( char *zipfile, const char *basename ) +static pack_t *FS_LoadZipFile( char *zipfile, const char *basename, const char *gamename ) { fileInPack_t *buildBuffer; pack_t *pack; @@ -1661,7 +1713,7 @@ static pack_t *FS_LoadZipFile( char *zipfile, const char *basename ) int fs_numHeaderLongs; int *fs_headerLongs; char *namePtr; - qboolean alreadyForeign = qfalse; + qboolean alreadydangerous = qfalse; fs_numHeaderLongs = 0; @@ -1707,6 +1759,7 @@ static pack_t *FS_LoadZipFile( char *zipfile, const char *basename ) Q_strncpyz( pack->pakFilename, zipfile, sizeof( pack->pakFilename ) ); Q_strncpyz( pack->pakBasename, basename, sizeof( pack->pakBasename ) ); + Q_strncpyz( pack->pakGamename, gamename, sizeof( pack->pakGamename ) ); // strip .pk3 if needed if ( strlen( pack->pakBasename ) > 4 && !Q_stricmp( pack->pakBasename + strlen( pack->pakBasename ) - 4, ".pk3" ) ) { @@ -1724,17 +1777,27 @@ static pack_t *FS_LoadZipFile( char *zipfile, const char *basename ) break; } - if (strstr(filename_inzip, ".qvm") && strstr(pack->pakFilename, "download/")) { - for (j = 0; j < foreignQVMsFound; j++) { - if (!strcmp(foreignQVMNames[j], pack->pakBasename)) { - alreadyForeign = qtrue; + if (!Q_stricmp(pack->pakGamename, "download") && ( + COM_CompareExtension(filename_inzip, ".qvm") || + !Q_stricmp(filename_inzip, "autoexec.cfg") || + !Q_stricmp(filename_inzip, "q3config.cfg"))) + { + + for (j = 0; j < dangerousPaksFound; j++) { + if (!strcmp(dangerousPakNames[j], pack->pakBasename)) { + alreadydangerous = qtrue; + break; } } - if (!alreadyForeign) { - Com_sprintf(foreignQVMNames[foreignQVMsFound], MAX_ZPATH, pack->pakBasename); - foreignQVMsFound++; + if (!alreadydangerous) { + Q_strncpyz(dangerousPakNames[dangerousPaksFound], pack->pakBasename, MAX_ZPATH); + dangerousPaksFound++; } + + Com_Printf(S_COLOR_RED "Dangerous file %s found in %s\n", + filename_inzip, + pack->pakFilename); } if (file_info.uncompressed_size > 0) { @@ -2464,10 +2527,8 @@ void FS_AddGameDirectory( const char *path, const char *dir ) { for ( i = 0 ; i < numfiles ; i++ ) { pakfile = FS_BuildOSPath( path, dir, pakfiles[i] ); - if ( ( pak = FS_LoadZipFile( pakfile, pakfiles[i] ) ) == 0 ) + if ( ( pak = FS_LoadZipFile( pakfile, pakfiles[i], dir ) ) == 0 ) continue; - // store the game name for downloading - strcpy(pak->pakGamename, dir); search = Z_Malloc (sizeof(searchpath_t)); search->pack = pak; @@ -2742,7 +2803,7 @@ static void FS_Startup( const char *gameName ) Com_Printf( "----- FS_Startup -----\n" ); - foreignQVMsFound = 0; + dangerousPaksFound = 0; fs_debug = Cvar_Get( "fs_debug", "0", 0 ); fs_basepath = Cvar_Get ("fs_basepath", Sys_DefaultInstallPath(), CVAR_INIT ); diff --git a/code/qcommon/q_shared.c b/code/qcommon/q_shared.c index 0a39a2805..567e2abfb 100644 --- a/code/qcommon/q_shared.c +++ b/code/qcommon/q_shared.c @@ -91,6 +91,30 @@ void COM_StripExtension( const char *in, char *out, int destsize ) } } +/* +============ +COM_CompareExtension + +string compare the end of the strings and return qtrue if strings match +============ +*/ +qboolean COM_CompareExtension(const char *in, const char *ext) +{ + int inlen, extlen; + + inlen = strlen(in); + extlen = strlen(ext); + + if(extlen <= inlen) + { + in += inlen - extlen; + + if(!Q_stricmp(in, ext)) + return qtrue; + } + + return qfalse; +} /* ================== diff --git a/code/qcommon/q_shared.h b/code/qcommon/q_shared.h index f6c9450a7..aacd53922 100644 --- a/code/qcommon/q_shared.h +++ b/code/qcommon/q_shared.h @@ -619,6 +619,7 @@ float Com_Clamp( float min, float max, float value ); char *COM_SkipPath( char *pathname ); const char *COM_GetExtension( const char *name ); void COM_StripExtension(const char *in, char *out, int destsize); +qboolean COM_CompareExtension(const char *in, const char *ext); void COM_DefaultExtension( char *path, int maxSize, const char *extension ); void COM_BeginParseSession( const char *name ); diff --git a/code/qcommon/qcommon.h b/code/qcommon/qcommon.h index 7ecd2c7b4..ad8568c24 100644 --- a/code/qcommon/qcommon.h +++ b/code/qcommon/qcommon.h @@ -542,8 +542,8 @@ issues. #define MAX_SEARCH_PATHS 4096 #define MAX_FILEHASH_SIZE 1024 -extern int foreignQVMsFound; -extern char foreignQVMNames[MAX_ZPATH][MAX_SEARCH_PATHS]; +extern int dangerousPaksFound; +extern char dangerousPakNames[MAX_ZPATH][MAX_SEARCH_PATHS]; // referenced flags // these are in loop specific order so don't change the order diff --git a/code/sys/sys_main.c b/code/sys/sys_main.c index 0763cb39b..7c006d0a9 100644 --- a/code/sys/sys_main.c +++ b/code/sys/sys_main.c @@ -384,6 +384,13 @@ static void* Sys_TryLibraryLoad(const char* base, const char* gamedir, const cha void* libHandle; char* fn; + // Don't load any DLLs that end with the pk3 extension + if (COM_CompareExtension(name, ".pk3")) + { + Com_Printf(S_COLOR_RED "Rejecting DLL named \"%s\"\n", name); + return NULL; + } + *fqpath = 0; fn = FS_BuildOSPath( base, gamedir, fname ); diff --git a/code/unix/linux_glimp.c b/code/unix/linux_glimp.c index 0a4495162..5edc1ced1 100644 --- a/code/unix/linux_glimp.c +++ b/code/unix/linux_glimp.c @@ -1382,6 +1382,12 @@ static qboolean GLW_LoadOpenGL( const char *name ) { qboolean fullscreen; + if ( COM_CompareExtension( name, ".pk3" ) ) + { + Com_Printf( S_COLOR_RED "Rejecting r_glDriver named \"%s\"\n", name ); + return qfalse; + } + ri.Printf( PRINT_ALL, "...loading %s: ", name ); // disable the 3Dfx splash screen and set gamma diff --git a/code/unix/sdl_glimp.c b/code/unix/sdl_glimp.c index a59190da1..08336c0d9 100644 --- a/code/unix/sdl_glimp.c +++ b/code/unix/sdl_glimp.c @@ -1065,6 +1065,12 @@ static qboolean GLW_LoadOpenGL( const char *name ) { qboolean fullscreen; + if ( COM_CompareExtension( name, ".pk3" ) ) + { + Com_Printf( S_COLOR_RED "Rejecting r_glDriver named \"%s\"\n", name ); + return qfalse; + } + ri.Printf( PRINT_ALL, "...loading %s:\n", name ); // disable the 3Dfx splash screen and set gamma diff --git a/code/win32/win_glimp.c b/code/win32/win_glimp.c index e565d4d8d..081956309 100644 --- a/code/win32/win_glimp.c +++ b/code/win32/win_glimp.c @@ -1243,6 +1243,12 @@ static qboolean GLW_LoadOpenGL( const char *drivername ) char buffer[1024]; qboolean cdsFullscreen; + if ( COM_CompareExtension( drivername, ".pk3" ) ) + { + Com_Printf( S_COLOR_RED "Rejecting r_glDriver named \"%s\"\n", drivername ); + return qfalse; + } + Q_strncpyz( buffer, drivername, sizeof(buffer) ); Q_strlwr(buffer); From afde689647d2294484e04ab9c7d86fd3f4f83a4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Thomas?= Date: Wed, 22 Mar 2017 22:24:32 +0100 Subject: [PATCH 2/2] Make sure FS_SV_Rename can rename to .pk3 files --- code/client/cl_curl.c | 2 +- code/client/cl_parse.c | 2 +- code/qcommon/files.c | 6 ++++-- code/qcommon/qcommon.h | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/code/client/cl_curl.c b/code/client/cl_curl.c index 3231ef0fc..146a63309 100644 --- a/code/client/cl_curl.c +++ b/code/client/cl_curl.c @@ -350,7 +350,7 @@ void CL_cURL_PerformDownload(void) } FS_FCloseFile(clc.download); if(msg->msg == CURLMSG_DONE && msg->data.result == CURLE_OK) { - FS_SV_Rename(clc.downloadTempName, clc.downloadName); + FS_SV_Rename(clc.downloadTempName, clc.downloadName, qfalse); clc.downloadRestart = qtrue; } else { diff --git a/code/client/cl_parse.c b/code/client/cl_parse.c index 67fab50c0..af40becf7 100644 --- a/code/client/cl_parse.c +++ b/code/client/cl_parse.c @@ -820,7 +820,7 @@ void CL_ParseDownload ( msg_t *msg ) { clc.download = 0; // rename the file - FS_SV_Rename ( clc.downloadTempName, clc.downloadName ); + FS_SV_Rename ( clc.downloadTempName, clc.downloadName, qfalse ); } *clc.downloadTempName = *clc.downloadName = 0; Cvar_Set( "cl_downloadName", "" ); diff --git a/code/qcommon/files.c b/code/qcommon/files.c index 512c1980c..21f2957ec 100644 --- a/code/qcommon/files.c +++ b/code/qcommon/files.c @@ -758,7 +758,7 @@ FS_SV_Rename =========== */ -void FS_SV_Rename( const char *from, const char *to ) { +void FS_SV_Rename( const char *from, const char *to, qboolean safe ) { char *from_ospath, *to_ospath; if ( !fs_searchpaths ) { @@ -777,7 +777,9 @@ void FS_SV_Rename( const char *from, const char *to ) { Com_Printf( "FS_SV_Rename: %s --> %s\n", from_ospath, to_ospath ); } - FS_CheckFilenameIsMutable( to_ospath, __func__ ); + if ( safe ) { + FS_CheckFilenameIsMutable( to_ospath, __func__ ); + } if (rename( from_ospath, to_ospath )) { // Failed, try copying it and deleting the original diff --git a/code/qcommon/qcommon.h b/code/qcommon/qcommon.h index ad8568c24..dffa43f72 100644 --- a/code/qcommon/qcommon.h +++ b/code/qcommon/qcommon.h @@ -587,7 +587,7 @@ fileHandle_t FS_FOpenFileWrite( const char *qpath ); int FS_filelength( fileHandle_t f ); fileHandle_t FS_SV_FOpenFileWrite( const char *filename ); int FS_SV_FOpenFileRead( const char *filename, fileHandle_t *fp ); -void FS_SV_Rename( const char *from, const char *to ); +void FS_SV_Rename( const char *from, const char *to, qboolean safe ); int FS_FOpenFileRead( const char *qpath, fileHandle_t *file, qboolean uniqueFILE ); // if uniqueFILE is true, then a new FILE will be fopened even if the file // is found in an already open pak file. If uniqueFILE is false, you must call