Repository navigation
Expand file tree
/
Copy pathcurl_20260909.html
More file actions
974 lines (911 loc) · 238 KB
/
Copy pathcurl_20260909.html
File metadata and controls
974 lines (911 loc) · 238 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="color-scheme" content="dark">
<title>Code Audit — Interactive</title>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{color-scheme:dark}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#0f172a;color:#e5e7eb;font-size:14px;line-height:1.5}
.app{max-width:1100px;margin:0 auto;padding:1.5rem 1rem}
.top{display:flex;justify-content:space-between;align-items:flex-start;gap:1rem;margin-bottom:1rem;flex-wrap:wrap}
h1{font-size:1.5rem;font-weight:700;color:#f1f5f9}
.meta{font-size:.8rem;color:#6b7280;margin-top:.2rem}
.top-right{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap}
.project-info{margin-bottom:1rem;padding:.75rem .9rem;background:#1e293b;border:1px solid #334155;border-radius:8px}
.project-info .pi-name{font-size:.95rem;font-weight:700;color:#f1f5f9}
.project-info .pi-desc{font-size:.82rem;color:#d1d5db;margin-top:.25rem}
.project-info .pi-row{display:flex;flex-wrap:wrap;gap:.4rem .9rem;margin-top:.45rem;font-size:.78rem;color:#9ca3af}
.project-info .pi-row .pi-k{color:#6b7280}
.project-info a{color:#60a5fa;text-decoration:none}
.project-info a:hover{text-decoration:underline}
.project-info code{font-family:"JetBrains Mono","Fira Code",Consolas,monospace;font-size:.74rem;color:#93c5fd}
.pi-repo-btn{display:inline-flex;align-items:center;gap:.3rem;padding:.18rem .5rem;background:#0f172a;border:1px solid #3b82f6;border-radius:6px;color:#60a5fa !important;font-weight:600}
.pi-repo-btn:hover{background:#1d283a;text-decoration:none !important}
#search{background:#1e293b;border:1px solid #334155;color:#e5e7eb;padding:.4rem .75rem;border-radius:6px;font-size:.875rem;width:220px;outline:none}
#search:focus{border-color:#3b82f6}
.group-label{font-size:.8rem;color:#9ca3af;display:flex;align-items:center;gap:.4rem}
select{background:#1e293b;border:1px solid #334155;color:#e5e7eb;padding:.35rem .6rem;border-radius:6px;font-size:.8rem;outline:none;cursor:pointer}
select:focus{border-color:#3b82f6}
.filterbar{display:flex;flex-direction:column;gap:.4rem;margin-bottom:.6rem;padding:.6rem .75rem;background:#1e293b;border:1px solid #334155;border-radius:8px}
.filter-row{display:flex;flex-wrap:wrap;gap:.3rem;align-items:center}
.filter-lbl{font-size:.65rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#4b5563;white-space:nowrap;margin-right:.25rem}
.filter-toggle{font-size:.65rem;font-weight:600;padding:.18rem .5rem;border-radius:4px;border:1px solid #334155;background:#0f172a;color:#9ca3af;cursor:pointer;margin-left:auto;white-space:nowrap;letter-spacing:.04em}
.filter-toggle:hover{background:#1e293b;color:#e5e7eb;border-color:#4b5563}
.badge{font-size:.68rem;font-weight:700;padding:.18rem .5rem;border-radius:4px;border:1px solid transparent;cursor:pointer;letter-spacing:.03em;transition:opacity .12s,filter .12s;user-select:none}
.badge.off{opacity:.28;filter:grayscale(.5)}
.sev-critical{background:#450a0a;color:#fca5a5;border-color:#991b1b}
.sev-high {background:#431407;color:#fdba74;border-color:#9a3412}
.sev-medium {background:#422006;color:#fcd34d;border-color:#854d0e}
.sev-low {background:#0c1a3a;color:#93c5fd;border-color:#1d4ed8}
.sev-negligible{background:#1e293b;color:#6b7280;border-color:#334155}
.type-badge{background:#0c1e33;color:#7dd3fc;border-color:#0369a1}
.diff-very-easy{background:#052e16;color:#86efac;border-color:#16a34a}
.diff-easy{background:#064e3b;color:#6ee7b7;border-color:#059669}
.diff-medium{background:#1e293b;color:#9ca3af;border-color:#4b5563}
.diff-hard{background:#2e1065;color:#c4b5fd;border-color:#7c3aed}
.st-open{background:#1e293b;color:#4b5563;border-color:#334155}
.st-fixed{background:#052e16;color:#4ade80;border-color:#16a34a}
.st-wont{background:#431407;color:#fdba74;border-color:#9a3412}
.st-fp{background:#2e1065;color:#c4b5fd;border-color:#7c3aed}
#progress-wrap{display:flex;align-items:center;gap:.75rem;margin-bottom:.6rem;flex-wrap:wrap}
#progress-bar{flex:1;min-width:120px;height:7px;background:#1e293b;border-radius:4px;overflow:hidden;border:1px solid #334155}
#progress-fill{height:100%;background:#22c55e;border-radius:4px;transition:width .35s ease;width:0}
#progress-text{font-size:.75rem;color:#6b7280;white-space:nowrap}
.action-btn{font-size:.72rem;padding:.25rem .6rem;border-radius:5px;border:1px solid #334155;background:#1e293b;color:#9ca3af;cursor:pointer;white-space:nowrap}
.action-btn:hover{background:#334155;color:#e5e7eb}
.action-btn.active{background:#1d4ed8;color:#fff;border-color:#2563eb}
.action-btn.active:hover{background:#2563eb}
.search-wrap{display:flex;align-items:center;gap:.35rem}
#hidden-bar{margin-bottom:.6rem}
.hidden-inner{display:flex;flex-wrap:wrap;gap:.35rem;align-items:center}
.hidden-lbl{font-size:.72rem;color:#4b5563}
.show-btn{font-size:.7rem;padding:.15rem .45rem;border-radius:4px;background:#1e293b;color:#9ca3af;border:1px solid #334155;cursor:pointer}
.show-btn:hover{background:#334155;color:#e5e7eb}
.show-all-btn{font-size:.7rem;padding:.15rem .45rem;border-radius:4px;background:#2563eb;color:#fff;border:none;cursor:pointer}
.show-all-btn:hover{background:#1d4ed8}
.group-section{margin-bottom:1.5rem}
.group-header{display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem;padding-bottom:.35rem;border-bottom:1px solid #334155}
.group-title{font-size:.95rem;font-weight:700;color:#d1d5db}
.group-title.all-done{color:#4ade80}
.group-count{font-size:.72rem;color:#4b5563;background:#1e293b;padding:.1rem .4rem;border-radius:10px;white-space:nowrap}
.icon-btn{background:transparent;border:none;color:#334155;cursor:pointer;font-size:.85rem;padding:.05rem .2rem;line-height:1}
.icon-btn:hover{color:#6b7280}
.hide-btn{margin-left:auto;font-size:.68rem;padding:.12rem .45rem;border-radius:4px;background:transparent;color:#334155;border:1px solid #1e293b;cursor:pointer}
.hide-btn:hover{background:#1e293b;color:#6b7280}
.card{background:#1e293b;border:1px solid #334155;border-radius:8px;margin-bottom:.6rem;overflow:hidden;transition:opacity .15s}
.card.resolved{opacity:.55;border-left:3px solid #16a34a}
.card.resolved:hover{opacity:.85}
.card-header{display:flex;align-items:center;gap:.5rem;padding:.45rem .85rem;background:#162032;border-bottom:1px solid #334155;flex-wrap:wrap;cursor:pointer;user-select:none}
.card-header:hover{background:#1a2840}
.card-arrow{font-size:.65rem;color:#334155;flex-shrink:0}
.card-id{font-size:.75rem;font-weight:700;color:#6b7280;font-family:monospace;letter-spacing:.04em}
.card-loc{font-size:.72rem;color:#6b7280;font-family:"JetBrains Mono","Fira Code",Consolas,monospace;letter-spacing:.02em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
.status-btn{margin-left:auto;font-size:.68rem;font-weight:600;padding:.15rem .5rem;border-radius:4px;border:1px solid transparent;cursor:pointer;white-space:nowrap;transition:filter .1s}
.status-btn:hover{filter:brightness(1.25)}
.copy-btn{font-size:.68rem;font-weight:600;padding:.15rem .5rem;border-radius:4px;border:1px solid #334155;background:#0f172a;color:#9ca3af;cursor:pointer;white-space:nowrap;transition:background .12s,color .12s}
.copy-btn:hover{background:#334155;color:#e5e7eb}
.card-body{padding:.85rem .9rem}
.field-lbl{font-size:.85rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:#7dd3fc;margin:1rem 0 .35rem;padding-bottom:.18rem;border-bottom:1px solid #1e293b}
.field-lbl:first-child{margin-top:0}
.desc,.fix-txt{font-size:.875rem;color:#d1d5db;line-height:1.55}
.desc p,.fix-txt p{margin:.4rem 0}
.desc p:first-child,.fix-txt p:first-child{margin-top:0}
.desc p:last-child,.fix-txt p:last-child{margin-bottom:0}
.loc-block{margin:.3rem 0 .6rem}
.loc-ref{font-size:.8rem;color:#38bdf8;font-family:"JetBrains Mono","Fira Code",Consolas,monospace;font-weight:600;margin-bottom:.22rem}
a.loc-link{color:inherit;text-decoration:none}
a.loc-link:hover{text-decoration:underline}
.loc-lines{color:#4b5563;font-weight:400}
pre{background:#0f172a;color:#cdd6f4;border-radius:5px;padding:.6rem .8rem;overflow-x:auto;white-space:pre;font-size:.8rem;line-height:1.55;font-family:"JetBrains Mono","Fira Code",Consolas,monospace;border:1px solid #1e293b;max-height:320px}
.desc pre.md-code,.fix-txt pre.md-code{margin:.5rem 0;font-size:.82rem;color:#e5e7eb;background:#0b1224;border:1px solid #1e293b;max-height:none}
code.md-inline{font-family:"JetBrains Mono","Fira Code",Consolas,monospace;font-size:.86em;background:#0b1224;color:#fca5a5;border:1px solid #1e293b;border-radius:3px;padding:.04rem .32rem;white-space:pre-wrap;word-break:break-word}
.not-found{font-size:.8rem;color:#334155;font-style:italic}
mark{background:#854d0e;color:#fef9c3;border-radius:2px;padding:0 .1rem}
.no-results{text-align:center;color:#334155;padding:3rem 1rem;font-size:.9rem}
</style>
</head>
<body>
<script type="application/json" id="D">[
{"id":"PANIC_1","type":"panic","type_label":"Panic","severity":"medium","difficulty":"easy","description":"NULL pointer dereference in FormAdd() for the CURLFORM_FILENAME / CURLFORM_BUFFER options. Unlike every other option handler (e.g. CURLFORM_COPYNAME, CURLFORM_FILE, CURLFORM_CONTENTTYPE) which first checks `if(avalue) ... else retval = CURL_FORMADD_NULL;`, this branch unconditionally calls `Curl_bufref_memdup0(&curr->showfilename, avalue, strlen(avalue))`. The `strlen(avalue)` argument is evaluated by the caller before the function runs, so passing a NULL value for CURLFORM_FILENAME/CURLFORM_BUFFER (an attacker- or caller-supplied bad combo) dereferences NULL and crashes. The other handlers in this very function guard against NULL.","fix":"Guard avalue before use, matching the other cases:\n avalue = form_ptr_arg(char *);\n if(!avalue)\n retval = CURL_FORMADD_NULL;\n else if(Curl_bufref_ptr(&curr->showfilename))\n retval = CURL_FORMADD_OPTION_TWICE;\n else if(Curl_bufref_memdup0(&curr->showfilename, avalue, strlen(avalue)))\n retval = CURL_FORMADD_MEMORY;","locations":[{"ref":"lib/formdata.c:549-556","code":" 549 | case CURLFORM_FILENAME:\n 550 | case CURLFORM_BUFFER:\n 551 | avalue = form_ptr_arg(char *);\n 552 | if(Curl_bufref_ptr(&curr->showfilename))\n 553 | retval = CURL_FORMADD_OPTION_TWICE;\n 554 | else if(Curl_bufref_memdup0(&curr->showfilename, avalue, strlen(avalue)))\n 555 | retval = CURL_FORMADD_MEMORY;\n 556 | break;","lines":8,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/formdata.c#L549-L556"}]},
{"id":"PANIC_2","type":"panic","type_label":"Panic","severity":"medium","difficulty":"very_easy","description":"In the HTTP/2 on_frame_recv() SETTINGS handler, `data` (obtained from CF_DATA_CURRENT(cf)) is treated as possibly NULL: the surrounding code guards uses with `if(data && ...)` (line 1192) and `if(data)` (line 1212). However, `CURL_REQ_WANT_SEND(data)` at line 1203 dereferences `data` unconditionally (it expands to `(data)->req.io_flags & REQ_IO_SEND`). If a connection-level SETTINGS frame arrives while no current easy handle is associated (the very case the other guards protect against), this is a NULL pointer dereference in release builds (only a DEBUGASSERT guards it).","fix":"Guard the unhold logic with the same NULL check, e.g. `if(data && CURL_REQ_WANT_SEND(data)) Curl_multi_mark_dirty(data);`.","locations":[{"ref":"lib/http2.c:1188-1200","code":" 1188 | if(data && max_conn != ctx->max_concurrent_streams) {\n 1189 | /* only signal change if the value actually changed */\n 1190 | CURL_TRC_CF(data, cf, \"[0] notify MAX_CONCURRENT_STREAMS: %u\",\n 1191 | ctx->max_concurrent_streams);\n 1192 | Curl_multi_connchanged(data->multi);\n 1193 | }\n 1194 | /* Since the initial stream window is 64K, a request might be on HOLD,\n 1195 | * due to exhaustion. The (initial) SETTINGS may announce a much larger\n 1196 | * window and *assume* that we treat this like a WINDOW_UPDATE. Some\n 1197 | * servers send an explicit WINDOW_UPDATE, but not all seem to do that.\n 1198 | * To be safe, we UNHOLD a stream in order not to stall. */\n 1199 | if(CURL_REQ_WANT_SEND(data))\n 1200 | Curl_multi_mark_dirty(data);","lines":13,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/http2.c#L1188-L1200"}]},
{"id":"PANIC_3","type":"panic","type_label":"Panic","severity":"medium","difficulty":"medium","description":"In mbed_configure_ssl() the ALPN protocols are copied into the fixed array `const char *protocols[3]` (struct member) with `for(i = 0; i < connssl->alpn->count; ++i) backend->protocols[i] = ...`. ALPN_ENTRIES_MAX is 3, so `count` can be 3, filling protocols[0..2] and leaving no NULL terminator. mbedtls_ssl_conf_alpn_protocols() requires a NULL-terminated list and will read past the array (OOB read / use of garbage pointer). With the common count of 2 it happens to work because protocols[2] stays NULL, masking the bug.","fix":"Size the array to ALPN_ENTRIES_MAX + 1 and ensure protocols[count] is set to NULL after the loop (the struct is zero-initialized, but only if the array is large enough to hold the terminator).","locations":[{"ref":"lib/vtls/mbedtls.c:87","code":" 87 | const char *protocols[3];","lines":1,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vtls/mbedtls.c#L87"},{"ref":"lib/vtls/mbedtls.c:1001-1014","code":" 1001 | if(connssl->alpn) {\n 1002 | struct alpn_proto_buf proto;\n 1003 | size_t i;\n 1004 | \n 1005 | for(i = 0; i < connssl->alpn->count; ++i) {\n 1006 | backend->protocols[i] = connssl->alpn->entries[i];\n 1007 | }\n 1008 | /* this function does not clone the protocols array, which is why we need\n 1009 | to keep it around */\n 1010 | if(mbedtls_ssl_conf_alpn_protocols(&backend->config,\n 1011 | &backend->protocols[0])) {\n 1012 | failf(data, \"Failed setting ALPN protocols\");\n 1013 | return CURLE_SSL_CONNECT_ERROR;\n 1014 | }","lines":14,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vtls/mbedtls.c#L1001-L1014"}]},
{"id":"PANIC_4","type":"panic","type_label":"Panic","severity":"low","difficulty":"very_easy","description":"In file_do(), `xfer_buf` is declared without initialization. If\nCurl_multi_xfer_buf_borrow() fails it returns early via `goto out`, where\nCurl_multi_xfer_buf_release(data, xfer_buf) is called with the uninitialized\npointer. Curl_multi_xfer_buf_release ignores the buffer value at runtime, but\nit contains a DEBUGASSERT(!buf || data->multi->xfer_buf == buf) that\ndereferences/compares the uninitialized pointer, which can fire a spurious\nassertion in debug builds. (Note: borrow sets *pbuf = NULL on the no-multi /\nbuffer-size-0 paths but returns CURLE_OUT_OF_MEMORY without touching *pbuf only\nafter already setting it to NULL at the top, so in practice xfer_buf is NULL on\nall current failure paths — but relying on that is fragile.)","fix":"Initialize `char *xfer_buf = NULL;` at declaration so the release path is well-defined regardless of how borrow fails.","locations":[{"ref":"lib/file.c:489-490","code":" 489 | char *xfer_buf;\n 490 | size_t xfer_blen;","lines":2,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/file.c#L489-L490"},{"ref":"lib/file.c:620-622","code":" 620 | result = Curl_multi_xfer_buf_borrow(data, &xfer_buf, &xfer_blen);\n 621 | if(result)\n 622 | goto out;","lines":3,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/file.c#L620-L622"},{"ref":"lib/file.c:693-695","code":" 693 | out:\n 694 | Curl_multi_xfer_buf_release(data, xfer_buf);\n 695 | return result;","lines":3,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/file.c#L693-L695"}]},
{"id":"PANIC_5","type":"panic","type_label":"Panic","severity":"low","difficulty":"easy","description":"ftp_state_size_resp() parses the SIZE reply. With ftpcode==213 it does\n`memchr(start, '\\r', len - 4)` where len = ftpc->pp.nfinal and start =\n&buf[4]. If a malformed/truncated cached response made nfinal < 4 this would\nunderflow to a huge length and read out of bounds. In practice ftp_endofresp\nguarantees a LASTLINE (>=4 chars + code) before this state is reached, so\nnfinal >= 6, but there is no defensive check here and the value flows directly\nfrom server-controlled framing.","fix":"Guard with `if(len > 4)` before computing `len - 4` and calling memchr, falling back to start otherwise.","locations":[{"ref":"lib/ftp.c:2654-2674","code":" 2654 | const char *buf = curlx_dyn_ptr(&ftpc->pp.recvbuf);\n 2655 | size_t len = ftpc->pp.nfinal;\n 2656 | \n 2657 | /* get the size from the ascii string: */\n 2658 | if(ftpcode == 213) {\n 2659 | /* To allow servers to prepend \"rubbish\" in the response string, we scan\n 2660 | for all the digits at the end of the response and parse only those as a\n 2661 | number. */\n 2662 | const char *start = &buf[4];\n 2663 | const char *fdigit = memchr(start, '\\r', len - 4);\n 2664 | if(fdigit) {\n 2665 | fdigit--;\n 2666 | if(*fdigit == '\\n')\n 2667 | fdigit--;\n 2668 | while(ISDIGIT(fdigit[-1]) && (fdigit > start))\n 2669 | fdigit--;\n 2670 | }\n 2671 | else\n 2672 | fdigit = start;\n 2673 | if(curlx_str_number(&fdigit, &filesize, CURL_OFF_T_MAX))\n 2674 | filesize = -1; /* size remain unknown */","lines":21,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/ftp.c#L2654-L2674"}]},
{"id":"PANIC_6","type":"panic","type_label":"Panic","severity":"low","difficulty":"very_easy","description":"schannel_get_internals() unconditionally returns &backend->ctxt->ctxt_handle. If it is ever called before the security context has been created (backend->ctxt == NULL, e.g. on a failed/early connection), this dereferences a NULL pointer. The other backends' get_internals return a directly embedded handle and cannot NULL-deref the same way, making schannel inconsistent.","fix":"Return NULL when backend->ctxt is NULL, e.g. `return backend->ctxt ? &backend->ctxt->ctxt_handle : NULL;`.","locations":[{"ref":"lib/vtls/schannel.c:2713-2721","code":" 2713 | static void *schannel_get_internals(struct ssl_connect_data *connssl,\n 2714 | CURLINFO info)\n 2715 | {\n 2716 | struct schannel_ssl_backend_data *backend =\n 2717 | (struct schannel_ssl_backend_data *)connssl->backend;\n 2718 | (void)info;\n 2719 | DEBUGASSERT(backend);\n 2720 | return &backend->ctxt->ctxt_handle;\n 2721 | }","lines":9,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vtls/schannel.c#L2713-L2721"}]},
{"id":"RACE_1","type":"race_condition","type_label":"Race Condition","severity":"low","difficulty":"medium","description":"easy_events() uses a function-local `static struct events evs` and the comment\nexplains it is made static so it survives after the function returns and\ncurl_multi_remove_handle() is called. As a consequence curl_easy_perform_ev()\nis not reentrant and not thread-safe: two concurrent event-based blocking\nperforms (or nested ones) share the same `evs` (including its socket-monitor\nlinked list `ev->list`), which can corrupt the monitor list. This is\nDEBUGBUILD-only test plumbing, so production impact is none, but it is a real\nshared-mutable-state hazard worth noting.","fix":"Avoid the static; if the struct must outlive the function, heap-allocate it and tie its lifetime to the multi/easy handle, or document and enforce single-threaded debug-only use.","locations":[{"ref":"lib/easy.c:703-713","code":" 703 | static CURLcode easy_events(struct Curl_multi *multi)\n 704 | {\n 705 | /* this struct is made static to allow it to be used after this function\n 706 | returns and curl_multi_remove_handle() is called */\n 707 | static struct events evs = { -1, FALSE, 0, NULL, 0 };\n 708 | \n 709 | /* if running event-based, do some further multi inits */\n 710 | events_setup(multi, &evs);\n 711 | \n 712 | return wait_or_timeout(multi, &evs);\n 713 | }","lines":11,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/easy.c#L703-L713"}]},
{"id":"MEM_1","type":"memory_leak","type_label":"Memory Leak","severity":"medium","difficulty":"very_easy","description":"doh_decode_rdata_name() leaks the local `thename` dynbuf on two of its error return paths. When the initial zero-length label is handled and the \".\" add fails (line 1096-1097) it `return CURLE_OUT_OF_MEMORY;` without curlx_dyn_free(&thename). When a label add fails inside the loop (line 1104-1106) it `return CURLE_TOO_LARGE;` again without freeing. The other two error paths in the same function (lines 1100-1102 and 1110-1113) correctly call curlx_dyn_free(&thename) first, confirming the omission is a bug. Any allocated dynbuf content is leaked. This is reachable from parsing attacker-controlled HTTPS RR RDATA.","fix":"Call curlx_dyn_free(&thename) before returning on both the line 1096-1097 and line 1104-1106 error paths (matching the other two error returns).","locations":[{"ref":"lib/vdns/doh.c:916-928","code":" 916 | if(clen == 0) {\n 917 | /* special case - return \".\" as name */\n 918 | if(curlx_dyn_addn(&thename, \".\", 1))\n 919 | return CURLE_OUT_OF_MEMORY;\n 920 | }\n 921 | while(clen) {\n 922 | if(clen >= rem) {\n 923 | curlx_dyn_free(&thename);\n 924 | return CURLE_OUT_OF_MEMORY;\n 925 | }\n 926 | if(curlx_dyn_addn(&thename, cp, clen) ||\n 927 | curlx_dyn_addn(&thename, \".\", 1))\n 928 | return CURLE_TOO_LARGE;","lines":13,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vdns/doh.c#L916-L928"}]},
{"id":"MEM_2","type":"memory_leak","type_label":"Memory Leak","severity":"medium","difficulty":"very_easy","description":"do_pubkey_field() inits a dynbuf `out` and only frees it inside the `if(!result)` branch. When ASN1tostr() returns a non-OK CURLcode (e.g. CURLE_WEIRD_SERVER_REPLY for an invalid wide char, or an OOM during dynbuf growth), the function returns with `out` still holding an allocated buffer that is never freed. This path is reachable while parsing a malicious/garbage server certificate public key, so it is an attacker-influenced leak repeated per certificate field.","fix":"Always free the dynbuf: move `curlx_dyn_free(&out);` out of the `if(!result)` block so it runs on every return path, e.g. call it unconditionally before `return result;`.","locations":[{"ref":"lib/vtls/x509asn1.c:957-975","code":" 957 | static CURLcode do_pubkey_field(struct Curl_easy *data, int certnum,\n 958 | const char *label,\n 959 | struct Curl_asn1Element *elem)\n 960 | {\n 961 | CURLcode result;\n 962 | struct dynbuf out;\n 963 | \n 964 | curlx_dyn_init(&out, CURL_X509_STR_MAX);\n 965 | \n 966 | /* Generate a certificate information record for the public key. */\n 967 | \n 968 | result = ASN1tostr(&out, elem);\n 969 | if(!result) {\n 970 | if(data->set.ssl.certinfo)\n 971 | result = ssl_push_certinfo_dyn(data, certnum, label, &out);\n 972 | curlx_dyn_free(&out);\n 973 | }\n 974 | return result;\n 975 | }","lines":19,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vtls/x509asn1.c#L957-L975"}]},
{"id":"MEM_3","type":"memory_leak","type_label":"Memory Leak","severity":"low","difficulty":"medium","description":"In split_to_dyn_array(), when a non-final segment's curlx_dyn_addn() fails (OOM), the code does `goto fail` after curlx_dyn_init(&db[index]) but before incrementing num_splits. Since *num_splits_out is set to num_splits (which does not include this index), the caller canon_query() frees only db[0..num_splits-1] via dyn_array_free(), leaking the dynbuf already initialized/allocated at db[index]. Only reachable on allocation failure.","fix":"On the addn-failure path, free db[index] before goto fail, or increment num_splits before the addn so the count includes the partially-built entry that dyn_array_free will then clean up.","locations":[{"ref":"lib/http_aws_sigv4.c:166-178","code":" 166 | if(segment_length) {\n 167 | curlx_dyn_init(&db[index], segment_length + 1);\n 168 | result = curlx_dyn_addn(&db[index], &source[start], segment_length);\n 169 | if(result)\n 170 | goto fail;\n 171 | \n 172 | segment_length = 0;\n 173 | index++;\n 174 | if(++num_splits == MAX_QUERY_COMPONENTS) {\n 175 | result = CURLE_TOO_LARGE;\n 176 | goto fail;\n 177 | }\n 178 | }","lines":13,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/http_aws_sigv4.c#L166-L178"}]},
{"id":"MEM_4","type":"memory_leak","type_label":"Memory Leak","severity":"low","difficulty":"easy","description":"In ipfs_gateway(), after opening the gateway file, the first line is read character-by-character into the dynbuf `dyn`. If curlx_dyn_addn() fails it does `goto fail`. The `fail:` label only closes gfile and frees ipfs_path_c; it never calls curlx_dyn_free(&dyn), so the partially-built dynbuf allocation is leaked on that error path. On the success path ownership of the dynbuf buffer is transferred to the caller, but the failure path has no such transfer.","fix":"Free the dynbuf on the error path, e.g. add `curlx_dyn_free(&dyn);` before/at the goto, or restructure so the fail label frees it. Since `dyn` is only in scope inside the `if(gfile)` block, free it there before jumping (or move the goto to a local cleanup that frees dyn).","locations":[{"ref":"src/tool_ipfs.c:71-96","code":" 71 | if(gfile) {\n 72 | int c;\n 73 | struct dynbuf dyn;\n 74 | char *gateway = NULL;\n 75 | curlx_dyn_init(&dyn, MAX_GATEWAY_URL_LEN);\n 76 | \n 77 | /* get the first line of the gateway file, ignore the rest */\n 78 | while((c = getc(gfile)) != EOF && c != '\\n' && c != '\\r') {\n 79 | char c_char = (char)c;\n 80 | if(curlx_dyn_addn(&dyn, &c_char, 1))\n 81 | goto fail;\n 82 | }\n 83 | \n 84 | if(curlx_dyn_len(&dyn))\n 85 | gateway = curlx_dyn_ptr(&dyn);\n 86 | \n 87 | curl_free(ipfs_path_c);\n 88 | curlx_fclose(gfile);\n 89 | \n 90 | return gateway;\n 91 | }\n 92 | fail:\n 93 | if(gfile)\n 94 | curlx_fclose(gfile);\n 95 | curl_free(ipfs_path_c);\n 96 | return NULL;","lines":26,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/src/tool_ipfs.c#L71-L96"}]},
{"id":"MEM_5","type":"memory_leak","type_label":"Memory Leak","severity":"low","difficulty":"easy","description":"varexpand() initializes a local dynbuf `buf` at line 294 (used to hold function-processed variable content). When the expanded value is found to contain a NUL byte, the function returns PARAM_EXPAND_ERROR at line 311 without calling curlx_dyn_free(&buf). On the normal path (line 316) and most error paths the buffer is freed, so this branch leaks whatever `varfunc()` allocated into `buf` (when a function suffix like ':b64' / ':64dec' was used). It is a terminal error in a short-lived CLI but still a genuine leak.","fix":"Free the buffer before returning, e.g. add `curlx_dyn_free(&buf);` immediately before `return PARAM_EXPAND_ERROR;` (and ensure `out` is also cleaned up consistently with the caller's expectations).","locations":[{"ref":"src/var.c:302-309","code":" 302 | if(value && vlen > 0) {\n 303 | /* A variable might contain null bytes. Such bytes cannot be shown\n 304 | using normal means, this is an error. */\n 305 | const char *nb = memchr(value, '\\0', vlen);\n 306 | if(nb) {\n 307 | errorf(\"variable contains null byte\");\n 308 | return PARAM_EXPAND_ERROR;\n 309 | }","lines":8,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/src/var.c#L302-L309"}]},
{"id":"RES_1","type":"resource_leak","type_label":"Resource Leak","severity":"low","difficulty":"easy","description":"In ftp_setup_connection(), if Curl_meta_set(CURL_META_FTP_EASY, ftp, ...)\nsucceeds but the subsequent allocation of `ftpc` fails (or\nCurl_conn_meta_set fails) the function returns CURLE_OUT_OF_MEMORY. The `ftp`\nobject is owned by the easy-handle meta store (freed by ftp_easy_dtor), so that\nis fine; but `ftpc` allocated at line 4414 is leaked when\nCurl_conn_meta_set() itself fails after a successful curlx_calloc, because\nmeta_set takes ownership only on success and the error path does not free ftpc.","fix":"On the `!ftpc || Curl_conn_meta_set(...)` failure, free ftpc when it was allocated but meta_set failed (mirror the pattern used for account/alternative_to_user).","locations":[{"ref":"lib/ftp.c:4437-4440","code":" 4437 | ftpc = curlx_calloc(1, sizeof(*ftpc));\n 4438 | if(!ftpc ||\n 4439 | Curl_conn_meta_set(conn, CURL_META_FTP_CONN, ftpc, ftp_conn_dtor))\n 4440 | return CURLE_OUT_OF_MEMORY;","lines":4,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/ftp.c#L4437-L4440"}]},
{"id":"RES_2","type":"resource_leak","type_label":"Resource Leak","severity":"low","difficulty":"medium","description":"In the non-pipe2 fallback of wakeup_pipe(), when nonblocking is requested and\ncurlx_nonblock() fails, both pipe fds are closed. However, in the HAVE_FCNTL\nblock just above, if only the FD_CLOEXEC fcntl on socks[1] fails (after the\nfcntl on socks[0] succeeded), both are closed correctly. The handling is fine\nthere. The genuine concern is the eventfd path: in wakeup_eventfd() both\nsocks[0] and socks[1] are set to the SAME fd. If a later caller iterates and\ncloses both entries it would double-close; Curl_wakeup_destroy() guards this\nwith `#ifndef USE_EVENTFD` for socks[1], but any other code path that closes\nsocks[0] and socks[1] independently for the eventfd backend would double-close\nthe same descriptor. This aliasing is fragile and undocumented at the call\nsites.","fix":"Document the socks[0]==socks[1] aliasing for the eventfd backend at the struct/usage level, or store CURL_SOCKET_BAD in socks[1] for eventfd so generic close loops cannot double-close.","locations":[{"ref":"lib/socketpair.c:38-47","code":" 38 | static int wakeup_eventfd(curl_socket_t socks[2], bool nonblocking)\n 39 | {\n 40 | int efd = eventfd(0, nonblocking ? EFD_CLOEXEC | EFD_NONBLOCK : EFD_CLOEXEC);\n 41 | if(efd == -1) {\n 42 | socks[0] = socks[1] = CURL_SOCKET_BAD;\n 43 | return -1;\n 44 | }\n 45 | socks[0] = socks[1] = efd;\n 46 | return 0;\n 47 | }","lines":10,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/socketpair.c#L38-L47"},{"ref":"lib/socketpair.c:353-362","code":" 353 | void Curl_wakeup_destroy(curl_socket_t socks[2])\n 354 | {\n 355 | #ifndef USE_EVENTFD\n 356 | if(socks[1] != CURL_SOCKET_BAD)\n 357 | wakeup_close(socks[1]);\n 358 | #endif\n 359 | if(socks[0] != CURL_SOCKET_BAD)\n 360 | wakeup_close(socks[0]);\n 361 | socks[0] = socks[1] = CURL_SOCKET_BAD;\n 362 | }","lines":10,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/socketpair.c#L353-L362"}]},
{"id":"RES_3","type":"resource_leak","type_label":"Resource Leak","severity":"low","difficulty":"easy","description":"wssl_random() initializes a wolfSSL RNG with wc_InitRng() but on two early-return error paths it does not call wc_FreeRng(): when `length > UINT_MAX` and when wc_RNG_GenerateBlock() fails. Both return CURLE_FAILED_INIT while leaking the initialized RNG context (which may hold OS handles / allocated state).","fix":"Free the RNG before returning on every path after a successful wc_InitRng(), e.g. use a goto cleanup that calls wc_FreeRng(&rng), or move the `length > UINT_MAX` check before wc_InitRng().","locations":[{"ref":"lib/vtls/wolfssl.c:2261-2271","code":" 2261 | WC_RNG rng;\n 2262 | (void)data;\n 2263 | if(wc_InitRng(&rng))\n 2264 | return CURLE_FAILED_INIT;\n 2265 | if(length > UINT_MAX)\n 2266 | return CURLE_FAILED_INIT;\n 2267 | if(wc_RNG_GenerateBlock(&rng, entropy, (unsigned)length))\n 2268 | return CURLE_FAILED_INIT;\n 2269 | if(wc_FreeRng(&rng))\n 2270 | return CURLE_FAILED_INIT;\n 2271 | return CURLE_OK;","lines":11,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vtls/wolfssl.c#L2261-L2271"}]},
{"id":"RES_4","type":"resource_leak","type_label":"Resource Leak","severity":"low","difficulty":"very_easy","description":"glob_match_url() builds the output into the local dynbuf `dyn`. On the out-of-memory error paths after a failed curlx_dyn_addn() the function returns the error without freeing `dyn`: line 784-785 (`if(result) return result;`) inside the main loop and line 788-789 (`if(curlx_dyn_addn(&dyn, \"\", 0)) return CURLE_OUT_OF_MEMORY;`). Other error paths in the same function correctly call curlx_dyn_free(&dyn) (e.g. line 753, 778, 796). These two paths leak the accumulated buffer on allocation failure.","fix":"Call curlx_dyn_free(&dyn) before returning on these two error paths (or use a single `goto error` label that frees the buffer).","locations":[{"ref":"src/tool_urlglob.c:782-787","code":" 782 | if(result)\n 783 | return result;\n 784 | }\n 785 | \n 786 | if(curlx_dyn_addn(&dyn, \"\", 0))\n 787 | return CURLE_OUT_OF_MEMORY;","lines":6,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/src/tool_urlglob.c#L782-L787"}]},
{"id":"OVF_1","type":"overflow_underflow","type_label":"Overflow Underflow","severity":"medium","difficulty":"easy","description":"tftp_set_timeouts() computes `timeout = (time_t)(timeout_ms + 500) / 1000` and\nthen `state->retry_max = (int)timeout / 5`. timeout_ms is a timediff_t\n(64-bit). The branch only enters this calculation when timeout_ms < 3600000, so\ntimeout fits, but note the cast precedence: `(time_t)(timeout_ms + 500) / 1000`\ncasts the sum to time_t before dividing — fine. The real concern is\n`state->retry_time = (int)(timeout / state->retry_max)`: retry_max is clamped to\n[3,50] so no divide-by-zero. This is sound; however the infof at lines 191-193\nprints timeout_ms with FMT_OFF_T while timeout_ms is timediff_t — on platforms\nwhere timediff_t and curl_off_t differ in size/signedness this is a\nformat/type mismatch.","fix":"Print timeout_ms with the timediff_t format macro (e.g. FMT_TIMEDIFF_T / appropriate cast) instead of FMT_OFF_T to avoid undefined behavior from a varargs type mismatch.","locations":[{"ref":"lib/tftp.c:172-193","code":" 172 | timeout = (time_t)(timeout_ms + 500) / 1000;\n 173 | else\n 174 | timeout = 15;\n 175 | \n 176 | /* Average reposting an ACK after 5 seconds */\n 177 | state->retry_max = (int)timeout / 5;\n 178 | \n 179 | /* Bound the total number */\n 180 | if(state->retry_max < 3)\n 181 | state->retry_max = 3;\n 182 | \n 183 | if(state->retry_max > 50)\n 184 | state->retry_max = 50;\n 185 | \n 186 | /* Compute the re-ACK interval to suit the timeout */\n 187 | state->retry_time = (int)(timeout / state->retry_max);\n 188 | if(state->retry_time < 1)\n 189 | state->retry_time = 1;\n 190 | \n 191 | infof(state->data,\n 192 | \"set timeouts for state %d; Total %\" FMT_OFF_T \", retry %d maxtry %d\",\n 193 | (int)state->state, timeout_ms, state->retry_time, state->retry_max);","lines":22,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/tftp.c#L172-L193"}]},
{"id":"OVF_2","type":"overflow_underflow","type_label":"Overflow Underflow","severity":"low","difficulty":"medium","description":"curlx_tvtoms() computes (tv->tv_sec * 1000) using time_t arithmetic before\nadding the microsecond contribution. On platforms where time_t is a 64-bit\nsigned type this is fine for realistic values, but the multiplication is done\nin time_t and then implicitly converted to timediff_t; for a large tv_sec\n(e.g. an absolute wall-clock timeval rather than a small interval) tv_sec*1000\ncan overflow time_t/timediff_t. Unlike the other timeval helpers in this file\n(curlx_ptimediff_ms etc.) there is no saturation/overflow guard here.","fix":"Cast tv_sec to timediff_t before multiplying and add a saturation check against\nTIMEDIFF_T_MAX/1000, mirroring the guards used in curlx_ptimediff_ms().","locations":[{"ref":"lib/curlx/timediff.c:82-85","code":" 82 | timediff_t curlx_tvtoms(struct timeval *tv)\n 83 | {\n 84 | return (tv->tv_sec * 1000) + (timediff_t)(tv->tv_usec / 1000);\n 85 | }","lines":4,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/curlx/timediff.c#L82-L85"}]},
{"id":"OVF_3","type":"overflow_underflow","type_label":"Overflow Underflow","severity":"low","difficulty":"easy","description":"mbed_load_cacert() and mbed_load_clicert() inspect `((char *)blob->data)[blob->len - 1]` to detect a NUL-terminated PEM blob. If a caller supplies a zero-length CURLOPT_CAINFO_BLOB / CURLOPT_SSLCERT_BLOB (len == 0), this indexes element [SIZE_MAX], an out-of-bounds read. The blob length is user-controlled via the public blob API.","fix":"Guard the access with a length check, e.g. only test the last byte when `blob->len > 0`; treat len==0 as an error (or as non-NUL-terminated PEM) before dereferencing `data[len-1]`.","locations":[{"ref":"lib/vtls/mbedtls.c:502-503","code":" 502 | if((ssl_cert_type && curl_strequal(ssl_cert_type, \"DER\")) ||\n 503 | ((char *)(ca_info_blob->data))[ca_info_blob->len - 1] == '\\0') {","lines":2,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vtls/mbedtls.c#L502-L503"},{"ref":"lib/vtls/mbedtls.c:614-615","code":" 614 | if((ssl_cert_type && curl_strequal(ssl_cert_type, \"DER\")) ||\n 615 | ((char *)(ssl_cert_blob->data))[ssl_cert_blob->len - 1] == '\\0') {","lines":2,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vtls/mbedtls.c#L614-L615"}]},
{"id":"LOGIC_1","type":"logic_error","type_label":"Logic Error","severity":"high","difficulty":"medium","description":"In tftp_receive_packet(), the address-pinning check uses\n`memcmp(&remote_addr, &state->remote_addr, fromlen)` where fromlen is the\nlength returned by recvfrom for the *current* packet. The pinned length is\nstored as remote_addrlen. The code first checks `state->remote_addrlen !=\nfromlen`, so the memcmp length matches both, but memcmp over a\nCurl_sockaddr_storage compares the entire reported socklen which for some\naddress families includes uninitialized padding bytes (e.g. sockaddr_in\nsin_zero). recvfrom does not guarantee those padding bytes are identical\nbetween packets, so a legitimate continuation packet from the same peer could\nbe wrongly rejected with \"Data received from another address\".","fix":"Compare only the meaningful address fields (family + addr + port) rather than the raw socklen-many bytes, or zero the storage before each recvfrom and memcpy so padding is deterministic.","locations":[{"ref":"lib/tftp.c:1050-1064","code":" 1050 | if((state->rbytes >= 0) && fromlen) {\n 1051 | if(state->remote_pinned) {\n 1052 | /* pinned, verify that it comes from the same address */\n 1053 | if((state->remote_addrlen != fromlen) ||\n 1054 | memcmp(&remote_addr, &state->remote_addr, fromlen)) {\n 1055 | failf(data, \"Data received from another address\");\n 1056 | return CURLE_RECV_ERROR;\n 1057 | }\n 1058 | }\n 1059 | else {\n 1060 | /* pin address on first use */\n 1061 | state->remote_pinned = TRUE;\n 1062 | state->remote_addrlen = fromlen;\n 1063 | memcpy(&state->remote_addr, &remote_addr, fromlen);\n 1064 | }","lines":15,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/tftp.c#L1050-L1064"}]},
{"id":"LOGIC_2","type":"logic_error","type_label":"Logic Error","severity":"high","difficulty":"very_easy","description":"Curl_thrdpool_await_idle() computes the remaining wait time with the\narguments to curlx_timediff_ms() swapped. curlx_timediff_ms(newer, older)\nreturns (newer - older) in milliseconds. Here it is called as\ncurlx_timediff_ms(curlx_now(), end), i.e. (now - end). Since `end` is set to\n`now + timeout_ms`, this expression is always approximately -timeout_ms, hence\nnegative. The subsequent check `if(remain_ms <= 0)` therefore fires on the very\nfirst iteration and the function returns CURLE_OPERATION_TIMEDOUT immediately,\nwithout ever calling Curl_cond_timedwait() to actually wait. The intended\nremaining time is `end - now`, which requires curlx_timediff_ms(end,\ncurlx_now()). As written, any caller that passes a non-zero timeout_ms gets an\ninstant spurious timeout instead of waiting for the pool to go idle.","fix":"Swap the arguments so the remaining time is positive while `now` is before\n`end`:\n remain_ms = curlx_timediff_ms(end, curlx_now());\n(optionally clamp to a small positive minimum to avoid a 0ms timedwait).","locations":[{"ref":"lib/thrdpool.c:435-440","code":" 435 | remain_ms = curlx_timediff_ms(curlx_now(), end);\n 436 | if(remain_ms <= 0)\n 437 | r = CURLE_OPERATION_TIMEDOUT;\n 438 | else\n 439 | r = Curl_cond_timedwait(&tpool->await, &tpool->lock,\n 440 | (uint32_t)remain_ms);","lines":6,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/thrdpool.c#L435-L440"}]},
{"id":"LOGIC_3","type":"logic_error","type_label":"Logic Error","severity":"medium","difficulty":"very_easy","description":"Curl_range() does not reject \"X-\" ranges where the start offset equals\nCURL_OFF_T_MAX. When the range is just \"X-\" (no end), `from` is parsed with a\ncap of CURL_OFF_T_MAX and stored directly into resume_from with no further\nvalidation. More importantly, the \"-Y\" branch reads `to` but the \"X-Y\" branch\ncomputes `totalsize = to - from` and only rejects when `totalsize ==\nCURL_OFF_T_MAX`; it does not reject the case `from == to` producing\nmaxdownload == 1 vs. an off-by-one in the inclusive-byte accounting is fine,\nbut a `to` value parsed equal to CURL_OFF_T_MAX combined with from==0 yields\ntotalsize==CURL_OFF_T_MAX which is rejected, yet from==1,to==CURL_OFF_T_MAX\nyields totalsize==CURL_OFF_T_MAX-1 and maxdownload = CURL_OFF_T_MAX (no\noverflow) — acceptable. The genuine issue: the final DEBUGF at lines 82-84\nprints `to` even in the \"X-\" branch where `to` was never assigned (the\ncurlx_str_number for `to` failed), reading an uninitialized variable in debug\nbuilds.","fix":"Initialize `to` to 0 (or a sentinel) at declaration so the trailing DEBUGF does not read an uninitialized value in the 'X-' (no second number) case.","locations":[{"ref":"lib/curl_range.c:48-53","code":" 48 | if(curlx_str_number(&p, &to, CURL_OFF_T_MAX)) {\n 49 | /* no second number */\n 50 | /* X - */\n 51 | data->state.resume_from = from;\n 52 | DEBUGF(infof(data, \"RANGE %\" FMT_OFF_T \" to end of file\", from));\n 53 | }","lines":6,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/curl_range.c#L48-L53"},{"ref":"lib/curl_range.c:82-84","code":" 82 | DEBUGF(infof(data, \"range-download from %\" FMT_OFF_T\n 83 | \" to %\" FMT_OFF_T \", totally %\" FMT_OFF_T \" bytes\",\n 84 | from, to, data->req.maxdownload));","lines":3,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/curl_range.c#L82-L84"}]},
{"id":"LOGIC_4","type":"logic_error","type_label":"Logic Error","severity":"medium","difficulty":"very_easy","description":"ftp_parse_url_path() FTPFILE_MULTICWD branch computes `clen = spos - curPos`\nwhere spos = strchr(curPos, '/'). After processing the last directory slash,\n`curPos = spos + 1`. The loop count `dirAlloc` equals numof_slashes(rawPath).\nIf the path contains a '/' but strchr returns NULL on a later iteration\n(impossible given dirAlloc was derived from the same string, unless the string\nwas mutated), spos would be NULL and `clen = NULL - curPos` is undefined. The\nstrings are not mutated between numof_slashes() and the loop, so spos is\nnon-NULL each iteration; this is safe but depends on the invariant that the\ndecoded path is identical to what numof_slashes counted. Worth an assertion.","fix":"Add `DEBUGASSERT(spos)` after strchr in the MULTICWD loop to document and catch any future violation of the slash-count invariant.","locations":[{"ref":"lib/ftp.c:263-299","code":" 263 | case FTPFILE_MULTICWD: {\n 264 | /* current position: begin of next path component */\n 265 | const char *curPos = rawPath;\n 266 | \n 267 | /* number of entries to allocate for the 'dirs' array */\n 268 | size_t dirAlloc = numof_slashes(rawPath);\n 269 | \n 270 | if(dirAlloc >= FTP_MAX_DIR_DEPTH)\n 271 | /* suspiciously deep directory hierarchy */\n 272 | return CURLE_URL_MALFORMAT;\n 273 | \n 274 | if(dirAlloc) {\n 275 | ftpc->dirs = curlx_calloc(dirAlloc, sizeof(ftpc->dirs[0]));\n 276 | if(!ftpc->dirs)\n 277 | return CURLE_OUT_OF_MEMORY;\n 278 | \n 279 | /* parse the URL path into separate path components */\n 280 | while(dirAlloc--) {\n 281 | const char *spos = strchr(curPos, '/');\n 282 | size_t clen = spos - curPos;\n 283 | \n 284 | /* path starts with a slash: add that as a directory */\n 285 | if(!clen && (ftpc->dirdepth == 0))\n 286 | ++clen;\n 287 | \n 288 | /* we skip empty path components, like \"x//y\" since the FTP command\n 289 | CWD requires a parameter and a non-existent parameter a) does not\n 290 | work on many servers and b) has no effect on the others. */\n 291 | if(clen) {\n 292 | ftpc->dirs[ftpc->dirdepth].start = (int)(curPos - rawPath);\n 293 | ftpc->dirs[ftpc->dirdepth].len = (int)clen;\n 294 | ftpc->dirdepth++;\n 295 | }\n 296 | curPos = spos + 1;\n 297 | }\n 298 | }\n 299 | fileName = curPos; /* the rest is the filename (or empty) */","lines":37,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/ftp.c#L263-L299"}]},
{"id":"LOGIC_5","type":"logic_error","type_label":"Logic Error","severity":"medium","difficulty":"easy","description":"match_pasv_6nums() / ftp_state_pasv_resp(): for a 227 PASV reply the loop scans\n`str` byte-by-byte calling match_pasv_6nums until it matches or hits NUL. str is\ncurlx_dyn_ptr(&pp->recvbuf)+4 and is NUL-terminated, so the scan is bounded.\nHowever ftp_state_pasv_resp() reads `str` from the recvbuf without verifying the\nbuffer actually has at least 4 bytes; for a crafted short final line that still\nsatisfied LASTLINE (which requires >=4 chars including code+space), +4 points at\nthe space/terminator region. This is bounded by the NUL but means the +4 offset\nassumes a minimum length that is only loosely guaranteed by the framing macro.","fix":"Assert/verify curlx_dyn_len(&pp->recvbuf) >= 4 before indexing recvbuf+4, consistently across the *_resp handlers that use the +4 offset.","locations":[{"ref":"lib/ftp.c:2055-2056","code":" 2055 | const char *str = curlx_dyn_ptr(&pp->recvbuf) + 4; /* start on the first\n 2056 | letter */","lines":2,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/ftp.c#L2055-L2056"},{"ref":"lib/ftp.c:2100-2109","code":" 2100 | while(*str) {\n 2101 | if(match_pasv_6nums(str, ip))\n 2102 | break;\n 2103 | str++;\n 2104 | }\n 2105 | \n 2106 | if(!*str) {\n 2107 | failf(data, \"Could not interpret the 227-response\");\n 2108 | return CURLE_FTP_WEIRD_227_FORMAT;\n 2109 | }","lines":10,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/ftp.c#L2100-L2109"}]},
{"id":"LOGIC_6","type":"logic_error","type_label":"Logic Error","severity":"medium","difficulty":"very_easy","description":"ftp_pwd_resp() uses `result` (declared at line 3009 without initialization) on\npaths where it may not have been assigned before being tested. Inside the\nquote-doubling loop every iteration assigns result via curlx_dyn_addn before the\n`if(result)` check, so the loop body is safe. However, after the loop, when\n`entry_extracted` is true, the code calls Curl_pp_sendf and assigns result\n(3076) only inside the `if(!ftpc->server_os && dir[0] != '/')` branch; on the\nother branch result is never assigned but also never read afterward. The\ngenuine latent risk is that if the for-loop at 3033 never executes its body\n(e.g. the character right after the opening quote is immediately a closing quote\nwith empty content), entry_extracted stays FALSE and result remains\nuninitialized, yet result is not read on that path — so currently benign but\nfragile.","fix":"Initialize `CURLcode result = CURLE_OK;` at declaration in ftp_pwd_resp() to remove the reliance on control-flow guarantees.","locations":[{"ref":"lib/ftp.c:3023","code":" 3023 | CURLcode result;","lines":1,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/ftp.c#L3023"},{"ref":"lib/ftp.c:3045-3074","code":" 3045 | if('\\\"' == *ptr) {\n 3046 | /* it started good */\n 3047 | for(ptr++; *ptr; ptr++) {\n 3048 | if('\\\"' == *ptr) {\n 3049 | if('\\\"' == ptr[1]) {\n 3050 | /* \"quote-doubling\" */\n 3051 | result = curlx_dyn_addn(&out, &ptr[1], 1);\n 3052 | ptr++;\n 3053 | }\n 3054 | else {\n 3055 | /* end of path */\n 3056 | if(curlx_dyn_len(&out))\n 3057 | entry_extracted = TRUE;\n 3058 | break; /* get out of this loop */\n 3059 | }\n 3060 | }\n 3061 | else {\n 3062 | if(ISCNTRL(*ptr)) {\n 3063 | /* control characters have no business in a path */\n 3064 | curlx_dyn_free(&out);\n 3065 | return CURLE_WEIRD_SERVER_REPLY;\n 3066 | }\n 3067 | result = curlx_dyn_addn(&out, ptr, 1);\n 3068 | }\n 3069 | if(result) {\n 3070 | curlx_dyn_free(&out);\n 3071 | return result;\n 3072 | }\n 3073 | }\n 3074 | }","lines":30,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/ftp.c#L3045-L3074"}]},
{"id":"LOGIC_7","type":"logic_error","type_label":"Logic Error","severity":"medium","difficulty":"medium","description":"imap_endofresp() detects a tagged response by matching imapc->resptag. During the initial connect the tag is set to the literal '*' (imap_connect() calls curlx_strcopy(imapc->resptag, ..., '*')). After that, the untagged-response check `!memcmp(\"* \", line, 2)` and the tagged-response check (id=='*') can both fire for a server greeting line beginning with '* '. The tagged branch runs first and, for a greeting like '* OK ...', sets *resp=IMAP_RESP_OK and returns TRUE, which is the intended SERVERGREET handling; however for a greeting such as '* BYE ...' the tagged branch returns IMAP_RESP_NOT_OK whereas the untagged path would have returned FALSE for non-greeting states. The overlap of the '*' tag with the untagged marker is fragile: any genuinely untagged line during SERVERGREET is classified as a tagged response. Confirm the greeting state relies on this; if a multiline untagged greeting arrives it is mis-parsed as tagged.","fix":"Use a tag value during SERVERGREET that cannot collide with the untagged '* ' marker (e.g. an empty/sentinel tag), or special-case IMAP_SERVERGREET before the generic tagged-response comparison.","locations":[{"ref":"lib/imap.c:327-342","code":" 327 | /* Do we have a tagged command response? */\n 328 | id = imapc->resptag;\n 329 | id_len = strlen(id);\n 330 | if(len >= id_len + 1 && !memcmp(id, line, id_len) && line[id_len] == ' ') {\n 331 | line += id_len + 1;\n 332 | len -= id_len + 1;\n 333 | \n 334 | if(len >= 2 && !memcmp(line, \"OK\", 2))\n 335 | *resp = IMAP_RESP_OK;\n 336 | else if(len >= 7 && !memcmp(line, \"PREAUTH\", 7))\n 337 | *resp = IMAP_RESP_PREAUTH;\n 338 | else\n 339 | *resp = IMAP_RESP_NOT_OK;\n 340 | \n 341 | return TRUE;\n 342 | }","lines":16,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/imap.c#L327-L342"},{"ref":"lib/imap.c:1980-1981","code":" 1980 | /* Start off with an response id of '*' */\n 1981 | curlx_strcopy(imapc->resptag, sizeof(imapc->resptag), STRCONST(\"*\"));","lines":2,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/imap.c#L1980-L1981"}]},
{"id":"LOGIC_8","type":"logic_error","type_label":"Logic Error","severity":"medium","difficulty":"medium","description":"tftp_parse_option_ack() rejects a server-offered blksize that is larger than\nthe *originally requested* blksize (state->requested_blksize), but the receive\nbuffer was actually allocated to need_blksize = max(requested, 512) in\ntftp_connect(). When the user requests a blksize smaller than 512 (e.g.\nCURLOPT_TFTP_BLKSIZE=8), requested_blksize is that small value while the\nbuffers hold 512+4 bytes. A server offering, say, 400 is then rejected even\nthough the buffer could hold it; conversely the comment at lines 299-301 claims\nthe rejection is because realloc is needed, which is misleading since the\nbuffer is already 512+4. The result is interoperability loss (valid OACKs\nrejected) rather than memory unsafety.","fix":"Compare against the actually-allocated buffer capacity (need_blksize) rather than requested_blksize, or document that small requested blksizes intentionally cap the negotiated size.","locations":[{"ref":"lib/tftp.c:298-305","code":" 298 | else if(blksize > state->requested_blksize) {\n 299 | /* could realloc pkt buffers here, but the spec does not call out\n 300 | * support for the server requesting a bigger blksize than the client\n 301 | * requests */\n 302 | failf(data, \"server requested blksize larger than allocated (%\"\n 303 | CURL_FORMAT_CURL_OFF_T \")\", blksize);\n 304 | return CURLE_TFTP_ILLEGAL;\n 305 | }","lines":8,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/tftp.c#L298-L305"},{"ref":"lib/tftp.c:919-941","code":" 919 | /* alloc pkt buffers based on specified blksize */\n 920 | if(data->set.tftp_blksize)\n 921 | /* range checked when set */\n 922 | blksize = (int)data->set.tftp_blksize;\n 923 | \n 924 | need_blksize = blksize;\n 925 | /* default size is the fallback when no OACK is received */\n 926 | if(need_blksize < TFTP_BLKSIZE_DEFAULT)\n 927 | need_blksize = TFTP_BLKSIZE_DEFAULT;\n 928 | \n 929 | if(!state->rpacket.data) {\n 930 | state->rpacket.data = curlx_calloc(1, need_blksize + 2 + 2);\n 931 | \n 932 | if(!state->rpacket.data)\n 933 | return CURLE_OUT_OF_MEMORY;\n 934 | }\n 935 | \n 936 | if(!state->spacket.data) {\n 937 | state->spacket.data = curlx_calloc(1, need_blksize + 2 + 2);\n 938 | \n 939 | if(!state->spacket.data)\n 940 | return CURLE_OUT_OF_MEMORY;\n 941 | }","lines":23,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/tftp.c#L919-L941"}]},
{"id":"LOGIC_9","type":"logic_error","type_label":"Logic Error","severity":"medium","difficulty":"medium","description":"sftp_download_stat() (libssh2): when stat returns success but with no SIZE flag or filesize==0, the code sets data->req.size/maxdownload = -1 and forces attrs.filesize = 0 (line 1302). Immediately afterwards, if data->state.resume_from is set, the resume logic at lines 1326-1351 uses attrs.filesize (now forced to 0) to validate the resume offset and to compute data->req.size = attrs.filesize - resume_from. For a positive resume_from on such a server, this both rejects the resume with a bogus 'beyond file size (0)' error and, for the negative case, can compute a negative req.size. The override of attrs.filesize to 0 for the 'unknown size' case is incompatible with the subsequent resume arithmetic that assumes attrs.filesize is the real size.","fix":"Skip the resume_from arithmetic (or treat size as unknown) when the server did not report a real file size, instead of running it against a forced filesize of 0.","locations":[{"ref":"lib/vssh/libssh2.c:1258-1320","code":" 1258 | if(rc ||\n 1259 | !(attrs.flags & LIBSSH2_SFTP_ATTR_SIZE) ||\n 1260 | (attrs.filesize == 0)) {\n 1261 | /*\n 1262 | * libssh2_sftp_open() did not return an error, so maybe the server\n 1263 | * does not support stat()\n 1264 | * OR the server does not return a file size with a stat()\n 1265 | * OR file size is 0\n 1266 | */\n 1267 | data->req.size = -1;\n 1268 | data->req.maxdownload = -1;\n 1269 | Curl_pgrsSetDownloadSize(data, -1);\n 1270 | attrs.filesize = 0; /* might be uninitialized but is read below */\n 1271 | }\n 1272 | else {\n 1273 | curl_off_t size = attrs.filesize;\n 1274 | \n 1275 | if(size < 0) {\n 1276 | failf(data, \"Bad file size (%\" FMT_OFF_T \")\", size);\n 1277 | return CURLE_BAD_DOWNLOAD_RESUME;\n 1278 | }\n 1279 | if(data->state.use_range) {\n 1280 | curl_off_t from;\n 1281 | CURLcode result = Curl_ssh_range(data, data->state.range, size,\n 1282 | &from, &size);\n 1283 | if(result)\n 1284 | return result;\n 1285 | \n 1286 | libssh2_sftp_seek64(sshc->sftp_handle, (libssh2_uint64_t)from);\n 1287 | }\n 1288 | data->req.size = size;\n 1289 | data->req.maxdownload = size;\n 1290 | Curl_pgrsSetDownloadSize(data, size);\n 1291 | }\n 1292 | \n 1293 | /* We can resume if we can seek to the resume position */\n 1294 | if(data->state.resume_from) {\n 1295 | if(data->state.resume_from < 0) {\n 1296 | /* We are supposed to download the last abs(from) bytes */\n 1297 | if((curl_off_t)attrs.filesize < -data->state.resume_from) {\n 1298 | failf(data, \"Offset (%\" FMT_OFF_T \") was beyond file size (%\"\n 1299 | FMT_OFF_T \")\",\n 1300 | data->state.resume_from, (curl_off_t)attrs.filesize);\n 1301 | return CURLE_BAD_DOWNLOAD_RESUME;\n 1302 | }\n 1303 | /* download from where? */\n 1304 | data->state.resume_from += attrs.filesize;\n 1305 | }\n 1306 | else {\n 1307 | if((curl_off_t)attrs.filesize < data->state.resume_from) {\n 1308 | failf(data, \"Offset (%\" FMT_OFF_T\n 1309 | \") was beyond file size (%\" FMT_OFF_T \")\",\n 1310 | data->state.resume_from, (curl_off_t)attrs.filesize);\n 1311 | return CURLE_BAD_DOWNLOAD_RESUME;\n 1312 | }\n 1313 | }\n 1314 | /* Now store the number of bytes we are expected to download */\n 1315 | data->req.size = attrs.filesize - data->state.resume_from;\n 1316 | data->req.maxdownload = attrs.filesize - data->state.resume_from;\n 1317 | Curl_pgrsSetDownloadSize(data, attrs.filesize - data->state.resume_from);\n 1318 | libssh2_sftp_seek64(sshc->sftp_handle,\n 1319 | (libssh2_uint64_t)data->state.resume_from);\n 1320 | }","lines":63,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vssh/libssh2.c#L1258-L1320"}]},
{"id":"LOGIC_10","type":"logic_error","type_label":"Logic Error","severity":"medium","difficulty":"very_easy","description":"In config2setopts(), the guard meant to limit REFERER/USERAGENT setup to HTTP(S)/RTSP transfers tests `if(proto_http || proto_rtsp)`. Both `proto_http` and `proto_rtsp` are global protocol-token pointers that are non-NULL whenever the corresponding protocol is built in (independent of the current URL's scheme), so this condition is effectively always true. The intent was clearly `use_proto == proto_http || use_proto == proto_rtsp` (compare with the other *_setopts helpers in this same file which correctly compare `use_proto`). As written, CURLOPT_REFERER and CURLOPT_USERAGENT are set for every transfer regardless of protocol.","fix":"Change the condition to compare the actual scheme of this transfer, e.g. `if(use_proto == proto_http || use_proto == proto_https || use_proto == proto_rtsp)`.","locations":[{"ref":"src/config2setopts.c:972-976","code":" 972 | if(proto_http || proto_rtsp) {\n 973 | MY_SETOPT_STR(curl, CURLOPT_REFERER, config->referer);\n 974 | MY_SETOPT_STR(curl, CURLOPT_USERAGENT, config->useragent ?\n 975 | config->useragent : CURL_NAME \"/\" CURL_VERSION);\n 976 | }","lines":5,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/src/config2setopts.c#L972-L976"}]},
{"id":"LOGIC_11","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"medium","description":"In cf_socket_send() the DEBUGBUILD partial-write simulation reduces `len` but\nthe EWOULDBLOCK simulation that runs first restores cf->conn->sock[sockindex]\nand returns CURLE_AGAIN correctly. The partial-write branch checks\n`cf->cft != &Curl_cft_udp`; however cf_socket_send is also used for the UNIX\nfilter (Curl_cft_unix) and the TCP-accept filter. For UNIX/accept the partial\nwrite simulation applies (only UDP is excluded). That is presumably acceptable\nfor stream sockets, but the guard is by-filter-identity rather than by\nsocktype, so a future stream filter reusing cf_socket_send is silently included.\nMinor maintainability concern, not a runtime bug.","fix":"Gate the simulation on the socket type (SOCK_STREAM) rather than on cf->cft != &Curl_cft_udp, to be robust to new filters.","locations":[{"ref":"lib/cf-socket.c:1588-1594","code":" 1588 | if(cf->cft != &Curl_cft_udp && ctx->wpartial_percent > 0 && len > 8) {\n 1589 | len = len * ctx->wpartial_percent / 100;\n 1590 | if(!len)\n 1591 | len = 1;\n 1592 | CURL_TRC_CF(data, cf, \"send(len=%zu) SIMULATE partial write of %zu bytes\",\n 1593 | orig_len, len);\n 1594 | }","lines":7,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/cf-socket.c#L1588-L1594"}]},
{"id":"LOGIC_12","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"easy","description":"In parse_netscape() field 2 (path), the test `strncmp(\"TRUE\", ptr, len) && strncmp(\"FALSE\", ptr, len)` uses the field length `len` as the strncmp limit. A path field whose value is a strict prefix of \"TRUE\"/\"FALSE\" (e.g. the 2-byte string \"TR\" or single \"F\") compares equal under strncmp with that short len and is therefore misclassified as a boolean field, causing the real path to be discarded and a default \"/\" path used plus a spurious extra field count. Crafted/odd cookie files can thus get their path silently dropped.","fix":"Compare with exact length, e.g. require `!(len == 4 && !strncmp(\"TRUE\", ptr, 4)) && !(len == 5 && !strncmp(\"FALSE\", ptr, 5))`, so only the full tokens TRUE/FALSE are treated as booleans.","locations":[{"ref":"lib/cookie.c:724-740","code":" 724 | case 2:\n 725 | /* The file format allows the path field to remain not filled in */\n 726 | if(strncmp(\"TRUE\", ptr, len) && strncmp(\"FALSE\", ptr, len)) {\n 727 | /* only if the path does not look like a boolean option! */\n 728 | co->path = sanitize_cookie_path(ptr, len);\n 729 | if(!co->path)\n 730 | return CURLE_OUT_OF_MEMORY;\n 731 | break;\n 732 | }\n 733 | else {\n 734 | /* this does not look like a path, make one up! */\n 735 | co->path = curlx_strdup(\"/\");\n 736 | if(!co->path)\n 737 | return CURLE_OUT_OF_MEMORY;\n 738 | }\n 739 | fields++; /* add a field and fall down to secure */\n 740 | FALLTHROUGH();","lines":17,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/cookie.c#L724-L740"}]},
{"id":"LOGIC_13","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"medium","description":"In the fnmatch fallback parser, setcharorrange() reads through the pattern with\npost-increment dereferences without bounds checks: `unsigned char endrange =\n*p++; if(endrange == '\\\\') endrange = *p++;`. For a pattern ending in a\ndangling range like \"[a-\\\" the code reads the NUL terminator into endrange,\nthen (because endrange=='\\\\' is false for NUL) proceeds; but for \"[a-\" it\nreads the NUL as endrange (value 0), and the subsequent `endrange >= c` test\nfails so the range is not applied. The reads stay within the NUL-terminated\nbuffer by one byte at most, but the function can advance `*pp` (= p) past the\nNUL terminator when a backslash escape consumes it, after which the caller's\nloop may dereference beyond the string.","fix":"After reading each character in setcharorrange(), stop (do not advance or apply the range) if the byte read is the NUL terminator, so the pattern pointer never moves past end-of-string.","locations":[{"ref":"lib/curl_fnmatch.c:137-156","code":" 137 | static void setcharorrange(const unsigned char **pp, unsigned char *charset)\n 138 | {\n 139 | const unsigned char *p = (*pp)++;\n 140 | unsigned char c = *p++;\n 141 | \n 142 | charset[c] = 1;\n 143 | if(ISALNUM(c) && *p++ == '-') {\n 144 | char_class cc = charclass(c);\n 145 | unsigned char endrange = *p++;\n 146 | \n 147 | if(endrange == '\\\\')\n 148 | endrange = *p++;\n 149 | if(endrange >= c && charclass(endrange) == cc) {\n 150 | while(c++ != endrange)\n 151 | if(charclass(c) == cc) /* Chars in class may be not consecutive. */\n 152 | charset[c] = 1;\n 153 | *pp = p;\n 154 | }\n 155 | }\n 156 | }","lines":20,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/curl_fnmatch.c#L137-L156"}]},
{"id":"LOGIC_14","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"medium","description":"ftp_state_use_pasv() / ftp_pp_statemachine FTP_PASV handling: when the EPSV\n(count1==0) parse succeeds at lines 2074-2085 but the inner `if(sep && ...)`\ncondition is false, `ptr` is set to NULL (line 2087) and then the `if(!ptr)`\nblock returns CURLE_FTP_WEIRD_PASV_REPLY. That is correct. However, when\ncount1==0 and ftpcode is NOT 229 (EPSV failed) the code at lines 2137-2140\ncalls ftp_epsv_disable to fall back to PASV — but if count1==0 and ftpcode==229\nyet the '(' is missing, it returns WEIRD_PASV without attempting the PASV\nfallback, so a server that sends a 229 with a non-standard body that curl\ncannot parse aborts instead of retrying with PASV.","fix":"On a 229 reply that fails to parse, consider falling back to ftp_epsv_disable() (PASV) rather than failing outright, mirroring the EPSV-connect-failure path.","locations":[{"ref":"lib/ftp.c:2057-2084","code":" 2057 | if((ftpc->count1 == 0) &&\n 2058 | (ftpcode == 229)) {\n 2059 | /* positive EPSV response */\n 2060 | const char *ptr = strchr(str, '(');\n 2061 | if(ptr) {\n 2062 | char sep;\n 2063 | ptr++;\n 2064 | /* |||12345| */\n 2065 | sep = ptr[0];\n 2066 | if(sep && (ptr[1] == sep) && (ptr[2] == sep) && ISDIGIT(ptr[3])) {\n 2067 | const char *p = &ptr[3];\n 2068 | curl_off_t num;\n 2069 | if(curlx_str_number(&p, &num, 0xffff) || (*p != sep)) {\n 2070 | failf(data, \"Illegal port number in EPSV reply\");\n 2071 | return CURLE_FTP_WEIRD_PASV_REPLY;\n 2072 | }\n 2073 | newport = (unsigned short)num;\n 2074 | result = ftp_control_addr_dup(data, &newhost);\n 2075 | if(result)\n 2076 | return result;\n 2077 | }\n 2078 | else\n 2079 | ptr = NULL;\n 2080 | }\n 2081 | if(!ptr) {\n 2082 | failf(data, \"Weirdly formatted EPSV reply\");\n 2083 | return CURLE_FTP_WEIRD_PASV_REPLY;\n 2084 | }","lines":28,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/ftp.c#L2057-L2084"},{"ref":"lib/ftp.c:2129-2136","code":" 2129 | else if(ftpc->count1 == 0) {\n 2130 | /* EPSV failed, move on to PASV */\n 2131 | return ftp_epsv_disable(data, ftpc, conn);\n 2132 | }\n 2133 | else {\n 2134 | failf(data, \"Bad PASV/EPSV response: %03d\", ftpcode);\n 2135 | return CURLE_FTP_WEIRD_PASV_REPLY;\n 2136 | }","lines":8,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/ftp.c#L2129-L2136"}]},
{"id":"LOGIC_15","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"easy","description":"In parse_unix_hlinks() (PL_UNIX_HLINKS_NUMBER) the link count is parsed with\n`curlx_str_number(&p, &hlinks, LONG_MAX)` and then stored as\n`info.hardlinks = (long)hlinks`. hlinks is curl_off_t. On platforms where long\nis 32-bit but curl_off_t is 64-bit, LONG_MAX caps the value so the cast is safe;\nbut the cap argument LONG_MAX is passed to a function expecting curl_off_t,\nwhich on LLP64 (Windows 64-bit, long==32-bit) limits hardlink counts to 2^31-1\nsilently — fine — yet info.hardlinks is itself a long, so values are bounded\ncorrectly. No overflow, but the mix of long/curl_off_t here is inconsistent with\nthe size-handling elsewhere (size uses CURL_OFF_T_MAX).","fix":"Use a curl_off_t field or document the deliberate long-typed hardlink count; keep the cap consistent with the destination type.","locations":[{"ref":"lib/ftplistparser.c:491-493","code":" 491 | if(!curlx_str_number(&p, &hlinks, LONG_MAX)) {\n 492 | parser->file_data->info.flags |= CURLFINFOFLAG_KNOWN_HLINKCOUNT;\n 493 | parser->file_data->info.hardlinks = (long)hlinks;","lines":3,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/ftplistparser.c#L491-L493"}]},
{"id":"LOGIC_16","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"very_easy","description":"start_req() scans the request line backwards for the SP separating TARGET and HTTP-VERSION starting at index parser->line_len: `for(i = parser->line_len; i > m_len; --i) { if(parser->line[i] == ' ') ... }`. The first iteration reads parser->line[parser->line_len], which is one byte past the logical (trimmed) line content. parser->line_len has already been reduced by trim_line() to exclude the trailing CR/LF, so line[line_len] is the stripped CR/LF (or, when scratch is used, the dynbuf NUL terminator). It is within the physical buffer so not an out-of-bounds read, but the loop reads a byte it should not consider; the intended start index is line_len-1.","fix":"Start the backward scan at `i = parser->line_len - 1` (guarding line_len != 0) so the loop only inspects bytes that belong to the trimmed line.","locations":[{"ref":"lib/http1.c:160-168","code":" 160 | /* URL may contain spaces so scan backwards */\n 161 | for(i = parser->line_len; i > m_len; --i) {\n 162 | if(parser->line[i] == ' ') {\n 163 | hv = &parser->line[i + 1];\n 164 | hv_len = parser->line_len - i;\n 165 | target_len = (hv - target) - 1;\n 166 | break;\n 167 | }\n 168 | }","lines":9,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/http1.c#L160-L168"}]},
{"id":"LOGIC_17","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"medium","description":"Curl_md4it truncates the input length with curlx_uztoui(len) and passes it to my_md4_update whose len parameter is 'unsigned long'. For the pure-C / nettle / wolfSSL backends update accepts the full length, but the truncation to unsigned int happens before the call, so on a 64-bit platform an input >= 4GiB would silently hash only the low 32 bits of the length's worth of data. Unlike Curl_md5it/Curl_sha256it/Curl_hmacit which loop in UINT_MAX chunks, Curl_md4it performs a single truncated call. In practice NTLM inputs are tiny, but the asymmetry is a latent correctness bug.","fix":"Mirror Curl_md5it: loop calling my_md4_update with CURLMIN(len, UINT_MAX)-sized chunks instead of a single curlx_uztoui(len) truncation.","locations":[{"ref":"lib/md4.c:474-483","code":" 474 | CURLcode Curl_md4it(unsigned char *output,\n 475 | const unsigned char *input, const size_t len)\n 476 | {\n 477 | my_md4_ctx ctx;\n 478 | if(!my_md4_init(&ctx))\n 479 | return CURLE_FAILED_INIT;\n 480 | my_md4_update(&ctx, input, curlx_uztoui(len));\n 481 | my_md4_final(output, &ctx);\n 482 | return CURLE_OK;\n 483 | }","lines":10,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/md4.c#L474-L483"}]},
{"id":"LOGIC_18","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"easy","description":"Curl_md5it/Curl_sha256it/Curl_hmacit chunk large inputs with a do/while loop using ilen = CURLMIN(len, UINT_MAX). On a platform where size_t is 32-bit (UINT_MAX == SIZE_MAX), if len == UINT_MAX the first iteration consumes ilen = UINT_MAX, len becomes 0, loop ends - fine. But the chunking is only meaningful where size_t > 32-bit; on 32-bit size_t the CURLMIN can never split and ilen could equal len exactly. The construct is correct, however the do/while form means a zero-length input still performs one update(0) call; while harmless for these backends it relies on every backend's update tolerating a 0-length call, which is an implicit contract not asserted.","fix":"Use a while(len) loop (or guard the zero-length case) so a 0-length input performs zero update calls, removing the implicit requirement that every hash backend's update handles len==0.","locations":[{"ref":"lib/md5.c:558-575","code":" 558 | CURLcode Curl_md5it(unsigned char *output,\n 559 | const unsigned char *input, size_t len)\n 560 | {\n 561 | CURLcode result;\n 562 | my_md5_ctx ctx;\n 563 | \n 564 | result = my_md5_init(&ctx);\n 565 | if(!result) {\n 566 | do {\n 567 | unsigned int ilen = (unsigned int)CURLMIN(len, UINT_MAX);\n 568 | my_md5_update(&ctx, input, ilen);\n 569 | input += ilen;\n 570 | len -= ilen;\n 571 | } while(len);\n 572 | my_md5_final(output, &ctx);\n 573 | }\n 574 | return result;\n 575 | }","lines":18,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/md5.c#L558-L575"},{"ref":"lib/sha256.c:484-501","code":" 484 | CURLcode Curl_sha256it(unsigned char *output, const unsigned char *input,\n 485 | size_t len)\n 486 | {\n 487 | CURLcode result;\n 488 | my_sha256_ctx ctx;\n 489 | \n 490 | result = my_sha256_init(&ctx);\n 491 | if(!result) {\n 492 | do {\n 493 | unsigned int ilen = (unsigned int)CURLMIN(len, UINT_MAX);\n 494 | my_sha256_update(&ctx, input, ilen);\n 495 | len -= ilen;\n 496 | input += ilen;\n 497 | } while(len);\n 498 | my_sha256_final(output, &ctx);\n 499 | }\n 500 | return result;\n 501 | }","lines":18,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/sha256.c#L484-L501"},{"ref":"lib/hmac.c:156-162","code":" 156 | /* Update the digest with the given challenge */\n 157 | do {\n 158 | unsigned int ilen = (unsigned int)CURLMIN(datalen, UINT_MAX);\n 159 | Curl_HMAC_update(ctxt, data, ilen);\n 160 | datalen -= ilen;\n 161 | data += ilen;\n 162 | } while(datalen);","lines":7,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/hmac.c#L156-L162"}]},
{"id":"LOGIC_19","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"medium","description":"In the MQTT CONNECT packet builder, add_passwd() sets only the password flag (0x40) in the CONNECT flags byte, and is called whenever a password is present even if no username was supplied (ulen == 0, so add_user() / the username flag 0x80 is never set). MQTT 3.1.1 section 3.1.2.9 mandates that the password flag MUST NOT be set unless the username flag is also set; a spec-compliant broker is entitled to reject such a CONNECT. This is a protocol-correctness issue (a password-only URL produces a malformed CONNECT), not a memory-safety problem.","fix":"Reject (or ignore) a password supplied without a username, or set the username flag/length appropriately, so that the password flag is never sent without the username flag as required by the MQTT specification.","locations":[{"ref":"lib/mqtt.c:336-344","code":" 336 | /* if passwd was provided, add it to the packet */\n 337 | if(plen) {\n 338 | rc = add_passwd(creds->passwd, plen, packet, start_pwd, remain_pos);\n 339 | if(rc) {\n 340 | failf(data, \"Password too long: [%zu]\", plen);\n 341 | result = CURLE_WEIRD_SERVER_REPLY;\n 342 | goto end;\n 343 | }\n 344 | }","lines":9,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/mqtt.c#L336-L344"},{"ref":"lib/mqtt.c:188-205","code":" 188 | /* add the passwd to the CONNECT packet */\n 189 | static int add_passwd(const char *passwd, const size_t plen,\n 190 | char *pkt, const size_t start, int remain_pos)\n 191 | {\n 192 | /* magic number that need to be set properly */\n 193 | const size_t conn_flags_pos = remain_pos + 8;\n 194 | if(plen > 0xffff)\n 195 | return 1;\n 196 | \n 197 | /* set password flag */\n 198 | pkt[conn_flags_pos] |= 0x40;\n 199 | \n 200 | /* length of password provided */\n 201 | pkt[start] = (char)((plen >> 8) & 0xFF);\n 202 | pkt[start + 1] = (char)(plen & 0xFF);\n 203 | memcpy(&pkt[start + 2], passwd, plen);\n 204 | return 0;\n 205 | }","lines":18,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/mqtt.c#L188-L205"}]},
{"id":"LOGIC_20","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"easy","description":"mqtt_verify_suback() rejects a SUBACK whose packet id or return code do not match, but unlike mqtt_verify_connack() (which calls failf() on mismatch) it silently returns CURLE_WEIRD_SERVER_REPLY with no failf()/infof() diagnostic. For a user this surfaces as a generic \"weird server reply\" with no indication that the SUBACK packet id or QoS byte was the problem, making subscription failures hard to diagnose.","fix":"Add a failf(data, \"SUBACK mismatch ...\") before setting result = CURLE_WEIRD_SERVER_REPLY, mirroring the diagnostic style used in mqtt_verify_connack().","locations":[{"ref":"lib/mqtt.c:533-539","code":" 533 | if(((unsigned char)ptr[0]) != ((mqtt->packetid >> 8) & 0xff) ||\n 534 | ((unsigned char)ptr[1]) != (mqtt->packetid & 0xff) ||\n 535 | ptr[2] != 0x00) {\n 536 | curlx_dyn_reset(&mq->recvbuf);\n 537 | result = CURLE_WEIRD_SERVER_REPLY;\n 538 | goto fail;\n 539 | }","lines":7,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/mqtt.c#L533-L539"}]},
{"id":"LOGIC_21","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"medium","description":"datecheck() validates day-of-month only against the universal upper bound 31 (`w->mday > 31`), independent of month, so dates like \"Feb 31\" or \"Apr 31\" parse as valid and are fed to time2epoch(), which uses cumulative_days[] plus the raw mday. The result is a silently wrong epoch (the overflow rolls into the next month) rather than a parse failure. For Set-Cookie/Alt-Svc/HSTS expiry parsing this means a malformed date can yield an unexpected (often slightly larger) expiry time instead of being rejected. The looseness is historically intentional, but worth noting as a correctness gap in security-relevant expiry handling.","fix":"If strict validation is desired, range-check mday against the actual days in w->mon (accounting for leap years for February) before accepting the date; otherwise document the intentional leniency explicitly.","locations":[{"ref":"lib/parsedate.c:485-503","code":" 485 | static int datecheck(struct when *w)\n 486 | {\n 487 | if(w->sec == -1)\n 488 | w->sec = w->min = w->hour = 0; /* no time, make it zero */\n 489 | \n 490 | if((w->mday == -1) || (w->mon == -1) || (w->year == -1))\n 491 | /* lacks vital info, fail */\n 492 | return PARSEDATE_FAIL;\n 493 | \n 494 | /* The Gregorian calendar was introduced 1582 */\n 495 | else if(w->year < 1583)\n 496 | return PARSEDATE_FAIL;\n 497 | \n 498 | else if((w->mday > 31) || (w->mon > 11) || (w->hour > 23) ||\n 499 | (w->min > 59) || (w->sec > 60))\n 500 | return PARSEDATE_FAIL; /* clearly an illegal date */\n 501 | \n 502 | return PARSEDATE_OK;\n 503 | }","lines":19,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/parsedate.c#L485-L503"}]},
{"id":"LOGIC_22","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"very_easy","description":"pop3_get_message() advances past blanks with `for(message += 2; ISBLANK(*message); message++, len--)` after `len -= 2`. The loop relies on the NUL terminator / a non-blank byte appearing within the buffer to stop before len underflows. If a server sends a final response line consisting only of the status code followed by blanks and the dynbuf content after offset 2 were entirely ISBLANK with no terminator inside nfinal, len (size_t) would wrap. In practice the recvbuf is NUL terminated and the trailing CRLF stops the scan, so this is defensive only, but the same pattern is duplicated in imap_get_message() and smtp_get_message() and none bound the decrement on len reaching 0.","fix":"Add a `len &&` guard to the blank-skipping loop condition, e.g. `for(message += 2; len && ISBLANK(*message); message++, len--)`.","locations":[{"ref":"lib/pop3.c:357-370","code":" 357 | if(len > 2) {\n 358 | /* Find the start of the message */\n 359 | len -= 2;\n 360 | for(message += 2; ISBLANK(*message); message++, len--)\n 361 | ;\n 362 | \n 363 | /* Find the end of the message */\n 364 | while(len--)\n 365 | if(!ISBLANK(message[len]) && !ISNEWLINE(message[len]))\n 366 | break;\n 367 | \n 368 | /* Terminate the message */\n 369 | message[++len] = '\\0';\n 370 | Curl_bufref_set(out, message, len, NULL);","lines":14,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/pop3.c#L357-L370"},{"ref":"lib/imap.c:436-449","code":" 436 | if(len > 2) {\n 437 | /* Find the start of the message */\n 438 | len -= 2;\n 439 | for(message += 2; ISBLANK(*message); message++, len--)\n 440 | ;\n 441 | \n 442 | /* Find the end of the message */\n 443 | while(len--)\n 444 | if(!ISNEWLINE(message[len]) && !ISBLANK(message[len]))\n 445 | break;\n 446 | \n 447 | /* Terminate the message */\n 448 | message[++len] = '\\0';\n 449 | Curl_bufref_set(out, message, len, NULL);","lines":14,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/imap.c#L436-L449"},{"ref":"lib/smtp.c:553-566","code":" 553 | if(len > 4) {\n 554 | /* Find the start of the message */\n 555 | len -= 4;\n 556 | for(message += 4; ISBLANK(*message); message++, len--)\n 557 | ;\n 558 | \n 559 | /* Find the end of the message */\n 560 | while(len--)\n 561 | if(!ISNEWLINE(message[len]) && !ISBLANK(message[len]))\n 562 | break;\n 563 | \n 564 | /* Terminate the message */\n 565 | message[++len] = '\\0';\n 566 | Curl_bufref_set(out, message, len, NULL);","lines":14,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/smtp.c#L553-L566"}]},
{"id":"LOGIC_23","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"very_easy","description":"Curl_rlimit_drain() guards against signed underflow with\n`if((INT64_MIN + val) < r->tokens) r->tokens -= val; else r->tokens = INT64_MIN;`.\nThe intent is to subtract `val` unless that would underflow below INT64_MIN, i.e.\nthe subtraction is safe whenever `r->tokens >= INT64_MIN + val`. The condition\nused is strict (`<`), so when `r->tokens == INT64_MIN + val` exactly, the code\ntakes the clamp branch and sets INT64_MIN. The computed result would also be\nexactly INT64_MIN, so the observable outcome is identical; this is a harmless\noff-by-one in the boundary test rather than a real bug, but the boundary\ncondition is inconsistent with the analogous overflow check at line 65\n(`(INT64_MAX - token_gain) > r->tokens`).","fix":"Use `>=` for the safe-subtraction test ((INT64_MIN + val) <= r->tokens) to match the boundary semantics, or leave as-is and document that the boundary is intentionally clamped.","locations":[{"ref":"lib/ratelimit.c:238-244","code":" 238 | {\n 239 | int64_t val = (int64_t)tokens;\n 240 | if((INT64_MIN + val) < r->tokens)\n 241 | r->tokens -= val;\n 242 | else\n 243 | r->tokens = INT64_MIN;\n 244 | }","lines":7,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/ratelimit.c#L238-L244"}]},
{"id":"LOGIC_24","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"medium","description":"tftp_send_first() builds the request with curl_msnprintf into\nstate->spacket.data + 2 using state->blksize as the size bound, but at the time\nof the first send state->blksize is TFTP_BLKSIZE_DEFAULT (512), while the buffer\nwas allocated as need_blksize (>=512) + 4. If the user requested a blksize\nlarger than 512, the buffer is larger than 512 but msnprintf is still bounded to\n512, so a long filename that would fit the actual buffer is rejected by the\nearlier length check at lines 700-704 (which also uses state->blksize). This is\na conservative under-utilization, not unsafe, but the filename length test and\nthe snprintf bound both assume 512 regardless of the larger allocation.","fix":"Use the negotiated/allocated capacity consistently when bounding the RRQ/WRQ construction and the filename-length check.","locations":[{"ref":"lib/tftp.c:700-709","code":" 700 | if(strlen(filename) + strlen(mode) + 4 > state->blksize) {\n 701 | failf(data, \"TFTP filename too long\");\n 702 | curlx_free(filename);\n 703 | return CURLE_TFTP_ILLEGAL; /* too long filename field */\n 704 | }\n 705 | \n 706 | sbytes = 2 +\n 707 | curl_msnprintf((char *)state->spacket.data + 2,\n 708 | state->blksize,\n 709 | \"%s%c%s%c\", filename, '\\0', mode, '\\0');","lines":10,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/tftp.c#L700-L709"}]},
{"id":"LOGIC_25","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"medium","description":"An empty HTTP Datagram capsule (capsule_length == 1, i.e. payload_len == 0) is consumed and Curl_capsule_process_udp_raw() returns 0 bytes with *err == CURLE_OK. In capsule_cf_recv() this 0/CURLE_OK is propagated up as a recv of 0 bytes, which higher layers interpret as a clean EOF / connection close, even though the tunnel is still alive and more datagrams may follow. A peer sending a zero-length datagram capsule could thus spuriously terminate the transfer.","fix":"Treat a fully-consumed zero-length payload as \"try again\" rather than EOF: set `*err = CURLE_AGAIN; return 0;` for the payload_len == 0 case (after skipping the header bytes), so the caller loops for the next capsule instead of seeing a 0-byte read.","locations":[{"ref":"lib/vquic/capsule.c:280-284","code":" 280 | Curl_bufq_skip(recvbufq, offset);\n 281 | if(!payload_len) {\n 282 | *err = CURLE_OK;\n 283 | return 0;\n 284 | }","lines":5,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vquic/capsule.c#L280-L284"},{"ref":"lib/vquic/cf-capsule.c:154-159","code":" 154 | /* try to extract a complete capsule datagram */\n 155 | *pnread = Curl_capsule_process_udp_raw(cf, data, &ctx->recvbuf,\n 156 | (unsigned char *)buf, len,\n 157 | &result);\n 158 | return result;\n 159 | }","lines":6,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vquic/cf-capsule.c#L154-L159"}]},
{"id":"LOGIC_26","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"very_easy","description":"Curl_alpn_to_proto_buf() rejects an ALPN list that would exactly fill buf->data. The bounds check `if(off + blen + 1 >= (int)sizeof(buf->data))` uses `>=`, so when `off + blen + 1 == sizeof(buf->data)` (the entry fits exactly with no room to spare and no trailing NUL needed for this wire-format buffer) it returns CURLE_FAILED_INIT even though the data fits. This is an off-by-one over-strict rejection. It is not a memory-safety issue, but it can spuriously fail valid ALPN configurations that are exactly the buffer size.","fix":"Use `if(off + blen + 1 > (int)sizeof(buf->data))` (this binary form has no terminating NUL, unlike Curl_alpn_to_proto_str which correctly needs the extra byte).","locations":[{"ref":"lib/vtls/vtls.c:1623-1629","code":" 1623 | blen = (unsigned char)len;\n 1624 | if(off + blen + 1 >= (int)sizeof(buf->data))\n 1625 | return CURLE_FAILED_INIT;\n 1626 | buf->data[off++] = blen;\n 1627 | memcpy(buf->data + off, spec->entries[i], blen);\n 1628 | off += blen;\n 1629 | }","lines":7,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vtls/vtls.c#L1623-L1629"}]},
{"id":"LOGIC_27","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"medium","description":"tool_readbusy_cb() uses a function-local `static curl_off_t ulprev;` to detect upload stalls (comparing the previous ulnow with the current). Because the variable is static and shared across all easy handles/transfers, when multiple transfers are running concurrently (parallel mode) their ulnow values interleave into the same `ulprev`, so the stall-detection waitfd/sleep heuristic can misfire (sleep when one transfer advanced but another did not, or fail to sleep when it should). This is a correctness/efficiency quirk of busy-read unpausing rather than a crash.","fix":"Track the previous ulnow per-transfer (e.g. store it in struct per_transfer) instead of in a single shared static, so each transfer compares against its own prior value.","locations":[{"ref":"src/tool_cb_rea.c:157-185","code":" 157 | int tool_readbusy_cb(void *clientp,\n 158 | curl_off_t dltotal, curl_off_t dlnow,\n 159 | curl_off_t ultotal, curl_off_t ulnow)\n 160 | {\n 161 | struct per_transfer *per = clientp;\n 162 | struct OperationConfig *config = per->config;\n 163 | static curl_off_t ulprev;\n 164 | \n 165 | (void)dltotal;\n 166 | (void)dlnow;\n 167 | (void)ultotal;\n 168 | (void)ulnow;\n 169 | \n 170 | if(config->readbusy) {\n 171 | if(ulprev == ulnow) {\n 172 | #ifndef _WIN32\n 173 | waitfd(1, per->infd);\n 174 | #else\n 175 | curlx_wait_ms(1); /* sleep */\n 176 | #endif\n 177 | }\n 178 | \n 179 | config->readbusy = FALSE;\n 180 | curl_easy_pause(per->curl, CURLPAUSE_CONT);\n 181 | }\n 182 | \n 183 | ulprev = ulnow;\n 184 | \n 185 | return per->noprogress ? 0 : CURL_PROGRESSFUNC_CONTINUE;","lines":29,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/src/tool_cb_rea.c#L157-L185"}]},
{"id":"LOGIC_28","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"very_easy","description":"In parse_header(), when reading headers from a file/stdin, the loop breaks out on the first add2list() failure (err != PARAM_OK). After the loop, `if(error) err = PARAM_READ_ERROR;` can overwrite a previously-set allocation error (PARAM_NO_MEM) with PARAM_READ_ERROR. In practice my_get_line only sets `error` on a genuine read/oom error so the two rarely both occur, but the unconditional overwrite means a real PARAM_NO_MEM from add2list could be masked as PARAM_READ_ERROR if the read also failed in the same iteration.","fix":"Only set err from `error` when err is still PARAM_OK, e.g. `if(error && !err) err = PARAM_READ_ERROR;`.","locations":[{"ref":"src/tool_getparam.c:1312-1322","code":" 1312 | while(my_get_line(file, &line, &error)) {\n 1313 | const char *ptr = curlx_dyn_ptr(&line);\n 1314 | err = add2list(cmd == C_PROXY_HEADER ? /* --proxy-header? */\n 1315 | &config->proxyheaders :\n 1316 | &config->headers, ptr);\n 1317 | if(err)\n 1318 | break;\n 1319 | }\n 1320 | if(error)\n 1321 | err = PARAM_READ_ERROR;\n 1322 | curlx_dyn_free(&line);","lines":11,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/src/tool_getparam.c#L1312-L1322"}]},
{"id":"LOGIC_29","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"easy","description":"secs2ms() bounds the integer seconds part to LONG_MAX/1000 - 1 but then computes `*val = ((long)secs * 1000) + ms;` where `ms` can be up to ~999. With secs == LONG_MAX/1000 - 1, secs*1000 is close to LONG_MAX and adding ms can overflow a signed long on platforms where LONG_MAX is not a multiple of 1000 plus slack (e.g. 32-bit long). The `-1` slack covers up to 1000, so it is safe for ms<1000, but the relationship is implicit and brittle; a future change to the fractional rounding (ms could momentarily exceed 999 if digs handling changed) would overflow. Worth documenting/asserting the invariant.","fix":"Add an explicit clamp/assert that ms is in [0,999] before the final multiply-add, or compute using a wider type (curl_off_t) and range-check before casting to long.","locations":[{"ref":"src/tool_paramhlp.c:311-329","code":" 311 | if(!str || curlx_str_number(&str, &secs, LONG_MAX / 1000 - 1))\n 312 | return PARAM_BAD_NUMERIC;\n 313 | if(!curlx_str_single(&str, '.')) {\n 314 | curl_off_t fracs;\n 315 | const char *s = str;\n 316 | size_t len;\n 317 | if(curlx_str_number(&str, &fracs, CURL_OFF_T_MAX))\n 318 | return PARAM_NUMBER_TOO_LARGE;\n 319 | /* how many milliseconds are in fracs ? */\n 320 | len = (str - s);\n 321 | while((len > CURL_ARRAYSIZE(digs) || (fracs > LONG_MAX / 100))) {\n 322 | fracs /= 10;\n 323 | len--;\n 324 | }\n 325 | ms = ((long)fracs * 100) / digs[len - 1];\n 326 | }\n 327 | \n 328 | *val = ((long)secs * 1000) + ms;\n 329 | return PARAM_OK;","lines":19,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/src/tool_paramhlp.c#L311-L329"}]},
{"id":"LOGIC_30","type":"logic_error","type_label":"Logic Error","severity":"low","difficulty":"easy","description":"add_glob() doubles glob->palloc BEFORE checking whether the new pattern count is acceptable. When the maximum is reached (glob->pnum >= 255) the function returns CURLE_URL_MALFORMAT, but glob->palloc has already been multiplied by 2. Although the error is propagated and the glob is cleaned up, the doubled palloc value is now inconsistent with the actual allocation size of glob->pattern (which was not reallocated on this branch). If any code path were to read palloc after this without aborting, it would over-estimate the array size. It is latent today because the error aborts parsing, but the bookkeeping is wrong.","fix":"Only update glob->palloc after the successful realloc (and the pnum<255 check), e.g. compute the new size in a local, check the limit, realloc, then assign palloc.","locations":[{"ref":"src/tool_urlglob.c:411-423","code":" 411 | if(++glob->pnum >= glob->palloc) {\n 412 | struct URLPattern *np = NULL;\n 413 | glob->palloc *= 2;\n 414 | if(glob->pnum < 255) { /* avoid ridiculous amounts */\n 415 | np = curlx_realloc(glob->pattern,\n 416 | glob->palloc * sizeof(struct URLPattern));\n 417 | if(!np)\n 418 | return globerror(glob, NULL, pos, CURLE_OUT_OF_MEMORY);\n 419 | }\n 420 | else\n 421 | return globerror(glob, \"too many {} sets\", pos, CURLE_URL_MALFORMAT);\n 422 | glob->pattern = np;\n 423 | }","lines":13,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/src/tool_urlglob.c#L411-L423"}]},
{"id":"LOGIC_31","type":"logic_error","type_label":"Logic Error","severity":"negligible","difficulty":"medium","description":"In curlx_str_quotedword(), an escaped character ('\\\\' followed by a byte)\nincrements the length counter `len` twice (once for the backslash at line 122\nand once for the following byte at line 126), and both increments are checked\nagainst `max`. As a result the reported word length `out->len` counts the\nbackslash escape characters, and the STRE_BIG limit is reached after fewer\nlogical characters than for an unescaped word of the same intended content.\nWhether this matches the intended semantics (length of the raw quoted span vs.\nthe unescaped content) is ambiguous; callers that treat out->len as the\nunescaped content length will be off.","fix":"Clarify/confirm the intended meaning of out->len for quoted words. If it should\nrepresent the raw span, the current behavior is fine; if it should represent\nunescaped content, do not count the backslash toward len.","locations":[{"ref":"lib/curlx/strparse.c:118-127","code":" 118 | while(*s && (*s != '\\\"')) {\n 119 | if(*s == '\\\\' && s[1]) {\n 120 | s++;\n 121 | if(++len > max)\n 122 | return STRE_BIG;\n 123 | }\n 124 | s++;\n 125 | if(++len > max)\n 126 | return STRE_BIG;\n 127 | }","lines":10,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/curlx/strparse.c#L118-L127"}]},
{"id":"LOGIC_32","type":"logic_error","type_label":"Logic Error","severity":"negligible","difficulty":"medium","description":"parse_localport() computes `config->localportrange -= (config->localport - 1);` after parsing the second port. If the end of the range is numerically smaller than the start, localportrange becomes <= 0 and the `< 1` check rejects it, which is correct. However the subtraction is performed on a `long` that was just validated to be in [0,65535]; if localport is 0 (allowed by str2unummax with max 65535 and min 0) then `localport - 1` is -1 and the range is incremented by 1, producing an off-by-one count for the corner case `--local-port 0-N`. Port 0 is generally meaningless but is accepted here, yielding a slightly wrong range count.","fix":"Reject localport == 0 explicitly, or compute the range count as (end - start + 1) with proper validation that start <= end before adjusting.","locations":[{"ref":"src/tool_getparam.c:1192-1203","code":" 1192 | curl_msnprintf(buffer, sizeof(buffer), \"%.*s\", (int)plen, nextarg);\n 1193 | if(str2unummax(&config->localport, buffer, 65535))\n 1194 | return PARAM_BAD_USE;\n 1195 | if(!pp)\n 1196 | config->localportrange = 1; /* default number of ports to try */\n 1197 | else {\n 1198 | if(str2unummax(&config->localportrange, pp, 65535))\n 1199 | return PARAM_BAD_USE;\n 1200 | config->localportrange -= (config->localport - 1);\n 1201 | if(config->localportrange < 1)\n 1202 | return PARAM_BAD_USE;\n 1203 | }","lines":12,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/src/tool_getparam.c#L1192-L1203"}]},
{"id":"CPY_1","type":"copy_paste","type_label":"Copy Paste","severity":"medium","difficulty":"very_easy","description":"In tool_ssls_load(), the loop that trims trailing whitespace/EOL from the session-data portion of a line dereferences the wrong pointer. The condition checks `ISBLANK(*line)` (the fixed start of the value) instead of `ISBLANK(*end)` (the moving end pointer). `*line` never changes inside the loop, so if the value begins with a space or tab, `ISBLANK(*line)` is permanently true and the loop keeps writing NUL and decrementing `end` until `end == line`, stripping the entire value down to a single byte regardless of its actual content. This corrupts the base64-encoded sdata before it is decoded and imported, silently breaking session restore for affected lines. The other two conditions (`*end == '\\n'`, `*end == '\\r'`) correctly use `*end`.","fix":"Change the loop condition to test the moving pointer: `while((end > line) && (*end == '\\n' || *end == '\\r' || ISBLANK(*end)))`.","locations":[{"ref":"src/tool_ssls.c:97-102","code":" 97 | line = c + 1;\n 98 | end = line + strlen(line) - 1;\n 99 | while((end > line) && (*end == '\\n' || *end == '\\r' || ISBLANK(*line))) {\n 100 | *end = '\\0';\n 101 | --end;\n 102 | }","lines":6,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/src/tool_ssls.c#L97-L102"}]},
{"id":"CPY_2","type":"copy_paste","type_label":"Copy Paste","severity":"low","difficulty":"very_easy","description":"sasl_choose_oauth() (OAUTHBEARER) gates on `Curl_creds_has_oauth_bearer(data->state.creds)` but builds the message from `sctx->conn->creds`. The sibling sasl_choose_oauth2() (XOAUTH2) consistently uses `sctx->conn->creds` for both the guard and the message. On a reused connection data->state.creds (the new transfer's credentials) and conn->creds (the connection's existing credentials) can differ, so the guard and the data source disagree. The other SASL choosers and Curl_sasl_can_authenticate() all key off conn->creds, making this the odd one out and a likely copy/paste slip.","fix":"Use the same creds source in the guard and the message. Change the guard to `Curl_creds_has_oauth_bearer(sctx->conn->creds)` to match sasl_choose_oauth2() and the rest of the file.","locations":[{"ref":"lib/curl_sasl.c:436-456","code":" 436 | static bool sasl_choose_oauth(struct Curl_easy *data, struct sasl_ctx *sctx)\n 437 | {\n 438 | if(Curl_creds_has_oauth_bearer(data->state.creds) &&\n 439 | (sctx->enabledmechs & SASL_MECH_OAUTHBEARER)) {\n 440 | const char *hostname;\n 441 | int port;\n 442 | Curl_conn_get_current_host(data, FIRSTSOCKET, &hostname, &port);\n 443 | \n 444 | sctx->mech = SASL_MECH_STRING_OAUTHBEARER;\n 445 | sctx->state1 = SASL_OAUTH2;\n 446 | sctx->state2 = SASL_OAUTH2_RESP;\n 447 | sctx->sasl->authused = SASL_MECH_OAUTHBEARER;\n 448 | \n 449 | if(sctx->sasl->force_ir || data->set.sasl_ir)\n 450 | sctx->result =\n 451 | Curl_auth_create_oauth_bearer_message(sctx->conn->creds,\n 452 | hostname, port, &sctx->resp);\n 453 | return TRUE;\n 454 | }\n 455 | return FALSE;\n 456 | }","lines":21,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/curl_sasl.c#L436-L456"},{"ref":"lib/curl_sasl.c:458-472","code":" 458 | static bool sasl_choose_oauth2(struct Curl_easy *data, struct sasl_ctx *sctx)\n 459 | {\n 460 | if(Curl_creds_has_oauth_bearer(sctx->conn->creds) &&\n 461 | (sctx->enabledmechs & SASL_MECH_XOAUTH2)) {\n 462 | sctx->mech = SASL_MECH_STRING_XOAUTH2;\n 463 | sctx->state1 = SASL_OAUTH2;\n 464 | sctx->sasl->authused = SASL_MECH_XOAUTH2;\n 465 | \n 466 | if(sctx->sasl->force_ir || data->set.sasl_ir)\n 467 | sctx->result = Curl_auth_create_xoauth_bearer_message(\n 468 | sctx->conn->creds, &sctx->resp);\n 469 | return TRUE;\n 470 | }\n 471 | return FALSE;\n 472 | }","lines":15,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/curl_sasl.c#L458-L472"}]},
{"id":"CPY_3","type":"copy_paste","type_label":"Copy Paste","severity":"low","difficulty":"easy","description":"pop3_perform_apop() null-checks `data->state.creds` before proceeding, but every other POP3 auth path checks `conn->creds` (e.g. pop3_perform_user() at line 530, pop3_perform_authentication() uses Curl_sasl_can_authenticate() which inspects conn->creds). APOP then dereferences conn->creds via Curl_creds_user()/ Curl_creds_passwd(). Because those accessor macros are NULL-safe this is not a crash, but the inconsistency means APOP may decide differently from USER on a reused connection where the two creds pointers diverge, sending empty/wrong credentials. It is an unintended divergence from the sibling functions.","fix":"Check `conn->creds` in pop3_perform_apop() to be consistent with pop3_perform_user() and the SASL path.","locations":[{"ref":"lib/pop3.c:565-571","code":" 565 | /* Check we have a username and password to authenticate with and end the\n 566 | connect phase if we do not */\n 567 | if(!data->state.creds) {\n 568 | pop3_state(data, POP3_STOP);\n 569 | \n 570 | return result;\n 571 | }","lines":7,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/pop3.c#L565-L571"},{"ref":"lib/pop3.c:528-534","code":" 528 | /* Check we have a username and password to authenticate with and end the\n 529 | connect phase if we do not */\n 530 | if(!conn->creds) {\n 531 | pop3_state(data, POP3_STOP);\n 532 | \n 533 | return result;\n 534 | }","lines":7,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/pop3.c#L528-L534"}]},
{"id":"ERR_1","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"medium","difficulty":"hard","description":"read_pkt_to_send() (quiche) treats quiche_conn_send() returning a value that is not QUICHE_ERR_DONE and not convertible by curlx_sztouz() as an error. curlx_sztouz() returns false for negative values; thus all negative quiche error codes other than QUICHE_ERR_DONE map to CURLE_SEND_ERROR. That is fine, but a return value of exactly 0 (rv==0) is accepted by curlx_sztouz() as success with *pnread==0. Curl_bufq_sipn() in cf_flush_egress then treats 0 written bytes as 'nread < gsolen' and flushes/loops; combined with quiche only signalling 'no more' via QUICHE_ERR_DONE, a spurious 0-return would not advance and not error. quiche does not document a 0 success return, so this is defensive only, but the contract handling differs from the ngtcp2 path which explicitly handles n==0 as 'nothing to send' -> CURLE_AGAIN.","fix":"Explicitly treat rv==0 from quiche_conn_send() as CURLE_AGAIN (nothing to send) like the ngtcp2 read_pkt_to_send() does, rather than as a zero-length success.","locations":[{"ref":"lib/vquic/cf-quiche.c:750-768","code":" 750 | static CURLcode read_pkt_to_send(void *userp,\n 751 | unsigned char *buf, size_t buflen,\n 752 | size_t *pnread)\n 753 | {\n 754 | struct read_ctx *x = userp;\n 755 | struct cf_quiche_ctx *ctx = x->cf->ctx;\n 756 | ssize_t rv;\n 757 | \n 758 | *pnread = 0;\n 759 | rv = quiche_conn_send(ctx->qconn, buf, buflen, &x->send_info);\n 760 | if(rv == QUICHE_ERR_DONE)\n 761 | return CURLE_AGAIN;\n 762 | \n 763 | if(!curlx_sztouz(rv, pnread)) {\n 764 | failf(x->data, \"quiche_conn_send returned %zd\", rv);\n 765 | return CURLE_SEND_ERROR;\n 766 | }\n 767 | return CURLE_OK;\n 768 | }","lines":19,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vquic/cf-quiche.c#L750-L768"}]},
{"id":"ERR_2","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"medium","description":"In altsvc_load(), Curl_get_line() failures (other than EOF) are silently swallowed: the loop condition is `while(!result && !eof)`, but `result` from Curl_get_line is captured into the function-level `result` which is then returned. However a non-OOM dynbuf/line error (NETRC-style) just stops the loop and is returned to the caller, while altsvc_add() result is intentionally ignored (`altsvc_add(asi, lineptr);` return value discarded). An out-of-memory inside altsvc_add()/altsvc_create() (which returns CURLE_OUT_OF_MEMORY) is discarded, so genuine OOM while loading the alt-svc cache is not propagated. Contrast with hsts_load(), which also ignores hsts_add()'s OOM the same way.","fix":"Capture and propagate the return value of altsvc_add()/hsts_add() (at least the CURLE_OUT_OF_MEMORY case) so that an allocation failure during cache load aborts the load instead of being silently lost.","locations":[{"ref":"lib/altsvc.c:256-264","code":" 256 | do {\n 257 | result = Curl_get_line(&buf, fp, &eof);\n 258 | if(!result) {\n 259 | const char *lineptr = curlx_dyn_ptr(&buf);\n 260 | curlx_str_passblanks(&lineptr);\n 261 | if(curlx_str_single(&lineptr, '#'))\n 262 | altsvc_add(asi, lineptr);\n 263 | }\n 264 | } while(!result && !eof);","lines":9,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/altsvc.c#L256-L264"},{"ref":"lib/hsts.c:577-591","code":" 577 | do {\n 578 | result = Curl_get_line(&buf, fp, &eof);\n 579 | if(!result) {\n 580 | const char *lineptr = curlx_dyn_ptr(&buf);\n 581 | curlx_str_passblanks(&lineptr);\n 582 | \n 583 | /* Skip empty or commented lines, since we know the line will have\n 584 | a trailing newline from Curl_get_line we can treat length 1 as\n 585 | empty. */\n 586 | if((*lineptr == '#') || strlen(lineptr) <= 1)\n 587 | continue;\n 588 | \n 589 | hsts_add(h, lineptr);\n 590 | }\n 591 | } while(!result && !eof);","lines":15,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/hsts.c#L577-L591"}]},
{"id":"ERR_3","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"easy","description":"In the POSIX strerror_r branch of curlx_strerror(), when strerror_r() returns a\nnon-zero value (indicating failure such as ERANGE for a truncated message or\nEINVAL for an unknown error number) the code only overwrites the buffer with a\nfallback \"Unknown error N\" string if buf[0] == '\\0'. If strerror_r() partially\nfilled the buffer (truncation/ERANGE) buf[0] is non-zero, so the truncated /\npossibly misleading partial message is returned and the error from strerror_r\nis silently ignored. This differs from the glibc and plain-strerror branches,\nwhich always format a clean message.","fix":"On a non-zero return from strerror_r, format the fallback \"Unknown error N\"\nunconditionally (when the buffer is large enough) rather than only when buf is\nempty, to avoid returning a truncated/garbage partial message.","locations":[{"ref":"lib/curlx/strerr.c:280-284","code":" 280 | if(strerror_r(err, buf, buflen) &&\n 281 | buflen > sizeof(\"Unknown error \") + 20) {\n 282 | if(buf[0] == '\\0')\n 283 | SNPRINTF(buf, buflen, \"Unknown error %d\", err);\n 284 | }","lines":5,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/curlx/strerr.c#L280-L284"}]},
{"id":"ERR_4","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"medium","description":"`Curl_dynhds_to_nva` calls `curlx_calloc(1, sizeof(nghttp2_nv) * dynhds->hds_len)`.\nWhen `hds_len` is 0 this is `calloc(1, 0)`, which may legitimately return NULL on\nsome allocators. The function then returns NULL with `*pcount` left at 0, which a\ncaller is likely to interpret as an out-of-memory failure rather than the valid\n'empty header set' case. The size multiplication `sizeof(nghttp2_nv) * hds_len`\nis also not overflow-checked, unlike the explicit overflow guards used elsewhere\nin this audit scope (bufq.c).","fix":"Special-case hds_len == 0 (return a non-NULL sentinel or have callers treat NULL+pcount==0 as success), and guard the multiplication against overflow.","locations":[{"ref":"lib/dynhds.c:363-383","code":" 363 | nghttp2_nv *Curl_dynhds_to_nva(struct dynhds *dynhds, size_t *pcount)\n 364 | {\n 365 | nghttp2_nv *nva = curlx_calloc(1, sizeof(nghttp2_nv) * dynhds->hds_len);\n 366 | size_t i;\n 367 | \n 368 | *pcount = 0;\n 369 | if(!nva)\n 370 | return NULL;\n 371 | \n 372 | for(i = 0; i < dynhds->hds_len; ++i) {\n 373 | struct dynhds_entry *e = dynhds->hds[i];\n 374 | DEBUGASSERT(e);\n 375 | nva[i].name = (unsigned char *)e->name;\n 376 | nva[i].namelen = e->namelen;\n 377 | nva[i].value = (unsigned char *)e->value;\n 378 | nva[i].valuelen = e->valuelen;\n 379 | nva[i].flags = NGHTTP2_NV_FLAG_NONE;\n 380 | }\n 381 | *pcount = dynhds->hds_len;\n 382 | return nva;\n 383 | }","lines":21,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/dynhds.c#L363-L383"}]},
{"id":"ERR_5","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"medium","description":"In the DEBUGBUILD-only event engine, events_socket() is registered as the\nCURLMOPT_SOCKETFUNCTION callback. The socket callback contract uses a return\nvalue of -1 to signal a fatal/abort condition; any other value is treated as\nsuccess. On allocation failure events_socket() does `return CURLE_OUT_OF_MEMORY;`\n(value 27), which is neither 0 nor -1. The out-of-memory condition is therefore\nsilently treated as success (the new socket is simply not monitored), instead of\nbeing reported as an error. The intended return on this path was almost\ncertainly -1. Impact is limited because this code only exists in debug builds\n(curl_easy_perform_ev), but it is still incorrect error handling.","fix":"Return -1 (or 0 and skip monitoring deliberately) on the allocation-failure path rather than a CURLcode value that is misinterpreted by the socket-callback machinery.","locations":[{"ref":"lib/easy.c:521-536","code":" 521 | else {\n 522 | m = curlx_malloc(sizeof(struct socketmonitor));\n 523 | if(m) {\n 524 | m->next = ev->list;\n 525 | m->socket.fd = s;\n 526 | m->socket.events = socketcb2poll(what);\n 527 | m->socket.revents = 0;\n 528 | ev->list = m;\n 529 | infof(data, \"socket cb: socket %\" FMT_SOCKET_T \" ADDED as %s%s\", s,\n 530 | (what & CURL_POLL_IN) ? \"IN\" : \"\",\n 531 | (what & CURL_POLL_OUT) ? \"OUT\" : \"\");\n 532 | }\n 533 | else\n 534 | return CURLE_OUT_OF_MEMORY;\n 535 | }\n 536 | }","lines":16,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/easy.c#L521-L536"}]},
{"id":"ERR_6","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"very_easy","description":"In curl_easy_unescape(), when the decoded output length exceeds INT_MAX the\ncode frees the result buffer (str becomes NULL via curlx_safefree) but does\nNOT assign anything to *outlength. The function then returns NULL, which is\ncorrect, but a caller that passed a non-NULL outlength and only checks the\nreturned pointer would read an uninitialized *outlength. More importantly the\ndocumented contract (\"Returns a pointer ... with length given in *olen\") is\nviolated silently on this error path: there is no way for the caller to learn\nthe decode failed via outlength, and the value is left as whatever was there\nbefore. The success path always writes *outlength, so the asymmetry is\nsurprising. Setting *outlength to 0 (or simply leaving str non-NULL would be\nworse) would make behavior consistent.","fix":"In the too-large branch set `*outlength = 0;` (or document the unset behavior). Currently only str is freed and *outlength is left untouched.","locations":[{"ref":"lib/escape.c:259-267","code":" 259 | if(outlength) {\n 260 | if(outputlen <= (size_t)INT_MAX)\n 261 | *outlength = curlx_uztosi(outputlen);\n 262 | else\n 263 | /* too large to return in an int, fail! */\n 264 | curlx_safefree(str);\n 265 | }\n 266 | }\n 267 | return str;","lines":9,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/escape.c#L259-L267"}]},
{"id":"ERR_7","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"very_easy","description":"In mqtt_connect() the return value of Curl_rand_alnum() (used to fill the random part of the generated MQTT client id) is stored in `result` but never checked before it is potentially overwritten by add_client_id()'s success path. If Curl_rand_alnum() fails, the random bytes are not produced. In practice the subsequent add_client_id() length check (client_id must be exactly MQTT_CLIENTID_LEN) catches the still-\"curl\" id and the function errors out, so there is no memory-safety impact, but a failing RNG is silently turned into a misleading \"Client ID length mismatched\" error instead of being reported as the real failure, and the dependency on the length check to mask an unchecked RNG error is fragile.","fix":"Check the result of Curl_rand_alnum() immediately and goto end / return on error, e.g. `result = Curl_rand_alnum(...); if(result) goto end;` before calling add_client_id().","locations":[{"ref":"lib/mqtt.c:308-316","code":" 308 | result = Curl_rand_alnum(data, (unsigned char *)&client_id[clen],\n 309 | MQTT_CLIENTID_LEN - clen + 1);\n 310 | /* add client id */\n 311 | rc = add_client_id(client_id, strlen(client_id), packet, pos + 1);\n 312 | if(rc) {\n 313 | failf(data, \"Client ID length mismatched: [%zu]\", strlen(client_id));\n 314 | result = CURLE_WEIRD_SERVER_REPLY;\n 315 | goto end;\n 316 | }","lines":9,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/mqtt.c#L308-L316"}]},
{"id":"ERR_8","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"very_easy","description":"In Curl_pp_statemach(), a select/poll error (rc == -1) is reported as CURLE_OUT_OF_MEMORY: `failf(data, \"select/poll error\"); result = CURLE_OUT_OF_MEMORY;`. A poll/select failure is not an out-of-memory condition; returning CURLE_OUT_OF_MEMORY misleads callers and users. This mislabeling is purely an error-code mismatch (the FTP/other pingpong consumers propagate it unchanged).","fix":"Return a more appropriate code such as CURLE_RECV_ERROR (or a dedicated select error code) instead of CURLE_OUT_OF_MEMORY.","locations":[{"ref":"lib/pingpong.c:108-111","code":" 108 | if(rc == -1) {\n 109 | failf(data, \"select/poll error\");\n 110 | result = CURLE_OUT_OF_MEMORY;\n 111 | }","lines":4,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/pingpong.c#L108-L111"}]},
{"id":"ERR_9","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"easy","description":"wakeup_inet() error path does `sclose(socks[0]); sclose(socks[1]);` after\n`socks[0] = socks[1] = CURL_SOCKET_BAD;` is only set at the very start (line\n167). Between obtaining `socks[0]` (line 192) and `socks[1]` (line 205) several\ngoto error jumps occur. On the early failures (e.g. bind/getsockname/listen\nfail before socks[0] is created) socks[0]/socks[1] are still CURL_SOCKET_BAD so\nsclose() is a no-op - that is fine. But after a successful accept sets\nsocks[1], a later failure in the read-verify loop jumps to error and closes\nlistener, socks[0], socks[1] - correct. No leak, but note that swrite() at line\n219 ignores its return value; a failed write leaves the subsequent poll/sread\nloop to time out (up to 60s) rather than failing fast.","fix":"Check the swrite() return value and goto error on failure instead of relying on the 60s read-loop timeout.","locations":[{"ref":"lib/socketpair.c:218-219","code":" 218 | /* write data to the socket */\n 219 | swrite(socks[0], rnd, sizeof(rnd));","lines":2,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/socketpair.c#L218-L219"}]},
{"id":"ERR_10","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"medium","description":"Curl_auth_gsasl_token() ignores the GSASL return distinction beyond OK/NEEDS_MORE and always sets the output bufref to (response, outlen) even when gsasl_step returned GSASL_OK with response==NULL/outlen==0. More importantly, on GSASL_NEEDS_MORE the function returns CURLE_OK with no indication to the caller that another round is required, relying entirely on protocol-level looping. While functional, the response buffer ownership (freed via gsasl_free) is set unconditionally; if gsasl_step set response to NULL this stores a NULL with a free callback which is benign but the unchecked outlen/response pairing is fragile.","fix":"Explicitly handle response==NULL (set an empty bufref without a free callback) and document/propagate the NEEDS_MORE state so callers cannot misinterpret a CURLE_OK with no token.","locations":[{"ref":"lib/vauth/gsasl.c:93-111","code":" 93 | CURLcode Curl_auth_gsasl_token(struct Curl_easy *data,\n 94 | const struct bufref *chlg,\n 95 | struct gsasldata *gsasl,\n 96 | struct bufref *out)\n 97 | {\n 98 | int res;\n 99 | char *response;\n 100 | size_t outlen;\n 101 | \n 102 | res = gsasl_step(gsasl->client, Curl_bufref_ptr(chlg), Curl_bufref_len(chlg),\n 103 | &response, &outlen);\n 104 | if(res != GSASL_OK && res != GSASL_NEEDS_MORE) {\n 105 | failf(data, \"GSASL step: %s\", gsasl_strerror(res));\n 106 | return CURLE_BAD_CONTENT_ENCODING;\n 107 | }\n 108 | \n 109 | Curl_bufref_set(out, response, outlen, gsasl_free);\n 110 | return CURLE_OK;\n 111 | }","lines":19,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vauth/gsasl.c#L93-L111"}]},
{"id":"ERR_11","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"medium","description":"Curl_auth_create_ntlm_type3_message can leak/abort inconsistently: in the NTLMv2 branch several early 'return result;' statements (after Curl_rand, Curl_ntlm_core_mk_nt_hash, Curl_ntlm_core_mk_ntlmv2_hash, Curl_ntlm_core_mk_lmv2_resp, Curl_ntlm_core_mk_ntlmv2_resp) return before reaching the 'error:' label. On the mk_ntlmv2_resp failure it sets ntlmv2resp via the out-param only on success, so those early returns are fine, but they also skip the Curl_auth_cleanup_ntlm(ntlm) cleanup performed at the error label, leaving ntlm->target_info allocated for the connection-scoped ntlm struct. Subsequent reuse calls Curl_auth_cleanup_ntlm at type1 generation, so it is eventually freed, but within a single failed type-3 attempt the target_info remains allocated and the documented post-condition (cleanup) is not honored on these error paths.","fix":"Replace the early 'return result;' statements in the NTLMv2 branch with 'goto error;' so Curl_auth_cleanup_ntlm and ntlmv2resp free run on every exit path.","locations":[{"ref":"lib/vauth/ntlm.c:618-644","code":" 618 | result = Curl_rand(data, entropy, 8);\n 619 | if(result)\n 620 | return result;\n 621 | \n 622 | result = Curl_ntlm_core_mk_nt_hash(passwdp, ntbuffer);\n 623 | if(result)\n 624 | return result;\n 625 | \n 626 | result = Curl_ntlm_core_mk_ntlmv2_hash(user, userlen, domain, domlen,\n 627 | ntbuffer, ntlmv2hash);\n 628 | if(result)\n 629 | return result;\n 630 | \n 631 | /* LMv2 response */\n 632 | result = Curl_ntlm_core_mk_lmv2_resp(ntlmv2hash, entropy,\n 633 | &ntlm->nonce[0], lmresp);\n 634 | if(result)\n 635 | return result;\n 636 | \n 637 | /* NTLMv2 response */\n 638 | result = Curl_ntlm_core_mk_ntlmv2_resp(ntlmv2hash, entropy,\n 639 | ntlm, &ntlmv2resp, &ntresplen);\n 640 | if(result)\n 641 | return result;\n 642 | \n 643 | ptr_ntresp = ntlmv2resp;\n 644 | }","lines":27,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vauth/ntlm.c#L618-L644"}]},
{"id":"ERR_12","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"easy","description":"qlog_callback() (ngtcp2) writes datalen bytes with write() and only checks for rc == -1 to disable further logging. A short write (0 <= rc < datalen, possible for a regular file on a full filesystem or signal) is ignored, silently truncating the qlog without disabling it; subsequent writes continue at the wrong offset producing a corrupt qlog file. This is diagnostic-only output so impact is low.","fix":"Treat a short write (rc >= 0 && (size_t)rc < datalen) the same as an error, or loop to write the remaining bytes.","locations":[{"ref":"lib/vquic/cf-ngtcp2-cmn.c:175-189","code":" 175 | static void qlog_callback(void *user_data, uint32_t flags,\n 176 | const void *data, size_t datalen)\n 177 | {\n 178 | struct Curl_cfilter *cf = user_data;\n 179 | struct cf_ngtcp2_ctx *ctx = cf->ctx;\n 180 | (void)flags;\n 181 | if(ctx->qlogfd != -1) {\n 182 | ssize_t rc = write(ctx->qlogfd, data, datalen);\n 183 | if(rc == -1) {\n 184 | /* on write error, stop further write attempts */\n 185 | curlx_close(ctx->qlogfd);\n 186 | ctx->qlogfd = -1;\n 187 | }\n 188 | }\n 189 | }","lines":15,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vquic/cf-ngtcp2-cmn.c#L175-L189"}]},
{"id":"ERR_13","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"medium","description":"ossl_keylog_callback() and ossl_log_tls12_secret() ignore the boolean result of Curl_tls_keylog_write_line()/Curl_tls_keylog_write(). A failed keylog write (e.g. the file handle became invalid) is silently dropped. This is informational logging so low severity, but combined with keylog_file_fp being a process-global static with no locking, simultaneous TLS handshakes from multiple threads call fputs() on the same FILE* concurrently. fputs itself is generally thread-safe per-call on POSIX, but interleaving of separate write_line/write calls can corrupt the SSLKEYLOGFILE output across threads.","fix":"Document the keylog file as best-effort/diagnostic, and consider serializing writes (the file is a process-global) if multi-threaded keylogging is to be supported.","locations":[{"ref":"lib/vtls/keylog.c:99-103","code":" 99 | /* Using fputs here instead of fprintf since libcurl's fprintf replacement\n 100 | may not be thread-safe. */\n 101 | fputs(buf, keylog_file_fp);\n 102 | return TRUE;\n 103 | }","lines":5,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vtls/keylog.c#L99-L103"},{"ref":"lib/vtls/keylog.c:144-147","code":" 144 | /* Using fputs here instead of fprintf since libcurl's fprintf replacement\n 145 | may not be thread-safe. */\n 146 | fputs((char *)line, keylog_file_fp);\n 147 | return TRUE;","lines":4,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vtls/keylog.c#L144-L147"}]},
{"id":"ERR_14","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"very_easy","description":"In X509V3_ext(), when BIO_new(BIO_s_mem()) fails the function does `return result`, but at that point `result` is still CURLE_OK (set at the top of the function and not modified before the first loop iteration). An allocation failure for the memory BIO is therefore silently reported as success, and all remaining certificate extensions for that certificate are silently dropped from the gathered cert info. The intent is clearly to bail out, but the returned status hides the OOM and produces an incomplete certinfo without signalling an error.","fix":"On BIO_new() failure return an explicit error, e.g. `result = CURLE_OUT_OF_MEMORY; return result;` (or break out of the loop and propagate the failure), instead of returning the stale CURLE_OK.","locations":[{"ref":"lib/vtls/openssl.c:242-245","code":" 242 | BIO *bio_out = BIO_new(BIO_s_mem());\n 243 | \n 244 | if(!bio_out)\n 245 | return result;","lines":4,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vtls/openssl.c#L242-L245"}]},
{"id":"ERR_15","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"easy","description":"ossl_init_certinfo()/Curl_ssl_init_certinfo() casts the caller-supplied `num` to size_t for curlx_calloc((size_t)num, ...) without validating that num >= 0. A negative num would be converted to a huge size_t and either fail allocation or, if num==0, allocate a zero-element table while still being used. The only in-tree caller (ossl_certchain) bounds numcerts to (0, MAX_ALLOWED_CERT_AMOUNT], so it is currently safe, but the public-looking helper performs no defensive check and a future/other caller passing 0 or a negative value would set ci->num_of_certs accordingly and lead to out-of-range certinfo indexing guarded only by a DEBUGASSERT.","fix":"Validate `num` (reject num <= 0 with CURLE_BAD_FUNCTION_ARGUMENT) before the (size_t) cast and before storing it in ci->num_of_certs.","locations":[{"ref":"lib/vtls/vtls.c:312-329","code":" 312 | CURLcode Curl_ssl_init_certinfo(struct Curl_easy *data, int num)\n 313 | {\n 314 | struct curl_certinfo *ci = &data->info.certs;\n 315 | struct curl_slist **table;\n 316 | \n 317 | /* Free any previous certificate information structures */\n 318 | Curl_ssl_free_certinfo(data);\n 319 | \n 320 | /* Allocate the required certificate information structures */\n 321 | table = curlx_calloc((size_t)num, sizeof(struct curl_slist *));\n 322 | if(!table)\n 323 | return CURLE_OUT_OF_MEMORY;\n 324 | \n 325 | ci->num_of_certs = num;\n 326 | ci->certinfo = table;\n 327 | \n 328 | return CURLE_OK;\n 329 | }","lines":18,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vtls/vtls.c#L312-L329"}]},
{"id":"ERR_16","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"very_easy","description":"Curl_extract_certinfo() inits the dynbuf `out` (curlx_dyn_init) and then, if Curl_parseX509() fails, executes `return CURLE_PEER_FAILED_VERIFICATION;` directly instead of going through the `done:` label. While curlx_dyn_init() itself does not allocate (so no heap leak), this bypasses the consistent `goto done` cleanup pattern used by every other error exit in the function and is fragile: any future change that allocates during/after init, or before the parse, would leak. It is also an inconsistent error path versus the rest of the function.","fix":"Replace the bare `return` with `result = CURLE_PEER_FAILED_VERIFICATION; goto done;` so cleanup stays centralized.","locations":[{"ref":"lib/vtls/x509asn1.c:1112-1117","code":" 1112 | curlx_dyn_init(&out, CURL_X509_STR_MAX);\n 1113 | /* Prepare the certificate information for curl_easy_getinfo(). */\n 1114 | \n 1115 | /* Extract the certificate ASN.1 elements. */\n 1116 | if(Curl_parseX509(&cert, beg, end))\n 1117 | return CURLE_PEER_FAILED_VERIFICATION;","lines":6,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vtls/x509asn1.c#L1112-L1117"}]},
{"id":"ERR_17","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"low","difficulty":"very_easy","description":"ftp_setopts() declares `CURLcode result;` uninitialized and returns it at the end. The value is only set by the MY_SETOPT_STR macro invocations (which return early on hard errors and otherwise assign `result`). The final `if(config->ftp_pret) my_setopt_long(...)` does not touch `result`, so the returned value is whatever the last MY_SETOPT_STR (CURLOPT_FTP_ALTERNATIVE_TO_USER, line 661) left. That call is unconditional so `result` is technically always assigned before the return, but relying on macro side effects for initialization is fragile: any reordering that makes the last MY_SETOPT_STR conditional would return an uninitialized value. Compare ssl/http/proxy setopt helpers which initialize `result = CURLE_OK;`.","fix":"Initialize `CURLcode result = CURLE_OK;` at declaration to make the function robust regardless of which setopt calls execute.","locations":[{"ref":"src/config2setopts.c:756-787","code":" 756 | static CURLcode ftp_setopts(struct OperationConfig *config, CURL *curl,\n 757 | const char *use_proto)\n 758 | {\n 759 | CURLcode result;\n 760 | \n 761 | if(use_proto != proto_ftp && use_proto != proto_ftps)\n 762 | return CURLE_OK;\n 763 | \n 764 | MY_SETOPT_STR(curl, CURLOPT_FTPPORT, config->ftpport);\n 765 | \n 766 | if(config->disable_epsv)\n 767 | /* disable it */\n 768 | my_setopt_long(curl, CURLOPT_FTP_USE_EPSV, 0);\n 769 | \n 770 | if(config->disable_eprt)\n 771 | /* disable it */\n 772 | my_setopt_long(curl, CURLOPT_FTP_USE_EPRT, 0);\n 773 | \n 774 | if(config->ftp_ssl_ccc)\n 775 | my_setopt_enum(curl, CURLOPT_FTP_SSL_CCC, config->ftp_ssl_ccc_mode);\n 776 | \n 777 | MY_SETOPT_STR(curl, CURLOPT_FTP_ACCOUNT, config->ftp_account);\n 778 | my_setopt_long(curl, CURLOPT_FTP_SKIP_PASV_IP, config->ftp_skip_ip);\n 779 | my_setopt_long(curl, CURLOPT_FTP_FILEMETHOD, config->ftp_filemethod);\n 780 | MY_SETOPT_STR(curl, CURLOPT_FTP_ALTERNATIVE_TO_USER,\n 781 | config->ftp_alternative_to_user);\n 782 | \n 783 | if(config->ftp_pret)\n 784 | my_setopt_long(curl, CURLOPT_FTP_USE_PRET, 1);\n 785 | \n 786 | return result;\n 787 | }","lines":32,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/src/config2setopts.c#L756-L787"}]},
{"id":"ERR_18","type":"incorrect_error_handling","type_label":"Incorrect Error Handling","severity":"negligible","difficulty":"very_easy","description":"In tool_debug_cb(), when opening the trace dump file, `global->trace_stream = curlx_fopen(global->trace_dump, FOPEN_WRITETEXT);` sets `global->trace_fopened = TRUE;` unconditionally, even when curlx_fopen() returns NULL (open failed). This leaves trace_fopened TRUE while trace_stream is NULL, an inconsistent state. It is not currently exploitable because the cleanup in tool_cfgable.c guards with `if(global->trace_fopened && global->trace_stream)` before fclose, but the flag should still only be set on a successful open to keep the invariant clean.","fix":"Only set `global->trace_fopened = TRUE;` after verifying the returned stream is non-NULL, e.g. set it inside an `if(global->trace_stream)` block.","locations":[{"ref":"src/tool_cb_dbg.c:176-179","code":" 176 | else {\n 177 | global->trace_stream = curlx_fopen(global->trace_dump, FOPEN_WRITETEXT);\n 178 | global->trace_fopened = TRUE;\n 179 | }","lines":4,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/src/tool_cb_dbg.c#L176-L179"}]},
{"id":"API_1","type":"api_misuse","type_label":"Api Misuse","severity":"low","difficulty":"very_easy","description":"cf_haproxy_adjust_pollset() dereferences cf->next->connected without first\nchecking cf->next for NULL. While the HAPROXY filter is always inserted above\na connected sub-chain in normal operation, every other adjust_pollset / connect\ncallback in these files defensively checks cf->next (e.g. cf_dns_adjust_pollset,\nCurl_cf_def_*). A close() that discards the sub-chain followed by a pollset\nadjust would NULL-deref here.","fix":"Guard with `if(cf->next && cf->next->connected && !cf->connected)` before dereferencing cf->next.","locations":[{"ref":"lib/cf-haproxy.c:169-180","code":" 169 | static CURLcode cf_haproxy_adjust_pollset(struct Curl_cfilter *cf,\n 170 | struct Curl_easy *data,\n 171 | struct easy_pollset *ps)\n 172 | {\n 173 | if(cf->next->connected && !cf->connected) {\n 174 | /* If we are not connected, but the filter \"below\" is\n 175 | * and not waiting on something, we are sending. */\n 176 | return Curl_pollset_set_out_only(\n 177 | data, ps, Curl_conn_cf_get_socket(cf, data));\n 178 | }\n 179 | return CURLE_OK;\n 180 | }","lines":12,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/cf-haproxy.c#L169-L180"}]},
{"id":"API_2","type":"api_misuse","type_label":"Api Misuse","severity":"low","difficulty":"hard","description":"In the Windows SSPI DIGEST-MD5 and HTTP-Digest paths, CompleteAuthToken() is called with the credentials handle (&credentials) instead of the security context handle. The SSPI CompleteAuthToken contract requires the partially formed context handle (phContext), not a credential handle. Compare with ntlm_sspi.c (CompleteAuthToken(ntlm->context, ...)) and spnego_sspi.c (CompleteAuthToken(nego->context, ...)) which correctly pass the context. Although DIGEST-SSP typically does not return SEC_I_COMPLETE_NEEDED, this is incorrect API usage and would misbehave if the package ever requested completion.","fix":"Pass the context handle to CompleteAuthToken (e.g. &context in the MD5 path and digest->http_context in the HTTP path), matching the NTLM and SPNEGO SSPI implementations.","locations":[{"ref":"lib/vauth/digest_sspi.c:196-198","code":" 196 | if(status == SEC_I_COMPLETE_NEEDED ||\n 197 | status == SEC_I_COMPLETE_AND_CONTINUE)\n 198 | Curl_pSecFn->CompleteAuthToken(&credentials, &resp_desc);","lines":3,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vauth/digest_sspi.c#L196-L198"},{"ref":"lib/vauth/digest_sspi.c:566-568","code":" 566 | if(status == SEC_I_COMPLETE_NEEDED ||\n 567 | status == SEC_I_COMPLETE_AND_CONTINUE)\n 568 | Curl_pSecFn->CompleteAuthToken(&credentials, &resp_desc);","lines":3,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vauth/digest_sspi.c#L566-L568"}]},
{"id":"PERF_1","type":"performance","type_label":"Performance","severity":"low","difficulty":"medium","description":"cw_out_append() calls cw_out_bufs_len(ctx) which walks the entire linked list of\nbuffered cw_out_buf chunks summing curlx_dyn_len() for each. It is called on\nevery append while a transfer is paused (or partially written). For a transfer\nthat pauses and accumulates many buffer chunks, this makes appending O(n) per\ncall and the overall buffering O(n^2) in the number of chunks. The trace call on\nline 350-351 also evaluates cw_out_bufs_len() a second time (even though\nCURL_TRC_WRITE may be compiled out, the argument is still evaluated when tracing\nis enabled).","fix":"Maintain a running total of buffered bytes in cw_out_ctx (incremented on append, decremented on flush, similar to cw-pause's buf_total) and compare against DYN_PAUSE_BUFFER instead of recomputing the length by walking the list each time.","locations":[{"ref":"lib/cw-out.c:346-356","code":" 346 | static CURLcode cw_out_append(struct cw_out_ctx *ctx,\n 347 | struct Curl_easy *data,\n 348 | cw_out_type otype,\n 349 | const char *buf, size_t blen)\n 350 | {\n 351 | CURL_TRC_WRITE(data, \"[OUT] paused, buffering %zu more bytes (%zu/%d)\",\n 352 | blen, cw_out_bufs_len(ctx), DYN_PAUSE_BUFFER);\n 353 | if(cw_out_bufs_len(ctx) + blen > DYN_PAUSE_BUFFER) {\n 354 | failf(data, \"pause buffer not large enough -> CURLE_TOO_LARGE\");\n 355 | return CURLE_TOO_LARGE;\n 356 | }","lines":11,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/cw-out.c#L346-L356"}]},
{"id":"PERF_2","type":"performance","type_label":"Performance","severity":"low","difficulty":"medium","description":"When `max_entries` is set, the entry-pointer array growth churns. The growth\ntest is `dynhds->hds_len + 1 >= dynhds->hds_allc`, which triggers a realloc when\nthere is still one free slot. When `nallc` is then clamped to `max_entries`\n(line 152-153), the new allocation can equal the current `hds_allc`, so every\nsubsequent add near the cap reallocates and memcpy-copies the whole array to a\nsame-sized buffer without ever growing it, until the top-of-function\n`hds_len >= max_entries` guard finally rejects the add. For a dynhds used near\nits max_entries limit this produces repeated full-array copies on the hot add\npath.","fix":"Use `dynhds->hds_len >= dynhds->hds_allc` as the growth test, and skip the realloc when the clamped `nallc` is not strictly greater than the current `hds_allc`.","locations":[{"ref":"lib/dynhds.c:148-165","code":" 148 | if(dynhds->hds_len + 1 >= dynhds->hds_allc) {\n 149 | size_t nallc = dynhds->hds_len + 16;\n 150 | struct dynhds_entry **nhds;\n 151 | \n 152 | if(dynhds->max_entries && nallc > dynhds->max_entries)\n 153 | nallc = dynhds->max_entries;\n 154 | \n 155 | nhds = curlx_calloc(nallc, sizeof(struct dynhds_entry *));\n 156 | if(!nhds)\n 157 | goto out;\n 158 | if(dynhds->hds) {\n 159 | memcpy(nhds, dynhds->hds,\n 160 | dynhds->hds_len * sizeof(struct dynhds_entry *));\n 161 | curlx_safefree(dynhds->hds);\n 162 | }\n 163 | dynhds->hds = nhds;\n 164 | dynhds->hds_allc = nallc;\n 165 | }","lines":18,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/dynhds.c#L148-L165"}]},
{"id":"PERF_3","type":"performance","type_label":"Performance","severity":"low","difficulty":"very_easy","description":"The hash slot table is allocated with the wrong element size. `h->table` is\ndeclared as `struct uint_hash_entry **` (an array of pointers), but the\nallocation uses `sizeof(*he)` where `he` has type `struct uint_hash_entry *`,\nso `*he` is the whole entry struct. This allocates\n`slots * sizeof(struct uint_hash_entry)` bytes instead of\n`slots * sizeof(struct uint_hash_entry *)`. The result is functionally correct\n(it over-allocates, never under-allocates) but wastes memory for every uint\nhash table (the struct is several times larger than a pointer). Compare with\nlib/hash.c:170 which correctly uses `sizeof(struct Curl_hash_element *)`.","fix":"Allocate `curlx_calloc(h->slots, sizeof(*h->table))` (or `sizeof(struct uint_hash_entry *)`) to size the pointer array correctly.","locations":[{"ref":"lib/uint-hash.c:120-124","code":" 120 | if(!h->table) {\n 121 | h->table = curlx_calloc(h->slots, sizeof(*he));\n 122 | if(!h->table)\n 123 | return FALSE; /* OOM */\n 124 | }","lines":5,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/uint-hash.c#L120-L124"}]},
{"id":"PERF_4","type":"performance","type_label":"Performance","severity":"low","difficulty":"medium","description":"ws_enc_write_payload() masks and writes the WebSocket payload one single byte at a time: for each input byte it XORs with the mask and calls Curl_bufq_write() with length 1. For large outgoing frames this means one bufq call (and the associated bookkeeping) per byte, which is very slow for bulk sends. The code itself acknowledges this with the comment \"not the most performant way to do this\".","fix":"Mask into a small stack buffer (e.g. 1-4 KiB) and write that chunk to the bufq in one Curl_bufq_write() call, looping until the payload is consumed, rather than writing a single byte per call. The mask index (enc->xori) can still be advanced per byte within the chunk.","locations":[{"ref":"lib/ws.c:1007-1040","code":" 1007 | static CURLcode ws_enc_write_payload(struct ws_encoder *enc,\n 1008 | struct Curl_easy *data,\n 1009 | const uint8_t *buf, size_t buflen,\n 1010 | struct bufq *out, size_t *pnwritten)\n 1011 | {\n 1012 | CURLcode result;\n 1013 | size_t i, len, n, remain;\n 1014 | \n 1015 | *pnwritten = 0;\n 1016 | if(Curl_bufq_is_full(out))\n 1017 | return CURLE_AGAIN;\n 1018 | \n 1019 | /* not the most performant way to do this */\n 1020 | len = buflen;\n 1021 | remain = curlx_sotouz_range(enc->payload_remain, 0, SIZE_MAX);\n 1022 | if(remain < len)\n 1023 | len = remain;\n 1024 | \n 1025 | for(i = 0; i < len; ++i) {\n 1026 | uint8_t c = buf[i] ^ enc->mask[enc->xori];\n 1027 | result = Curl_bufq_write(out, &c, 1, &n);\n 1028 | if(result) {\n 1029 | if((result != CURLE_AGAIN) || !i)\n 1030 | return result;\n 1031 | break;\n 1032 | }\n 1033 | enc->xori++;\n 1034 | enc->xori &= 3;\n 1035 | }\n 1036 | *pnwritten = i;\n 1037 | enc->payload_remain -= (curl_off_t)i;\n 1038 | ws_enc_info(enc, data, \"buffered\");\n 1039 | return CURLE_OK;\n 1040 | }","lines":34,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/ws.c#L1007-L1040"}]},
{"id":"PERF_5","type":"performance","type_label":"Performance","severity":"negligible","difficulty":"very_easy","description":"multipart_size() keeps iterating over all remaining subparts after one of them reports an unknown size (sz < 0 sets `size = sz`). Once `size` is negative the per-iteration body only recomputes sub-part sizes whose results are discarded, since `if(size >= 0)` is never true again. The function still returns the correct unknown (-1) value, but the work is wasted, and for deeply nested multiparts mime_size()/multipart_size() recursion is re-run needlessly.","fix":"Break out of the loop as soon as `size` becomes negative, e.g. after `size = sz;` do `break;` (the value is already final), or test `if(size < 0) break;` at the top of the loop body.","locations":[{"ref":"lib/mime.c:1570-1593","code":" 1570 | static curl_off_t multipart_size(curl_mime *mime)\n 1571 | {\n 1572 | curl_off_t size;\n 1573 | curl_off_t boundarysize;\n 1574 | curl_mimepart *part;\n 1575 | \n 1576 | if(!mime)\n 1577 | return 0; /* Not present -> empty. */\n 1578 | \n 1579 | boundarysize = 4 + MIME_BOUNDARY_LEN + 2;\n 1580 | size = boundarysize; /* Final boundary - CRLF after headers. */\n 1581 | \n 1582 | for(part = mime->firstpart; part; part = part->nextpart) {\n 1583 | curl_off_t sz = mime_size(part);\n 1584 | \n 1585 | if(sz < 0)\n 1586 | size = sz;\n 1587 | \n 1588 | if(size >= 0)\n 1589 | size += boundarysize + sz;\n 1590 | }\n 1591 | \n 1592 | return size;\n 1593 | }","lines":24,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/mime.c#L1570-L1593"}]},
{"id":"PERF_6","type":"performance","type_label":"Performance","severity":"negligible","difficulty":"medium","description":"cs_zip_to_str() re-walks the cs_txt blob from the beginning for every index it needs to render: for each output part it loops `for(idx = 1, entry = cs_txt + 1; idx < indexes[i]; idx++) entry += strlen(entry)+1;`. This makes name reconstruction O(parts * CS_TXT_LEN * avg_strlen) instead of O(total length). The lists are small so impact is negligible, but it is needless repeated linear scanning of the static text table.","fix":"Precompute an offset table for cs_txt entries once, or accumulate the scan position across iterations since indexes are processed in ascending textual order only incidentally; at minimum cache strlen results.","locations":[{"ref":"lib/vtls/cipher_suite.c:611-630","code":" 611 | for(i = 0; i < 8 && indexes[i] != 0 && len < buf_size; i++) {\n 612 | if(indexes[i] >= CS_TXT_LEN)\n 613 | return -1;\n 614 | \n 615 | /* lookup the part string for the index (skip empty string at 0) */\n 616 | for(idx = 1, entry = cs_txt + 1; idx < indexes[i]; idx++) {\n 617 | size_t elen = strlen(entry);\n 618 | entry += elen + 1;\n 619 | }\n 620 | \n 621 | /* append the part string to the buffer */\n 622 | if(i > 0)\n 623 | r = curl_msnprintf(&buf[len], buf_size - len, \"%c%s\", separator, entry);\n 624 | else\n 625 | r = curl_msnprintf(&buf[len], buf_size - len, \"%s\", entry);\n 626 | \n 627 | if(r < 0)\n 628 | return -1;\n 629 | len += r;\n 630 | }","lines":20,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vtls/cipher_suite.c#L611-L630"}]},
{"id":"DEAD_1","type":"dead_code","type_label":"Dead Code","severity":"low","difficulty":"very_easy","description":"In Curl_pp_statemach() the third branch is unreachable. The first `if` already tests `Curl_conn_data_pending(data, FIRSTSOCKET)` and sets rc=1; the later `else if(!pp->sendleft && Curl_conn_data_pending(data, FIRSTSOCKET))` can only be reached when Curl_conn_data_pending() is FALSE, so its data-pending sub-condition is always FALSE and the branch is dead. The intent was presumably to combine a different condition (e.g. checking the SSL layer separately), but as written it duplicates the first check.","fix":"Remove the redundant `else if(!pp->sendleft && Curl_conn_data_pending(...))` branch, or replace its condition with the genuinely distinct check that was intended (the comment says 'data ready in the SSL library').","locations":[{"ref":"lib/pingpong.c:86-99","code":" 86 | if(Curl_conn_data_pending(data, FIRSTSOCKET))\n 87 | rc = 1;\n 88 | else if(pp->overflow)\n 89 | /* We are receiving and there is data in the cache so read it */\n 90 | rc = 1;\n 91 | else if(!pp->sendleft && Curl_conn_data_pending(data, FIRSTSOCKET))\n 92 | /* We are receiving and there is data ready in the SSL library */\n 93 | rc = 1;\n 94 | else {\n 95 | rc = Curl_socket_check(pp->sendleft ? CURL_SOCKET_BAD : sock, /* reading */\n 96 | CURL_SOCKET_BAD,\n 97 | pp->sendleft ? sock : CURL_SOCKET_BAD, /* writing */\n 98 | interval_ms);\n 99 | }","lines":14,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/pingpong.c#L86-L99"}]},
{"id":"DEAD_2","type":"dead_code","type_label":"Dead Code","severity":"low","difficulty":"very_easy","description":"struct SMTP carries `size_t eob` and `BIT(trailing_crlf)`, and smtp_perform() writes `smtp->trailing_crlf = TRUE;` and `smtp->eob = 2;` (lines 1785-1786), but neither field is ever read anywhere in the SMTP code. End-of-body escaping is now fully handled by the cr_eob_ctx client reader (struct cr_eob_ctx has its own n_eob field). These struct members and their initialization are leftover dead code.","fix":"Remove the unused `eob` and `trailing_crlf` members from struct SMTP and delete the two assignments in smtp_perform().","locations":[{"ref":"lib/smtp.c:131-135","code":" 131 | size_t eob; /* Number of bytes of the EOB (End Of Body) that\n 132 | have been received so far */\n 133 | BIT(rcpt_had_ok); /* Whether any of RCPT TO commands (depends on\n 134 | total number of recipients) succeeded so far */\n 135 | BIT(trailing_crlf); /* Specifies if the trailing CRLF is present */","lines":5,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/smtp.c#L131-L135"},{"ref":"lib/smtp.c:1791-1794","code":" 1791 | /* Initial data character is the first character in line: it is implicitly\n 1792 | preceded by a virtual CRLF. */\n 1793 | smtp->trailing_crlf = TRUE;\n 1794 | smtp->eob = 2;","lines":4,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/smtp.c#L1791-L1794"}]},
{"id":"DEAD_3","type":"dead_code","type_label":"Dead Code","severity":"low","difficulty":"very_easy","description":"The struct curl_thrdpool field `fatal_err` (CURLcode) is never written or read\nanywhere in the code base. It is zero-initialized by curlx_calloc() in\nCurl_thrdpool_create() and then never touched. It appears to be a leftover from\nan earlier design and only adds confusion / memory to the struct.","fix":"Remove the unused `fatal_err` member from struct curl_thrdpool.","locations":[{"ref":"lib/thrdpool.c:63","code":" 63 | CURLcode fatal_err;","lines":1,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/thrdpool.c#L63"}]},
{"id":"DEAD_4","type":"dead_code","type_label":"Dead Code","severity":"low","difficulty":"easy","description":"The struct curl_thrdpool bitfield `detached` is set to TRUE in\nCurl_thrdpool_destroy() (when threads are still running and being detached) but\nis never read anywhere. It has no observable effect on behavior and is dead\nstate.","fix":"Either remove the `detached` bit and its assignment, or actually use it (e.g.\nin DEBUGASSERTs / tracing) if it was meant to guard later behavior.","locations":[{"ref":"lib/thrdpool.c:72","code":" 72 | BIT(detached);","lines":1,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/thrdpool.c#L72"},{"ref":"lib/thrdpool.c:378","code":" 378 | tpool->detached = TRUE;","lines":1,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/thrdpool.c#L378"}]},
{"id":"DEAD_5","type":"dead_code","type_label":"Dead Code","severity":"negligible","difficulty":"very_easy","description":"In http_proxy_cf_connect(), after `if(alpn)` the code uses `else if(!alpn)`. Since the only way to reach the else branch is alpn being falsy, the `!alpn` condition is always true and redundant. Inside that branch alpn is then always assigned a non-NULL fallback, so the later strcmp(alpn, ...) calls can never deref NULL, but the redundant condition is confusing dead logic.","fix":"Replace `else if(!alpn)` with a plain `else` to make it clear the branch handles the no-ALPN case unconditionally.","locations":[{"ref":"lib/http_proxy.c:586-605","code":" 586 | if(alpn)\n 587 | infof(data, \"%s: '%s' negotiated\", tunnel_type, alpn);\n 588 | else if(!alpn) {\n 589 | /* No ALPN, proxytype rules. Fake ALPN */\n 590 | infof(data, \"%s: no ALPN negotiated\", tunnel_type);\n 591 | switch(ctx->proxytype) {\n 592 | case CURLPROXY_HTTP_1_0:\n 593 | alpn = \"http/1.0\";\n 594 | break;\n 595 | case CURLPROXY_HTTPS2:\n 596 | alpn = \"h2\";\n 597 | break;\n 598 | case CURLPROXY_HTTPS3:\n 599 | alpn = \"h3\";\n 600 | break;\n 601 | default:\n 602 | alpn = \"http/1.1\";\n 603 | break;\n 604 | }\n 605 | }","lines":20,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/http_proxy.c#L586-L605"}]},
{"id":"DEAD_6","type":"dead_code","type_label":"Dead Code","severity":"negligible","difficulty":"easy","description":"redirect_url() contains a defensive `if(!base) return CURLUE_MALFORMED_INPUT;`\nimmediately after a DEBUGASSERT(base && relurl && u), but `base` is already\ndereferenced one line earlier when computing\n`protsep = base + strlen(scheme) + 3`. The NULL check is therefore dead with\nrespect to protecting that dereference: if base were NULL the crash would\nalready have happened in the strlen()/pointer arithmetic above. The check can\nnever fire for the intended NULL-base case.","fix":"Move the NULL check before the `protsep = base + ...` computation, or remove it since callers always pass a non-NULL base (as asserted).","locations":[{"ref":"lib/urlapi.c:1293-1296","code":" 1293 | const char *protsep = base + strlen(scheme) + 3;\n 1294 | DEBUGASSERT(base && relurl && u); /* all set here */\n 1295 | if(!base)\n 1296 | return CURLUE_MALFORMED_INPUT; /* should never happen */","lines":4,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/urlapi.c#L1293-L1296"}]},
{"id":"DEAD_7","type":"dead_code","type_label":"Dead Code","severity":"negligible","difficulty":"very_easy","description":"In Curl_auth_create_gssapi_security_message (GSSAPI) the variables sec_layer and max_size are validated and then forcibly reset (sec_layer &= GSSAUTH_P_NONE; max_size = 0) before being written into the outgoing message. The 'if(max_size > 0) { max_size = 0; }' block is effectively dead: it unconditionally yields 0, so the conditional and comment add no behavior. Same pattern duplicated in krb5_sspi.c.","fix":"Replace the conditional with a direct 'max_size = 0;' (or drop it and write 0 bytes directly) to remove the no-op branch.","locations":[{"ref":"lib/vauth/krb5_gssapi.c:240-246","code":" 240 | /* Process the maximum message size the server can receive */\n 241 | if(max_size > 0) {\n 242 | /* The server has told us it supports a maximum receive buffer, but as we\n 243 | do not require one unless we are encrypting data, we tell the server\n 244 | our receive buffer is zero. */\n 245 | max_size = 0;\n 246 | }","lines":7,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vauth/krb5_gssapi.c#L240-L246"},{"ref":"lib/vauth/krb5_sspi.c:323-329","code":" 323 | /* Process the maximum message size the server can receive */\n 324 | if(max_size > 0) {\n 325 | /* The server has told us it supports a maximum receive buffer, but as we\n 326 | do not require one unless we are encrypting data, we tell the server\n 327 | our receive buffer is zero. */\n 328 | max_size = 0;\n 329 | }","lines":7,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vauth/krb5_sspi.c#L323-L329"}]},
{"id":"DEAD_8","type":"dead_code","type_label":"Dead Code","severity":"negligible","difficulty":"very_easy","description":"In easysrc_perform(), the inner condition `if(easysrc_toohard && !result)` re-tests `easysrc_toohard` even though the whole block is already guarded by `if(easysrc_toohard)` and `easysrc_toohard` is not modified in between. The `easysrc_toohard &&` part of the inner test is always true and is redundant.","fix":"Simplify the inner condition to `if(!result)`.","locations":[{"ref":"src/tool_easysrc.c:138-149","code":" 138 | if(easysrc_toohard) {\n 139 | int i;\n 140 | struct curl_slist *ptr;\n 141 | result = easysrc_add(&easysrc_code, \"\");\n 142 | /* Preamble comment */\n 143 | for(i = 0; srchard[i] && !result; i++)\n 144 | result = easysrc_add(&easysrc_code, srchard[i]);\n 145 | /* Each unconverted option */\n 146 | if(easysrc_toohard && !result) {\n 147 | for(ptr = easysrc_toohard->first; ptr && !result; ptr = ptr->next)\n 148 | result = easysrc_add(&easysrc_code, ptr->data);\n 149 | }","lines":12,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/src/tool_easysrc.c#L138-L149"}]},
{"id":"CMT_1","type":"invalid_comment","type_label":"Invalid Comment","severity":"low","difficulty":"very_easy","description":"Misleading failure message: the error text claims `nghttp2_session_upgrade2()` failed, but this branch handles a negative return from `nghttp2_submit_request()` (called a few lines above). The wrong function name will mislead anyone debugging proxy CONNECT submission failures.","fix":"Change the message to reference nghttp2_submit_request(), e.g. `failf(data, \"nghttp2_submit_request() failed: %s(%d)\", nghttp2_strerror(stream_id), stream_id);`.","locations":[{"ref":"lib/cf-h2-proxy.c:750-753","code":" 750 | if(stream_id < 0) {\n 751 | failf(data, \"nghttp2_session_upgrade2() failed: %s(%d)\",\n 752 | nghttp2_strerror(stream_id), stream_id);\n 753 | result = CURLE_SEND_ERROR;","lines":4,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/cf-h2-proxy.c#L750-L753"}]},
{"id":"CMT_2","type":"invalid_comment","type_label":"Invalid Comment","severity":"low","difficulty":"very_easy","description":"checkmonth() comment claims it returns \"the offset or -1, no real offset is -1\", but the function returns the month index 0-11 (a real, valid value of 0 is possible) or -1; the \"offset\" wording is copy-pasted from a timezone-style helper and is misleading for a month-index function. Minor, but can confuse future maintainers about the meaning of the 0 return.","fix":"Reword the trailing comment to: \"return the month index (0-11) or -1 if not a month\".","locations":[{"ref":"lib/parsedate.c:215-228","code":" 215 | static int checkmonth(const char *check, size_t len)\n 216 | {\n 217 | int i;\n 218 | const char * const *what = &Curl_month[0];\n 219 | if(len != 3)\n 220 | return -1; /* not a month */\n 221 | \n 222 | for(i = 0; i < 12; i++) {\n 223 | if(curl_strnequal(check, what[0], 3))\n 224 | return i;\n 225 | what++;\n 226 | }\n 227 | return -1; /* return the offset or -1, no real offset is -1 */\n 228 | }","lines":14,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/parsedate.c#L215-L228"}]},
{"id":"CMT_3","type":"invalid_comment","type_label":"Invalid Comment","severity":"negligible","difficulty":"very_easy","description":"In Curl_ntlm_core_mk_ntlmv2_resp the parameter documentation for 'ntresp' is an incomplete sentence: 'The address where a pointer to newly allocated memory holding the NTLMv2 response.' (missing a verb such as 'is stored').","fix":"Complete the sentence, e.g. 'The address where a pointer to newly allocated memory holding the NTLMv2 response is stored.'","locations":[{"ref":"lib/curl_ntlm_core.c:543-544","code":" 543 | * memory holding the NTLMv2 response.\n 544 | * ntresp_len [out] - The length of the output message.","lines":2,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/curl_ntlm_core.c#L543-L544"}]},
{"id":"CMT_4","type":"invalid_comment","type_label":"Invalid Comment","severity":"negligible","difficulty":"very_easy","description":"The trace message in cw_out_append() always says \"[OUT] paused, buffering ...\"\nbut cw_out_append() is also called from cw_out_do_write() on the non-paused\npartial-write path (when the client callback consumed only part of the data\nwithout requesting a pause). In that case data is being buffered while the\ntransfer is NOT paused, so the \"paused\" wording in the trace is misleading.","fix":"Reword the trace to e.g. \"[OUT] buffering %zu more bytes\" without asserting that the transfer is paused, or only emit the \"paused\" variant when ctx->paused is set.","locations":[{"ref":"lib/cw-out.c:346-353","code":" 346 | static CURLcode cw_out_append(struct cw_out_ctx *ctx,\n 347 | struct Curl_easy *data,\n 348 | cw_out_type otype,\n 349 | const char *buf, size_t blen)\n 350 | {\n 351 | CURL_TRC_WRITE(data, \"[OUT] paused, buffering %zu more bytes (%zu/%d)\",\n 352 | blen, cw_out_bufs_len(ctx), DYN_PAUSE_BUFFER);\n 353 | if(cw_out_bufs_len(ctx) + blen > DYN_PAUSE_BUFFER) {","lines":8,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/cw-out.c#L346-L353"}]},
{"id":"CMT_5","type":"invalid_comment","type_label":"Invalid Comment","severity":"negligible","difficulty":"very_easy","description":"parse_unix_totalsize() has the comment `/* continue to fall through */` after\nsetting parser->state.UNIX.main = PL_UNIX_FILETYPE inside the\nPL_UNIX_TOTALSIZE_INIT case, but the code then `break;`s out of the switch\nrather than falling through. The actual fall-through to PL_UNIX_FILETYPE happens\none level up in parse_unix() (which re-dispatches after detecting the main-state\nchange), not here. The comment is misleading about local control flow.","fix":"Reword the comment to clarify the re-dispatch happens in parse_unix(), not via a C fall-through here.","locations":[{"ref":"lib/ftplistparser.c:400-404","code":" 400 | else {\n 401 | parser->state.UNIX.main = PL_UNIX_FILETYPE;\n 402 | /* continue to fall through */\n 403 | }\n 404 | break;","lines":5,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/ftplistparser.c#L400-L404"}]},
{"id":"CMT_6","type":"invalid_comment","type_label":"Invalid Comment","severity":"negligible","difficulty":"very_easy","description":"Comment in http_proxy.c says 'colons in IPv6 addresses MUST be percent-encoded ... (e.g. \"2001:db8::1\" -> \"2001%3Adb8%3A%3A1\")'. The encoding loop emits '%3A' for each ':' using uppercase hex, but RFC 9298 percent-encoding example aside, the surrounding code only handles ':' and leaves other reserved characters unencoded; the comment implies general percent-encoding of the host while the implementation only escapes colons. Minor: the comment overstates what the code does (it is colon-only escaping, not full RFC 3986 percent-encoding of the IPv6 literal).","fix":"Reword the comment to state that only ':' is percent-encoded here (sufficient for IPv6 literals in this URI template position), to avoid implying broader encoding than is implemented.","locations":[{"ref":"lib/http_proxy.c:307-332","code":" 307 | if(dest->ipv6) {\n 308 | /* RFC 9298: colons in IPv6 addresses MUST be percent-encoded\n 309 | * in the URI template (e.g. \"2001:db8::1\" -> \"2001%3Adb8%3A%3A1\") */\n 310 | const char *s = dest->hostname;\n 311 | char *d;\n 312 | size_t hlen = strlen(s);\n 313 | encoded_host = curlx_malloc(hlen * 3 + 1);\n 314 | if(!encoded_host) {\n 315 | result = CURLE_OUT_OF_MEMORY;\n 316 | goto out;\n 317 | }\n 318 | d = encoded_host;\n 319 | while(*s) {\n 320 | if(*s == ':') {\n 321 | *d++ = '%';\n 322 | *d++ = '3';\n 323 | *d++ = 'A';\n 324 | }\n 325 | else\n 326 | *d++ = *s;\n 327 | s++;\n 328 | }\n 329 | *d = '\\0';\n 330 | path = curl_maprintf(\"/.well-known/masque/udp/%s/%u/\",\n 331 | encoded_host, (unsigned int)dest->port);\n 332 | }","lines":26,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/http_proxy.c#L307-L332"}]},
{"id":"CMT_7","type":"invalid_comment","type_label":"Invalid Comment","severity":"negligible","difficulty":"easy","description":"The comment for SASL_FLAG_BASE64 handling in build_message() says 'Explicit empty response' sets msg to '=', but the preceding decode path in get_server_message() treats a leading '=' as an empty server message. The comment in pop3_get_message header block also claims it detects 'the supported authentication types and allowed SASL mechanisms', yet pop3_get_message() only extracts the trimmed message text; capability/mech parsing happens in pop3_state_capa_resp(). The doc comment is attached to the wrong function and is misleading about its responsibilities.","fix":"Correct the pop3_get_message() banner comment to describe only message extraction; move the capability-detection description to pop3_endofresp/pop3_state_capa_resp.","locations":[{"ref":"lib/pop3.c:340-346","code":" 340 | /***********************************************************************\n 341 | *\n 342 | * pop3_get_message()\n 343 | *\n 344 | * Gets the authentication message from the response buffer.\n 345 | */\n 346 | static CURLcode pop3_get_message(struct Curl_easy *data, struct bufref *out)","lines":7,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/pop3.c#L340-L346"}]},
{"id":"CMT_8","type":"invalid_comment","type_label":"Invalid Comment","severity":"negligible","difficulty":"very_easy","description":"In cidr4_match() the locals are named `haddr`/`hcheck` (suggesting host byte\norder) and are produced via htonl(). `address`/`check` come from\ncurlx_inet_pton(AF_INET, ...) which already returns network byte order, so the\ncorrect conversion to host order is ntohl(), not htonl(). The code happens to\nwork because on every supported platform htonl() and ntohl() perform the same\ntransformation (identity on big-endian, byte-swap on little-endian) and the\nmask is applied symmetrically to both operands, so the XOR/mask comparison is\ncorrect. The use of htonl() to obtain a value called \"host address\" is however\nmisleading and inconsistent with the variable naming and the standard idiom.","fix":"Use ntohl() instead of htonl() to convert the network-order pton results to host order, matching the haddr/hcheck naming; behavior is unchanged but intent is clear.","locations":[{"ref":"lib/proxy.c:72-86","code":" 72 | if(bits && (bits != 32)) {\n 73 | unsigned int mask = 0xffffffff << (32 - bits);\n 74 | unsigned int haddr = htonl(address);\n 75 | unsigned int hcheck = htonl(check);\n 76 | #if 0\n 77 | curl_mfprintf(stderr, \"Host %s (%x) network %s (%x) \"\n 78 | \"bits %u mask %x => %x\\n\",\n 79 | ipv4, haddr, network, hcheck, bits, mask,\n 80 | (haddr ^ hcheck) & mask);\n 81 | #endif\n 82 | if((haddr ^ hcheck) & mask)\n 83 | return FALSE;\n 84 | return TRUE;\n 85 | }\n 86 | return address == check;","lines":15,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/proxy.c#L72-L86"}]},
{"id":"CMT_9","type":"invalid_comment","type_label":"Invalid Comment","severity":"negligible","difficulty":"very_easy","description":"The closing #endif comment for the SMB feature block claims an extra condition `SIZEOF_CURL_OFF_T > 4` that does not exist in the matching #if at the top of the file. The actual guard is `#if defined(CURL_ENABLE_SMB) && defined(USE_CURL_NTLM_CORE)` (line 28), but the trailing comment reads `#endif /* CURL_ENABLE_SMB && USE_CURL_NTLM_CORE && SIZEOF_CURL_OFF_T > 4 */`. The comment is misleading about the compilation condition.","fix":"Update the trailing comment to match the actual guard: `#endif /* CURL_ENABLE_SMB && USE_CURL_NTLM_CORE */`.","locations":[{"ref":"lib/smb.c:28","code":" 28 | #if defined(CURL_ENABLE_SMB) && defined(USE_CURL_NTLM_CORE)","lines":1,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/smb.c#L28"}]},
{"id":"CMT_10","type":"invalid_comment","type_label":"Invalid Comment","severity":"negligible","difficulty":"very_easy","description":"The comment on the wrap-around scan in `Curl_uint32_tbl_add` references a field\nname that does not exist: 'no free entry at or above tbl->maybe_next_key'. The\ncode actually uses `tbl->last_key_added` (via `start_pos`). The stale field name\nsuggests a renamed member that was not propagated to the comment.","fix":"Update the comment to refer to `tbl->last_key_added`/`start_pos`.","locations":[{"ref":"lib/uint-table.c:135-136","code":" 135 | /* no free entry at or above tbl->maybe_next_key, wrap around */\n 136 | for(key = 0; key < start_pos; ++key) {","lines":2,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/uint-table.c#L135-L136"}]},
{"id":"CMT_11","type":"invalid_comment","type_label":"Invalid Comment","severity":"negligible","difficulty":"very_easy","description":"The function header comment for Curl_auth_decode_ntlm_type2_message contains stray asterisks and broken wording: 'is checked * for validity ... a type-3 message is * written'. The misplaced '*' characters are leftover/garbled and make the doc comment read incorrectly.","fix":"Remove the stray '*' tokens so the sentence reads 'The raw NTLM message is checked for validity before the appropriate data for creating a type-3 message is written to the given NTLM data structure.'","locations":[{"ref":"lib/vauth/ntlm.c:320-334","code":" 320 | /*\n 321 | * Curl_auth_decode_ntlm_type2_message()\n 322 | *\n 323 | * This is used to decode an NTLM type-2 message. The raw NTLM message is\n 324 | * checked * for validity before the appropriate data for creating a type-3\n 325 | * message is * written to the given NTLM data structure.\n 326 | *\n 327 | * Parameters:\n 328 | *\n 329 | * data [in] - The session handle.\n 330 | * type2ref [in] - The type-2 message.\n 331 | * ntlm [in/out] - The NTLM data struct being used and modified.\n 332 | *\n 333 | * Returns CURLE_OK on success.\n 334 | */","lines":15,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vauth/ntlm.c#L320-L334"}]},
{"id":"CMT_12","type":"invalid_comment","type_label":"Invalid Comment","severity":"negligible","difficulty":"very_easy","description":"The doc comment for Curl_async_global_init() in asyn-thrdd.c states \"Does nothing here.\" but the function does perform work when built with c-ares support: it calls ares_library_init(ARES_LIB_INIT_ALL) and returns CURLE_FAILED_INIT on failure. The same misleading \"Does nothing here.\" comment is repeated on Curl_async_global_cleanup() which calls ares_library_cleanup().","fix":"Remove or correct the \"Does nothing here.\" sentences to reflect that the functions initialize/cleanup the c-ares library when USE_ARES is defined.","locations":[{"ref":"lib/vdns/asyn-thrdd.c:75-100","code":" 75 | /*\n 76 | * Curl_async_global_init()\n 77 | * Called from curl_global_init() to initialize global resolver environment.\n 78 | * Does nothing here.\n 79 | */\n 80 | int Curl_async_global_init(void)\n 81 | {\n 82 | #if defined(USE_ARES) && defined(CARES_HAVE_ARES_LIBRARY_INIT)\n 83 | if(ares_library_init(ARES_LIB_INIT_ALL)) {\n 84 | return CURLE_FAILED_INIT;\n 85 | }\n 86 | #endif\n 87 | return CURLE_OK;\n 88 | }\n 89 | \n 90 | /*\n 91 | * Curl_async_global_cleanup()\n 92 | * Called from curl_global_cleanup() to destroy global resolver environment.\n 93 | * Does nothing here.\n 94 | */\n 95 | void Curl_async_global_cleanup(void)\n 96 | {\n 97 | #if defined(USE_ARES) && defined(CARES_HAVE_ARES_LIBRARY_INIT)\n 98 | ares_library_cleanup();\n 99 | #endif\n 100 | }","lines":26,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vdns/asyn-thrdd.c#L75-L100"}]},
{"id":"CMT_13","type":"invalid_comment","type_label":"Invalid Comment","severity":"negligible","difficulty":"very_easy","description":"The comment block in Curl_loadhostpairs() enumerates \"two reasons\" for deleting an existing cache entry but then lists four numbered reasons (1..4). The prose (\"there are two reasons for this\") is stale/incorrect relative to the list it introduces.","fix":"Update the comment to say \"there are several reasons\" (or the correct count) to match the enumerated list.","locations":[{"ref":"lib/vdns/dnscache.c:967-976","code":" 967 | /* delete old entry, there are two reasons for this\n 968 | 1. old entry may have different addresses.\n 969 | 2. even if entry with correct addresses is already in the cache,\n 970 | but if it is close to expire, then by the time next http\n 971 | request is made, it can get expired and pruned because old\n 972 | entry is not necessarily marked as permanent.\n 973 | 3. when adding a non-permanent entry, we want it to remove and\n 974 | replace an existing permanent entry.\n 975 | 4. when adding a non-permanent entry, we want it to get a \"fresh\"\n 976 | timeout that starts _now_. */","lines":10,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vdns/dnscache.c#L967-L976"}]},
{"id":"CMT_14","type":"invalid_comment","type_label":"Invalid Comment","severity":"negligible","difficulty":"very_easy","description":"The closing comment of the sockopt feature block reads `/* IP_TOD || IPV6_TCLASS || SO_PRIORITY */` — 'IP_TOD' is a typo for 'IP_TOS', the macro actually tested in the matching `#if defined(IP_TOS) || defined(IPV6_TCLASS) || defined(SO_PRIORITY)`.","fix":"Fix the comment to read `/* IP_TOS || IPV6_TCLASS || SO_PRIORITY */`.","locations":[{"ref":"src/config2setopts.c:122","code":" 122 | #endif /* IP_TOD || IPV6_TCLASS || SO_PRIORITY */","lines":1,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/src/config2setopts.c#L122"}]},
{"id":"DUP_1","type":"duplicate_code","type_label":"Duplicate Code","severity":"negligible","difficulty":"medium","description":"ascii_to_unicode_le() in curl_ntlm_core.c and unicodecpy() in ntlm.c are byte-for-byte identical (copy src into dest interleaving zero bytes for little-endian UTF-16). The same routine also appears as the body of ascii_uppercase_to_unicode_le minus the toupper. Maintaining three near-identical copies risks divergence.","fix":"Consolidate into a single shared helper (e.g. in curl_ntlm_core) and have ntlm.c call it, parameterizing the optional uppercase transform.","locations":[{"ref":"lib/curl_ntlm_core.c:396-404","code":" 396 | static void ascii_to_unicode_le(unsigned char *dest, const char *src,\n 397 | size_t srclen)\n 398 | {\n 399 | size_t i;\n 400 | for(i = 0; i < srclen; i++) {\n 401 | dest[2 * i] = (unsigned char)src[i];\n 402 | dest[(2 * i) + 1] = '\\0';\n 403 | }\n 404 | }","lines":9,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/curl_ntlm_core.c#L396-L404"},{"ref":"lib/vauth/ntlm.c:396-403","code":" 396 | static void unicodecpy(unsigned char *dest, const char *src, size_t length)\n 397 | {\n 398 | size_t i;\n 399 | for(i = 0; i < length; i++) {\n 400 | dest[2 * i] = (unsigned char)src[i];\n 401 | dest[(2 * i) + 1] = '\\0';\n 402 | }\n 403 | }","lines":8,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vauth/ntlm.c#L396-L403"}]},
{"id":"OTHER_1","type":"other","type_label":"Other","severity":"low","difficulty":"easy","description":"Potential NULL dereference of ctx->dest in the HTTP/2 proxy CF_QUERY_HOST_PORT handler. It dereferences `ctx->dest->port` and `ctx->dest->hostname` without any NULL check, whereas the analogous HTTP/1 handler (cf_h1_proxy_query) guards with `if(!ts || !ts->dest) break;`. While ctx->dest is normally linked in Curl_cf_h2_proxy_insert_after(), the inconsistent lack of a guard makes this fragile if the query is ever issued before/after dest is set.","fix":"Mirror the h1 handler and guard: `if(!ctx->dest) break;` before dereferencing ctx->dest in the CF_QUERY_HOST_PORT case.","locations":[{"ref":"lib/cf-h2-proxy.c:1419-1422","code":" 1419 | case CF_QUERY_HOST_PORT:\n 1420 | *pres1 = (int)ctx->dest->port;\n 1421 | *((const char **)pres2) = ctx->dest->hostname;\n 1422 | return CURLE_OK;","lines":4,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/cf-h2-proxy.c#L1419-L1422"},{"ref":"lib/cf-h1-proxy.c:918-923","code":" 918 | case CF_QUERY_HOST_PORT:\n 919 | if(!ts || !ts->dest)\n 920 | break;\n 921 | *pres1 = (int)ts->dest->port;\n 922 | *((const char **)pres2) = ts->dest->hostname;\n 923 | return CURLE_OK;","lines":6,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/cf-h1-proxy.c#L918-L923"}]},
{"id":"OTHER_2","type":"other","type_label":"Other","severity":"low","difficulty":"medium","description":"extend_key_56_to_64() and the DES setup paths treat NTLM password-derived key material; Curl_ntlm_core_mk_lm_hash leaves the working buffer 'pw' (containing the uppercased password) on the stack without zeroing it before return. Similarly the v1 NTLM path in Curl_auth_create_ntlm_type3_message keeps ntbuffer/lmbuffer (password hashes) on the stack without scrubbing. Sensitive key material is not wiped, unlike the local md4/md5/sha512_256 finishers which memset their contexts.","fix":"Zero stack buffers holding password-derived material (pw, ntbuffer, lmbuffer, ntlmv2hash) with curlx_memzero before the functions return.","locations":[{"ref":"lib/curl_ntlm_core.c:348-394","code":" 348 | CURLcode Curl_ntlm_core_mk_lm_hash(const char *password,\n 349 | unsigned char *lmbuffer /* 21 bytes */)\n 350 | {\n 351 | unsigned char pw[14];\n 352 | static const unsigned char magic[] = {\n 353 | 0x4B, 0x47, 0x53, 0x21, 0x40, 0x23, 0x24, 0x25 /* i.e. KGS!@#$% */\n 354 | };\n 355 | size_t len = CURLMIN(strlen(password), 14);\n 356 | \n 357 | Curl_strntoupper((char *)pw, password, len);\n 358 | memset(&pw[len], 0, 14 - len);\n 359 | \n 360 | {\n 361 | /* Create LanManager hashed password. */\n 362 | #ifdef USE_OPENSSL_DES\n 363 | DES_key_schedule ks;\n 364 | \n 365 | setup_des_key(pw, &ks);\n 366 | DES_ecb_encrypt((DES_cblock *)CURL_UNCONST(magic),\n 367 | (DES_cblock *)lmbuffer, &ks, DES_ENCRYPT);\n 368 | \n 369 | setup_des_key(pw + 7, &ks);\n 370 | DES_ecb_encrypt((DES_cblock *)CURL_UNCONST(magic),\n 371 | (DES_cblock *)(lmbuffer + 8), &ks, DES_ENCRYPT);\n 372 | #elif defined(USE_WOLFSSL_DES)\n 373 | Des des;\n 374 | setup_des_key(pw, &des);\n 375 | wc_Des_EcbEncrypt(&des, lmbuffer, magic, DES_KEY_SIZE);\n 376 | setup_des_key(pw + 7, &des);\n 377 | wc_Des_EcbEncrypt(&des, lmbuffer + 8, magic, DES_KEY_SIZE);\n 378 | #elif defined(USE_GNUTLS_DES)\n 379 | struct des_ctx des;\n 380 | setup_des_key(pw, &des);\n 381 | des_encrypt(&des, 8, lmbuffer, magic);\n 382 | setup_des_key(pw + 7, &des);\n 383 | des_encrypt(&des, 8, lmbuffer + 8, magic);\n 384 | #elif defined(USE_MBEDTLS_DES) || defined(USE_OS400CRYPTO) || \\\n 385 | defined(USE_WIN32_CRYPTO)\n 386 | encrypt_des(magic, lmbuffer, pw);\n 387 | encrypt_des(magic, lmbuffer + 8, pw + 7);\n 388 | #endif\n 389 | \n 390 | memset(lmbuffer + 16, 0, 21 - 16);\n 391 | }\n 392 | \n 393 | return CURLE_OK;\n 394 | }","lines":47,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/curl_ntlm_core.c#L348-L394"},{"ref":"lib/vauth/ntlm.c:609-668","code":" 609 | if(ntlm->flags & NTLMFLAG_NEGOTIATE_NTLM2_KEY) {\n 610 | unsigned char ntbuffer[0x18];\n 611 | unsigned char entropy[8];\n 612 | unsigned char ntlmv2hash[0x18];\n 613 | \n 614 | /* Full NTLM version 2\n 615 | Although this cannot be negotiated, it is used here if available, as\n 616 | servers featuring extended security are likely supporting also\n 617 | NTLMv2. */\n 618 | result = Curl_rand(data, entropy, 8);\n 619 | if(result)\n 620 | return result;\n 621 | \n 622 | result = Curl_ntlm_core_mk_nt_hash(passwdp, ntbuffer);\n 623 | if(result)\n 624 | return result;\n 625 | \n 626 | result = Curl_ntlm_core_mk_ntlmv2_hash(user, userlen, domain, domlen,\n 627 | ntbuffer, ntlmv2hash);\n 628 | if(result)\n 629 | return result;\n 630 | \n 631 | /* LMv2 response */\n 632 | result = Curl_ntlm_core_mk_lmv2_resp(ntlmv2hash, entropy,\n 633 | &ntlm->nonce[0], lmresp);\n 634 | if(result)\n 635 | return result;\n 636 | \n 637 | /* NTLMv2 response */\n 638 | result = Curl_ntlm_core_mk_ntlmv2_resp(ntlmv2hash, entropy,\n 639 | ntlm, &ntlmv2resp, &ntresplen);\n 640 | if(result)\n 641 | return result;\n 642 | \n 643 | ptr_ntresp = ntlmv2resp;\n 644 | }\n 645 | else {\n 646 | \n 647 | unsigned char ntbuffer[0x18];\n 648 | unsigned char lmbuffer[0x18];\n 649 | \n 650 | /* NTLM version 1 */\n 651 | \n 652 | result = Curl_ntlm_core_mk_nt_hash(passwdp, ntbuffer);\n 653 | if(result)\n 654 | return result;\n 655 | \n 656 | Curl_ntlm_core_lm_resp(ntbuffer, &ntlm->nonce[0], ntresp);\n 657 | \n 658 | result = Curl_ntlm_core_mk_lm_hash(passwdp, lmbuffer);\n 659 | if(result)\n 660 | return result;\n 661 | \n 662 | Curl_ntlm_core_lm_resp(lmbuffer, &ntlm->nonce[0], lmresp);\n 663 | ntlm->flags &= ~(unsigned int)NTLMFLAG_NEGOTIATE_NTLM2_KEY;\n 664 | \n 665 | /* A safer but less compatible alternative is:\n 666 | * Curl_ntlm_core_lm_resp(ntbuffer, &ntlm->nonce[0], lmresp);\n 667 | * See https://davenport.sourceforge.net/ntlm.html#ntlmVersion2 */\n 668 | }","lines":60,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vauth/ntlm.c#L609-L668"}]},
{"id":"OTHER_3","type":"other","type_label":"Other","severity":"low","difficulty":"very_easy","description":"`Curl_hash_str` casts each key byte via `size_t j = (size_t)*key_str++;` where\n`*key_str` is a plain `char`. On platforms where `char` is signed, bytes >= 0x80\nsign-extend to a huge `size_t` before being XORed into the accumulator. The hash\nremains self-consistent within a single build (so lookups still work), but the\nslot distribution differs between signed-char and unsigned-char platforms and\nhigh-bit bytes contribute pathologically large values. Cast through\n`unsigned char` for a portable, well-distributed hash.","fix":"Use `size_t j = (size_t)(unsigned char)*key_str++;`.","locations":[{"ref":"lib/hash.c:409-422","code":" 409 | size_t Curl_hash_str(void *key, size_t key_length, size_t slots_num)\n 410 | {\n 411 | const char *key_str = (const char *)key;\n 412 | const char *end = key_str + key_length;\n 413 | size_t h = 5381;\n 414 | \n 415 | while(key_str < end) {\n 416 | size_t j = (size_t)*key_str++;\n 417 | h += h << 5;\n 418 | h ^= j;\n 419 | }\n 420 | \n 421 | return (h % slots_num);\n 422 | }","lines":14,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/hash.c#L409-L422"}]},
{"id":"OTHER_4","type":"other","type_label":"Other","severity":"low","difficulty":"very_easy","description":"Leftover debug/junk log message shipped in production code. In the MQTT_PUB_REMAIN handling of mqtt_read_publish(), when Curl_xfer_recv() returns CURLE_AGAIN the code logs the nonsensical string \"EEEE AAAAGAIN\" via infof(). This is clearly a leftover debugging artifact (note the misspelling and shouting caps) that ends up in the user-visible verbose output of a released library. It provides no actionable information to a user and looks unprofessional / confusing.","fix":"Remove the EEEE AAAAGAIN infof() (the whole `if(result == CURLE_AGAIN)` block), or replace it with a meaningful trace message such as `CURL_TRC_*(data, \"PUBLISH read again\")`.","locations":[{"ref":"lib/mqtt.c:731-735","code":" 731 | if(result) {\n 732 | if(result == CURLE_AGAIN) {\n 733 | infof(data, \"EEEE AAAAGAIN\");\n 734 | }\n 735 | goto end;","lines":5,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/mqtt.c#L731-L735"}]},
{"id":"OTHER_5","type":"other","type_label":"Other","severity":"low","difficulty":"easy","description":"Curl_auth_create_spnego_message() (SSPI variant) frees *outptr on the failure path but does not reset *outptr to NULL afterwards. When base64 encoding yields a non-NULL pointer but zero length, the function frees that buffer and returns CURLE_REMOTE_ACCESS_DENIED while leaving *outptr pointing at freed memory. The GSSAPI counterpart consistently nulls out its token pointer/length on failure; this path should do the same to avoid a dangling pointer being observed by the caller.","fix":"After curlx_free(*outptr) set *outptr = NULL (and *outlen = 0) before returning the error, mirroring the GSSAPI implementation.","locations":[{"ref":"lib/vauth/spnego_sspi.c:312-325","code":" 312 | CURLcode Curl_auth_create_spnego_message(struct negotiatedata *nego,\n 313 | char **outptr, size_t *outlen)\n 314 | {\n 315 | /* Base64 encode the already generated response */\n 316 | CURLcode result = curlx_base64_encode(nego->output_token,\n 317 | nego->output_token_length, outptr,\n 318 | outlen);\n 319 | if(!result && (!*outptr || !*outlen)) {\n 320 | curlx_free(*outptr);\n 321 | result = CURLE_REMOTE_ACCESS_DENIED;\n 322 | }\n 323 | \n 324 | return result;\n 325 | }","lines":14,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vauth/spnego_sspi.c#L312-L325"}]},
{"id":"OTHER_6","type":"other","type_label":"Other","severity":"low","difficulty":"very_easy","description":"In Curl_vtls_apple_verify(), the error-description buffer is allocated as curlx_malloc(size + 1) but CFStringGetCString() is then called with bufferSize == size (not size + 1). CFStringGetCString treats its bufferSize argument as the total available bytes including the NUL terminator, so passing `size` under-reports the real buffer by one byte and can cause CFStringGetCString to fail (returning false) for a description whose UTF-8 form needs exactly `size` bytes plus the terminator. Not a buffer overflow, but a correctness/robustness bug that may drop the error text.","fix":"Pass the actual allocated size: `CFStringGetCString(error_ref, err_desc, size + 1, kCFStringEncodingUTF8)`.","locations":[{"ref":"lib/vtls/apple.c:241-248","code":" 241 | CFIndex size = CFStringGetMaximumSizeForEncoding(\n 242 | CFStringGetLength(error_ref), kCFStringEncodingUTF8);\n 243 | err_desc = curlx_malloc(size + 1);\n 244 | if(err_desc) {\n 245 | if(!CFStringGetCString(error_ref, err_desc, size,\n 246 | kCFStringEncodingUTF8))\n 247 | curlx_safefree(err_desc);\n 248 | }","lines":8,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vtls/apple.c#L241-L248"}]},
{"id":"OTHER_7","type":"other","type_label":"Other","severity":"negligible","difficulty":"easy","description":"`Curl_memrchr` decrements `p` past the start of the buffer on the final\nunsuccessful iteration: when `p == q == s` and no match, `p--` makes `p` point to\nthe byte before `s`, and the loop condition `p >= q` then evaluates that pointer.\nForming and comparing a pointer before the start of an array is technically\nundefined behavior in C (only one-past-the-end is permitted). The value is never\ndereferenced and the idiom works on all mainstream targets, but a strict\noptimizer is permitted to misbehave.","fix":"Iterate with an index instead of a pre-start pointer, e.g. `for(i = n; i-- > 0;) if(p[i] == (unsigned char)c) return CURL_UNCONST(&p[i]);`.","locations":[{"ref":"lib/curl_memrchr.c:43-49","code":" 43 | p += n - 1;\n 44 | \n 45 | while(p >= q) {\n 46 | if(*p == (unsigned char)c)\n 47 | return CURL_UNCONST(p);\n 48 | p--;\n 49 | }","lines":7,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/curl_memrchr.c#L43-L49"}]},
{"id":"OTHER_8","type":"other","type_label":"Other","severity":"negligible","difficulty":"medium","description":"Curl_getdate_capped() treats PARSEDATE_LATER and PARSEDATE_SOONER (time_t overflow/underflow at the ends of the representable range) as success (it only returns non-zero for PARSEDATE_FAIL). For PARSEDATE_SOONER the output is clamped to TIME_T_MIN / 0, meaning a date that underflows becomes a very-past or zero timestamp that callers (cookie/hsts/altsvc) interpret as \"expired/now\" rather than an error. This is the intended \"capped\" behavior for the LATER case, but the SOONER case silently turning a parse-range issue into a near-zero expiry is easy to overlook and is not documented in the function comment.","fix":"Document that Curl_getdate_capped() also returns success for under/overflow (clamping to TIME_T_MIN/TIME_T_MAX), or explicitly treat PARSEDATE_SOONER as an error if callers should reject under-range dates.","locations":[{"ref":"lib/parsedate.c:598-606","code":" 598 | /* Curl_getdate_capped() differs from curl_getdate() in that this will return\n 599 | TIME_T_MAX in case the parsed time value was too big, instead of an\n 600 | error. Returns non-zero on error. */\n 601 | \n 602 | int Curl_getdate_capped(const char *p, time_t *tp)\n 603 | {\n 604 | int rc = parsedate(p, tp);\n 605 | return (rc == PARSEDATE_FAIL);\n 606 | }","lines":9,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/parsedate.c#L598-L606"},{"ref":"lib/parsedate.c:518-537","code":" 518 | static int mktimet(curl_off_t seconds, time_t *output)\n 519 | {\n 520 | #if SIZEOF_TIME_T < 5\n 521 | if(seconds > TIME_T_MAX) {\n 522 | *output = TIME_T_MAX;\n 523 | return PARSEDATE_LATER;\n 524 | }\n 525 | else if(seconds < TIME_T_MIN) {\n 526 | *output = TIME_T_MIN;\n 527 | return PARSEDATE_SOONER;\n 528 | }\n 529 | #elif defined(HAVE_TIME_T_UNSIGNED)\n 530 | if(seconds < 0) {\n 531 | *output = 0;\n 532 | return PARSEDATE_SOONER;\n 533 | }\n 534 | #endif\n 535 | *output = (time_t)seconds;\n 536 | return PARSEDATE_OK;\n 537 | }","lines":20,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/parsedate.c#L518-L537"}]},
{"id":"OTHER_9","type":"other","type_label":"Other","severity":"negligible","difficulty":"easy","description":"ntlm_print_hex() takes a FILE* 'handle' parameter, marks it (void)handle, and then unconditionally writes to stderr instead of the passed handle. All callers pass stderr so behavior is correct, but the parameter is misleading dead state and the (void)handle plus hardcoded stderr is inconsistent with ntlm_print_flags() which honors its handle argument. Debug-only code (DEBUG_ME).","fix":"Either use the 'handle' argument in the curl_mfprintf calls or drop the parameter to avoid the misleading signature.","locations":[{"ref":"lib/vauth/ntlm.c:228-237","code":" 228 | static void ntlm_print_hex(FILE *handle, const char *buf, size_t len)\n 229 | {\n 230 | const char *p = buf;\n 231 | \n 232 | (void)handle;\n 233 | \n 234 | curl_mfprintf(stderr, \"0x\");\n 235 | while(len-- > 0)\n 236 | curl_mfprintf(stderr, \"%02.2x\", (unsigned int)*p++);\n 237 | }","lines":10,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vauth/ntlm.c#L228-L237"}]},
{"id":"OTHER_10","type":"other","type_label":"Other","severity":"negligible","difficulty":"easy","description":"httpsrr_decode_alpn() stores up to MAX_HTTPSRR_ALPNS (4) ALPN ids into a fixed array of exactly MAX_HTTPSRR_ALPNS bytes. When 4 distinct known ALPN ids are decoded, idnum reaches 4 and the code correctly avoids writing the ALPN_none terminator (`if(idnum < MAX_HTTPSRR_ALPNS)`), but the resulting array then has no terminator. Current consumers iterate with CURL_ARRAYSIZE() so there is no OOB read today, but other code that scans for an ALPN_none sentinel (matching the function's documented 'terminate the list' contract) would read past the array. The full-array case silently violates the terminator invariant.","fix":"Either size the array MAX_HTTPSRR_ALPNS+1 to always allow a terminator, or document/assert that callers must use the array bound rather than relying on an ALPN_none sentinel.","locations":[{"ref":"lib/vdns/httpsrr.c:56-68","code":" 56 | if(idnum == MAX_HTTPSRR_ALPNS)\n 57 | break;\n 58 | if(idnum && memchr(alpns, id, idnum))\n 59 | /* this ALPN id is already stored */\n 60 | ;\n 61 | else\n 62 | alpns[idnum++] = (unsigned char)id;\n 63 | }\n 64 | cp += tlen;\n 65 | len -= tlen;\n 66 | }\n 67 | if(idnum < MAX_HTTPSRR_ALPNS)\n 68 | alpns[idnum] = ALPN_none; /* terminate the list */","lines":13,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vdns/httpsrr.c#L56-L68"}]},
{"id":"OTHER_11","type":"other","type_label":"Other","severity":"negligible","difficulty":"medium","description":"curl_version_info() mutates the file-static `version_info` struct and the\nfile-static `feature_names[]` array on every call without any synchronization.\nTwo threads calling curl_version_info() concurrently (it is part of the public\nAPI and may be invoked at any time) race on these globals, and a reader holding\na previously returned pointer can observe a transiently inconsistent\nfeatures/feature_names state. The header comment for curl_version() explicitly\ndocuments a careful multi-thread-safe pattern, but curl_version_info() has no\nsuch guarantee and writes shared state unconditionally each call.","fix":"Document that curl_version_info() is not thread-safe with respect to concurrent\ncalls, or compute the features/feature_names only once (guarded\ninitialization) so repeated calls do not rewrite shared globals.","locations":[{"ref":"lib/version.c:579-681","code":" 579 | curl_version_info_data *curl_version_info(CURLversion stamp)\n 580 | {\n 581 | size_t n;\n 582 | const struct feat *p;\n 583 | int features = 0;\n 584 | \n 585 | #ifdef USE_SSH\n 586 | static char ssh_buf[80]; /* 'ssh_buffer' clashes with libssh/libssh.h */\n 587 | #endif\n 588 | #ifdef USE_SSL\n 589 | #ifdef CURL_WITH_MULTI_SSL\n 590 | static char ssl_buffer[200];\n 591 | #else\n 592 | static char ssl_buffer[80];\n 593 | #endif\n 594 | #endif\n 595 | #ifdef HAVE_BROTLI\n 596 | static char brotli_buffer[80];\n 597 | #endif\n 598 | #ifdef HAVE_ZSTD\n 599 | static char zstd_buffer[80];\n 600 | #endif\n 601 | \n 602 | (void)stamp;\n 603 | \n 604 | #ifdef USE_SSL\n 605 | Curl_ssl_version(ssl_buffer, sizeof(ssl_buffer));\n 606 | version_info.ssl_version = ssl_buffer;\n 607 | #endif\n 608 | \n 609 | #ifdef HAVE_LIBZ\n 610 | version_info.libz_version = zlibVersion();\n 611 | /* libz left NULL if non-existing */\n 612 | #endif\n 613 | #ifdef USE_ARES\n 614 | {\n 615 | int aresnum;\n 616 | version_info.ares = ares_version(&aresnum);\n 617 | version_info.ares_num = aresnum;\n 618 | }\n 619 | #endif\n 620 | #ifdef USE_LIBIDN2\n 621 | /* This returns a version string if we use the given version or later,\n 622 | otherwise it returns NULL */\n 623 | version_info.libidn = idn2_check_version(IDN2_VERSION);\n 624 | #endif\n 625 | \n 626 | #ifdef USE_SSH\n 627 | Curl_ssh_version(ssh_buf, sizeof(ssh_buf));\n 628 | version_info.libssh_version = ssh_buf;\n 629 | #endif\n 630 | \n 631 | #ifdef HAVE_BROTLI\n 632 | version_info.brotli_ver_num = BrotliDecoderVersion();\n 633 | brotli_version(brotli_buffer, sizeof(brotli_buffer));\n 634 | version_info.brotli_version = brotli_buffer;\n 635 | #endif\n 636 | \n 637 | #ifdef HAVE_ZSTD\n 638 | version_info.zstd_ver_num = ZSTD_versionNumber();\n 639 | zstd_version(zstd_buffer, sizeof(zstd_buffer));\n 640 | version_info.zstd_version = zstd_buffer;\n 641 | #endif\n 642 | \n 643 | #ifdef USE_NGHTTP2\n 644 | {\n 645 | nghttp2_info *h2 = nghttp2_version(0);\n 646 | version_info.nghttp2_ver_num = (unsigned int)h2->version_num;\n 647 | version_info.nghttp2_version = h2->version_str;\n 648 | }\n 649 | #endif\n 650 | \n 651 | #if !defined(CURL_DISABLE_HTTP) && defined(USE_HTTP3)\n 652 | {\n 653 | static char quicbuffer[80];\n 654 | Curl_quic_ver(quicbuffer, sizeof(quicbuffer));\n 655 | version_info.quic_version = quicbuffer;\n 656 | }\n 657 | #endif\n 658 | \n 659 | #ifdef USE_GSASL\n 660 | {\n 661 | version_info.gsasl_version = gsasl_check_version(NULL);\n 662 | }\n 663 | #endif\n 664 | \n 665 | /* Get available features, build bitmask and names array. */\n 666 | n = 0;\n 667 | for(p = features_table; p->name; p++)\n 668 | if(!p->present || p->present(&version_info)) {\n 669 | features |= p->bitmask;\n 670 | feature_names[n++] = p->name;\n 671 | }\n 672 | \n 673 | #ifdef DEBUGBUILD\n 674 | features |= CURL_VERSION_CURLDEBUG; /* for compatibility */\n 675 | #endif\n 676 | \n 677 | feature_names[n] = NULL; /* Terminate array. */\n 678 | version_info.features = features;\n 679 | \n 680 | return &version_info;\n 681 | }","lines":103,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/version.c#L579-L681"}]},
{"id":"OTHER_12","type":"other","type_label":"Other","severity":"negligible","difficulty":"very_easy","description":"Curl_vquic_tls_before_recv() OpenSSL branch sets `ctx->ossl.x509_store_setup = TRUE` after a successful Curl_ssl_setup_x509_store(), but the WolfSSL branch does not set `ctx->wssl.x509_store_setup` after Curl_wssl_setup_x509_store(). This is not a bug because Curl_wssl_setup_x509_store() sets the flag internally, but the inconsistency between the two branches is a maintenance hazard: a future refactor of the wolfSSL helper that drops the internal flag-set would silently re-run the store setup on every recv.","fix":"Either set ctx->wssl.x509_store_setup = TRUE explicitly in the WolfSSL branch (mirroring OpenSSL) or add a comment that the flag is set inside Curl_wssl_setup_x509_store().","locations":[{"ref":"lib/vquic/vquic-tls.c:126-144","code":" 126 | #ifdef USE_OPENSSL\n 127 | if(!ctx->ossl.x509_store_setup) {\n 128 | CURLcode result = Curl_ssl_setup_x509_store(cf, data, &ctx->ossl);\n 129 | if(result)\n 130 | return result;\n 131 | ctx->ossl.x509_store_setup = TRUE;\n 132 | }\n 133 | #elif defined(USE_WOLFSSL)\n 134 | if(!ctx->wssl.x509_store_setup) {\n 135 | CURLcode result = Curl_wssl_setup_x509_store(cf, data, &ctx->wssl);\n 136 | if(result)\n 137 | return result;\n 138 | }\n 139 | #elif defined(USE_GNUTLS)\n 140 | if(!ctx->gtls.shared_creds->trust_setup) {\n 141 | CURLcode result = Curl_gtls_client_trust_setup(cf, data, &ctx->gtls);\n 142 | if(result)\n 143 | return result;\n 144 | }","lines":19,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vquic/vquic-tls.c#L126-L144"}]},
{"id":"OTHER_13","type":"other","type_label":"Other","severity":"negligible","difficulty":"very_easy","description":"Curl_cipher_suite_walk_str() uses the expressions `*str[0]` and `*end[0]`. Due to C operator precedence these parse as `*(str[0])` == `**str` and `*(end[0])` == `**end`, which is the intended 'first character of the current string' but is needlessly obfuscated and easy to misread as indexing. It is correct, but the idiom invites future bugs.","fix":"Use the clearer `(*str)[0]` / `(*end)[0]` forms to make the double-dereference intent explicit.","locations":[{"ref":"lib/vtls/cipher_suite.c:663-674","code":" 663 | uint16_t Curl_cipher_suite_walk_str(const char **str, const char **end)\n 664 | {\n 665 | /* move string pointer to first non-separator or end of string */\n 666 | for(; cs_is_separator(*str[0]); (*str)++)\n 667 | ;\n 668 | \n 669 | /* move end pointer to next separator or end of string */\n 670 | for(*end = *str; *end[0] != '\\0' && !cs_is_separator(*end[0]); (*end)++)\n 671 | ;\n 672 | \n 673 | return Curl_cipher_suite_lookup_id(*str, *end - *str);\n 674 | }","lines":12,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vtls/cipher_suite.c#L663-L674"}]},
{"id":"OTHER_14","type":"other","type_label":"Other","severity":"negligible","difficulty":"very_easy","description":"GTime2str() format string emits a fixed `%.4s-%.2s-%.2s %.2s:%.2s` reading bytes beg..beg+11. The switch on `fracp - beg - 12` guarantees at least 12 leading digits exist, so the reads are in-bounds; however the code relies entirely on that implicit invariant with no explicit comment near the printf, making it easy to break in future edits. Documenting the minimum-12-byte precondition would harden the parser. (No defect today.)","fix":"Add a short comment/assert before the curlx_dyn_addf() noting that the preceding switch guarantees fracp-beg >= 12 and therefore beg[0..11] are valid.","locations":[{"ref":"lib/vtls/x509asn1.c:515-574","code":" 515 | /* Get seconds digits. */\n 516 | sec1 = '0';\n 517 | switch(fracp - beg - 12) {\n 518 | case 0:\n 519 | sec2 = '0';\n 520 | break;\n 521 | case 2:\n 522 | sec1 = fracp[-2];\n 523 | FALLTHROUGH();\n 524 | case 1:\n 525 | sec2 = fracp[-1];\n 526 | break;\n 527 | default:\n 528 | return CURLE_BAD_FUNCTION_ARGUMENT;\n 529 | }\n 530 | \n 531 | /* timezone follows optional fractional seconds. */\n 532 | tzp = fracp;\n 533 | fracl = 0; /* no fractional seconds detected so far */\n 534 | if(fracp < end && (*fracp == '.' || *fracp == ',')) {\n 535 | /* Have fractional seconds, e.g. \"[.,]\\d+\". How many? */\n 536 | fracp++; /* should be a digit char or BAD ARGUMENT */\n 537 | tzp = fracp;\n 538 | while(tzp < end && ISDIGIT(*tzp))\n 539 | tzp++;\n 540 | if(tzp == fracp) /* never looped, no digit after [.,] */\n 541 | return CURLE_BAD_FUNCTION_ARGUMENT;\n 542 | fracl = tzp - fracp; /* number of fractional sec digits */\n 543 | DEBUGASSERT(fracl > 0);\n 544 | /* Strip trailing zeroes in fractional seconds.\n 545 | * May reduce fracl to 0 if only '0's are present. */\n 546 | while(fracl && fracp[fracl - 1] == '0')\n 547 | fracl--;\n 548 | }\n 549 | \n 550 | /* Process timezone. */\n 551 | if(tzp >= end) {\n 552 | tzp = \"\";\n 553 | tzl = 0;\n 554 | }\n 555 | else if(*tzp == 'Z') {\n 556 | sep = \" \";\n 557 | tzp = \"GMT\";\n 558 | tzl = 3;\n 559 | }\n 560 | else if((*tzp == '+') || (*tzp == '-')) {\n 561 | sep = \" UTC\";\n 562 | tzl = end - tzp;\n 563 | }\n 564 | else {\n 565 | sep = \" \";\n 566 | tzl = end - tzp;\n 567 | }\n 568 | \n 569 | return curlx_dyn_addf(store,\n 570 | \"%.4s-%.2s-%.2s %.2s:%.2s:%c%c%s%.*s%s%.*s\",\n 571 | beg, beg + 4, beg + 6,\n 572 | beg + 8, beg + 10, sec1, sec2,\n 573 | fracl ? \".\" : \"\", (int)fracl, fracp,\n 574 | sep, (int)tzl, tzp);","lines":60,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/lib/vtls/x509asn1.c#L515-L574"}]},
{"id":"OTHER_15","type":"other","type_label":"Other","severity":"negligible","difficulty":"easy","description":"data_urlencode() casts the file/string size to int when calling curl_easy_escape(enc = curl_easy_escape(NULL, postdata, (int)size)). `size` is a size_t that can be up to MAX_FILE2MEMORY; for inputs larger than INT_MAX this truncates to a (possibly negative) int. file2memory caps the size to fit in an int per its own comment, so this is currently bounded, but the cast is an implicit dependency on that invariant and would silently corrupt the encode length if the cap were ever raised.","fix":"Validate that size <= INT_MAX before the cast (returning PARAM_NUMBER_TOO_LARGE otherwise), or use a length-aware escape API.","locations":[{"ref":"src/tool_getparam.c:717","code":" 717 | char *enc = curl_easy_escape(NULL, postdata, (int)size);","lines":1,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/src/tool_getparam.c#L717"}]},
{"id":"OTHER_16","type":"other","type_label":"Other","severity":"negligible","difficulty":"medium","description":"checkpasswd() uses a fixed 2048-byte stack buffer `passwd` for the prompted password (line 567) and a 256-byte `prompt`. getpass_r() truncates to the buffer size, so passwords longer than 2047 bytes are silently truncated even though the surrounding code uses a MAX_USERPWDLENGTH (100KB) dynbuf, implying much larger inputs are anticipated. This is a silent-truncation usability issue rather than a memory-safety bug.","fix":"Document the password length limit, or read into a larger/dynamic buffer if very long interactive passwords need to be supported.","locations":[{"ref":"src/tool_paramhlp.c:567-595","code":" 567 | char passwd[2048] = \"\";\n 568 | char prompt[256];\n 569 | struct dynbuf dyn;\n 570 | \n 571 | curlx_dyn_init(&dyn, MAX_USERPWDLENGTH);\n 572 | if(osep)\n 573 | *osep = '\\0';\n 574 | \n 575 | /* build a nice-looking prompt */\n 576 | if(!i && last)\n 577 | curl_msnprintf(prompt, sizeof(prompt),\n 578 | \"Enter %s password for user '%s':\",\n 579 | kind, *userpwd);\n 580 | else\n 581 | curl_msnprintf(prompt, sizeof(prompt),\n 582 | \"Enter %s password for user '%s' on URL #%zu:\",\n 583 | kind, *userpwd, i + 1);\n 584 | \n 585 | /* get password */\n 586 | getpass_r(prompt, passwd, sizeof(passwd));\n 587 | if(osep)\n 588 | *osep = ';';\n 589 | \n 590 | if(curlx_dyn_addf(&dyn, \"%s:%s\", *userpwd, passwd))\n 591 | return CURLE_OUT_OF_MEMORY;\n 592 | \n 593 | /* return the new string */\n 594 | curlx_free(*userpwd);\n 595 | *userpwd = curlx_dyn_ptr(&dyn);","lines":29,"link":"https://github.com/curl/curl/blob/323c184b6bdf5b1988a10c0a61560666e1588149/src/tool_paramhlp.c#L567-L595"}]}
]</script>
<script type="application/json" id="PI">{"name":"curl","git_url":"https://github.com/curl/curl.git","repo_url":"https://github.com/curl/curl","commit":"323c184b6bdf5b1988a10c0a61560666e1588149","commit_short":"323c184b6b","commit_url":"https://github.com/curl/curl/commit/323c184b6bdf5b1988a10c0a61560666e1588149","branch":"master","generated":"2026-09-09","description":"A command line tool and library for transferring data with URL syntax, supporting DICT, FILE, FTP, FTPS, GOPHER, GOPHERS, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, MQTT, MQTTS, POP3, POP3S, RTSP, SCP, SFTP, SMB, SMBS, SMTP, SMTPS, TELNET, TFTP, WS and WSS. libcurl offers a myriad of powerful features"}</script>
<div class="app">
<div class="top">
<div>
<h1>Code Audit Report — curl</h1>
<div class="meta">Generated 2026-09-09 — <span id="stats"></span></div>
</div>
<div class="top-right">
<div class="search-wrap">
<input type="search" id="search" placeholder="Search findings…" autocomplete="off">
<button id="search-scope" class="action-btn" type="button" title="Toggle: search everywhere vs. only in file/folder paths">📁 Paths only</button>
</div>
<label class="group-label">Group by
<select id="groupby">
<option value="type">Type</option>
<option value="severity">Severity</option>
<option value="file">File</option>
<option value="none">None (flat)</option>
</select>
</label>
</div>
</div>
<div class="project-info"><div class="pi-name">curl</div><div class="pi-desc">A command line tool and library for transferring data with URL syntax, supporting DICT, FILE, FTP, FTPS, GOPHER, GOPHERS, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, MQTT, MQTTS, POP3, POP3S, RTSP, SCP, SFTP, SMB, SMBS, SMTP, SMTPS, TELNET, TFTP, WS and WSS. libcurl offers a myriad of powerful features</div><div class="pi-row"><a class="pi-repo-btn" href="https://github.com/curl/curl" target="_blank" rel="noopener">🔗 https://github.com/curl/curl</a><span><span class="pi-k">branch</span> <code>master</code></span><span><span class="pi-k">commit</span> <a href="https://github.com/curl/curl/commit/323c184b6bdf5b1988a10c0a61560666e1588149" target="_blank" rel="noopener"><code>323c184b6b</code></a></span></div></div>
<div class="filterbar" id="filterbar"></div>
<div id="progress-wrap">
<div id="progress-bar"><div id="progress-fill"></div></div>
<span id="progress-text"></span>
<button class="action-btn" data-action="collapse-all">⊟ Collapse all</button>
<button class="action-btn" data-action="expand-all">⊞ Expand all</button>
<button class="action-btn" data-action="download-md">⬇ MD</button>
<button class="action-btn" data-action="download-md-short">⬇ MD Short</button>
<button class="action-btn" data-action="download-toml">⬇ TOML</button>
</div>
<div id="hidden-bar"></div>
<div id="content"></div>
</div>
<script>
(function(){
'use strict';
// Data
const FINDINGS = JSON.parse(document.getElementById('D').textContent);
const PROJECT_INFO = JSON.parse(document.getElementById('PI').textContent);
const SEVS = ['critical','high','medium','low','negligible'];
const STATUSES = ['open','fixed','wont-fix','false-positive'];
const ALL_TYPES = [...new Set(FINDINGS.map(f => f.type))].sort();
// Build lookup map for O(1) access
const FINDING_BY_ID = new Map(FINDINGS.map(f => [f.id, f]));
// Only severities actually present in the report — the filter bar hides the rest.
const PRESENT_SEVS = SEVS.filter(s => FINDINGS.some(f => (f.severity || 'medium') === s));
// Whether any finding is flagged easy — the "Quick wins" filter row is hidden otherwise.
const DIFFICULTY_ORDER = ['very_easy','easy','medium','hard'];
const DIFFICULTY_LBL = {very_easy:'⚡ Very Easy', easy:'Easy', medium:'Medium', hard:'Hard'};
const DIFFICULTY_CLS = {very_easy:'diff-very-easy', easy:'diff-easy', medium:'diff-medium', hard:'diff-hard'};
const PRESENT_DIFFS = DIFFICULTY_ORDER.filter(d => FINDINGS.some(f => f.difficulty === d));
const HAS_DIFFICULTY = PRESENT_DIFFS.length > 0;
const STORE_KEY = 'audit-status:' + location.href;
const STATUS_CLS = {open:'st-open', fixed:'st-fixed', 'wont-fix':'st-wont', 'false-positive':'st-fp'};
const STATUS_LBL = {open:'● open', fixed:'✓ fixed', 'wont-fix':"— won't fix", 'false-positive':'✗ false pos.'};
const STATUS_NEXT = {open:'fixed', fixed:'wont-fix', 'wont-fix':'false-positive', 'false-positive':'open'};
// State
const S = {
search: '', searchScope: 'all', groupBy: 'type',
hiddenSev: new Set(), hiddenType: new Set(),
hiddenStatus: new Set(), hiddenDiff: new Set(), hiddenGroup: new Set(),
collapsed: new Set(),
};
const statuses = {};
// Persistence
function loadStatuses() {
try { Object.assign(statuses, JSON.parse(localStorage.getItem(STORE_KEY) || '{}')); } catch {}
}
function saveStatuses() {
try { localStorage.setItem(STORE_KEY, JSON.stringify(statuses)); } catch {}
}
function statusOf(id) { return statuses[id] || 'open'; }
// URL hash — debounced so rapid filter changes don't thrash history
let _hashTimer = 0;
function syncHash() {
clearTimeout(_hashTimer);
_hashTimer = setTimeout(() => {
const p = new URLSearchParams();
if (S.groupBy !== 'type') p.set('g', S.groupBy);
if (S.hiddenSev.size) p.set('hs', [...S.hiddenSev].join(','));
if (S.hiddenType.size) p.set('ht', [...S.hiddenType].join(','));
if (S.hiddenStatus.size) p.set('hst', [...S.hiddenStatus].join(','));
if (S.hiddenDiff.size) p.set('hd', [...S.hiddenDiff].join(','));
if (S.search) p.set('q', S.search);
if (S.searchScope !== 'all') p.set('qs', S.searchScope);
const str = p.toString();
history.replaceState(null, '', str ? '#' + str : location.pathname + location.search);
}, 300);
}
function loadHash() {
const p = new URLSearchParams(location.hash.slice(1));
if (p.has('g')) { S.groupBy = p.get('g'); document.getElementById('groupby').value = S.groupBy; }
if (p.has('q')) { S.search = p.get('q'); document.getElementById('search').value = S.search; }
if (p.has('qs')) { S.searchScope = p.get('qs'); }
document.getElementById('search-scope').classList.toggle('active', S.searchScope === 'locations');
if (p.has('hs')) p.get('hs') .split(',').filter(Boolean).forEach(v => S.hiddenSev.add(v));
if (p.has('ht')) p.get('ht') .split(',').filter(Boolean).forEach(v => S.hiddenType.add(v));
if (p.has('hst')) p.get('hst').split(',').filter(Boolean).forEach(v => S.hiddenStatus.add(v));
if (p.has('hd')) p.get('hd') .split(',').filter(Boolean).forEach(v => S.hiddenDiff.add(v));
}
// Helpers
function esc(s) {
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
}
function highlight(text, q) {
if (!q) return esc(text);
const re = new RegExp('(' + q.replace(/[.*+?^${}()|[\]\\]/g,'\\$&') + ')', 'gi');
return String(text).split(re).map((p, i) => i % 2 ? '<mark>' + esc(p) + '</mark>' : esc(p)).join('');
}
// Render text with ```fenced``` code blocks and `inline` backticks,
// also paragraph-splitting on blank lines. Output is HTML-safe.
function renderMd(text, q) {
const src = String(text == null ? '' : text);
const FENCE = /```[ \t]*([A-Za-z0-9_+-]*)[ \t]*\r?\n([\s\S]*?)```/g;
const parts = [];
let last = 0, m;
while ((m = FENCE.exec(src)) !== null) {
parts.push({kind: 'text', value: src.slice(last, m.index)});
parts.push({kind: 'code', value: m[2].replace(/\r?\n$/, '')});
last = m.index + m[0].length;
}
parts.push({kind: 'text', value: src.slice(last)});
function renderInline(s) {
const re = /`([^`\n]+)`/g;
let out = '', i = 0, mm;
while ((mm = re.exec(s)) !== null) {
out += highlight(s.slice(i, mm.index), q);
out += '<code class="md-inline">' + highlight(mm[1], q) + '</code>';
i = mm.index + mm[0].length;
}
out += highlight(s.slice(i), q);
return out;
}
function renderTextChunk(s) {
const paragraphs = s.split(/\r?\n[ \t]*\r?\n/);
const out = [];
for (const p of paragraphs) {
const trimmed = p.replace(/^\s+|\s+$/g, '');
if (!trimmed) continue;
out.push('<p>' + renderInline(trimmed).replace(/\r?\n/g, '<br>') + '</p>');
}
return out.join('');
}
let html = '';
for (const p of parts) {
if (p.kind === 'text') html += renderTextChunk(p.value);
else html += '<pre class="md-code">' + highlight(p.value, q) + '</pre>';
}
return html;
}
function sevCls(s) { return SEVS.includes(s) ? 'sev-'+s : 'sev-negligible'; }
function sevLbl(s) { return s ? s[0].toUpperCase()+s.slice(1) : 'Medium'; }
function typeLabel(t) { return t.replace(/_/g,' ').replace(/\b\w/g, c => c.toUpperCase()); }
// First sentence of text (up to and including the first period).
function firstSentence(text) {
const m = String(text).match(/\.(\s|$)/);
return m ? text.slice(0, text.indexOf(m[0]) + 1) : text;
}
// Render a "**Label:** value" line. GitHub/CommonMark only recognizes a
// ``` fence when it starts at the beginning of a line, so if the value
// itself opens with a fence, put it on its own line instead of right
// after the inline label - otherwise the fence marker gets glued onto
// "**Label:** ```" and never renders as a code block.
function mdField(label, value) {
value = value || '';
return /^```/.test(value.trim())
? '**' + label + ':**\n\n' + value
: '**' + label + ':** ' + value;
}
// Render the "**Locations:**" block. When a location has a commit-pinned
// permalink (GitHub/GitLab), just paste the bare URL on its own line -
// GitHub/GitLab auto-expand a line-range blob link into an inline code
// preview, so re-pasting the snippet as a fenced block would duplicate it.
// Forges without a detected permalink shape (or missing commit/repo info)
// fall back to the fenced ref+snippet so the location is still readable.
function mdLocations(locations) {
if (!locations.length) return '';
let out = '**Locations:**\n\n';
for (const l of locations) {
if (l.link) out += l.link + '\n\n';
else if (l.code != null) out += '```\n' + l.ref + '\n' + l.code + '\n```\n\n';
else out += '- `' + l.ref + '` _(file not found)_\n\n';
}
return out;
}
// Build full MD report for in-browser download.
function buildMdFull() {
const SORD = ['critical','high','medium','low','negligible'];
const today = new Date().toISOString().slice(0, 10);
let out = '# Code Audit Report\n\n_Generated ' + today + '_\n\n';
const typeMap = new Map();
for (const f of FINDINGS) {
if (!typeMap.has(f.type)) typeMap.set(f.type, []);
typeMap.get(f.type).push(f);
}
const sortedTypes = [...typeMap.keys()].sort();
out += '## Summary\n\n| Type | Severity | Count |\n|------|----------|-------|\n';
for (const t of sortedTypes) {
const items = typeMap.get(t);
const topSev = items.map(f => f.severity || 'medium').reduce((best, s) => {
const bi = SORD.indexOf(best), si = SORD.indexOf(s);
return (si >= 0 && (bi < 0 || si < bi)) ? s : best;
}, 'negligible');
out += '| ' + typeLabel(t) + ' | ' + topSev + ' | ' + items.length + ' |\n';
}
for (const t of sortedTypes) {
out += '\n## ' + typeLabel(t) + '\n\n';
for (const f of typeMap.get(t)) {
const sevStr = f.severity ? ' `' + f.severity.toUpperCase() + '`' : '';
out += '### ' + f.id + sevStr + '\n\n';
out += mdField('Description', f.description) + '\n\n';
out += mdLocations(f.locations);
out += mdField('Fix', f.fix) + '\n\n---\n\n';
}
}
return out;
}
// Build MD short checklist for in-browser download.
function buildMdShort() {
const lines = [];
const ids = [];
const sorted = [...FINDINGS].sort((a, b) => a.type.localeCompare(b.type));
for (const f of sorted) {
ids.push(f.id);
const desc = firstSentence(f.description || '');
const locs = f.locations.map(l => l.ref);
const locStr = locs.length ? ' - [' + locs.join(', ') + ']' : '';
const sevStr = f.severity ? ' [' + f.severity.toUpperCase() + ']' : '';
lines.push('- [ ] - ' + f.id + sevStr + ' - ' + desc + locStr);
}
lines.push('', '## IDs only', '');
ids.forEach(id => lines.push('- [ ] - ' + id));
return lines.join('\n') + '\n';
}
function downloadText(text, filename, mime) {
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([text], {type: mime || 'text/plain'}));
a.download = filename;
a.click();
setTimeout(() => URL.revokeObjectURL(a.href), 1000);
}
function tomlStr(s) {
return '"' + String(s == null ? '' : s)
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t') + '"';
}
// Build a [project] header block from embedded PROJECT_INFO, so the exported
// findings.toml is self-contained (doesn't depend on a sibling project-info.toml
// that a second, separately-triggered download could silently fail to save).
// '' when there is no project info at all.
function buildProjectToml() {
const ORDER = ['name','description','git_url','repo_url','commit','commit_short','commit_url','branch','generated'];
const pi = PROJECT_INFO || {};
if (!Object.keys(pi).length) return '';
const lines = ['[project]'];
for (const k of ORDER) {
if (k in pi) lines.push(k + ' = ' + tomlStr(pi[k]));
}
return lines.join('\n') + '\n\n';
}
// Build TOML matching the original findings.toml format (re-parseable by toml_to_report.py).
function buildFindingsToml() {
function pad(key) { return (key + ' ').slice(0, 12); }
const chunks = [];
for (const f of FINDINGS) {
const lns = ['[[finding]]'];
lns.push(pad('type') + '= ' + tomlStr(f.type));
lns.push(pad('severity') + '= ' + tomlStr(f.severity || 'medium'));
if (f.difficulty) lns.push(pad('difficulty') + '= ' + tomlStr(f.difficulty));
lns.push(pad('description') + '= ' + tomlStr(f.description || ''));
const refs = f.locations.map(l => tomlStr(l.ref));
lns.push(pad('locations') + '= [' + refs.join(', ') + ']');
lns.push(pad('fix') + '= ' + tomlStr(f.fix || ''));
chunks.push(lns.join('\n'));
}
return buildProjectToml() + chunks.join('\n\n') + '\n';
}
// Build the GitHub-ready Markdown for one finding (mirrors findings.md).
function findingToMd(f) {
let out = '### ' + f.id + (f.severity ? ' `' + f.severity.toUpperCase() + '`' : '') + '\n\n';
out += mdField('Description', f.description) + '\n\n';
out += mdLocations(f.locations);
out += mdField('Fix', f.fix) + '\n';
return out;
}
function flashBtn(btn, msg) {
btn.textContent = msg;
setTimeout(() => { btn.textContent = '⧉ Copy'; }, 1200);
}
function copyText(text, btn) {
const ok = () => flashBtn(btn, '✓ Copied');
const no = () => flashBtn(btn, '✗ Failed');
const fallback = () => {
try {
const ta = document.createElement('textarea');
ta.value = text; ta.style.position = 'fixed'; ta.style.top = '-9999px';
document.body.appendChild(ta); ta.select();
const r = document.execCommand('copy');
document.body.removeChild(ta);
r ? ok() : no();
} catch { no(); }
};
if (navigator.clipboard && navigator.clipboard.writeText)
navigator.clipboard.writeText(text).then(ok, fallback);
else fallback();
}
// Filter logic
function getKey(f) {
if (S.groupBy === 'severity') return f.severity || 'medium';
if (S.groupBy === 'file') return f.locations.length ? f.locations[0].ref.replace(/:.*/,'') : '(no location)';
if (S.groupBy === 'none') return '';
return f.type_label;
}
// Pre-compute search haystacks per finding (cleared on full re-render).
// Two variants: everything (default), and file/folder paths only - for when a
// search term (e.g. a directory name like 'dlls/abc') also happens to appear
// inside description/fix prose and you only want path matches.
const _hay = new Map();
const _hayLoc = new Map();
function hayOf(f) {
if (!_hay.has(f.id)) {
_hay.set(f.id, [f.id, f.type_label, f.severity, f.description, f.fix,
f.difficulty ? (DIFFICULTY_LBL[f.difficulty] || f.difficulty) : '',
...f.locations.map(l => l.ref)].join(' ').toLowerCase());
}
return _hay.get(f.id);
}
function hayLocOf(f) {
if (!_hayLoc.has(f.id)) {
_hayLoc.set(f.id, f.locations.map(l => l.ref).join(' ').toLowerCase());
}
return _hayLoc.get(f.id);
}
function matches(f) {
if (S.hiddenDiff.size > 0 && f.difficulty && S.hiddenDiff.has(f.difficulty)) return false;
if (S.hiddenSev.has(f.severity || 'medium')) return false;
if (S.hiddenType.has(f.type)) return false;
const st = statusOf(f.id);
if (S.hiddenStatus.has(st)) return false;
if (S.search) {
const q = S.search.toLowerCase();
if (S.searchScope === 'locations') {
if (!hayLocOf(f).includes(q)) return false;
} else if (!hayOf(f).includes(q) && !st.includes(q)) {
return false;
}
}
return true;
}
function sortKeys(keys) {
if (S.groupBy === 'severity') {
return [...keys].sort((a, b) => {
const ai = SEVS.indexOf(a), bi = SEVS.indexOf(b);
return (ai < 0 ? 99 : ai) - (bi < 0 ? 99 : bi);
});
}
return [...keys].sort();
}
// Build body HTML for an expanded card (called lazily on first expand).
function renderCardBody(f, q) {
let locs = '';
for (const l of f.locations) {
const lineInfo = l.lines != null
? ' <span class="loc-lines">(' + l.lines + ' line' + (l.lines === 1 ? '' : 's') + ')</span>'
: '';
const refHtml = l.link
? '<a class="loc-link" href="' + esc(l.link) + '" target="_blank" rel="noopener" title="Open at this commit">' + highlight(l.ref, q) + ' ↗</a>'
: highlight(l.ref, q);
locs += '<div class="loc-block"><div class="loc-ref">' + refHtml + lineInfo + '</div>';
locs += l.code != null ? '<pre>' + esc(l.code) + '</pre>' : '<div class="not-found">(file not found)</div>';
locs += '</div>';
}
return (
'<div class="field-lbl">Description</div>' +
'<div class="desc">' + renderMd(f.description, q) + '</div>' +
(locs ? '<div class="field-lbl">Locations</div>' + locs : '') +
'<div class="field-lbl">Fix</div>' +
'<div class="fix-txt">' + renderMd(f.fix, q) + '</div>'
);
}
// Render card shell only — header + empty or populated body.
// When collapsed the body is empty (display:none) — avoids renderMd for 1800+ cards.
function renderCard(f, q) {
const sc = sevCls(f.severity || 'medium');
const sl = sevLbl(f.severity || 'medium');
const st = statusOf(f.id);
const sc2 = STATUS_CLS[st] || 'st-open';
const sl2 = STATUS_LBL[st] || '● open';
const isCollapsed = S.collapsed.has(f.id);
const isResolved = st !== 'open';
const locSummary = f.locations.length
? f.locations[0].ref + (f.locations.length > 1 ? ' \u2026' : '')
: '';
const bodyInner = isCollapsed ? '' : renderCardBody(f, q);
return (
'<div class="card' + (isResolved ? ' resolved' : '') + '" data-card-id="' + esc(f.id) + '">' +
'<div class="card-header" data-action="toggle-card" data-id="' + esc(f.id) + '">' +
'<span class="card-arrow">' + (isCollapsed ? '\u25b6' : '\u25bc') + '</span>' +
'<span class="card-id">' + esc(f.id) + '</span>' +
'<span class="badge ' + sc + '" style="cursor:default">' + esc(sl) + '</span>' +
'<span class="badge type-badge" style="cursor:default">' + esc(f.type_label) + '</span>' +
(f.difficulty ? '<span class="badge ' + (DIFFICULTY_CLS[f.difficulty]||'') + '" style="cursor:default" title="Difficulty: ' + esc(DIFFICULTY_LBL[f.difficulty]||f.difficulty) + '">' + esc(DIFFICULTY_LBL[f.difficulty]||f.difficulty) + '</span>' : '') +
(locSummary ? '<span class="card-loc" title="' + esc(locSummary) + '">' + esc(locSummary) + '</span>' : '') +
'<button class="status-btn ' + sc2 + '" data-action="cycle-status" data-id="' + esc(f.id) + '">' + sl2 + '</button>' +
'<button class="copy-btn" data-action="copy-md" data-id="' + esc(f.id) + '" title="Copy as Markdown for GitHub">\u29c9 Copy</button>' +
'</div>' +
'<div class="card-body" data-body-id="' + esc(f.id) + '"' + (isCollapsed ? ' style="display:none"' : '') + '>' +
bodyInner +
'</div></div>'
);
}
function initFilterBar() {
const fb = document.getElementById('filterbar');
let h = '<div class="filter-row"><span class="filter-lbl">Severity:</span>';
for (const s of PRESENT_SEVS)
h += '<button class="badge ' + sevCls(s) + '" data-sev="' + s + '">' + sevLbl(s) + '</button>';
h += '<button class="filter-toggle" data-toggle-all="sev"></button></div>';
h += '<div class="filter-row"><span class="filter-lbl">Type:</span>';
for (const t of ALL_TYPES)
h += '<button class="badge type-badge" data-type="' + esc(t) + '">' + esc(typeLabel(t)) + '</button>';
h += '<button class="filter-toggle" data-toggle-all="type"></button></div>';
h += '<div class="filter-row"><span class="filter-lbl">Status:</span>';
for (const st of STATUSES)
h += '<button class="badge ' + (STATUS_CLS[st]||'st-open') + '" data-status="' + st + '">' + (STATUS_LBL[st]||st) + '</button>';
h += '<button class="filter-toggle" data-toggle-all="status"></button></div>';
if (HAS_DIFFICULTY) {
h += '<div class="filter-row"><span class="filter-lbl">Difficulty:</span>';
for (const d of PRESENT_DIFFS)
h += '<button class="badge ' + (DIFFICULTY_CLS[d]||'') + '" data-diff="' + d + '">' + esc(DIFFICULTY_LBL[d]||d) + '</button>';
h += '<button class="filter-toggle" data-toggle-all="diff"></button></div>';
}
fb.innerHTML = h;
}
function updateToggles() {
for (const b of document.querySelectorAll('[data-sev]')) b.classList.toggle('off', S.hiddenSev.has(b.dataset.sev));
for (const b of document.querySelectorAll('[data-type]')) b.classList.toggle('off', S.hiddenType.has(b.dataset.type));
for (const b of document.querySelectorAll('[data-status]')) b.classList.toggle('off', S.hiddenStatus.has(b.dataset.status));
for (const b of document.querySelectorAll('[data-diff]')) b.classList.toggle('off', S.hiddenDiff.has(b.dataset.diff));
for (const b of document.querySelectorAll('[data-toggle-all]')) {
const k = b.dataset.toggleAll;
const set = k === 'sev' ? S.hiddenSev : k === 'type' ? S.hiddenType : k === 'status' ? S.hiddenStatus : k === 'diff' ? S.hiddenDiff : null;
if (set) b.textContent = set.size === 0 ? 'Hide all' : 'Show all';
}
}
function updateProgress() {
const total = FINDINGS.length;
const done = FINDINGS.filter(f => statusOf(f.id) !== 'open').length;
const pct = total ? Math.round(done / total * 100) : 0;
document.getElementById('progress-fill').style.width = pct + '%';
document.getElementById('progress-text').textContent = done + ' / ' + total + ' resolved (' + pct + '%)';
}
// Surgical toggle: flip one card without touching the rest of the DOM.
function toggleCard(id) {
const wasCollapsed = S.collapsed.has(id);
if (wasCollapsed) S.collapsed.delete(id); else S.collapsed.add(id);
const body = document.querySelector('[data-body-id="' + id + '"]');
const arrow = document.querySelector('[data-card-id="' + id + '"] .card-arrow');
if (!body) { render(); return; }
if (wasCollapsed) {
if (!body.firstChild) {
const f = FINDING_BY_ID.get(id);
if (f) body.innerHTML = renderCardBody(f, S.search);
}
body.style.display = '';
if (arrow) arrow.textContent = '\u25bc';
} else {
body.style.display = 'none';
if (arrow) arrow.textContent = '\u25b6';
}
}
// Surgical status update: update one card's button + resolved class.
function cycleStatus(id) {
statuses[id] = STATUS_NEXT[statusOf(id)] || 'open';
saveStatuses();
const st = statusOf(id);
const btn = document.querySelector('.status-btn[data-id="' + id + '"]');
if (!btn) { render(); return; }
btn.className = 'status-btn ' + (STATUS_CLS[st] || 'st-open');
btn.textContent = STATUS_LBL[st] || '\u25cf open';
const card = document.querySelector('[data-card-id="' + id + '"]');
if (card) card.classList.toggle('resolved', st !== 'open');
updateProgress();
}
// Surgical collapse/expand for a group: toggle all bodies in the group without full re-render.
function collapseGroup(key, collapse) {
FINDINGS.forEach(f => {
if (getKey(f) !== key) return;
if (collapse) S.collapsed.add(f.id); else S.collapsed.delete(f.id);
const body = document.querySelector('[data-body-id="' + f.id + '"]');
const arrow = document.querySelector('[data-card-id="' + f.id + '"] .card-arrow');
if (!body) return;
if (collapse) {
body.style.display = 'none';
if (arrow) arrow.textContent = '\u25b6';
} else {
if (!body.firstChild) {
const ff = FINDING_BY_ID.get(f.id);
if (ff) body.innerHTML = renderCardBody(ff, S.search);
}
body.style.display = '';
if (arrow) arrow.textContent = '\u25bc';
}
});
}
// Full re-render: used for filter/group-by/search changes.
// Batched via requestAnimationFrame.
let _rafPending = false;
function scheduleRender() {
if (_rafPending) return;
_rafPending = true;
requestAnimationFrame(() => { _rafPending = false; render(); });
}
function render() {
_hay.clear();
_hayLoc.clear();
updateToggles();
updateProgress();
const q = S.search;
const visible = FINDINGS.filter(matches);
document.getElementById('stats').textContent = visible.length + ' / ' + FINDINGS.length + ' findings';
// Build groups
const map = new Map();
for (const f of visible) {
const k = getKey(f);
if (!map.has(k)) map.set(k, []);
map.get(k).push(f);
}
// Hidden-groups bar
const hbar = document.getElementById('hidden-bar');
if (S.hiddenGroup.size > 0) {
let hh = '<div class="hidden-inner"><span class="hidden-lbl">Hidden:</span>';
for (const g of [...S.hiddenGroup].sort())
hh += '<button class="show-btn" data-action="show-group" data-key="' + esc(g) + '">' + esc(g || '(all findings)') + '</button>';
hh += '<button class="show-all-btn" data-action="show-all">Show all</button></div>';
hbar.innerHTML = hh;
} else {
hbar.innerHTML = '';
}
// Content
const content = document.getElementById('content');
if (!visible.length) {
content.innerHTML = '<div class="no-results">No findings match the current filters.</div>';
syncHash();
return;
}
let hh = '';
for (const k of sortKeys([...map.keys()])) {
if (S.hiddenGroup.has(k)) continue;
const items = map.get(k);
const title = k || 'All Findings';
const done = items.filter(f => statusOf(f.id) !== 'open').length;
const allDone = done === items.length;
hh += '<div class="group-section">' +
'<div class="group-header">' +
'<span class="group-title' + (allDone ? ' all-done' : '') + '">' + esc(title) + '</span>' +
'<span class="group-count">' + done + '/' + items.length + '</span>' +
'<button class="icon-btn" data-action="collapse-group" data-key="' + esc(k) + '" title="Collapse all">\u229f</button>' +
'<button class="icon-btn" data-action="expand-group" data-key="' + esc(k) + '" title="Expand all">\u229e</button>' +
'<button class="hide-btn" data-action="hide-group" data-key="' + esc(k) + '">Hide</button>' +
'</div>';
for (const f of items) hh += renderCard(f, q);
hh += '</div>';
}
content.innerHTML = hh;
syncHash();
}
// Long-press solo: hold a filter badge 450 ms to isolate that value (hide all others in the group).
let _soloTimer = 0;
let _soloFired = false;
function _applySolo(b) {
_soloFired = true;
const sev = b.dataset.sev, typ = b.dataset.type, sta = b.dataset.status, dif = b.dataset.diff;
if (sev) { S.hiddenSev.clear(); PRESENT_SEVS.filter(v => v !== sev).forEach(v => S.hiddenSev.add(v)); }
else if (typ) { S.hiddenType.clear(); ALL_TYPES.filter(v => v !== typ).forEach(v => S.hiddenType.add(v)); }
else if (sta) { S.hiddenStatus.clear(); STATUSES.filter(v => v !== sta).forEach(v => S.hiddenStatus.add(v)); }
else if (dif) { S.hiddenDiff.clear(); PRESENT_DIFFS.filter(v => v !== dif).forEach(v => S.hiddenDiff.add(v)); }
scheduleRender();
}
const _fb = document.getElementById('filterbar');
_fb.addEventListener('pointerdown', e => {
const b = e.target.closest('[data-sev],[data-type],[data-status],[data-diff]');
if (!b) return;
_soloFired = false;
clearTimeout(_soloTimer);
_soloTimer = setTimeout(() => _applySolo(b), 450);
}, {passive: true});
_fb.addEventListener('pointerup', () => clearTimeout(_soloTimer), {passive: true});
_fb.addEventListener('pointercancel', () => clearTimeout(_soloTimer), {passive: true});
_fb.addEventListener('pointermove', e => {
if (!_soloTimer) return;
if (!e.target.closest('[data-sev],[data-type],[data-status],[data-diff]')) clearTimeout(_soloTimer);
}, {passive: true});
// Event delegation
_fb.addEventListener('click', e => {
if (_soloFired) { _soloFired = false; return; }
const b = e.target.closest('[data-sev],[data-type],[data-status],[data-toggle-all],[data-diff]');
if (!b) return;
if (b.dataset.toggleAll) {
const k = b.dataset.toggleAll;
const set = k === 'sev' ? S.hiddenSev : k === 'type' ? S.hiddenType : k === 'status' ? S.hiddenStatus : k === 'diff' ? S.hiddenDiff : null;
const full = k === 'sev' ? PRESENT_SEVS : k === 'type' ? ALL_TYPES : k === 'status' ? STATUSES : k === 'diff' ? PRESENT_DIFFS : [];
if (!set) return;
if (set.size === 0) full.forEach(v => set.add(v));
else set.clear();
}
else if (b.dataset.sev) { const s = S.hiddenSev; s.has(b.dataset.sev) ? s.delete(b.dataset.sev) : s.add(b.dataset.sev); }
else if (b.dataset.type) { const s = S.hiddenType; s.has(b.dataset.type) ? s.delete(b.dataset.type) : s.add(b.dataset.type); }
else if (b.dataset.status) { const s = S.hiddenStatus; s.has(b.dataset.status) ? s.delete(b.dataset.status) : s.add(b.dataset.status); }
else if (b.dataset.diff) { const s = S.hiddenDiff; s.has(b.dataset.diff) ? s.delete(b.dataset.diff) : s.add(b.dataset.diff); }
scheduleRender();
});
document.addEventListener('click', e => {
const b = e.target.closest('[data-action]');
if (!b) return;
const { action, key, id } = b.dataset;
if (action === 'hide-group') { S.hiddenGroup.add(key); scheduleRender(); }
else if (action === 'show-group') { S.hiddenGroup.delete(key); scheduleRender(); }
else if (action === 'show-all') { S.hiddenGroup.clear(); scheduleRender(); }
else if (action === 'collapse-all') { FINDINGS.forEach(f => S.collapsed.add(f.id)); scheduleRender(); }
else if (action === 'expand-all') { S.collapsed.clear(); scheduleRender(); }
else if (action === 'copy-md') {
e.stopPropagation();
const f = FINDING_BY_ID.get(id);
if (f) copyText(findingToMd(f), b);
}
else if (action === 'download-md') { downloadText(buildMdFull(), 'findings.md'); }
else if (action === 'download-md-short') { downloadText(buildMdShort(), 'findings-short.md'); }
else if (action === 'download-toml') {
// Single download (was two) - a second auto-triggered download is commonly
// blocked by the browser unless the user explicitly allows it, which silently
// drops project-info.toml. Embedding [project] in findings.toml itself avoids
// that failure mode entirely.
downloadText(buildFindingsToml(), 'findings.toml', 'application/toml');
}
else if (action === 'cycle-status') {
e.stopPropagation();
cycleStatus(id);
}
else if (action === 'toggle-card') {
toggleCard(id);
}
else if (action === 'collapse-group') {
collapseGroup(key, true);
}
else if (action === 'expand-group') {
collapseGroup(key, false);
}
});
// Debounced search — 180 ms after last keystroke
let _searchTimer = 0;
document.getElementById('search').addEventListener('input', e => {
S.search = e.target.value;
clearTimeout(_searchTimer);
_searchTimer = setTimeout(scheduleRender, 180);
});
document.getElementById('groupby').addEventListener('change', e => {
S.groupBy = e.target.value;
S.hiddenGroup.clear();
scheduleRender();
});
document.getElementById('search-scope').addEventListener('click', e => {
S.searchScope = S.searchScope === 'locations' ? 'all' : 'locations';
e.currentTarget.classList.toggle('active', S.searchScope === 'locations');
scheduleRender();
});
// Init
loadStatuses();
initFilterBar();
loadHash();
FINDINGS.forEach(f => S.collapsed.add(f.id));
render();
})();
</script>
</body></html>