From 6e292fd88002b48d17ad2129b21b37b0e3e9f8a3 Mon Sep 17 00:00:00 2001 From: Ryan Lopopolo Date: Mon, 24 Mar 2025 22:39:59 -0700 Subject: [PATCH 1/3] Improve fuzzing harness to avoid memory exhaustion This patch improves the fuzzing harness by explicitly limiting memory usage and maximum input length to prevent out-of-memory (OOM) errors during fuzz testing. It sets an RSS limit of 2GB and restricts maximum input string length to 2048 bytes, ensuring that buffer allocations remain within safe limits defined by the formatter's internal size limiter. Changes include: - Setting `-rss_limit_mb=2048` and `-max_len=2048` for the fuzz targets in the GitHub Actions workflow. - Adding explanatory comments to clarify these limits. - Referencing MRI Ruby's `strftime.c` for the size limit calculation rationale. - Enhancing fuzz target implementations to include calls to formatter methods to test input handling. - Adding explicit test cases for invalid format strings containing embedded null bytes and incomplete format specifiers. These improvements ensure that fuzzing effectively explores edge cases without triggering resource exhaustion, aligning the testing approach more closely with MRI Ruby's handling of large or invalid inputs. Ref: https://github.com/ruby/ruby/blob/v3_4_2/strftime.c#L921-L928 See also https://github.com/artichoke/artichoke/issues/2840 which was opened as part of this investigation. --- .github/workflows/fuzz.yaml | 10 ++++++++-- fuzz/fuzz_targets/bytes.rs | 2 ++ fuzz/fuzz_targets/string.rs | 2 ++ src/format/mod.rs | 1 + src/tests/format.rs | 5 +++++ 5 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/workflows/fuzz.yaml b/.github/workflows/fuzz.yaml index 27089983..8df8a5a7 100644 --- a/.github/workflows/fuzz.yaml +++ b/.github/workflows/fuzz.yaml @@ -24,7 +24,10 @@ jobs: run: cargo install cargo-fuzz - name: Fuzz - run: cargo fuzz run bytes -- -max_total_time=1800 # 30 minutes + # run for 30 minutes with a 2GB memory limit and a maximum input length of 2048 bytes + # the maximum input length is set so that the formatter's size limiter will prevent + # allocating more memory than the memory limit + run: cargo fuzz run bytes -- -max_total_time=1800 -rss_limit_mb=2048 -max_len=2048 string: name: Fuzz string @@ -46,4 +49,7 @@ jobs: run: cargo install cargo-fuzz - name: Fuzz - run: cargo fuzz run string -- -max_total_time=1800 # 30 minutes + # run for 30 minutes with a 2GB memory limit and a maximum input length of 2048 bytes + # the maximum input length is set so that the formatter's size limiter will prevent + # allocating more memory than the memory limit + run: cargo fuzz run string -- -max_total_time=1800 -rss_limit_mb=2048 -max_len=2048 diff --git a/fuzz/fuzz_targets/bytes.rs b/fuzz/fuzz_targets/bytes.rs index 13498ad2..89ec018f 100644 --- a/fuzz/fuzz_targets/bytes.rs +++ b/fuzz/fuzz_targets/bytes.rs @@ -7,6 +7,8 @@ use mock::MockTime; fuzz_target!(|data: (MockTime, &[u8])| { let (time, format) = data; + let _ignored = strftime::bytes::strftime(&time, format, &mut buf[..]); + // Give each fuzzer input a 16kb buffer to write to. let mut buf = vec![0u8; 16 * 1024].into_boxed_slice(); let _ignored = strftime::buffered::strftime(&time, format, &mut buf[..]); diff --git a/fuzz/fuzz_targets/string.rs b/fuzz/fuzz_targets/string.rs index e1c09489..44cafe9e 100644 --- a/fuzz/fuzz_targets/string.rs +++ b/fuzz/fuzz_targets/string.rs @@ -30,6 +30,8 @@ impl<'a> fmt::Write for LimitedBuf<'a> { fuzz_target!(|data: (MockTime, &str)| { let (time, format) = data; + let _ignored = strftime::string::strftime(&time, format, &mut buf[..]); + // Give each fuzzer input a 16kb buffer to write to. let mut buf = vec![0u8; 16 * 1024].into_boxed_slice(); diff --git a/src/format/mod.rs b/src/format/mod.rs index 17768d8c..20bc7ba3 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -787,6 +787,7 @@ impl<'t, 'f, T: CheckedTime> TimeFormatter<'t, 'f, T> { // Use a size limiter to limit the maximum size of the resulting // formatted string + // Ref: let size_limit = self.format.len().saturating_mul(512 * 1024); let mut f = SizeLimiter::new(buf, size_limit); diff --git a/src/tests/format.rs b/src/tests/format.rs index 7a7a693a..bdef1076 100644 --- a/src/tests/format.rs +++ b/src/tests/format.rs @@ -795,6 +795,11 @@ fn test_format_invalid() { let err = get_format_err(&time, format); assert!(matches!(err, Error::InvalidFormatString)); } + + for format in ["\0%", "\0%-4", "\0%-", "\0%-_"] { + let err = get_format_err(&time, format); + assert!(matches!(err, Error::InvalidFormatString)); + } } #[test] From 76cf6ebe766b257b31715fe899227de08b3a02b6 Mon Sep 17 00:00:00 2001 From: Ryan Lopopolo Date: Tue, 25 Mar 2025 07:31:48 -0700 Subject: [PATCH 2/3] Update fuzz/fuzz_targets/bytes.rs Co-authored-by: x-hgg-x <39058530+x-hgg-x@users.noreply.github.com> --- fuzz/fuzz_targets/bytes.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fuzz/fuzz_targets/bytes.rs b/fuzz/fuzz_targets/bytes.rs index 89ec018f..1b31a0de 100644 --- a/fuzz/fuzz_targets/bytes.rs +++ b/fuzz/fuzz_targets/bytes.rs @@ -7,7 +7,7 @@ use mock::MockTime; fuzz_target!(|data: (MockTime, &[u8])| { let (time, format) = data; - let _ignored = strftime::bytes::strftime(&time, format, &mut buf[..]); + let _ignored = strftime::bytes::strftime(&time, format); // Give each fuzzer input a 16kb buffer to write to. let mut buf = vec![0u8; 16 * 1024].into_boxed_slice(); From 89f54b7428b51d817042f340da57f600e093b68b Mon Sep 17 00:00:00 2001 From: Ryan Lopopolo Date: Tue, 25 Mar 2025 07:31:54 -0700 Subject: [PATCH 3/3] Update fuzz/fuzz_targets/string.rs Co-authored-by: x-hgg-x <39058530+x-hgg-x@users.noreply.github.com> --- fuzz/fuzz_targets/string.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fuzz/fuzz_targets/string.rs b/fuzz/fuzz_targets/string.rs index 44cafe9e..1196b129 100644 --- a/fuzz/fuzz_targets/string.rs +++ b/fuzz/fuzz_targets/string.rs @@ -30,7 +30,7 @@ impl<'a> fmt::Write for LimitedBuf<'a> { fuzz_target!(|data: (MockTime, &str)| { let (time, format) = data; - let _ignored = strftime::string::strftime(&time, format, &mut buf[..]); + let _ignored = strftime::string::strftime(&time, format); // Give each fuzzer input a 16kb buffer to write to. let mut buf = vec![0u8; 16 * 1024].into_boxed_slice();