PHP Development Proposal

Proposal Publish Date: 2019-11-14

PHP Version: 7.3.6

Module: Shared Memory Extension

Folder: /ext/shmop

Problem Description

Shared Memory PHP Extension offers shmop_read function to read a STRING from a shared memory.

/* {{{ proto string shmop_read(resource shmid, int start, int count)
   reads from a shm segment */
PHP_FUNCTION(shmop_read)
{
	zval *shmid;
	zend_long start, count;
	struct php_shmop *shmop;
	char *startaddr;
	int bytes;
	zend_string *return_string;

	if (zend_parse_parameters(ZEND_NUM_ARGS(), "rll", &shmid, &start, &count) == FAILURE) {
		return;
	}

	if ((shmop = (struct php_shmop *)zend_fetch_resource(Z_RES_P(shmid), "shmop", shm_type)) == NULL) {
		RETURN_FALSE;
	}

	if (start < 0 || start > shmop->size) {
		php_error_docref(NULL, E_WARNING, "start is out of range");
		RETURN_FALSE;
	}

	if (count < 0 || start > (INT_MAX - count) || start + count > shmop->size) {
		php_error_docref(NULL, E_WARNING, "count is out of range");
		RETURN_FALSE;
	}

	startaddr = shmop->addr + start;
	bytes = count ? count : shmop->size - start;

	return_string = zend_string_init(startaddr, bytes, 0);

	RETURN_NEW_STR(return_string);
}
/* }}} */			
			

The problem, which I have experienced is around zend_string_init function located in /Zend/zend_string.h

			
static zend_always_inline zend_string *zend_string_init(const char *str, size_t len, int persistent)
{
	zend_string *ret = zend_string_alloc(len, persistent);

	memcpy(ZSTR_VAL(ret), str, len);
	ZSTR_VAL(ret)[len] = '\0';
	return ret;
}			
			

The problem affects PHP performance as shmop_read requires a parameter (int count) to be passed in order to indicate the amount of bytes, which need to be read from a shared memory. Let's assume we call the function with the following parameters:

			
$str = shmop_read($shared_memory_id, 0, 0);
			
where shared memory size is 1Mb (1024 * 1000 bytes) and contains the following data:
						
shared_memory_start:
0x30 0x31 0x32 0x33 0x34 0x35 0x36 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 ...
                               ... 0x00
shared_memory_end			
			
1Mb of shared memory contains "0123456" string NULL terminated. The remaining bytes are NULL. zend_string_init function will attempt to copy 1Mb of shared memory contents to ZVAL_STR. This can lead to performance issues if the length of a string in shared memory is not known before calling shmop_read.

Proposed Solution

Read bytes from shared memory until NULL is found and return a STRING variable. Proposed name of a new function is:

string shmop_read_string(resource shmid, int start)

/* {{{ proto string shmop_read_string(resource shmid, int start)
   reads from a shm segment until NULL (0x00) is found */
PHP_FUNCTION(shmop_read_string)
{
	zval *shmid;
	zend_long start;
	struct php_shmop *shmop;
	char *startaddr;
	int bytes;
	zend_string *return_string;

	if (zend_parse_parameters(ZEND_NUM_ARGS(), "rl", &shmid, &start) == FAILURE) {
		return;
	}

	if ((shmop = (struct php_shmop *)zend_fetch_resource(Z_RES_P(shmid), "shmop", shm_type)) == NULL) {
		RETURN_FALSE;
	}

	if (start < 0 || start > shmop->size) {
		php_error_docref(NULL, E_WARNING, "start is out of range");
		RETURN_FALSE;
	}

	if (start > (INT_MAX) || start >= shmop->size) {
		php_error_docref(NULL, E_WARNING, "start is out of range");
		RETURN_FALSE;
	}

	startaddr = shmop->addr + start;

	for(bytes = 0; bytes < (shmop->size - start); bytes++){
		if (startaddr[bytes] == 0x00)
			break;			
	}

	return_string = zend_string_init(startaddr, bytes, 0);

	RETURN_NEW_STR(return_string);
}
/* }}} */			
			
It is also possible to add an optional "count" variable to limit the number of iterations while searching for NULL byte (instead of scanning the entire shared memory region).