Conversation
There was a problem hiding this comment.
🟡 Changes recommended
The current OpenMP decompression implementation still has a correctness/termination hazard if the runtime provides fewer threads than the stream’s chunk count, and exceptions inside OpenMP regions can still terminate before callers can handle them.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This PR hardens SZ3’s CLI and HDF5 filter integration so that invalid-but-non-malicious inputs fail gracefully (with useful error reporting) rather than aborting the caller’s process, and it scopes OpenMP usage so it doesn’t mutate the host application’s thread settings.
Changes:
- Wrap the
sz3CLI entrypoint to convert thrown exceptions into a message + nonzero exit code. - Add HDF5 error-class registration and catch exceptions inside HDF5 C callbacks, reporting failures via
H5Epush. - Rework OpenMP usage to avoid
omp_set_num_threads, fix index overflow risk withsize_t, respect stream framing, and add CI coverage for OpenMP streams.
File summaries
| File | Description |
|---|---|
| tools/sz3/sz3.cpp | Wraps CLI execution in try/catch to prevent aborts on invalid configs. |
| tools/H5Z-SZ3/src/H5Z_SZ3.cpp | Registers HDF5 error class and catches exceptions in HDF5 filter callbacks to avoid unwinding through C frames. |
| include/SZ3/api/impl/SZImplOMP.hpp | Uses num_threads and size_t for safer OpenMP chunking; improves non-OpenMP behavior for OpenMP-framed streams. |
| include/SZ3/api/impl/SZImpl.hpp | Stops overriding conf.openmp in non-OpenMP builds so stream framing is respected. |
| .github/workflows/cmake.yml | Adds CI step to exercise OpenMP encode/decode paths and cross-thread-count readback. |
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+170
to
+174
| #pragma omp parallel num_threads(nThreads) | ||
| { | ||
| int tid = omp_get_thread_num(); | ||
| auto dims_t = conf.dims; | ||
| int lo = tid * conf.dims[0] / nThreads; | ||
| int hi = (tid + 1) * conf.dims[0] / nThreads; | ||
| size_t lo = static_cast<size_t>(tid) * conf.dims[0] / nThreads; |
Comment on lines
+71
to
+77
| # The OpenMP path splits the data along the slowest dimension and compresses each piece on | ||
| # its own thread, so it has its own stream framing and its own decoder. Nothing else in CI | ||
| # enters it. | ||
| printf '[GlobalSettings]\nOpenMP = YES\n' > omp.config | ||
| ./tools/sz3/sz3 -f -i ../input.dat -3 ${{ env.DIMS }} -M ${{ env.MODE }} ${{ env.TOL }} -a \ | ||
| -c omp.config -o omp.dat -z omp.sz3 \ | ||
| | tee omp.log |
Comment on lines
+13
to
+17
| static void register_errclass_once() { | ||
| if (H5Z_SZ_ERRCLASS < 0) { | ||
| H5Z_SZ_ERRCLASS = H5Eregister_class("H5Z-SZ3", "SZ3", SZ3_VER); | ||
| } | ||
| } |
…its own call An exception out of the HDF5 filter aborts the application. H5Z_filter_sz3 and H5Z_sz3_set_local are called from C and threw back through HDF5's frames, so reading an ordinary 2023-era SZ3 file, a 4D or 5D dataset, or one whose cd_values name an unknown datatype ended the process instead of failing the read. The unknown-datatype branch called std::exit outright. Both callbacks catch now; zero and a negative return are how a filter reports failure. The CLI had the same hole: a 4D dataset with either MD algorithm, or InterpolationDirection = 3 on 1D data, gave a script SIGABRT and no message. Neither barrier reached the OpenMP path, because an exception leaving an OpenMP region is a terminate before any caller's catch runs. Each chunk holds its own now and the first is rethrown after the region. The regions were also indexed by omp_get_thread_num() while assuming the runtime would supply exactly the requested threads. Under OMP_THREAD_LIMIT it supplies fewer, and the chunks belonging to the absent thread ids were simply never processed -- uninitialised bytes into the stream on the way out, an unwritten span of the output on the way back. Both loops run over chunk indices. H5Z_SZ_PUSH_AND_GOTO pushed with H5Z_SZ_ERRCLASS, which was declared, initialised to -1 and never registered, so every push failed with "can't locate ID" and dropped its message. blosc and H5Z-ZFP both push with the built-in H5E_ERR_CLS and register nothing; this does the same and the variable goes away. The macro is variadic like theirs, because H5Epush takes a format string and e.what() must not be one. The OpenMP split index was an int, so tid * conf.dims[0] overflowed above INT_MAX: at eight threads 2.4e9 elements was fine and 2.5e9 was a SIGSEGV. That is 8.6 GB of float32. omp_set_num_threads changes the thread count for the rest of the calling process, and the filter runs inside one -- reading a chunk written with eight threads dropped a 32-thread application to eight. The parallel regions carry num_threads instead, and the two printf calls per operation are gone. SZ_decompress_impl forced conf.openmp to false without OpenMP and handed per-thread chunks to the serial decoder, which failed far from the cause. The flag came out of the stream and says how the data was framed, so it is respected, and a build without OpenMP says what is missing. CI enters the OpenMP path for the first time: a round trip, and a stream written with one thread count read back with another. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Five things a GROMACS build would meet, found by hunting for them on real trajectories. None needs
a malicious input; three end the calling process.
An exception out of the HDF5 filter aborts the application.
H5Z_filter_sz3andH5Z_sz3_set_localare called from C and threw back through HDF5's frames. Reading an ordinary2023-era SZ3 file, or a 4D or 5D dataset, or one whose
cd_valuesname an unknown datatype, endedthe process instead of failing the read. The unknown-datatype branch called
std::exitoutright.Both callbacks catch now; zero and a negative return are how a filter reports failure.
H5Z_SZ_PUSH_AND_GOTOnever reported anything. It pushed withH5Z_SZ_ERRCLASS, which wasdeclared, initialised to
-1and never registered, so every push failed with "can't locate ID" anddropped its message — including the call sites that were already there. blosc and H5Z-ZFP both push
with the built-in
H5E_ERR_CLSand register nothing; this now does the same and the variable isgone. The macro is variadic like theirs, because
H5Epushtakes a format string ande.what()must not be one.
The CLI had the same hole. A sweep of 2520 combinations of algorithm, error-bound mode,
dimensionality and
[AlgoSettings]on valid data found two refusals — a 4D dataset with either MDalgorithm, and
InterpolationDirection = 3on 1D — and no wrong answers anywhere. Both aborted;maincatches now.Neither barrier reached the OpenMP path. An exception leaving an OpenMP region is a terminate
before any caller's catch runs. Each chunk holds its own now and the first is rethrown after the
region — a corrupted OMP stream comes back as
rc=1 sz3: SZ3 lossless: stream does not decompress to the size it declaresinstead of killing the process.The regions were indexed by
omp_get_thread_num()while assuming the runtime supplies exactlythe requested threads. Under
OMP_THREAD_LIMITit supplies fewer, and the chunks belonging to theabsent thread ids were never processed — uninitialised bytes into the stream on the way out, an
unwritten span of the output on the way back. Both loops run over chunk indices now, and a stream
written with 8 threads reads back identically at
OMP_THREAD_LIMIT8, 2 and 1.The OpenMP split index was an
int.tid * conf.dims[0]overflows aboveINT_MAX:8.6 GB of float32, inside the range of a trajectory.
size_tnow, in both splits.SZ3 changed the caller's thread count and talked on its stdout.
omp_set_num_threadsmutatesthe setting for the rest of the process, and the filter runs inside one: reading a chunk written
with eight threads dropped a 32-thread application to eight. The parallel regions carry
num_threadsinstead, and the two per-operationprintfcalls are gone.Decompression ignored the stream's own framing.
SZ_decompress_implforcedconf.openmptofalse when built without OpenMP and handed per-thread chunks to the serial decoder, which failed
far from the cause. The flag is respected; a build without OpenMP says what is missing.
CI enters the OpenMP path for the first time: a round trip, and a stream written with one
thread count read back with another.
Verified on Linux, gcc 13.3, Release with
BUILD_TESTING=ONandBUILD_H5Z_FILTER=ON: 0 warnings,23/23 ctest, every reproducer above changed as described, and the 2520-combination sweep produces
no wrong answers and no aborts.
🤖 Generated with Claude Code