Advertisement

AngelScript crash using disposed script function that was loaded from bytecode

Started by March 23, 2020 03:14 PM
8 comments, last by WitchLord 4 years, 4 months ago

This was driving me insane that Angelscript sometimes ends up with corrupted functions after module cleanup and this took me a while to get around and debug properly.
There seem to be multiple things that need to fall in line for this crash to be produced and I'm not even sure whch part to start from. Did confirm that this bug still exists on version 2.34.

The crash is to do with shared classes and compiled scripts producing duplicate versions of virtual functions for base shared classes.

*scripts
You'll need 4 scripts for an easy scenario.
One for a bunch of functions to later unload and free up engine's function array slots at the front of the array.
One for base class.
One for derived script class that has the base class definition listed after (this bit is super important).
One for derived script class that calls base class virtual function.

*bytecode compilation
How you compile the scripts also matters :(

For an easy test all the scripts should be compiled in isolation except for the last one with base class virtual function call that we'll use to crash the engine. You'll want to create a base class module before making a derived class module that you'll call “SaveByteCode” on. What this will do is write the bytecode 's' instead of ‘m’. The ‘m’ will result in a function search inside the asCModule::scriptFunctions while ‘s’ will result in a search inside asCScriptEngine::scriptFunctions. This is probably one of the issues as asCModule::scriptFunctions already seems to have features to populate with shared functions from other modules.

The other special thing in the bytecode is if you have the base shared class defined after the derived class, then the restoration of the derived class function asCReader::ReadFunction gets the bytecode ‘f’ instead of ‘r’. Because the shared derived class doesn't exist, all the arguments (bool addToModule, bool addToEngine, bool addToGC) are set to true and a new copy of the base shared class function is forced to be created.

*crash
When a duplicate base shared class function gets made, it could end up more in front of the script engine function array than the already loaded original base class module functions. The bytecode loader for our last script would then correctly populates the module script function array with functions from the shared base class module, but the “asCReader::ReadUsedFunctions()” would then use bytecode 's', search through the engine functions for a matchng one and find the new duplicate version that will be recorded for execution. If we now unload the module that created the duplicate shared base class function, well clear that duplicate function content (because it will not find a new owner module). The last script will now crash when trying to use the discarded function.


Took me a while to unravel everything. The bytecode codebase needs few more tweaks. Did not expect to get different results by just changing class definition order. Will need to make a simple example later and submit a bug.
If anyone else were getting invalid weird script function crashes from bytecodes (not sure if this also happens on raw script compilation) the the easiest thing to do would probably be to change the script compiler to include scripts in front of the file but that will mess up all the debug line numbers.

I think I managed to make a simple scenario:

#include <iostream>
#include "../../include/angelscript.h"
#include <string>
#include "assert.h"

//simple bytecode stream
class BytecodeStream : public asIBinaryStream
{
public:
	BytecodeStream() : m_buffer(nullptr), m_size(0), m_capacity(0), m_readIndex(0)
	{}

	int Write(const void* ptr, asUINT size)
	{
		size_t newSize = size + m_size;
		if (newSize > m_capacity)
		{
			do
			{
				m_capacity += 512;
			} while (newSize > m_capacity);

			char* newBuffer = new char[m_capacity];
			if (m_buffer)
			{
				memcpy(newBuffer, m_buffer, m_size);
				delete m_buffer;
			}
			m_buffer = newBuffer;
		}

		if (size == 0) return 0;

		memcpy(m_buffer + m_size, ptr, size);
		m_size += size;

		return 0;
	}
	int Read(void* ptr, asUINT size)
	{
		if (size == 0) return 0;

		memcpy(ptr, m_buffer + m_readIndex, size);
		m_readIndex += size;
		return 0;
	}

	char* m_buffer;

	size_t m_readIndex;
	size_t m_size;
	size_t m_capacity;
};


int CompileScripts(asIScriptEngine* engine, BytecodeStream& stream1, BytecodeStream& stream2, BytecodeStream& stream3, BytecodeStream& stream4);


void MessageCallback(const asSMessageInfo *msg, void *param)
{
	const char *type = "ERR ";
	if (msg->type == asMSGTYPE_WARNING)
		type = "WARN";
	else if (msg->type == asMSGTYPE_INFORMATION)
		type = "INFO";
	printf("%s (%d, %d) : %s : %s\n", msg->section, msg->row, msg->col, type, msg->message);
}


int main(int argCount, char* argVal[])
{
	asIScriptEngine* engine = asCreateScriptEngine();
	if (engine == 0)
	{
		std::cout << "Failed to create script engine." << std::endl;
		return -1;
	}
	engine->SetMessageCallback(asFUNCTION(MessageCallback), 0, asCALL_CDECL);

	int r = 0;

	BytecodeStream stream1;
	BytecodeStream stream2;
	BytecodeStream stream3;
	BytecodeStream stream4;

	CompileScripts(engine, stream1, stream2, stream3, stream4);

	//load some functions to reserve thr front spaces of the script engine function array
	asIScriptModule* mod4 = engine->GetModule("test4", asGM_ALWAYS_CREATE);
	r = mod4->LoadByteCode(&stream4);
	assert(r == 0);

	//load a module with the base script class that should be the owning module of the shared base class functions
	auto* mod1 = engine->GetModule("test1", asGM_ALWAYS_CREATE);
	r = mod1->LoadByteCode(&stream1);
	assert(r == 0);

	//make the front of the script engine function array available for use
	mod4->Discard();
	engine->GarbageCollect();

	//load a derived class module with the base class definition being included afterthe derived class (important!)
	//this will force new base class shared function creation
	auto* mod2 = engine->GetModule("test2", asGM_ALWAYS_CREATE);
	r = mod2->LoadByteCode(&stream2);

	//load a derived class module that was compiled while another module that owned the base class existed (important!)
	//this will result in slightly different bytecode that will do a function search in the script engine's function array
	//(rather than the module's function array) and locate the new duplicate base class shared function introduced by module2
	asIScriptModule* mod3 = engine->GetModule("test3", asGM_ALWAYS_CREATE);
	r = mod3->LoadByteCode(&stream3);
	assert(r == 0);

	//get rid of module2. This will dispose the duplicate shared function because a new owner could not be located
	mod2->Discard();
	engine->GarbageCollect();

	//try executing a function in module3 that calls a base class virtual function.
	//this will crash doe to trying to use the function pointer from module2 rather than module1
	auto test1TypeInfo = mod3->GetTypeInfoByName("Test3");
	auto test1Func = test1TypeInfo->GetMethodByDecl("int function1()");
	auto factoryFunc = test1TypeInfo->GetFactoryByIndex(0);
	
	auto context = engine->RequestContext();
	context->Prepare(factoryFunc);
	context->Execute();

	asIScriptObject* scriptObj = (asIScriptObject*)context->GetReturnObject();
	scriptObj->AddRef();
	context->Prepare(test1Func);
	context->SetObject(scriptObj);
	context->Execute();

	scriptObj->Release();
	engine->ReturnContext(context);


	mod3->Discard();
	engine->GarbageCollect();

	return 0;
}

//file for the base class
const char* file1 = " \
	shared class Test0 { \
	int function1() { return 0; } \
	} \
	";

//file for the first derived class
const char* file2 = " \
	shared class Test2 : Test0 { \
	} \
	shared class Test0 { \
		int function1() { return 0; } \
	} \
	";

//file for the second derived class
const char* file3 = " \
	shared class Test3 : Test0 { \
		int function1() { Test0::function1(); return 1; } \
	} \
	shared class Test0 { \
		int function1() { return 0; } \
	} \
	";

//file for loading in some functions and disposing them to make room
//in the front of the script engine's function array
const char* file4 = " \
	shared class Test4 { \
		int function1() { return 0; }	\
		int function2() { return 0; }	\
		int function3() { return 0; }	\
		int function4() { return 0; }	\
		int function5() { return 0; }	\
		int function6() { return 0; }	\
		int function7() { return 0; }	\
		int function8() { return 0; }	\
		int function9() { return 0; }	\
		int function10() { return 0; }	\
	} \
	";


int CompileScripts(asIScriptEngine* engine, BytecodeStream& stream1, BytecodeStream& stream2, BytecodeStream& stream3, BytecodeStream& stream4)
{
	int r;

	asIScriptModule* mod4 = engine->GetModule("test4", asGM_ALWAYS_CREATE);
	r = mod4->AddScriptSection("test4", file4, strlen(file4));
	assert(r >= 0);
	r = mod4->Build();
	assert(r >= 0);

	r = mod4->SaveByteCode(&stream4);
	assert(r >= 0);

	mod4->Discard();
	engine->GarbageCollect();

	asIScriptModule* mod2 = engine->GetModule("test2", asGM_ALWAYS_CREATE);
	r = mod2->AddScriptSection("test2", file2, strlen(file2));
	assert(r >= 0);
	r = mod2->Build();
	assert(r >= 0);

	r = mod2->SaveByteCode(&stream2);
	assert(r >= 0);

	mod2->Discard();
	engine->GarbageCollect();


	asIScriptModule* mod1 = engine->GetModule("test1", asGM_ALWAYS_CREATE);
	r = mod1->AddScriptSection("test1", file1, strlen(file1));
	assert(r >= 0);
	r = mod1->Build();
	assert(r >= 0);
	r = mod1->SaveByteCode(&stream1);
	assert(r >= 0);

	//! module1 is not discarded before saving bytecode for module3 to produce a different bytecode !

	asIScriptModule* mod3 = engine->GetModule("test3", asGM_ALWAYS_CREATE);
	r = mod3->AddScriptSection("test3", file3, strlen(file3));
	assert(r >= 0);
	r = mod3->Build();
	assert(r >= 0);

	r = mod3->SaveByteCode(&stream3);
	assert(r >= 0);


	mod1->Discard();
	mod3->Discard();
	engine->GarbageCollect();

	return 0;
}
Advertisement

Thanks for letting me know, and for creating the test code. This is obviously a very complex scenario so it would probably have been quite difficult to reproduce without your code.

I'll investigate this and have it fixed as soon as possible.

AngelCode.com - game development and more - Reference DB - game developer references
AngelScript - free scripting library - BMFont - free bitmap font generator - Tower - free puzzle game

@witchlord I'm still seeing other problems where AngelScript can end up with some semi deleted functions. I think the cases I've seen were to do with ownership transfer of shared script classes or c++ template class instances. Modules seem to always record the class types that existed in the module but “scriptFunctions” seem to only store functions that were in use, is that right? I see scenarios where types get ownership transfer to other modules after initial module disposal, but some of the type's functions end up cleaning up because they are not explicitly listed in that other module's "scriptFunctions”. Could only solve this by having an additional step at the end of “asCBuilder::Build()” to go through each module class type and making sure the behavior functions, factory functions and vTable functions were listed in the module's “scriptFunctions”

I've fixed this problem in revision 2640.

AngelCode.com - game development and more - Reference DB - game developer references
AngelScript - free scripting library - BMFont - free bitmap font generator - Tower - free puzzle game

MrFloat said:

@witchlord I'm still seeing other problems where AngelScript can end up with some semi deleted functions. I think the cases I've seen were to do with ownership transfer of shared script classes or c++ template class instances. Modules seem to always record the class types that existed in the module but “scriptFunctions” seem to only store functions that were in use, is that right? I see scenarios where types get ownership transfer to other modules after initial module disposal, but some of the type's functions end up cleaning up because they are not explicitly listed in that other module's "scriptFunctions”. Could only solve this by having an additional step at the end of “asCBuilder::Build()” to go through each module class type and making sure the behavior functions, factory functions and vTable functions were listed in the module's “scriptFunctions”

If you can provide sample code that reproduces the problem it would be great.

AngelCode.com - game development and more - Reference DB - game developer references
AngelScript - free scripting library - BMFont - free bitmap font generator - Tower - free puzzle game

Advertisement

@witchlord so not quite the example I mentioned before, but got a simple case for a shared class crash. This time it only happens if you're using non-bytecode module building. The shred base class function gets associated with the module only when explicitly referenced, so when a module unloads and tries to transfer the function ownership, the other module doesn't have that base class function listed in the script function vector (I think it only has the base type as a whole listed in the type vector)

#include <iostream>
#include "../../include/angelscript.h"
#include <string>
#include "assert.h"
#include <iostream>

//simple bytecode stream
class BytecodeStream : public asIBinaryStream
{
public:
	BytecodeStream() : m_buffer(nullptr), m_size(0), m_capacity(0), m_readIndex(0)
	{}

	int Write(const void* ptr, asUINT size)
	{
		size_t newSize = size + m_size;
		if (newSize > m_capacity)
		{
			do
			{
				m_capacity += 512;
			} while (newSize > m_capacity);

			char* newBuffer = new char[m_capacity];
			if (m_buffer)
			{
				memcpy(newBuffer, m_buffer, m_size);
				delete m_buffer;
			}
			m_buffer = newBuffer;
		}

		if (size == 0) return 0;

		memcpy(m_buffer + m_size, ptr, size);
		m_size += size;

		return 0;
	}
	int Read(void* ptr, asUINT size)
	{
		if (size == 0) return 0;

		memcpy(ptr, m_buffer + m_readIndex, size);
		m_readIndex += size;
		return 0;
	}

	char* m_buffer;

	size_t m_readIndex;
	size_t m_size;
	size_t m_capacity;
};


int CompileScript3(asIScriptEngine* engine, BytecodeStream& stream1, BytecodeStream& stream2);


void MessageCallback(const asSMessageInfo *msg, void *param)
{
	const char *type = "ERR ";
	if (msg->type == asMSGTYPE_WARNING)
		type = "WARN";
	else if (msg->type == asMSGTYPE_INFORMATION)
		type = "INFO";
	printf("%s (%d, %d) : %s : %s\n", msg->section, msg->row, msg->col, type, msg->message);
}


const char* file1 = "					\
	class Test1 : Test0 {						\
		int function1() { return Test0::function1(); }	\
	}									\
	shared class Test0 {				\
		int function1() { return 1; }	\
	}									\
	";

const char* file2 = "					\
	class Test2 : Test0 {				\
	}									\
	shared class Test0 {				\
		int function1() { return 1; }	\
	}									\
	";


int main(int argCount, char* argVal[])
{
	asIScriptEngine* engine = asCreateScriptEngine();
	if (engine == 0)
	{
		std::cout << "Failed to create script engine." << std::endl;
		return -1;
	}

	int r = 0;
	r = engine->SetMessageCallback(asFUNCTION(MessageCallback), 0, asCALL_CDECL);
	assert(r >= 0);

	BytecodeStream stream1;
	BytecodeStream stream2;
	CompileScript3(engine, stream1, stream2);

#if 0
	auto mod1 = engine->GetModule("test1", asGM_ALWAYS_CREATE);
	r = mod1->LoadByteCode(&stream1);
	assert(r >= 0);
	auto mod2 = engine->GetModule("test2", asGM_ALWAYS_CREATE);
	r = mod2->LoadByteCode(&stream2);
	assert(r >= 0);
#else
	auto mod1 = engine->GetModule("test1", asGM_ALWAYS_CREATE);
	mod1->AddScriptSection("test1sec", file1, strlen(file1));
	r = mod1->Build();
	assert(r >= 0);
	auto mod2 = engine->GetModule("test2", asGM_ALWAYS_CREATE);
	mod2->AddScriptSection("test2sec", file2, strlen(file2));
	r = mod2->Build();
	assert(r >= 0);
#endif

	auto testTypeInfo = mod2->GetTypeInfoByName("Test2");
	auto test1Func = testTypeInfo->GetMethodByDecl("int function1()");
	auto factoryFunc = testTypeInfo->GetFactoryByIndex(0);

	mod1->Discard();
	engine->GarbageCollect();

	auto context = engine->RequestContext();
	context->Prepare(factoryFunc);
	context->Execute();
	asIScriptObject* scriptObj = (asIScriptObject*)context->GetReturnObject();
	scriptObj->AddRef();

	context->Prepare(test1Func);
	context->SetObject(scriptObj);
	int funcCall = context->Execute();

	scriptObj->Release();
	engine->ReturnContext(context);

	mod2->Discard();
	engine->GarbageCollect();

	return 0;
}





int CompileScript3(asIScriptEngine* engine, BytecodeStream& stream1, BytecodeStream& stream2)
{
	int r;

	asIScriptModule* mod1 = engine->GetModule("test1", asGM_ALWAYS_CREATE);
	r = mod1->AddScriptSection("test1", file1, strlen(file1));
	assert(r >= 0);
	r = mod1->Build();
	assert(r >= 0);
	r = mod1->SaveByteCode(&stream1);
	assert(r >= 0);

	asIScriptModule* mod2 = engine->GetModule("test2", asGM_ALWAYS_CREATE);
	r = mod2->AddScriptSection("test2", file2, strlen(file2));
	assert(r >= 0);
	r = mod2->Build();
	assert(r >= 0);
	r = mod2->SaveByteCode(&stream2);
	assert(r >= 0);


	mod1->Discard();
	mod2->Discard();
	engine->GarbageCollect();

	return 0;
}

Thanks for providing the code to reproduce the problem.

I'll look into this as soon as I can.

AngelCode.com - game development and more - Reference DB - game developer references
AngelScript - free scripting library - BMFont - free bitmap font generator - Tower - free puzzle game

I've fixed this latest scenario in rev 2662.

Sorry for the delay.

AngelCode.com - game development and more - Reference DB - game developer references
AngelScript - free scripting library - BMFont - free bitmap font generator - Tower - free puzzle game

This topic is closed to new replies.

Advertisement