Skip to content

Fix CFITSIO safety issues: nullptr params, error handling, nrow clamping, CI coverage#28

Merged
asgr merged 22 commits intomasterfrom
test_claude
Apr 1, 2026
Merged

Fix CFITSIO safety issues: nullptr params, error handling, nrow clamping, CI coverage#28
asgr merged 22 commits intomasterfrom
test_claude

Conversation

@asgr
Copy link
Copy Markdown
Owner

@asgr asgr commented Apr 1, 2026

Addresses several safety and correctness issues flagged in code review on the CFITSIO/Rcpp interface.

src/Rfits.cpp

  • fits_get_hdrpos nullptr — CFITSIO's position out-parameter is not optional; passing nullptr risks a segfault. Added int keypos at all 4 call sites and pass &keypos.
  • fits_close_file status ignored — Move-assignment and operator=(fitsfile*) silently dropped close errors. Both now check status and throw via fits_throw_exception. Move-assignment is no longer noexcept.
  • fits_get_colname unhandled errors — Loop was treating all non-COL_NOT_FOUND statuses as success, risking UB from uninitialized colname. Now pushes only on status == 0; throws on any other real error code.
  • nrow out-of-range — Changed from warning-and-proceed (which would still error/corrupt in CFITSIO) to clamping nrow = nrow_total - startrow + 1.
  • ensure_lossless_32bit_int indentation — Fallback memcpy/attr/return block was missing braces and misaligned; re-indented for unambiguous control flow.

.github/workflows/main.yml

  • Removed branches filter from push trigger so CI runs on all branches, not just main/master.
  • Restored {os: ubuntu-latest, r: 'devel'} to the matrix for forward-compatibility coverage.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_file with 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 use long.

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.

Comment on lines 688 to 691
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);

Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
src/Rfits.cpp Outdated
Comment on lines +706 to +708
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);
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
src/Rfits.cpp Outdated
Comment on lines +746 to +748
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);
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
src/Rfits.cpp Outdated
Comment on lines +979 to +982
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);
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 192 to 194
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");
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
src/Rfits.cpp Outdated
Comment on lines +39 to +47
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;
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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);
}

Copilot uses AI. Check for mistakes.
src/Rfits.cpp Outdated
Comment on lines +139 to +142
[](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;
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
[](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;

Copilot uses AI. Check for mistakes.
Comment on lines 5 to 8
branches: [main, master]
pull_request:
branches: [main, master]

Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
branches: [main, master]
pull_request:
branches: [main, master]
pull_request:
branches: [main, master]

Copilot uses AI. Check for mistakes.
- {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'}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
- {os: ubuntu-latest, r: 'oldrel-1'}
- {os: ubuntu-latest, r: 'oldrel-1'}
- {os: ubuntu-latest, r: 'devel'}

Copilot uses AI. Check for mistakes.
…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>
Copilot AI changed the title Test claude Fix CFITSIO safety issues: nullptr params, error handling, nrow clamping, CI coverage Apr 1, 2026
Copilot finished work on behalf of asgr April 1, 2026 05:53
@asgr asgr merged commit 8f8aac7 into master Apr 1, 2026
8 checks passed
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.

3 participants