Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/gh24280/bench.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php
class Plain {
public $a = 1, $b = 2, $c = 3, $d = 4;
}
class Typed {
public int $a = 1, $b = 2, $c = 3, $d = 4;
}
class Restricted {
public private(set) int $a = 1, $b = 2, $c = 3, $d = 4;
public function run(int $count): int {
$sum = 0;
for ($i = 0; $i < $count; ++$i) {
foreach ($this as &$value) { $sum += $value; }
unset($value);
}
return $sum;
}
}
function run(string $case, int $count): int {
if ($case === 'restricted-inside') { return (new Restricted)->run($count); }
$data = match ($case) {
'array' => [1, 2, 3, 4],
'stdclass' => (object) ['a'=>1, 'b'=>2, 'c'=>3, 'd'=>4],
'plain', 'retained' => new Plain,
'typed' => new Typed,
};
$retained = [];
if ($case === 'retained') {
foreach ($data as &$value) { $retained[] =& $value; }
unset($value);
}
$sum = 0;
for ($i = 0; $i < $count; ++$i) {
foreach ($data as &$value) { $sum += $value; }
unset($value);
}
return $sum;
}
$case = $argv[1];
run($case, 10000);
$start = hrtime(true);
$sum = run($case, 1500000);
$elapsed = (hrtime(true) - $start) / 1e6;
if ($sum !== 15000000) { throw new LogicException('Wrong result'); }
echo json_encode(['ms'=>$elapsed,'sum'=>$sum]), "\n";
25 changes: 25 additions & 0 deletions .github/gh24280/bench.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import json
import pathlib
import statistics
import subprocess
import sys

base, patched, script = map(lambda p: str(pathlib.Path(p).resolve()), sys.argv[1:])
results = {}
for case in ['array', 'stdclass', 'plain', 'typed', 'retained', 'restricted-inside']:
values = {'base': [], 'patched': []}
for repeat in range(20):
order = [('base', base), ('patched', patched)]
if repeat % 2:
order.reverse()
for name, binary in order:
run = subprocess.run([binary, '-n', '-d', 'opcache.enable_cli=0', script, case], capture_output=True, text=True)
assert run.returncode == 0 and not run.stderr, run.stdout + run.stderr
value = json.loads(run.stdout)
assert value['sum'] == 15000000
values[name].append(value['ms'])
medians = {name: statistics.median(samples) for name, samples in values.items()}
delta = 100 * (medians['patched'] / medians['base'] - 1)
results[case] = {'samples': values, 'median_ms': medians, 'delta_percent': delta}
print(f'{case}: base {medians["base"]:.3f} ms; patched {medians["patched"]:.3f} ms; delta {delta:+.2f}%', flush=True)
pathlib.Path('benchmark.json').write_text(json.dumps(results, indent=2))
54 changes: 54 additions & 0 deletions .github/gh24280/check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import json
import pathlib
import subprocess
import sys
import tempfile

kind, binary, tests = sys.argv[1:]
binary = str(pathlib.Path(binary).resolve())
tests = pathlib.Path(tests)
files = sorted(tests.glob('gh24280*.phpt'))
assert len(files) == 3
version = subprocess.check_output([binary, '-n', '-v'], text=True)
print(version)

with tempfile.TemporaryDirectory() as directory:
if kind == 'baseline':
source = (tests / 'gh24280_foreach_by_ref.phpt').read_text()
script = pathlib.Path(directory) / 'baseline.php'
script.write_text(source.split('--FILE--\n', 1)[1].split('--EXPECT--\n', 1)[0])
result = subprocess.run([binary, '-n', str(script)], capture_output=True, text=True)
assert result.returncode == 0, result.stdout + result.stderr
for signal in ['private outside: accepted', 'existing reference outside: accepted',
'private child: accepted', 'protected outside: accepted']:
assert signal in result.stdout, result.stdout
assert 'readonly outside: Cannot acquire reference' in result.stdout
print('RED: four setter-visibility bypasses reproduced on this exact base')
sys.exit(0)

