Buffer Overflow in cURL Internal printf Function
What the report claims
The report claims a buffer overflow in curl_msprintf() due to lack of bounds checking when writing to a user-provided buffer.
Compare with curl's recorded outcome
Labeled AI slop
curl's maintainer lists this report as AI slop
Shown for comparison after analysis. The pipeline did not see this label.
ProveIt's finding
The code does what the report says, but curl's own policy may exclude it. A maintainer decides.
curl policy: API misuseSee the withdrawn model reasoning
The following reasoning failed the citation check and does not support the final finding.
- curl_msprintf is a public API function analogous to sprintf: it takes a buffer pointer but no size parameter, and the caller is responsible for providing a sufficiently large buffer. This is documented API behavior, not a flaw.
- The project policy explicitly states: 'If a reported issue only triggers by an application using the API in a way that is not documented to work or even documented to not work, it is probably not going to be considered a security problem. We only guarantee secure and proper functionality when the APIs are used as expected and documented.'
- The PoC demonstrates API misuse (passing a 16-byte buffer with a format string producing >16 bytes), not a vulnerability in curl. The attack vector 'Network' is unsubstantiated—no path from network input to curl_msprintf with attacker-controlled format string and fixed buffer is shown.
- curl likely provides a bounded variant (curl_msnprintf) for safe use, though not in the provided code excerpt.
What still needs checking
- Documentation of curl_msprintf's contract (whether it promises bounds checking or requires caller to ensure buffer size)
- Identification of any internal curl function that calls curl_msprintf with a fixed-size buffer and attacker-controlled format string
How this analysis ran 6 claims · 8 citations verified
Evidence check: the behavior is real; the project's policy section 'API misuse' may exclude it
Checked against curl master on 2025-12-12 (fe8393d7db8c). Dev evaluation split. Citation verification checks that a referenced line was in the evidence; it does not prove the model interpreted it correctly.
Inspect the withdrawn draft reply
This draft belongs to the withdrawn judgment. It may contain unsupported claims.
Thank you for the report. curl_msprintf() is a public API function designed like sprintf(): it takes a buffer pointer but no size parameter, and the caller must ensure the buffer is large enough for the formatted output. This is documented API behavior, not a vulnerability. Our policy treats API misuse as a non-security issue. The PoC demonstrates passing a too-small buffer, which is incorrect use of the API. If you have found an internal curl code path where a fixed-size buffer is passed to curl_msprintf with attacker-controlled format data, please share that specific call chain.
Inspect all claims and source code 6 claims · 4 code excerpts
This model judgment was withdrawn. The claim assessments below are preserved for inspection and may be wrong.
Report claims
curl_msprintf() writes formatted output to a user-provided buffer without performing any bounds checking
Read the reasoning
The code confirms curl_msprintf() provides no bounds checking. The storebuffer callback (lines 1195-1201) writes each output character to the user-provided buffer and increments the pointer without any size validation. curl_msprintf (lines 1203-1212) passes the buffer pointer to formatf but never supplies a buffer length.
File lib/mprintf.c, function curl_msprintf()
Read the reasoning
Deterministic checks confirm the file and function exist at the reported revision.
Providing a malicious format string to any cURL function that internally uses curl_msprintf()
Read the reasoning
The report claims any cURL function using curl_msprintf internally can be attacked via malicious format strings, but no such internal callers are identified in the provided code. The PoC calls curl_msprintf directly, which is a public API function. Whether network input reaches curl_msprintf with attacker-controlled format strings and fixed buffers is not demonstrated.
Potential arbitrary code execution
Read the reasoning
Arbitrary code execution is a theoretical consequence of buffer overflow, but exploitability depends on mitigations (stack canaries, ASLR, etc.) not in scope. The report provides no exploit demonstration.
Using a long format string that generates output larger than the target buffer
Read the reasoning
formatf processes format strings and writes output character by character via the stream callback. A format string with many literal characters or %s with a long argument will generate output exceeding any fixed buffer size.
Buffer overflow occurs - writes beyond 16-byte buffer
Read the reasoning
The PoC demonstrates the overflow: a 16-byte buffer receives more than 16 bytes of formatted output because storebuffer performs no bounds check.
Read the original report (excerpt)
Source code
| 1195 | static int storebuffer(unsigned char outc, void *f) |
| 1196 | { |
| 1197 | char **buffer = f; |
| 1198 | **buffer = (char)outc; |
| C1: storebuffer writes each character to the buffer without any bounds check | |
| C6: storebuffer writes unconditionally to buffer | |
| 1199 | (*buffer)++; |
| C1: Pointer incremented without validating remaining buffer space | |
| C6: Pointer advanced unconditionally | |
| 1200 | return 0; |
| 1201 | } |
| 1203 | int curl_msprintf(char *buffer, const char *format, ...) |
| C2: curl_msprintf function definition at line 1203 in lib/mprintf.c | |
| 1204 | { |
| 1205 | va_list ap_save; /* argument pointer */ |
| 1206 | int retcode; |
| 1207 | va_start(ap_save, format); |
| 1208 | retcode = formatf(&buffer, storebuffer, format, ap_save); |
| C1: curl_msprintf calls formatf with storebuffer callback, passing buffer pointer but no size | |
| 1209 | va_end(ap_save); |
| 1210 | *buffer = 0; /* we terminate this with a zero byte */ |
| 1211 | return retcode; |
| 1212 | } |
| 968 | static int formatf(void *userp, /* untouched by format(), just sent to the |
| 969 | stream() function in the second argument */ |
| 970 | /* function pointer called for each output character */ |
| 971 | int (*stream)(unsigned char, void *), |
| 972 | const char *format, /* %-formatted string */ |
| 973 | va_list ap_save) /* list of parameters */ |
| 974 | { |
| 975 | int done = 0; /* number of characters written */ |
| 976 | int i; |
| 977 | int ocount = 0; /* number of output segments */ |
| 978 | int icount = 0; /* number of input arguments */ |
| 979 | |
| 980 | struct outsegment output[MAX_SEGMENTS]; |
| 981 | struct va_input input[MAX_PARAMETERS]; |
| 982 | char work[BUFFSIZE + 2]; |
| 983 | |
| 984 | /* Parse the format string */ |
| 985 | if(parsefmt(format, output, input, &ocount, &icount, ap_save)) |
| 986 | return 0; |
| 987 | |
| 988 | for(i = 0; i < ocount; i++) { |
| 989 | struct outsegment *optr = &output[i]; |
| 990 | struct va_input *iptr = &input[optr->input]; |
| 991 | struct mproperty p; |
| 992 | size_t outlen = optr->outlen; |
| 993 | |
| 994 | if(outlen) { |
| 995 | const char *str = optr->start; |
| 996 | for(; outlen && *str; outlen--) { |
| C5: formatf loops writing each character via stream callback (storebuffer) for literal format string content | |
| 997 | if(stream(*str++, userp)) |
| 998 | return done; |
| 999 | done++; |
| 1000 | } |
| 1001 | if(optr->flags & FLAGS_SUBSTR) |
| 1002 | /* this is just a substring */ |
| 1003 | continue; |
| 1004 | } |
| 1005 | |
| 1006 | p.flags = optr->flags; |
| 1007 | |
| 1008 | /* pick up the specified width */ |
| 1009 | if(p.flags & FLAGS_WIDTHPARAM) { |
| 1010 | p.width = (int)input[optr->width].val.nums; |
| 1011 | if(p.width < 0) { |
| 1012 | /* "A negative field width is taken as a '-' flag followed by a |
| 1013 | positive field width." */ |
| 1014 | if(p.width == INT_MIN) |
| 1015 | p.width = INT_MAX; |
| 1016 | else |
| 1017 | p.width = -p.width; |
| 1018 | p.flags |= FLAGS_LEFT; |
| 1019 | p.flags &= ~(unsigned int)FLAGS_PAD_NIL; |
| 1020 | } |
| 1021 | } |
| 1022 | else |
| 1023 | p.width = optr->width; |
| 1024 | |
| 1025 | /* pick up the specified precision */ |
| 1026 | if(p.flags & FLAGS_PRECPARAM) { |
| 1027 | p.prec = (int)input[optr->precision].val.nums; |
| 1028 | if(p.prec < 0) |
| 1029 | /* "A negative precision is taken as if the precision were |
| 1030 | omitted." */ |
| 1031 | p.prec = -1; |
| 1032 | } |
| 1033 | else if(p.flags & FLAGS_PREC) |
| 1034 | p.prec = optr->precision; |
| 1035 | else |
| 1036 | p.prec = -1; |
| 1037 | |
| 1038 | switch(iptr->type) { |
| 1039 | case FORMAT_INTU: |
| 1040 | case FORMAT_LONGU: |
| 1041 | case FORMAT_LONGLONGU: |
| 1042 | p.flags |= FLAGS_UNSIGNED; |
| 1043 | if(out_number(userp, stream, &p, iptr->val.numu, 0, work, &done)) |
| 1044 | return done; |
| 1045 | break; |
| 1046 | |
| 1047 | case FORMAT_INT: |
| 1048 | case FORMAT_LONG: |
| 1049 | case FORMAT_LONGLONG: |
| 1050 | if(out_number(userp, stream, &p, iptr->val.numu, |
| 1051 | iptr->val.nums, work, &done)) |
| 1052 | return done; |
| 1053 | break; |
| 1054 | |
| C5: out_string called for %s format specifier, can write arbitrary-length strings | |
| 1055 | case FORMAT_STRING: |
| 1056 | if(out_string(userp, stream, &p, iptr->val.str, &done)) |
| 1057 | return done; |
| 1058 | break; |
| 1059 | |
| 1060 | case FORMAT_PTR: |
| 1061 | if(out_pointer(userp, stream, &p, iptr->val.ptr, work, &done)) |
| 1062 | return done; |
| 1063 | break; |
| 1064 | |
| 1065 | case FORMAT_DOUBLE: |
| 1066 | if(out_double(userp, stream, &p, iptr->val.dnum, work, &done)) |
| 1067 | return done; |
| 1068 | break; |
| 1069 | |
| 1070 | case FORMAT_INTPTR: |
| 1071 | /* Answer the count of characters written. */ |
| 1072 | #ifdef HAVE_LONG_LONG_TYPE |
| 1073 | if(p.flags & FLAGS_LONGLONG) |
| 1074 | *(LONG_LONG_TYPE *)iptr->val.ptr = (LONG_LONG_TYPE)done; |
| 1075 | else |
| 1076 | #endif |
| 1077 | if(p.flags & FLAGS_LONG) |
| 1078 | *(long *)iptr->val.ptr = (long)done; |
| 1079 | else if(!(p.flags & FLAGS_SHORT)) |
| 1080 | *(int *)iptr->val.ptr = (int)done; |
| 1081 | else |
| 1082 | *(short *)iptr->val.ptr = (short)done; |
| 1083 | break; |
| 1084 | |
| 1085 | default: |
| 1086 | break; |
| 1087 | } |
| 1088 | } |
| 1089 | return done; |
| 1090 | } |
Show 124 lines (not cited by the judge)
| 965 | static int formatf( |
| 966 | void *userp, /* untouched by format(), just sent to the stream() function in |
| 967 | the second argument */ |
| 968 | /* function pointer called for each output character */ |
| 969 | int (*stream)(unsigned char, void *), |
| 970 | const char *format, /* %-formatted string */ |
| 971 | va_list ap_save) /* list of parameters */ |
| 972 | { |
| 973 | int done = 0; /* number of characters written */ |
| 974 | int i; |
| 975 | int ocount = 0; /* number of output segments */ |
| 976 | int icount = 0; /* number of input arguments */ |
| 977 | |
| 978 | struct outsegment output[MAX_SEGMENTS]; |
| 979 | struct va_input input[MAX_PARAMETERS]; |
| 980 | char work[BUFFSIZE + 2]; |
| 981 | |
| 982 | /* Parse the format string */ |
| 983 | if(parsefmt(format, output, input, &ocount, &icount, ap_save)) |
| 984 | return 0; |
| 985 | |
| 986 | for(i = 0; i < ocount; i++) { |
| 987 | struct outsegment *optr = &output[i]; |
| 988 | struct va_input *iptr = &input[optr->input]; |
| 989 | struct mproperty p; |
| 990 | size_t outlen = optr->outlen; |
| 991 | |
| 992 | if(outlen) { |
| 993 | const char *str = optr->start; |
| 994 | for(; outlen && *str; outlen--) { |
| 995 | if(stream(*str++, userp)) |
| 996 | return done; |
| 997 | done++; |
| 998 | } |
| 999 | if(optr->flags & FLAGS_SUBSTR) |
| 1000 | /* this is just a substring */ |
| 1001 | continue; |
| 1002 | } |
| 1003 | |
| 1004 | p.flags = optr->flags; |
| 1005 | |
| 1006 | /* pick up the specified width */ |
| 1007 | if(p.flags & FLAGS_WIDTHPARAM) { |
| 1008 | p.width = (int)input[optr->width].val.nums; |
| 1009 | if(p.width < 0) { |
| 1010 | /* "A negative field width is taken as a '-' flag followed by a |
| 1011 | positive field width." */ |
| 1012 | if(p.width == INT_MIN) |
| 1013 | p.width = INT_MAX; |
| 1014 | else |
| 1015 | p.width = -p.width; |
| 1016 | p.flags |= FLAGS_LEFT; |
| 1017 | p.flags &= ~(unsigned int)FLAGS_PAD_NIL; |
| 1018 | } |
| 1019 | } |
| 1020 | else |
| 1021 | p.width = optr->width; |
| 1022 | |
| 1023 | /* pick up the specified precision */ |
| 1024 | if(p.flags & FLAGS_PRECPARAM) { |
| 1025 | p.prec = (int)input[optr->precision].val.nums; |
| 1026 | if(p.prec < 0) |
| 1027 | /* "A negative precision is taken as if the precision were |
| 1028 | omitted." */ |
| 1029 | p.prec = -1; |
| 1030 | } |
| 1031 | else if(p.flags & FLAGS_PREC) |
| 1032 | p.prec = optr->precision; |
| 1033 | else |
| 1034 | p.prec = -1; |
| 1035 | |
| 1036 | switch(iptr->type) { |
| 1037 | case FORMAT_INTU: |
| 1038 | case FORMAT_LONGU: |
| 1039 | case FORMAT_LONGLONGU: |
| 1040 | p.flags |= FLAGS_UNSIGNED; |
| 1041 | if(out_number(userp, stream, &p, iptr->val.numu, 0, work, &done)) |
| 1042 | return done; |
| 1043 | break; |
| 1044 | |
| 1045 | case FORMAT_INT: |
| 1046 | case FORMAT_LONG: |
| 1047 | case FORMAT_LONGLONG: |
| 1048 | if(out_number(userp, stream, &p, iptr->val.numu, |
| 1049 | iptr->val.nums, work, &done)) |
| 1050 | return done; |
| 1051 | break; |
| 1052 | |
| 1053 | case FORMAT_STRING: |
| 1054 | if(out_string(userp, stream, &p, iptr->val.str, &done)) |
| 1055 | return done; |
| 1056 | break; |
| 1057 | |
| 1058 | case FORMAT_PTR: |
| 1059 | if(out_pointer(userp, stream, &p, iptr->val.ptr, work, &done)) |
| 1060 | return done; |
| 1061 | break; |
| 1062 | |
| 1063 | case FORMAT_DOUBLE: |
| 1064 | if(out_double(userp, stream, &p, iptr->val.dnum, work, &done)) |
| 1065 | return done; |
| 1066 | break; |
| 1067 | |
| 1068 | case FORMAT_INTPTR: |
| 1069 | /* Answer the count of characters written. */ |
| 1070 | #ifdef HAVE_LONG_LONG_TYPE |
| 1071 | if(p.flags & FLAGS_LONGLONG) |
| 1072 | *(LONG_LONG_TYPE *) iptr->val.ptr = (LONG_LONG_TYPE)done; |
| 1073 | else |
| 1074 | #endif |
| 1075 | if(p.flags & FLAGS_LONG) |
| 1076 | *(long *) iptr->val.ptr = (long)done; |
| 1077 | else if(!(p.flags & FLAGS_SHORT)) |
| 1078 | *(int *) iptr->val.ptr = (int)done; |
| 1079 | else |
| 1080 | *(short *) iptr->val.ptr = (short)done; |
| 1081 | break; |
| 1082 | |
| 1083 | default: |
| 1084 | break; |
| 1085 | } |
| 1086 | } |
| 1087 | return done; |
| 1088 | } |