diff --git a/infra/base-images/base-builder-ruby/Dockerfile b/infra/base-images/base-builder-ruby/Dockerfile index 14f88112e8c9..fc91499b27d9 100644 --- a/infra/base-images/base-builder-ruby/Dockerfile +++ b/infra/base-images/base-builder-ruby/Dockerfile @@ -21,6 +21,9 @@ RUN git clone https://github.com/trailofbits/ruzzy.git $SRC/ruzzy RUN /usr/local/bin/install_ruby.sh RUN /usr/local/bin/gem update --system 3.5.11 +# Install simplecov for coverage builds +RUN gem install simplecov + # Install ruzzy WORKDIR $SRC/ruzzy @@ -51,3 +54,8 @@ ENV GEM_HOME="$OUT/fuzz-gem" ENV GEM_PATH="/install/ruzzy" COPY ruzzy-build /usr/bin/ruzzy-build +COPY ruzzy-build-coverage /usr/bin/ruzzy-build-coverage +COPY build_ruby_fuzzer /usr/bin/build_ruby_fuzzer +COPY ruzzy /usr/bin/ruzzy +COPY coverage_helper.rb /usr/local/lib/coverage_helper.rb +COPY ossfuzz_helper.rb /usr/local/lib/ossfuzz_helper.rb diff --git a/infra/base-images/base-builder-ruby/build_ruby_fuzzer b/infra/base-images/base-builder-ruby/build_ruby_fuzzer new file mode 100755 index 000000000000..b243eeae1360 --- /dev/null +++ b/infra/base-images/base-builder-ruby/build_ruby_fuzzer @@ -0,0 +1,54 @@ +#!/bin/bash +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +################################################################################ +# +# Unified Ruby fuzzer builder - handles both normal fuzzing and coverage modes. +# +# Usage: build_ruby_fuzzer +# +# This script automatically detects the SANITIZER environment variable and: +# - For coverage: Creates coverage-instrumented wrapper via ruzzy-build-coverage +# - For normal fuzzing: Creates standard fuzzer wrapper via ruzzy-build +# +# Arguments: +# $1 - Path to the Ruby fuzzer harness file (e.g., /src/harnesses/fuzz_parse.rb) +# $2 - Output directory where the wrapper will be created (e.g., $OUT) +# $3 - Project name for corpus directory creation (e.g., ox-ruby) +# + +fuzz_target_path="$1" +out_dir="$2" +project_name="$3" + +if [ -z "$fuzz_target_path" ] || [ -z "$out_dir" ] || [ -z "$project_name" ]; then + echo "Error: Missing required arguments" + echo "Usage: build_ruby_fuzzer " + exit 1 +fi + +if [ ! -f "$fuzz_target_path" ]; then + echo "Error: Fuzzer harness not found: $fuzz_target_path" + exit 1 +fi + +if [[ "$SANITIZER" == "coverage" ]]; then + # Coverage mode: Use coverage wrapper builder + /usr/bin/ruzzy-build-coverage "$fuzz_target_path" "$out_dir" "$project_name" +else + # Normal fuzzing mode: Copy harness and use ruzzy-build + cp "$fuzz_target_path" "$out_dir/" + /usr/bin/ruzzy-build "$fuzz_target_path" "$out_dir" +fi diff --git a/infra/base-images/base-builder-ruby/coverage_helper.rb b/infra/base-images/base-builder-ruby/coverage_helper.rb new file mode 100644 index 000000000000..def7da9cf1a6 --- /dev/null +++ b/infra/base-images/base-builder-ruby/coverage_helper.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +################################################################################ + +# Coverage helper for Ruby fuzzers +# This script sets up SimpleCov for code coverage collection + +require 'simplecov' +require 'json' + +# Configure SimpleCov +SimpleCov.start do + # Set coverage directory from environment or use default + coverage_dir ENV['COVERAGE_DIR'] || File.join(Dir.pwd, 'coverage') + + # Use simple formatter for now, we'll customize the output + formatter SimpleCov::Formatter::SimpleFormatter + + # Track all files + track_files '**/*.rb' + + # Add filters to exclude test files and gems + add_filter '/spec/' + add_filter '/test/' + add_filter 'ossfuzz_helper' + add_filter 'coverage_helper' + + # Enable branch coverage for better insights + enable_coverage :branch + + # Merge results from multiple runs + use_merging true + merge_timeout 3600 +end + +# Store coverage command name based on fuzzer +if ENV['FUZZER_NAME'] + SimpleCov.command_name ENV['FUZZER_NAME'] +end diff --git a/infra/base-images/base-builder-ruby/ossfuzz_helper.rb b/infra/base-images/base-builder-ruby/ossfuzz_helper.rb new file mode 100644 index 000000000000..4ee3a0493e08 --- /dev/null +++ b/infra/base-images/base-builder-ruby/ossfuzz_helper.rb @@ -0,0 +1,48 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +################################################################################ + +# Only require ruzzy in normal fuzzing mode (not in coverage mode) +if ENV["COVERAGE_MODE"] != "true" + begin + require 'ruzzy' + rescue LoadError + # Ruzzy not available, will fail later if needed + end +end + +module OSSFuzz + # Unified fuzzing entry point that handles both normal fuzzing and coverage modes. + # + # In normal fuzzing mode: calls Ruzzy.fuzz() to start the fuzzing engine + # In coverage mode: reads corpus file from ARGV[0] and calls the target directly + # + # Usage: + # fuzz_target = lambda do |data| + # # your fuzzing logic + # end + # OSSFuzz.fuzz(fuzz_target) + # + def self.fuzz(fuzz_target) + if ENV["COVERAGE_MODE"] == "true" + # Coverage mode: execute target on input file + data = File.binread(ARGV[0]) + fuzz_target.call(data) + else + # Normal fuzzing mode + Ruzzy.fuzz(fuzz_target) + end + end +end diff --git a/infra/base-images/base-builder-ruby/ruzzy b/infra/base-images/base-builder-ruby/ruzzy new file mode 100755 index 000000000000..044d86d0e87a --- /dev/null +++ b/infra/base-images/base-builder-ruby/ruzzy @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +################################################################################ + +ASAN_OPTIONS="allocator_may_return_null=1:detect_leaks=0:use_sigaltstack=0" \ +LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \ +ruby $@ diff --git a/infra/base-images/base-builder-ruby/ruzzy-build b/infra/base-images/base-builder-ruby/ruzzy-build index 990004890988..90abb464fd1d 100755 --- a/infra/base-images/base-builder-ruby/ruzzy-build +++ b/infra/base-images/base-builder-ruby/ruzzy-build @@ -20,11 +20,15 @@ harness_sh=${fuzz_target::-3} cp $1 $OUT/$fuzz_target -echo """#!/usr/bin/env bash +# Copy ossfuzz_helper.rb for OSSFuzz.fuzz() support +cp /usr/local/lib/ossfuzz_helper.rb $OUT/ossfuzz_helper.rb + +cat > $OUT/$harness_sh << EOF +#!/usr/bin/env bash # LLVMFuzzerTestOneInput for fuzzer detection. -this_dir=\$(dirname \"\$0\") +this_dir=\$(dirname "\$0") export GEM_HOME=\$this_dir/fuzz-gem -ruzzy \$this_dir/$fuzz_target \$@ -""" > $OUT/$harness_sh +ruzzy -I\$this_dir \$this_dir/$fuzz_target \$@ +EOF chmod +x $OUT/$harness_sh diff --git a/infra/base-images/base-builder-ruby/ruzzy-build-coverage b/infra/base-images/base-builder-ruby/ruzzy-build-coverage new file mode 100755 index 000000000000..08564660cf8a --- /dev/null +++ b/infra/base-images/base-builder-ruby/ruzzy-build-coverage @@ -0,0 +1,94 @@ +#!/bin/bash +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +################################################################################ +# +# Generic coverage wrapper generator for Ruby fuzzers. +# +# Usage: ruzzy-build-coverage +# +# This script creates a coverage-instrumented wrapper for a Ruby fuzzer harness. +# It modifies the harness to support both normal fuzzing (via Ruzzy.fuzz) and +# coverage mode (via direct execution on corpus files). +# +# Arguments: +# $1 - Path to the Ruby fuzzer harness file (e.g., /src/harnesses/fuzz_memory.rb) +# $2 - Output directory where the wrapper will be created (e.g., $OUT) +# $3 - Project name for corpus directory creation (e.g., ffi) +# + +fuzz_target_path="$1" +out_dir="$2" +project_name="$3" + +if [ ! -f "$fuzz_target_path" ]; then + echo "Error: Fuzzer harness not found: $fuzz_target_path" + exit 1 +fi + +fuzzer_name=$(basename "$fuzz_target_path" .rb) + +echo "Building coverage wrapper for $fuzzer_name" + +# Create corpus directory for this fuzzer +mkdir -p "../../build/corpus/${project_name}/${fuzzer_name}" + +# Copy the harness as-is (no modification needed with OSSFuzz.fuzz()) +cp "$fuzz_target_path" "$out_dir/${fuzzer_name}.rb" + +# Ensure ossfuzz_helper.rb and coverage_helper.rb are available +cp /usr/local/lib/ossfuzz_helper.rb "$out_dir/" +cp /usr/local/lib/coverage_helper.rb "$out_dir/" + +# Create bash wrapper that runs the modified harness with Ruby coverage +# IMPORTANT: Include OSSFuzz.fuzz comment for fuzzer detection by base-runner coverage script +cat > "$out_dir/$fuzzer_name" << 'WRAPPER_EOF' +#!/usr/bin/env bash +# Coverage wrapper for Ruby fuzzer +# OSSFuzz.fuzz - This comment is needed for fuzzer detection +this_dir=$(dirname "$0") +export GEM_HOME=$this_dir/fuzz-gem +export GEM_PATH=$this_dir/fuzz-gem:/usr/local/lib/ruby/gems/3.3.0 + +# If corpus directory provided, run fuzzer on each corpus file with coverage +if [ -n "$FUZZ_CORPUS_DIR" ] && [ -d "$FUZZ_CORPUS_DIR" ]; then + export COVERAGE_MODE=true + # COVERAGE_DIR is set by the coverage script + mkdir -p "$COVERAGE_DIR" + + # Iterate over corpus files and run harness with coverage + for corpus_file in "$FUZZ_CORPUS_DIR"/*; do + if [ -f "$corpus_file" ]; then + ruby -I"$this_dir" -I/usr/local/lib -rcoverage_helper "$this_dir/FUZZER_NAME_PLACEHOLDER.rb" "$corpus_file" 2>/dev/null || true + fi + done + + # Copy resultset to DUMPS_DIR root for coverage script to find + if [ -f "$COVERAGE_DIR/.resultset.json" ]; then + cp "$COVERAGE_DIR/.resultset.json" "$(dirname $COVERAGE_DIR)/FUZZER_NAME_PLACEHOLDER.resultset.json" + fi +else + # No corpus directory, just exit + echo "No corpus directory provided" + exit 0 +fi +WRAPPER_EOF + +# Replace placeholder with actual fuzzer name +sed -i "s/FUZZER_NAME_PLACEHOLDER/${fuzzer_name}/g" "$out_dir/$fuzzer_name" + +chmod +x "$out_dir/$fuzzer_name" + +echo "Created coverage wrapper: $out_dir/$fuzzer_name" diff --git a/infra/base-images/base-builder/install_ruby.sh b/infra/base-images/base-builder/install_ruby.sh index d9e443cdf26c..96613cd1510c 100755 --- a/infra/base-images/base-builder/install_ruby.sh +++ b/infra/base-images/base-builder/install_ruby.sh @@ -30,4 +30,7 @@ cd ../ # Clean up the sources. rm -rf ./ruby-$RUBY_VERSION ruby-$RUBY_VERSION.tar.gz +# Install simplecov for coverage builds +gem install simplecov + echo "Finished installing ruby" \ No newline at end of file diff --git a/infra/base-images/base-runner/Dockerfile b/infra/base-images/base-runner/Dockerfile index 5f5f455f11f1..27f45403a8a2 100644 --- a/infra/base-images/base-runner/Dockerfile +++ b/infra/base-images/base-runner/Dockerfile @@ -117,9 +117,15 @@ COPY --from=base-ruby /usr/local/bin/gem /usr/local/bin/gem COPY --from=base-ruby /usr/local/lib/ruby /usr/local/lib/ruby COPY --from=base-ruby /usr/local/include/ruby-3.3.0 /usr/local/include/ruby-3.3.0 +# Install libyaml and simplecov for Ruby coverage reporting +RUN apt-get update && apt-get install -y libyaml-0-2 && \ + gem install simplecov && \ + apt-get clean && rm -rf /var/lib/apt/lists/* # Do this last to make developing these files easier/faster due to caching. COPY bad_build_check \ + consolidate_ruby_coverage.sh \ + consolidate_html.py \ coverage \ coverage_helper \ download_corpus \ @@ -128,6 +134,7 @@ COPY bad_build_check \ rcfilt \ reproduce \ run_fuzzer \ + ruzzy \ parse_options.py \ generate_differential_cov_report.py \ profraw_update.py \ diff --git a/infra/base-images/base-runner/consolidate_html.py b/infra/base-images/base-runner/consolidate_html.py new file mode 100644 index 000000000000..e19fe6fbbe96 --- /dev/null +++ b/infra/base-images/base-runner/consolidate_html.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Consolidates a SimpleCov HTML report into a single standalone HTML file +by inlining all CSS, JavaScript, and images as data URIs. +""" + +import base64 +import os +import re +import sys + + +def get_mime_type(file_path): + """Determine MIME type based on file extension.""" + ext = os.path.splitext(file_path)[1].lower() + mime_types = { + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.gif': 'image/gif', + '.svg': 'image/svg+xml', + '.ico': 'image/x-icon', + } + return mime_types.get(ext, 'application/octet-stream') + + +def inline_css_images(css_content, css_dir): + """Inline images referenced in CSS using url().""" + + def replace_url(match): + img_href = match.group(1).strip('\'"') + img_path = os.path.join(css_dir, img_href) + if os.path.isfile(img_path): + mime_type = get_mime_type(img_path) + with open(img_path, 'rb') as f: + data = base64.b64encode(f.read()).decode('utf-8') + return f'url(data:{mime_type};base64,{data})' + return match.group(0) + + return re.sub(r'url\([\'"]?([^\'")\s]+)[\'"]?\)', replace_url, css_content) + + +def inline_file(html, base_path): + """Inline all external resources (CSS, JS, images) into the HTML.""" + + # Inline CSS files + def replace_css(match): + href = match.group(1) + css_path = os.path.join(base_path, href) + if os.path.isfile(css_path): + with open(css_path, 'r', encoding='utf-8') as f: + css_content = f.read() + # Inline images within CSS + css_dir = os.path.dirname(css_path) + css_content = inline_css_images(css_content, css_dir) + return f'' + return match.group(0) + + html = re.sub(r']+href=[\'"]([^\'"]+\.css)[\'"][^>]*>', + replace_css, + html, + flags=re.IGNORECASE) + + # Inline JavaScript files + def replace_js(match): + src = match.group(1) + js_path = os.path.join(base_path, src) + if os.path.isfile(js_path): + with open(js_path, 'r', encoding='utf-8') as f: + js_content = f.read() + return f"" + return match.group(0) + + html = re.sub(r']+src=[\'"]([^\'"]+\.js)[\'"][^>]*>', + replace_js, + html, + flags=re.IGNORECASE) + + # Inline images + def replace_img(match): + full_tag = match.group(0) + src_match = re.search(r'src=[\'"]([^\'"]+)[\'"]', full_tag) + if not src_match: + return full_tag + + src = src_match.group(1) + if src.startswith('data:'): + return full_tag + + img_path = os.path.join(base_path, src) + if os.path.isfile(img_path): + mime_type = get_mime_type(img_path) + with open(img_path, 'rb') as f: + data = base64.b64encode(f.read()).decode('utf-8') + return re.sub(r'src=[\'"]([^\'"]+)[\'"]', + f"src='data:{mime_type};base64,{data}'", full_tag) + return full_tag + + html = re.sub(r']+src=[\'"][^\'"]+[\'"][^>]*>', + replace_img, + html, + flags=re.IGNORECASE) + + # Inline favicon + def replace_favicon(match): + full_tag = match.group(0) + href_match = re.search(r'href=[\'"]([^\'"]+)[\'"]', full_tag) + if not href_match: + return full_tag + + href = href_match.group(1) + icon_path = os.path.join(base_path, href) + if os.path.isfile(icon_path): + mime_type = get_mime_type(icon_path) + with open(icon_path, 'rb') as f: + data = base64.b64encode(f.read()).decode('utf-8') + return re.sub(r'href=[\'"]([^\'"]+)[\'"]', + f"href='data:{mime_type};base64,{data}'", full_tag) + return full_tag + + html = re.sub(r']+rel=[\'"]icon[\'"][^>]*>', + replace_favicon, + html, + flags=re.IGNORECASE) + + return html + + +def main(): + if len(sys.argv) < 3: + print(f"Usage: {sys.argv[0]} ") + sys.exit(1) + + input_path = sys.argv[1] + output_path = sys.argv[2] + + if not os.path.isfile(input_path): + print(f"Error: Input file not found: {input_path}") + sys.exit(1) + + base_path = os.path.dirname(input_path) + + with open(input_path, 'r', encoding='utf-8') as f: + html = f.read() + + print(f"Consolidating {input_path}...") + html = inline_file(html, base_path) + + with open(output_path, 'w', encoding='utf-8') as f: + f.write(html) + + size_mb = os.path.getsize(output_path) / 1024 / 1024 + print(f"Created standalone HTML: {output_path}") + print(f"Size: {size_mb:.1f}MB") + + +if __name__ == '__main__': + main() diff --git a/infra/base-images/base-runner/consolidate_ruby_coverage.sh b/infra/base-images/base-runner/consolidate_ruby_coverage.sh new file mode 100755 index 000000000000..1cad687057ba --- /dev/null +++ b/infra/base-images/base-runner/consolidate_ruby_coverage.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +################################################################################ +# +# Consolidates SimpleCov HTML reports into standalone HTML files. +# +# This script creates standalone HTML files with all assets inlined (CSS, JS, images) +# for easy distribution and viewing without a web server. +# +# Usage: consolidate_ruby_coverage.sh +# +# Environment variables required: +# REPORT_PLATFORM_DIR - Directory containing Ruby coverage HTML report +# +################################################################################ + +set -e + +echo "Creating standalone HTML report..." + +# Create standalone HTML file by inlining all assets +if [ -f "$REPORT_PLATFORM_DIR/index.html" ]; then + python3 /usr/local/bin/consolidate_html.py \ + "$REPORT_PLATFORM_DIR/index.html" \ + "$REPORT_PLATFORM_DIR/standalone.html" + + echo "Standalone report created at $REPORT_PLATFORM_DIR/standalone.html" +else + echo "Warning: No index.html found at $REPORT_PLATFORM_DIR" +fi + +set +e diff --git a/infra/base-images/base-runner/coverage b/infra/base-images/base-runner/coverage index c3300fb8a6da..b70be2df0493 100755 --- a/infra/base-images/base-runner/coverage +++ b/infra/base-images/base-runner/coverage @@ -29,6 +29,7 @@ else fi COVERAGE_OUTPUT_DIR=${COVERAGE_OUTPUT_DIR:-$OUT} +COVERAGE_EXTRA_ARGS=${COVERAGE_EXTRA_ARGS:-} DUMPS_DIR="$COVERAGE_OUTPUT_DIR/dumps" FUZZERS_COVERAGE_DUMPS_DIR="$DUMPS_DIR/fuzzers_coverage" @@ -389,6 +390,34 @@ function run_javascript_fuzz_target { nyc_report_converter.py $nyc_json_summary_file $summary_file } +function run_ruby_fuzz_target { + local target=$1 + local corpus_real="$CORPUS_DIR/${target}" + + # Write dummy stats file for now + echo "{}" > "$FUZZER_STATS_DIR/$target.json" + + # Set environment for coverage collection + export FUZZ_CORPUS_DIR="$corpus_real" + export COVERAGE_DIR="$DUMPS_DIR/coverage_${target}" + export FUZZER_NAME="$target" + + # Run the wrapper which will iterate over corpus and collect coverage + timeout $TIMEOUT $OUT/$target > $LOGS_DIR/$target.log 2>&1 + if (( $? != 0 )); then + echo "Error occurred while running Ruby target $target:" + cat $LOGS_DIR/$target.log + return 0 + fi + + # SimpleCov creates coverage data in .resultset.json + if [ -f "$COVERAGE_DIR/.resultset.json" ]; then + cp "$COVERAGE_DIR/.resultset.json" "$DUMPS_DIR/${target}.resultset.json" + else + echo "Warning: No coverage data generated for $target" + fi +} + function generate_html { local profdata=$1 local shared_libraries=$2 @@ -414,6 +443,8 @@ export SYSGOPATH=$GOPATH export GOPATH=$OUT/$GOPATH # Run each fuzz target, generate raw coverage dumps. for fuzz_target in $FUZZ_TARGETS; do + # TODO: Remove after this has proven to run in cloud. + echo "DEBUG: Processing fuzz_target=$fuzz_target" # Test if fuzz target is a golang one. if [[ $FUZZING_LANGUAGE == "go" ]]; then # Continue if not a fuzz target. @@ -456,6 +487,18 @@ for fuzz_target in $FUZZ_TARGETS; do # Run the coverage collection. run_javascript_fuzz_target $fuzz_target & + elif [[ $FUZZING_LANGUAGE == "ruby" ]]; then + # Continue if not a fuzz target (check for Ruzzy.fuzz pattern) + if [[ $FUZZING_ENGINE != "none" ]]; then + grep -E "(OSSFuzz\.fuzz|Ruzzy\.fuzz|LLVMFuzzerTestOneInput)" $fuzz_target > /dev/null 2>&1 || continue + fi + + echo "Running Ruby target $fuzz_target" + # Log the target in the targets file. + echo ${fuzz_target} >> $COVERAGE_TARGET_FILE + + # Run the coverage collection. + run_ruby_fuzz_target $fuzz_target & else # Continue if not a fuzz target. if [[ $FUZZING_ENGINE != "none" ]]; then @@ -612,6 +655,84 @@ elif [[ $FUZZING_LANGUAGE == "javascript" ]]; then # Write llvm-cov summary file. nyc_report_converter.py $nyc_json_summary_file $SUMMARY_FILE + set +e +elif [[ $FUZZING_LANGUAGE == "ruby" ]]; then + echo "Generating Ruby coverage report..." + + # From this point on the script does not tolerate any errors. + set -e + + # Create a Ruby script to merge and generate coverage reports + cat > /tmp/merge_coverage.rb << 'RUBY_SCRIPT' +require 'simplecov' +require 'json' + +# Get parameters from environment +dumps_dir = ENV['DUMPS_DIR'] +report_dir = ENV['REPORT_PLATFORM_DIR'] +summary_file = ENV['SUMMARY_FILE'] + +# Find all resultset files +resultset_files = Dir.glob("#{dumps_dir}/*.resultset.json") + +puts "Found #{resultset_files.length} resultset files" + +if resultset_files.empty? + puts "No coverage data found" + exit 0 +end + +# Set up SimpleCov root +SimpleCov.root "/out" + +# Configure SimpleCov +SimpleCov.configure do + coverage_dir report_dir + formatter SimpleCov::Formatter::HTMLFormatter + enable_coverage :branch + track_files '{/out,/usr/local/lib}/**/*.rb' +end + +# Collate generates the report and returns nil +# We need to access the result through SimpleCov.result after collate +SimpleCov.collate(resultset_files) + +# Get the result +result = SimpleCov.result + +puts "Coverage result type: #{result.class}" +puts "Files covered: #{result.files.length}" + +# Generate summary +summary = { + "totals" => { + "lines" => { + "count" => result.total_lines.to_i, + "covered" => result.covered_lines.to_i, + "percent" => result.covered_percent.round(2) + } + } +} + +# Write summary +File.write(summary_file, JSON.pretty_generate(summary)) + +puts "Coverage report generated at #{report_dir}" +puts "Lines: #{result.covered_lines}/#{result.total_lines} (#{result.covered_percent.round(2)}%)" +RUBY_SCRIPT + + # Run the merge script + DUMPS_DIR="$DUMPS_DIR" \ + REPORT_PLATFORM_DIR="$REPORT_PLATFORM_DIR" \ + SUMMARY_FILE="$SUMMARY_FILE" \ + ruby /tmp/merge_coverage.rb + + echo "Finished generating code coverage report for Ruby fuzz targets." + + # Consolidate Ruby coverage reports and merge with C coverage + export REPORT_PLATFORM_DIR + bash /usr/local/bin/consolidate_ruby_coverage.sh + set +e else @@ -657,9 +778,12 @@ find $REPORT_ROOT_DIR $REPORT_BY_TARGET_ROOT_DIR -type d -exec chmod +x {} + # HTTP_PORT is optional. set +u +HTTP_PORT=${HTTP_PORT:-8008} if [[ -n $HTTP_PORT ]]; then # Serve the report locally. echo "Serving the report on http://127.0.0.1:$HTTP_PORT/linux/index.html" cd $REPORT_ROOT_DIR python3 -m http.server $HTTP_PORT +else + echo "Coverage report generated at $REPORT_ROOT_DIR/linux/index.html" fi diff --git a/infra/base-images/base-runner/ubuntu-20-04.Dockerfile b/infra/base-images/base-runner/ubuntu-20-04.Dockerfile index c421cb21a003..a70198f31bca 100644 --- a/infra/base-images/base-runner/ubuntu-20-04.Dockerfile +++ b/infra/base-images/base-runner/ubuntu-20-04.Dockerfile @@ -117,6 +117,9 @@ COPY --from=base-ruby /usr/local/bin/gem /usr/local/bin/gem COPY --from=base-ruby /usr/local/lib/ruby /usr/local/lib/ruby COPY --from=base-ruby /usr/local/include/ruby-3.3.0 /usr/local/include/ruby-3.3.0 +RUN apt-get update && apt-get install -y libyaml-0-2 && \ + gem install simplecov + # Do this last to make developing these files easier/faster due to caching. COPY bad_build_check \ coverage \ @@ -134,4 +137,6 @@ COPY bad_build_check \ test_all.py \ test_one.py \ python_coverage_runner_help.py \ - /usr/local/bin/ \ No newline at end of file + consolidate_ruby_coverage.sh \ + consolidate_html.py \ + /usr/local/bin/ diff --git a/infra/base-images/base-runner/ubuntu-24-04.Dockerfile b/infra/base-images/base-runner/ubuntu-24-04.Dockerfile index 2e252a25ffd8..95c1d199ad97 100644 --- a/infra/base-images/base-runner/ubuntu-24-04.Dockerfile +++ b/infra/base-images/base-runner/ubuntu-24-04.Dockerfile @@ -117,6 +117,9 @@ COPY --from=base-ruby /usr/local/bin/gem /usr/local/bin/gem COPY --from=base-ruby /usr/local/lib/ruby /usr/local/lib/ruby COPY --from=base-ruby /usr/local/include/ruby-3.3.0 /usr/local/include/ruby-3.3.0 +RUN apt-get install -y libyaml-0-2 && \ + gem install simplecov + # Do this last to make developing these files easier/faster due to caching. COPY bad_build_check \ coverage \ @@ -130,9 +133,10 @@ COPY bad_build_check \ parse_options.py \ generate_differential_cov_report.py \ profraw_update.py \ - targets_list \ test_all.py \ test_one.py \ python_coverage_runner_help.py \ - /usr/local/bin/ \ No newline at end of file + consolidate_ruby_coverage.sh \ + consolidate_html.py \ + /usr/local/bin/ diff --git a/projects/ox-ruby/build.sh b/projects/ox-ruby/build.sh index 97bcc655e35a..5661e5b44766 100644 --- a/projects/ox-ruby/build.sh +++ b/projects/ox-ruby/build.sh @@ -15,43 +15,26 @@ # ################################################################################ -export GEM_HOME=$OUT/fuzz_parse-gem - -# setup -BUILD=$WORK/Build - +# Build the ox gem cd $SRC/ox-ruby gem build -RUZZY_DEBUG=1 gem install --development --verbose *.gem - -# Sync gems folder with ruzzy -rsync -avu /install/ruzzy/* $OUT/fuzz_parse-gem - -#for fuzz_target_path in $SRC/harnesses/fuzz_*.rb; do -# ruzzy-build "$fuzz_target_path" -#done - -cp $SRC/harnesses/fuzz_parse.rb $OUT/ -export GEM_PATH=$OUT/fuzz_parse-gem - -echo """#!/usr/bin/env bash -# LLVMFuzzerTestOneInput for fuzzer detection. -this_dir=\$(dirname \"\$0\") - -echo "GEM_HOME FIRST: \$GEM_HOME" - -export GEM_HOME=\$this_dir/fuzz_parse-gem -export GEM_PATH=\$this_dir/fuzz_parse-gem -echo "GEM_PATH: \$GEM_PATH" -echo "GEM_HOME: \$GEM_HOME" -echo "Showing gem home:" -ls -la \$GEM_HOME - -echo "Showing this dir:" -ls -la \$this_dir - -ASAN_OPTIONS="allocator_may_return_null=1:detect_leaks=0:use_sigaltstack=0" LD_PRELOAD=\$(ruby -e 'require \"ruzzy\"; print Ruzzy::ASAN_PATH') ruby \$this_dir/fuzz_parse.rb \$@""" > $OUT/fuzz_parse - -chmod +x $OUT/fuzz_parse - -#mv $OUT/fuzz-gem $OUT/fuzz_parse-gem +gem install --install-dir $OUT/fuzz-gem --verbose *.gem + +# Set up gem environment +export GEM_HOME=$OUT/fuzz-gem +export GEM_PATH=$OUT/fuzz-gem:/usr/local/lib/ruby/gems/3.3.0 + +# Copy Ruzzy and dependencies (for normal fuzzing, not needed for coverage) +if [[ "$SANITIZER" != "coverage" ]]; then + rsync -avu /install/ruzzy/bin /install/ruzzy/build_info /install/ruzzy/cache /install/ruzzy/doc /install/ruzzy/extensions /install/ruzzy/gems /install/ruzzy/plugins /install/ruzzy/specifications $OUT/fuzz-gem/ +fi + +# Create fuzzer executables from harness files +for fuzz_target_path in $SRC/harnesses/fuzz_*.rb; do + if [ ! -f "$fuzz_target_path" ]; then + continue + fi + + # Use unified builder that handles both fuzzing and coverage modes + /usr/bin/build_ruby_fuzzer "$fuzz_target_path" "$OUT" "ox-ruby" +done diff --git a/projects/ox-ruby/fuzz_parse.rb b/projects/ox-ruby/fuzz_parse.rb index fee40df63cd5..41a18b060ed1 100644 --- a/projects/ox-ruby/fuzz_parse.rb +++ b/projects/ox-ruby/fuzz_parse.rb @@ -14,7 +14,8 @@ # limitations under the License. # ################################################################################ -require 'ruzzy' + +require 'ossfuzz_helper' require 'ox' test_one_input = lambda do |data| @@ -29,4 +30,4 @@ return 0 end -Ruzzy.fuzz(test_one_input) +OSSFuzz.fuzz(test_one_input) diff --git a/projects/ox-ruby/fuzz_sax_parse.rb b/projects/ox-ruby/fuzz_sax_parse.rb index dea206f6d855..575c587949e1 100644 --- a/projects/ox-ruby/fuzz_sax_parse.rb +++ b/projects/ox-ruby/fuzz_sax_parse.rb @@ -14,8 +14,10 @@ # limitations under the License. # ################################################################################ -require 'ruzzy' + +require 'ossfuzz_helper' require 'ox' +require 'stringio' class MyHandler < Ox::Sax # Called for the opening of an element @@ -44,4 +46,4 @@ def end_element(name) return 0 end -Ruzzy.fuzz(test_one_input) +OSSFuzz.fuzz(test_one_input)