Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
cda204a
Add fuzzing to CI
Dreamsorcerer Jun 9, 2026
791fb23
Track upstream
Dreamsorcerer Jun 9, 2026
f2bf4db
Create bug
Dreamsorcerer Jun 9, 2026
d841c79
Apply suggestion from @Dreamsorcerer
Dreamsorcerer Jun 9, 2026
dee8c7b
Create http_parser.py
Dreamsorcerer Jun 15, 2026
ac99f7f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 15, 2026
f1c49ab
Create http_payload_parser.py
Dreamsorcerer Jun 15, 2026
be6b0fe
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 15, 2026
09e782a
Create multipart.py
Dreamsorcerer Jun 15, 2026
4e4515f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 15, 2026
74b62f4
Create payload_url.py
Dreamsorcerer Jun 15, 2026
6d46483
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 15, 2026
9af2d98
Create web_request.py
Dreamsorcerer Jun 15, 2026
e3b8698
Update .mypy.ini
Dreamsorcerer Jun 15, 2026
6df241f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 15, 2026
4263424
Update http_payload_parser.py
Dreamsorcerer Jun 15, 2026
af37f57
Update http_parser.py
Dreamsorcerer Jun 15, 2026
88c72f5
Apply suggestions from code review
Dreamsorcerer Jun 15, 2026
791e31c
Apply suggestions from code review
Dreamsorcerer Jun 15, 2026
8e2070c
Update lint.in
Dreamsorcerer Jun 15, 2026
3bd9887
Update lint.txt
Dreamsorcerer Jun 15, 2026
a97ecd9
Apply suggestions from code review
Dreamsorcerer Jun 15, 2026
cfe8f4d
Update multipart.py
Dreamsorcerer Jun 16, 2026
3fe6800
Update http_payload_parser.py
Dreamsorcerer Jun 16, 2026
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
75 changes: 75 additions & 0 deletions .github/workflows/cifuzz.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: CIFuzz

on:
push:
branches:
- 'master'
- '[0-9].[0-9]+' # matches to backport branches, e.g. 3.6
pull_request:
branches:
- 'master'
- '[0-9].[0-9]+'

permissions: {}

jobs:
Fuzzing:
name: Fuzzing (${{ matrix.sanitizer }})
runs-on: ubuntu-latest
permissions:
contents: read # For build_fuzzers to check out the code
issues: write # To create a new issue in the last step
strategy:
fail-fast: false
matrix:
sanitizer: [address, undefined]
steps:
- name: Build Fuzzers
id: build
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master
with:
oss-fuzz-project-name: 'aiohttp'
language: python
sanitizer: ${{ matrix.sanitizer }}
- name: Run Fuzzers
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
with:
oss-fuzz-project-name: 'aiohttp'
language: python
fuzz-seconds: 600
sanitizer: ${{ matrix.sanitizer }}
- name: Upload Crash
uses: actions/upload-artifact@v4
if: failure() && steps.build.outcome == 'success'
with:
name: ${{ matrix.sanitizer }}-artifacts
path: ./out/artifacts
- name: Open issue on post-merge crash
if: >-
failure()
&& steps.build.outcome == 'success'
&& github.event_name == 'push'
uses: actions/github-script@v9
env:
SANITIZER: ${{ matrix.sanitizer }}
with:
script: |
const branch = context.ref.replace('refs/heads/', '');
const shortSha = context.sha.substring(0, 7);
const sanitizer = process.env.SANITIZER;
const runUrl =
`${context.serverUrl}/${context.repo.owner}` +
`/${context.repo.repo}/actions/runs/${context.runId}`;
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `CIFuzz: ${sanitizer} crash on ${branch} @ ${shortSha}`,
body: [
`CIFuzz found a crash on \`${branch}\` at ${context.sha}.`,
``,
`- Sanitizer: \`${sanitizer}\``,
`- Run: ${runUrl}`,
`- Download \`${sanitizer}-artifacts\` from the run page for the reproducer.`,
].join('\n'),
type: 'Bug',
});
2 changes: 1 addition & 1 deletion .mypy.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[mypy]
files = aiohttp, docs/code, examples, tests
files = aiohttp, docs/code, examples, fuzzers, tests
check_untyped_defs = True
follow_imports_for_stubs = True
disallow_any_decorated = True
Expand Down
45 changes: 45 additions & 0 deletions fuzzers/http_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/python3

# Copyright 2022-2025 Google LLC, 2026 aio-libs contributors
#
# 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.

import asyncio
import sys
from contextlib import suppress
from unittest import mock

import atheris # noqa: I900

with atheris.instrument_imports():
from aiohttp.base_protocol import BaseProtocol
from aiohttp.http_exceptions import BadHttpMessage
from aiohttp.http_parser import HttpRequestParserC, HttpRequestParserPy

AIOHTTP_VAL = 0

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unclear how this variable is supposed to change.

HttpRequestParser = HttpRequestParserC if AIOHTTP_VAL == 0 else HttpRequestParserPy
LOOP = mock.create_autospec(asyncio.AbstractEventLoop, spec_set=True, instance=True)
PROTOCOL = BaseProtocol(LOOP)


@atheris.instrument_func
def TestOneInput(data: bytes) -> None:
parser = HttpRequestParser(PROTOCOL, LOOP, 32768)
with suppress(BadHttpMessage):
parser.feed_data(data)
parser.feed_eof()


