Fix CFITSIO safety issues: nullptr params, error handling, nrow clamping, CI coverage#28
Fix CFITSIO safety issues: nullptr params, error handling, nrow clamping, CI coverage#28
Conversation
Now there are no more recommended fixes (or possible corner case bugs) highlighted.
Bump Rfits package version to 1.14.5 and update release date
There was a problem hiding this comment.
Pull request overview
This PR focuses on making the CFITSIO/Rcpp interface code in Rfits.cpp safer by tightening ownership semantics, reducing manual memory management, and improving integer handling (especially around 32-bit vs 64-bit boundaries).
Changes:
- Hardened FITS file handle ownership via non-copyable
fits_filewith move semantics and safer reassignment behavior. - Reduced heap churn / improved safety in several read paths (e.g., TSTRING column reads, header raw reads) using RAII containers.
- Updated CI configuration and a couple of exported function signatures (
Cfits_read_nrow,Cfits_write_col) to uselong.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 11 comments.
| File | Description |
|---|---|
| src/Rfits.cpp | Safety/RAII improvements, integer range handling, API tweaks, and various read/write path refactors. |
| src/RcppExports.cpp | Regenerated/updated export signatures to match long-typed C++ APIs. |
| R/Rfits_methods_file.R | Minor whitespace cleanup. |
| .github/workflows/main.yml | CI trigger/matrix adjustments and dependency install tweaks. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| fits_invoke(open_image, fptr, filename.get_cstring(), READONLY); | ||
| fits_invoke(movabs_hdu, fptr, ext, &hdutype); | ||
| fits_invoke(get_hdrpos, fptr, &nkeys, &keypos); | ||
| fits_invoke(get_hdrpos, fptr, &nkeys, nullptr); | ||
|
|
There was a problem hiding this comment.
fits_get_hdrpos expects a valid int* for the current header position; passing nullptr risks a null dereference inside CFITSIO. Keep a local keypos variable (even if unused) and pass &keypos (or switch to fits_get_hdrspace with a real nmore variable).
src/Rfits.cpp
Outdated
| fits_invoke(open_image, fptr, filename.get_cstring(), READONLY); | ||
| fits_invoke(movabs_hdu, fptr, ext, &hdutype); | ||
| fits_invoke(get_hdrpos, fptr, &nkeys, &keypos); | ||
| fits_invoke(get_hdrpos, fptr, &nkeys, nullptr); |
There was a problem hiding this comment.
Same issue here: fits_get_hdrpos's position out-parameter is not optional in the CFITSIO API; passing nullptr may crash. Preserve a keypos local and pass &keypos.
src/Rfits.cpp
Outdated
| fits_invoke(open_image, fptr, filename.get_cstring(), READWRITE); | ||
| fits_invoke(movabs_hdu, fptr, ext, &hdutype); | ||
| fits_invoke(get_hdrpos, fptr, &nkeys, &keypos); | ||
| fits_invoke(get_hdrpos, fptr, &nkeys, nullptr); |
There was a problem hiding this comment.
Same fits_get_hdrpos null out-parameter problem: passing nullptr for the position argument is unsafe and can segfault inside CFITSIO. Pass a real int keypos instead.
src/Rfits.cpp
Outdated
| fits_file fptr; | ||
| fits_invoke(open_image, fptr, filename.get_cstring(), READONLY); | ||
| fits_invoke(movabs_hdu, fptr, ext, &hdutype); | ||
| fits_invoke(get_hdrpos, fptr, &nkeys, &keypos); | ||
| fits_invoke(get_hdrpos, fptr, &nkeys, nullptr); |
There was a problem hiding this comment.
Same fits_get_hdrpos usage issue: the third parameter is an output pointer (position) and CFITSIO's declaration does not indicate it can be null. Passing nullptr here can lead to a crash; pass &keypos.
| if (startrow + nrow - 1 > nrow_total) { | ||
| Rcpp::stop("Requested range exceeds number of rows in table"); | ||
| Rcpp::warning("Requested range exceeds number of rows in table"); | ||
| } |
There was a problem hiding this comment.
This switches the out-of-range condition from stop() to warning() but still proceeds to call fits_read_col with the original nrow. That will typically still error in CFITSIO (or return partial/undefined results). Either keep this as a hard error, or clamp nrow to nrow_total - startrow + 1 before reading.
src/Rfits.cpp
Outdated
| fits_file &operator=(fits_file &&other) noexcept { | ||
| if (this != &other) { | ||
| if (m_fptr) { | ||
| int status = 0; | ||
| fits_close_file(m_fptr, &status); | ||
| } | ||
| m_fptr = other.m_fptr; | ||
| other.m_fptr = nullptr; | ||
| } |
There was a problem hiding this comment.
When replacing an owned handle (both in move-assignment and operator=(fitsfile*)), fits_close_file's status is ignored. If the close fails (e.g., write/flush error), this will be silently dropped. Consider checking status here and surfacing it via fits_throw_exception("close_file", status) (safe to throw here; only the destructor should suppress close errors).
There was a problem hiding this comment.
Applied in commit 4abb4ab. Both operator=(fits_file&&) (also removed noexcept) and operator=(fitsfile*) now check the status returned by fits_close_file and call fits_throw_exception("close_file", status) if it fails. The destructor continues to suppress close errors as intended.
| return *this; | ||
| if (m_fptr) { | ||
| int status = 0; | ||
| fits_close_file(m_fptr, &status); |
There was a problem hiding this comment.
operator=(fitsfile*) closes the existing file but ignores the returned status. If close fails, the error is lost and the code proceeds to overwrite m_fptr. Consider checking status and reporting it (throw) before taking ownership of new_fptr (only the destructor should ignore close errors).
| fits_close_file(m_fptr, &status); | |
| fits_close_file(m_fptr, &status); | |
| if (status) { | |
| // Report close error before taking ownership of the new pointer. | |
| fits_throw_exception("close_file", status); | |
| } |
src/Rfits.cpp
Outdated
| [](long v) { return static_cast<int64_t>(v); }); | ||
| if (n > 0) std::memcpy(&(output[0]), &(values64[0]), n * sizeof(int64_t)); | ||
| output.attr("class") = "integer64"; | ||
| return output; |
There was a problem hiding this comment.
In ensure_lossless_32bit_int, the fallback block has inconsistent indentation and multiple statements on the same indentation level as the surrounding loop (e.g., the memcpy, attr, and return lines). Please re-indent and add braces so the control flow is unambiguous and maintainable.
| [](long v) { return static_cast<int64_t>(v); }); | |
| if (n > 0) std::memcpy(&(output[0]), &(values64[0]), n * sizeof(int64_t)); | |
| output.attr("class") = "integer64"; | |
| return output; | |
| [](long v) { return static_cast<int64_t>(v); }); | |
| if (n > 0) { | |
| std::memcpy(&(output[0]), &(values64[0]), n * sizeof(int64_t)); | |
| } | |
| output.attr("class") = "integer64"; | |
| return output; |
.github/workflows/main.yml
Outdated
| branches: [main, master] | ||
| pull_request: | ||
| branches: [main, master] | ||
|
|
There was a problem hiding this comment.
Adding push.branches: [main, master] will stop CI from running on pushes to any other branches (feature branches, release branches, etc.). If that’s not intended, remove the branch filter or broaden it (e.g., include development/release patterns).
| branches: [main, master] | |
| pull_request: | |
| branches: [main, master] | |
| pull_request: | |
| branches: [main, master] |
| - {os: windows-latest, r: 'release'} | ||
| - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} | ||
| - {os: ubuntu-latest, r: 'release'} | ||
| - {os: ubuntu-latest, r: 'oldrel-1'} |
There was a problem hiding this comment.
The matrix no longer runs ubuntu-latest with r: devel. This reduces forward-compatibility coverage and can delay detection of upcoming R changes. If CI time isn’t the driver, consider keeping the devel job or gating it (e.g., scheduled/weekly) instead of removing it entirely.
| - {os: ubuntu-latest, r: 'oldrel-1'} | |
| - {os: ubuntu-latest, r: 'oldrel-1'} | |
| - {os: ubuntu-latest, r: 'devel'} |
…ssignment operators Agent-Logs-Url: https://github.com/asgr/Rfits/sessions/cd685580-08b6-4b59-8870-dfb15131a891 Co-authored-by: asgr <5617132+asgr@users.noreply.github.com>
…name errors, indentation, CI fixes Agent-Logs-Url: https://github.com/asgr/Rfits/sessions/90408e21-b322-4cb6-aad4-c67c5025252c Co-authored-by: asgr <5617132+asgr@users.noreply.github.com>
Addresses several safety and correctness issues flagged in code review on the CFITSIO/Rcpp interface.
src/Rfits.cppfits_get_hdrposnullptr — CFITSIO'spositionout-parameter is not optional; passingnullptrrisks a segfault. Addedint keyposat all 4 call sites and pass&keypos.fits_close_filestatus ignored — Move-assignment andoperator=(fitsfile*)silently dropped close errors. Both now check status and throw viafits_throw_exception. Move-assignment is no longernoexcept.fits_get_colnameunhandled errors — Loop was treating all non-COL_NOT_FOUNDstatuses as success, risking UB from uninitializedcolname. Now pushes only onstatus == 0; throws on any other real error code.nrow = nrow_total - startrow + 1.ensure_lossless_32bit_intindentation — Fallbackmemcpy/attr/returnblock was missing braces and misaligned; re-indented for unambiguous control flow..github/workflows/main.ymlbranchesfilter frompushtrigger so CI runs on all branches, not justmain/master.{os: ubuntu-latest, r: 'devel'}to the matrix for forward-compatibility coverage.