10

PHP7 offers a bytecode caching mechanism called opcache. I'd like to know if there is any way to distribute and run the "opcached" version of a PHP script (.bin file extension) without distributing its source code. (I enabled the opcache.file_cache directive in php.ini to obtain the .bin file.)

I assume that when executing a script, PHP7 will check the opcache directory for a .bin file with matching name, timestamp, and maybe even compare a checksum or hash value. If all things match, PHP7 will execute the .bin file instead of parse the .php file. Maybe it is possible to 'trick' PHP into executing the .bin file even when the corresponding .php script is not present?

dasup
  • 3,815
  • 1
  • 16
  • 25

1 Answers1

11

PHP needs to be able to open the file for opcache to be invoked; If it doesn't exist, it can't be loaded ...

Let's look in detail, to see what tricks we might play:

if (!file_handle->filename || !ZCG(enabled) || !accel_startup_ok) {
    /* The Accelerator is disabled, act as if without the Accelerator */
    return accelerator_orig_compile_file(file_handle, type);
#ifdef HAVE_OPCACHE_FILE_CACHE
} else if (ZCG(accel_directives).file_cache_only) {
    return file_cache_compile_file(file_handle, type);
#endif
} else if ((!ZCG(counted) && !ZCSG(accelerator_enabled)) ||
           (ZCSG(restart_in_progress) && accel_restart_is_active())) {
#ifdef HAVE_OPCACHE_FILE_CACHE
    if (ZCG(accel_directives).file_cache) {
        return file_cache_compile_file(file_handle, type);
    }
#endif
    return accelerator_orig_compile_file(file_handle, type);
}

We can see that where the file cache is enabled, it takes precedence over the shared memory cache.

Next, we want to look at file_cache_compile_file:

  1. block signals
  2. protect shared memory
  3. zend_file_cache_script_load

Now we look at zend_file_cache_script_load:

  1. open
  2. read header (layout)
  3. verify magic "OPCACHE"
  4. verify system id
  5. optionally validate timestamp
  6. perform read of cached file
  7. verify checksum

So the first problem we have is that the system id is not unique, but is made up of the following elements:

  1. PHP version
  2. Zend Extension build identifier
  3. Binary identifier, contains the following:
    1. sizeof(char)
    2. sizeof(int)
    3. sizeof(long)
    4. sizeof(size_t)
    5. sizeof(zend_long)
    6. ZEND_MM_ALIGNMENT
  4. If not using a dev version of PHP (unreleased, from git):
    1. ___DATE__ compile date of binary
    2. ___TIME___ compile time of binary

The PHP version and build identifier are required because at least the following may change between versions or builds:

  • integral identifiers for opcodes
  • the layout of internal structures
  • the sequence of instructions the VM expects (details of an existing control structure may change fe. foreach)
  • optimizations performed by opcache (because previous ones may be discovered to be unsafe)

The binary identifier is required because at least the layout of a zval changes with endianess and architecture: Architecture may effect the size of some basic compiler types (long, size_t and so on) as well as the upper and lower limits of those types, while endianess can effect the order of members in the structure, as well as the binary representation of basic compiler types.

Note that rather a lot of effort is expended to identify the current system, that should give you pause for thought ...

Disabling validation of timestamps opcache.validate_timestamps=0 will allow the loading of a file cache entry, even if the current file on the file system is empty.

The checksum included in the header is only to verify the script section of the file (which comes after the header), it doesn't (and can't) include the header where the system identifier, or checksum itself is written.

So you can trick PHP into loading a cached file from another machine by changing the system identifier in the header of the cached file to correspond with the target machines identifier.

Should you ?

For fun perhaps, but as a method of deploying your software, definitely not.

The file cache is not intended for this purpose, loading caches from different architectures and or builds will crash PHP.

Ikari
  • 3,176
  • 3
  • 29
  • 34
Joe Watkins
  • 17,032
  • 5
  • 41
  • 62
  • 1
    Thank you for your detailed breakdown! `opcache.validate_timestamps=0` together with a zero-length .php file does the trick for copying bytecode to another machine (given that both machines have same arch and PHP version). I understand that this is not suitable for "production". Is there any other way to deploy bytecode? Maybe this is something for [softwarerecs](http://softwarerecs.stackexchange.com/). – dasup Nov 18 '16 at 13:24
  • If you want to deploy obfuscated code, use a tool which obfuscates plain PHP code. – Andrea Dec 06 '16 at 16:46