AMD ROCm PR #11169: Why a Child Exit Code of 4 Became 1024 on Linux


AMD ROCm PR #11169 fixes a test that confused a process status with an exit code.

A parent HIP test launched a child Catch2 binary with std::system() and accepted only a raw return value of zero:

REQUIRE(std::system(cmd) == 0);

On an MI200-class GPU, the child correctly skipped every test because that architecture did not support the generic target being tested. Catch2 represented an all-skipped run with exit code 4.

Linux did not return the integer 4 directly from std::system(). It returned a wait-status value from which the exit code had to be decoded. In the reported run, the status was 1024, so the parent treated a valid skip as a failed assertion.

The patch extracts the child exit code on Linux, recognizes Catch2’s all-skipped code, and skips the parent test as well.

The GPU test was actually two processes

The failure appeared in tests named:

Unit_test_generic_target_only_in_regular_fatbin
Unit_test_generic_target_only_in_compressed_fatbin

Those parent test cases did not run the GPU kernel themselves.

They launched a separate binary:

hipSquareGenericTargetOnly

or its compressed-fatbin counterpart.

The child binary contained the device capability check and the actual generic-target test. This separation let the suite verify that a binary containing only generic-target code could run on a supported architecture.

The parent’s job was therefore orchestration:

start the child process
        ↓
observe its termination
        ↓
translate the child result into the parent test result

The bug was in the final translation.

The child had a legitimate reason to skip

ROCm’s generic-target support table maps specific GPU architectures to generic code-object targets.

At the merged commit, the table included several GFX9 devices such as:

gfx900
gfx902
gfx904
gfx906
gfx909
gfx90c

and a separate GFX9.4 data-center group containing:

gfx940
gfx941
gfx942
gfx950

It did not contain gfx90a or gfx908.

The helper therefore returned false for an MI200 or MI250 device using gfx90a.

The child test responded through:

HIP_SKIP_TEST("generic target is not supported on this device.");

HIP_SKIP_TEST ultimately invokes Catch2’s SKIP facility. Since the child invocation had no executed test left to pass or fail, Catch2 terminated with its dedicated all-skipped status.

Catch2 defines:

1  unspecified error
2  no tests run
3  unmatched test specification
4  all tests skipped
5  invalid test specification
42 test failure

Exit code 4 was therefore a semantic result, not an assertion failure.

std::system() returned a transport value

The parent assumed that the integer returned by std::system() was the command’s exit code.

That assumption is not portable.

On POSIX systems, system() returns the command interpreter’s termination status in the format used by waitpid(). The caller must inspect that status with macros such as:

WIFEXITED(status)
WEXITSTATUS(status)
WIFSIGNALED(status)
WTERMSIG(status)

WIFEXITED determines whether the process terminated normally. Only then does WEXITSTATUS provide the low-order exit value supplied by the process.

In the reported Linux environment, a normal exit code of 4 appeared in the raw wait status as:

1024

The exact bit layout should not be reimplemented by application arithmetic. The portable operation is to use the wait-status macros.

The original assertion saw:

1024 == 0

and failed before it had interpreted what 1024 represented.

Windows exposes a different contract

The Microsoft C runtime documents system() as returning the value returned by the command interpreter when the command is non-null.

For the relevant normal exit, the Windows parent can compare the returned integer directly with 4.

The merged code makes that platform difference explicit:

int rc = std::system(cmd);

#if HT_WIN
  const int exitCode = rc;
#else
  const int exitCode = WIFEXITED(rc) ? WEXITSTATUS(rc) : -1;
#endif

The project’s HT_WIN and HT_LINUX macros come from a shared platform header. That header accepts Windows or Linux and emits a compile-time error for an unrecognized operating system.

The <sys/wait.h> include is guarded by HT_LINUX, because the POSIX wait macros are not part of the Windows header environment.

This is not cosmetic conditional compilation. The two platforms supply differently encoded values to the same C++ call site.

The parent now propagates one specific semantic state

After normalizing the result, the patch checks:

if (exitCode == 4) {
  HIP_SKIP_TEST("child skipped: generic target unsupported.");
}

The parent does not convert every nonzero result into a skip.

Only Catch2’s all-skipped result receives that treatment.

The rest still reach:

REQUIRE(rc == 0);

That preserves the failure boundary.

A child assertion failure still produces a nonzero std::system() result. A command error still produces a nonzero result. An abnormal POSIX termination yields exitCode = -1 and also fails the remaining assertion.

The PR’s negative validation substituted child programs returning the Catch2 result codes and reported:

child 0  → parent PASS
child 1  → parent FAIL
child 2  → parent FAIL
child 4  → parent SKIP
child 42 → parent FAIL

The patch changes the meaning of exactly one result.

Why the parent must skip instead of pass

The parent could have treated child exit code 4 as success and returned normally.

That would misreport the test.

The child did not verify generic-target execution on the current GPU. It declined to run because the capability was unavailable.

A passing parent would claim that the intended behavior had been exercised. A failing parent would claim that exercised behavior was incorrect.

The accurate state is:

not applicable on this device

Propagating the skip preserves that distinction through both process layers.

