/src/pjsip/tests/fuzz/fuzz-audio.c
Line | Count | Source |
1 | | /* |
2 | | * Copyright (C) 2026 Teluu Inc. (http://www.teluu.com) |
3 | | * |
4 | | * This program is free software; you can redistribute it and/or modify |
5 | | * it under the terms of the GNU General Public License as published by |
6 | | * the Free Software Foundation; either version 2 of the License, or |
7 | | * (at your option) any later version. |
8 | | * |
9 | | * This program is distributed in the hope that it will be useful, |
10 | | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 | | * GNU General Public License for more details. |
13 | | * |
14 | | * You should have received a copy of the GNU General Public License |
15 | | * along with this program; if not, write to the Free Software |
16 | | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
17 | | */ |
18 | | #include <stdio.h> |
19 | | #include <stdint.h> |
20 | | #include <stdlib.h> |
21 | | #include <string.h> |
22 | | |
23 | | #include <pjlib.h> |
24 | | #include <pjmedia.h> |
25 | | #include <pjmedia-codec.h> |
26 | | #include <pjmedia-codec/g722.h> |
27 | | #include <pjmedia-codec/gsm.h> |
28 | | #include <pjmedia-codec/speex.h> |
29 | | #include <pjmedia-codec/ilbc.h> |
30 | | #include <pjmedia-codec/l16.h> |
31 | | #if defined(PJMEDIA_HAS_OPUS_CODEC) && (PJMEDIA_HAS_OPUS_CODEC != 0) |
32 | | # include <pjmedia-codec/opus.h> |
33 | | #endif |
34 | | |
35 | | /* Codec configuration structure */ |
36 | | typedef struct { |
37 | | pjmedia_codec **codec_ptr; |
38 | | const char *name; |
39 | | size_t min_frame_size; |
40 | | size_t pcm_frame_size; |
41 | | unsigned channel_cnt; |
42 | | } codec_config_t; |
43 | | |
44 | | /* Global state for persistent fuzzing */ |
45 | | static pj_caching_pool caching_pool; |
46 | | static pjmedia_endpt *endpt = NULL; |
47 | | static pj_pool_t *pool = NULL; |
48 | | static pjmedia_codec_mgr *codec_mgr = NULL; |
49 | | static int initialized = 0; |
50 | | |
51 | | /* Codec instances */ |
52 | | static pjmedia_codec *codec_pcma = NULL; |
53 | | static pjmedia_codec *codec_pcmu = NULL; |
54 | | static pjmedia_codec *codec_g722 = NULL; |
55 | | static pjmedia_codec *codec_gsm = NULL; |
56 | | static pjmedia_codec *codec_speex = NULL; |
57 | | static pjmedia_codec *codec_ilbc = NULL; |
58 | | static pjmedia_codec *codec_l16_8k = NULL; |
59 | | static pjmedia_codec *codec_l16_16k = NULL; |
60 | | static pjmedia_codec *codec_l16_48k = NULL; |
61 | | #if defined(PJMEDIA_HAS_OPUS_CODEC) && (PJMEDIA_HAS_OPUS_CODEC != 0) |
62 | | static pjmedia_codec *codec_opus = NULL; |
63 | | #endif |
64 | | |
65 | | /* Codec configurations array */ |
66 | | static codec_config_t codec_configs[] = { |
67 | | {&codec_pcma, "G.711 A-Law", 80, 160, 1}, |
68 | | {&codec_pcmu, "G.711 U-Law", 80, 160, 1}, |
69 | | {&codec_g722, "G.722", 80, 320, 1}, |
70 | | {&codec_gsm, "GSM", 33, 320, 1}, |
71 | | {&codec_speex, "Speex", 10, 320, 1}, |
72 | | {&codec_ilbc, "iLBC", 38, 480, 1}, |
73 | | {&codec_l16_8k, "L16 8kHz", 320, 320, 1}, |
74 | | {&codec_l16_16k, "L16 16kHz", 640, 640, 1}, |
75 | | {&codec_l16_48k, "L16 48kHz", 1920, 1920, 1}, |
76 | | #if defined(PJMEDIA_HAS_OPUS_CODEC) && (PJMEDIA_HAS_OPUS_CODEC != 0) |
77 | | /* Opus stereo @ 48 kHz: 20 ms frame = 960 samples * 2 ch * 2 bytes = 3840 */ |
78 | | {&codec_opus, "Opus", 10, 3840, 2}, |
79 | | #endif |
80 | | }; |
81 | | |
82 | 10.5k | #define NUM_CODECS (sizeof(codec_configs) / sizeof(codec_configs[0])) |
83 | | |
84 | | /* Codec ID strings for initialization */ |
85 | | static const char* codec_id_strings[] = { |
86 | | "PCMA/8000/1", |
87 | | "PCMU/8000/1", |
88 | | "G722/16000/1", |
89 | | "GSM/8000/1", |
90 | | "speex/8000/1", |
91 | | "iLBC/8000/1", |
92 | | "L16/8000/1", |
93 | | "L16/16000/1", |
94 | | "L16/48000/1", |
95 | | #if defined(PJMEDIA_HAS_OPUS_CODEC) && (PJMEDIA_HAS_OPUS_CODEC != 0) |
96 | | "opus/48000/2" |
97 | | #endif |
98 | | }; |
99 | | |
100 | | /* Clock rates for each codec (must match codec_id_strings order) */ |
101 | | static const unsigned codec_clock_rates[] = { |
102 | | 8000, /* PCMA */ |
103 | | 8000, /* PCMU */ |
104 | | 16000, /* G.722 */ |
105 | | 8000, /* GSM */ |
106 | | 8000, /* Speex */ |
107 | | 8000, /* iLBC */ |
108 | | 8000, /* L16 8kHz */ |
109 | | 16000, /* L16 16kHz */ |
110 | | 48000, /* L16 48kHz */ |
111 | | #if defined(PJMEDIA_HAS_OPUS_CODEC) && (PJMEDIA_HAS_OPUS_CODEC != 0) |
112 | | 48000 /* Opus */ |
113 | | #endif |
114 | | }; |
115 | | |
116 | | /* Helper function to allocate and open a codec */ |
117 | | static void alloc_codec(const char *id_str, pjmedia_codec **codec_ptr) |
118 | 10 | { |
119 | 10 | pj_status_t status; |
120 | 10 | pjmedia_codec_param param; |
121 | 10 | const pjmedia_codec_info *codec_info; |
122 | 10 | unsigned count = 1; |
123 | 10 | pj_str_t codec_id = pj_str((char*)id_str); |
124 | | |
125 | 10 | status = pjmedia_codec_mgr_find_codecs_by_id(codec_mgr, &codec_id, |
126 | 10 | &count, &codec_info, NULL); |
127 | 10 | if (status == PJ_SUCCESS && count > 0) { |
128 | 7 | status = pjmedia_codec_mgr_get_default_param(codec_mgr, codec_info, ¶m); |
129 | 7 | if (status == PJ_SUCCESS) { |
130 | 7 | param.setting.plc = 1; |
131 | 7 | status = pjmedia_codec_mgr_alloc_codec(codec_mgr, codec_info, codec_ptr); |
132 | 7 | if (status == PJ_SUCCESS && *codec_ptr) { |
133 | 7 | pjmedia_codec_open(*codec_ptr, ¶m); |
134 | 7 | } |
135 | 7 | } |
136 | 7 | } |
137 | 10 | } |
138 | | |
139 | | static int init_codecs(void) |
140 | 1.48k | { |
141 | 1.48k | pj_status_t status; |
142 | 1.48k | size_t i; |
143 | | |
144 | 1.48k | if (initialized) { |
145 | 1.48k | return 0; |
146 | 1.48k | } |
147 | | |
148 | | /* Initialize PJLIB */ |
149 | 1 | status = pj_init(); |
150 | 1 | if (status != PJ_SUCCESS) { |
151 | 0 | return -1; |
152 | 0 | } |
153 | | |
154 | 1 | pj_log_set_level(0); |
155 | | |
156 | | /* Initialize caching pool */ |
157 | 1 | pj_caching_pool_init(&caching_pool, &pj_pool_factory_default_policy, 0); |
158 | | |
159 | | /* Create memory pool */ |
160 | 1 | pool = pj_pool_create(&caching_pool.factory, "fuzzer", 4000, 4000, NULL); |
161 | 1 | if (!pool) { |
162 | 0 | return -1; |
163 | 0 | } |
164 | | |
165 | | /* Create media endpoint */ |
166 | 1 | status = pjmedia_endpt_create(&caching_pool.factory, NULL, 1, &endpt); |
167 | 1 | if (status != PJ_SUCCESS) { |
168 | 0 | return -1; |
169 | 0 | } |
170 | | |
171 | | /* Get codec manager */ |
172 | 1 | codec_mgr = pjmedia_endpt_get_codec_mgr(endpt); |
173 | 1 | if (!codec_mgr) { |
174 | 0 | return -1; |
175 | 0 | } |
176 | | |
177 | | /* Register all codecs */ |
178 | 1 | pjmedia_codec_g711_init(endpt); |
179 | 1 | pjmedia_codec_g722_init(endpt); |
180 | 1 | pjmedia_codec_gsm_init(endpt); |
181 | 1 | pjmedia_codec_speex_init(endpt, 0, -1, -1); |
182 | 1 | pjmedia_codec_ilbc_init(endpt, 30); |
183 | 1 | pjmedia_codec_l16_init(endpt, 0); |
184 | 1 | #if defined(PJMEDIA_HAS_OPUS_CODEC) && (PJMEDIA_HAS_OPUS_CODEC != 0) |
185 | 1 | pjmedia_codec_opus_init(endpt); |
186 | | |
187 | | /* The Opus factory is registered as stereo (per RFC 7587: clock rate |
188 | | * 48000, 2 channels), but its default codec param is mono since |
189 | | * opus_cfg.channel_cnt defaults to 1. Without overriding it here, |
190 | | * alloc_codec() would open Opus as mono via the default param, which |
191 | | * contradicts the "opus/48000/2" id and the 3840-byte (20 ms @ 48 kHz |
192 | | * stereo) PCM frame size, leaving the stereo encode/decode paths |
193 | | * unexercised. Configure 2 channels so Opus actually opens in stereo. |
194 | | * |
195 | | * The stereo override is only applied when every query/config step |
196 | | * succeeds. If any step fails, Opus stays at its factory (mono) default, |
197 | | * so we downgrade the harness Opus channel count and PCM frame size to |
198 | | * match how the codec will be opened (while still looking it up by the |
199 | | * registered "opus/48000/2" ID). |
200 | | */ |
201 | 1 | { |
202 | 1 | pjmedia_codec_param opus_param; |
203 | 1 | pjmedia_codec_opus_config opus_cfg; |
204 | 1 | const pjmedia_codec_info *opus_info; |
205 | 1 | unsigned opus_cnt = 1; |
206 | 1 | pj_str_t opus_id = pj_str("opus/48000/2"); |
207 | 1 | pj_bool_t stereo_ok = PJ_FALSE; |
208 | | /* Opus is the last entry in codec_configs[] (see the array above). */ |
209 | 1 | codec_config_t *opus_config = &codec_configs[NUM_CODECS - 1]; |
210 | | |
211 | 1 | if (pjmedia_codec_mgr_find_codecs_by_id(codec_mgr, &opus_id, &opus_cnt, |
212 | 1 | &opus_info, NULL) == PJ_SUCCESS && |
213 | 1 | opus_cnt > 0 && |
214 | 1 | pjmedia_codec_mgr_get_default_param(codec_mgr, opus_info, |
215 | 1 | &opus_param) == PJ_SUCCESS && |
216 | 1 | pjmedia_codec_opus_get_config(&opus_cfg) == PJ_SUCCESS) |
217 | 1 | { |
218 | 1 | opus_cfg.channel_cnt = 2; |
219 | 1 | if (pjmedia_codec_opus_set_default_param(&opus_cfg, &opus_param) |
220 | 1 | == PJ_SUCCESS) |
221 | 1 | { |
222 | 1 | stereo_ok = PJ_TRUE; |
223 | 1 | } |
224 | 1 | } |
225 | | |
226 | 1 | if (!stereo_ok) { |
227 | | /* Mono fallback: 20 ms @ 48 kHz mono = 960 samples * 2 bytes. */ |
228 | 0 | opus_config->channel_cnt = 1; |
229 | 0 | opus_config->pcm_frame_size = 1920; |
230 | 0 | } |
231 | 1 | } |
232 | 1 | #endif |
233 | | |
234 | | /* Allocate all codecs using the configuration array */ |
235 | 11 | for (i = 0; i < NUM_CODECS; i++) { |
236 | 10 | alloc_codec(codec_id_strings[i], codec_configs[i].codec_ptr); |
237 | 10 | } |
238 | | |
239 | 1 | initialized = 1; |
240 | 1 | return 0; |
241 | 1 | } |
242 | | |
243 | | /* Helper function to test codec encode/decode cycle */ |
244 | | static void test_codec_cycle(pjmedia_codec *codec, const uint8_t *Data, size_t Size, |
245 | | size_t min_frame_size, size_t pcm_frame_size, |
246 | | unsigned clock_rate, unsigned channel_cnt) |
247 | 5.76k | { |
248 | 5.76k | pj_status_t status; |
249 | 5.76k | pjmedia_frame input_frame, output_frame; |
250 | 5.76k | pj_int16_t pcm_buffer[4096] = {0}; |
251 | 5.76k | pj_uint8_t encoded_buffer[2048]; |
252 | 5.76k | pjmedia_codec_param param; |
253 | | |
254 | 5.76k | if (!codec || Size < min_frame_size) return; |
255 | | |
256 | | /* Initialise pjmedia_frame */ |
257 | 5.76k | pj_bzero(&input_frame, sizeof(input_frame)); |
258 | 5.76k | pj_bzero(&output_frame, sizeof(output_frame)); |
259 | | |
260 | | /* Test runtime parameter modification (tests VAD toggling, keep PLC enabled) */ |
261 | 5.76k | if (Size >= min_frame_size + 2) { |
262 | 5.06k | pj_bzero(¶m, sizeof(param)); |
263 | 5.06k | param.info.avg_bps = 128000; |
264 | 5.06k | param.info.clock_rate = clock_rate; |
265 | | /* Use the codec's actual channel count (e.g. 2 for Opus) rather than |
266 | | * forcing mono, so the modify path does not contradict how the codec |
267 | | * was opened and the stereo paths stay exercised. */ |
268 | 5.06k | param.info.channel_cnt = channel_cnt; |
269 | 5.06k | param.info.frm_ptime = 20; |
270 | 5.06k | param.info.pcm_bits_per_sample = 16; |
271 | 5.06k | param.setting.vad = (Data[min_frame_size] & 0x01) ? 1 : 0; |
272 | 5.06k | param.setting.plc = 1; |
273 | 5.06k | if (codec->op->modify) { |
274 | 5.06k | pjmedia_codec_modify(codec, ¶m); |
275 | 5.06k | } |
276 | 5.06k | } |
277 | | |
278 | | /* Test decode */ |
279 | 5.76k | input_frame.type = PJMEDIA_FRAME_TYPE_AUDIO; |
280 | 5.76k | input_frame.buf = (void*)Data; |
281 | 5.76k | input_frame.size = min_frame_size; |
282 | 5.76k | input_frame.timestamp.u64 = 0; |
283 | | |
284 | 5.76k | output_frame.buf = pcm_buffer; |
285 | 5.76k | output_frame.size = sizeof(pcm_buffer); |
286 | | |
287 | 5.76k | status = pjmedia_codec_decode(codec, &input_frame, sizeof(pcm_buffer), &output_frame); |
288 | | |
289 | | /* If decode succeeded, try to encode the output back */ |
290 | 5.76k | if (status == PJ_SUCCESS && output_frame.type == PJMEDIA_FRAME_TYPE_AUDIO) { |
291 | 4.70k | input_frame.type = PJMEDIA_FRAME_TYPE_AUDIO; |
292 | 4.70k | input_frame.buf = output_frame.buf; |
293 | 4.70k | input_frame.size = output_frame.size; |
294 | 4.70k | input_frame.timestamp.u64 = 0; |
295 | | |
296 | 4.70k | output_frame.buf = encoded_buffer; |
297 | 4.70k | output_frame.size = sizeof(encoded_buffer); |
298 | | |
299 | 4.70k | pjmedia_codec_encode(codec, &input_frame, sizeof(encoded_buffer), &output_frame); |
300 | 4.70k | } |
301 | | |
302 | | /* Test encode with fuzzer input as PCM - use proper frame size */ |
303 | 5.76k | if (Size >= pcm_frame_size && pcm_frame_size <= sizeof(pcm_buffer)) { |
304 | 3.05k | memcpy(pcm_buffer, Data, pcm_frame_size); |
305 | | |
306 | 3.05k | input_frame.type = PJMEDIA_FRAME_TYPE_AUDIO; |
307 | 3.05k | input_frame.buf = pcm_buffer; |
308 | 3.05k | input_frame.size = pcm_frame_size; |
309 | 3.05k | input_frame.timestamp.u64 = 0; |
310 | | |
311 | 3.05k | output_frame.buf = encoded_buffer; |
312 | 3.05k | output_frame.size = sizeof(encoded_buffer); |
313 | | |
314 | 3.05k | pjmedia_codec_encode(codec, &input_frame, sizeof(encoded_buffer), &output_frame); |
315 | 3.05k | } |
316 | | |
317 | | /* Test parse */ |
318 | 5.76k | if (codec->op->parse) { |
319 | 5.76k | pjmedia_frame frames[256]; |
320 | 5.76k | unsigned frame_cnt = 256; |
321 | | |
322 | 5.76k | input_frame.type = PJMEDIA_FRAME_TYPE_AUDIO; |
323 | 5.76k | input_frame.buf = (void*)Data; |
324 | 5.76k | input_frame.size = Size; |
325 | 5.76k | input_frame.timestamp.u64 = 0; |
326 | | |
327 | 5.76k | status = pjmedia_codec_parse(codec, (void*)Data, Size, &input_frame.timestamp, |
328 | 5.76k | &frame_cnt, frames); |
329 | 5.76k | (void)status; |
330 | 5.76k | } |
331 | | |
332 | | /* Test PLC if function exists */ |
333 | 5.76k | if (codec->op->recover) { |
334 | 5.76k | output_frame.buf = pcm_buffer; |
335 | 5.76k | output_frame.size = sizeof(pcm_buffer); |
336 | | |
337 | | /* Wrap in error handling to avoid asserts in codecs with incomplete PLC */ |
338 | 5.76k | status = pjmedia_codec_recover(codec, sizeof(pcm_buffer), &output_frame); |
339 | 5.76k | if (status == PJ_SUCCESS) { |
340 | 5.44k | output_frame.buf = pcm_buffer; |
341 | 5.44k | output_frame.size = sizeof(pcm_buffer); |
342 | 5.44k | pjmedia_codec_recover(codec, sizeof(pcm_buffer), &output_frame); |
343 | 5.44k | } |
344 | 5.76k | } |
345 | 5.76k | } |
346 | | |
347 | | int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) |
348 | 1.48k | { |
349 | 1.48k | const uint8_t *current_data; |
350 | 1.48k | size_t remaining_size; |
351 | 1.48k | size_t i; |
352 | 1.48k | codec_config_t *config; |
353 | 1.48k | pjmedia_codec *codec; |
354 | 1.48k | size_t chunk_size; |
355 | 1.48k | size_t consumed; |
356 | 1.48k | pj_int16_t pcm_val; |
357 | 1.48k | pj_uint8_t alaw; |
358 | 1.48k | pj_uint8_t ulaw; |
359 | | |
360 | | /* Initialize codecs on first run */ |
361 | 1.48k | if (init_codecs() != 0) { |
362 | 0 | return 0; |
363 | 0 | } |
364 | | |
365 | 1.48k | current_data = Data; |
366 | 1.48k | remaining_size = Size; |
367 | | |
368 | | /* Test each codec sequentially with non-overlapping chunks of data */ |
369 | 10.5k | for (i = 0; i < NUM_CODECS; i++) { |
370 | 9.95k | config = &codec_configs[i]; |
371 | 9.95k | codec = *(config->codec_ptr); |
372 | | |
373 | | /* Skip if codec not available or not enough data */ |
374 | 9.95k | if (!codec || remaining_size < config->min_frame_size) { |
375 | 4.19k | continue; |
376 | 4.19k | } |
377 | | |
378 | | /* Calculate chunk size for this codec (at least min_frame_size, max pcm_frame_size) */ |
379 | 5.76k | chunk_size = config->pcm_frame_size; |
380 | 5.76k | if (chunk_size > remaining_size) { |
381 | 2.70k | chunk_size = remaining_size; |
382 | 2.70k | } |
383 | | |
384 | | /* Test this codec with current data chunk */ |
385 | 5.76k | test_codec_cycle(codec, current_data, chunk_size, |
386 | 5.76k | config->min_frame_size, config->pcm_frame_size, |
387 | 5.76k | codec_clock_rates[i], config->channel_cnt); |
388 | | |
389 | | /* Advance to next chunk of data */ |
390 | 5.76k | consumed = config->min_frame_size; |
391 | 5.76k | if (consumed > remaining_size) { |
392 | 0 | consumed = remaining_size; |
393 | 0 | } |
394 | 5.76k | current_data += consumed; |
395 | 5.76k | remaining_size -= consumed; |
396 | | |
397 | | /* Stop if we've run out of data */ |
398 | 5.76k | if (remaining_size < 10) { |
399 | 891 | break; |
400 | 891 | } |
401 | 5.76k | } |
402 | | |
403 | | /* Test low-level G.711 conversion functions with remaining data */ |
404 | 1.48k | if (remaining_size > 1) { |
405 | 58.2k | for (i = 0; i + 1 < remaining_size && i < 256; i += 2) { |
406 | 57.4k | pcm_val = (pj_int16_t)((current_data[i] << 8) | current_data[i+1]); |
407 | | |
408 | 57.4k | alaw = pjmedia_linear2alaw(pcm_val); |
409 | 57.4k | ulaw = pjmedia_linear2ulaw(pcm_val); |
410 | 57.4k | pcm_val = pjmedia_alaw2linear(alaw); |
411 | 57.4k | pcm_val = pjmedia_ulaw2linear(ulaw); |
412 | 57.4k | ulaw = pjmedia_alaw2ulaw(current_data[i]); |
413 | 57.4k | alaw = pjmedia_ulaw2alaw(current_data[i]); |
414 | 57.4k | } |
415 | 785 | } |
416 | | |
417 | 1.48k | return 0; |
418 | 1.48k | } |