source-sdk-2013 icon indicating copy to clipboard operation
source-sdk-2013 copied to clipboard

Access Violation during datatable encoding/decoding when the local network backdoor is enabled.

Open RoyaleNoir opened this issue 11 months ago • 1 comments

Attempting to compile any of the included singleplayer games (hl2, episodic, lostcoast) and run the resulting mod causes an Access Violation / Segmentation Fault in SendProxy_AnimTime, as pStruct is set to -1 whenever a server starts.

Previously, the singleplayer games worked fine even on the multiplayer branch and were even able to take advantage of new features.

This error does not trigger when maxplayers is set to 2, or when cl_localnetworkbackdoor is set to 0, implying the issue is with the local network backdoor part of the engine, which is disabled in multiplayer.

RoyaleNoir avatar Feb 18 '25 21:02 RoyaleNoir

Might also affect the "video stress test" in CS:S, according to a VDC edit. https://developer.valvesoftware.com/w/index.php?title=Source_2013&diff=next&oldid=464793

SirYodaJedi avatar Feb 25 '25 21:02 SirYodaJedi

Might also affect the "video stress test" in CS:S, according to a VDC edit. https://developer.valvesoftware.com/w/index.php?title=Source_2013&diff=next&oldid=464793

Doesn't seem to affect Video Stress Test on Source Engine Test (sourcetest) on updated Source 2013 Base MP (also on TF2 branch) through. Both maxplayers 1 and cl_localnetworkbackdoor 1.

megakarlach avatar Mar 23 '25 06:03 megakarlach

Doesn't seem to affect Video Stress Test on Source Engine Test (sourcetest) on updated Source 2013 Base MP (also on TF2 branch)

That is because sourcetest only launches the 32-bit version (hl2.exe) when launched from steam, even if you're on a 64-bit OS

RiverHornet3209 avatar Apr 16 '25 10:04 RiverHornet3209

Doesn't seem to affect Video Stress Test on Source Engine Test (sourcetest) on updated Source 2013 Base MP (also on TF2 branch)

That is because sourcetest only launches the 32-bit version (hl2.exe) when launched from steam, even if you're on a 64-bit OS

Can confirm aswell. CS: Source Video Stress Test didn't crash on 32-bit, but does on 64-bit.

megakarlach avatar May 22 '25 07:05 megakarlach

CS: Source Video Stress Test didn't crash on 32-bit, but does on 64-bit.

It crashes on 64-bit if cl_localnetworkbackdoor is set to 1 (The crash also occurs on the prerelease branch)

Also, cl_localnetworkbackdoor is set to 1 by default

RiverHornet3209 avatar Aug 10 '25 10:08 RiverHornet3209

I wonder, what does cl_localnetworkbackdoor even do? Enables some network optimizations in singleplayer doesn't help, it just vague. Like what does it do exactly?

CoolDude034 avatar Aug 12 '25 16:08 CoolDude034

It makes data tables get transmitted by directly performing memcopies to the local client instead of doing serialization and sending via a local network loopback which is slower.

copperpixel avatar Aug 12 '25 17:08 copperpixel

~~I reverse engineered related functions and I believe the issue is due to the struct base pointer getting cast to an int when used in prop offset computation. While this is valid when targeting 32-bit due to an int and a pointer having the same size, when targeting 64-bit a pointer is 64-bits and an int is 32-bits, so after a cast the higher bits get truncated. This causes the offsets to become garbage.~~

~~Here's a snippet of the disassembly as pseudo-C:~~

//...
int64 nStructBase = var_878[ ( uint64 ) * ( uint8 * )( *( uint64 * )( ( char * )pStructBaseMaybe + 0x68 ) + i_5 ) ];

