├── .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 |
--------------------------------------------------------------------------------