Skip to content

Auditor: reduce task fan-out and pipe leaks in the parallel audit - #1451

Closed
vchuravy wants to merge 1 commit into
masterfrom
vc/auditor-threading
Closed

vchuravy wants to merge 1 commit into
masterfrom
vc/auditor-threading

Conversation

@vchuravy

@vchuravy vchuravy commented Sep 9, 2026

Copy link
Copy Markdown
Member

Yggdrasil runs the auditor with --threads 16 on Julia 1.12 and has seen audits stall indefinitely (e.g. during the Clang build, last output being the Skipping binary analysis of tools/... lines), something never observed on Julia 1.7. I could not reproduce a deadlock locally (seven runs of the real audit on 118 binaries with 16 threads on 1.12.7 all completed), but reviewing the threaded paths turned up three concrete problems, fixed here.

Serial per-library loop in check_dynamic_linkage

Every iteration of that loop modifies the same binary and serialises on its patchelf lock, so the nested Threads.@threads gained nothing. On Julia 1.7 nested @threads loops ran serially anyway; since Julia 1.9 they spawn nthreads() tasks per outer task, so on CI up to 256 tasks contended on locks and subprocess pipes, each pinned to its thread once it spawned a process with an IOBuffer stdout (that goes through @async). This is the only behavioural difference between 1.7 and 1.12 on this code path, so this change restores the 1.7 schedule.

dlopen probe no longer leaks a pipe

open(cmd) followed by wait(p) left an unread stdout pipe open per shared library. That leaks a file descriptor each time (the reason Yggdrasil's build.sh raises ulimit -n), and a child writing more than the pipe buffer to stdout would block forever. The probe now runs with stdin/stdout set to devnull and checks success; stderr is still inherited for verbose error display.

Warm the compiler shard cache before spawning tasks

BinaryBuilderBase.all_compiler_shards() lazily assigns an empty vector and then pushes into it, which is not thread-safe. With a cold cache, 13 of 16 audit tasks failed with Impossible compiler constraints ... upon GCCBuild[] when check_isa constructed runners concurrently. In autobuild the build warms the cache first, so this only bites standalone audit calls, but it is cheap to guard. The proper fix belongs in BinaryBuilderBase.

Testing

Verified on Julia 1.12.7: package loads; check_dynamic_linkage with autofix=true runs on real libraries with 8 threads; the new probe succeeds for a child writing 100 kB to stdout (the old code would block there) and reports a child exiting with status 1.

🤖 Generated with Claude Code

Yggdrasil runs the auditor with 16 threads on Julia 1.12 and has seen
audits stall indefinitely, something never observed on Julia 1.7.

* Make the per-library loop in `check_dynamic_linkage` serial.  Every
  iteration modifies the same binary and serialises on its patchelf
  lock, so the nested `Threads.@threads` gained nothing.  On Julia 1.7
  nested `@threads` loops ran serially anyway; since Julia 1.9 they
  spawn `nthreads()` tasks per outer task, so on CI up to 256 tasks
  contended on locks and subprocess pipes.  This is the only
  behavioural difference between 1.7 and 1.12 on this code path.

* Run the `dlopen` probe with stdout discarded.  `open(cmd)` followed
  by `wait` left an unread stdout pipe open per shared library, leaking
  a file descriptor each time (the reason Yggdrasil raises `ulimit -n`)
  and blocking the child forever if it wrote more than the pipe buffer.

* Populate `BinaryBuilderBase.all_compiler_shards()` before spawning
  tasks.  Its lazy initialisation is not thread-safe; with a cold cache,
  13 of 16 audit tasks failed with "Impossible compiler constraints ...
  upon GCCBuild[]" when `check_isa` constructed runners concurrently.

Assisted-by: Claude Code (Fable 5.1)
Comment thread src/Auditor.jl
all_ok = Threads.Atomic{Bool}(true)

# Populate the compiler shard cache before we start spawning tasks: its lazy
# initialisation in BinaryBuilderBase is not thread-safe, and every sandboxed

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Comment thread src/Auditor.jl
p = open(`$(Base.julia_cmd()) -e $dlopen_cmd`)
wait(p)
if p.exitcode != 0
# Discard stdout: `open(cmd)` would leave an unread pipe open per library,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So we'll never know why a library couldn't be dlopened? That's very useful information

@vchuravy vchuravy closed this Sep 11, 2026
@vchuravy
vchuravy deleted the vc/auditor-threading branch September 11, 2026 12:28
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