Skip to content

[BUG] Unified backend: af_get_last_error calls unresolved symbol without null check #3713

Description

@phil-opp

af_get_last_error in the unified backend calls a function pointer obtained from LOAD_SYMBOL() without checking it for null. When no backend library is loaded — or the symbol cannot be resolved — this is an indirect call through a null pointer.

This is the only use of LOAD_SYMBOL() in the unified backend, and it is unguarded.

Description

src/api/unified/error.cpp:38-47:

} else {
    // If false, the error is coming from active backend.
    typedef void (*af_func)(char **, dim_t *);
    void *vfn    = LOAD_SYMBOL();
    af_func func = nullptr;
    memcpy(&func, &vfn, sizeof(void *));
    func(str, len);            // no null check
}

LOAD_SYMBOL() expands to getFunctionPointer(getActiveHandle(), __FUNCTION__), which is GetProcAddress on Windows / dlsym elsewhere. Both return NULL rather than faulting when the handle is NULL or the symbol is absent, so func is NULL and func(str, len) jumps to address 0.

How the else branch is reached

The branch is taken when the unified layer's own error string is empty. That string is thread_local (src/backend/common/err_common.cpp):

std::string &get_global_error_string() noexcept {
    thread_local auto *global_error_string = new std::string("");
    return *global_error_string;
}

and af_get_last_error clears it after a successful read. So there are several ordinary ways to land in the else branch with no backend loaded:

  • Calling af_get_last_error twice in a row — the first read consumes and clears the string, the second finds it empty.
  • Calling it from a different thread than the one that produced the error, since both the string and getActiveHandle() are thread-local.
  • Calling it defensively before any failing call.

Impact

STATUS_ACCESS_VIOLATION (0xC0000005) with faulting address 0x0000000000000000.

This crash occurs inside the error-reporting path. When backend loading fails, the attempt to retrieve the explanatory message is itself what crashes, so the user never sees "ArrayFire couldn't locate any backends." That is very likely why this class of failure has gone undiagnosed for years in the Rust bindings, whose default error handler calls af_get_last_error on every error:

Reproducible Code and/or Steps

see above

System Information

general issue

Checklist

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

Disclaimer

Found by Claude Opus 5 when prompted to look for potential sources for spurious 0xC0000005 errors on Windows.

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