Source-Python-Dev-Team / Source.Python

This plugin aims to use boost::python and create an easily accessible wrapper around the Source Engine API for scripter use.
http://forums.sourcepython.com
GNU General Public License v3.0
164 stars 32 forks source link

[Linux, CS:S] SRCDS crash when trying to fire certain inputs on certain entities #132

Closed KirillMysnik closed 8 years ago

KirillMysnik commented 8 years ago

Map to reproduce: cs_office (contains trigger_once that opens garage door on CT spawn) Code to reproduce:

from filters.entities import EntityIter

def load():
    for entity in EntityIter('trigger_once'):
        entity.enable()    # entity.disable crashes too

Tested on different linux servers. Also affects trigger_multipe and trigger_teleport - that's what I tested on.

jordanbriere commented 8 years ago

I can't reproduce the crash - work fine for me. Try disabling everything except SP and the snippet you posted above.

KirillMysnik commented 8 years ago

I did a clean install of SRCDS and installed latest SP over it. It crashes.

jordanbriere commented 8 years ago

Does the following also crashes?

from entities.datamaps import InputData
from filters.entities import EntityIter
from memory import Convention
from memory import DataType

def load():
    for entity in EntityIter('trigger_once'):
        desc = entity.datamap.find('Enable')
        func = desc.function.make_function(Convention.THISCALL,
            (DataType.POINTER, DataType.POINTER), DataType.VOID)
        func(entity, InputData())
KirillMysnik commented 8 years ago

Yes. I will try to find some logs.

crash_20160620034624_1.dmp[7582]: Uploading dump (out-of-process)
/tmp/dumps/crash_20160620034624_1.dmp
Segmentation fault (core dumped)
Add "-debug" to the ./srcds_run command line to generate a debug.log to help with solving this problem

Already added -debug to the cmd line, but I can't find said debug.log anywhere

Edit: server version 3398447 - just downloaded SP build 374 cs_office Debian 8.5 & Ubuntu 15.10 Ubuntu GLIBC 2.21-0ubuntu4.1

jordanbriere commented 8 years ago

What if you create the entity on your own instead of using existing ones? Also, what if you use the "Toggle" input instead of Enable/Disable?

KirillMysnik commented 8 years ago
from entities.entity import Entity

def load():
    entity = Entity.create('trigger_once')
    entity.spawn()
    entity.toggle()

Crashes. So, yes, Toggle crashes too. And even brushless trigger_once crashes.

Edit: Not all entities are affected, just to make sure:

from filters.entities import EntityIter

def load():
    for entity in EntityIter('func_button'):
        entity.lock()

doesn't crash.

EDIT: I've added "Linux" tag not because I haven't tested it on Windows but because on WIndows it works just fine. Also, I have a feeling like this code wasn't crashing just yesterday (and I haven't updated SP since yesterday for sure). But I tested it on 2 different machines, and on a cleanly installed SRCDS too. I'm sure there weren't any updates since yesterday, were they?

jordanbriere commented 8 years ago

What if you disable SP, and use "ent_fire trigger_once Enable" in your client console? (while sv_cheats is enabled and you are authentified by rcon)

KirillMysnik commented 8 years ago

Tried that with SP enabled, did not crash

jordanbriere commented 8 years ago

What if you set activator/caller to a valid player?

KirillMysnik commented 8 years ago

Quick update: the following code does not crash

from entities.datamaps import InputData
from filters.entities import EntityIter
from memory import Convention, DataType, find_binary

INPUT_DISABLE_IDENTIFIER = "_ZN20CBaseVPhysicsTrigger12InputDisableER11inputdata_t"

server = find_binary('server')

input_disable = server[INPUT_DISABLE_IDENTIFIER].make_function(
    Convention.THISCALL,
    (
        DataType.POINTER,
        DataType.POINTER,
    ),
    DataType.VOID,
)

def load():
    for entity in EntityIter('trigger_once'):
        input_disable(entity.pointer, InputData())

(calling InputDisable directly).

