Handle API (SourceMod)

From AlliedModders Wiki
Jump to: navigation, search

The SourceMod Handle System marshals pointers into typed, unique, 32-bit integers. This makes coding in SourceMod more cross-platform compatible, type safe, and mistake safe than its predecessor, AMX Mod X. It also allows for safe object sharing and reference count based object destruction.

The fundamental aspect of Handles is that they encapsulate a single pointer. This pointer is private data, and can only be read by the Handle's creator. When the Handle is freed, the pointer is automatically freed (via a special interface), thus ensuring that memory is not leaked.

Handles also provide a simple "security" system for restricting which areas of SourceMod are allowed to directly call certain functions on Handles under a given type.


Basic Types

Handle_t

The Handle_t type is a 32bit integer. Currently, it is composed of two 16-bit integers, a serial number and a handle index. The serial number is private and used for integrity checking. The index is the internal ID of the Handle, which is tied to the encapsulated pointer and security information.

Each Handle_t is a unique Handle, however, there are cloned Handles which are copies of another Handle. They have their own unique signatures, but they refer to another Handle internally. Each Handle is also associated with a type, explained below.

HandleType_t

The HandleType_t describes a type under which Handles can be created. Types can have up to 15 sub-types; sub-types cannot have their own sub-types, called child types. When Handles are being read, they are "type checked," to make sure they are being read under a given type. When a type is destroyed, all Handles under the type are destroyed. If the type has child types, each child type is also destroyed.

Handle types also allow overloading of various Handle operations. Currently, the only overloaded action is for overloading when a Handle is destroyed. This allows the encapsulated pointer to be safely destroyed by the type interface. The interface which overloads type operations is called IHandleTypeDispatch.

Lastly, types provide a "default security" system which lets programmers restrict which functions can be accessed by other areas in SourceMod. This is explained later.

Security

Note: Type defaults can be set via IHandleSys::InitAccessDefaults().

Type Security

Often, extensions may want to share HandleType_t values with each other, so they can create Handles of external types. However, the owner of the handle type may not want other extensions to perform certain actions. Thus, types can be secured by an IdentityToken_t when created. Unless the same IdentityToken is provided, a function may fail if restricted.

Type permissions are declared in a TypeAccess struct, which has the following user-set members:

  • ident: The IdentityToken_t owner of this type.
  • access: An array where each index is a HTypeAccessRight enumeration member. If an index is set to false, functions requiring that right will fail unless the correct IdentityToken_t is passed in.

The HTypeAccessRight enumeration has the following values:

  • HTypeAccess_Create: Handle creation using this type; default is false.
  • HTypeAccess_Inherit: Inheriting this type for child types; default is false.

Below is a list of each Handle System function that requires type permissions, and which permissions may be required:

  • CreateType: HTypeAccess_Inherit if a parent is specified
  • RemoveType: (none, always secured)
  • CreateHandle: HTypeAccess_Create

Handle Security

Handle security permissions inherit from their parent type unless given an alternate set of permissions. Handles are "secured" by two properties, unlike handle types, which just have one. When verifying your identity with a secured Handle function, you must use the HandleSecurity struct, which has two properties:

  • pOwner: The owner of the Handle, which is usually a plugin IdentityToken_t.
  • pIdentity: The owner of the Handle's type, or the IdentityToken_t securing its type.

Handle permissions are specified via the HandleAccess struct. It is passed either through CreateType for default settings in a given type, or through CreateHandleEx for explicit per-Handle permissions. This struct has the following members:

  • access: An array where each index is a HandleAccessRight enumeration member. If set to 0, an access right is allowed without a HandleSecurity instance. Otherwise, the following bitwise flags are checked:
    • HANDLE_RESTRICT_IDENTITY: If flagged, this access right is only granted if the pIdentity member of the HandleSecurity struct matches the Handle's type's owner.
    • HANDLE_RESTRICT_OWNER: If flagged, this access right is only granted if the pOwner member of the Handlesecurity struct matches the Handle's owner.

The following access rights exist for Handles:

  • HandleAccess_Read: This Handle's encapsulated pointer can be retrieved. Defaults to HANDLE_RESTRICT_IDENTITY.
  • HandleAccess_Delete: This Handle can be removed or freed. Defaults to HANDLE_RESTRICT_OWNER.
  • HandleAccess_Clone: This Handle can be cloned. Defaults to 0.

