|
| 1 | +/* Regression test for memory leaks in libretrodb_open and |
| 2 | + * libretrodb_close (libretro-db/libretrodb.c). |
| 3 | + * |
| 4 | + * Two pre-this-patch leaks: |
| 5 | + * |
| 6 | + * 1. libretrodb_open's error: label freed neither db->path |
| 7 | + * (strdup'd at line 209) nor the intfstream_t struct (alloc'd |
| 8 | + * by intfstream_open_file at line 201). Triggered by any |
| 9 | + * malformed .rdb -- bad magic, bad metadata_offset, truncated |
| 10 | + * header. Reachable across a directory scan: every malformed |
| 11 | + * .rdb the user has on disk leaks 48 + (path-length) bytes. |
| 12 | + * |
| 13 | + * 2. libretrodb_close (success-path counterpart) called |
| 14 | + * intfstream_close on db->fd but never freed the struct |
| 15 | + * itself. Same 48-byte leak per opened-and-closed database |
| 16 | + * file. |
| 17 | + * |
| 18 | + * 3. libretrodb_cursor_close had the same close-without-free |
| 19 | + * pattern on cursor->fd. |
| 20 | + * |
| 21 | + * libretro-common convention is for callers to free the |
| 22 | + * intfstream_t after intfstream_close (see core_info.c, |
| 23 | + * core_backup.c, cdfs.c, rpng_encode.c trailing free() calls). |
| 24 | + * libretrodb's own teardown paths just didn't follow it. |
| 25 | + * |
| 26 | + * The test writes a few minimal .rdb files into /tmp, runs them |
| 27 | + * through libretrodb_open + libretrodb_close cycles, and exits |
| 28 | + * cleanly. Under -fsanitize=address (the SANITIZER=address |
| 29 | + * Makefile build) any reintroduced leak is flagged at exit. |
| 30 | + * |
| 31 | + * Run with: make SANITIZER=address && ./libretrodb_leak_test |
| 32 | + */ |
| 33 | + |
| 34 | +#include <stdio.h> |
| 35 | +#include <stdlib.h> |
| 36 | +#include <string.h> |
| 37 | +#include <stdint.h> |
| 38 | +#include <unistd.h> |
| 39 | + |
| 40 | +#include "../../libretrodb.h" |
| 41 | + |
| 42 | +#ifndef RETRO_VFS_FILE_ACCESS_READ |
| 43 | +#define RETRO_VFS_FILE_ACCESS_READ (1 << 0) |
| 44 | +#endif |
| 45 | + |
| 46 | +static int failures = 0; |
| 47 | + |
| 48 | +static void put64be(uint8_t *p, uint64_t v) |
| 49 | +{ |
| 50 | + int i; |
| 51 | + for (i = 0; i < 8; i++) |
| 52 | + p[i] = (uint8_t)(v >> (56 - 8 * i)); |
| 53 | +} |
| 54 | + |
| 55 | +/* Build a .rdb header at *p. Returns bytes written. |
| 56 | + * Magic is "RARCHDB\0" (8) + 8-byte big-endian metadata_offset. */ |
| 57 | +static size_t make_header(uint8_t *p, uint64_t metadata_offset) |
| 58 | +{ |
| 59 | + memcpy(p, "RARCHDB", 7); |
| 60 | + p[7] = '\0'; |
| 61 | + put64be(p + 8, metadata_offset); |
| 62 | + return 16; |
| 63 | +} |
| 64 | + |
| 65 | +/* Write `len` bytes from `buf` to a temp file at `path`. Returns |
| 66 | + * 0 on success, -1 on failure. */ |
| 67 | +static int write_temp(const char *path, const uint8_t *buf, size_t len) |
| 68 | +{ |
| 69 | + FILE *f = fopen(path, "wb"); |
| 70 | + if (!f) |
| 71 | + return -1; |
| 72 | + if (fwrite(buf, 1, len, f) != len) |
| 73 | + { |
| 74 | + fclose(f); |
| 75 | + return -1; |
| 76 | + } |
| 77 | + fclose(f); |
| 78 | + return 0; |
| 79 | +} |
| 80 | + |
| 81 | +static void test_open_close_valid(void) |
| 82 | +{ |
| 83 | + /* Minimal valid .rdb: header pointing at a fixmap-1 with |
| 84 | + * {"count": 0} immediately after. */ |
| 85 | + uint8_t buf[64]; |
| 86 | + const char *path = "/tmp/libretrodb_leak_test_valid.rdb"; |
| 87 | + libretrodb_t *db; |
| 88 | + int rv; |
| 89 | + size_t len = make_header(buf, 16); |
| 90 | + buf[len++] = 0x81; /* fixmap-1 */ |
| 91 | + buf[len++] = 0xa5; memcpy(buf + len, "count", 5); len += 5; |
| 92 | + buf[len++] = 0x00; /* fixint 0 */ |
| 93 | + if (write_temp(path, buf, len) < 0) |
| 94 | + { |
| 95 | + printf("[ERROR] write_temp failed\n"); failures++; return; |
| 96 | + } |
| 97 | + |
| 98 | + db = libretrodb_new(); |
| 99 | + if (!db) { printf("[ERROR] libretrodb_new\n"); failures++; goto cleanup; } |
| 100 | + rv = libretrodb_open(path, db, false); |
| 101 | + if (rv != 0) |
| 102 | + { |
| 103 | + printf("[ERROR] valid .rdb open failed (rv=%d)\n", rv); |
| 104 | + failures++; |
| 105 | + } |
| 106 | + else |
| 107 | + { |
| 108 | + libretrodb_close(db); |
| 109 | + printf("[SUCCESS] valid .rdb open + close cycle clean\n"); |
| 110 | + } |
| 111 | + libretrodb_free(db); |
| 112 | +cleanup: |
| 113 | + unlink(path); |
| 114 | +} |
| 115 | + |
| 116 | +static void test_open_bad_magic(void) |
| 117 | +{ |
| 118 | + /* Bad magic: header starts with 'XXXX...' instead of 'RARCHDB'. |
| 119 | + * Pre-fix this leaked 48 bytes (intfstream) + 11 bytes |
| 120 | + * (strdup'd /tmp/...) per call. */ |
| 121 | + uint8_t buf[16]; |
| 122 | + const char *path = "/tmp/libretrodb_leak_test_badmagic.rdb"; |
| 123 | + libretrodb_t *db; |
| 124 | + int rv; |
| 125 | + memset(buf, 'X', 8); |
| 126 | + put64be(buf + 8, 16); |
| 127 | + if (write_temp(path, buf, sizeof(buf)) < 0) |
| 128 | + { |
| 129 | + printf("[ERROR] write_temp failed\n"); failures++; return; |
| 130 | + } |
| 131 | + |
| 132 | + db = libretrodb_new(); |
| 133 | + if (!db) { printf("[ERROR] libretrodb_new\n"); failures++; goto cleanup; } |
| 134 | + rv = libretrodb_open(path, db, false); |
| 135 | + if (rv == 0) |
| 136 | + { |
| 137 | + printf("[ERROR] bad-magic .rdb accepted\n"); |
| 138 | + failures++; |
| 139 | + libretrodb_close(db); |
| 140 | + } |
| 141 | + else |
| 142 | + printf("[SUCCESS] bad-magic .rdb rejected without leak\n"); |
| 143 | + libretrodb_free(db); |
| 144 | +cleanup: |
| 145 | + unlink(path); |
| 146 | +} |
| 147 | + |
| 148 | +static void test_open_bad_metadata_offset(void) |
| 149 | +{ |
| 150 | + /* metadata_offset past EOF: 16-byte file, offset = 100. */ |
| 151 | + uint8_t buf[16]; |
| 152 | + const char *path = "/tmp/libretrodb_leak_test_badoff.rdb"; |
| 153 | + libretrodb_t *db; |
| 154 | + int rv; |
| 155 | + make_header(buf, 100); |
| 156 | + if (write_temp(path, buf, sizeof(buf)) < 0) |
| 157 | + { |
| 158 | + printf("[ERROR] write_temp failed\n"); failures++; return; |
| 159 | + } |
| 160 | + |
| 161 | + db = libretrodb_new(); |
| 162 | + if (!db) { printf("[ERROR] libretrodb_new\n"); failures++; goto cleanup; } |
| 163 | + rv = libretrodb_open(path, db, false); |
| 164 | + if (rv == 0) |
| 165 | + { |
| 166 | + printf("[ERROR] bad-offset .rdb accepted\n"); |
| 167 | + failures++; |
| 168 | + libretrodb_close(db); |
| 169 | + } |
| 170 | + else |
| 171 | + printf("[SUCCESS] bad-offset .rdb rejected without leak\n"); |
| 172 | + libretrodb_free(db); |
| 173 | +cleanup: |
| 174 | + unlink(path); |
| 175 | +} |
| 176 | + |
| 177 | +static void test_repeat_open_close(void) |
| 178 | +{ |
| 179 | + /* Open and close the valid .rdb several times on the same |
| 180 | + * libretrodb_t. libretrodb_open at line ~206-209 frees |
| 181 | + * db->path if it's already set then strdup's the new one -- |
| 182 | + * which is fine, but the intfstream_t leak compounded across |
| 183 | + * each call pre-fix. */ |
| 184 | + uint8_t buf[64]; |
| 185 | + const char *path = "/tmp/libretrodb_leak_test_repeat.rdb"; |
| 186 | + libretrodb_t *db; |
| 187 | + int i; |
| 188 | + size_t len = make_header(buf, 16); |
| 189 | + buf[len++] = 0x81; |
| 190 | + buf[len++] = 0xa5; memcpy(buf + len, "count", 5); len += 5; |
| 191 | + buf[len++] = 0x00; |
| 192 | + if (write_temp(path, buf, len) < 0) |
| 193 | + { |
| 194 | + printf("[ERROR] write_temp failed\n"); failures++; return; |
| 195 | + } |
| 196 | + |
| 197 | + db = libretrodb_new(); |
| 198 | + if (!db) { printf("[ERROR] libretrodb_new\n"); failures++; goto cleanup; } |
| 199 | + for (i = 0; i < 5; i++) |
| 200 | + { |
| 201 | + int rv = libretrodb_open(path, db, false); |
| 202 | + if (rv != 0) |
| 203 | + { |
| 204 | + printf("[ERROR] repeat open #%d failed\n", i); |
| 205 | + failures++; |
| 206 | + break; |
| 207 | + } |
| 208 | + libretrodb_close(db); |
| 209 | + } |
| 210 | + libretrodb_free(db); |
| 211 | + if (i == 5) |
| 212 | + printf("[SUCCESS] 5x open+close cycle clean\n"); |
| 213 | +cleanup: |
| 214 | + unlink(path); |
| 215 | +} |
| 216 | + |
| 217 | +int main(void) |
| 218 | +{ |
| 219 | + test_open_close_valid(); |
| 220 | + test_open_bad_magic(); |
| 221 | + test_open_bad_metadata_offset(); |
| 222 | + test_repeat_open_close(); |
| 223 | + |
| 224 | + if (failures) |
| 225 | + { |
| 226 | + printf("\n%d libretrodb leak test(s) failed\n", failures); |
| 227 | + return 1; |
| 228 | + } |
| 229 | + printf("\nAll libretrodb leak regression tests passed.\n"); |
| 230 | + return 0; |
| 231 | +} |
0 commit comments