├── .gitignore ├── LICENSE ├── Makefile.frag ├── README.md ├── composer.json ├── config.m4 ├── php_ilimit.c ├── php_ilimit.h ├── src ├── ilimit.c └── ilimit.h ├── stubs.php └── tests ├── 001.phpt ├── 002.phpt ├── 003.phpt ├── 004.phpt ├── 005.phpt ├── 006.phpt ├── 007.phpt ├── 008.phpt ├── 009.phpt ├── 010.phpt ├── 011.phpt ├── 012.phpt ├── 013.phpt ├── 014.phpt ├── 015.phpt ├── 016.phpt ├── 017.phpt └── 018.phpt /.gitignore: -------------------------------------------------------------------------------- 1 | # Object files 2 | *.o 3 | *.lo 4 | 5 | # Libraries 6 | *.lib 7 | *.a 8 | *.la 9 | 10 | # Shared objects (inc. Windows DLLs) 11 | *.dll 12 | *.so 13 | *.so.* 14 | *.dylib 15 | 16 | # archives 17 | ilimit-*.tgz 18 | 19 | # autotools 20 | .deps 21 | .libs 22 | config.cache 23 | config.guess 24 | config.h 25 | config.h.in 26 | config.h.in~ 27 | config.log 28 | config.nice 29 | config.status 30 | config.sub 31 | configure 32 | configure.ac 33 | configure.in 34 | conftest 35 | conftest.c 36 | Makefile 37 | Makefile.fragments 38 | Makefile.global 39 | Makefile.objects 40 | acinclude.m4 41 | aclocal.m4 42 | autom4te.cache 43 | build 44 | install-sh 45 | libtool 46 | ltmain.sh 47 | ltmain.sh.backup 48 | missing 49 | mkinstalldirs 50 | modules 51 | run-tests.php 52 | run-tests.log 53 | tmp-php.ini 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------- 2 | The PHP License, version 3.01 3 | Copyright (c) 1999 - 2018 The PHP Group. All rights reserved. 4 | -------------------------------------------------------------------- 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, is permitted provided that the following conditions 8 | are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 13 | 2. Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in 15 | the documentation and/or other materials provided with the 16 | distribution. 17 | 18 | 3. The name "PHP" must not be used to endorse or promote products 19 | derived from this software without prior written permission. For 20 | written permission, please contact group@php.net. 21 | 22 | 4. Products derived from this software may not be called "PHP", nor 23 | may "PHP" appear in their name, without prior written permission 24 | from group@php.net. You may indicate that your software works in 25 | conjunction with PHP by saying "Foo for PHP" instead of calling 26 | it "PHP Foo" or "phpfoo" 27 | 28 | 5. The PHP Group may publish revised and/or new versions of the 29 | license from time to time. Each version will be given a 30 | distinguishing version number. 31 | Once covered code has been published under a particular version 32 | of the license, you may always continue to use it under the terms 33 | of that version. You may also choose to use such covered code 34 | under the terms of any subsequent version of the license 35 | published by the PHP Group. No one other than the PHP Group has 36 | the right to modify the terms applicable to covered code created 37 | under this License. 38 | 39 | 6. Redistributions of any form whatsoever must retain the following 40 | acknowledgment: 41 | "This product includes PHP software, freely available from 42 | ". 43 | 44 | THIS SOFTWARE IS PROVIDED BY THE PHP DEVELOPMENT TEAM ``AS IS'' AND 45 | ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 46 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 47 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE PHP 48 | DEVELOPMENT TEAM OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 49 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 50 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 51 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 52 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 53 | STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 54 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 55 | OF THE POSSIBILITY OF SUCH DAMAGE. 56 | 57 | -------------------------------------------------------------------- 58 | 59 | This software consists of voluntary contributions made by many 60 | individuals on behalf of the PHP Group. 61 | 62 | The PHP Group can be contacted via Email at group@php.net. 63 | 64 | For more information on the PHP Group and the PHP project, 65 | please see . 66 | 67 | PHP includes the Zend Engine, freely available at 68 | . 69 | -------------------------------------------------------------------------------- /Makefile.frag: -------------------------------------------------------------------------------- 1 | ilimit-test-coverage: 2 | CCACHE_DISABLE=1 EXTRA_CFLAGS="-fprofile-arcs -ftest-coverage" TEST_PHP_ARGS="-q" $(MAKE) clean test 3 | 4 | ilimit-test-coverage-lcov: ilimit-test-coverage 5 | lcov -c --directory $(top_srcdir)/src/.libs --output-file $(top_srcdir)/coverage.info 6 | 7 | ilimit-test-coverage-html: ilimit-test-coverage-lcov 8 | genhtml $(top_srcdir)/coverage.info --output-directory=$(top_srcdir)/html 9 | 10 | ilimit-test-coverage-travis: 11 | CCACHE_DISABLE=1 EXTRA_CFLAGS="-fprofile-arcs -ftest-coverage" $(MAKE) 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ilimit 2 | ====== 3 | 4 | `ilimit` provides a method to execute a call while imposing limits on the time and memory that the call may consume. 5 | 6 | Requirements 7 | ============ 8 | 9 | * PHP 7.1+ 10 | * NTS 11 | * pthread.h 12 | 13 | Stubs 14 | ===== 15 | 16 | This repository includes PHP files with method headers for IDE integration and 17 | static analysis support. 18 | 19 | To install, run the following command: 20 | ``` 21 | composer require krakjoe/ilimit 22 | ``` 23 | 24 | API 25 | === 26 | 27 | ```php 28 | namespace ilimit { 29 | /** 30 | * Call a callback while imposing limits on the time and memory that 31 | * the call may consume. 32 | * 33 | * @param callable $callable The invocation to make. 34 | * @param array $arguments The list of arguments. 35 | * @param int $timeout The maximum execution time, in microseconds. 36 | * @param int $maxMemory The maximum amount of memory, in bytes. 37 | * If set to zero, no limit is imposed. 38 | * @param int $checkInterval The interval between memory checks, 39 | * in microseconds. If set to zero or less, 40 | * a default interval of 100 microseconds is used. 41 | * 42 | * @return mixed Returns the return value of the callback. 43 | * 44 | * @throws Error\Runtime If timeout is not positive. 45 | * @throws Error\Runtime If maxMemory is negative. 46 | * @throws Error\System If the system lacks necessary resources to make the call. 47 | * @throws Error\Timeout If the invocation exceeds the allowed time. 48 | * @throws Error\Memory If the invocation exceeds the allowed memory. 49 | */ 50 | function call(callable $callable, array $arguments, int $timeout, int $maxMemory = 0, int $checkInterval = 0); 51 | } 52 | ``` 53 | 54 | 55 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "krakjoe/ilimit", 3 | "description": "IDE and static analysis helper for the krakjoe/ilimit extension", 4 | "keywords": ["ilimit", "timeout", "call", "limit"], 5 | "type": "library", 6 | "license": "PHP-3.01", 7 | "authors": [ 8 | { 9 | "name": "Joe Watkins", 10 | "email": "krakjoe@php.net" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=7.1.0", 15 | "ext-ilimit": "*" 16 | }, 17 | "autoload-dev": { 18 | "files": ["stubs.php"] 19 | }, 20 | "archive": { 21 | "exclude": ["/*", "!/stubs.php"] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /config.m4: -------------------------------------------------------------------------------- 1 | dnl config.m4 for extension ilimit 2 | 3 | PHP_ARG_ENABLE([ilimit], 4 | [whether to enable ilimit support], 5 | [AS_HELP_STRING([--enable-ilimit], 6 | [Enable ilimit support])], 7 | [no]) 8 | 9 | PHP_ARG_ENABLE([ilimit-coverage], 10 | [whether to enable ilimit coverage support], 11 | [AS_HELP_STRING([--enable-ilimit-coverage], 12 | [Enable ilimit coverage support])], 13 | [no]) 14 | 15 | if test "$PHP_ILIMIT" != "no"; then 16 | AC_MSG_CHECKING([for NTS]) 17 | if test "$PHP_THREAD_SAFETY" != "no"; then 18 | AC_MSG_ERROR([ilimit requires NTS, please use PHP with ZTS disabled]) 19 | else 20 | AC_MSG_RESULT([ok]) 21 | fi 22 | 23 | PHP_ADD_LIBRARY(pthread,, ILIMIT_SHARED_LIBADD) 24 | 25 | PHP_NEW_EXTENSION(ilimit, php_ilimit.c src/ilimit.c, $ext_shared) 26 | 27 | PHP_ADD_BUILD_DIR($ext_builddir/src, 1) 28 | PHP_ADD_INCLUDE($ext_srcdir) 29 | 30 | AC_MSG_CHECKING([ilimit coverage]) 31 | if test "$PHP_ILIMIT_COVERAGE" != "no"; then 32 | AC_MSG_RESULT([enabled]) 33 | 34 | PHP_ADD_MAKEFILE_FRAGMENT 35 | else 36 | AC_MSG_RESULT([disabled]) 37 | fi 38 | 39 | 40 | PHP_SUBST(ILIMIT_SHARED_LIBADD) 41 | fi 42 | -------------------------------------------------------------------------------- /php_ilimit.c: -------------------------------------------------------------------------------- 1 | /* 2 | +----------------------------------------------------------------------+ 3 | | ilimit | 4 | +----------------------------------------------------------------------+ 5 | | Copyright (c) Joe Watkins 2019 | 6 | +----------------------------------------------------------------------+ 7 | | This source file is subject to version 3.01 of the PHP license, | 8 | | that is bundled with this package in the file LICENSE, and is | 9 | | available through the world-wide-web at the following url: | 10 | | http://www.php.net/license/3_01.txt | 11 | | If you did not receive a copy of the PHP license and are unable to | 12 | | obtain it through the world-wide-web, please send a note to | 13 | | license@php.net so we can mail you a copy immediately. | 14 | +----------------------------------------------------------------------+ 15 | | Author: krakjoe | 16 | +----------------------------------------------------------------------+ 17 | */ 18 | 19 | #ifdef HAVE_CONFIG_H 20 | # include "config.h" 21 | #endif 22 | 23 | #include "php.h" 24 | #include "ext/standard/info.h" 25 | #include "php_ilimit.h" 26 | 27 | #include "src/ilimit.h" 28 | 29 | /* {{{ PHP_MINIT_FUNCTION 30 | */ 31 | PHP_MINIT_FUNCTION(ilimit) 32 | { 33 | php_ilimit_startup(); 34 | 35 | return SUCCESS; 36 | } /* }}} */ 37 | 38 | /* {{{ PHP_MINFO_FUNCTION 39 | */ 40 | PHP_MINFO_FUNCTION(ilimit) 41 | { 42 | php_info_print_table_start(); 43 | php_info_print_table_header(2, "ilimit support", "enabled"); 44 | php_info_print_table_end(); 45 | } 46 | /* }}} */ 47 | 48 | /* {{{ arg info */ 49 | ZEND_BEGIN_ARG_INFO_EX(zend_ilimit_arginfo, 0, 0, 1) 50 | ZEND_ARG_TYPE_INFO(0, callable, IS_CALLABLE, 0) 51 | ZEND_ARG_TYPE_INFO(0, arguments, IS_ARRAY, 0) 52 | ZEND_ARG_TYPE_INFO(0, cpu, IS_LONG, 0) 53 | ZEND_ARG_TYPE_INFO(0, memory, IS_LONG, 0) 54 | ZEND_ARG_TYPE_INFO(0, interval, IS_LONG, 0) 55 | ZEND_END_ARG_INFO() /* }}} */ 56 | 57 | /* {{{ proto mixed \ilimit\call(callable $function [, array $args, int $timeoutMS, int $memoryBytes, int $intervalMs = 100]) */ 58 | ZEND_NAMED_FUNCTION(zend_ilimit_call) 59 | { 60 | php_ilimit_call_t call; 61 | zval *args = NULL; 62 | 63 | php_ilimit_call_init(&call, execute_data); 64 | 65 | ZEND_PARSE_PARAMETERS_START(1, 5) 66 | Z_PARAM_FUNC(call.zend.info, call.zend.cache) 67 | Z_PARAM_OPTIONAL 68 | Z_PARAM_ARRAY(args) 69 | Z_PARAM_LONG(call.limits.timeout) 70 | Z_PARAM_LONG(call.limits.memory.max) 71 | Z_PARAM_LONG(call.limits.memory.interval) 72 | ZEND_PARSE_PARAMETERS_END(); 73 | 74 | call.zend.info.retval = return_value; 75 | 76 | if (args) { 77 | zend_fcall_info_args(&call.zend.info, args); 78 | } 79 | 80 | php_ilimit_call(&call); 81 | 82 | if (args) { 83 | zend_fcall_info_args_clear(&call.zend.info, 1); 84 | } 85 | } /* }}} */ 86 | 87 | /* {{{ zend_ilimit_functions[] 88 | */ 89 | static const zend_function_entry zend_ilimit_api[] = { 90 | ZEND_NS_FENTRY("ilimit", call, zend_ilimit_call, zend_ilimit_arginfo, 0) 91 | ZEND_FE_END 92 | }; 93 | /* }}} */ 94 | 95 | /* {{{ ilimit_module_entry 96 | */ 97 | zend_module_entry ilimit_module_entry = { 98 | STANDARD_MODULE_HEADER, 99 | "ilimit", 100 | zend_ilimit_api, 101 | PHP_MINIT(ilimit), 102 | NULL, 103 | NULL, 104 | NULL, 105 | PHP_MINFO(ilimit), 106 | PHP_ILIMIT_VERSION, 107 | STANDARD_MODULE_PROPERTIES 108 | }; 109 | /* }}} */ 110 | 111 | #ifdef COMPILE_DL_ILIMIT 112 | # ifdef ZTS 113 | ZEND_TSRMLS_CACHE_DEFINE() 114 | # endif 115 | ZEND_GET_MODULE(ilimit) 116 | #endif 117 | -------------------------------------------------------------------------------- /php_ilimit.h: -------------------------------------------------------------------------------- 1 | /* 2 | +----------------------------------------------------------------------+ 3 | | ilimit | 4 | +----------------------------------------------------------------------+ 5 | | Copyright (c) Joe Watkins 2019 | 6 | +----------------------------------------------------------------------+ 7 | | This source file is subject to version 3.01 of the PHP license, | 8 | | that is bundled with this package in the file LICENSE, and is | 9 | | available through the world-wide-web at the following url: | 10 | | http://www.php.net/license/3_01.txt | 11 | | If you did not receive a copy of the PHP license and are unable to | 12 | | obtain it through the world-wide-web, please send a note to | 13 | | license@php.net so we can mail you a copy immediately. | 14 | +----------------------------------------------------------------------+ 15 | | Author: krakjoe | 16 | +----------------------------------------------------------------------+ 17 | */ 18 | 19 | #ifndef PHP_ILIMIT_H 20 | # define PHP_ILIMIT_H 21 | 22 | extern zend_module_entry ilimit_module_entry; 23 | # define phpext_ilimit_ptr &ilimit_module_entry 24 | 25 | # define PHP_ILIMIT_VERSION "0.0.1" 26 | 27 | # if defined(ZTS) && defined(COMPILE_DL_ILIMIT) 28 | ZEND_TSRMLS_CACHE_EXTERN() 29 | # endif 30 | 31 | #endif /* PHP_ILIMIT_H */ 32 | -------------------------------------------------------------------------------- /src/ilimit.c: -------------------------------------------------------------------------------- 1 | /* 2 | +----------------------------------------------------------------------+ 3 | | ilimit | 4 | +----------------------------------------------------------------------+ 5 | | Copyright (c) Joe Watkins 2019 | 6 | +----------------------------------------------------------------------+ 7 | | This source file is subject to version 3.01 of the PHP license, | 8 | | that is bundled with this package in the file LICENSE, and is | 9 | | available through the world-wide-web at the following url: | 10 | | http://www.php.net/license/3_01.txt | 11 | | If you did not receive a copy of the PHP license and are unable to | 12 | | obtain it through the world-wide-web, please send a note to | 13 | | license@php.net so we can mail you a copy immediately. | 14 | +----------------------------------------------------------------------+ 15 | | Author: krakjoe | 16 | +----------------------------------------------------------------------+ 17 | */ 18 | #ifndef HAVE_PHP_ILIMIT_C 19 | #define HAVE_PHP_ILIMIT_C 20 | 21 | #include 22 | 23 | #include "ilimit.h" 24 | 25 | #define PHP_ILIMIT_RUNNING 0x00000001 26 | #define PHP_ILIMIT_FINISHED 0x00000010 27 | #define PHP_ILIMIT_TIMEOUT 0x00000100 28 | #define PHP_ILIMIT_MEMORY 0x00001000 29 | #define PHP_ILIMIT_INTERRUPT 0x00010000 30 | #define PHP_ILIMIT_INTERRUPTED 0x00100000 31 | 32 | zend_class_entry *php_ilimit_ex; 33 | zend_class_entry *php_ilimit_runtime_ex; 34 | zend_class_entry *php_ilimit_sys_ex; 35 | zend_class_entry *php_ilimit_timeout_ex; 36 | zend_class_entry *php_ilimit_memory_ex; 37 | 38 | __thread php_ilimit_call_t *__context; 39 | 40 | void (*zend_interrupt_callback)(zend_execute_data *); 41 | 42 | static void php_ilimit_interrupt(zend_execute_data *execute_data) { /* {{{ */ 43 | if (!__context) { 44 | if (zend_interrupt_callback) { 45 | zend_interrupt_callback(execute_data); 46 | } 47 | return; 48 | } 49 | 50 | pthread_mutex_lock(&__context->mutex); 51 | 52 | if (!(__context->state & PHP_ILIMIT_INTERRUPT)) { 53 | pthread_mutex_unlock(&__context->mutex); 54 | 55 | if (zend_interrupt_callback) { 56 | zend_interrupt_callback(execute_data); 57 | } 58 | return; 59 | } 60 | 61 | __context->state |= PHP_ILIMIT_INTERRUPTED; 62 | 63 | pthread_cond_broadcast(&__context->cond); 64 | pthread_mutex_unlock(&__context->mutex); 65 | 66 | pthread_exit(NULL); 67 | } /* }}} */ 68 | 69 | void php_ilimit_startup(void) { /* {{{ */ 70 | zend_class_entry ce; 71 | 72 | INIT_NS_CLASS_ENTRY(ce, "ilimit", "Error", NULL); 73 | 74 | php_ilimit_ex = 75 | zend_register_internal_class_ex(&ce, zend_ce_exception); 76 | 77 | INIT_NS_CLASS_ENTRY(ce, "ilimit", "Error\\Runtime", NULL); 78 | 79 | php_ilimit_runtime_ex = 80 | zend_register_internal_class_ex(&ce, php_ilimit_ex); 81 | php_ilimit_runtime_ex->ce_flags |= ZEND_ACC_FINAL; 82 | 83 | INIT_NS_CLASS_ENTRY(ce, "ilimit", "Error\\System", NULL); 84 | 85 | php_ilimit_sys_ex = 86 | zend_register_internal_class_ex(&ce, php_ilimit_ex); 87 | php_ilimit_sys_ex->ce_flags |= ZEND_ACC_FINAL; 88 | 89 | INIT_NS_CLASS_ENTRY(ce, "ilimit", "Error\\Timeout", NULL); 90 | 91 | php_ilimit_timeout_ex = 92 | zend_register_internal_class_ex(&ce, php_ilimit_ex); 93 | php_ilimit_timeout_ex->ce_flags |= ZEND_ACC_FINAL; 94 | 95 | INIT_NS_CLASS_ENTRY(ce, "ilimit", "Error\\Memory", NULL); 96 | 97 | php_ilimit_memory_ex = 98 | zend_register_internal_class_ex(&ce, php_ilimit_ex); 99 | php_ilimit_memory_ex->ce_flags |= ZEND_ACC_FINAL; 100 | 101 | zend_interrupt_callback = zend_interrupt_function; 102 | zend_interrupt_function = php_ilimit_interrupt; 103 | } /* }}} */ 104 | 105 | static zend_always_inline void php_ilimit_clock(struct timespec *clock, zend_long ms) { /* {{{ */ 106 | struct timeval time; 107 | 108 | gettimeofday(&time, NULL); 109 | 110 | time.tv_sec += (ms / 1000000L); 111 | time.tv_sec += (time.tv_usec + (ms % 1000000L)) / 1000000L; 112 | time.tv_usec = (time.tv_usec + (ms % 1000000L)) % 1000000L; 113 | 114 | clock->tv_sec = time.tv_sec; 115 | clock->tv_nsec = time.tv_usec * 1000; 116 | } /* }}} */ 117 | 118 | static void php_ilimit_call_cancel(php_ilimit_call_t *call) { /* {{{ */ 119 | zend_bool cancelled = 0; 120 | zend_long max = 10000, tick = 0; 121 | 122 | pthread_mutex_lock(&call->mutex); 123 | 124 | call->state |= PHP_ILIMIT_INTERRUPT; 125 | 126 | EG(vm_interrupt) = 1; 127 | 128 | while (!(call->state & PHP_ILIMIT_INTERRUPTED)) { 129 | struct timespec clock; 130 | 131 | php_ilimit_clock(&clock, 100); 132 | 133 | switch (pthread_cond_timedwait(&call->cond, &call->mutex, &clock)) { 134 | case ETIMEDOUT: 135 | if (!cancelled) { 136 | pthread_cancel( 137 | call->threads.timeout); 138 | cancelled = 1; 139 | } 140 | 141 | if (tick++ > max) { 142 | goto __php_ilimit_call_cancel_bail; 143 | } 144 | break; 145 | 146 | case SUCCESS: 147 | /* do nothing, signalled */ 148 | break; 149 | } 150 | } 151 | 152 | __php_ilimit_call_cancel_bail: 153 | pthread_mutex_unlock(&call->mutex); 154 | } /* }}} */ 155 | 156 | static void __php_ilimit_call_thread_cancel(php_ilimit_call_t *call) { /* {{{ */ 157 | pthread_mutex_lock(&call->mutex); 158 | 159 | call->state |= PHP_ILIMIT_INTERRUPTED; 160 | 161 | pthread_cond_broadcast(&call->cond); 162 | pthread_mutex_unlock(&call->mutex); 163 | } /* }}} */ 164 | 165 | static void* __php_ilimit_call_thread(void *arg) { /* {{{ */ 166 | php_ilimit_call_t *call = __context = 167 | (php_ilimit_call_t*) arg; 168 | 169 | pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL); 170 | 171 | pthread_mutex_lock(&call->mutex); 172 | call->state |= PHP_ILIMIT_RUNNING; 173 | pthread_cond_broadcast(&call->cond); 174 | pthread_mutex_unlock(&call->mutex); 175 | 176 | pthread_cleanup_push( 177 | (void (*)(void*))__php_ilimit_call_thread_cancel, call); 178 | 179 | zend_call_function( 180 | &call->zend.info, &call->zend.cache); 181 | 182 | pthread_cleanup_pop(0); 183 | 184 | pthread_mutex_lock(&call->mutex); 185 | call->state &= ~PHP_ILIMIT_RUNNING; 186 | call->state |= PHP_ILIMIT_FINISHED; 187 | pthread_cond_broadcast(&call->cond); 188 | pthread_mutex_unlock(&call->mutex); 189 | 190 | pthread_exit(NULL); 191 | } /* }}} */ 192 | 193 | static void* __php_ilimit_memory_thread(void *arg) { /* {{{ */ 194 | php_ilimit_call_t *call = 195 | (php_ilimit_call_t*) arg; 196 | struct timespec clock; 197 | 198 | pthread_mutex_lock(&call->mutex); 199 | 200 | while (!(call->state & 201 | (PHP_ILIMIT_RUNNING|PHP_ILIMIT_FINISHED|PHP_ILIMIT_TIMEOUT))) { 202 | pthread_cond_wait(&call->cond, &call->mutex); 203 | } 204 | 205 | pthread_mutex_unlock(&call->mutex); 206 | 207 | /* the call is running ... start checking memory */ 208 | 209 | pthread_mutex_lock(&call->mutex); 210 | 211 | while (!(call->state & (PHP_ILIMIT_FINISHED|PHP_ILIMIT_TIMEOUT))) { 212 | php_ilimit_clock(&clock, call->limits.memory.interval); 213 | 214 | switch (pthread_cond_timedwait(&call->cond, &call->mutex, &clock)) { 215 | case SUCCESS: 216 | /* do nothing, signalled */ 217 | break; 218 | 219 | case ETIMEDOUT: 220 | if (zend_memory_usage(0) > call->limits.memory.max) { 221 | call->zend.frame = EG(current_execute_data); 222 | 223 | call->state |= 224 | PHP_ILIMIT_MEMORY|PHP_ILIMIT_FINISHED; 225 | 226 | pthread_cond_broadcast(&call->cond); 227 | pthread_mutex_unlock(&call->mutex); 228 | 229 | php_ilimit_call_cancel(call); 230 | 231 | goto __php_ilimit_memory_finish; 232 | } 233 | break; 234 | } 235 | } 236 | 237 | pthread_mutex_unlock(&call->mutex); 238 | 239 | __php_ilimit_memory_finish: 240 | pthread_exit(NULL); 241 | } /* }}} */ 242 | 243 | static zend_always_inline int php_ilimit_memory(php_ilimit_call_t *call) { /* {{{ */ 244 | call->limits.memory.max += zend_memory_usage(0); 245 | 246 | if (call->limits.memory.max > PG(memory_limit)) { 247 | return FAILURE; 248 | } 249 | 250 | if (call->limits.memory.interval <= 0) { 251 | call->limits.memory.interval = 100; 252 | } 253 | 254 | return SUCCESS; 255 | } /* }}} */ 256 | 257 | void php_ilimit_call_init(php_ilimit_call_t *call, zend_execute_data *entry) { /* {{{ */ 258 | memset(call, 0, sizeof(php_ilimit_call_t)); 259 | 260 | pthread_mutex_init(&call->mutex, NULL); 261 | pthread_cond_init(&call->cond, NULL); 262 | 263 | call->zend.entry = entry; 264 | } /* }}} */ 265 | 266 | static zend_always_inline void php_ilimit_call_cleanup(php_ilimit_call_t *call) { /* {{{ */ 267 | zend_execute_data *execute_data = call->zend.frame, 268 | *execute_entry = call->zend.entry; 269 | 270 | while (execute_data && execute_data != execute_entry) { 271 | zend_execute_data *prev; 272 | uint32_t info = 273 | ZEND_CALL_INFO(execute_data); 274 | zend_bool user = (EX(func)->type == ZEND_USER_FUNCTION); 275 | 276 | zval *var = EX_VAR_NUM(0), 277 | *end = var + 278 | (user ? 279 | EX(func)->op_array.last_var : 280 | EX(func)->common.num_args); 281 | 282 | while (var < end) { 283 | if (Z_OPT_REFCOUNTED_P(var)) { 284 | zval_ptr_dtor_nogc(var); 285 | } 286 | var++; 287 | } 288 | 289 | if (user && EX(func)->op_array.last_live_range) { 290 | int i; 291 | uint32_t op = EX(opline) - EX(func)->op_array.opcodes; 292 | 293 | for (i = 0; i < EX(func)->op_array.last_live_range; i++) { 294 | const zend_live_range *range = &EX(func)->op_array.live_range[i]; 295 | 296 | if (range->start > op) { 297 | break; 298 | } 299 | 300 | if (op < range->end) { 301 | uint32_t kind = range->var & ZEND_LIVE_MASK; 302 | uint32_t var_num = range->var & ~ZEND_LIVE_MASK; 303 | zval *var = EX_VAR(var_num); 304 | 305 | if (kind == ZEND_LIVE_TMPVAR) { 306 | zval_ptr_dtor_nogc(var); 307 | } else 308 | #ifdef ZEND_LIVE_NEW 309 | if (kind == ZEND_LIVE_NEW) { 310 | zend_object_store_ctor_failed(Z_OBJ_P(var)); 311 | OBJ_RELEASE(Z_OBJ_P(var)); 312 | } else 313 | #endif 314 | if (kind == ZEND_LIVE_LOOP) { 315 | if (Z_TYPE_P(var) != IS_ARRAY && Z_FE_ITER_P(var) != (uint32_t)-1) { 316 | zend_hash_iterator_del(Z_FE_ITER_P(var)); 317 | } 318 | zval_ptr_dtor_nogc(var); 319 | } else if (kind == ZEND_LIVE_ROPE) { 320 | 321 | zend_string **rope = (zend_string **)var; 322 | zend_op *last = EX(func)->op_array.opcodes + op; 323 | while ((last->opcode != ZEND_ROPE_ADD && last->opcode != ZEND_ROPE_INIT) 324 | || last->result.var != var_num) { 325 | ZEND_ASSERT(last >= EX(func)->op_array.opcodes); 326 | last--; 327 | } 328 | if (last->opcode == ZEND_ROPE_INIT) { 329 | #if PHP_VERSION_ID >= 70300 330 | zend_string_release_ex(*rope, 0); 331 | #else 332 | zend_string_release(*rope); 333 | #endif 334 | } else { 335 | int j = last->extended_value; 336 | do { 337 | #if PHP_VERSION_ID >= 70300 338 | zend_string_release_ex(rope[j], 0); 339 | #else 340 | zend_string_release(rope[j]); 341 | #endif 342 | } while (j--); 343 | } 344 | } else if (kind == ZEND_LIVE_SILENCE) { 345 | if (!EG(error_reporting) && Z_LVAL_P(var) != 0) { 346 | EG(error_reporting) = Z_LVAL_P(var); 347 | } 348 | } 349 | } 350 | } 351 | } 352 | 353 | if (info & ZEND_CALL_FREE_EXTRA_ARGS) { 354 | zend_vm_stack_free_extra_args_ex(info, execute_data); 355 | } 356 | 357 | if (info & ZEND_CALL_RELEASE_THIS) { 358 | #ifdef ZEND_CALL_CTOR 359 | if (info & ZEND_CALL_CTOR) { 360 | #if PHP_VERSION_ID >= 70300 361 | GC_DELREF(Z_OBJ(EX(This))); 362 | #else 363 | GC_REFCOUNT(Z_OBJ(EX(This)))--; 364 | #endif 365 | if (GC_REFCOUNT(Z_OBJ(EX(This))) == 1) { 366 | zend_object_store_ctor_failed(Z_OBJ(EX(This))); 367 | } 368 | } 369 | #endif 370 | OBJ_RELEASE(Z_OBJ(EX(This))); 371 | } 372 | 373 | if (EX(func)->common.fn_flags & ZEND_ACC_CLOSURE) { 374 | #if PHP_VERSION_ID >= 70300 375 | OBJ_RELEASE(ZEND_CLOSURE_OBJECT(EX(func))); 376 | #else 377 | OBJ_RELEASE((zend_object*) EX(func)->common.prototype); 378 | #endif 379 | } 380 | 381 | prev = EX(prev_execute_data); 382 | 383 | if (prev != execute_entry) { 384 | zend_vm_stack_free_call_frame_ex(info, execute_data); 385 | } 386 | 387 | execute_data = prev; 388 | } 389 | } /* }}} */ 390 | 391 | static zend_always_inline void php_ilimit_call_destroy(php_ilimit_call_t *call) { /* {{{ */ 392 | if (call->state & PHP_ILIMIT_TIMEOUT) { 393 | zend_throw_exception_ex(php_ilimit_timeout_ex, 0, 394 | "the time limit of %" PRIu64 " microseconds has been reached", 395 | call->limits.timeout); 396 | php_ilimit_call_cleanup(call); 397 | } 398 | 399 | if (call->state & PHP_ILIMIT_MEMORY) { 400 | zend_throw_exception_ex(php_ilimit_memory_ex, 0, 401 | "the memory limit of %" PRIu64 " bytes has been reached", 402 | call->limits.memory.max); 403 | php_ilimit_call_cleanup(call); 404 | } 405 | 406 | pthread_mutex_destroy(&call->mutex); 407 | pthread_cond_destroy(&call->cond); 408 | } /* }}} */ 409 | 410 | void php_ilimit_call(php_ilimit_call_t *call) { /* {{{ */ 411 | struct timespec clock; 412 | 413 | pthread_mutex_lock(&call->mutex); 414 | 415 | if (call->limits.timeout <= 0) { 416 | zend_throw_exception_ex(php_ilimit_runtime_ex, 0, 417 | "timeout must be positive"); 418 | call->state |= PHP_ILIMIT_FINISHED; 419 | pthread_mutex_unlock(&call->mutex); 420 | 421 | goto __php_ilimit_call_destroy; 422 | } 423 | 424 | if (call->limits.memory.max < 0) { 425 | zend_throw_exception_ex(php_ilimit_runtime_ex, 0, 426 | "memory must not be negative"); 427 | call->state |= PHP_ILIMIT_FINISHED; 428 | pthread_mutex_unlock(&call->mutex); 429 | 430 | goto __php_ilimit_call_destroy; 431 | } 432 | 433 | if (call->limits.memory.max > 0) { 434 | if (php_ilimit_memory(call) != SUCCESS) { 435 | zend_throw_exception_ex(php_ilimit_memory_ex, 0, 436 | "memory limit of %" PRIu64 " bytes would be exceeded", 437 | PG(memory_limit)); 438 | call->state |= PHP_ILIMIT_FINISHED; 439 | pthread_mutex_unlock(&call->mutex); 440 | 441 | goto __php_ilimit_call_destroy; 442 | } 443 | 444 | if (pthread_create(&call->threads.memory, NULL, __php_ilimit_memory_thread, call) != SUCCESS) { 445 | zend_throw_exception_ex(php_ilimit_sys_ex, 0, 446 | "cannot create memory management thread"); 447 | call->state |= PHP_ILIMIT_FINISHED; 448 | pthread_mutex_unlock(&call->mutex); 449 | 450 | goto __php_ilimit_call_destroy; 451 | } 452 | } 453 | 454 | php_ilimit_clock(&clock, call->limits.timeout); 455 | 456 | if (pthread_create(&call->threads.timeout, NULL, __php_ilimit_call_thread, call) != SUCCESS) { 457 | zend_throw_exception_ex(php_ilimit_sys_ex, 0, 458 | "cannot create timeout management thread"); 459 | call->state |= PHP_ILIMIT_FINISHED; 460 | pthread_cond_broadcast(&call->cond); 461 | pthread_mutex_unlock(&call->mutex); 462 | 463 | if (call->limits.memory.max) { 464 | pthread_join(call->threads.memory, NULL); 465 | } 466 | 467 | goto __php_ilimit_call_destroy; 468 | } 469 | 470 | while (!(call->state & PHP_ILIMIT_FINISHED)) { 471 | switch (pthread_cond_timedwait(&call->cond, &call->mutex, &clock)) { 472 | case SUCCESS: 473 | /* do nothing, signalled */ 474 | break; 475 | 476 | case ETIMEDOUT: { 477 | call->zend.frame = 478 | EG(current_execute_data); 479 | 480 | call->state |= PHP_ILIMIT_TIMEOUT; 481 | 482 | pthread_cond_broadcast(&call->cond); 483 | pthread_mutex_unlock(&call->mutex); 484 | 485 | php_ilimit_call_cancel(call); 486 | } 487 | 488 | goto __php_ilimit_call_finish; 489 | } 490 | } 491 | 492 | pthread_mutex_unlock(&call->mutex); 493 | 494 | __php_ilimit_call_finish: 495 | pthread_join(call->threads.timeout, NULL); 496 | 497 | if (call->limits.memory.max) { 498 | pthread_join(call->threads.memory, NULL); 499 | } 500 | 501 | __php_ilimit_call_destroy: 502 | php_ilimit_call_destroy(call); 503 | } /* }}} */ 504 | 505 | #endif 506 | -------------------------------------------------------------------------------- /src/ilimit.h: -------------------------------------------------------------------------------- 1 | /* 2 | +----------------------------------------------------------------------+ 3 | | ilimit | 4 | +----------------------------------------------------------------------+ 5 | | Copyright (c) Joe Watkins 2019 | 6 | +----------------------------------------------------------------------+ 7 | | This source file is subject to version 3.01 of the PHP license, | 8 | | that is bundled with this package in the file LICENSE, and is | 9 | | available through the world-wide-web at the following url: | 10 | | http://www.php.net/license/3_01.txt | 11 | | If you did not receive a copy of the PHP license and are unable to | 12 | | obtain it through the world-wide-web, please send a note to | 13 | | license@php.net so we can mail you a copy immediately. | 14 | +----------------------------------------------------------------------+ 15 | | Author: krakjoe | 16 | +----------------------------------------------------------------------+ 17 | */ 18 | #ifndef HAVE_PHP_ILIMIT_H 19 | #define HAVE_PHP_ILIMIT_H 20 | 21 | #include "zend_exceptions.h" 22 | #include "zend_closures.h" 23 | #include "zend_generators.h" 24 | 25 | #ifdef ZTS 26 | # error "Cannot support thread safe builds" 27 | #else 28 | # include 29 | #endif 30 | 31 | extern zend_class_entry *php_ilimit_ex; 32 | extern zend_class_entry *php_ilimit_sys_ex; 33 | extern zend_class_entry *php_ilimit_timeout_ex; 34 | extern zend_class_entry *php_ilimit_memory_ex; 35 | 36 | typedef struct _php_ilimit_call_t { 37 | pthread_mutex_t mutex; 38 | pthread_cond_t cond; 39 | zend_ulong state; 40 | 41 | struct _php_ilimit_call_threads { 42 | pthread_t timeout; 43 | pthread_t memory; 44 | } threads; 45 | 46 | struct _php_ilimit_call_zend { 47 | zend_fcall_info info; 48 | zend_fcall_info_cache cache; 49 | zend_execute_data *entry; 50 | zend_execute_data *frame; 51 | } zend; 52 | 53 | struct _php_ilimit_call_limits { 54 | zend_long timeout; 55 | struct _memory { 56 | zend_long max; 57 | zend_long interval; 58 | } memory; 59 | } limits; 60 | 61 | } php_ilimit_call_t; 62 | 63 | void php_ilimit_startup(void); 64 | 65 | void php_ilimit_call_init(php_ilimit_call_t *call, zend_execute_data *entry); 66 | void php_ilimit_call(php_ilimit_call_t *call); 67 | #endif 68 | -------------------------------------------------------------------------------- /stubs.php: -------------------------------------------------------------------------------- 1 | 9 | --FILE-- 10 | 15 | --EXPECTF-- 16 | Fatal error: Uncaught ilimit\Error\Timeout: the time limit of 1000000 microseconds has been reached in %s/001.php:3 17 | Stack trace: 18 | #0 %s/001.php(3): sleep(5) 19 | #1 [internal function]: {closure}() 20 | #2 %s/001.php(4): ilimit\call(Object(Closure), Array, 1000000) 21 | #3 {main} 22 | thrown in %s/001.php on line 3 23 | -------------------------------------------------------------------------------- /tests/002.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check ilimit memory 3 | --SKIPIF-- 4 | 9 | --FILE-- 10 | 17 | --EXPECTF-- 18 | Fatal error: Uncaught ilimit\Error\Memory: the memory limit of %d bytes has been reached in %s:4 19 | Stack trace: 20 | #0 [internal function]: {closure}() 21 | #1 %s(6): ilimit\call(Object(Closure), Array, 100000000, 10000) 22 | #2 {main} 23 | thrown in %s on line 4 24 | 25 | -------------------------------------------------------------------------------- /tests/003.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check ilimit catch Timeout 3 | --SKIPIF-- 4 | 9 | --FILE-- 10 | 19 | --EXPECT-- 20 | OK 21 | -------------------------------------------------------------------------------- /tests/004.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check ilimit catch memory 3 | --SKIPIF-- 4 | 9 | --FILE-- 10 | 21 | --EXPECT-- 22 | OK 23 | -------------------------------------------------------------------------------- /tests/005.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check finally blocks execution 3 | --SKIPIF-- 4 | 9 | --FILE-- 10 | 21 | --EXPECTF-- 22 | Fatal error: Uncaught ilimit\Error\Timeout: the time limit of 1000000 microseconds has been reached in %s:4 23 | Stack trace: 24 | #0 %s(4): sleep(2000) 25 | #1 [internal function]: wait() 26 | #2 %s(10): ilimit\call('wait', Array, 1000000) 27 | #3 {main} 28 | thrown in %s on line 4 29 | -------------------------------------------------------------------------------- /tests/006.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check timeout within while loops 3 | --SKIPIF-- 4 | 9 | --FILE-- 10 | 15 | --EXPECTF-- 16 | Fatal error: Uncaught ilimit\Error\Timeout: the time limit of 1000000 microseconds has been reached in %s:3 17 | Stack trace: 18 | #0 [internal function]: {closure}() 19 | #1 %s(4): ilimit\call(Object(Closure), Array, 1000000) 20 | #2 {main} 21 | thrown in %s on line 3 22 | -------------------------------------------------------------------------------- /tests/007.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check nested ilimit calls 3 | --SKIPIF-- 4 | 9 | --FILE-- 10 | 17 | --EXPECTF-- 18 | Fatal error: Uncaught ilimit\Error\Timeout: the time limit of 500000 microseconds has been reached in %s:4 19 | Stack trace: 20 | #0 %s(4): sleep(10) 21 | #1 [internal function]: {closure}() 22 | #2 %s(5): ilimit\call(Object(Closure), Array, 500000) 23 | #3 [internal function]: {closure}() 24 | #4 %s(6): ilimit\call(Object(Closure), Array, 1000000) 25 | #5 {main} 26 | thrown in %s on line 4 27 | -------------------------------------------------------------------------------- /tests/008.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check ilimit restores silence 3 | --SKIPIF-- 4 | 9 | --FILE-- 10 | 19 | --EXPECTF-- 20 | Warning: fopen(.non_existent_path): failed to open stream: No such file or directory in %s on line 8 21 | 22 | -------------------------------------------------------------------------------- /tests/009.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check ilimit interrupts foreach over non-array 3 | --SKIPIF-- 4 | 9 | --FILE-- 10 | $value) { 14 | sleep(10); 15 | } 16 | }, [new class { 17 | public $a = "apples"; 18 | public $b = "oranges"; 19 | }], 1000000); 20 | } catch (\ilimit\Error\Timeout $e) { 21 | echo "OK\n"; 22 | } 23 | ?> 24 | --EXPECTF-- 25 | OK 26 | 27 | -------------------------------------------------------------------------------- /tests/010.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check ilimit interrupts constructor 3 | --SKIPIF-- 4 | 9 | --FILE-- 10 | 25 | --EXPECTF-- 26 | OK 27 | 28 | -------------------------------------------------------------------------------- /tests/011.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check ilimit interrupts constructor 3 | --SKIPIF-- 4 | 9 | --FILE-- 10 | 25 | --EXPECTF-- 26 | OK 27 | 28 | -------------------------------------------------------------------------------- /tests/012.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check ilimit runtime invalid timeout 3 | --SKIPIF-- 4 | 9 | --FILE-- 10 | getMessage()); 15 | } 16 | ?> 17 | --EXPECT-- 18 | string(24) "timeout must be positive" 19 | 20 | 21 | -------------------------------------------------------------------------------- /tests/013.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check ilimit free extra args 3 | --SKIPIF-- 4 | 9 | --FILE-- 10 | 19 | --EXPECTF-- 20 | Fatal error: Uncaught ilimit\Error\Timeout: the time limit of 1000000 microseconds has been reached in %s:4 21 | Stack trace: 22 | #0 %s(4): sleep(10) 23 | #1 [internal function]: {closure}('one', 'two', 'three') 24 | #2 %s(5): ilimit\call(Object(Closure), Array, 1000000) 25 | #3 {main} 26 | thrown in %s on line 4 27 | 28 | 29 | -------------------------------------------------------------------------------- /tests/014.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check ilimit runtime invalid memory 3 | --SKIPIF-- 4 | 9 | --FILE-- 10 | getMessage()); 15 | } 16 | ?> 17 | --EXPECT-- 18 | string(27) "memory must not be negative" 19 | 20 | 21 | -------------------------------------------------------------------------------- /tests/015.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check ilimit runtime precondition memory 3 | --SKIPIF-- 4 | 9 | --FILE-- 10 | getMessage()); 19 | } 20 | ?> 21 | --EXPECTF-- 22 | string(%d) "memory limit of %d bytes would be exceeded" 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/016.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check ilimit rope interrupt 3 | --SKIPIF-- 4 | 9 | --FILE-- 10 | 29 | --EXPECT-- 30 | OK 31 | 32 | 33 | -------------------------------------------------------------------------------- /tests/017.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check timeout with foreach 3 | --SKIPIF-- 4 | 9 | --FILE-- 10 | $value) { 22 | continue; 23 | } 24 | }, [], 9000); 25 | } catch (\ilimit\Error\Timeout $t) { 26 | echo "OK"; 27 | } 28 | 29 | ?> 30 | --EXPECT-- 31 | OK 32 | -------------------------------------------------------------------------------- /tests/018.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check timeout with recursive functions 3 | --SKIPIF-- 4 | 9 | --FILE-- 10 | 23 | --EXPECT-- 24 | OK 25 | 26 | --------------------------------------------------------------------------------