Below is a list of each Handle system function that requires permissions, and which permissions may be required:

  • FreeHandle: HandleAccess_Delete
  • CloneHandle/CloneHandleEx: HandleAccess_Clone
  • ReadHandle: HandleAccess_Read

SourceMod's generic native wrappers assume follow these rules, however, they assume certain access rights, and will return INVALID_HANDLE or 0 if these rights are denied. Thus, you should make sure your permissions are compatible, unless you explicitly want to deny usage of these functions.

  • CloneHandle(): Passes NULL for HandleSecurity, meaning HandleAccess_Clone must be 0.
  • CloseHandle(): Passes NULL for HandleSecurity::pIdentity, and the plugin's IdentityToken_t for HandleSecurity::pOwner. This means that at most, the only restriction can be HANDLE_RESTRICT_OWNER for HandleAccess_Delete.

Usage Examples

Here are some examples of Handle usages.

Basic Handles

The most basic use of Handles is to encapsulate a data structure and allow CloseHandle to universally destroy it. Let's say we want to implement two natives - CreateFile for creating an empty file, and WriteLine for writing a line to this file. How could we implement this using the Handle system?

After reading this, it is tempting to think, "Why can't we just return the FILE pointer as a cell_t?" The reason is that a cell_t is guaranteed to be a 32bit integer. There is no guarantee that a pointer will always be this size, however, and thus casting on future platforms could break. This problem happened in AMX Mod X and greatly restricted flexibility.

Creating the type

First, we have to create the type and its Dispatch interface.

HandleType_t g_FileType = 0;	/* Holds the HandleType ID */
 
class FileTypeHandler : public IHandleTypeDispatch
{
public:
	void OnHandleDestroy(HandleType_t type, void *object)
	{
		/* :TODO: implement this */
	}
};
 
/* Create an instance of the handler */
FileTypeHandler g_FileTypeHandler;
 
Initialize()
{
	/* Register the type with default security permissions */
	g_FileType = g_pHandleSys->CreateType("File", 
		&g_FileTypeHandler, 
		0, 
		NULL, 
		NULL, 
		myself->GetIdentity(), 
		NULL);
}
 
Shutdown()
{
	/* Remove the type on shutdown */
	g_pHandleSys->RemoveType(g_FileType, myself->GetIdentity());
}

Creating Handles

Creating the Handles is fairly easy once we have a valid pointer to stuff in it.

/* native CreateFile(const String:file[]) */
static cell_t sm_CreateFile(IPluginContext *pContext, const cell_t *params)
{
	char path[PLATFORM_MAX_PATH];
	char *filename;
 
	/* Get the filename */
	pContext->LocalToString(params[1], &filename);
	/* Build a full path */
	g_pSM->BuildPath(Path_SM, path, sizeof(path), "%s", filename);
 
	/* Open for writing */
	FILE *fp = fopen(path, "wt");
	if (!fp)
	{
		return BAD_HANDLE;
	}
 
	/* Create the Handle with our type, the FILE pointer, the plugin's identity, and our identity */
	return g_pHandleSys->CreateHandle(g_FileType, 
		fp, 
		pContext->GetIdentity(), 
		myself->GetIdentity(), 
		NULL);
}

Reading/Checking Handles

Handles aren't very useful unless you have natives to use them. Let's say we want to write lines of text to our File type.

/* native bool WriteLine(Handle hndl, const char[] line); */
static cell_t sm_WriteLine(IPluginContext *pContext, const cell_t *params)
{
	Handle_t hndl = static_cast<Handle_t>(params[1]);
	HandleError err;
	HandleSecurity sec;
 
	/* Build our security descriptor */
	sec.pOwner = nullptr;	/* Not needed, owner access is not checked */
	sec.pIdentity = myself->GetIdentity();	/* But only this extension can read */
 
	/* Attempt to read the given handle as our type, using our security info.
	 * Note that we read the pointer directly in with a little cast.
	 * This type of cast is safe since sizeof(void **) == sizeof(void *) == sizeof(T *) in almost all cases.
	 */
	FILE *fp = nullptr;
	if ((err = g_pHandleSys->ReadHandle(hndl, g_FileType, &sec, (void **)&fp))
	     != HandleError_None)
	{
		return pContext->ThrowNativeError("Invalid file handle %x (error %d)", hndl, err);
	}
 
	/* Get the text */
	char *text = nullptr;
	pContext->LocalToString(params[2], &text);
 
	/* Write it */
	fputs(text, fp);
 
	return 1;
}

