diff --git a/appsec/src/extension/commands_helpers.c b/appsec/src/extension/commands_helpers.c
index f589b94d0b5..efc4eee3d5c 100644
--- a/appsec/src/extension/commands_helpers.c
+++ b/appsec/src/extension/commands_helpers.c
@@ -179,7 +179,7 @@ static dd_result _dd_command_exec(dd_conn *nonnull conn,
"Response message for %.*s does not have the expected form",
NAME_L);
- return dd_error;
+ return dd_network;
}
if (res != dd_success && res != dd_should_block &&
res != dd_should_redirect && res != dd_should_record) {
diff --git a/appsec/src/extension/ddappsec.h b/appsec/src/extension/ddappsec.h
index c19fa82618e..17d219f5060 100644
--- a/appsec/src/extension/ddappsec.h
+++ b/appsec/src/extension/ddappsec.h
@@ -38,6 +38,7 @@ ZEND_BEGIN_MODULE_GLOBALS(ddappsec)
bool to_be_configured : 1;
bool skip_rshutdown : 1;
+ // used to avoid a bailout during request shutdown
bool during_request_shutdown : 1;
ZEND_END_MODULE_GLOBALS(ddappsec)
// clang-format on
diff --git a/appsec/src/extension/request_lifecycle.c b/appsec/src/extension/request_lifecycle.c
index 4d9cb27088f..c627ac2ee0b 100644
--- a/appsec/src/extension/request_lifecycle.c
+++ b/appsec/src/extension/request_lifecycle.c
@@ -283,7 +283,39 @@ static zend_array *nullable _do_request_begin(
return spec;
}
+static void _do_req_lifecycle_rshutdown(bool ignore_verdict, bool force);
+
void dd_req_lifecycle_rshutdown(bool ignore_verdict, bool force)
+{
+ // temporarily remove the memory limit to avoid bailouts
+ // and as a safety net do a try-catch
+ __auto_type orig_mem_limit = PG(memory_limit);
+ zend_set_memory_limit((size_t)Z_L(-1) >> 1);
+
+ zend_try {
+ _do_req_lifecycle_rshutdown(ignore_verdict, force);
+ } zend_catch {
+ if (PG(last_error_message)) {
+ mlog_g(dd_log_error,
+ "Bailout in request shutdown; disconnecting from helper: %s",
+ ZSTR_VAL(PG(last_error_message)));
+ } else {
+ mlog_g(dd_log_error,
+ "Bailout in request shutdown; disconnecting from helper");
+ }
+ _reset_globals();
+ dd_helper_close_conn(); // note: not completely bailout-safe,
+ // but should be fine with the raised mem limit
+ } zend_end_try();
+
+ __auto_type res = zend_set_memory_limit(orig_mem_limit);
+ if (res == FAILURE) {
+ mlog(dd_log_error,
+ "Failed to restore memory limit in request shutdown");
+ }
+}
+
+static void _do_req_lifecycle_rshutdown(bool ignore_verdict, bool force)
{
if (DDAPPSEC_G(enabled) == APPSEC_FULLY_DISABLED) {
mlog_g(dd_log_debug, "Skipping all request shutdown actions because "
@@ -437,6 +469,7 @@ static zend_array *_do_request_finish_user_req(bool ignore_verdict,
return spec;
}
+// Should be bailout-safe -- means, in particular, no emalloc allocations
static void _reset_globals(void)
{
_set_cur_span(NULL);
@@ -491,7 +524,10 @@ static void _set_cur_span(zend_object *nullable span)
}
}
-bool dd_req_lifecycle_is_active(void) { return _between_init_shutdown_msgs; }
+bool dd_req_lifecycle_is_active(void)
+{
+ return _between_init_shutdown_msgs && DDAPPSEC_G(active);
+}
zend_object *nullable dd_req_lifecycle_get_cur_span(void)
{
diff --git a/appsec/src/extension/user_tracking.c b/appsec/src/extension/user_tracking.c
index e1a865ee3d8..2ec46df104e 100644
--- a/appsec/src/extension/user_tracking.c
+++ b/appsec/src/extension/user_tracking.c
@@ -228,6 +228,12 @@ void dd_find_and_apply_verdict_for_user(zend_string *nullable user_id,
return;
}
+ if (!dd_req_lifecycle_is_active()) {
+ mlog_g(dd_log_info,
+ "Not running inside a tracked request; skipping user verdict");
+ return;
+ }
+
dd_conn *conn = dd_helper_mgr_cur_conn();
if (conn == NULL) {
mlog(dd_log_debug, "No connection; unable to check user");
diff --git a/appsec/tests/integration/src/main/groovy/com/datadog/appsec/php/docker/LogFile.groovy b/appsec/tests/integration/src/main/groovy/com/datadog/appsec/php/docker/LogFile.groovy
new file mode 100644
index 00000000000..db95a1c2267
--- /dev/null
+++ b/appsec/tests/integration/src/main/groovy/com/datadog/appsec/php/docker/LogFile.groovy
@@ -0,0 +1,49 @@
+package com.datadog.appsec.php.docker
+
+/**
+ * A log file inside an {@link AppSecContainer}. Supports marking the current
+ * end-of-file position and then reading only what was appended since the mark,
+ * which is the usual way a test isolates the log output of a single action.
+ *
+ *
+ * def lf = new LogFile(container, 'helper.log')
+ * lf.markEndPos()
+ * ... do something that writes to the log ...
+ * lf.linesSinceMark().any { it.contains('boom') }
+ *
+ *
+ * A {@code name} without a leading {@code /} is resolved under {@link #LOG_DIR}.
+ */
+class LogFile {
+ static final String LOG_DIR = '/tmp/logs'
+
+ private final AppSecContainer container
+ final String path
+ private long markPos = 0
+
+ LogFile(AppSecContainer container, String name) {
+ this.container = container
+ this.path = name.startsWith('/') ? name : "${LOG_DIR}/${name}"
+ }
+
+ /** Current size of the log in bytes (0 if it does not exist yet). */
+ long size() {
+ container.execInContainer('bash', '-c', "wc -c < ${path} 2>/dev/null || echo 0".toString())
+ .stdout.trim() as long
+ }
+
+ /** Record the current end position; subsequent reads start from here. */
+ void markEndPos() {
+ markPos = size()
+ }
+
+ /** Raw text appended since the last {@link #markEndPos()}. */
+ String getTextSinceMark() {
+ container.execInContainer('bash', '-c', "tail -c +${markPos + 1} ${path}".toString()).stdout
+ }
+
+ /** Lines appended since the last {@link #markEndPos()}. */
+ List getLinesSinceMark() {
+ getTextSinceMark().readLines()
+ }
+}
diff --git a/appsec/tests/integration/src/main/groovy/com/datadog/appsec/php/docker/PhpFpm.groovy b/appsec/tests/integration/src/main/groovy/com/datadog/appsec/php/docker/PhpFpm.groovy
new file mode 100644
index 00000000000..e7c768b4f89
--- /dev/null
+++ b/appsec/tests/integration/src/main/groovy/com/datadog/appsec/php/docker/PhpFpm.groovy
@@ -0,0 +1,97 @@
+package com.datadog.appsec.php.docker
+
+import groovy.util.logging.Slf4j
+import org.testcontainers.containers.Container.ExecResult
+
+/**
+ * Helpers for reconfiguring PHP-FPM inside a running {@link AppSecContainer}.
+ *
+ * Two strategies are offered:
+ *
+ * - {@link #restart} hard-kills every php-fpm process and relaunches the
+ * master, optionally exporting extra environment variables or loading a
+ * different php.ini. Use it for changes that only take effect on a cold
+ * start (environment variables, php.ini edits).
+ * - {@link #reload} sends SIGUSR2 for a graceful reload and blocks until the
+ * pre-reload workers have been replaced. Use it for pool.d settings (e.g.
+ * {@code pm.max_children}) that FPM re-reads on USR2; pair it with
+ * {@link #setPoolValue} / {@link #backupPoolConfig} / {@link #restorePoolConfig}.
+ *
+ */
+@Slf4j
+class PhpFpm {
+ static final String FPM_CONF = '/etc/php-fpm.conf'
+ static final String DEFAULT_INI = '/etc/php/php.ini'
+ static final String POOL_CONF = '/etc/php-fpm.d/www.conf'
+
+ private final AppSecContainer container
+
+ PhpFpm(AppSecContainer container) {
+ this.container = container
+ }
+
+ /**
+ * Hard-restart php-fpm: kill every php-fpm process and relaunch the master.
+ *
+ * @param env extra environment variables exported before launch
+ * @param iniPath the php.ini to load (defaults to {@link #DEFAULT_INI})
+ */
+ ExecResult restart(Map env = [:], String iniPath = DEFAULT_INI) {
+ container.flushProfilingData()
+ String exports = env.collect { k, v -> "export ${k}=${v};" }.join(' ')
+ ExecResult res = container.execInContainer('bash', '-c',
+ "kill -9 `pgrep php-fpm`; ${exports} php-fpm -y ${FPM_CONF} -c ${iniPath}".toString())
+ assert res.exitCode == 0 : "php-fpm restart failed: ${res.stderr}"
+ res
+ }
+
+ /** Rewrite a single pool directive (e.g. {@code pm.max_children}) in place. */
+ void setPoolValue(String key, String value, String poolConf = POOL_CONF) {
+ container.execInContainer('sed', '-i', "s/${key} = .*/${key} = ${value}/".toString(), poolConf)
+ }
+
+ /** Back up the pool config so {@link #restorePoolConfig} can revert any edits. */
+ void backupPoolConfig(String poolConf = POOL_CONF) {
+ container.execInContainer('cp', poolConf, "${poolConf}.bak".toString())
+ }
+
+ /** Restore the pool config saved by {@link #backupPoolConfig}. */
+ void restorePoolConfig(String poolConf = POOL_CONF) {
+ container.execInContainer('mv', "${poolConf}.bak".toString(), poolConf)
+ }
+
+ /**
+ * Gracefully reload the FPM master (re-reads pool config without dropping the
+ * socket) and block until every pre-reload worker has been replaced by a
+ * freshly-spawned one.
+ */
+ void reload(long timeoutMillis = 5_000) {
+ List old = workerPids()
+ // Locate the master via its pid file, falling back to the oldest php-fpm process.
+ container.execInContainer('bash', '-c',
+ 'kill -USR2 $(cat /run/php-fpm*.pid /var/run/php-fpm*.pid 2>/dev/null | head -1) ' +
+ '2>/dev/null || pkill -USR2 -o php-fpm || true')
+ waitForWorkerTurnover(old, timeoutMillis)
+ }
+
+ /** PIDs of the current pool worker processes (the master is excluded). */
+ List workerPids() {
+ container.execInContainer('bash', '-c', "pgrep -f 'php-fpm: pool' || true")
+ .stdout.readLines()*.trim().findAll { it }
+ }
+
+ private void waitForWorkerTurnover(List old, long timeoutMillis) {
+ long deadline = System.currentTimeMillis() + timeoutMillis
+ while (true) {
+ List current = workerPids()
+ if (!current.isEmpty() && current.intersect(old).isEmpty()) {
+ return
+ }
+ if (System.currentTimeMillis() > deadline) {
+ throw new IllegalStateException(
+ "php-fpm workers were not reloaded in time (old=${old}, current=${current})".toString())
+ }
+ Thread.sleep(100)
+ }
+ }
+}
diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Apache2FpmTests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Apache2FpmTests.groovy
index 9be5159b761..e92c05240b2 100644
--- a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Apache2FpmTests.groovy
+++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Apache2FpmTests.groovy
@@ -3,6 +3,7 @@ package com.datadog.appsec.php.integration
import com.datadog.appsec.php.docker.AppSecContainer
import com.datadog.appsec.php.docker.FailOnUnmatchedTraces
import com.datadog.appsec.php.docker.InspectContainerHelper
+import com.datadog.appsec.php.docker.PhpFpm
import groovy.util.logging.Slf4j
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.condition.DisabledIf
@@ -79,21 +80,11 @@ class Apache2FpmTests implements CommonTests, SamplingTestsInFpm, EndpointFallba
}
void setRateLimit(String limit) {
- flushProfilingData()
- def res = container.execInContainer(
- 'bash', '-c',
- """kill -9 `pgrep php-fpm`;
- export DD_APPSEC_TRACE_RATE_LIMIT=$limit;
- php-fpm -y /etc/php-fpm.conf -c /etc/php/php.ini""")
- assert res.exitCode == 0
+ new PhpFpm(container).restart([DD_APPSEC_TRACE_RATE_LIMIT: limit])
}
private void resetFpm() {
- flushProfilingData()
- container.execInContainer(
- 'bash', '-c',
- '''kill -9 `pgrep php-fpm`;
- php-fpm -y /etc/php-fpm.conf -c /etc/php/php.ini''')
+ new PhpFpm(container).restart()
}
@Test
diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/EndpointFallbackSamplingTests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/EndpointFallbackSamplingTests.groovy
index 6c051a9ebd9..d9c32c1e6b8 100644
--- a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/EndpointFallbackSamplingTests.groovy
+++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/EndpointFallbackSamplingTests.groovy
@@ -1,6 +1,7 @@
package com.datadog.appsec.php.integration
import com.datadog.appsec.php.docker.AppSecContainer
+import com.datadog.appsec.php.docker.PhpFpm
import org.junit.jupiter.api.Test
import java.net.http.HttpResponse
@@ -110,12 +111,6 @@ trait EndpointFallbackSamplingTests extends SamplingTestsInFpm {
}
void disableEndpointRenaming() {
- flushProfilingData()
- def res = container.execInContainer(
- 'bash', '-c',
- '''kill -9 `pgrep php-fpm`;
- export DD_TRACE_RESOURCE_RENAMING_ENABLED=false;
- php-fpm -y /etc/php-fpm.conf -c /etc/php/php.ini''')
- assert res.exitCode == 0
+ new PhpFpm(container).restart([DD_TRACE_RESOURCE_RENAMING_ENABLED: 'false'])
}
}
diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/NginxFpmTests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/NginxFpmTests.groovy
index 8b22701d5d6..606b091b8ed 100644
--- a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/NginxFpmTests.groovy
+++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/NginxFpmTests.groovy
@@ -3,7 +3,10 @@ package com.datadog.appsec.php.integration
import com.datadog.appsec.php.docker.AppSecContainer
import com.datadog.appsec.php.docker.FailOnUnmatchedTraces
import com.datadog.appsec.php.docker.InspectContainerHelper
+import com.datadog.appsec.php.docker.LogFile
+import com.datadog.appsec.php.docker.PhpFpm
import groovy.util.logging.Slf4j
+import org.junit.jupiter.api.Assumptions
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.condition.DisabledIf
@@ -14,6 +17,7 @@ import java.net.http.HttpResponse
import static com.datadog.appsec.php.integration.TestParams.getPhpVersion
import static com.datadog.appsec.php.integration.TestParams.getVariant
+import static java.net.http.HttpResponse.BodyHandlers.ofString
@Testcontainers
@Slf4j
@@ -47,4 +51,72 @@ class NginxFpmTests implements CommonTests {
}
}
+ /**
+ * Regression test: OOM inside dd_entity_body_convert() (called from
+ * dd_request_shutdown() via _request_pack()) triggers zend_bailout(), which
+ * the per-module zend_try swallows, so the helper never gets RequestShutdown
+ * and sees an out-of-order RequestInit on the next request.
+ */
+ @Test
+ void 'no unexpected RequestInit due to RSHUTDOWN OOM bail'() {
+ Assumptions.assumeTrue(System.getProperty('USE_HELPER_RUST') != null,
+ 'the C++ helper silently swallows out-of-order commands.')
+ // PHP 8.3 release only (zts already excluded at class level): the debug
+ // allocator's heap-protection turns the mid-allocation OOM bailout into
+ // a spurious "zend_mm_heap corrupted" SIGABRT that masks the real bug.
+ Assumptions.assumeTrue(phpVersion == '8.3' && !variant.contains('debug'),
+ 'requires a PHP 8.3 release build')
+
+ // Drop the pool to a single worker so the OOM request and the follow-up
+ // land on the same FPM process / helper socket.
+ PhpFpm fpm = new PhpFpm(container)
+ fpm.backupPoolConfig()
+
+ try {
+ fpm.setPoolValue('pm.max_children', '1')
+ fpm.reload()
+
+ LogFile helperLog = new LogFile(container, 'helper.log')
+ helperLog.markEndPos()
+
+ // Warm-up: establish the helper connection so it is in its outer loop
+ // waiting for request_init before the OOM request.
+ container.traceFromRequest('/hello.php') { HttpResponse resp ->
+ assert resp.statusCode() == 200
+ }
+
+ // Trigger Pattern B: rshutdown_oom.php sets a 32M memory_limit, pins
+ // ~28 MiB live in $GLOBALS, then emits ~400 KiB of JSON. During
+ // RSHUTDOWN, dd_request_shutdown()'s _request_pack() callback parses
+ // that body via dd_entity_body_convert(), overflowing the ceiling;
+ // zend_bailout() fires before _omsg_send(), so the socket is untouched.
+ container.traceFromRequest('/rshutdown_oom.php', ofString()) {
+ HttpResponse resp ->
+ // The script completes (OOM happens during RSHUTDOWN, after the
+ // response is on the wire).
+ assert resp.statusCode() == 200
+ }
+
+ // originally, the would actually fail here because the bailout during
+ // rshutdown would skip _reset_globals() and _cur_req_span would not be
+ // reset. This would either lead to a crash (502), or, if the same slot
+ // was used for the span in the next request, for the span to be
+ // prematurely deleted on the next request as _cur_req_span was being
+ // replaced
+ container.traceFromRequest('/hello.php') { HttpResponse resp ->
+ assert resp.statusCode() == 200
+ }
+
+ List lines = helperLog.linesSinceMark
+ assert !lines.any { it.contains('unexpected command RequestInit') } :
+ 'Error message found. Relevant helper log:\n' +
+ lines.findAll {
+ it.contains('unexpected command') || it.contains('error in request loop')
+ }.join('\n')
+ } finally {
+ fpm.restorePoolConfig()
+ fpm.reload()
+ }
+ }
+
}
diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/RoadRunnerTests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/RoadRunnerTests.groovy
index 51ade591d40..04ae7007b68 100644
--- a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/RoadRunnerTests.groovy
+++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/RoadRunnerTests.groovy
@@ -3,6 +3,7 @@ package com.datadog.appsec.php.integration
import com.datadog.appsec.php.docker.AppSecContainer
import com.datadog.appsec.php.docker.FailOnUnmatchedTraces
import com.datadog.appsec.php.docker.InspectContainerHelper
+import com.datadog.appsec.php.docker.LogFile
import groovy.util.logging.Slf4j
import org.junit.jupiter.api.Assumptions
import org.junit.jupiter.api.BeforeAll
@@ -62,8 +63,8 @@ class RoadRunnerTests implements WorkerStrategyTests {
Assumptions.assumeTrue(System.getProperty('USE_HELPER_RUST') != null,
'This bug only manifests on the Rust helper (strict outer/inner loop state machine).')
- long logOffset = (CONTAINER.execInContainer('bash', '-c',
- 'wc -c < /tmp/logs/helper.log').stdout.trim() as long)
+ LogFile helperLog = new LogFile(CONTAINER, 'helper.log')
+ helperLog.markEndPos()
// PostRespondLfiHandler sets a callback that calls fopen('../etc/passwd')
// after respond() returns. By that point, request_shutdown has been sent
@@ -78,15 +79,52 @@ class RoadRunnerTests implements WorkerStrategyTests {
assert resp.statusCode() == 200
}
- String helperLog = CONTAINER.execInContainer('bash', '-c',
- "tail -c +${logOffset + 1} /tmp/logs/helper.log").stdout
+ List lines = helperLog.linesSinceMark
+ log.info("Helper log since offset:\n{}", lines.join('\n'))
- log.info("Helper log since offset:\n{}", helperLog)
+ assert !lines.any { it.contains('unexpected command RequestExec') } :
+ "Helper received RequestExec in outer loop. " +
+ "Relevant log:\n" +
+ lines.findAll {
+ it.contains('unexpected command') || it.contains('error in request loop')
+ }.join('\n')
+ }
+
+ /**
+ * Regression test for the AppSec helper "unexpected command RequestExec" bug,
+ * variant where the post-respond RequestExec sender is the user-tracking SDK
+ * (track_user_login_success -> dd_find_and_apply_verdict_for_user) rather than
+ * push_addresses.
+ */
+ @Test
+ void 'no unexpected RequestExec in outer loop after post-respond track_user_login'() {
+ Assumptions.assumeTrue(System.getProperty('USE_HELPER_RUST') != null,
+ 'This bug only manifests on the Rust helper (strict outer/inner loop state machine).')
+
+ LogFile helperLog = new LogFile(CONTAINER, 'helper.log')
+ helperLog.markEndPos()
+
+ // PostRespondTrackUserHandler sets a callback that calls
+ // track_user_login_success() after respond() returns. By that point,
+ // request_shutdown has been sent via the response_committed hook. If
+ // dd_find_and_apply_verdict_for_user still reaches the helper (socket
+ // open, active=true), it sends RequestExec into the outer loop.
+ CONTAINER.traceFromRequest('/post-respond-track-user') { HttpResponse resp ->
+ assert resp.statusCode() == 200
+ }
+
+ // Follow-up request verifies the connection is still usable.
+ CONTAINER.traceFromRequest('/') { HttpResponse resp ->
+ assert resp.statusCode() == 200
+ }
+
+ List lines = helperLog.linesSinceMark
+ log.info("Helper log since offset:\n{}", lines.join('\n'))
- assert !helperLog.contains('unexpected command RequestExec') :
+ assert !lines.any { it.contains('unexpected command RequestExec') } :
"Helper received RequestExec in outer loop. " +
"Relevant log:\n" +
- helperLog.readLines().findAll {
+ lines.findAll {
it.contains('unexpected command') || it.contains('error in request loop')
}.join('\n')
}
diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/SamplingTestsInFpm.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/SamplingTestsInFpm.groovy
index 7fbea7b5ec3..6a7b8f643df 100644
--- a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/SamplingTestsInFpm.groovy
+++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/SamplingTestsInFpm.groovy
@@ -1,6 +1,7 @@
package com.datadog.appsec.php.integration
import com.datadog.appsec.php.docker.AppSecContainer
+import com.datadog.appsec.php.docker.PhpFpm
import com.datadog.appsec.php.model.Trace
import org.junit.jupiter.api.Test
@@ -102,21 +103,11 @@ trait SamplingTestsInFpm {
}
void setSamplingPeriod(String period) {
- flushProfilingData()
- def res = container.execInContainer(
- 'bash', '-c',
- """kill -9 `pgrep php-fpm`;
- export DD_API_SECURITY_SAMPLE_DELAY=$period;
- php-fpm -y /etc/php-fpm.conf -c /etc/php/php.ini""")
- assert res.exitCode == 0
+ new PhpFpm(container).restart([DD_API_SECURITY_SAMPLE_DELAY: period])
}
private void resetFpm() {
- flushProfilingData()
- container.execInContainer(
- 'bash', '-c',
- '''kill -9 `pgrep php-fpm`;
- php-fpm -y /etc/php-fpm.conf -c /etc/php/php.ini''')
+ new PhpFpm(container).restart()
}
void flushProfilingData() {
diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/ZtsGshutdownTests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/ZtsGshutdownTests.groovy
index 2552a9dfd84..a8fc83ceb00 100644
--- a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/ZtsGshutdownTests.groovy
+++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/ZtsGshutdownTests.groovy
@@ -2,6 +2,7 @@ package com.datadog.appsec.php.integration
import com.datadog.appsec.php.docker.AppSecContainer
import com.datadog.appsec.php.docker.InspectContainerHelper
+import com.datadog.appsec.php.docker.LogFile
import groovy.util.logging.Slf4j
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.condition.EnabledIf
@@ -63,9 +64,8 @@ class ZtsGshutdownTests {
@Test
void 'no crash during GSHUTDOWN when MaxConnectionsPerChild 1 triggers ZTS worker lifecycle'() {
- long errorLogOffset = (CONTAINER.execInContainer('sh', '-c',
- 'stat -c %s /tmp/logs/apache2/error.log 2>/dev/null || echo 0')
- .stdout.trim() as long)
+ LogFile errorLog = new LogFile(CONTAINER, 'apache2/error.log')
+ errorLog.markEndPos()
ExecResult backupResult = CONTAINER.execInContainer('sh', '-c',
'cp /etc/apache2/mods-enabled/mpm_event.conf /etc/apache2/mods-enabled/mpm_event.conf.bak_zts')
@@ -111,13 +111,11 @@ class ZtsGshutdownTests {
// Additionally check Apache's error.log for crashes that generate SIGABRT
// before a core dump can be written (e.g. Rust allocator panics on
// poisoned memory).
- ExecResult logCheck = CONTAINER.execInContainer('sh', '-c',
- "tail -c +${errorLogOffset + 1} /tmp/logs/apache2/error.log")
- String errorLog = logCheck.stdout ?: ''
- assert !errorLog.contains('exit signal Aborted'):
- "Apache worker exited via SIGABRT during GSHUTDOWN:\n" + errorLog
- assert !errorLog.contains('exit signal Segmentation'):
- "Apache worker segfaulted during GSHUTDOWN:\n" + errorLog
+ String errorLogText = errorLog.getTextSinceMark()
+ assert !errorLogText.contains('exit signal Aborted'):
+ "Apache worker exited via SIGABRT during GSHUTDOWN:\n" + errorLogText
+ assert !errorLogText.contains('exit signal Segmentation'):
+ "Apache worker segfaulted during GSHUTDOWN:\n" + errorLogText
} finally {
CONTAINER.execInContainer('sh', '-c',
'cp /etc/apache2/mods-enabled/mpm_event.conf.bak_zts' +
diff --git a/appsec/tests/integration/src/test/www/_handlers/src/PostRespondTrackUserHandler.php b/appsec/tests/integration/src/test/www/_handlers/src/PostRespondTrackUserHandler.php
new file mode 100644
index 00000000000..cfa55535915
--- /dev/null
+++ b/appsec/tests/integration/src/test/www/_handlers/src/PostRespondTrackUserHandler.php
@@ -0,0 +1,23 @@
+ 'text/plain'], 'OK');
+ }
+}
diff --git a/appsec/tests/integration/src/test/www/base/public/rshutdown_oom.php b/appsec/tests/integration/src/test/www/base/public/rshutdown_oom.php
new file mode 100644
index 00000000000..3c565281b6f
--- /dev/null
+++ b/appsec/tests/integration/src/test/www/base/public/rshutdown_oom.php
@@ -0,0 +1,66 @@
+addRoute('/json', new \App\JsonHandler());
$router->addRoute('/xml', new \App\XmlHandler());
$router->addRoute('/post-respond-lfi', new \App\PostRespondLfiHandler());
+$router->addRoute('/post-respond-track-user', new \App\PostRespondTrackUserHandler());
while ($req = $httpWorker->waitRequest()) {
/** @var \Spiral\RoadRunner\Http\Request $req */