Disk Space Exhaustion leading to a Denial of Service (DoS)
What the report claims
The curl debug trace function can write unlimited data to log files, causing disk space exhaustion and denial of service when large data transfers occur.
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: Debug & ExperimentsSee the withdrawn model reasoning
The following reasoning failed the citation check and does not support the final finding.
- The --trace/--trace-ascii/--trace-bin options are debug features explicitly enabled by the user (off by default).
- The project's security policy explicitly states: 'Vulnerabilities in features which are off by default (in the build) and documented as experimental, or exist only in debug mode, are not eligible for a reward and we do not consider them security problems.'
- The disk growth is a direct, documented consequence of enabling trace logging — the feature is designed to capture all transferred data for debugging.
- An attacker would need the user to voluntarily run curl with --trace against a malicious server; this is equivalent to tricking a user into running a crafted command line, which the policy also excludes.
How this analysis ran 5 claims · 7 citations verified
Evidence check: the behavior is real; the project's policy section 'Debug & Experiments' may exclude it
Checked against curl master on 2025-07-14 (b6c636256d43). Test 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 detailed report and PoC. The behavior you describe is real: when --trace or --trace-ascii is used, curl writes all transferred data to the trace file without a size limit. However, curl's security policy classifies debug/tracing features (which are off by default and must be explicitly enabled by the user) as 'not security problems' — see the 'Debug & Experiments' section. Since the user must choose to enable tracing and specify the output file, the disk consumption is an expected consequence of the feature's design, not a vulnerability. We therefore do not consider this a security issue.
Inspect all claims and source code 5 claims · 8 code excerpts
This model judgment was withdrawn. The claim assessments below are preserved for inspection and may be wrong.
Report claims
tool_debug_cb function writes large amounts of debug data to a log file without size limits when --trace or --trace-ascii options are used
Read the reasoning
The code confirms tool_debug_cb writes trace data to global->trace_stream without any cumulative size checks or limits. The dump function (called at line 230-231) writes formatted hex/ASCII output per byte of transferred data.
writes raw HTTP header data to heads->stream in TRACE_PLAIN case CURLINFO_HEADER_OUT
Read the reasoning
In TRACE_PLAIN mode, CURLINFO_HEADER_OUT writes header data line by line to the output stream (which is global->trace_stream when tracing enabled) without size limits.
large HTTP response or unlimited upload causes continuous writes to trace log
Read the reasoning
A server sending unlimited chunked data (as in the PoC) will trigger repeated CURLINFO_DATA_IN callbacks, each causing writes to the trace log. For TRACE_ASCII/BIN, dump() writes O(data size) formatted output.
disk space exhaustion can disrupt other services on the same server
Read the reasoning
The trace file is opened in append/text mode and writes accumulate without bound. Disk exhaustion is a plausible consequence of unbounded writes.
run curl with --trace output.log against an unlimited data server to observe growing output.log
Read the reasoning
The PoC uses --trace output.log which enables tracing to a file. The provided server streams unlimited data, triggering continuous trace writes. Reproduction is feasible.
Deterministic checks that failed
- symbol estimate_formatted_data_size() does not exist in curl at b6c636256d43 or in any release since 7.50
- symbol tool_strbytel() does not exist in curl at b6c636256d43 or in any release since 7.50
- quote 5 of 12 quoted lines appear nowhere in curl, e.g. 'if (global->trace_stream && global->trace_fopened && global-'
- quote 6 of 12 quoted lines appear nowhere in curl, e.g. 'trace tracetype, curl_infotype infotype, struct GlobalConfig'
Read the original report (excerpt)
Source code
| 78 | int tool_debug_cb(CURL *handle, curl_infotype type, |
| 79 | char *data, size_t size, |
| 80 | void *userdata) |
| 81 | { |
| 82 | struct OperationConfig *operation = userdata; |
| 83 | struct GlobalConfig *global = operation->global; |
| 84 | FILE *output = tool_stderr; |
| 85 | const char *text; |
| 86 | struct timeval tv; |
| 87 | char timebuf[20]; |
| 88 | /* largest signed 64-bit is: 9,223,372,036,854,775,807 |
| 89 | * max length in decimal: 1 + (6*3) = 19 |
| 90 | * formatted via TRC_IDS_FORMAT_IDS_2 this becomes 2 + 19 + 1 + 19 + 2 = 43 |
| 91 | * negative xfer-id are not printed, negative conn-ids use TRC_IDS_FORMAT_1 |
| 92 | */ |
| 93 | char idsbuf[60]; |
| 94 | curl_off_t xfer_id, conn_id; |
| 95 | |
| 96 | (void)handle; /* not used */ |
| 97 | |
| 98 | if(global->tracetime) { |
| 99 | tv = tvrealnow(); |
| 100 | msnprintf(timebuf, sizeof(timebuf), "%s.%06ld ", |
| 101 | hms_for_sec(tv.tv_sec), (long)tv.tv_usec); |
| 102 | } |
| 103 | else |
| 104 | timebuf[0] = 0; |
| 105 | |
| 106 | if(handle && global->traceids && |
| 107 | !curl_easy_getinfo(handle, CURLINFO_XFER_ID, &xfer_id) && xfer_id >= 0) { |
| 108 | if(!curl_easy_getinfo(handle, CURLINFO_CONN_ID, &conn_id) && |
| 109 | conn_id >= 0) { |
| 110 | msnprintf(idsbuf, sizeof(idsbuf), TRC_IDS_FORMAT_IDS_2, |
| 111 | xfer_id, conn_id); |
| 112 | } |
| 113 | else { |
| 114 | msnprintf(idsbuf, sizeof(idsbuf), TRC_IDS_FORMAT_IDS_1, xfer_id); |
| 115 | } |
| 116 | } |
| 117 | else |
| 118 | idsbuf[0] = 0; |
| 119 | |
| 120 | if(!global->trace_stream) { |
| 121 | /* open for append */ |
| 122 | if(!strcmp("-", global->trace_dump)) |
| C5: --trace sets global->trace_dump, enabling trace_stream | |
| 123 | global->trace_stream = stdout; |
| 124 | else if(!strcmp("%", global->trace_dump)) |
| 125 | /* Ok, this is somewhat hackish but we do it undocumented for now */ |
| 126 | global->trace_stream = tool_stderr; |
| 127 | else { |
| 128 | global->trace_stream = fopen(global->trace_dump, FOPEN_WRITETEXT); |
| C4: Trace file opened with fopen(global->trace_dump, FOPEN_WRITETEXT) - grows indefinitely | |
| 129 | global->trace_fopened = TRUE; |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | if(global->trace_stream) |
| 134 | output = global->trace_stream; |
| 135 | |
| 136 | if(!output) { |
| 137 | warnf(global, "Failed to create/open output"); |
| 138 | return 0; |
| 139 | } |
| 140 | |
| 141 | if(global->tracetype == TRACE_PLAIN) { |
| C1: TRACE_PLAIN case writes via fwrite/fprintf to output (global->trace_stream) without size limits | |
| 142 | static bool newl = FALSE; |
| 143 | static bool traced_data = FALSE; |
| 144 | |
| 145 | switch(type) { |
| 146 | case CURLINFO_HEADER_OUT: |
| C2: CURLINFO_HEADER_OUT case in TRACE_PLAIN writes headers via fwrite at lines 155, 162 | |
| 147 | if(size > 0) { |
| 148 | size_t st = 0; |
| 149 | size_t i; |
| 150 | for(i = 0; i < size - 1; i++) { |
| 151 | if(data[i] == '\n') { /* LF */ |
| 152 | if(!newl) { |
| 153 | log_line_start(output, timebuf, idsbuf, type); |
| 154 | } |
| 155 | (void)fwrite(data + st, i - st + 1, 1, output); |
| 156 | st = i + 1; |
| 157 | newl = FALSE; |
| 158 | } |
| 159 | } |
| 160 | if(!newl) |
| 161 | log_line_start(output, timebuf, idsbuf, type); |
| 162 | (void)fwrite(data + st, i - st + 1, 1, output); |
| 163 | } |
| 164 | newl = (size && (data[size - 1] != '\n')); |
| 165 | traced_data = FALSE; |
| 166 | break; |
| 167 | case CURLINFO_TEXT: |
| 168 | case CURLINFO_HEADER_IN: |
| 169 | if(!newl) |
| 170 | log_line_start(output, timebuf, idsbuf, type); |
| 171 | (void)fwrite(data, size, 1, output); |
| 172 | newl = (size && (data[size - 1] != '\n')); |
| 173 | traced_data = FALSE; |
| 174 | break; |
| 175 | case CURLINFO_DATA_OUT: |
| C3: CURLINFO_DATA_IN/OUT cases write '[%zu bytes data]\n' but dump() logs every byte for TRACE_ASCII/BIN | |
| 176 | case CURLINFO_DATA_IN: |
| 177 | case CURLINFO_SSL_DATA_IN: |
| 178 | case CURLINFO_SSL_DATA_OUT: |
| 179 | if(!traced_data) { |
| 180 | /* if the data is output to a tty and we are sending this debug trace |
| 181 | to stderr or stdout, we do not display the alert about the data not |
| 182 | being shown as the data _is_ shown then just not via this |
| 183 | function */ |
| 184 | if(!global->isatty || |
| 185 | ((output != tool_stderr) && (output != stdout))) { |
| 186 | if(!newl) |
| 187 | log_line_start(output, timebuf, idsbuf, type); |
| 188 | fprintf(output, "[%zu bytes data]\n", size); |
| 189 | newl = FALSE; |
| 190 | traced_data = TRUE; |
| 191 | } |
| 192 | } |
| 193 | break; |
| 194 | default: /* nada */ |
| 195 | newl = FALSE; |
| 196 | traced_data = FALSE; |
| 197 | break; |
| 198 | } |
| 199 | |
| 200 | return 0; |
| 201 | } |
| 202 | |
| 203 | switch(type) { |
| 204 | case CURLINFO_TEXT: |
| 205 | fprintf(output, "%s%s== Info: %.*s", timebuf, idsbuf, (int)size, data); |
| 206 | FALLTHROUGH(); |
| 207 | default: /* in case a new one is introduced to shock us */ |
| 208 | return 0; |
| 209 | |
| 210 | case CURLINFO_HEADER_OUT: |
| 211 | text = "=> Send header"; |
| 212 | break; |
| 213 | case CURLINFO_DATA_OUT: |
| 214 | text = "=> Send data"; |
| 215 | break; |
| 216 | case CURLINFO_HEADER_IN: |
| 217 | text = "<= Recv header"; |
| 218 | break; |
| 219 | case CURLINFO_DATA_IN: |
| 220 | text = "<= Recv data"; |
| 221 | break; |
| 222 | case CURLINFO_SSL_DATA_IN: |
| 223 | text = "<= Recv SSL data"; |
| 224 | break; |
| 225 | case CURLINFO_SSL_DATA_OUT: |
| 226 | text = "=> Send SSL data"; |
| 227 | break; |
| 228 | } |
| 229 | |
| 230 | dump(timebuf, idsbuf, text, output, (unsigned char *) data, size, |
| C1: dump() called for TRACE_ASCII/TRACE_BIN, writes formatted data without size limits | |
| C3: dump() called for data types, writes formatted output proportional to data size | |
| 231 | global->tracetype, type); |
| 232 | return 0; |
| 233 | } |
| 80 | int tool_debug_cb(CURL *handle, curl_infotype type, |
| 81 | char *data, size_t size, |
| 82 | void *userdata) |
| 83 | { |
| 84 | struct OperationConfig *operation = userdata; |
| 85 | struct GlobalConfig *config = operation->global; |
| 86 | FILE *output = tool_stderr; |
| 87 | const char *text; |
| 88 | struct timeval tv; |
| 89 | char timebuf[20]; |
| 90 | /* largest signed 64-bit is: 9,223,372,036,854,775,807 |
| 91 | * max length in decimal: 1 + (6*3) = 19 |
| 92 | * formatted via TRC_IDS_FORMAT_IDS_2 this becomes 2 + 19 + 1 + 19 + 2 = 43 |
| 93 | * negative xfer-id are not printed, negative conn-ids use TRC_IDS_FORMAT_1 |
| 94 | */ |
| 95 | char idsbuf[60]; |
| 96 | curl_off_t xfer_id, conn_id; |
| 97 | |
| 98 | (void)handle; /* not used */ |
| 99 | |
| 100 | if(config->tracetime) { |
| 101 | tv = tvrealnow(); |
| 102 | msnprintf(timebuf, sizeof(timebuf), "%s.%06ld ", |
| 103 | hms_for_sec(tv.tv_sec), (long)tv.tv_usec); |
| 104 | } |
| 105 | else |
| 106 | timebuf[0] = 0; |
| 107 | |
| 108 | if(handle && config->traceids && |
| 109 | !curl_easy_getinfo(handle, CURLINFO_XFER_ID, &xfer_id) && xfer_id >= 0) { |
| 110 | if(!curl_easy_getinfo(handle, CURLINFO_CONN_ID, &conn_id) && |
| 111 | conn_id >= 0) { |
| 112 | msnprintf(idsbuf, sizeof(idsbuf), TRC_IDS_FORMAT_IDS_2, |
| 113 | xfer_id, conn_id); |
| 114 | } |
| 115 | else { |
| 116 | msnprintf(idsbuf, sizeof(idsbuf), TRC_IDS_FORMAT_IDS_1, xfer_id); |
| 117 | } |
| 118 | } |
| 119 | else |
| 120 | idsbuf[0] = 0; |
| 121 | |
| 122 | if(!config->trace_stream) { |
| 123 | /* open for append */ |
| 124 | if(!strcmp("-", config->trace_dump)) |
| 125 | config->trace_stream = stdout; |
| 126 | else if(!strcmp("%", config->trace_dump)) |
| 127 | /* Ok, this is somewhat hackish but we do it undocumented for now */ |
| 128 | config->trace_stream = tool_stderr; |
| 129 | else { |
| 130 | config->trace_stream = fopen(config->trace_dump, FOPEN_WRITETEXT); |
| 131 | config->trace_fopened = TRUE; |
| 132 | } |
| 133 | } |
| 134 | |
| 135 | if(config->trace_stream) |
| 136 | output = config->trace_stream; |
| 137 | |
| 138 | if(!output) { |
| 139 | warnf(config, "Failed to create/open output"); |
| 140 | return 0; |
| 141 | } |
| 142 | |
| 143 | if(config->tracetype == TRACE_PLAIN) { |
| 144 | static bool newl = FALSE; |
| 145 | static bool traced_data = FALSE; |
| 146 | |
| 147 | switch(type) { |
| 148 | case CURLINFO_HEADER_OUT: |
| 149 | if(size > 0) { |
| 150 | size_t st = 0; |
| 151 | size_t i; |
| 152 | for(i = 0; i < size - 1; i++) { |
| 153 | if(data[i] == '\n') { /* LF */ |
| 154 | if(!newl) { |
| 155 | log_line_start(output, timebuf, idsbuf, type); |
| 156 | } |
| 157 | (void)fwrite(data + st, i - st + 1, 1, output); |
| 158 | st = i + 1; |
| 159 | newl = FALSE; |
| 160 | } |
| 161 | } |
| 162 | if(!newl) |
| 163 | log_line_start(output, timebuf, idsbuf, type); |
| 164 | (void)fwrite(data + st, i - st + 1, 1, output); |
| 165 | } |
| 166 | newl = (size && (data[size - 1] != '\n')); |
| 167 | traced_data = FALSE; |
| 168 | break; |
| 169 | case CURLINFO_TEXT: |
| 170 | case CURLINFO_HEADER_IN: |
| 171 | if(!newl) |
| 172 | log_line_start(output, timebuf, idsbuf, type); |
| 173 | (void)fwrite(data, size, 1, output); |
| 174 | newl = (size && (data[size - 1] != '\n')); |
| 175 | traced_data = FALSE; |
| 176 | break; |
| 177 | case CURLINFO_DATA_OUT: |
| 178 | case CURLINFO_DATA_IN: |
| 179 | case CURLINFO_SSL_DATA_IN: |
| 180 | case CURLINFO_SSL_DATA_OUT: |
| 181 | if(!traced_data) { |
| 182 | /* if the data is output to a tty and we are sending this debug trace |
| 183 | to stderr or stdout, we do not display the alert about the data not |
| 184 | being shown as the data _is_ shown then just not via this |
| 185 | function */ |
| 186 | if(!config->isatty || |
| 187 | ((output != tool_stderr) && (output != stdout))) { |
| 188 | if(!newl) |
| 189 | log_line_start(output, timebuf, idsbuf, type); |
| 190 | fprintf(output, "[%zu bytes data]\n", size); |
| 191 | newl = FALSE; |
| 192 | traced_data = TRUE; |
| 193 | } |
| 194 | } |
| 195 | break; |
| 196 | default: /* nada */ |
| 197 | newl = FALSE; |
| 198 | traced_data = FALSE; |
| 199 | break; |
| 200 | } |
| 201 | |
| 202 | return 0; |
| 203 | } |
| 204 | |
| 205 | switch(type) { |
| 206 | case CURLINFO_TEXT: |
| 207 | fprintf(output, "%s%s== Info: %.*s", timebuf, idsbuf, (int)size, data); |
| 208 | FALLTHROUGH(); |
| 209 | default: /* in case a new one is introduced to shock us */ |
| 210 | return 0; |
| 211 | |
| 212 | case CURLINFO_HEADER_OUT: |
| 213 | text = "=> Send header"; |
| 214 | break; |
| 215 | case CURLINFO_DATA_OUT: |
| 216 | text = "=> Send data"; |
| 217 | break; |
| 218 | case CURLINFO_HEADER_IN: |
| 219 | text = "<= Recv header"; |
| 220 | break; |
| 221 | case CURLINFO_DATA_IN: |
| 222 | text = "<= Recv data"; |
| 223 | break; |
| 224 | case CURLINFO_SSL_DATA_IN: |
| 225 | text = "<= Recv SSL data"; |
| 226 | break; |
| 227 | case CURLINFO_SSL_DATA_OUT: |
| 228 | text = "=> Send SSL data"; |
| 229 | break; |
| 230 | } |
| 231 | |
| 232 | dump(timebuf, idsbuf, text, output, (unsigned char *) data, size, |
| 233 | config->tracetype, type); |
| 234 | return 0; |
| 235 | } |
Show 22 lines (not cited by the judge)
| 33 | static void dump(const char *timebuf, const char *idsbuf, const char *text, |
| 34 | FILE *stream, const unsigned char *ptr, size_t size, |
| 35 | trace tracetype, curl_infotype infotype); |
| 36 | |
| 37 | /* |
| 38 | * Return the formatted HH:MM:SS for the tv_sec given. |
| 39 | * NOT thread safe. |
| 40 | */ |
| 41 | static const char *hms_for_sec(time_t tv_sec) |
| 42 | { |
| 43 | static time_t cached_tv_sec; |
| 44 | static char hms_buf[12]; |
| 45 | |
| 46 | if(tv_sec != cached_tv_sec) { |
| 47 | /* !checksrc! disable BANNEDFUNC 1 */ |
| 48 | struct tm *now = localtime(&tv_sec); /* not thread safe either */ |
| 49 | msnprintf(hms_buf, sizeof(hms_buf), "%02d:%02d:%02d", |
| 50 | now->tm_hour, now->tm_min, now->tm_sec); |
| 51 | cached_tv_sec = tv_sec; |
| 52 | } |
| 53 | return hms_buf; |
| 54 | } |
Show 9 lines (not cited by the judge)
| 140 | void errorf(struct GlobalConfig *global, const char *fmt, ...) |
| 141 | { |
| 142 | if(!global->silent || global->showerror) { |
| 143 | va_list ap; |
| 144 | va_start(ap, fmt); |
| 145 | voutf(global, ERROR_PREFIX, fmt, ap); |
| 146 | va_end(ap); |
| 147 | } |
| 148 | } |
Show 9 lines (not cited by the judge)
| 142 | void errorf(struct GlobalConfig *config, const char *fmt, ...) |
| 143 | { |
| 144 | if(!config->silent || config->showerror) { |
| 145 | va_list ap; |
| 146 | va_start(ap, fmt); |
| 147 | voutf(config, ERROR_PREFIX, fmt, ap); |
| 148 | va_end(ap); |
| 149 | } |
| 150 | } |
Show 15 lines (not cited by the judge)
| 56 | static void log_line_start(FILE *log, const char *timebuf, |
| 57 | const char *idsbuf, curl_infotype type) |
| 58 | { |
| 59 | /* |
| 60 | * This is the trace look that is similar to what libcurl makes on its |
| 61 | * own. |
| 62 | */ |
| 63 | static const char * const s_infotype[] = { |
| 64 | "* ", "< ", "> ", "{ ", "} ", "{ ", "} " |
| 65 | }; |
| 66 | if((timebuf && *timebuf) || (idsbuf && *idsbuf)) |
| 67 | fprintf(log, "%s%s%s", timebuf, idsbuf, s_infotype[type]); |
| 68 | else |
| 69 | fputs(s_infotype[type], log); |
| 70 | } |
Show 7 lines (not cited by the judge)
| 106 | void warnf(struct GlobalConfig *global, const char *fmt, ...) |
| 107 | { |
| 108 | va_list ap; |
| 109 | va_start(ap, fmt); |
| 110 | voutf(global, WARN_PREFIX, fmt, ap); |
| 111 | va_end(ap); |
| 112 | } |
Show 7 lines (not cited by the judge)
| 108 | void warnf(struct GlobalConfig *config, const char *fmt, ...) |
| 109 | { |
| 110 | va_list ap; |
| 111 | va_start(ap, fmt); |
| 112 | voutf(config, WARN_PREFIX, fmt, ap); |
| 113 | va_end(ap); |
| 114 | } |