From e9c5f32d23aa4ea1f67de1e075eeb9976ac76a8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gonz=C3=A1lez?= Date: Thu, 26 Mar 2026 14:58:45 +0100 Subject: [PATCH] Fix IOBuffer data race in stdio_loop / take! Two background tasks (stdout + stderr) wrote to the same IOBuffer concurrently, and the main test loop called take! without any synchronisation. Julia's IOBuffer is not thread-safe, so this caused torn reads of io.size vs io.data under higher I/O load, manifesting as: DimensionMismatch: Attempted to wrap a MemoryRef of length N with an Array of size dims=(M,) Fix: add a ReentrantLock to PTRWorker; both stdio_loop write tasks and the take! call in runtests hold the lock. Co-Authored-By: Claude Sonnet 4.6 --- src/ParallelTestRunner.jl | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/ParallelTestRunner.jl b/src/ParallelTestRunner.jl index 4386fb7..92f8656 100644 --- a/src/ParallelTestRunner.jl +++ b/src/ParallelTestRunner.jl @@ -29,15 +29,17 @@ const ID_COUNTER = Threads.Atomic{Int}(0) struct PTRWorker <: Malt.AbstractWorker w::Malt.Worker io::IOBuffer + io_lock::ReentrantLock id::Int end function PTRWorker(; exename=Base.julia_cmd()[1], exeflags=String[], env=String[]) io = IOBuffer() + io_lock = ReentrantLock() wrkr = Malt.Worker(; exename, exeflags, env, monitor_stdout=false, monitor_stderr=false) - stdio_loop(wrkr, io) + stdio_loop(wrkr, io, io_lock) id = ID_COUNTER[] += 1 - return PTRWorker(wrkr, io, id) + return PTRWorker(wrkr, io, io_lock, id) end worker_id(wrkr::PTRWorker) = wrkr.id @@ -258,11 +260,11 @@ function print_test_crashed(wrkr, test, ctx::TestIOContext) end # Adapted from `Malt._stdio_loop` -function stdio_loop(worker::Malt.Worker, io) +function stdio_loop(worker::Malt.Worker, io, io_lock::ReentrantLock) Threads.@spawn while !eof(worker.stdout) && Malt.isrunning(worker) try bytes = readavailable(worker.stdout) - write(io, bytes) + @lock io_lock write(io, bytes) catch break end @@ -270,7 +272,7 @@ function stdio_loop(worker::Malt.Worker, io) Threads.@spawn while !eof(worker.stderr) && Malt.isrunning(worker) try bytes = readavailable(worker.stderr) - write(io, bytes) + @lock io_lock write(io, bytes) catch break end @@ -1061,7 +1063,7 @@ function runtests(mod::Module, args::ParsedArgs; ex end test_t1 = time() - output = String(take!(wrkr.io)) + output = @lock wrkr.io_lock String(take!(wrkr.io)) push!(results, (test, result, output, test_t0, test_t1)) # act on the results