I've set up a clean Lubuntu virtual machine, clean SRCDS install with the latest (374) SP. Still crashes.

Now I will try setting activator/caller.

KirillMysnik commented 8 years ago

Yes,

from filters.entities import EntityIter

def load():
    for entity in EntityIter('trigger_once'):
        entity.enable(activator=entity.index, caller=entity.index)

still crashes. Sorry for not setting this to a valid player, but it's still a valid entity.

L'In20Cible, what setup are you running on? It seems to me that it crashes only for me, because decompile wasn't able to crash it, too.

jordanbriere commented 8 years ago

CBaseVPhysicsTrigger, that class is not even into the CTriggerOnce hierarchy. It is inherited by CTriggerVPhysicsMotion, CTriggerWind, etc. but not CTriggerOnce:

CTriggerOnce → CTriggerMultiple → CBaseTrigger → CBaseToggle → CBaseEntity → ...

What is the outputs you get with the following:

from core import PLATFORM
from entities.entity import BaseEntity
from entities.datamaps import InputData
from memory import get_object_pointer
from memory import Convention
from memory import DataType
from memory.hooks import PreHook

entity = BaseEntity.find_or_create('trigger_once')

print('trigger_once found or created with index:', entity.index)

func = entity.datamap.find('Enable').function.make_function(
    Convention.THISCALL,
    (DataType.POINTER, DataType.POINTER), DataType.VOID)

print('InputEnable found into descriptor at address:', func.address)

ptr = get_object_pointer(entity)
vfunc = ptr.get_virtual_func(190 if PLATFORM == 'linux' else 189).make_function(
    Convention.THISCALL,
    (DataType.POINTER, DataType.POINTER), DataType.VOID)

print('InputEnable found into vtable at address:', vfunc.address)

@PreHook(func)
def _pre_func(stack):
    print('InputEnable found into descriptor called with args:', stack[0].address, stack[1].address)

@PreHook(vfunc)
def _pre_vfunc(stack):
    print('InputEnable found into vtable called with args:', stack[0].address, stack[1].address)

data = InputData()

# This should call the function found into the datamap which should jump to the vtable one, so both hooks should be called here.
func(entity, data)

# Only the vtable hook should be called here.
vfunc(entity, data)

entity.destroy()
KirillMysnik commented 8 years ago
trigger_once found or created with index: 468
InputEnable found into descriptor at address: 761
InputEnable found into vtable at address: 4062562240
Uploading dump (in-process) [proxy '']
/tmp/dumps/assert_20160620065421_1.dmp
success = no
error:  libcurl.so: cannot open shared object file: No such file or directory
Segmentation fault (core dumped)
Add "-debug" to the ./srcds_run command line to generate a debug.log to help with solving this problem
jordanbriere commented 8 years ago

761 is not a valid address - that's why it crashes. What if you replace the following line:

func = entity.datamap.find('Enable').function.make_function(
    Convention.THISCALL,
    (DataType.POINTER, DataType.POINTER), DataType.VOID)

WIth:

func = get_object_pointer(entity.datamap.find('Enable')).get_pointer(28).make_function(
    Convention.THISCALL,
    (DataType.POINTER, DataType.POINTER), DataType.VOID)
KirillMysnik commented 8 years ago
trigger_once found or created with index: 468
InputEnable found into descriptor at address: 761
InputEnable found into vtable at address: 4062562240
Uploading dump (in-process) [proxy '']
/tmp/dumps/assert_20160620085320_1.dmp
success = no
jordanbriere commented 8 years ago

Then that means the descriptor itself is storing an invalid function address (or we are misinterpreting it?). Not sure why, this will take some time to figure out.

jordanbriere commented 8 years ago

Out of curiousity, if you run the following:

from entities.entity import BaseEntity
from entities.datamaps import InputData
from entities.datamaps import TypeDescriptionFlags

entity = BaseEntity.find_or_create('trigger_once')

