Repository navigation
Conversation
convert(image_u8_t, image) returns a struct pointing into a temporary row-major copy that nothing roots, so a garbage collection on another thread during apriltag_detector_detect can free the image under the detector: tags go missing, or the process segfaults. Detection now goes through with_image_u8, which holds the copy under GC.@preserve for the whole call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
convert(image_u8_t, image)builds a temporary row-major copy of the image (image'[:]) and returns animage_u8_tholding only a raw pointer into it. Nothing roots that copy. If a garbage collection runs on another thread whileapriltag_detector_detectis reading it, the copy is freed under the detector. Tags then go missing on good frames, or the process segfaults when the memory was returned to the OS.Detection now goes through a small helper that holds the copy for the whole call:
convert(::Type{image_u8_t}, …)is unchanged, for compatibility, with a comment that its pointer can dangle.CI now runs the tests with
JULIA_NUM_THREADS: 4. On one thread nothing can collect during the C call, so the new test would pass whatever the code did.Evidence
Found in Fromage.jl, which detects tags in drone footage with several videos processed in parallel. The detections run under a global lock, but a lock cannot stop the GC. 16 runs lost their first registrations this way.
--threads=8): 30 detections of a synthetic 3-tag scene while 4 tasks allocate and callGC.gc(false).signal 11), at--check-bounds=yesandautoalike.JULIA_NUM_THREADS=8,Pkg.test()on Julia 1.13.1): 300 detections while 8 tasks churn.68 passed.Merge Danger
Door: two-way.
Blast Radius: detection
Every detection now makes one copy of the image, as before, and keeps it until the C call returns. Results are unchanged wherever the race did not strike.
with_image_u8accepts anyAbstractMatrixofUInt8,N0f8orGray{N0f8}; the detector methods' signatures are unchanged.🤖 Generated with Claude Code