Open leonid9851 opened 5 years ago
Plugins like Metamod can interfere with normal operation of games so i'd advise reporting issues occurring on vanilla servers only, or reproducing issues on vanilla servers if you've encountered them on modified servers.
In cases like this reporting them anyway is a good thing since it seems to be happening regardless of plugins though.
I looked into this and it seems that this line is causing it: https://github.com/ValveSoftware/halflife/blob/c7240b965743a53a29491dd49320c88eecf6257b/dlls/func_tank.cpp#L496
The controller is non-null but invalid, i suspect the following scenario occurred:
Fixing it requires disconnecting clients to stop using func_tank. This should be done somewhere in here: https://github.com/ValveSoftware/halflife/blob/c7240b965743a53a29491dd49320c88eecf6257b/dlls/client.cpp#L97-L136
I would recommend moving this bit: https://github.com/ValveSoftware/halflife/blob/c7240b965743a53a29491dd49320c88eecf6257b/dlls/client.cpp#L120-L133
Into a new method:
void CBasePlayer::OnDisconnect()
{
CSound *pSound = CSoundEnt::SoundPointerForIndex( CSoundEnt::ClientSoundIndex( edict() ) );
{
// since this client isn't around to think anymore, reset their sound.
if ( pSound )
{
pSound->Reset();
}
}
// since the edict doesn't get deleted, fix it so it doesn't interfere.
pev->takedamage = DAMAGE_NO;// don't attract autoaim
pev->solid = SOLID_NOT;// nonsolid
UTIL_SetOrigin ( pev, pev->origin );
if ( m_pTank != NULL )
{
// Stop controlling the tank
m_pTank->Use( this, this, USE_OFF, 0 );
m_pTank = NULL;
}
}
And then calling it in ClientDisconnect
.
This applies to all GoldSource engine games since they use the same code for tanks.
@mikela-valve this must be fixed.
It is an easy exploit to crash a server...
On Pawn language, it had helped me already:
#include <amxmodx>
#include <fakemeta>
#include <hamsandwich>
#include <engine>
#define PLUGIN "Fix Tank"
#define VERSION "1.8.2"
#define AUTHOR "SamVanheer"
#define USE_OFF 0
const m_pTank = 1408;
new iEnt;
public plugin_init(){
register_plugin(PLUGIN, VERSION, AUTHOR);
iEnt = find_ent_by_class(-1, "func_tank");
}
public client_disconnect(id){
if(!iEnt) return PLUGIN_CONTINUE;
static iCheck; iCheck = get_pdata_ent(id, m_pTank);
if(iCheck != 0){
ExecuteHamB(Ham_Use, iEnt, id, id, USE_OFF, 0.0);
set_pdata_int(id, m_pTank, false, 20);
}
return PLUGIN_CONTINUE;
}
That plugin only works if there is only one func_tank in the map.
Yeap. Are there any other classes work like func_tank in cs ? That must be enough, mustn't it?
There is more than one type of func_tank, but if there is more than one func_tank entity instance it also won't work. Your code will make the first func_tank stop being controlled, even if the disconnecting player was using another tank.
According to this: https://wiki.alliedmods.net/AMX_Mod_X_1.9_API_Changes
You can use get_pdata_ehandle
and set_pdata_ehandle
to directly use the tank ehandle. Use that instead of locating the func_tank separately.
See documentation here: https://www.amxmodx.org/api/fakemeta/get_pdata_ehandle https://www.amxmodx.org/api/fakemeta/set_pdata_ehandle
This should work with any number of func_tanks, regardless of type.
Hm.. really, that's work just for a one. I have Amxx 1.8.2, 'cause I don't know why, but my Jb engine is falling from time to time with 1.9.0 without any reasons, instead of 1.8.2 .
I think smth like that should be the solution for a several func_tank entities(not a great, but anyway..), correct me if I'm wrong:
#include <amxmodx>
#include <fakemeta>
#include <hamsandwich>
#include <engine>
#define PLUGIN "Fix Tank"
#define VERSION "1.8.2"
#define AUTHOR "SamVanheer"
#define USE_OFF 0
const m_pTank = 1408;
new g_fwrdSpawn;
static Array:g_DataEntities, iSize, bool:g_CheckFuncs;
public plugin_precache(){
g_DataEntities=ArrayCreate(4);
g_fwrdSpawn = register_forward(FM_Spawn, "HookSpawn");
}
public HookSpawn(iEnt)
{
if(!pev_valid(iEnt)) return FMRES_IGNORED;
static szClassName[33]; pev(iEnt, pev_classname, szClassName, charsmax(szClassName));
if(equal(szClassName, "func_tank")){
ArrayPushCell(g_DataEntities, iEnt);
if(!g_CheckFuncs) g_CheckFuncs = true;
}
return FMRES_IGNORED;
}
public plugin_init(){
unregister_forward(FM_Spawn, g_fwrdSpawn);
iSize = ArraySize(g_DataEntities);
register_plugin(PLUGIN, VERSION, AUTHOR);
}
public client_disconnect(id){
if(!g_CheckFuncs) return PLUGIN_CONTINUE;
static iCheck; iCheck = get_pdata_ent(id, m_pTank);
if(iCheck != 0){
for( new iStr; iStr < iSize; iStr++ )
{
new iEnt = ArrayGetCell(g_DataEntities,iStr);
ExecuteHamB(Ham_Use, iEnt, id, id, USE_OFF, 0.0);
}
set_pdata_int(id, m_pTank, false, 20);
}
return PLUGIN_CONTINUE;
}
Hope it won't crash your server :)
func_tank isn't the only name of that entity. It can be func_tankmortar, func_tankrocket or func_tanklaser too (I don't know if there are more).
Is m_pTank offset returning true or false only? Is there another way to check the entity player is using?
The way you are currently using in the plugin can be improved a lot.
is this fixed on rehlds / regamedll ?
@RauliTop, I've done it simply for my server and there was just one thing for a using. U could do next for a several classes:
if(equal(szClassName, "func_tank"))
----------------------------------------->
if(equal(szClassName, "func_tank") || equal(szClassName, "func_tankrocket") || equal(szClassName, "func_tanklaser"))
or make these checks of the classnames in a cycle one by one. I think the solution of @SamVanheer is the best with
get_pdata_ehandle/set_pdata_ehandle
@justgo97, I don't see in Re cleaning data with using tank, when a player is leaving the server.
- A player was controlling a func_tank
- The player disconnected
- Another player connected into the same slot, causing the disconnected player's CBasePlayer instance to be freed, invalidating pointers to it
- The tank's m_pController pointer wasn't zeroed out because there is no code to handle player disconnection while controlling tanks
- The func_tank thinks and tries to access the controller's variables, causing a segfault
Impossible to reproduce it for me. This is what I tried:
I'm pretty sure there is a crash problem with this entities. I have blocked them at the past on my server because were doing crashes when players were using them constantly.
Probably there's another way to produce the crash or I forgot something.
@leonid9851 and @SamVanheer , did you test something?
I tested this with a vanilla Half-Life SDK build and in that codebase it does stop controlling the tank because of this: https://github.com/ValveSoftware/halflife/blob/c7240b965743a53a29491dd49320c88eecf6257b/dlls/client.cpp#L135 https://github.com/ValveSoftware/halflife/blob/c7240b965743a53a29491dd49320c88eecf6257b/dlls/multiplay_gamerules.cpp#L501 https://github.com/ValveSoftware/halflife/blob/c7240b965743a53a29491dd49320c88eecf6257b/dlls/player.cpp#L818-L822
However i don't see this code in Counter-Strike's version of CBasePlayer::RemoveAllItems
.
I did some further testing and i found that disconnecting and reconnecting reallocated the CBasePlayer
instance at the same memory location, so the pointer becomes valid again.
I'm guessing the presence of Metamod and AMXMod resulted in additional memory allocations that alter the address and eventually causes the pointer to be invalid, resulting in a segfault.
Re-adding the code to stop using tanks in CBasePlayer::RemoveAllItems
should fix this, but i don't expect it will work when using another gamerules (CHalfLifeRules
, though i don't see any uses of it). CHalfLifeTraining
inherits from CHalfLifeMultiplay
so it should work with that.
I did some further testing and i found that disconnecting and reconnecting reallocated the
CBasePlayer
instance at the same memory location, so the pointer becomes valid again.I'm guessing the presence of Metamod and AMXMod resulted in additional memory allocations that alter the address and eventually causes the pointer to be invalid, resulting in a segfault.
Do you mean disconnecting and reconnecting the same player? Or is it the same for other player?
The test I realized was in a server using Metamod and AMXModX
The same player, but the engine just picks the first free slot. Getting the underlying memory allocator to return a different address requires you to leave a free chunk of memory that occurs earlier in the list of memory blocks so it's hard to get it to happen in an isolated test.
The same player, but the engine just picks the first free slot. Getting the underlying memory allocator to return a different address requires you to leave a free chunk of memory that occurs earlier in the list of memory blocks so it's hard to get it to happen in an isolated test.
So, the crash will happen only when a player disconnects and another player connects to his slot. It will be impossible to reproduce by one player doing retry, we need two different players.
Is that what you mean?
You can reproduce the issue by allocating the original memory region to another temporary entity:
static int connectCount = 0;
/*
===========
ClientPutInServer
called each time a player is spawned
============
*/
void ClientPutInServer( edict_t *pEntity )
{
CBasePlayer* pDummy = nullptr;
if( connectCount > 0 )
{
pDummy = GetClassPtr<CBasePlayer>( nullptr );
}
++connectCount;
CBasePlayer *pPlayer;
entvars_t *pev = &pEntity->v;
pPlayer = GetClassPtr((CBasePlayer *)pev);
pPlayer->SetCustomDecalFrames(-1); // Assume none;
if( pDummy )
{
REMOVE_ENTITY( pDummy->edict() );
}
// Allocate a CBasePlayer for pev, and call spawn
pPlayer->Spawn() ;
// Reset interpolation during first frame
pPlayer->pev->effects |= EF_NOINTERP;
pPlayer->pev->iuser1 = 0; // disable any spec modes
pPlayer->pev->iuser2 = 0;
}
The first time you connect it allocates the original memory region to your player. Then start controlling a tank, disconnect and reconnect.
This code will allocate the original memory region to a dummy player entity (it's a player to match the size in bytes, which ensures no part of the original region could be assigned to your player entity), allocates another region to your player entity, and frees the dummy.
If you do this then the game will crash:
> hl.dll!CFuncTank::TrackTarget() Line 496 C++
hl.dll!CFuncTank::Think() Line 478 C++
hl.dll!DispatchThink(edict_s * pent) Line 236 C++
swds.dll!02906fbf() Unknown
The line numbers are off by a few but it's the same code.
Make sure to also check my notes above for disabling the code in CBasePlayer::RemoveAllItems
to reproduce this in the SDK.
I did one more test. Again crash was impossible to reproduce alone.
I notice an important aspect, after disconnect and connect again using a func_tank:
So, it's true that func_tank doesn't reset when a player disconnects using it. Probably the crash will happen when another player tries to control that func_tank after doing all steps above.
Yeah it still copies your angles because the controller pointer points to your CBasePlayer instance.
If you didn't modify the code (either in SDK or using Metamod/AMXMod) then it will pretty much always reuse the same memory region. If you use a pre and post hook for ClientPutInServer
to replicate my code you can probably reproduce the crash.
I finally reproduce the crash.
We need two different players:
I made a plugin to fix it.
I will later post at alliedmodders and edit here. Plugin posted: https://forums.alliedmods.net/showthread.php?t=317763
@mikela-valve seems that it was an attempt to fix this or something related: https://steamcommunity.com/games/70/announcements/detail/2199168953960582492
But the bug stills there. Should be solved on Next release because can cause server crash.
I've fixed this issue like this: https://github.com/Solokiller/halflife-updated/commit/de2751ca4127d0e98bca1d15e9566d1118fbb1c8
It also accounts for the possibility that the player CBaseEntity instance is null (if the player connects and immediately disconnects there could be a small window where this can happen).
Hi there! There's a problem with gamedll, hope u could fix this.
So, let's start :
Build version of hlds is 7882
Modules list:
Crash log: