| /* Standalone reproduction of the exact allocation-size vs. write-loop-bound mismatch in | |
| * H5B2__cache_leaf_deserialize() (src/H5B2cache.c:993-1076), isolated from the rest of HDF5's | |
| * build system and compiled with AddressSanitizer to get a real, tool-confirmed heap-buffer-overflow | |
| * report rather than just reasoning about it. | |
| * | |
| * Real formulas copied verbatim from the audited source: | |
| * H5B2_LEAF_PREFIX_SIZE = H5B2_METADATA_PREFIX_SIZE = 4 (magic) + 1 (version) + 1 (type) + 4 (checksum) = 10 | |
| * (H5B2pkg.h:55-60,91-96) | |
| * H5B2_NUM_LEAF_REC(n, r) = (n - H5B2_LEAF_PREFIX_SIZE) / r (H5B2hdr.c:44) | |
| * max_nrec computed once at header-init time from node_size/rrec_size (H5B2hdr.c:140-147), | |
| * used to size the H5FL_FAC_MALLOC-backed native-record buffer to exactly | |
| * (nrec_size * max_nrec) bytes (H5B2hdr.c:147). | |
| * | |
| * H5B2__cache_leaf_deserialize (H5B2cache.c:1042,1046-1054): | |
| * leaf->nrec = udata->nrec; // attacker-controlled, read from file, | |
| * // UP TO UINT16 (65535) per the campaign's | |
| * // own FASE 3 trace (UINT64DECODE_VAR / | |
| * // hdr->max_nrec_size), NEVER validated | |
| * // against max_nrec before this point | |
| * for (u = 0; u < leaf->nrec; u++) { | |
| * (decode)(image, native, ctx); // writes nrec_size bytes to `native` | |
| * native += nrec_size; | |
| * } | |
| */ | |
| int main(void) { | |
| /* Attacker-chosen (but entirely ordinary) B-tree v2 header parameters, e.g. as used for a | |
| * chunk index or dense-attribute index -- both plausible, valid HDF5 configurations. */ | |
| unsigned node_size = 512; /* bytes, a common/realistic node size */ | |
| unsigned rrec_size = 8; /* bytes per record, realistic for a chunk-index record */ | |
| unsigned max_nrec = H5B2_NUM_LEAF_REC(node_size, rrec_size); /* real formula from H5B2hdr.c:140 */ | |
| printf("Computed max_nrec (real formula from H5B2hdr.c) = %u\n", max_nrec); | |
| size_t native_buf_size = (size_t)rrec_size * (size_t)max_nrec; /* real sizing from H5B2hdr.c:147 */ | |
| printf("Native-record buffer allocated (H5FL_FAC_MALLOC-equivalent) = %zu bytes\n", native_buf_size); | |
| /* Attacker-controlled node_nrec read from the malicious leaf node on disk | |
| * (H5B2cache.c:672-673, UINT64DECODE_VAR bounded only by hdr->max_nrec_size, up to UINT16_MAX | |
| * for a 2-byte-width tree). NEVER validated against max_nrec before being assigned to | |
| * leaf->nrec (H5B2cache.c:1042) and used as the loop bound (H5B2cache.c:1046). */ | |
| unsigned malicious_node_nrec = 65535; | |
| printf("Attacker-declared node_nrec (from the malicious leaf node) = %u\n", malicious_node_nrec); | |
| printf("(no validation exists against max_nrec anywhere on this path -- confirmed in FASE 3)\n\n"); | |
| unsigned char *native = malloc(native_buf_size); | |
| if (!native) { fprintf(stderr, "malloc failed\n"); return 1; } | |
| printf("=== Replicating H5B2__cache_leaf_deserialize's write loop verbatim ===\n"); | |
| printf("for (u = 0; u < leaf->nrec /* = %u */; u++) { decode into native; native += rrec_size; }\n\n", | |
| malicious_node_nrec); | |
| unsigned char *p = native; | |
| for (unsigned u = 0; u < malicious_node_nrec; u++) { | |
| /* Exact analog of (hdr->cls->decode)(image, native, ctx) writing rrec_size bytes */ | |
| memset(p, 0x41, rrec_size); | |
| p += rrec_size; | |
| } | |
| printf("Loop completed writing %u records x %u bytes = %zu bytes into a %zu-byte buffer.\n", | |
| malicious_node_nrec, rrec_size, (size_t)malicious_node_nrec * rrec_size, native_buf_size); | |
| printf("(If you see this line, ASan should already have reported heap-buffer-overflow above,\n" | |
| " since the overflow happens partway through the loop, long before it completes.)\n"); | |
| free(native); | |
| return 0; | |
| } | |