Skip to content

Conversation

@z33ky
Copy link

@z33ky z33ky commented Jun 11, 2025

This inserts some checks on the script instance for which to invoke a native member function on.

The first commit prevents this from crashing(/corrupting) the game:

CBaseEntity.BreakIt <- CBaseEntity.GetClassname.bindenv(this)
local ent = Entities.FindByName(null,"*")
ent.BreakIt()

and the second commit, this

CBaseEntity.BreakIt <- CScriptKeyValues.FindOrCreateKey
local ent = Entities.FindByName(null,"*")
ent.BreakIt("")

PR Checklist

  • My PR follows all guidelines in the CONTRIBUTING.md file
  • My PR targets a develop branch OR targets another branch with a specific goal in mind

@samisalreadytaken
Copy link

A simpler test code would be:

access uninitialised stack variable self - sq_getinstanceup fails but self is not null

CBaseEntity.GetHealth.call( null )

interpret CHL2_Player* as CTriggerCamera* - no crash because offsets are valid

CTriggerCamera.GetFov.call( player )

crash via invalid offset

CScriptKeyValues.FindOrCreateKey.call( player, "" )

The results of sq_getinstanceup and sq_gettypetag are also unchecked in SQVector::Construct and constructor_stub.

access uninitialised stack variable

Vector.constructor.call( null )

crash via null ptr deref

class C{}
CBaseEntity.constructor.call( C() )

crash via invalid offset

CBaseEntity.constructor.call( Vector() )

get/set/tostring_stub are only accessible through the squirrel debugger, not through code.

@z33ky
Copy link
Author

z33ky commented Jun 18, 2025 via email

@z33ky z33ky force-pushed the vscript-member-function-call-safety branch from 3e2f0f9 to 7b96eb0 Compare June 23, 2025 19:41
@z33ky
Copy link
Author

z33ky commented Jun 23, 2025

I tested that all of the provided scripts throw an error (and none cause a crash).
I'm not 100% confident in the commit descriptions, it's some guess work limited by my marginal understanding of the Squirrel VM.

@z33ky z33ky force-pushed the vscript-member-function-call-safety branch from 7b96eb0 to bc412a3 Compare June 23, 2025 19:48
… has userpointer type

This prevents memory errors when incorrectly invoking native member
functions from Squirrel.
@z33ky z33ky force-pushed the vscript-member-function-call-safety branch from bc412a3 to f6b27ab Compare June 23, 2025 20:37
@z33ky
Copy link
Author

z33ky commented Jun 23, 2025

The latest changes also fix CBaseEntity.GetHealth.call( Vector() ) and

class C{}
Vector.constructor.call( C() )

Copy link

@samisalreadytaken samisalreadytaken left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Expected class userpointer" error doesn't really make sense, the data is attached to the instance. sq_getinstanceup already sets the error message, I feel like we could just let that pass and return SQ_ERROR. "invalid type tag" error is as vague as "Expected userpointer" to the end user anyway.

The way to access get/set/tostring_stub through the debugger is to view class metamethods, copy the function address, then call or assign it from variable watches (*0x00000000).call(null). Exploiting this requires a malicious client to connect locally or through an open port to the debugger which the user had to have enabled themselves. It doesn't really hurt to check for them too though.

Somewhat related issues I just noticed that you could fix here if you want to:

sq_resetobject(&pSquirrelVM->lastError_) lines in constructor_stub and function_stub can be removed since that object can only change through SquirrelVM::RaiseException which should only be called from function_stub binding call m_pfnBinding() where lastError_ is reset if it was changed.

The error message is also leaked; sq_release should be called on lastError_ after pushing it to the stack on if (!sq_isnull(pSquirrelVM->lastError_)) check in function_stub.

In getVariant() OT_INSTANCE case, sq_gettypetag check can be removed because it is only used for TYPETAG_VECTOR validation which is already done by sq_getinstanceup.

An irrelevant issue, SQVector::{_get, _set} are accessing out of bounds at index 48 with key['0']. I remember seeing this years ago but I forgot about it.


