You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
chd_open_core_file_callbacks() closes the caller's handle on some failure paths and not on others, and the caller cannot tell which happened.
Once it has allocated its chd_file, a failure goes through cleanup: → chd_close() → core_fclose(), which calls the caller's fclose callback. The three earliest failures — callbacks == NULL, an invalid parent, and malloc of the chd_file itself — return before that and leave the handle open. Both kinds can report CHDERR_OUT_OF_MEMORY.
Every bridge in contrib/ assumes the handle is still theirs on failure and closes it:
contrib/esp32p4/chd/chd_esp_vfs.c — its callback does fclose(f), and on failure the bridge does fclose(f) again. A second fclose() on the same FILE* is undefined behaviour and can corrupt the heap. Reachable from any image that fails after allocation: bad header, corrupt map, allocation failure while opening.
contrib/tangcore-bl616/chd/chd_fatfs.c — same shape, but FatFs's f_close() rejects an object that is already closed, so it was harmless. FatFs bridges: build a cluster link map on open #194 makes that callback explicitly idempotent because it now frees a cluster link map there.
Two ways to close it:
Make the contract simple: on failure the handle is always the caller's. Clear newchd->file.callbacks before chd_close() on the cleanup: path. That matches what every bridge already assumes, but it changes behaviour for any caller relying on libchdr closing on failure — worth checking chd_open()'s own stdio path, and DuckStation's CHD_OPEN_TRANSFER_FILE, which exists precisely because of this ownership question.
Keep the behaviour, document it, and make each bridge's close idempotent — which a plain FILE* cannot be without a wrapper.
The first is the better contract. Either way, chd_esp_vfs.c needs fixing.
chd_open_core_file_callbacks()closes the caller's handle on some failure paths and not on others, and the caller cannot tell which happened.Once it has allocated its
chd_file, a failure goes throughcleanup:→chd_close()→core_fclose(), which calls the caller'sfclosecallback. The three earliest failures —callbacks == NULL, an invalid parent, andmallocof thechd_fileitself — return before that and leave the handle open. Both kinds can reportCHDERR_OUT_OF_MEMORY.Every bridge in
contrib/assumes the handle is still theirs on failure and closes it:contrib/esp32p4/chd/chd_esp_vfs.c— its callback doesfclose(f), and on failure the bridge doesfclose(f)again. A secondfclose()on the sameFILE*is undefined behaviour and can corrupt the heap. Reachable from any image that fails after allocation: bad header, corrupt map, allocation failure while opening.contrib/tangcore-bl616/chd/chd_fatfs.c— same shape, but FatFs'sf_close()rejects an object that is already closed, so it was harmless. FatFs bridges: build a cluster link map on open #194 makes that callback explicitly idempotent because it now frees a cluster link map there.Two ways to close it:
newchd->file.callbacksbeforechd_close()on thecleanup:path. That matches what every bridge already assumes, but it changes behaviour for any caller relying on libchdr closing on failure — worth checkingchd_open()'s own stdio path, and DuckStation'sCHD_OPEN_TRANSFER_FILE, which exists precisely because of this ownership question.FILE*cannot be without a wrapper.The first is the better contract. Either way,
chd_esp_vfs.cneeds fixing.