datamap = entity.datamap
while datamap:
    for desc in datamap:
        if not desc.flags & TypeDescriptionFlags.INPUT:
            continue
        print(datamap.class_name, desc.name, desc.function.address)
    datamap = datamap.base

entity.destroy()

Does only the CBaseTrigger's inputs looks invalid?

EDIT: My guess here is that only the inputs that are virtual are fucked up.

KirillMysnik commented 8 years ago

I will run it when I get home (2-3 hours). I'm still wondering why this bug only happens for me.

KirillMysnik commented 8 years ago
CBaseTrigger InputEnable 761
CBaseTrigger InputDisable 765
CBaseTrigger InputDisableAndEndTouch 769
CBaseTrigger InputToggle 773
CBaseTrigger InputTouchTest 777
CBaseTrigger InputStartTouch 781
CBaseTrigger InputEndTouch 785
CBaseEntity m_iInitialTeamNum 0
CBaseEntity InputSetTeam 4059752512
CBaseEntity InputKill 4059753552
CBaseEntity InputKillHierarchy 4059753264
CBaseEntity InputUse 4059752384
CBaseEntity InputAlpha 4059769376
CBaseEntity InputAlternativeSorting 4059766464
CBaseEntity InputColor 4059768256
CBaseEntity InputSetParent 4059783376
CBaseEntity InputSetParentAttachment 4059802192
CBaseEntity InputSetParentAttachmentMaintainOffset 4059802112
CBaseEntity InputClearParent 4059752448
CBaseEntity InputSetDamageFilter 4059753040
CBaseEntity InputEnableDamageForces 4059780992
CBaseEntity InputDisableDamageForces 4059780768
CBaseEntity InputDispatchEffect 4059759616
CBaseEntity InputDispatchResponse 4059759536
CBaseEntity InputAddContext 4059832000
CBaseEntity InputRemoveContext 4059813424
CBaseEntity InputClearContext 4059752592
CBaseEntity InputDisableShadow 4059753984
CBaseEntity InputEnableShadow 4059781024
CBaseEntity InputAddOutput 4059759344
CBaseEntity InputFireUser1 4059754784
CBaseEntity InputFireUser2 4059754736
CBaseEntity InputFireUser3 4059754688
CBaseEntity InputFireUser4 4059754640
jordanbriere commented 8 years ago

I guess I was right. Did you notice how they are sorted the same as they are ordered in the class structure and how there is 4 bytes between each others? Definitely something going on with how the addresses are set (or extracted?).

NOTE: The _miInitialTeamNum being 0 is not a bug. Simply forgot to exclude KEY descriptors in the code I posted above.

If you run the following:

from core import PLATFORM
from entities.entity import BaseEntity
from entities.datamaps import InputData
from memory import get_object_pointer
from memory import Convention
from memory import DataType
from memory.hooks import PreHook

entity = BaseEntity.find_or_create('trigger_once')

print('trigger_once found or created with index:', entity.index)

ptr = get_object_pointer(entity)
vfunc = ptr.get_virtual_func(190 if PLATFORM == 'linux' else 189).make_function(
    Convention.THISCALL,
    (DataType.POINTER, DataType.POINTER), DataType.VOID)

print('InputEnable found into vtable at address:', vfunc.address)

@PreHook(vfunc)
def _pre_vfunc(stack):
    print('InputEnable found into vtable called with args:', stack[0].address, stack[1].address)

And use ent_fire trigger_once Enable (again, with sv_cheats on and rcon auth/autokick disabled), does the hooked function get called?

Ayuto commented 8 years ago

We can easily find out whether an input function is virtual or not:

print('Virtual input functions:')
entity = BaseEntity.create('trigger_once')
datamap = entity.datamap
while datamap:
    for desc in datamap:
        if not desc.flags & TypeDescriptionFlags.INPUT:
            continue

        addr = desc.function.address
        if not addr & 1:
            continue

        vtable_index = int((addr - 1) / 4)
        print(datamap.class_name, desc.name, vtable_index)

    datamap = datamap.base

entity.destroy()

Output:

