Skip to content

Stop a bad input ending the caller's process, and keep OpenMP inside its own call - #149

Open
ayzk wants to merge 1 commit into
masterfrom
md-hardening
Open

ayzk wants to merge 1 commit into
masterfrom
md-hardening

Conversation

@ayzk

@ayzk ayzk commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

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_sz3 and
H5Z_sz3_set_local are called from C and threw back through HDF5's frames. Reading an ordinary
2023-era SZ3 file, or 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.

before: terminate called after throwing 'std::out_of_range' ... Aborted (core dumped)   rc=134
after:  OSError: Can't synchronously read data (SZ3 Config::load: dimensions
        inconsistent with the element count)                                            rc=0

H5Z_SZ_PUSH_AND_GOTO never reported anything. It 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 — including the call sites that were already there. blosc and H5Z-ZFP both push
with the built-in H5E_ERR_CLS and register nothing; this now does the same and the variable is
gone. The macro is variadic like theirs, because H5Epush takes a format string and e.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 MD
algorithm, and InterpolationDirection = 3 on 1D — and no wrong answers anywhere. Both aborted;
main catches now.

before: rc=-6  terminate called after throwing 'std::invalid_argument'
after:  rc=1   sz3: SZBioMDXtcDecomposition only support 1D, 2D or 3D data

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 declares instead of killing the process.

The regions were indexed by omp_get_thread_num() while assuming the runtime supplies exactly
the requested threads. Under OMP_THREAD_LIMIT it supplies fewer, and the chunks belonging to the
absent 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_LIMIT 8, 2 and 1.

The OpenMP split index was an int. tid * conf.dims[0] overflows above INT_MAX:

OMP_NUM_THREADS=8  2.4e9 elements -> ok        7 * 2.4e9 / 8 = 2.10e9 < INT_MAX
OMP_NUM_THREADS=8  2.5e9 elements -> SIGSEGV   7 * 2.5e9 / 8 = 2.19e9 > INT_MAX

8.6 GB of float32, inside the range of a trajectory. size_t now, in both splits.

SZ3 changed the caller's thread count and talked on its stdout. omp_set_num_threads mutates
the 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_threads instead, and the two per-operation printf calls are gone.

Decompression ignored the stream's own framing. SZ_decompress_impl forced conf.openmp to
false 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=ON and BUILD_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

Copilot AI lite review requested due to automatic review settings September 17, 2026 05:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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 sz3 CLI 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 with size_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 thread include/SZ3/api/impl/SZImplOMP.hpp Outdated
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 thread tools/H5Z-SZ3/src/H5Z_SZ3.cpp Outdated
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>
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.

2 participants