Skip to content

Close the file and parent on every failed open - #195

Merged
rtissera merged 1 commit into
masterfrom
fix-open-failure-ownership
Sep 10, 2026
Merged

Close the file and parent on every failed open#195
rtissera merged 1 commit into
masterfrom
fix-open-failure-ownership

Conversation

@rtissera

Copy link
Copy Markdown
Owner

Fixes #193.

chd_open_core_file_callbacks() closes the caller's file and parent on a failed open, through chd_close() on the half-built handle. Two exits before that handle exists skipped it: an invalid parent, and a failed malloc of the chd_file. Depending on where the open failed, the same call either consumed its arguments or leaked them.

This PR closes them on those two exits too, and documents the contract in chd.h: once passed, the file and the parent belong to libchdr, whether the open succeeds or not.

Why not the other way round

#193 proposed the opposite: leave the file to the caller on failure. That would break existing callers:

  • One consumer wraps its file in an object that its fclose callback deletes. It relies on libchdr calling that on failure, and it retries a child that returned CHDERR_REQUIRES_PARENT with a new wrapper.
  • No consumer I checked closes the parent after a failed open. They depend on libchdr having done it.

chd_open_file() is unaffected either way, since its fclose callback does nothing.

What changes for callers

  • The behaviour changes on two rare paths, from leaking to closing. There is no ABI change and no SOVERSION bump.
  • A caller that closes the file again after a failure is now wrong on those two paths. It was already closing it twice on every other failure path.
  • contrib/esp32p4/chd/chd_esp_vfs.c was such a caller: it did fclose(f) after a failed open, which is a double fclose() and undefined behaviour. It and the FatFs bridge now leave the close to libchdr.

Test

tests/open_ownership.c builds one-hunk CHDv5 images in memory, so it runs without the generated corpus. It counts fclose calls for these cases:

  • success (closed once by chd_close())
  • bad header
  • truncated file
  • a parent that is not a chd_file
  • CHDERR_REQUIRES_PARENT followed by a retry with the parent
  • a mismatched parent (both files closed once)
  • NULL callbacks

For chd_open(), which opens its own FILE, the test checks on POSIX that the file descriptor was released. LeakSanitizer cannot see this, because glibc keeps every open FILE reachable.

  • Without the fix, the test fails on the invalid-parent exit, through both the counting callback and the descriptor check.
  • With it, the whole test suite passes under ASan+UBSan in 7 build configurations: default, LOWRAM_TARGET, system zlib, no subcode/raw sectors, no block CRC, micro-flac, and LOWRAM_TARGET with micro-flac.
  • The failed-malloc exit is not covered by a test.

Follow-ups

🤖 Generated with Claude Code

chd_open_core_file_callbacks() closes the caller's file and parent when an
open fails: the cleanup path runs chd_close() on the half-built handle. The
two exits before that handle exists - an invalid parent, and a failed
malloc of the chd_file - returned without closing anything, so the same
call either consumed its arguments or leaked them depending on where it
failed.

Close them on those two exits as well, and document the contract in chd.h.
Taking the other side - leaving the file to the caller on failure - would
break existing callers: one wraps the file in an object its fclose callback
deletes and relies on libchdr to do so on failure, and none of them close
the parent after a failed open. chd_open_file() is unaffected, its fclose
callback does nothing. The only callers this can hurt are ones that close
the file again after a failure, and those already close it twice on every
other failure.

The ESP-IDF bridge in contrib/ was one: it fclose()d the FILE after a
failed open, a double fclose() on any image that failed past the
allocation. It and the FatFs bridge now leave the close to libchdr.

The new test builds its images in memory, so it runs without the corpus.
Without this change it fails on the invalid-parent exit, both through a
counting fclose callback and, for chd_open(), through the file descriptor
the failed open kept.

Fixes #193

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01846EhHuAFk5qvxwEA5Gq6y
@rtissera
rtissera merged commit 8e7b8bd into master Sep 10, 2026
38 checks passed
rtissera added a commit that referenced this pull request Sep 10, 2026
Without one, every backward f_lseek walks the FAT chain again from the file's
first cluster - and every COMPRESSION_SELF hunk is a backward seek, to an
earlier hunk the image references instead of storing twice. The cost grows with
how far into the file the reader is, so a short test near the start never shows
it. On an ESP32-P4 a 29%-self-referenced image did not finish in 47 minutes
until the benchmark built a map (070341a). The BL616 bridge an integration
copies never had that fix, and its FatFs is built with FF_USE_FASTSEEK on.

chd_fatfs_open() now builds the map right after f_open, before chd_open, so the
header and map reads benefit too. The table is two words per fragment plus one;
it starts at 16 words, grows to exactly what FatFs reports it needs, and past
CHD_FATFS_CLMT_MAX runs without a map rather than failing - slower backward
seeks, still correct. Compiled out entirely when FF_USE_FASTSEEK is 0.

The close callback frees the table. libchdr calls it on a failed open too
(#195), so the bridge's error path has nothing left to close.

The RP2350 benchmark builds the same map, behind BENCH_LINKMAP so it can be
measured on and off. Not measured yet on either board.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01846EhHuAFk5qvxwEA5Gq6y
@rtissera
rtissera deleted the fix-open-failure-ownership branch September 10, 2026 23:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

chd_open_core_file_callbacks: whether the handle is closed on failure depends on where it failed

1 participant