This matters in heterogeneous hardware CI. A test that is unsupported on one GPU family should not turn the entire lane red, but it should also not contribute a false pass.

Catch2 and CTest already agree on code 4

Catch2’s CMake discovery helper assigns:

SKIP_RETURN_CODE 4

to discovered tests unless the caller asks for skips to be treated as failures.

That lets CTest classify a Catch2 process returning 4 as:

***Skipped

The parent process therefore needs to return the same semantic state.

Calling HIP_SKIP_TEST inside the parent causes the parent’s Catch2 invocation to become all-skipped. Catch2 exits with 4, and CTest applies the skip property it already understands.

The full reporting chain becomes:

unsupported GPU architecture
        ↓
child test calls SKIP
        ↓
child Catch2 process exits 4
        ↓
parent decodes system() status
        ↓
parent test calls SKIP
        ↓
parent Catch2 process exits 4
        ↓
CTest reports Skipped

The patch does not invent a new test status. It carries the existing one across a process boundary.

The reported before-and-after result is concrete

The author validated the change on real MI250 hardware using gfx90a.

Before the patch, the two parent tests failed with the characteristic raw value:

REQUIRE(std::system(cmd) == 0)

with expansion:
1024 == 0

CTest reported two failed cases.

After the patch, the four relevant regular and compressed-fatbin cases were reported as skipped, and the run completed with no failed tests.

The parent process itself returned 4, allowing CTest to classify the outcome correctly.

This evidence establishes the hardware and CI behavior reported by the PR. It is author-supplied validation rather than a new automated regression file committed with the change.

One diagnostic inconsistency remains

Public review suggested changing the final assertion from:

REQUIRE(rc == 0);

to:

REQUIRE(exitCode == 0);

The merged code retains the raw rc assertion.

That does not cause a genuine failure to pass. Zero remains the successful raw status, while other child outcomes remain nonzero.

It does affect the failure message on Linux. A child Catch2 failure with exit code 42 may be displayed as its encoded wait status rather than as 42.

The patch therefore fixes status classification while leaving failure diagnostics less normalized than they could be.

This is a minor clarity issue, not a blocking correctness defect in the merged behavior.

The automated regression boundary is limited

The repository policy bot noted that a source file changed without an accompanying file recognized as a new unit test.

That warning is understandable but slightly unusual here: the changed production artifact is itself a test harness source file.

The PR description reports four kinds of manual evidence:

reproduction on pristine source
validation after the patch
confirmation of parent exit code 4
negative stubs for other Catch2 exit codes

The merged diff does not add a deterministic unit test for a helper that maps raw platform status to parent outcome.

As a result, the mechanism is simple and directly inspectable, but long-term regression protection depends on the hardware test continuing to run on an architecture that takes the skip path.

A small extracted normalization helper with platform-focused tests could make that contract more explicit. The current patch remains sufficient for the reported failure.

The contract restored by the patch

A process API can return an encoded status that contains an exit code.

Those values occupy different semantic layers.

The safe sequence is:

run the command
        ↓
interpret the platform-specific process status
        ↓
recover the semantic exit code
        ↓
map that code into pass, fail, or skip
        ↓
publish the parent test result

The old code skipped the interpretation step and assigned test meaning to the transport representation.

A wait status is evidence about process termination. It is not itself a portable exit code.

AMD ROCm PR #11169 restores that boundary and turns a false CI failure back into the skip the child originally reported.

Sources

https://github.com/ROCm/rocm-systems/pull/11169

https://github.com/ROCm/rocm-systems/commit/6f29d180d04e92d3c68ec5250cfdb37544bafee8

https://github.com/ROCm/rocm-systems/blob/6f29d180d04e92d3c68ec5250cfdb37544bafee8/projects/hip-tests/catch/unit/compiler/hipSquareGenericTarget.cc

https://github.com/ROCm/rocm-systems/blob/6f29d180d04e92d3c68ec5250cfdb37544bafee8/projects/hip-tests/catch/include/hip_test_common.hh

https://github.com/ROCm/rocm-systems/blob/6f29d180d04e92d3c68ec5250cfdb37544bafee8/projects/hip-tests/catch/include/hip_test_context.hh

https://github.com/ROCm/rocm-systems/blob/6f29d180d04e92d3c68ec5250cfdb37544bafee8/projects/hip-tests/catch/hipTestMain/hip_test_features.cc

https://github.com/catchorg/Catch2/blob/de1330d036168492a04b649a1fa0593faece6f74/src/catch2/catch_session.hpp

https://github.com/catchorg/Catch2/blob/de1330d036168492a04b649a1fa0593faece6f74/extras/Catch.cmake

https://pubs.opengroup.org/onlinepubs/9799919799/functions/system.html

https://pubs.opengroup.org/onlinepubs/9799919799/functions/wait.html

https://learn.microsoft.com/en-us/previous-versions/277bwbdz%28v%3Dvs.140%29

Popular posts from this blog

AMD Is Trying to Turn Kernel Diversity Into One Software Asset

PyTorch #188031 — Why Did Forward Use 64-Bit Indexing While Backward Still Used uint32_t?

NVIDIA CUTLASS #3017 — Why load() and store() Mapped the Same Tile Coordinate to Different Addresses