Virtual input functions:
CBaseTrigger InputEnable 190
CBaseTrigger InputDisable 191
CBaseTrigger InputDisableAndEndTouch 192
CBaseTrigger InputToggle 193
CBaseTrigger InputTouchTest 194
CBaseTrigger InputStartTouch 195
CBaseTrigger InputEndTouch 196

See also: https://github.com/Source-Python-Dev-Team/Source.Python/blob/master/src/core/modules/memory/memory_function_info.h#L88-L112

And since they are virtual that attribute can't really store a function address.

KirillMysnik commented 8 years ago

And use ent_fire trigger_once Enable (again, with sv_cheats on and rcon auth/autokick disabled), does the hooked function get called?

I tried that through server console (with sv_cheats 1) - no output. It didn't get called.

jordanbriere commented 8 years ago

@Ayuto Awesome! Didn't realize those could have been the offsets directly. Guess windows is optimizing by setting the addresses. That's quite easy to implement.

@KirillMysnik Well, first of all that command won't work from the server console as it requires a player as caller/activator. Like mentionned, you need to set sv_cheats 1 on the server, and execute that command on your client-console while you are authentified by rcon (or you have the autokick flag removed with mp_disable_autokick, for example).

KirillMysnik commented 8 years ago
autokick is disabled for iPlayer
InputEnable found into vtable called with args: 191310432 4288164556
InputEnable found into vtable called with args: 191312224 4288164556

Guess there were two of trigger_once's

jordanbriere commented 8 years ago

https://github.com/Source-Python-Dev-Team/Source.Python/commit/bc0573907d9f0ddcf68a5fb17a93cc4fb5bd8f6d should fix this issue. Build 416 should be available in few minutes. I will close this issue once you confirmed it works for you too, @KirillMysnik.

Ayuto commented 8 years ago

I think you now broke input functions on Windows. That little snippet is only for Linux.

We should also apply the fix to the input_function attribute.

KirillMysnik commented 8 years ago

I can confirm that the snippet from the first post does not crash anymore.

KirillMysnik commented 8 years ago

Windows SRCDS now produces the following exception

[SP] Caught an Exception: 
Traceback (most recent call last): 
  File '..\addons\source-python\packages\source-python\plugins\manager.py', line 81, in __missing__ 
    instance.globals['load']() 
  File '..\addons\source-python\plugins\input_test\input_test.py', line 5, in load 
    entity.enable()    # entity.disable crashes too 
  File '..\addons\source-python\packages\source-python\entities\entity.py', line 99, in __getattr__ 
    return getattr(make_object(server_class, self.pointer), attr) 
  File '..\addons\source-python\packages\source-python\entities\classes.py', line 528, in fget 
    func = desc.function 

AttributeError: 'TypeDescription' object has no attribute 'function' 
Ayuto commented 8 years ago

The function attribute has been added lately. Updating your installation should fix the issue.

jordanbriere commented 8 years ago

I think you now broke input functions on Windows. That little snippet is only for Linux.

I originally thought about adding a check against PLATFORM but thought it was useless. Virtual inputs are valid addresses (thunk?) on windows and you are going to crash whenever it passes the & 1 condition as they are not valid in such cases.

We should also apply the fix to the input_function attribute.

Good point, didn't think about that one.

jordanbriere commented 8 years ago

Went ahead and still added the check against PLATFORM as you are right, better be safe than sorry. Also added the operator change @Mahi proposed.

Will add a fix for input_function later today. Though, if you got the time to, go ahead.

Thanks @KirillMysnik for confirming it worked for you.

Ayuto commented 8 years ago

Just tested input_function and it's working fine, so there is no need to apply that fix.

However, it's another reason to work on this TODO: https://github.com/Source-Python-Dev-Team/Source.Python/blob/1fdd379f1d0f956f84f547c38584ce3226830c84/addons/source-python/packages/source-python/entities/classes.py#L534-L536

jordanbriere commented 8 years ago

Doing so, we would lose the ability to register hooks on InputFunction instances, which is a regression.