module = pathlib.Path(binary).parents[2] / 'modules/opcache.so'
extension = ['-d', f'zend_extension={module}'] if module.exists() else []
modes = {
'interpreter': ['-n', '-d', 'opcache.enable_cli=0'],
'opcache': ['-n', *extension, '-d', 'opcache.enable_cli=1', '-d', 'opcache.jit=disable'],
'jit-tracing': ['-n', *extension, '-d', 'opcache.enable_cli=1', '-d', 'opcache.jit_buffer_size=64M', '-d', 'opcache.jit=tracing'],
'jit-function': ['-n', *extension, '-d', 'opcache.enable_cli=1', '-d', 'opcache.jit_buffer_size=64M', '-d', 'opcache.jit=function'],
}
for mode, options in modes.items():
if mode != 'interpreter':
status = subprocess.run([binary, *options, '-r',
"echo json_encode(opcache_get_status(false));"], capture_output=True, text=True)
assert status.returncode == 0 and not status.stderr, status.stdout + status.stderr
state = json.loads(status.stdout)
assert isinstance(state, dict) and state['opcache_enabled']
if mode.startswith('jit-'):
assert state['jit']['on'], state
for test in files:
content = test.read_text()
script = pathlib.Path(directory) / (test.stem + '.php')
script.write_text(content.split('--FILE--\n', 1)[1].split('--EXPECT--\n', 1)[0])
result = subprocess.run([binary, *options, str(script)], capture_output=True, text=True)
expected = content.split('--EXPECT--\n', 1)[1].strip()
assert result.returncode == 0 and not result.stderr and result.stdout.strip() == expected, (
f'{test.name}/{mode}: {result.returncode}\n{result.stdout}\n{result.stderr}')
print(f'PASS {test.name}/{mode}')
50 changes: 50 additions & 0 deletions .github/gh24280/vm.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h
index a5605f9a43..68f7bffcbb 100644
--- a/Zend/zend_vm_def.h
+++ b/Zend/zend_vm_def.h
@@ -7295,22 +7295,29 @@ ZEND_VM_HANDLER(126, ZEND_FE_FETCH_RW, VAR, ANY, JMP_ADDR)
value_type = Z_TYPE_INFO_P(value);
if (EXPECTED(value_type != IS_UNDEF)
&& EXPECTED(zend_check_property_access(Z_OBJ_P(array), p->key, 0) == SUCCESS)) {
- if ((value_type & Z_TYPE_MASK) != IS_REFERENCE) {
- zend_property_info *prop_info =
- zend_get_property_info_for_slot(Z_OBJ_P(array), value);
- if (prop_info) {
- if (UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)) {
- zend_throw_error(NULL,
- "Cannot acquire reference to readonly property %s::$%s",
- ZSTR_VAL(prop_info->ce->name), ZSTR_VAL(p->key));
- UNDEF_RESULT();
- HANDLE_EXCEPTION();
- }
- if (ZEND_TYPE_IS_SET(prop_info->type)) {
- ZVAL_NEW_REF(value, value);
- ZEND_REF_ADD_TYPE_SOURCE(Z_REF_P(value), prop_info);
- value_type = IS_REFERENCE_EX;
- }
+ zend_property_info *prop_info =
+ zend_get_property_info_for_slot(Z_OBJ_P(array), value);
+ if (prop_info) {
+ if ((value_type & Z_TYPE_MASK) != IS_REFERENCE
+ && UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)) {
+ zend_throw_error(NULL,
+ "Cannot acquire reference to readonly property %s::$%s",
+ ZSTR_VAL(prop_info->ce->name), ZSTR_VAL(p->key));
+ UNDEF_RESULT();
+ HANDLE_EXCEPTION();
+ }
+ /* An existing reference must not bypass setter visibility. */
+ if (UNEXPECTED(prop_info->flags & ZEND_ACC_PPP_SET_MASK)
+ && UNEXPECTED(!zend_asymmetric_property_has_set_access(prop_info))) {
+ zend_asymmetric_visibility_property_modification_error(prop_info, "indirectly modify");
+ UNDEF_RESULT();
+ HANDLE_EXCEPTION();
+ }
+ if ((value_type & Z_TYPE_MASK) != IS_REFERENCE
+ && ZEND_TYPE_IS_SET(prop_info->type)) {
+ ZVAL_NEW_REF(value, value);
+ ZEND_REF_ADD_TYPE_SOURCE(Z_REF_P(value), prop_info);
+ value_type = IS_REFERENCE_EX;
}
}
break;
133 changes: 133 additions & 0 deletions .github/workflows/gh24280-current.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
name: GH-24280 current qualification
on:
push:
branches: [fix/gh24280-current-20261023]
workflow_dispatch:
permissions:
contents: read
jobs:
generated:
runs-on: ubuntu-24.04
permissions:
contents: write
outputs:
sha: ${{ steps.generated.outputs.sha }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.sha }}
- run: sudo apt-get update && sudo apt-get install -y php-cli
- id: generated
run: |
php Zend/zend_vm_gen.php
git add Zend/zend_vm_execute.h Zend/zend_vm_opcodes.c Zend/zend_vm_opcodes.h
if ! git diff --cached --quiet; then
git -c user.name='Xauele' -c user.email='251239544+Xauele@users.noreply.github.com' commit -m 'Regenerate Zend VM executor from current patched source'
git push origin HEAD:refs/heads/fix/gh24280-current-20261023 --force-with-lease=refs/heads/fix/gh24280-current-20261023:${{ github.sha }}
fi
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
native:
needs: generated
runs-on: ubuntu-24.04
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- version: PHP-8.4
base: f6852befa34b1107da57d26ead79e575ccd73e2a
flags: --enable-opcache
- version: PHP-8.4-ZTS
base: f6852befa34b1107da57d26ead79e575ccd73e2a
flags: --enable-opcache --enable-zts
- version: PHP-8.5
base: 9da174f52a1d4f9f1f3613ade049283ca3995875
flags: ''
- version: master
base: 022073187266c934fea9d5aafd9dde49cc09d449
flags: ''
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.generated.outputs.sha }}
path: candidate
persist-credentials: false
- uses: actions/checkout@v4
with:
repository: php/php-src
ref: ${{ matrix.base }}
path: source
persist-credentials: false
- name: Dependencies
run: |
sudo apt-get update
sudo apt-get install -y autoconf bison re2c build-essential pkg-config libxml2-dev php-cli
- name: Build exact unmodified base
working-directory: source
run: |
./buildconf --force
php Zend/zend_vm_gen.php
./configure --disable-all --enable-cli --disable-cgi --enable-debug ${{ matrix.flags }}
make -j2 > /tmp/build-base.log 2>&1 || { tail -100 /tmp/build-base.log; exit 1; }
- name: Reproduce setter bypass on exact base
run: python3 candidate/.github/gh24280/check.py baseline source/sapi/cli/php candidate/Zend/tests/asymmetric_visibility
- name: Apply patch and rebuild
working-directory: source
run: |
git apply --check ../candidate/.github/gh24280/vm.patch
git apply ../candidate/.github/gh24280/vm.patch
cp ../candidate/Zend/tests/asymmetric_visibility/gh24280*.phpt Zend/tests/asymmetric_visibility/
php Zend/zend_vm_gen.php
if [ '${{ matrix.version }}' = 'PHP-8.4' ]; then cmp Zend/zend_vm_execute.h ../candidate/Zend/zend_vm_execute.h; fi
make -j2 > /tmp/build-patch.log 2>&1 || { tail -100 /tmp/build-patch.log; exit 1; }
- name: Complete Zend test suite
working-directory: source
env:
NO_INTERACTION: 1
REPORT_EXIT_STATUS: 1
run: sapi/cli/php -n run-tests.php -q -j2 Zend/tests
- name: Interpreter OPcache and real JIT checks
if: success() || failure()
run: python3 candidate/.github/gh24280/check.py patched source/sapi/cli/php source/Zend/tests/asymmetric_visibility
- uses: actions/upload-artifact@v4
if: failure()
with:
name: diagnostics-${{ matrix.version }}
path: |
source/Zend/tests/**/*.diff
source/Zend/tests/**/*.out
performance:
needs: generated
runs-on: ubuntu-24.04
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.generated.outputs.sha }}
path: candidate
persist-credentials: false
- uses: actions/checkout@v4
with:
repository: php/php-src
ref: f6852befa34b1107da57d26ead79e575ccd73e2a
path: source
persist-credentials: false
- run: sudo apt-get update && sudo apt-get install -y autoconf bison re2c build-essential pkg-config libxml2-dev php-cli
- name: Build release baseline and candidate identically
working-directory: source
run: |
./buildconf --force
php Zend/zend_vm_gen.php
./configure --disable-all --enable-cli --disable-cgi
make -j2 > /tmp/build-base.log 2>&1 || { tail -100 /tmp/build-base.log; exit 1; }
cp sapi/cli/php ../php-baseline
git apply ../candidate/.github/gh24280/vm.patch
php Zend/zend_vm_gen.php
make -j2 > /tmp/build-patch.log 2>&1 || { tail -100 /tmp/build-patch.log; exit 1; }
cp sapi/cli/php ../php-patched
- name: Alternating same-runner A/B microbenchmark
run: python3 candidate/.github/gh24280/bench.py php-baseline php-patched candidate/.github/gh24280/bench.php
- uses: actions/upload-artifact@v4
with:
name: foreach-ab-benchmark
path: benchmark.json
Loading
Loading