Heap Buffer Over-Read via Malicious SMB Server READ_ANDX Response
What the report claims
A heap buffer over-read in libcurl's SMB implementation allows a malicious server to cause curl to read and output up to 28KB of uninitialized heap memory per request.
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 available evidence does not settle the report. A maintainer should investigate.
Why this finding?
- The bounds check in SMB_DOWNLOAD (line 1121) correctly uses smbc->got as the limit, but smbc->got is set in smb_send_and_recv which is not provided.
- If smb_send_and_recv sets smbc->got from the NetBIOS header length before fully receiving the message, or if the receive buffer is larger than smbc->got and not zeroed, the over-read could occur. The provided code does not settle this.
- The report is detailed and the code location is accurate, but the critical receive-path logic is missing from the evidence.
What still needs checking
- The smb_send_and_recv function implementation (how msg buffer is allocated, how smbc->got is updated, whether NetBIOS header length is trusted before full receive).
How this analysis ran 5 claims · 9 citations verified
Checked against curl master on 2025-12-18 (d7bde803ee83). Dev evaluation split. Citation verification checks that a referenced line was in the evidence; it does not prove the model interpreted it correctly.
Review a draft reply to the reporter
Edit and verify before sending.
Thank you for the detailed report. The SMB_DOWNLOAD bounds check (lib/smb.c:1121) uses smbc->got as the limit, but we need to see smb_send_and_recv to verify whether an attacker can inflate smbc->got via the NetBIOS header while the receive buffer msg contains uninitialized heap memory. Could you point to that function or explain how smbc->got is updated relative to the actual bytes received?
Inspect all claims and source code 5 claims · 6 code excerpts
Report claims
A malicious SMB server can send a crafted READ_ANDX response that causes curl to read and output up to 28KB of uninitialized heap memory per request.
Read the reasoning
The claim depends on whether smbc->got can be made larger than the actual valid data in the receive buffer msg. The provided code shows the bounds check (line 1121) and the read (line 1126), but the critical function smb_send_and_recv that populates msg and sets smbc->got is not in the provided source. Without it, we cannot confirm whether an attacker can inflate smbc->got (e.g., via NetBIOS header length) while msg contains uninitialized heap memory beyond the bytes actually received.
The vulnerability exists in lib/smb.c within the smb_request_state() function, specifically in the SMB_DOWNLOAD state handler at lines 1110-1137.
Read the reasoning
The vulnerable code location matches exactly: smb_request_state function, SMB_DOWNLOAD case at lines 1110-1137 in the provided lib/smb.c source.
A malicious SMB server sends a specially crafted READ_ANDX response that claims a large message size and pads with null bytes to match the claimed size.
Read the reasoning
The trigger describes attacker control of NetBIOS header length and padding to manipulate smbc->got. This logic resides in smb_send_and_recv, which is not in the provided code. Cannot verify from given source.
curl outputs approximately 28KB of leaked heap memory when the server only sent 256 bytes of legitimate data.
Read the reasoning
The 28KB figure depends on C1 being true (that len can be large while reading uninitialized memory). Since C1 is not verifiable, the impact magnitude cannot be confirmed from provided code.
The PoC demonstrates that a server sending 256 bytes of data results in curl writing 28,672 bytes to a file, with the extra bytes being heap memory.
Read the reasoning
The PoC scripts (run_exploit.sh, smb_exploit_server.py) are not included in the provided context. Cannot verify reproduction claims.
Deterministic checks that failed
- file lib/smb.c existed in curl's history but not at bc677a8f16a5
- file lib/smb.c existed in curl's history but not at bc677a8f16a5
Read the original report (excerpt)
Source code
| 1023 | static CURLcode smb_request_state(struct Curl_easy *data, bool *done) |
| 1024 | { |
| 1025 | struct connectdata *conn = data->conn; |
| 1026 | struct smb_conn *smbc = Curl_conn_meta_get(conn, CURL_META_SMB_CONN); |
| 1027 | struct smb_request *req = Curl_meta_get(data, CURL_META_SMB_EASY); |
| 1028 | struct smb_header *h; |
| 1029 | enum smb_req_state next_state = SMB_DONE; |
| 1030 | unsigned short len; |
| 1031 | unsigned short off; |
| 1032 | CURLcode result; |
| 1033 | void *msg = NULL; |
| 1034 | const struct smb_nt_create_response *smb_m; |
| 1035 | |
| 1036 | if(!smbc || !req) |
| 1037 | return CURLE_FAILED_INIT; |
| 1038 | |
| 1039 | if(data->state.upload && (data->state.infilesize < 0)) { |
| 1040 | failf(data, "SMB upload needs to know the size up front"); |
| 1041 | return CURLE_SEND_ERROR; |
| 1042 | } |
| 1043 | |
| 1044 | /* Start the request */ |
| 1045 | if(req->state == SMB_REQUESTING) { |
| 1046 | result = smb_send_tree_connect(data, smbc, req); |
| 1047 | if(result) { |
| 1048 | connclose(conn, "SMB: failed to send tree connect message"); |
| 1049 | return result; |
| 1050 | } |
| 1051 | |
| 1052 | request_state(data, SMB_TREE_CONNECT); |
| 1053 | } |
| 1054 | |
| 1055 | /* Send the previous message and check for a response */ |
| 1056 | result = smb_send_and_recv(data, smbc, &msg); |
| 1057 | if(result && result != CURLE_AGAIN) { |
| 1058 | connclose(conn, "SMB: failed to communicate"); |
| 1059 | return result; |
| 1060 | } |
| 1061 | |
| 1062 | if(!msg) |
| 1063 | return CURLE_OK; |
| C3: smb_send_and_recv called to receive msg; NetBIOS header parsing and smbc->got update happen there (not provided) | |
| 1064 | |
| 1065 | h = msg; |
| 1066 | |
| 1067 | switch(req->state) { |
| 1068 | case SMB_TREE_CONNECT: |
| 1069 | if(h->status) { |
| 1070 | req->result = CURLE_REMOTE_FILE_NOT_FOUND; |
| 1071 | if(h->status == smb_swap32(SMB_ERR_NOACCESS)) |
| 1072 | req->result = CURLE_REMOTE_ACCESS_DENIED; |
| 1073 | break; |
| 1074 | } |
| 1075 | req->tid = smb_swap16(h->tid); |
| 1076 | next_state = SMB_OPEN; |
| 1077 | break; |
| 1078 | |
| 1079 | case SMB_OPEN: |
| 1080 | if(h->status || smbc->got < sizeof(struct smb_nt_create_response)) { |
| 1081 | req->result = CURLE_REMOTE_FILE_NOT_FOUND; |
| 1082 | if(h->status == smb_swap32(SMB_ERR_NOACCESS)) |
| 1083 | req->result = CURLE_REMOTE_ACCESS_DENIED; |
| 1084 | next_state = SMB_TREE_DISCONNECT; |
| 1085 | break; |
| 1086 | } |
| 1087 | smb_m = (const struct smb_nt_create_response *)msg; |
| 1088 | req->fid = smb_swap16(smb_m->fid); |
| 1089 | data->req.offset = 0; |
| 1090 | if(data->state.upload) { |
| 1091 | data->req.size = data->state.infilesize; |
| 1092 | Curl_pgrsSetUploadSize(data, data->req.size); |
| 1093 | next_state = SMB_UPLOAD; |
| 1094 | } |
| 1095 | else { |
| 1096 | data->req.size = smb_swap64(smb_m->end_of_file); |
| 1097 | if(data->req.size < 0) { |
| 1098 | req->result = CURLE_WEIRD_SERVER_REPLY; |
| 1099 | next_state = SMB_CLOSE; |
| 1100 | } |
| 1101 | else { |
| 1102 | Curl_pgrsSetDownloadSize(data, data->req.size); |
| 1103 | if(data->set.get_filetime) |
| 1104 | get_posix_time(&data->info.filetime, smb_m->last_change_time); |
| 1105 | next_state = SMB_DOWNLOAD; |
| 1106 | } |
| 1107 | } |
| 1108 | break; |
| 1109 | |
| 1110 | case SMB_DOWNLOAD: |
| C2: SMB_DOWNLOAD case starts at line 1110 | |
| 1111 | if(h->status || smbc->got < sizeof(struct smb_header) + 15) { |
| 1112 | req->result = CURLE_RECV_ERROR; |
| 1113 | next_state = SMB_CLOSE; |
| 1114 | break; |
| 1115 | } |
| 1116 | len = Curl_read16_le(((const unsigned char *)msg) + |
| C2: len read from offset sizeof(struct smb_header) + 11 | |
| 1117 | sizeof(struct smb_header) + 11); |
| 1118 | off = Curl_read16_le(((const unsigned char *)msg) + |
| C2: off read from offset sizeof(struct smb_header) + 13 | |
| 1119 | sizeof(struct smb_header) + 13); |
| 1120 | if(len > 0) { |
| 1121 | if(off + sizeof(unsigned int) + len > smbc->got) { |
| C1: Bounds check uses smbc->got as the limit, but smbc->got is set in smb_send_and_recv (not provided). Cannot verify if smbc->got can exceed actual initialized data in msg. | |
| C2: Bounds check against smbc->got | |
| 1122 | failf(data, "Invalid input packet"); |
| 1123 | result = CURLE_RECV_ERROR; |
| 1124 | } |
| 1125 | else |
| 1126 | result = Curl_client_write(data, CLIENTWRITE_BODY, |
| C1: Read uses msg + off + 4 with length len, bounded by smbc->got per line 1121. If smbc->got accurately reflects received bytes, no over-read occurs. | |
| C2: Curl_client_write call with computed pointer and len | |
| C4: Output length is len, which comes from attacker-controlled response at line 1116 | |
| 1127 | (char *)msg + off + sizeof(unsigned int), |
| 1128 | len); |
| 1129 | if(result) { |
| 1130 | req->result = result; |
| 1131 | next_state = SMB_CLOSE; |
| 1132 | break; |
| 1133 | } |
| 1134 | } |
| 1135 | data->req.offset += len; |
| 1136 | next_state = (len < MAX_PAYLOAD_SIZE) ? SMB_CLOSE : SMB_DOWNLOAD; |
| 1137 | break; |
| 1138 | |
| 1139 | case SMB_UPLOAD: |
| 1140 | if(h->status || smbc->got < sizeof(struct smb_header) + 7) { |
| 1141 | req->result = CURLE_UPLOAD_FAILED; |
| 1142 | next_state = SMB_CLOSE; |
| 1143 | break; |
| 1144 | } |
| 1145 | len = Curl_read16_le(((const unsigned char *)msg) + |
| 1146 | sizeof(struct smb_header) + 5); |
| 1147 | data->req.bytecount += len; |
| 1148 | data->req.offset += len; |
| 1149 | Curl_pgrs_upload_inc(data, len); |
| 1150 | if(data->req.bytecount >= data->req.size) |
| 1151 | next_state = SMB_CLOSE; |
| 1152 | else |
| 1153 | next_state = SMB_UPLOAD; |
| 1154 | break; |
| 1155 | |
| 1156 | case SMB_CLOSE: |
| 1157 | /* We do not care if the close failed, proceed to tree disconnect anyway */ |
| 1158 | next_state = SMB_TREE_DISCONNECT; |
| 1159 | break; |
| 1160 | |
| 1161 | case SMB_TREE_DISCONNECT: |
| 1162 | next_state = SMB_DONE; |
| 1163 | break; |
| 1164 | |
| 1165 | default: |
| 1166 | smb_pop_message(smbc); |
| 1167 | return CURLE_OK; /* ignore */ |
| 1168 | } |
| 1169 | |
| 1170 | smb_pop_message(smbc); |
| 1171 | |
| 1172 | switch(next_state) { |
| 1173 | case SMB_OPEN: |
| 1174 | result = smb_send_open(data, smbc, req); |
| 1175 | break; |
| 1176 | |
| 1177 | case SMB_DOWNLOAD: |
| 1178 | result = smb_send_read(data, smbc, req); |
| 1179 | break; |
| 1180 | |
| 1181 | case SMB_UPLOAD: |
| 1182 | result = smb_send_write(data, smbc, req); |
| 1183 | break; |
| 42 | unsigned short Curl_read16_le(const unsigned char *buf) |
| 43 | { |
| 44 | return (unsigned short)(((unsigned short)buf[0]) | |
| 45 | ((unsigned short)buf[1] << 8)); |
| 46 | } |
Show 27 lines (not cited by the judge)
| 66 | CURLcode Curl_client_write(struct Curl_easy *data, |
| 67 | int type, const char *buf, size_t blen) |
| 68 | { |
| 69 | CURLcode result; |
| 70 | |
| 71 | /* it is one of those, at least */ |
| 72 | DEBUGASSERT(type & |
| 73 | (CLIENTWRITE_BODY | CLIENTWRITE_HEADER | CLIENTWRITE_INFO)); |
| 74 | /* BODY is only BODY (with optional EOS) */ |
| 75 | DEBUGASSERT(!(type & CLIENTWRITE_BODY) || |
| 76 | ((type & ~(CLIENTWRITE_BODY | CLIENTWRITE_EOS)) == 0)); |
| 77 | /* INFO is only INFO (with optional EOS) */ |
| 78 | DEBUGASSERT(!(type & CLIENTWRITE_INFO) || |
| 79 | ((type & ~(CLIENTWRITE_INFO | CLIENTWRITE_EOS)) == 0)); |
| 80 | |
| 81 | if(!data->req.writer_stack) { |
| 82 | result = do_init_writer_stack(data); |
| 83 | if(result) |
| 84 | return result; |
| 85 | DEBUGASSERT(data->req.writer_stack); |
| 86 | } |
| 87 | |
| 88 | result = Curl_cwriter_write(data, data->req.writer_stack, type, buf, blen); |
| 89 | CURL_TRC_WRITE(data, "client_write(type=%x, len=%zu) -> %d", |
| 90 | type, blen, result); |
| 91 | return result; |
| 92 | } |
Show 26 lines (not cited by the judge)
| 70 | CURLcode Curl_client_write(struct Curl_easy *data, |
| 71 | int type, const char *buf, size_t blen) |
| 72 | { |
| 73 | CURLcode result; |
| 74 | |
| 75 | /* it is one of those, at least */ |
| 76 | DEBUGASSERT(type & (CLIENTWRITE_BODY|CLIENTWRITE_HEADER|CLIENTWRITE_INFO)); |
| 77 | /* BODY is only BODY (with optional EOS) */ |
| 78 | DEBUGASSERT(!(type & CLIENTWRITE_BODY) || |
| 79 | ((type & ~(CLIENTWRITE_BODY|CLIENTWRITE_EOS)) == 0)); |
| 80 | /* INFO is only INFO (with optional EOS) */ |
| 81 | DEBUGASSERT(!(type & CLIENTWRITE_INFO) || |
| 82 | ((type & ~(CLIENTWRITE_INFO|CLIENTWRITE_EOS)) == 0)); |
| 83 | |
| 84 | if(!data->req.writer_stack) { |
| 85 | result = do_init_writer_stack(data); |
| 86 | if(result) |
| 87 | return result; |
| 88 | DEBUGASSERT(data->req.writer_stack); |
| 89 | } |
| 90 | |
| 91 | result = Curl_cwriter_write(data, data->req.writer_stack, type, buf, blen); |
| 92 | CURL_TRC_WRITE(data, "client_write(type=%x, len=%zu) -> %d", |
| 93 | type, blen, result); |
| 94 | return result; |
| 95 | } |
Show 161 lines (not cited by the judge)
| 1030 | static CURLcode smb_request_state(struct Curl_easy *data, bool *done) |
| 1031 | { |
| 1032 | struct connectdata *conn = data->conn; |
| 1033 | struct smb_conn *smbc = Curl_conn_meta_get(conn, CURL_META_SMB_CONN); |
| 1034 | struct smb_request *req = Curl_meta_get(data, CURL_META_SMB_EASY); |
| 1035 | struct smb_header *h; |
| 1036 | enum smb_req_state next_state = SMB_DONE; |
| 1037 | unsigned short len; |
| 1038 | unsigned short off; |
| 1039 | CURLcode result; |
| 1040 | void *msg = NULL; |
| 1041 | const struct smb_nt_create_response *smb_m; |
| 1042 | |
| 1043 | if(!smbc || !req) |
| 1044 | return CURLE_FAILED_INIT; |
| 1045 | |
| 1046 | if(data->state.upload && (data->state.infilesize < 0)) { |
| 1047 | failf(data, "SMB upload needs to know the size up front"); |
| 1048 | return CURLE_SEND_ERROR; |
| 1049 | } |
| 1050 | |
| 1051 | /* Start the request */ |
| 1052 | if(req->state == SMB_REQUESTING) { |
| 1053 | result = smb_send_tree_connect(data, smbc, req); |
| 1054 | if(result) { |
| 1055 | connclose(conn, "SMB: failed to send tree connect message"); |
| 1056 | return result; |
| 1057 | } |
| 1058 | |
| 1059 | request_state(data, SMB_TREE_CONNECT); |
| 1060 | } |
| 1061 | |
| 1062 | /* Send the previous message and check for a response */ |
| 1063 | result = smb_send_and_recv(data, smbc, &msg); |
| 1064 | if(result && result != CURLE_AGAIN) { |
| 1065 | connclose(conn, "SMB: failed to communicate"); |
| 1066 | return result; |
| 1067 | } |
| 1068 | |
| 1069 | if(!msg) |
| 1070 | return CURLE_OK; |
| 1071 | |
| 1072 | h = msg; |
| 1073 | |
| 1074 | switch(req->state) { |
| 1075 | case SMB_TREE_CONNECT: |
| 1076 | if(h->status) { |
| 1077 | req->result = CURLE_REMOTE_FILE_NOT_FOUND; |
| 1078 | if(h->status == smb_swap32(SMB_ERR_NOACCESS)) |
| 1079 | req->result = CURLE_REMOTE_ACCESS_DENIED; |
| 1080 | break; |
| 1081 | } |
| 1082 | req->tid = smb_swap16(h->tid); |
| 1083 | next_state = SMB_OPEN; |
| 1084 | break; |
| 1085 | |
| 1086 | case SMB_OPEN: |
| 1087 | if(h->status || smbc->got < sizeof(struct smb_nt_create_response)) { |
| 1088 | req->result = CURLE_REMOTE_FILE_NOT_FOUND; |
| 1089 | if(h->status == smb_swap32(SMB_ERR_NOACCESS)) |
| 1090 | req->result = CURLE_REMOTE_ACCESS_DENIED; |
| 1091 | next_state = SMB_TREE_DISCONNECT; |
| 1092 | break; |
| 1093 | } |
| 1094 | smb_m = (const struct smb_nt_create_response*) msg; |
| 1095 | req->fid = smb_swap16(smb_m->fid); |
| 1096 | data->req.offset = 0; |
| 1097 | if(data->state.upload) { |
| 1098 | data->req.size = data->state.infilesize; |
| 1099 | Curl_pgrsSetUploadSize(data, data->req.size); |
| 1100 | next_state = SMB_UPLOAD; |
| 1101 | } |
| 1102 | else { |
| 1103 | data->req.size = smb_swap64(smb_m->end_of_file); |
| 1104 | if(data->req.size < 0) { |
| 1105 | req->result = CURLE_WEIRD_SERVER_REPLY; |
| 1106 | next_state = SMB_CLOSE; |
| 1107 | } |
| 1108 | else { |
| 1109 | Curl_pgrsSetDownloadSize(data, data->req.size); |
| 1110 | if(data->set.get_filetime) |
| 1111 | get_posix_time(&data->info.filetime, smb_m->last_change_time); |
| 1112 | next_state = SMB_DOWNLOAD; |
| 1113 | } |
| 1114 | } |
| 1115 | break; |
| 1116 | |
| 1117 | case SMB_DOWNLOAD: |
| 1118 | if(h->status || smbc->got < sizeof(struct smb_header) + 15) { |
| 1119 | req->result = CURLE_RECV_ERROR; |
| 1120 | next_state = SMB_CLOSE; |
| 1121 | break; |
| 1122 | } |
| 1123 | len = Curl_read16_le(((const unsigned char *) msg) + |
| 1124 | sizeof(struct smb_header) + 11); |
| 1125 | off = Curl_read16_le(((const unsigned char *) msg) + |
| 1126 | sizeof(struct smb_header) + 13); |
| 1127 | if(len > 0) { |
| 1128 | if(off + sizeof(unsigned int) + len > smbc->got) { |
| 1129 | failf(data, "Invalid input packet"); |
| 1130 | result = CURLE_RECV_ERROR; |
| 1131 | } |
| 1132 | else |
| 1133 | result = Curl_client_write(data, CLIENTWRITE_BODY, |
| 1134 | (char *)msg + off + sizeof(unsigned int), |
| 1135 | len); |
| 1136 | if(result) { |
| 1137 | req->result = result; |
| 1138 | next_state = SMB_CLOSE; |
| 1139 | break; |
| 1140 | } |
| 1141 | } |
| 1142 | data->req.offset += len; |
| 1143 | next_state = (len < MAX_PAYLOAD_SIZE) ? SMB_CLOSE : SMB_DOWNLOAD; |
| 1144 | break; |
| 1145 | |
| 1146 | case SMB_UPLOAD: |
| 1147 | if(h->status || smbc->got < sizeof(struct smb_header) + 7) { |
| 1148 | req->result = CURLE_UPLOAD_FAILED; |
| 1149 | next_state = SMB_CLOSE; |
| 1150 | break; |
| 1151 | } |
| 1152 | len = Curl_read16_le(((const unsigned char *) msg) + |
| 1153 | sizeof(struct smb_header) + 5); |
| 1154 | data->req.bytecount += len; |
| 1155 | data->req.offset += len; |
| 1156 | Curl_pgrsSetUploadCounter(data, data->req.bytecount); |
| 1157 | if(data->req.bytecount >= data->req.size) |
| 1158 | next_state = SMB_CLOSE; |
| 1159 | else |
| 1160 | next_state = SMB_UPLOAD; |
| 1161 | break; |
| 1162 | |
| 1163 | case SMB_CLOSE: |
| 1164 | /* We do not care if the close failed, proceed to tree disconnect anyway */ |
| 1165 | next_state = SMB_TREE_DISCONNECT; |
| 1166 | break; |
| 1167 | |
| 1168 | case SMB_TREE_DISCONNECT: |
| 1169 | next_state = SMB_DONE; |
| 1170 | break; |
| 1171 | |
| 1172 | default: |
| 1173 | smb_pop_message(smbc); |
| 1174 | return CURLE_OK; /* ignore */ |
| 1175 | } |
| 1176 | |
| 1177 | smb_pop_message(smbc); |
| 1178 | |
| 1179 | switch(next_state) { |
| 1180 | case SMB_OPEN: |
| 1181 | result = smb_send_open(data, smbc, req); |
| 1182 | break; |
| 1183 | |
| 1184 | case SMB_DOWNLOAD: |
| 1185 | result = smb_send_read(data, smbc, req); |
| 1186 | break; |
| 1187 | |
| 1188 | case SMB_UPLOAD: |
| 1189 | result = smb_send_write(data, smbc, req); |
| 1190 | break; |
Show 1 lines (not cited by the judge)
| 63 | #define failf Curl_failf |