Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
27 changes: 27 additions & 0 deletions projects/puma/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Copyright 2026 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.
#
################################################################################

FROM gcr.io/oss-fuzz-base/base-builder-ruby

RUN git clone --depth 1 --single-branch https://github.com/puma/puma.git $SRC/puma

ENV CFLAGS="$CFLAGS -fsanitize=fuzzer-no-link -fno-omit-frame-pointer -fno-common -fPIC -g"
ENV CXXFLAGS="$CXXFLAGS -fsanitize=fuzzer-no-link -fno-omit-frame-pointer -fno-common -fPIC -g"

COPY build.sh $SRC/build.sh
COPY fuzz_http_parser.rb fuzz_http_parser.dict $SRC/harnesses/

WORKDIR $SRC
90 changes: 90 additions & 0 deletions projects/puma/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/bin/bash -eu
# Copyright 2026 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.

cd $SRC/puma

# GEM_HOME must be set before any gem install so gems land in $OUT/fuzz-gems,
# which is the only directory available when OSS-Fuzz copies $OUT to run fuzzers.
export GEM_HOME=$OUT/fuzz-gems

# PUMA_DISABLE_SSL=1 skips mini_ssl.c (OpenSSL TLS wrapper) — we target only the
# HTTP/1.1 request parser (http11_parser.c, Ragel-generated state machine).
# The Ragel-generated http11_parser.c is already committed; no Ragel needed.
#
# Remove the nio4r runtime dependency from the gemspec: we only use
# Puma::HttpParser (the C extension), which has no dependency on nio4r's
# event loop. Without this, RubyGems raises MissingSpecError on gem activation.
sed -i '/nio4r/d' puma.gemspec

PUMA_DISABLE_SSL=1 gem build puma.gemspec
RUZZY_DEBUG=1 gem install --verbose puma-*.gem
rsync -avu /install/ruzzy/* $OUT/fuzz-gems

# ASAN_OPTIONS required for Ruby C extension targets.
ASAN_OPTS="allocator_may_return_null=1:detect_leaks=0:use_sigaltstack=0:detect_stack_use_after_return=0:detect_stack_use_after_scope=0"

for target in fuzz_http_parser; do
cp $SRC/harnesses/${target}.rb $OUT/
cat > $OUT/${target} << WRAPPER
#!/usr/bin/env bash
# LLVMFuzzerTestOneInput for fuzzer detection.
this_dir=\$(dirname "\$0")
export GEM_HOME=\$this_dir/fuzz-gems
export GEM_PATH=\$this_dir/fuzz-gems
ASAN_OPTIONS="${ASAN_OPTS}" \
LD_PRELOAD=\$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby \$this_dir/${target}.rb "\$@"
WRAPPER
chmod +x $OUT/${target}
done

# Dictionary: HTTP tokens and CVE-relevant sequences.
cp $SRC/harnesses/fuzz_http_parser.dict $OUT/

# Seed corpus: representative HTTP/1.1 requests covering common paths and
# historical CVE patterns (request smuggling, chunked encoding, LF injection).
mkdir -p /tmp/corpus

# Valid GET request
printf 'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n' \
> /tmp/corpus/get_basic.txt

# POST with Content-Length
printf 'POST /submit HTTP/1.1\r\nHost: localhost\r\nContent-Length: 5\r\n\r\nhello' \
> /tmp/corpus/post_content_length.txt

# Multiple headers
printf 'GET /path?query=1 HTTP/1.1\r\nHost: example.com\r\nConnection: keep-alive\r\nAccept: */*\r\nUser-Agent: fuzzer\r\n\r\n' \
> /tmp/corpus/get_headers.txt

# Transfer-Encoding: chunked (CVE-2023-40175 class)
printf 'POST / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n0\r\n\r\n' \
> /tmp/corpus/post_chunked.txt

# OPTIONS / HEAD
printf 'OPTIONS * HTTP/1.1\r\nHost: localhost\r\n\r\n' \
> /tmp/corpus/options.txt
printf 'HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n' \
> /tmp/corpus/head.txt

# Partial request (incomplete — exercises incremental parse path)
printf 'GET / HTTP/1.1\r\nHost: loc' \
> /tmp/corpus/partial.txt

# Zero Content-Length (CVE-2023-40175 class)
printf 'POST / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\nTransfer-Encoding: chunked\r\n\r\n' \
> /tmp/corpus/zero_content_length.txt

zip -j $OUT/fuzz_http_parser_seed_corpus.zip /tmp/corpus/*.txt
39 changes: 39 additions & 0 deletions projects/puma/fuzz_http_parser.dict
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# HTTP methods
"GET"
"POST"
"PUT"
"DELETE"
"HEAD"
"OPTIONS"
"PATCH"
"CONNECT"
"TRACE"
# HTTP versions
"HTTP/1.1"
"HTTP/1.0"
# Line endings — critical for request smuggling detection
"\x0d\x0a"
"\x0d\x0a\x0d\x0a"
"\x0a"
# Common headers — high-value for CVE-class bugs
"Host"
"Content-Length"
"Transfer-Encoding"
"chunked"
"Connection"
"keep-alive"
"close"
"Content-Type"
"Accept"
"Authorization"
"Cookie"
"X-Forwarded-For"
# Separators
":"
" "
"/"
"?"
"#"
# Zero-length / edge-case values (CVE-2023-40175 class)
"0"
"0\x0d\x0a"
56 changes: 56 additions & 0 deletions projects/puma/fuzz_http_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# frozen_string_literal: true
# Copyright 2026 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.

# Exercises ext/puma_http11/http11_parser.c (Ragel-generated, 1057 LOC)
# via Puma::HttpParser#execute — the main HTTP/1.1 request header parser.
#
# Historical CVEs in this parser:
# CVE-2022-24790 (CRITICAL) — request smuggling, RFC7230 mismatch
# CVE-2023-40175 (CRITICAL) — request smuggling via chunked + zero Content-Length
# CVE-2021-41136 (LOW) — request smuggling via LF in header values
# CVE-2020-5247 (HIGH) — response splitting via CR/LF in headers
# CVE-2024-45614 (MEDIUM) — header value clobbering

require 'ruzzy'
require 'puma/puma_http11'

PARSE_FNS = [
# Standard parse from offset 0 — main code path
->(str) {
Puma::HttpParser.new.execute({}, str, 0)
},
# Resume parse after partial read — exercises incremental parsing path
->(str) {
parser = Puma::HttpParser.new
nread = parser.execute({}, str, 0)
parser.execute({}, str, nread) unless parser.finished?
},
].freeze

test_one_input = lambda do |data|
return 0 if data.empty?
str = data.to_s
fn = PARSE_FNS[data.length % PARSE_FNS.size]
begin
fn.call(str)
rescue Puma::HttpParserError
rescue SystemStackError
rescue EncodingError, ArgumentError, TypeError
rescue StandardError
end
0
end

Ruzzy.fuzz(test_one_input)
11 changes: 11 additions & 0 deletions projects/puma/project.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
homepage: "https://github.com/puma/puma"
language: ruby
primary_contact: "nate.berkopec@gmail.com"
auto_ccs:
- "tranquac0312@gmail.com"
main_repo: "https://github.com/puma/puma"
sanitizers:
- address
- undefined
fuzzing_engines:
- libfuzzer
Loading