if ( nStructBase )
{
	int32 r12_1 = 0;
	void *pPropMaybe = *( uint64 * )( *( uint64 * )( ( char * )pPrecalc + 0x48 ) + rbp_1 );
	int32 j_2 = 1;
	int32 nOffsetMaybe = *( uint32 * )( ( char * )pPropMaybe + 0x70 ) - 1 + ( uint32 )nStructBase;

	if ( *( uint32 * )( ( char * )pPropMaybe + 0x10 ) == 5 )
	{
		r12_1 = *( uint32 * )( ( char * )pPropMaybe + 0x34 );
		j_2 = *( uint32 * )( ( char * )pPropMaybe + 0x30 );
		nOffsetMaybe = *( uint32 * )( *( uint64 * )( ( char * )pPropMaybe + 0x20 ) + 0x70 ) - 1 + ( uint32 )nStructBase;
	}
	//...

~~Probably how the affected part looks in the source code:~~

int nOffsetMaybe = pPropMaybe->GetOffset() + ( int )pStructBase - 1;

~~Should be something like this instead:~~

int nOffsetMaybe = pPropMaybe->GetOffset() + ( intp )pStructBase - 1;

~~Same goes for when handling array prop offsets~~

copperpixel avatar Aug 12 '25 21:08 copperpixel

Replying to https://github.com/ValveSoftware/source-sdk-2013/issues/610#issuecomment-3181053579

This is not correct, the offsets are always within 32-bit range. The issue is caused by a 32-bit comparison being performed instead of 64-bit comparison in other parts of engine code

The following patches can be temporarily used to fix local network backdoor (as well as an issue with the signon buffer being too small for TF2, and GC being unavailable) on SDK2013. Windows only, Linux is an exercise for the reader:

	mem::pointer ptr_CBaseServer__Clear_signon = mem::scan( mem::pattern( "75 4A 48 8B 07" ), executable_engine );
	mem::pointer ptr_LocalTransfer_TransferEntity = mem::scan( mem::pattern( "48 89 5C 24 ? 48 89 74 24 ? 48 89 7C 24 ? 55 41 54 41 55 41 56 41 57 48 8D AC 24 ? ? ? ? B8" ), executable_engine );
	mem::pointer ptr_SV_ActivateServer_Steam = mem::scan( mem::pattern( "7E 6D E8 ?? ?? ?? ?? " ), executable_engine );
	if ( ptr_CBaseServer__Clear_signon && ptr_LocalTransfer_TransferEntity && ptr_SV_ActivateServer_Steam )
	{
		// use MP buffer size
		// jnz -> jmp
		{
			mem::protect write( { ptr_CBaseServer__Clear_signon, 1 } );
			*ptr_CBaseServer__Clear_signon.as<byte*>() = 0xEB;
		}
	
		// fix signed 32bit comparison
		// mov r10d, 0FFFFFFFFh
		// ->
		// xor r10, r10
		// not r10
		{
			mem::region rgn_LocalTransfer_TransferEntity = { ptr_LocalTransfer_TransferEntity, 0x4A2 };
			std::vector< mem::pointer > vec_Sequences = mem::scan_all( mem::pattern( "41 BA FF FF FF FF" ), rgn_LocalTransfer_TransferEntity );
	
			if ( vec_Sequences.size() == 5 )
			{
				byte payload [] = { 0x4d, 0x31, 0xd2, 0x49, 0xf7, 0xd2 };
				for ( mem::pointer ptr : vec_Sequences )
				{
					mem::protect write( { ptr, sizeof( payload ) } );
					memcpy( ptr.as<void*>(), payload, sizeof( payload ) );
				}
			}
		}
	
		// keep steam connected in singleplayer for GC
		{
			// jle -> nop
			mem::protect write( { ptr_SV_ActivateServer_Steam, 2 } );
			write.fill( NOP );
		}
	}

Note that for TF2 there is still several other singleplayer-related issues on the game side, which I will make a PR for the in the future, but this allows atleast booting maps successfully.

ficool2 avatar Aug 13 '25 13:08 ficool2