CloseHandle Support

Lastly, we need to make it so CloseHandle() or FreeHandle() will close our file pointer when removing the Handle, which we skipped before.

class FileTypeHandler : public IHandleTypeDispatch
{
public:
	void OnHandleDestroy(HandleType_t type, void *object)
	{
		fclose( (FILE *)object );
	}
};

Restricting CloseHandle

Let's say that, for various reasons, you don't want users to be able to call CloseHandle(). An example of this might be a data type that is non-temporary, or is being called from a forward, and thus should not be touched. Or, you might have a special deconstructor that takes extra parameters, and thus you need a separate destroy function.

For example, let's create a separate function called CloseFile. It doesn't do anything particularly special, but we are going to force its usage over CloseHandle().

New Security Rules

Initialize()
{
	/* Create default access rights */
	HandleAccess rules;
	g_pHandleSys->InitAccessDefaults(NULL, &rules);
 
	/* Restrict delete to only our identity */
	rules.access[HandleAccess_Delete] = HANDLE_RESTRICT_IDENTITY;
 
	/* Register the type with our security permissions */
	g_FileType = g_pHandleSys->CreateType("File", 
			&g_FileTypeHandler, 
			0, 
			NULL, 
			&rules, 
			myself->GetIdentity(), 
			NULL);
}

Implementing the Native

/* native Closefile(Handle:hndl); */
static cell_t sm_CloseFile(IPluginContext *pContext, const cell_t *params)
{
	Handle_t hndl = static_cast<Handle_t>(params[1]);
	HandleError err;
	HandleSecurity sec;
 
	/* Build our security descriptor */
	sec.pOwner = pContext->GetIdentity();	/* Needed, HANDLE_RESTRICT_OWNER is default */
	sec.pIdentity = myself->GetIdentity();	/* But only this extension can read */
 
	/* Attempt to read the given handle as our type, using our security info.
	 * Note that we read the pointer directly in with a little cast.
	 * This type of cast is safe since sizeof(void **) == sizeof(void *) == sizeof(T *) in almost all cases.
	 */
	FILE *fp;
	if ((err = g_pHandleSys->FreeHandle(hndl, &sec))
	     != HandleError_None)
	{
		return pContext->ThrowNativeError("Invalid file handle %x (error %d)", hndl, err);
	}
 
	return 1;
}

Restricting CloseHandle Per-Handle

Taking our above example, it is possible that we might want some Handles to be closed via CloseHandle(), but others not. Again, this is useful if you wish to pass a Handle as read-only to a plugin, commonly done when using non-temporary structures or Handles used in Forwards.

In this example, we'll create a global instance of our file type, and this instance will be denied access to CloseHandle(), whereas files returned by OpenFile() will not.

Implementation

Handle_t g_GlobalFile;
 
Initialize()
{
	/* Register the type with default security permissions */
	g_FileType = g_pHandleSys->CreateType("File", 
			&g_FileTypeHandler, 
			0, 
			NULL, 
			NULL, 
			myself->GetIdentity(), 
			NULL);
 
	/* Create our 'global' file */
	FILE *fp = tmpfile();
 
	/* Set up the security descriptor */
	HandleSecurity sec;
	sec.pOwner = NULL;		/* No owner for this Handle */
	sec.pIdentity = myself->GetIdentity();	/* Only this identity will be allowed */
 
	/* Set up the access permissions */
	HandleAccess rules;
	g_pHandleSys->InitAccessDefaults(NULL, &rules);
	rules.access[HandleAccess_Delete] |= HANDLE_RESTRICT_IDENTITY;
 
	/* Finally, create the Handle with our rules */
	g_GlobalFile = g_pHandleSys->CreateHandleEx(g_FileType, fp, &sec, &rules, NULL);
}
 
Shutdown()
{
	/* Remove our global Handle (not needed, but we do it for clarity */
	HandleSecurity sec;
	sec.pOwner = NULL;
	sec.pIdentity = myself->GetIdentity();
 
	g_pHandleSys->FreeHandle(g_GlobalFile, &sec);
 
	/* Remove the type on shutdown */
	g_pHandleSys->RemoveType(g_FileType, myself->GetIdentity());
}