Skip to content

[BUG] af_device_info: CUDA backend writes 257 bytes into buffers documented as 64 #3712

Description

@phil-opp

The CUDA backend's devprop() writes up to 257 bytes into d_name, but af_device_info's documented contract states a recommended minimum size of 64. Callers that follow the documentation get 64–256 bytes of memory past their buffer overwritten.

The CPU and OpenCL backends honour the documented size; only CUDA does not.

Description

src/backend/cuda/platform.cpp:290-307:

snprintf(d_name, 256, "%s", dev.name);          // up to 256 bytes
snprintf(d_platform, 10, "CUDA");
snprintf(d_toolkit, 64, "v%s", cudaRuntime.c_str());
snprintf(d_compute, 10, "%d.%d", dev.major, dev.minor);

// Sanitize input
for (int i = 0; i < 256; i++) {
    if (d_name[i] == ' ') {                     // reads d_name[0..256]
        if (d_name[i + 1] == 0 || d_name[i + 1] == ' ') {
            d_name[i] = 0;                      // writes d_name[0..255]
        } else {
            d_name[i] = '_';
        }
    }
}

Two separate problems:

  1. snprintf(d_name, 256, ...) may write up to 256 bytes into a buffer the docs say can be 64.
  2. The sanitize loop is unconditional. It does not stop at the NUL terminator, so it scans all 257 bytes (d_name[i + 1] at i == 255 reads index 256) regardless of how short the device name is, and writes 0x00 or '_' at every index where it happens to read a 0x20 byte. So the overflow is not conditional on having a long GPU name — it fires on every call.

The documented contract, docs/details/device.dox:10-16:

\param d_name pointer to a user-allocated char array. Recommended minimum size is 64.
\param d_platform pointer to a user-allocated char array. Recommended minimum size is 10.
\param d_toolkit pointer to a user-allocated char array. Recommended minimum size is 64.
\param d_compute pointer to a user-allocated char array. Recommended minimum size is 10.

The signature takes no length parameters:

AFAPI af_err af_device_info(char* d_name, char* d_platform, char *d_toolkit, char* d_compute);

so the documented minimum is the only size contract available to a caller.

Any caller sizing d_name per the documentation has up to 193 bytes of adjacent memory clobbered. For a stack-allocated buffer that spans the other three output buffers, spilled registers, the /GS cookie and the return address — producing STATUS_ACCESS_VIOLATION (0xC0000005) or STATUS_STACK_BUFFER_OVERRUN (0xC0000409) on Windows.

Because whether it faults depends on which adjacent bytes happen to contain 0x20, the symptom is intermittent, which makes it hard to attribute.

This is a plausible root cause for several long-standing crash reports in the Rust bindings, which allocate exactly the documented 64/10/64/10 and call af_device_info in the first example program:

Reproducible Code and/or Steps

see above

Suggested fix

Bound the writes to the documented sizes:

snprintf(d_name, 64, "%s", dev.name);
...
for (int i = 0; i < 63 && d_name[i] != '\0'; i++) {

Or at least update the documented minimum buffer size to 256.

System Information

code correctness issue, not system-specific

Checklist

  • Using the latest available ArrayFire release
  • GPU drivers are up to date

Disclaimer

Found using Claude Opus 5. I prompted it to look for potential sources for spurious 0xC0000005 errors in the Rust wrapper (sometimes triggered by just enumerating devices). I looked through the mentioned code parts manually and it seems like this finding is correct.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions