Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to locally enforce payload size limit #515

Merged
merged 3 commits into from
Apr 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions memcached.ini
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@
; the default is 0
;memcached.store_retry_count = 0

; The maximum payload size in bytes that can be written.
; Writing a payload larger than the limit will result in RES_E2BIG error.
; Specifying 0 means no limit is enforced, though the server may still reject with RES_E2BIG.
; Default is 0.
;memcached.item_size_limit = 1000000

; Sets the default for consistent hashing for new connections.
; (To configure consistent hashing for session connections,
; use memcached.sess_consistent_hash instead)
Expand Down
45 changes: 45 additions & 0 deletions php_memcached.c
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ static int php_memc_list_entry(void) {
#define MEMC_OPT_STORE_RETRY_COUNT -1005
#define MEMC_OPT_USER_FLAGS -1006
#define MEMC_OPT_COMPRESSION_LEVEL -1007
#define MEMC_OPT_ITEM_SIZE_LIMIT -1008

/****************************************
Custom result codes
Expand Down Expand Up @@ -162,6 +163,7 @@ typedef struct {

zend_long store_retry_count;
zend_long set_udf_flags;
zend_long item_size_limit;

#ifdef HAVE_MEMCACHED_SASL
zend_bool has_sasl_data;
Expand Down Expand Up @@ -423,6 +425,7 @@ PHP_INI_BEGIN()
MEMC_INI_ENTRY("compression_threshold", "2000", OnUpdateLong, compression_threshold)
MEMC_INI_ENTRY("serializer", SERIALIZER_DEFAULT_NAME, OnUpdateSerializer, serializer_name)
MEMC_INI_ENTRY("store_retry_count", "0", OnUpdateLong, store_retry_count)
MEMC_INI_ENTRY("item_size_limit", "0", OnUpdateLongGEZero, item_size_limit)

MEMC_INI_BOOL ("default_consistent_hash", "0", OnUpdateBool, default_behavior.consistent_hash_enabled)
MEMC_INI_BOOL ("default_binary_protocol", "0", OnUpdateBool, default_behavior.binary_protocol_enabled)
Expand Down Expand Up @@ -1127,6 +1130,21 @@ zend_string *s_zval_to_payload(php_memc_object_t *intern, zval *value, uint32_t
return payload;
}

static
zend_bool s_is_payload_too_big(php_memc_object_t *intern, zend_string *payload)
{
php_memc_user_data_t *memc_user_data = memcached_get_user_data(intern->memc);

/* An item size limit of 0 implies no limit enforced */
if (memc_user_data->item_size_limit == 0) {
return 0;
}
if (ZSTR_LEN(payload) > memc_user_data->item_size_limit) {
return 1;
}
return 0;
}

static
zend_bool s_should_retry_write (php_memc_object_t *intern, memcached_return status)
{
Expand All @@ -1153,6 +1171,12 @@ zend_bool s_memc_write_zval (php_memc_object_t *intern, php_memc_write_op op, ze
s_memc_set_status(intern, MEMC_RES_PAYLOAD_FAILURE, 0);
return 0;
}

if (s_is_payload_too_big(intern, payload)) {
s_memc_set_status(intern, MEMCACHED_E2BIG, 0);
zend_string_release(payload);
return 0;
}
}

#define memc_write_using_fn(fn_name) payload ? fn_name(intern->memc, ZSTR_VAL(key), ZSTR_LEN(key), ZSTR_VAL(payload), ZSTR_LEN(payload), expiration, flags) : MEMC_RES_PAYLOAD_FAILURE;
Expand Down Expand Up @@ -1305,6 +1329,7 @@ static PHP_METHOD(Memcached, __construct)
memc_user_data->encoding_enabled = 0;
memc_user_data->store_retry_count = MEMC_G(store_retry_count);
memc_user_data->set_udf_flags = -1;
memc_user_data->item_size_limit = MEMC_G(item_size_limit);
memc_user_data->is_persistent = is_persistent;

memcached_set_user_data(intern->memc, memc_user_data);
Expand Down Expand Up @@ -2145,6 +2170,12 @@ static void php_memc_cas_impl(INTERNAL_FUNCTION_PARAMETERS, zend_bool by_key)
RETURN_FALSE;
}

if (s_is_payload_too_big(intern, payload)) {
intern->rescode = MEMCACHED_E2BIG;
zend_string_release(payload);
RETURN_FALSE;
}

if (by_key) {
status = memcached_cas_by_key(intern->memc, ZSTR_VAL(server_key), ZSTR_LEN(server_key), ZSTR_VAL(key), ZSTR_LEN(key), ZSTR_VAL(payload), ZSTR_LEN(payload), expiration, flags, cas);
} else {
Expand Down Expand Up @@ -2970,6 +3001,9 @@ static PHP_METHOD(Memcached, getOption)
case MEMC_OPT_COMPRESSION:
RETURN_BOOL(memc_user_data->compression_enabled);

case MEMC_OPT_ITEM_SIZE_LIMIT:
RETURN_LONG(memc_user_data->item_size_limit);

case MEMC_OPT_PREFIX_KEY:
{
memcached_return retval;
Expand Down Expand Up @@ -3041,6 +3075,15 @@ int php_memc_set_option(php_memc_object_t *intern, long option, zval *value)
}
break;

case MEMC_OPT_ITEM_SIZE_LIMIT:
lval = zval_get_long(value);
if (lval < 0) {
php_error_docref(NULL, E_WARNING, "ITEM_SIZE_LIMIT must be >= 0");
return 0;
}
memc_user_data->item_size_limit = lval;
break;

case MEMC_OPT_PREFIX_KEY:
{
zend_string *str;
Expand Down Expand Up @@ -4013,6 +4056,7 @@ PHP_GINIT_FUNCTION(php_memcached)
php_memcached_globals->memc.compression_factor = 1.30;
php_memcached_globals->memc.compression_level = 3;
php_memcached_globals->memc.store_retry_count = 2;
php_memcached_globals->memc.item_size_limit = 0;

php_memcached_globals->memc.sasl_initialised = 0;
php_memcached_globals->no_effect = 0;
Expand Down Expand Up @@ -4063,6 +4107,7 @@ static void php_memc_register_constants(INIT_FUNC_ARGS)

REGISTER_MEMC_CLASS_CONST_LONG(OPT_USER_FLAGS, MEMC_OPT_USER_FLAGS);
REGISTER_MEMC_CLASS_CONST_LONG(OPT_STORE_RETRY_COUNT, MEMC_OPT_STORE_RETRY_COUNT);
REGISTER_MEMC_CLASS_CONST_LONG(OPT_ITEM_SIZE_LIMIT, MEMC_OPT_ITEM_SIZE_LIMIT);

/*
* Indicate whether igbinary serializer is available
Expand Down
5 changes: 3 additions & 2 deletions php_memcached_private.h
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,9 @@ ZEND_BEGIN_MODULE_GLOBALS(php_memcached)
char *compression_name;
zend_long compression_threshold;
double compression_factor;
zend_long store_retry_count;
zend_long compression_level;
zend_long store_retry_count;
zend_long compression_level;
zend_long item_size_limit;

/* Converted values*/
php_memc_serializer_type serializer_type;
Expand Down
32 changes: 32 additions & 0 deletions tests/cas_e2big.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
--TEST--
set data exceeding size limit
--SKIPIF--
<?php include "skipif.inc";?>
--FILE--
<?php
include dirname (__FILE__) . '/config.inc';
$m = memc_get_instance (array (
Memcached::OPT_ITEM_SIZE_LIMIT => 100,
));

$m->delete('cas_e2big_test');

$m->set('cas_e2big_test', 'hello');
$result = $m->get('cas_e2big_test', null, Memcached::GET_EXTENDED);
var_dump(is_array($result) && isset($result['cas']) && isset($result['value']) && $result['value'] == 'hello');

$value = str_repeat('a large payload', 1024 * 1024);

var_dump($m->cas($result['cas'], 'cas_e2big_test', $value, 360));
var_dump($m->getResultCode() == Memcached::RES_E2BIG);
var_dump($m->getResultMessage() == 'ITEM TOO BIG');
var_dump($m->get('cas_e2big_test') == 'hello');
var_dump($m->getResultCode() == Memcached::RES_SUCCESS);
?>
--EXPECT--
bool(true)
bool(false)
bool(true)
bool(true)
bool(true)
bool(true)
32 changes: 32 additions & 0 deletions tests/options.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,26 @@ var_dump($m->getOption(Memcached::OPT_COMPRESSION_TYPE) == Memcached::COMPRESSIO

var_dump($m->setOption(Memcached::OPT_COMPRESSION_TYPE, 0));
var_dump($m->getOption(Memcached::OPT_COMPRESSION_TYPE) == Memcached::COMPRESSION_FASTLZ);

echo "item_size_limit setOption\n";
var_dump($m->setOption(Memcached::OPT_ITEM_SIZE_LIMIT, 0));
var_dump($m->getOption(Memcached::OPT_ITEM_SIZE_LIMIT) === 0);
var_dump($m->setOption(Memcached::OPT_ITEM_SIZE_LIMIT, -1));
var_dump($m->setOption(Memcached::OPT_ITEM_SIZE_LIMIT, 1000000));
var_dump($m->getOption(Memcached::OPT_ITEM_SIZE_LIMIT) == 1000000);

echo "item_size_limit ini\n";
ini_set('memcached.item_size_limit', '0');
$m = new Memcached();
var_dump($m->getOption(Memcached::OPT_ITEM_SIZE_LIMIT) === 0);

ini_set('memcached.item_size_limit', '1000000');
$m = new Memcached();
var_dump($m->getOption(Memcached::OPT_ITEM_SIZE_LIMIT) == 1000000);

ini_set('memcached.item_size_limit', null);
$m = new Memcached();
var_dump($m->getOption(Memcached::OPT_ITEM_SIZE_LIMIT) === 0);
?>
--EXPECTF--
bool(true)
Expand All @@ -41,3 +61,15 @@ bool(true)
bool(true)
bool(false)
bool(true)
item_size_limit setOption
bool(true)
bool(true)

Warning: Memcached::setOption(): ITEM_SIZE_LIMIT must be >= 0 in %s on line %d
bool(false)
bool(true)
bool(true)
item_size_limit ini
bool(true)
bool(true)
bool(true)
4 changes: 3 additions & 1 deletion tests/set_large.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ set large data
--FILE--
<?php
include dirname (__FILE__) . '/config.inc';
$m = memc_get_instance ();
$m = memc_get_instance (array (
Memcached::OPT_ITEM_SIZE_LIMIT => 0,
));

$key = 'foobarbazDEADC0DE';
$value = str_repeat("foo bar", 1024 * 1024);
Expand Down
27 changes: 27 additions & 0 deletions tests/set_large_e2big.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
--TEST--
set data exceeding size limit
--SKIPIF--
<?php include "skipif.inc";?>
--FILE--
<?php
include dirname (__FILE__) . '/config.inc';
$m = memc_get_instance (array (
Memcached::OPT_ITEM_SIZE_LIMIT => 100,
));

$m->delete('set_large_e2big_test');

$value = str_repeat('a large payload', 1024 * 1024);

var_dump($m->set('set_large_e2big_test', $value));
var_dump($m->getResultCode() == Memcached::RES_E2BIG);
var_dump($m->getResultMessage() == 'ITEM TOO BIG');
var_dump($m->get('set_large_e2big_test') === false);
var_dump($m->getResultCode() == Memcached::RES_NOTFOUND);
?>
--EXPECT--
bool(false)
bool(true)
bool(true)
bool(true)
bool(true)
3 changes: 3 additions & 0 deletions tests/setoptions.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ var_dump($m->setOptions(array(
Memcached::OPT_COMPRESSION => 0,
Memcached::OPT_LIBKETAMA_COMPATIBLE => 1,
Memcached::OPT_CONNECT_TIMEOUT => 5000,
Memcached::OPT_ITEM_SIZE_LIMIT => 1000000,
)));

var_dump($m->getOption(Memcached::OPT_PREFIX_KEY) == 'a_prefix');
var_dump($m->getOption(Memcached::OPT_SERIALIZER) == Memcached::SERIALIZER_PHP);
var_dump($m->getOption(Memcached::OPT_COMPRESSION) == 0);
var_dump($m->getOption(Memcached::OPT_LIBKETAMA_COMPATIBLE) == 1);
var_dump($m->getOption(Memcached::OPT_ITEM_SIZE_LIMIT) == 1000000);

echo "test invalid options\n";

Expand All @@ -36,6 +38,7 @@ bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
test invalid options

Warning: Memcached::setOptions(): invalid configuration option in %s on line %d
Expand Down