Skip to content

Automatic crash processing: initialise the Cycles cache path once, safely - #7

Open
andylebihan wants to merge 1 commit into
rhino8_cycles35from
andy/8.x/RH-84838-path-cache-race
Open

andylebihan wants to merge 1 commit into
rhino8_cycles35from
andy/8.x/RH-84838-path-cache-race

Conversation

@andylebihan

Copy link
Copy Markdown
Member

Fixes RH-84838 — 57 reports, all macOS, Rhino 8.12 through 8.19, all on a Metal shader-compile thread.

What happens

path_cache_get lazily fills in a file-static string and then reads it:

if (cached_xdg_cache_path == "") {
  cached_xdg_cache_path = path_xdg_cache_get();
}
string result = path_join(cached_xdg_cache_path, "cycles");

Nothing synchronises that. ShaderCache starts max_mtlcompiler_threads (2) compile threads, both run MetalKernelPipeline::compile(), and both reach path_cache_get through the use_binary_archive block in kernel.mm. So two threads can find the string empty at the same time and both assign it — string::operator= frees the existing buffer and installs a new one — while the other is reading it into path_join.

path_join takes the global by const reference and copies from it. If the buffer it is reading was freed and replaced by the other thread, the copy is left pointing at freed memory, and the append that follows reallocates and frees that stale pointer. That is the reported stack, exactly:

operator delete                          <-- find_zone_and_free -> malloc_report -> abort
std::string::__grow_by_and_replace
std::string::append
ccl::path_join
ccl::path_cache_get
ccl::MetalKernelPipeline::compile
ccl::ShaderCache::compile_thread_func

RhCore::operator delete in that stack is a red herring — on macOS opennurbs' global operator delete is a plain free. The abort is the allocator refusing a pointer it did not hand out, which is what a freed-and-replaced string buffer looks like.

This also explains why it could not be reproduced by hand: it needs two kernels to compile at once with the cache path still unset, which is a narrow window on the first raytrace of a session.

The fix

A function-local static const string, initialised once. Thread-safe initialisation of function-local statics is guaranteed by the language, and the string is never written again afterwards, so concurrent readers are safe.

Notes

  • path_get and path_user_get have the same lazy-assignment shape on cached_path / cached_user_path. They are left alone: path_init() normally sets both at startup before any worker thread exists, so their lazy branch is a fallback rather than the common path. Worth tidying, but not on this change.
  • Not built or run here, and the race itself is not directly testable — it reproduces on about one session in many thousands.

🤖 Generated with Claude Code

path_cache_get lazily assigned a file-static string and then read it, with no
synchronisation. The Metal shader cache runs two compile threads that both reach
it, so one thread could be copying the string while the other replaced it, and
the copy was left holding a freed buffer. Freeing that buffer again in path_join
aborted the process.

A function-local static is initialised once, under the guarantee the language
already provides, and is then never written again.

Fixes RH-84838

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@deranen deranen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good to me.

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