Skip to content

perf: move sandboxed environment to the C side#2058

Open
AlliBalliBaba wants to merge 50 commits intomainfrom
refactor/cleanup-env
Open

perf: move sandboxed environment to the C side#2058
AlliBalliBaba wants to merge 50 commits intomainfrom
refactor/cleanup-env

Conversation

@AlliBalliBaba
Copy link
Contributor

This PR uses zend_array_dup to simplify the environment sandboxing logic. It removes the CGO overhead from $_ENV and $_SERVER registration in regular threads and saves some memory on many threads. (wip)

Quick Benchmark Main This PR
BenchmarkHelloWorld-20 7071 7482
BenchmarkEcho-20 12160 12098
BenchmarkServerSuperGlobal-20 5550 5829
BenchmarkUncommonHeaders-20 6444 6709

AlliBalliBaba and others added 30 commits November 1, 2025 21:33
* removes backoff.

* Adjusts comment.

* Suggestions by @dunglas

* Removes 'max_consecutive_failures'

* Removes 'max_consecutive_failures'

* Adjusts warning.

* Disables the logger in tests.

* Revert "Adjusts warning."

This reverts commit e93a6a9.

* Revert "Removes 'max_consecutive_failures'"

This reverts commit ba28ea0.

* Revert "Removes 'max_consecutive_failures'"

This reverts commit 32e649c.

* Only fails on max failures again.

* Restores failure timings.
# Conflicts:
#	phpmainthread_test.go
# Conflicts:
#	phpthread.go
#	threadworker.go
# Conflicts:
#	phpthread.go
#	threadinactive.go
#	threadregular.go
#	threadworker.go
#	worker.go
# Conflicts:
#	phpmainthread_test.go
#	threadregular.go
# Conflicts:
#	env.go
#	frankenphp.c
#	phpmainthread.go
#	phpthread.go
#	threadregular.go
# Conflicts:
#	env.go
#	frankenphp.c
#	phpmainthread.go
#	phpthread.go
#	threadregular.go
@AlliBalliBaba AlliBalliBaba marked this pull request as ready for review December 12, 2025 13:27
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR shifts environment sandboxing ($_ENV/$_SERVER + getenv/putenv) from Go into the C/Zend side by snapshotting the process environment once and using zend_array_dup / zend_hash_copy to avoid per-request CGO overhead.

Changes:

  • Add a main-thread environment snapshot (main_thread_env) in C and use it to populate $_SERVER/$_ENV.
  • Rework frankenphp_putenv/frankenphp_getenv to use a thread-local sandbox HashTable (sandboxed_env) and reset it at script termination.
  • Remove Go-side sandbox env caches/cleanup and update tests/fixtures accordingly.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
env.go Replaces Go map-based env snapshot with a C HashTable initializer called from C.
frankenphp.c Implements C-side env snapshotting + sandboxed getenv/putenv, and updates $_SERVER/$_ENV registration.
phpthread.go Removes per-thread Go-side sandbox env storage.
phpmainthread.go Removes main-thread Go-side sandbox env storage.
threadregular.go Drops Go-side sandbox env clearing for regular threads.
threadworker.go Drops Go-side sandbox env clearing during worker script setup.
worker_test.go Adjusts worker env test to rely on package TestMain setup.
frankenphp_test.go Sets a required env var in TestMain and updates expected env test output.
testdata/env/test-env.php Extends env behavior checks (putenv shouldn’t affect $_SERVER, invalid key handling).
caddy/caddy_test.go Moves env var setup for tests into package TestMain.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 18 to 25
zkey := C.frankenphp_init_persistent_string(toUnsafeChar(key), C.size_t(len(key)))
zvalStr := C.frankenphp_init_persistent_string(toUnsafeChar(val), C.size_t(len(val)))

// copy the main thread env to the thread specific env
func cloneSandboxedEnv(thread *phpThread) {
if thread.sandboxedEnv != nil {
return
}
thread.sandboxedEnv = make(map[string]*C.zend_string, len(mainThread.sandboxedEnv))
for key, value := range mainThread.sandboxedEnv {
thread.sandboxedEnv[key] = value
var zval C.zval
*(*uint32)(unsafe.Pointer(&zval.u1)) = C.IS_INTERNED_STRING_EX
*(**C.zend_string)(unsafe.Pointer(&zval.value)) = zvalStr
C.zend_hash_update(mainThreadEnv, zkey, &zval)
}
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

go_init_os_env constructs a C.zval by writing directly into zval.u1 / zval.value using unsafe.Pointer. This relies on Zend Engine internal layout details and is likely to break across PHP versions/architectures (and can lead to subtle memory corruption). Prefer adding a small C helper (e.g., using ZVAL_STR/ZVAL_INTERNED_STR + zend_hash_update) and call that from Go, rather than manually poking the struct fields.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would avoid implementing this suggestion because it would involve a CGO call.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a wrapper function that does both ZVAL_STR and zend_hash_update to keep it at 1 Cgo call.

Comment on lines 1238 to 1243
/* take a snapshot of the environment for sandboxing */
if (main_thread_env == NULL) {
main_thread_env = malloc(sizeof(HashTable));
zend_hash_init(main_thread_env, 8, NULL, NULL, 1);
go_init_os_env(main_thread_env);
}
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

main_thread_env is allocated with malloc(sizeof(HashTable)) and initialized, but it is never destroyed/freed during shutdown. If FrankenPHP is used as an embedded library or in test harnesses that initialize/shutdown multiple times, this becomes a real leak (and it also bypasses Zend's persistent allocator conventions). Consider allocating via pemalloc(..., 1)/zend_new_array() and releasing it (e.g., zend_hash_destroy(main_thread_env) + free/pefree) during module/SAPI shutdown.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we ever want to free this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah these strings should never be freed. I changed it to pemalloc(..., 1) to keep with convention

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants