# [Pre-RFC] Honour Linux AT_EXECVE_CHECK: W^X for PHP scripts

- Thread: https://externals.io/message/133055
- Original archive: https://news-web.php.net/php.internals/133055
- Started by Alexander Mikhalitsyn on 2026-11-10T12:03:35+00:00
- Messages: 1
- Reply by email to internals@lists.php.net
- More for agents: search with https://externals.io/search.md?q={query} · RFCs at https://externals.io/rfc.md · JSON API at https://externals.io/api · documentation at https://externals.io/llms.txt

---

## #133055 — Alexander Mikhalitsyn — 2026-11-10T12:03:35+00:00

URL: https://externals.io/message/133055#133055

Dear friends,

I'd like to get early feedback on a small change: making PHP honour the
Linux AT_EXECVE_CHECK mechanism, so that a system-wide W^X policy can
also cover PHP code, not only native binaries.

Background
----------

Linux 6.14 added two related features [1]:

 - AT_EXECVE_CHECK, a flag for execveat(2). It asks the kernel "could
   this file be executed?" without executing it. The kernel applies the
   same checks as a real execve(): the noexec mount option, the
   executable bit, and LSM policies such as IPE or SELinux.

 - Two securebits, set per process and inherited by children:
   SECBIT_EXEC_RESTRICT_FILE and SECBIT_EXEC_DENY_INTERACTIVE. They
tell
   script interpreters to enforce the result of that check.

Today a system can forbid running an unknown binary, but
`php /tmp/payload.php` or an include of an uploaded file still works:
the interpreter is the loophole. The kernel side was designed for
interpreters to close it.

What the patch does
-------------------

compile_file() checks the descriptor a script is read from, before
compiling it. The check always runs, so it can be audited (for example
with strace), but it is only enforced when a securebit is set:

 - SECBIT_EXEC_RESTRICT_FILE: the main script, include and require from
   a file that isn't executable throw an Error. This happens at compile
   time, before any of its functions or classes are bound. Streams with
   no file behind them, such as data:// and user stream wrappers, are
   refused too.

 - SECBIT_EXEC_DENY_INTERACTIVE: code that doesn't come from an
   executable file is refused. That covers php -r/-B/-R/-E, php -a,
   piped stdin and including php://stdin.

Notes:

 - phar entries are checked against the archive file, and a
   Phar::mount()ed file is checked against itself.
 - php://filter over a plain file is checked against that file.
 - php -l still works on any file, because it doesn't execute anything.

There is no INI setting. The policy belongs to whoever starts the
process (systemd, a container runtime, an administrator), and is
enforced the same way for every interpreter on the system. Without the
securebits, PHP behaves exactly as before. The cost is one execveat()
syscall per compiled file. With opcache, a cache hit compiles nothing,
so it costs nothing. On other platforms and on older kernels, nothing
changes.

The patch is small: about 300 lines across Zend/zend_stream.c,
compile_file(), main, phar and the CLI SAPI.

  https://github.com/mihalicyn/php-src/commits/at_execve_check_support

Internals changes, listed in UPGRADING.INTERNALS:

 - zend_stream has a new fd callback. Code that builds
   ZEND_HANDLE_STREAM handles itself should set it. If it doesn't, the
   code is treated as not coming from a file.
 - There is a new PHP_STREAM_OPTION_EXEC_FD stream option, with a
   php_stream_get_exec_fd() helper. It lets a stream that reads code
   from part of a file (an archive entry) name the file to check.
 - There is a new ZEND_COMPILE_NO_EXEC_CHECK compiler option, for code
   that is compiled but not executed.

Field test
----------

I ran phpBB 3.3 with SQLite under SECBIT_EXEC_RESTRICT_FILE, on PHP's
built-in server. A crawl of the board and of the admin panel passes.
Running `chmod a-x` on a file makes every page that uses it fail with
"Failed to execute ...: blocked by EXEC_RESTRICT_FILE".

As we know real applications pretty often generate PHP code at
runtime. phpBB writes its config.php, its compiled Symfony container,
its router, its BBCode renderer and its Twig templates. Under W^X, all
of that has to be generated ahead of time and marked executable (and
read-only) at deploy time. A warm-up script can do it. This is exactly
the property the policy is meant to enforce, but frameworks will need a
documented "compile everything at deploy" step. I have a self-contained
demo (a script that builds the patched PHP and installs phpBB) in the
same branch on GitHub.

Open questions
--------------

1. opcache: a script served from the cache is not checked again. A
   `chmod a-x` takes effect only after a cache reset. My view is that
   processes in different security domains must not share an opcache
   (shared memory or opcache.file_cache) anyway, so the RFC would
   document this rather than add checks to opcache. Opinions welcome.

2. Compressed phars (.phar.gz, .phar.bz2) are refused under
   SECBIT_EXEC_RESTRICT_FILE, because phar keeps no descriptor of the
   original archive. This fails closed. Is that acceptable as a known
   limitation?

3. Under SECBIT_EXEC_RESTRICT_FILE, user stream wrappers can't provide
   executable code at all. I think that's correct for this policy, but
   it will surprise some setups.

4. Does this need an RFC vote? Without the securebits it changes no
   behaviour, but it is a new security feature and touches an internal
   ABI. I'm happy to write the RFC, and phpt tests are on the way.

Kind regards,
Alex

[1] https://docs.kernel.org/userspace-api/check_exec.html