if __name__ == "__main__":
atheris.Setup(sys.argv, TestOneInput, enable_python_coverage=True)
atheris.Fuzz()
44 changes: 44 additions & 0 deletions fuzzers/http_payload_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/usr/bin/python3

# Copyright 2022-2025 Google LLC, 2026 aio-libs contributors
#
# 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.

import asyncio
import sys
Comment thread
Dreamsorcerer marked this conversation as resolved.
from contextlib import suppress
from unittest import mock

import atheris # noqa: I900

with atheris.instrument_imports():
from aiohttp import StreamReader
from aiohttp.base_protocol import BaseProtocol
Comment thread
Dreamsorcerer marked this conversation as resolved.
from aiohttp.http_exceptions import BadHttpMessage
from aiohttp.http_parser import HeadersParser, HttpPayloadParser

LOOP = mock.create_autospec(asyncio.AbstractEventLoop, spec_set=True, instance=True)
PROTOCOL = BaseProtocol(LOOP)


@atheris.instrument_func
def TestOneInput(data: bytes) -> None:
out = StreamReader(PROTOCOL, 2**16, loop=LOOP)
parser = HttpPayloadParser(out, headers_parser=HeadersParser())
with suppress(BadHttpMessage):
parser.feed_data(data)


if __name__ == "__main__":
atheris.Setup(sys.argv, TestOneInput, enable_python_coverage=True)
atheris.Fuzz()
67 changes: 67 additions & 0 deletions fuzzers/multipart.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/python3

# Copyright 2022-2025 Google LLC, 2026 aio-libs contributors
#
# 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.

import asyncio
import io
import sys
Comment thread
Dreamsorcerer marked this conversation as resolved.
from contextlib import suppress

import atheris # noqa: I900

with atheris.instrument_imports():
from aiohttp import BodyPartReader
from aiohttp.hdrs import CONTENT_TYPE
from aiohttp.helpers import HeadersDictProxy


class FuzzStream(StreamReader):
def __init__(self, content: bytes):
self.content = io.BytesIO(content)

async def read(self, size: int | None = None) -> bytes:
return self.content.read(size)

def at_eof(self) -> bool:
return self.content.tell() == len(self.content.getbuffer())

async def readline(self) -> bytes:
return self.content.readline()

def unread_data(self, data: bytes) -> None:
self.content = io.BytesIO(data + self.content.read())


@atheris.instrument_func
async def fuzz_bodypart_reader(data: bytes) -> None:
fdp = atheris.FuzzedDataProvider(data)
obj = BodyPartReader(
b"--:",
HeadersDictProxy({CONTENT_TYPE: fdp.ConsumeUnicode(30)}),
FuzzStream(fdp.ConsumeBytes(atheris.ALL_REMAINING)),
)
if not obj.at_eof():
await obj.form()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we get the coverage from the fuzzing run? I'm not sure this is covering enough of the multipart code.



@atheris.instrument_func
def TestOneInput(data: bytes) -> None:
with suppress(ValueError):
asyncio.run(fuzz_bodypart_reader(data))


if __name__ == "__main__":
atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz()
41 changes: 41 additions & 0 deletions fuzzers/payload_url.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/python3

# Copyright 2022-2025 Google LLC, 2026 aio-libs contributors
#
# 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.

import sys
from contextlib import suppress

import atheris # noqa: I900

with atheris.instrument_imports():
from yarl import URL

from aiohttp.payload import StringPayload


@atheris.instrument_func
def TestOneInput(data: bytes) -> None:
fdp = atheris.FuzzedDataProvider(data)
original = fdp.ConsumeString(sys.maxsize)

with suppress(UnicodeEncodeError):
StringPayload(original)
with suppress(ValueError):
URL(original)


if __name__ == "__main__":
atheris.Setup(sys.argv, TestOneInput, enable_python_coverage=True)
atheris.Fuzz()
54 changes: 54 additions & 0 deletions fuzzers/web_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/python3

# Copyright 2022-2025 Google LLC, 2026 aio-libs contributors
#
# 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.

import asyncio
import sys

import atheris # noqa: I900

with atheris.instrument_imports():
from multidict import CIMultiDict
from yarl import URL

from aiohttp.test_utils import make_mocked_request


@atheris.instrument_func
async def fuzz_run_one_async(data: bytes) -> None:
fdp = atheris.FuzzedDataProvider(data)
url_s = fdp.ConsumeString(fdp.ConsumeIntInRange(0, 512))
try:
URL(url_s)
except ValueError:
return

headers = CIMultiDict(
{fdp.ConsumeString(20): fdp.ConsumeString(fdp.ConsumeIntInRange(0, 512))}
)
req = make_mocked_request("GET", url_s, headers=headers)

req.forwarded
await req.post()


@atheris.instrument_func
def TestOneInput(data: bytes) -> None:
asyncio.run(fuzz_run_one_async(data))


if __name__ == "__main__":
atheris.Setup(sys.argv, TestOneInput, enable_python_coverage=True)
atheris.Fuzz()
1 change: 1 addition & 0 deletions requirements/lint.in
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
aiodns
atheris
backports.zstd; implementation_name == "cpython" and python_version < "3.14"
blockbuster
freezegun
Expand Down
2 changes: 2 additions & 0 deletions requirements/lint.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ async-timeout==5.0.1
# via
# aiohttp
# valkey
atheris==3.0.0
# via -r requirements/lint.in
attrs==26.1.0
# via aiohttp
backports-asyncio-runner==1.2.0
Expand Down
Loading