SQUserPointer userptr = nullptr;
sq_getuserpointer(vm, top, &userptr);
if (SQ_FAILED(sq_getuserpointer(vm, top, &userptr)))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check isn't needed, it is guaranteed to be a userpointer from SQVM::CallNative due to closure registration sq_newclosure(vm_, function_stub, 1);

This line also could be simplified by removing userptr variable: sq_getuserpointer(vm, top, (SQUserPointer*)&pFunc);

// expect construction to always succeed
Assert(sq_isnull(pSquirrelVM->lastError_));
// expect construction to always succeed
Assert(sq_isnull(pSquirrelVM->lastError_));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sq_getsharedforeignptr line should be after m_pfnConstruct() line because it is only used for lastError_ assertion. This extra block indentation could also be removed, it's a bit out of place in this small function.

{
SQUserPointer self;
sq_getinstanceup(vm, 1, &self, nullptr);
if (SQ_FAILED(sq_getinstanceup(vm, 1, &self, 0)))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could also be ClassInstanceData *self; and sq_getinstanceup(vm, 1, (SQUserPointer*)&self, 0)

Having known types makes it easier to inspect in debuggers.

z33ky added 2 commits June 24, 2025 21:10
…on call

This enhances the ScriptFuncDescriptor_t to record the ScriptClassDesc_t
of the class for which it gets invoked.
The ScriptClassDesc_t are traversed in the function_stub() for native
function calls for member functions, to ensure the passed in instance
has a compatible type.
This prevents memory errors when incorrectly invoking native member
functions from Squirrel.
This prevents manual invocations of the Vector.constructor with an
invalid value.
@z33ky z33ky force-pushed the vscript-member-function-call-safety branch from f6b27ab to b087346 Compare June 24, 2025 19:12
@z33ky
Copy link
Author

z33ky commented Jun 24, 2025

Thanks for the review @samisalreadytaken. I checked off all your suggestions (the 6 points in your overview comment and the 3 inline comments).

SquirrelVM* pSquirrelVM = (SquirrelVM*)sq_getsharedforeignptr(vm);
Assert(pSquirrelVM);
// expect construction to always succeed
Assert(sq_isnull(pSquirrelVM->lastError_));
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this still need a sq_resetobject(&pSquirrelVM->lastError_); before pClassDesc->m_pfnConstruct()? I don't seem to have issues without it though. I guess sq_release() also nulls the object?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not in here, we previously established that constructors don't raise script exceptions; it needs to be reset after returning it to the script on function_stub (I guess I wasn't clear on that). sq_release doesn't nullify the input, RaiseException resetting lastError_ is irrelevant

lastError_ is a temporary object that is only meant to carry its state from within m_pfnBinding() call to function_stub() body.

if lastError_ is not null
	sq_pushobject lastError_
	sq_release lastError_
	sq_resetobject lastError_

This is the same logic of returning the result of RegisterInstance and HSCRIPT_RC if you recall that.

@z33ky z33ky force-pushed the vscript-member-function-call-safety branch 2 times, most recently from 89202e0 to 5355a52 Compare June 24, 2025 20:36
Copy link

@samisalreadytaken samisalreadytaken left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like 2 changes were amended to the wrong commits (sq_getuserpointer line at b578bf4, indentation at 9c2f8da), and the description of "RaiseException resetting lastError" is irrelevant, but those are not important

z33ky added 8 commits July 1, 2025 09:40
…meters

This prevents manual invocations of the native class constructor for
non-class values or non-native classes.
This prevents manual invocations of the native class constructor with an
invalid value.
The function_stub() resets it when used.
Also streamline SQUserPointer usage in sq_getinstanceup() and
sq_getuserpointer() calls to write directly to a pointer of the expected
type.
@z33ky z33ky force-pushed the vscript-member-function-call-safety branch from 5355a52 to c659af5 Compare July 1, 2025 07:46
@Blixibon Blixibon merged commit b3d5152 into mapbase-source:develop Jul 24, 2025
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants