May 20, 2026·5 min read·engineeringrubysecuritynetworkingconcurrency

Ruby's DNS timeout cancellation could outlive its shared state

Ruby 4.0.5 fixes a use-after-free in timed name resolution. A delayed DNS response could race with cancellation in the pthread-based resolver and crash an affected process.

A DNS timeout tells a caller that it has waited long enough. It does not necessarily stop the resolver work running in another thread. Ruby's CVE-2026-46727 advisory, published on May 20, 2026, describes a race in the cancellation path connecting those operations. An attacker able to delay DNS responses near the configured timeout can cause an affected Ruby process to dereference freed memory and crash.

Ruby 4.0.5 contains the fix. The affected stable versions are Ruby 4.0.0 through 4.0.4, alongside the development branch before correction. Ruby 3.4 and earlier are explicitly unaffected. This is a runtime update, with a version range narrower than a general warning about Ruby networking.

I appreciate Ruby's approachable syntax, but concurrency is one place where a short call can conceal substantial machinery. A keyword argument expressing a timeout looks like an ordinary option. Implementing it safely requires a decision about the lifetime of work that may still be running after the caller stops waiting. That runtime responsibility remains even when the application code is easy to read.

The caller and resolver have different lifetimes

The advisory identifies rb_getaddrinfo as the affected implementation, reached through Addrinfo.getaddrinfo with timeout: and Socket.tcp with resolv_timeout:. Its scope concerns the pthread-based timeout handler. The problem occurs when cancellation and resolution completion overlap near the timeout boundary. The consequence described is access to freed memory, rather than a normal Ruby exception reporting delayed resolution.

The Ruby 4.0.5 resolver source documents several implementation choices. One calls the system resolver directly. Another calls it without the GVL but without cancellation. The pthread implementation uses a dedicated native thread, with separate work for waiting and handling interruption. The build configuration selects the relevant implementation.

In the pthread path, a shared argument structure carries the hostname and service, resolver hints and result, and state flags. It also contains a lock and condition variable. The allocation copies the textual inputs into the shared storage. This prevents the worker's access to those strings from depending on a caller's temporary argument buffer.

The structure starts with two references. Both the resolving thread and the caller's path participate in its lifetime. Completion records a result under the lock, signals the waiter when appropriate, and releases a reference. The caller likewise reads the shared state and releases its reference under synchronization. Destruction occurs only after the remaining ownership count reaches zero.

That structure explains why timing matters. The caller may conclude that it has timed out while the native resolver is about to finish. Either path can become the last user of the shared state. A correct implementation needs an agreement about who may still read it, including the cancellation fields used to decide what the caller should do next.

Cancellation is an operation on shared state

The corrected implementation's cancellation helper sets a flag and signals the condition variable under the lock. The waiting loop distinguishes completion, cancellation and timeout. Once waiting ends, the caller copies the information it needs into local variables before dropping its reference. The later timeout decision can then use local state without requiring the shared allocation to remain alive.

This is an explanation of the corrected code's lifetime arrangement. The advisory establishes the vulnerable race, while the source shows the synchronization and ownership used by the fixed release. It would overstate the evidence to infer every possible platform-specific failure schedule from the advisory's short description. The useful invariant is that no participant can continue reading shared storage after its final release.

The resolver also handles unsuccessful completion after cancellation. In that case, result storage can need cleanup even though the caller is no longer interested in receiving it. Abandoning a wait does not make the worker's eventual output disappear. The completion path must still account for any native allocation it produced.

Ruby's source separately preserves the resolver's error value and errno information. Native errno is thread-local, so the worker's value has to travel back through the shared structure. The caller restores the relevant error after collecting it. Even the error-reporting path therefore depends on moving data between lifetimes safely.

The workaround changes which operation receives a deadline

The recommended remediation is Ruby 4.0.5 or later. For an installation that cannot upgrade immediately, the advisory says to avoid timeout: on Addrinfo.getaddrinfo and resolv_timeout: on Socket.tcp. This avoids the identified timeout-cancellation path. It is a temporary change to the operation's behavior, not a new general recommendation to leave networking without time limits.

Removing those arguments also removes the deadline supplied through them. A caller that previously expected a bounded resolution wait needs to account for that difference. The advisory does not propose a substitute wrapper or establish that interrupting the same native work through another API would be equivalent. The fixed runtime is the direct remedy for retaining the intended timeout behavior.

Resolution and connection establishment are distinct stages. The socket implementation carries separate resolution and connection timeout values, and also supports an overall open timeout. That separation explains the specific keyword names in the advisory. A connection deadline should not be casually described as proof that DNS resolution has the same deadline.

The source also rejects combining an overall open timeout with the separate resolution or connection timeouts in the shown initialization path. These options express different budgeting arrangements. Changing them to avoid one implementation path can alter how long an operation waits elsewhere, which is why the advisory's exact workaround should remain distinct from an invented timeout recipe.

Delayed DNS becomes an availability problem

The disclosed attacker capability is to delay responses near a specified boundary. That makes the failure different from a resolver rejecting malformed input. Network timing can select a vulnerable interleaving even when the eventual reply is otherwise ordinary. The runtime has to remain memory-safe when the response and timeout happen close together.

The advisory credits cantina-security with discovering the issue and shioimm with the patch. It does not claim arbitrary code execution. The stated process crash is already an availability concern for applications that reach the affected path. Keeping that consequence precise helps distinguish the established problem from stronger outcomes the source has not demonstrated.

My view of Ruby's readability does not depend on pretending the implementation is simple. Here the source shows a native ownership protocol beneath a short networking call. Ruby 4.0.5 repairs the cancellation path so applications can retain their intended resolution deadlines. For a deployment on the affected stable series, updating the interpreter addresses the runtime code that owns that shared resolver state.