diff --git a/.github/workflows/linux-x64.yml b/.github/workflows/linux-x64.yml index 1908e4b0..fd4bd87f 100644 --- a/.github/workflows/linux-x64.yml +++ b/.github/workflows/linux-x64.yml @@ -368,7 +368,7 @@ jobs: path: build/integration-* phpt: - name: PHPT - PHP ${{ matrix.php }} ZTS + name: PHPT - PHP ${{ matrix.php }} ZTS${{ matrix.sanitize && format(' ({0} sanitizer)', matrix.sanitize) || '' }} if: ${{ startsWith(github.ref, 'refs/tags/') || !contains(github.event.head_commit.message || '', '--skip-tests') }} runs-on: ubuntu-22.04 timeout-minutes: 180 @@ -377,6 +377,14 @@ jobs: fail-fast: false matrix: php: ["8.4", "8.5"] + # Empty means no sanitizer. The include below overrides it, so GitHub + # adds a separate job instead of merging into the PHP 8.5 one. + sanitize: [""] + include: + # Undefined behavior in the generated C++ (division by zero, shift + # counts, signed overflow, misaligned access...) aborts the test binary. + - php: "8.5" + sanitize: undefined env: PHPX_HOME: ${{ github.workspace }}/third_party/phpx NO_INTERACTION: 1 @@ -489,6 +497,7 @@ jobs: mkdir -p build/phpt-metrics .github/scripts/observe-command.sh build/phpt-metrics -- \ php run-tests.php -q -j8 --compiler .github/scripts/observe-tpc.sh \ + ${{ matrix.sanitize && format('--sanitize {0}', matrix.sanitize) || '' }} \ -w build/failed-tests.txt -W build/test-results.txt tests/compiler - name: Summarize PHPT compiler metrics @@ -503,13 +512,13 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: phpt-metrics-linux-x64-php-${{ matrix.php }}-zts + name: phpt-metrics-linux-x64-php-${{ matrix.php }}-zts${{ matrix.sanitize && format('-{0}', matrix.sanitize) || '' }} if-no-files-found: warn retention-days: 14 path: build/phpt-metrics - name: Package tested Linux compiler - if: startsWith(github.ref, 'refs/tags/') + if: startsWith(github.ref, 'refs/tags/') && !matrix.sanitize shell: bash run: | composer install --no-dev --prefer-dist --no-progress --classmap-authoritative @@ -521,7 +530,7 @@ jobs: test "$(find . -maxdepth 1 -name 'tpc_v*_linux_x64_php${{ matrix.php }}.*-zts.tar.gz' -type f | wc -l)" -eq 1 - name: Upload Linux release package - if: startsWith(github.ref, 'refs/tags/') + if: startsWith(github.ref, 'refs/tags/') && !matrix.sanitize uses: actions/upload-artifact@v4 with: name: release-linux-x64-php-${{ matrix.php }}-zts @@ -533,7 +542,7 @@ jobs: if: failure() uses: actions/upload-artifact@v4 with: - name: phpt-failures-linux-x64-php-${{ matrix.php }}-zts + name: phpt-failures-linux-x64-php-${{ matrix.php }}-zts${{ matrix.sanitize && format('-{0}', matrix.sanitize) || '' }} if-no-files-found: ignore retention-days: 7 path: | diff --git a/run-tests.php b/run-tests.php index e5cbc226..cb92314e 100755 --- a/run-tests.php +++ b/run-tests.php @@ -132,6 +132,11 @@ function show_usage(): void --no-aot Run tests without AOT compilation (plain PHP mode). + --sanitize + Compile every test binary with the given sanitizers (passed to + the compiler as --sanitize), e.g. undefined or address,undefined. + A sanitizer report aborts the binary, so the test fails. + --compiler Use specified compiler binary (default: ./bin/tpc.php). For bootstrap testing, use: --compiler ./tpc @@ -169,7 +174,7 @@ function main(): void $temp_source, $temp_target, $test_cnt, $test_files, $test_idx, $test_results, $testfile, $valgrind, $sum_results, $shuffle, $file_cache, $num_repeats, - $show_progress, $aot_parallel_root, $test_target; + $show_progress, $aot_parallel_root, $test_target, $aot_sanitize; // Parallel testing global $workers, $workerID; global $context_line_count; @@ -610,6 +615,15 @@ function main(): void . ':print_suppressions=0'; } break; + case '--sanitize': + $aot_sanitize = parse_sanitize_option($argv[++$i] ?? ''); + // A report must fail the test instead of scrolling past in its output. + $environment['UBSAN_OPTIONS'] = 'halt_on_error=1:abort_on_error=0:print_stacktrace=1'; + if (in_array('address', $aot_sanitize, true)) { + $environment['ASAN_OPTIONS'] = 'halt_on_error=1:detect_leaks=0'; + $environment['SKIP_ASAN'] = 1; + } + break; case '--repeat': $num_repeats = (int) $argv[++$i]; $environment['SKIP_REPEAT'] = 1; @@ -2497,6 +2511,10 @@ function run_test(string $php, $file, array $env): string if (!$no_aot) { try { $aot_args = $test->hasSection('AOT_ARGS') ? trim($test->getSection('AOT_ARGS')) : ''; + global $aot_sanitize; + if (!empty($aot_sanitize)) { + $aot_args = trim($aot_args . ' --sanitize ' . implode(',', $aot_sanitize)); + } if ($test_target === 'native') { $bin_file = compile_php_file($test_file, $aot_args); } else { @@ -4534,6 +4552,22 @@ function normalize_wasm_test_output(string $output): string return str_replace("\r\n", "\n", trim($output)); } +/** + * Parse --sanitize: a comma-separated subset of the sanitizers the compiler supports. + * + * @return list + */ +function parse_sanitize_option(string $value): array +{ + $sanitizers = array_values(array_unique(array_filter(array_map('trim', explode(',', $value))))); + $unknown = array_diff($sanitizers, ['address', 'undefined']); + if ($sanitizers === [] || $unknown !== []) { + fwrite(STDERR, "--sanitize expects address and/or undefined, got: " . $value . "\n"); + exit(1); + } + return $sanitizers; +} + function compile_php_file(string $file, string $compiler_args = ''): string { global $compiler_path, $workerID, $aot_parallel_root;