Line | Count | Source |
1 | | /* Copyright 2025 Google LLC |
2 | | Licensed under the Apache License, Version 2.0 (the "License"); |
3 | | you may not use this file except in compliance with the License. |
4 | | You may obtain a copy of the License at |
5 | | http://www.apache.org/licenses/LICENSE-2.0 |
6 | | Unless required by applicable law or agreed to in writing, software |
7 | | distributed under the License is distributed on an "AS IS" BASIS, |
8 | | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
9 | | See the License for the specific language governing permissions and |
10 | | limitations under the License. |
11 | | */ |
12 | | |
13 | | /* |
14 | | * Fuzzer for Ruby's Array#pack and String#unpack (pack.c) |
15 | | * |
16 | | * Purpose: Test binary packing/unpacking with various template directives |
17 | | * to find bugs in template parsing, data conversion, and boundary handling. |
18 | | * |
19 | | * Coverage: |
20 | | * - Template parsing: All pack directives (C, S, L, Q, c, s, l, q, A, a, Z, etc.) |
21 | | * - Array#pack: Convert Ruby objects to binary string |
22 | | * - String#unpack: Parse binary data according to template |
23 | | * - Round-trip: pack → unpack consistency |
24 | | * - Edge cases: Invalid templates, buffer overflows, encoding issues |
25 | | */ |
26 | | |
27 | | #include <stdint.h> |
28 | | #include <stddef.h> |
29 | | #include <stdlib.h> |
30 | | #include <fuzzer/FuzzedDataProvider.h> |
31 | | #include "ruby.h" |
32 | | |
33 | | static int ruby_initialized = 0; |
34 | | |
35 | | extern "C" VALUE ruby_verbose; |
36 | | |
37 | | // Test Array#pack with fuzzer-provided template |
38 | 1.26k | static VALUE call_array_pack(VALUE arg) { |
39 | 1.26k | VALUE *args = (VALUE *)arg; |
40 | 1.26k | VALUE ary = args[0]; |
41 | 1.26k | VALUE template_str = args[1]; |
42 | | |
43 | | // Call Array#pack with the template |
44 | | // This exercises the pack template parser and binary packing logic |
45 | 1.26k | VALUE result = rb_funcall(ary, rb_intern("pack"), 1, template_str); |
46 | | |
47 | | // Try to use the result to ensure it's valid |
48 | 1.26k | if (!NIL_P(result)) { |
49 | 0 | rb_funcall(result, rb_intern("length"), 0); |
50 | 0 | rb_funcall(result, rb_intern("encoding"), 0); |
51 | 0 | } |
52 | | |
53 | 1.26k | return result; |
54 | 1.26k | } |
55 | | |
56 | | // Test String#unpack with fuzzer-provided template and data |
57 | 630 | static VALUE call_string_unpack(VALUE arg) { |
58 | 630 | VALUE *args = (VALUE *)arg; |
59 | 630 | VALUE str = args[0]; |
60 | 630 | VALUE template_str = args[1]; |
61 | | |
62 | | // Call String#unpack with the template |
63 | | // This exercises the unpack template parser and binary unpacking logic |
64 | 630 | VALUE result = rb_funcall(str, rb_intern("unpack"), 1, template_str); |
65 | | |
66 | | // Try to iterate the result to ensure it's valid |
67 | 630 | if (!NIL_P(result) && RB_TYPE_P(result, T_ARRAY)) { |
68 | 0 | long len = RARRAY_LEN(result); |
69 | 0 | for (long i = 0; i < len && i < 10; i++) { |
70 | 0 | (void)rb_ary_entry(result, i); // Suppress unused warning |
71 | 0 | } |
72 | 0 | } |
73 | | |
74 | 630 | return result; |
75 | 630 | } |
76 | | |
77 | 632 | extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { |
78 | | // Initialize Ruby once on first call |
79 | 632 | if (!ruby_initialized) { |
80 | 1 | ruby_init(); |
81 | 1 | ruby_initialized = 1; |
82 | | |
83 | | // Suppress Ruby warnings to avoid log noise |
84 | 1 | ruby_verbose = Qfalse; |
85 | 1 | } |
86 | | |
87 | 632 | if (size < 2) { |
88 | 2 | return 0; |
89 | 2 | } |
90 | | |
91 | | // Use FuzzedDataProvider for structured data consumption |
92 | 630 | FuzzedDataProvider fdp(data, size); |
93 | | |
94 | | // Split input: template and binary data |
95 | 630 | size_t template_len = fdp.ConsumeIntegralInRange<size_t>(1, 10000); |
96 | 630 | std::string template_data = fdp.ConsumeBytesAsString(template_len); |
97 | 630 | size_t binary_data_len = fdp.ConsumeIntegralInRange<size_t>(1, 10000); |
98 | 630 | std::string binary_data = fdp.ConsumeBytesAsString(binary_data_len); |
99 | | |
100 | 630 | VALUE template_str = rb_str_new(template_data.data(), template_data.size()); |
101 | 630 | VALUE binary_str = rb_str_new(binary_data.data(), binary_data.size()); |
102 | | |
103 | 630 | int state = 0; |
104 | | |
105 | | // Test String#unpack with fuzzer data |
106 | 630 | VALUE unpack_args[] = {binary_str, template_str}; |
107 | 630 | rb_protect(call_string_unpack, (VALUE)unpack_args, &state); |
108 | 630 | if (state) { |
109 | 630 | rb_set_errinfo(Qnil); |
110 | 630 | state = 0; |
111 | 630 | } |
112 | | |
113 | | // Test Array#pack with fuzzer template and randomized array data |
114 | 630 | VALUE test_array = rb_ary_new(); |
115 | | |
116 | | // Add various Ruby objects with random data from fuzzer |
117 | 630 | rb_ary_push(test_array, INT2FIX(fdp.ConsumeIntegral<int16_t>())); |
118 | 630 | rb_ary_push(test_array, INT2FIX(fdp.ConsumeIntegral<int16_t>())); |
119 | 630 | rb_ary_push(test_array, LONG2NUM(fdp.ConsumeIntegral<int32_t>())); |
120 | 630 | rb_ary_push(test_array, rb_float_new(fdp.ConsumeFloatingPoint<double>())); |
121 | | |
122 | 630 | size_t str1_len = fdp.ConsumeIntegralInRange<size_t>(0, 10000); |
123 | 630 | std::string str1 = fdp.ConsumeBytesAsString(str1_len); |
124 | 630 | rb_ary_push(test_array, rb_str_new(str1.data(), str1.size())); |
125 | | |
126 | 630 | size_t str2_len = fdp.ConsumeIntegralInRange<size_t>(0, 10000); |
127 | 630 | std::string str2 = fdp.ConsumeBytesAsString(str2_len); |
128 | 630 | rb_ary_push(test_array, rb_str_new(str2.data(), str2.size())); |
129 | | |
130 | 630 | if (fdp.remaining_bytes() >= 8) { |
131 | 165 | rb_ary_push(test_array, UINT2NUM(fdp.ConsumeIntegral<uint32_t>())); |
132 | 165 | rb_ary_push(test_array, UINT2NUM(fdp.ConsumeIntegral<uint32_t>())); |
133 | 165 | } |
134 | | |
135 | 630 | VALUE pack_args[] = {test_array, template_str}; |
136 | 630 | rb_protect(call_array_pack, (VALUE)pack_args, &state); |
137 | 630 | if (state) { |
138 | 630 | rb_set_errinfo(Qnil); |
139 | 630 | } |
140 | | |
141 | | // Test round-trip: pack then unpack |
142 | 630 | VALUE packed = Qnil; |
143 | 630 | state = 0; |
144 | 630 | packed = rb_protect(call_array_pack, (VALUE)pack_args, &state); |
145 | | |
146 | 630 | if (state == 0 && !NIL_P(packed)) { |
147 | 0 | VALUE roundtrip_args[] = {packed, template_str}; |
148 | 0 | state = 0; |
149 | 0 | rb_protect(call_string_unpack, (VALUE)roundtrip_args, &state); |
150 | 0 | if (state) { |
151 | 0 | rb_set_errinfo(Qnil); |
152 | 0 | } |
153 | 630 | } else if (state) { |
154 | 630 | rb_set_errinfo(Qnil); |
155 | 630 | } |
156 | | |
157 | | // Force GC |
158 | 630 | rb_gc_start(); |
159 | | |
160 | 630 | return 0; |
161 | 632 | } |